mirror of
https://github.com/umami-software/umami.git
synced 2026-02-19 12:05:41 +01:00
sync umami
This commit is contained in:
commit
cc4b21a070
600 changed files with 10884 additions and 3381 deletions
|
|
@ -21,8 +21,12 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
|
|||
router.push(`/console/${value}`);
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
window['umami'].track({ url: '/page-view', referrer: 'https://www.google.com' });
|
||||
function handleRunScript() {
|
||||
window['umami'].track(props => ({
|
||||
...props,
|
||||
url: '/page-view',
|
||||
referrer: 'https://www.google.com',
|
||||
}));
|
||||
window['umami'].track('track-event-no-data');
|
||||
window['umami'].track('track-event-with-data', {
|
||||
test: 'test-data',
|
||||
|
|
@ -44,7 +48,7 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
|
|||
});
|
||||
}
|
||||
|
||||
function handleIdentifyClick() {
|
||||
function handleRunIdentify() {
|
||||
window['umami'].identify({
|
||||
userId: 123,
|
||||
name: 'brian',
|
||||
|
|
@ -145,10 +149,10 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
|
|||
</div>
|
||||
<div className={styles.group}>
|
||||
<div className={styles.header}>Javascript events</div>
|
||||
<Button id="manual-button" variant="primary" onClick={handleClick}>
|
||||
<Button id="manual-button" variant="primary" onClick={handleRunScript}>
|
||||
Run script
|
||||
</Button>
|
||||
<Button id="manual-button" variant="primary" onClick={handleIdentifyClick}>
|
||||
<Button id="manual-button" variant="primary" onClick={handleRunIdentify}>
|
||||
Run identify
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,5 +17,6 @@
|
|||
grid-row: 2 / 3;
|
||||
min-height: 0;
|
||||
height: calc(100vh - 60px);
|
||||
height: calc(100dvh - 60px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,12 +14,8 @@ export default function ReportsDataTable({
|
|||
}) {
|
||||
const queryResult = useReports({ websiteId, teamId });
|
||||
|
||||
if (queryResult?.result?.data?.length === 0) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
<DataTable queryResult={queryResult}>
|
||||
<DataTable queryResult={queryResult} renderEmpty={() => children}>
|
||||
{({ data }) => <ReportsTable data={data} showDomain={!websiteId} />}
|
||||
</DataTable>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GridColumn, GridTable, Icon, Icons, Text, useBreakpoint } from 'react-basics';
|
||||
import { GridColumn, GridTable, Icon, Icons, Text } from 'react-basics';
|
||||
import LinkButton from 'components/common/LinkButton';
|
||||
import { useMessages, useLogin, useTeamUrl } from 'components/hooks';
|
||||
import { REPORT_TYPES } from 'lib/constants';
|
||||
|
|
@ -7,11 +7,10 @@ import ReportDeleteButton from './ReportDeleteButton';
|
|||
export function ReportsTable({ data = [], showDomain }: { data: any[]; showDomain?: boolean }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useLogin();
|
||||
const breakpoint = useBreakpoint();
|
||||
const { renderTeamUrl } = useTeamUrl();
|
||||
|
||||
return (
|
||||
<GridTable data={data} cardMode={['xs', 'sm', 'md'].includes(breakpoint)}>
|
||||
<GridTable data={data}>
|
||||
<GridColumn name="name" label={formatMessage(labels.name)} />
|
||||
<GridColumn name="description" label={formatMessage(labels.description)} />
|
||||
<GridColumn name="type" label={formatMessage(labels.type)}>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
import { useState, useMemo } from 'react';
|
||||
import {
|
||||
Form,
|
||||
FormRow,
|
||||
Item,
|
||||
Flexbox,
|
||||
Dropdown,
|
||||
Button,
|
||||
SearchField,
|
||||
TextField,
|
||||
Text,
|
||||
Icon,
|
||||
Icons,
|
||||
Menu,
|
||||
Loading,
|
||||
} from 'react-basics';
|
||||
import { useMessages, useFilters, useFormat, useLocale, useWebsiteValues } from 'components/hooks';
|
||||
import { useFilters, useFormat, useLocale, useMessages, useWebsiteValues } from 'components/hooks';
|
||||
import { OPERATORS } from 'lib/constants';
|
||||
import { isEqualsOperator } from 'lib/params';
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
Flexbox,
|
||||
Form,
|
||||
FormRow,
|
||||
Icon,
|
||||
Icons,
|
||||
Item,
|
||||
Loading,
|
||||
Menu,
|
||||
SearchField,
|
||||
Text,
|
||||
TextField,
|
||||
} from 'react-basics';
|
||||
import styles from './FieldFilterEditForm.module.css';
|
||||
|
||||
export interface FieldFilterFormProps {
|
||||
|
|
@ -69,6 +69,16 @@ export default function FieldFilterEditForm({
|
|||
search,
|
||||
});
|
||||
|
||||
const filterDropdownItems = (name: string) => {
|
||||
const limitedFilters = ['country', 'region', 'city'];
|
||||
|
||||
if (limitedFilters.includes(name)) {
|
||||
return filters.filter(f => f.type === type && !f.label.match(/contain/gi));
|
||||
} else {
|
||||
return filters.filter(f => f.type === type);
|
||||
}
|
||||
};
|
||||
|
||||
const formattedValues = useMemo(() => {
|
||||
if (!values) {
|
||||
return {};
|
||||
|
|
@ -142,7 +152,7 @@ export default function FieldFilterEditForm({
|
|||
{allowFilterSelect && (
|
||||
<Dropdown
|
||||
className={styles.dropdown}
|
||||
items={filters.filter(f => f.type === type)}
|
||||
items={filterDropdownItems(name)}
|
||||
value={operator}
|
||||
renderValue={renderFilterValue}
|
||||
onChange={handleOperatorChange}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,12 @@ export function ReportTemplates({ showHeader = true }: { showHeader?: boolean })
|
|||
url: renderTeamUrl('/reports/journey'),
|
||||
icon: <Path />,
|
||||
},
|
||||
// {
|
||||
// title: formatMessage(labels.revenue),
|
||||
// description: formatMessage(labels.revenueDescription),
|
||||
// url: renderTeamUrl('/reports/revenue'),
|
||||
// icon: <Money />,
|
||||
// },
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export function EventDataParameters() {
|
|||
groups,
|
||||
};
|
||||
|
||||
const handleSubmit = values => {
|
||||
const handleSubmit = (values: any) => {
|
||||
runReport(values);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@
|
|||
background-color: var(--base100);
|
||||
}
|
||||
|
||||
.step:last-child::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export function RetentionTable({ days = DAYS }) {
|
|||
{rows.map(({ date, visitors, records }, rowIndex) => {
|
||||
return (
|
||||
<div key={rowIndex} className={styles.row}>
|
||||
<div className={styles.date}>{formatDate(`${date} 00:00:00`, 'PP', locale)}</div>
|
||||
<div className={styles.date}>{formatDate(date, 'PP', locale)}</div>
|
||||
<div className={styles.visitors}>{visitors}</div>
|
||||
{days.map(day => {
|
||||
if (totalDays - rowIndex < day) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import Report from '../[reportId]/Report';
|
|||
import ReportHeader from '../[reportId]/ReportHeader';
|
||||
import ReportMenu from '../[reportId]/ReportMenu';
|
||||
import ReportBody from '../[reportId]/ReportBody';
|
||||
import Target from 'assets/target.svg';
|
||||
import Money from 'assets/money.svg';
|
||||
import { REPORT_TYPES } from 'lib/constants';
|
||||
|
||||
const defaultParameters = {
|
||||
|
|
@ -15,12 +15,12 @@ const defaultParameters = {
|
|||
export default function RevenueReport({ reportId }: { reportId?: string }) {
|
||||
return (
|
||||
<Report reportId={reportId} defaultParameters={defaultParameters}>
|
||||
<ReportHeader icon={<Target />} />
|
||||
<ReportHeader icon={<Money />} />
|
||||
<ReportMenu>
|
||||
<RevenueParameters />
|
||||
</ReportMenu>
|
||||
<ReportBody>
|
||||
<RevenueChart />
|
||||
<RevenueChart unit="day" />
|
||||
</ReportBody>
|
||||
</Report>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -15,12 +15,8 @@ export function TeamsDataTable({
|
|||
const { user } = useLogin();
|
||||
const queryResult = useTeams(user.id);
|
||||
|
||||
if (queryResult?.result?.data?.length === 0) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
<DataTable queryResult={queryResult}>
|
||||
<DataTable queryResult={queryResult} renderEmpty={() => children}>
|
||||
{({ data }) => {
|
||||
return <TeamsTable data={data} allowEdit={allowEdit} showActions={showActions} />;
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GridColumn, GridTable, Icon, Text, useBreakpoint } from 'react-basics';
|
||||
import { GridColumn, GridTable, Icon, Text } from 'react-basics';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import Icons from 'components/icons';
|
||||
import { ROLES } from 'lib/constants';
|
||||
|
|
@ -13,10 +13,9 @@ export function TeamsTable({
|
|||
showActions?: boolean;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const breakpoint = useBreakpoint();
|
||||
|
||||
return (
|
||||
<GridTable data={data} cardMode={['xs', 'sm', 'md'].includes(breakpoint)}>
|
||||
<GridTable data={data}>
|
||||
<GridColumn name="name" label={formatMessage(labels.name)} />
|
||||
<GridColumn name="owner" label={formatMessage(labels.owner)}>
|
||||
{row => row.teamUser.find(({ role }) => role === ROLES.teamOwner)?.user?.username}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,8 @@ export function UsersDataTable({
|
|||
}) {
|
||||
const queryResult = useUsers();
|
||||
|
||||
if (queryResult?.result?.data?.length === 0) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
<DataTable queryResult={queryResult}>
|
||||
<DataTable queryResult={queryResult} renderEmpty={() => children}>
|
||||
{({ data }) => <UsersTable data={data} showActions={showActions} />}
|
||||
</DataTable>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Text, Icon, Icons, GridTable, GridColumn, useBreakpoint } from 'react-basics';
|
||||
import { Text, Icon, Icons, GridTable, GridColumn } from 'react-basics';
|
||||
import { formatDistance } from 'date-fns';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import { useMessages, useLocale } from 'components/hooks';
|
||||
|
|
@ -14,10 +14,9 @@ export function UsersTable({
|
|||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { dateLocale } = useLocale();
|
||||
const breakpoint = useBreakpoint();
|
||||
|
||||
return (
|
||||
<GridTable data={data} cardMode={['xs', 'sm', 'md'].includes(breakpoint)}>
|
||||
<GridTable data={data}>
|
||||
<GridColumn name="username" label={formatMessage(labels.username)} style={{ minWidth: 0 }} />
|
||||
<GridColumn name="role" label={formatMessage(labels.role)} width={'120px'}>
|
||||
{row =>
|
||||
|
|
|
|||
|
|
@ -18,12 +18,8 @@ export function WebsitesDataTable({
|
|||
}) {
|
||||
const queryResult = useWebsites({ teamId });
|
||||
|
||||
if (queryResult?.result?.data?.length === 0) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
<DataTable queryResult={queryResult}>
|
||||
<DataTable queryResult={queryResult} renderEmpty={() => children}>
|
||||
{({ data }) => (
|
||||
<WebsitesTable
|
||||
teamId={teamId}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { ReactNode } from 'react';
|
||||
import { Text, Icon, Icons, GridTable, GridColumn, useBreakpoint } from 'react-basics';
|
||||
import { Text, Icon, Icons, GridTable, GridColumn } from 'react-basics';
|
||||
import { useMessages, useTeamUrl } from 'components/hooks';
|
||||
import LinkButton from 'components/common/LinkButton';
|
||||
|
||||
|
|
@ -20,7 +20,6 @@ export function WebsitesTable({
|
|||
children,
|
||||
}: WebsitesTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const breakpoint = useBreakpoint();
|
||||
const { renderTeamUrl } = useTeamUrl();
|
||||
|
||||
if (!data?.length) {
|
||||
|
|
@ -28,7 +27,7 @@ export function WebsitesTable({
|
|||
}
|
||||
|
||||
return (
|
||||
<GridTable data={data} cardMode={['xs', 'sm', 'md'].includes(breakpoint)}>
|
||||
<GridTable data={data}>
|
||||
<GridColumn name="name" label={formatMessage(labels.name)} />
|
||||
<GridColumn name="domain" label={formatMessage(labels.domain)} />
|
||||
{showActions && (
|
||||
|
|
|
|||
|
|
@ -13,11 +13,18 @@ export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?:
|
|||
const { teamId, renderTeamUrl } = useTeamUrl();
|
||||
const router = useRouter();
|
||||
const { result } = useTeams(user.id);
|
||||
const hasTeams = result?.data?.length > 0;
|
||||
const isTeamOwner =
|
||||
(!teamId && hasTeams) ||
|
||||
(hasTeams &&
|
||||
result?.data
|
||||
const canTransferWebsite =
|
||||
(
|
||||
!teamId &&
|
||||
result.data.filter(({ teamUser }) =>
|
||||
teamUser.find(
|
||||
({ role, userId }) =>
|
||||
[ROLES.teamOwner, ROLES.teamManager].includes(role) && userId === user.id,
|
||||
),
|
||||
)
|
||||
).length > 0 ||
|
||||
(teamId &&
|
||||
!!result?.data
|
||||
?.find(({ id }) => id === teamId)
|
||||
?.teamUser.find(({ role, userId }) => role === ROLES.teamOwner && userId === user.id));
|
||||
|
||||
|
|
@ -37,8 +44,8 @@ export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?:
|
|||
label={formatMessage(labels.transferWebsite)}
|
||||
description={formatMessage(messages.transferWebsite)}
|
||||
>
|
||||
<ModalTrigger disabled={!isTeamOwner}>
|
||||
<Button variant="secondary" disabled={!isTeamOwner}>
|
||||
<ModalTrigger disabled={!canTransferWebsite}>
|
||||
<Button variant="secondary" disabled={!canTransferWebsite}>
|
||||
{formatMessage(labels.transfer)}
|
||||
</Button>
|
||||
<Modal title={formatMessage(labels.transferWebsite)}>
|
||||
|
|
|
|||
|
|
@ -71,7 +71,8 @@ export function WebsiteTransferForm({
|
|||
{result.data
|
||||
.filter(({ teamUser }) =>
|
||||
teamUser.find(
|
||||
({ role, userId }) => role === ROLES.teamOwner && userId === user.id,
|
||||
({ role, userId }) =>
|
||||
[ROLES.teamOwner, ROLES.teamManager].includes(role) && userId === user.id,
|
||||
),
|
||||
)
|
||||
.map(({ id, name }) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GridColumn, GridTable, useBreakpoint } from 'react-basics';
|
||||
import { GridColumn, GridTable } from 'react-basics';
|
||||
import { useMessages, useLogin } from 'components/hooks';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import TeamMemberRemoveButton from './TeamMemberRemoveButton';
|
||||
|
|
@ -15,7 +15,6 @@ export function TeamMembersTable({
|
|||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useLogin();
|
||||
const breakpoint = useBreakpoint();
|
||||
|
||||
const roles = {
|
||||
[ROLES.teamOwner]: formatMessage(labels.teamOwner),
|
||||
|
|
@ -25,7 +24,7 @@ export function TeamMembersTable({
|
|||
};
|
||||
|
||||
return (
|
||||
<GridTable data={data} cardMode={['xs', 'sm', 'md'].includes(breakpoint)}>
|
||||
<GridTable data={data}>
|
||||
<GridColumn name="username" label={formatMessage(labels.username)}>
|
||||
{row => row?.user?.username}
|
||||
</GridColumn>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GridColumn, GridTable, Icon, Text, useBreakpoint } from 'react-basics';
|
||||
import { GridColumn, GridTable, Icon, Text } from 'react-basics';
|
||||
import { useLogin, useMessages } from 'components/hooks';
|
||||
import Icons from 'components/icons';
|
||||
import LinkButton from 'components/common/LinkButton';
|
||||
|
|
@ -14,10 +14,9 @@ export function TeamWebsitesTable({
|
|||
}) {
|
||||
const { user } = useLogin();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const breakpoint = useBreakpoint();
|
||||
|
||||
return (
|
||||
<GridTable data={data} cardMode={['xs', 'sm', 'md'].includes(breakpoint)}>
|
||||
<GridTable data={data}>
|
||||
<GridColumn name="name" label={formatMessage(labels.name)} />
|
||||
<GridColumn name="domain" label={formatMessage(labels.domain)} />
|
||||
<GridColumn name="createdBy" label={formatMessage(labels.createdBy)}>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { useMemo } from 'react';
|
||||
import PageviewsChart from 'components/metrics/PageviewsChart';
|
||||
import { getDateArray } from 'lib/date';
|
||||
import useWebsitePageviews from 'components/hooks/queries/useWebsitePageviews';
|
||||
import { useDateRange } from 'components/hooks';
|
||||
|
||||
|
|
@ -19,8 +18,8 @@ export function WebsiteChart({
|
|||
const chartData = useMemo(() => {
|
||||
if (data) {
|
||||
const result = {
|
||||
pageviews: getDateArray(pageviews, startDate, endDate, unit),
|
||||
sessions: getDateArray(sessions, startDate, endDate, unit),
|
||||
pageviews,
|
||||
sessions,
|
||||
};
|
||||
|
||||
if (compare) {
|
||||
|
|
@ -43,7 +42,15 @@ export function WebsiteChart({
|
|||
return { pageviews: [], sessions: [] };
|
||||
}, [data, startDate, endDate, unit]);
|
||||
|
||||
return <PageviewsChart data={chartData} unit={unit} isLoading={isLoading} />;
|
||||
return (
|
||||
<PageviewsChart
|
||||
data={chartData}
|
||||
minDate={startDate.toISOString()}
|
||||
maxDate={endDate.toISOString()}
|
||||
unit={unit}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default WebsiteChart;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import WebsiteExpandedView from './WebsiteExpandedView';
|
|||
import WebsiteHeader from './WebsiteHeader';
|
||||
import WebsiteMetricsBar from './WebsiteMetricsBar';
|
||||
import WebsiteTableView from './WebsiteTableView';
|
||||
import WebsiteProvider from './WebsiteProvider';
|
||||
import { FILTER_COLUMNS } from 'lib/constants';
|
||||
|
||||
export default function WebsiteDetailsPage({ websiteId }: { websiteId: string }) {
|
||||
|
|
@ -25,13 +24,13 @@ export default function WebsiteDetailsPage({ websiteId }: { websiteId: string })
|
|||
}, {});
|
||||
|
||||
return (
|
||||
<WebsiteProvider websiteId={websiteId}>
|
||||
<>
|
||||
<WebsiteHeader websiteId={websiteId} showLinks={showLinks} />
|
||||
<FilterTags websiteId={websiteId} params={params} />
|
||||
<WebsiteMetricsBar websiteId={websiteId} showFilter={true} showChange={true} sticky={true} />
|
||||
<WebsiteChart websiteId={websiteId} />
|
||||
{!view && <WebsiteTableView websiteId={websiteId} />}
|
||||
{view && <WebsiteExpandedView websiteId={websiteId} />}
|
||||
</WebsiteProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
}
|
||||
|
||||
.back {
|
||||
align-self: start;
|
||||
align-self: flex-start;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
.dropdown {
|
||||
display: flex;
|
||||
width: 200px;
|
||||
align-self: end;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.menu {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
.header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
padding: 20px 0px;
|
||||
}
|
||||
|
||||
.title {
|
||||
|
|
@ -12,7 +14,7 @@
|
|||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
height: 100px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
|
|
@ -22,6 +24,7 @@
|
|||
justify-content: flex-end;
|
||||
gap: 30px;
|
||||
min-height: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.selected {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import Link from 'next/link';
|
|||
import { usePathname } from 'next/navigation';
|
||||
import { ReactNode } from 'react';
|
||||
import { Button, Icon, Text } from 'react-basics';
|
||||
import Lightning from 'assets/lightning.svg';
|
||||
import styles from './WebsiteHeader.module.css';
|
||||
|
||||
export function WebsiteHeader({
|
||||
|
|
@ -31,25 +32,30 @@ export function WebsiteHeader({
|
|||
path: '',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.compare),
|
||||
icon: <Icons.Compare />,
|
||||
path: '/compare',
|
||||
label: formatMessage(labels.events),
|
||||
icon: <Lightning />,
|
||||
path: '/events',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.sessions),
|
||||
icon: <Icons.User />,
|
||||
path: '/sessions',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.realtime),
|
||||
icon: <Icons.Clock />,
|
||||
path: '/realtime',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.compare),
|
||||
icon: <Icons.Compare />,
|
||||
path: '/compare',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.reports),
|
||||
icon: <Icons.Reports />,
|
||||
path: '/reports',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.eventData),
|
||||
icon: <Icons.Nodes />,
|
||||
path: '/event-data',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
@ -64,7 +70,7 @@ export function WebsiteHeader({
|
|||
<div className={styles.links}>
|
||||
{links.map(({ label, icon, path }) => {
|
||||
const selected = path
|
||||
? pathname.endsWith(path)
|
||||
? pathname.includes(path)
|
||||
: pathname.match(/^\/websites\/[\w-]+$/);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
'use client';
|
||||
import { createContext, ReactNode, useEffect } from 'react';
|
||||
import { useModified, useWebsite } from 'components/hooks';
|
||||
import { Loading } from 'react-basics';
|
||||
|
|
|
|||
|
|
@ -1,22 +1,23 @@
|
|||
import { useState } from 'react';
|
||||
import { Grid, GridRow } from 'components/layout/Grid';
|
||||
import BrowsersTable from 'components/metrics/BrowsersTable';
|
||||
import CountriesTable from 'components/metrics/CountriesTable';
|
||||
import DevicesTable from 'components/metrics/DevicesTable';
|
||||
import EventsChart from 'components/metrics/EventsChart';
|
||||
import EventsTable from 'components/metrics/EventsTable';
|
||||
import HostsTable from 'components/metrics/HostsTable';
|
||||
import OSTable from 'components/metrics/OSTable';
|
||||
import PagesTable from 'components/metrics/PagesTable';
|
||||
import ReferrersTable from 'components/metrics/ReferrersTable';
|
||||
import BrowsersTable from 'components/metrics/BrowsersTable';
|
||||
import OSTable from 'components/metrics/OSTable';
|
||||
import DevicesTable from 'components/metrics/DevicesTable';
|
||||
import WorldMap from 'components/metrics/WorldMap';
|
||||
import CountriesTable from 'components/metrics/CountriesTable';
|
||||
import EventsTable from 'components/metrics/EventsTable';
|
||||
import EventsChart from 'components/metrics/EventsChart';
|
||||
import HostsTable from 'components/metrics/HostsTable';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
export default function WebsiteTableView({ websiteId }: { websiteId: string }) {
|
||||
const [countryData, setCountryData] = useState();
|
||||
const pathname = usePathname();
|
||||
const tableProps = {
|
||||
websiteId,
|
||||
limit: 10,
|
||||
};
|
||||
const isSharePage = pathname.includes('/share/');
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
|
|
@ -30,16 +31,18 @@ export default function WebsiteTableView({ websiteId }: { websiteId: string }) {
|
|||
<DevicesTable {...tableProps} />
|
||||
</GridRow>
|
||||
<GridRow columns="two-one">
|
||||
<WorldMap data={countryData} />
|
||||
<CountriesTable {...tableProps} onDataLoad={setCountryData} />
|
||||
</GridRow>
|
||||
<GridRow columns="one-two">
|
||||
<EventsTable {...tableProps} />
|
||||
<EventsChart websiteId={websiteId} />
|
||||
<WorldMap websiteId={websiteId} />
|
||||
<CountriesTable {...tableProps} />
|
||||
</GridRow>
|
||||
<GridRow columns="three">
|
||||
<HostsTable {...tableProps} />
|
||||
</GridRow>
|
||||
{isSharePage && (
|
||||
<GridRow columns="one-two">
|
||||
<EventsTable {...tableProps} />
|
||||
<EventsChart websiteId={websiteId} />
|
||||
</GridRow>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { useNavigation } from 'components/hooks';
|
|||
import { FILTER_COLUMNS } from 'lib/constants';
|
||||
import WebsiteChart from '../WebsiteChart';
|
||||
import WebsiteCompareTables from './WebsiteCompareTables';
|
||||
import WebsiteProvider from '../WebsiteProvider';
|
||||
|
||||
export function WebsiteComparePage({ websiteId }) {
|
||||
const { query } = useNavigation();
|
||||
|
|
@ -19,13 +18,13 @@ export function WebsiteComparePage({ websiteId }) {
|
|||
}, {});
|
||||
|
||||
return (
|
||||
<WebsiteProvider websiteId={websiteId}>
|
||||
<>
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<FilterTags websiteId={websiteId} params={params} />
|
||||
<WebsiteMetricsBar websiteId={websiteId} compareMode={true} showFilter={true} />
|
||||
<WebsiteChart websiteId={websiteId} compareMode={true} />
|
||||
<WebsiteCompareTables websiteId={websiteId} />
|
||||
</WebsiteProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
min-height: 90px;
|
||||
margin-bottom: 20px;
|
||||
background: var(--base50);
|
||||
z-index: var(--z-index-above);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.container {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import { useApi, useDateRange, useMessages } from 'components/hooks';
|
||||
import MetricCard from 'components/metrics/MetricCard';
|
||||
import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
|
||||
import MetricsBar from 'components/metrics/MetricsBar';
|
||||
import styles from './EventDataMetricsBar.module.css';
|
||||
|
||||
export function EventDataMetricsBar({ websiteId }: { websiteId: string }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { get, useQuery } = useApi();
|
||||
const { dateRange } = useDateRange(websiteId);
|
||||
const { startDate, endDate } = dateRange;
|
||||
|
||||
const { data, error, isLoading, isFetched } = useQuery({
|
||||
queryKey: ['event-data:stats', { websiteId, startDate, endDate }],
|
||||
queryFn: () =>
|
||||
get(`/event-data/stats`, {
|
||||
websiteId,
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}>
|
||||
<MetricCard label={formatMessage(labels.events)} value={data?.events} />
|
||||
<MetricCard label={formatMessage(labels.fields)} value={data?.fields} />
|
||||
<MetricCard label={formatMessage(labels.totalRecords)} value={data?.records} />
|
||||
</MetricsBar>
|
||||
<div className={styles.actions}>
|
||||
<WebsiteDateFilter websiteId={websiteId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EventDataMetricsBar;
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
'use client';
|
||||
import WebsiteHeader from '../WebsiteHeader';
|
||||
import WebsiteEventData from './WebsiteEventData';
|
||||
|
||||
export default function EventDataPage({ websiteId }) {
|
||||
return (
|
||||
<>
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<WebsiteEventData websiteId={websiteId} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import Link from 'next/link';
|
||||
import { GridTable, GridColumn } from 'react-basics';
|
||||
import { useMessages, useNavigation } from 'components/hooks';
|
||||
import Empty from 'components/common/Empty';
|
||||
import { DATA_TYPES } from 'lib/constants';
|
||||
|
||||
export function EventDataTable({ data = [] }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { renderUrl } = useNavigation();
|
||||
|
||||
if (data.length === 0) {
|
||||
return <Empty />;
|
||||
}
|
||||
|
||||
return (
|
||||
<GridTable data={data}>
|
||||
<GridColumn name="eventName" label={formatMessage(labels.event)}>
|
||||
{row => (
|
||||
<Link href={renderUrl({ event: row.eventName })} shallow={true}>
|
||||
{row.eventName}
|
||||
</Link>
|
||||
)}
|
||||
</GridColumn>
|
||||
<GridColumn name="fieldName" label={formatMessage(labels.field)}>
|
||||
{row => row.fieldName}
|
||||
</GridColumn>
|
||||
<GridColumn name="dataType" label={formatMessage(labels.type)}>
|
||||
{row => DATA_TYPES[row.dataType]}
|
||||
</GridColumn>
|
||||
<GridColumn name="total" label={formatMessage(labels.totalRecords)}>
|
||||
{({ total }) => total.toLocaleString()}
|
||||
</GridColumn>
|
||||
</GridTable>
|
||||
);
|
||||
}
|
||||
|
||||
export default EventDataTable;
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import { GridTable, GridColumn } from 'react-basics';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import Empty from 'components/common/Empty';
|
||||
import { DATA_TYPES } from 'lib/constants';
|
||||
|
||||
export function EventDataValueTable({ data = [], event }: { data: any[]; event: string }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={event} />
|
||||
{data.length <= 0 && <Empty />}
|
||||
{data.length > 0 && (
|
||||
<GridTable data={data}>
|
||||
<GridColumn name="fieldName" label={formatMessage(labels.field)} />
|
||||
<GridColumn name="dataType" label={formatMessage(labels.type)}>
|
||||
{row => DATA_TYPES[row.dataType]}
|
||||
</GridColumn>
|
||||
<GridColumn name="fieldValue" label={formatMessage(labels.value)} />
|
||||
<GridColumn name="total" label={formatMessage(labels.totalRecords)} width="200px">
|
||||
{({ total }) => total.toLocaleString()}
|
||||
</GridColumn>
|
||||
</GridTable>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default EventDataValueTable;
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
.container a {
|
||||
color: var(--font-color100);
|
||||
}
|
||||
|
||||
.container a:hover {
|
||||
color: var(--primary400);
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import { Flexbox, Loading } from 'react-basics';
|
||||
import EventDataTable from './EventDataTable';
|
||||
import EventDataValueTable from './EventDataValueTable';
|
||||
import { EventDataMetricsBar } from './EventDataMetricsBar';
|
||||
import { useDateRange, useApi, useNavigation } from 'components/hooks';
|
||||
import styles from './WebsiteEventData.module.css';
|
||||
|
||||
function useData(websiteId: string, event: string) {
|
||||
const { dateRange } = useDateRange(websiteId);
|
||||
const { startDate, endDate } = dateRange;
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, error, isLoading } = useQuery({
|
||||
queryKey: ['event-data:events', { websiteId, startDate, endDate, event }],
|
||||
queryFn: () =>
|
||||
get('/event-data/events', {
|
||||
websiteId,
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
event,
|
||||
}),
|
||||
enabled: !!(websiteId && startDate && endDate),
|
||||
});
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
export default function WebsiteEventData({ websiteId }) {
|
||||
const {
|
||||
query: { event },
|
||||
} = useNavigation();
|
||||
const { data, isLoading } = useData(websiteId, event);
|
||||
|
||||
return (
|
||||
<Flexbox className={styles.container} direction="column" gap={20}>
|
||||
<EventDataMetricsBar websiteId={websiteId} />
|
||||
{!event && <EventDataTable data={data} />}
|
||||
{isLoading && <Loading position="page" />}
|
||||
{event && data && <EventDataValueTable event={event} data={data} />}
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
|
||||
gap: 60px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.table {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
cursor: pointer;
|
||||
color: var(--primary400);
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.chart {
|
||||
min-height: 620px;
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import { GridColumn, GridTable } from 'react-basics';
|
||||
import { useEventDataProperties, useEventDataValues, useMessages } from 'components/hooks';
|
||||
import { LoadingPanel } from 'components/common/LoadingPanel';
|
||||
import PieChart from 'components/charts/PieChart';
|
||||
import { useState } from 'react';
|
||||
import { CHART_COLORS } from 'lib/constants';
|
||||
import styles from './EventProperties.module.css';
|
||||
|
||||
export function EventProperties({ websiteId }: { websiteId: string }) {
|
||||
const [propertyName, setPropertyName] = useState('');
|
||||
const [eventName, setEventName] = useState('');
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { data, isLoading, isFetched, error } = useEventDataProperties(websiteId);
|
||||
const { data: values } = useEventDataValues(websiteId, eventName, propertyName);
|
||||
const chartData =
|
||||
propertyName && values
|
||||
? {
|
||||
labels: values.map(({ value }) => value),
|
||||
datasets: [
|
||||
{
|
||||
data: values.map(({ total }) => total),
|
||||
backgroundColor: CHART_COLORS,
|
||||
borderWidth: 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
: null;
|
||||
|
||||
const handleRowClick = row => {
|
||||
setEventName(row.eventName);
|
||||
setPropertyName(row.propertyName);
|
||||
};
|
||||
|
||||
return (
|
||||
<LoadingPanel isLoading={isLoading} isFetched={isFetched} data={data} error={error}>
|
||||
<div className={styles.container}>
|
||||
<GridTable data={data} cardMode={false} className={styles.table}>
|
||||
<GridColumn name="eventName" label={formatMessage(labels.name)}>
|
||||
{row => (
|
||||
<div className={styles.link} onClick={() => handleRowClick(row)}>
|
||||
{row.eventName}
|
||||
</div>
|
||||
)}
|
||||
</GridColumn>
|
||||
<GridColumn name="propertyName" label={formatMessage(labels.property)}>
|
||||
{row => (
|
||||
<div className={styles.link} onClick={() => handleRowClick(row)}>
|
||||
{row.propertyName}
|
||||
</div>
|
||||
)}
|
||||
</GridColumn>
|
||||
<GridColumn name="total" label={formatMessage(labels.count)} alignment="end" />
|
||||
</GridTable>
|
||||
{propertyName && (
|
||||
<div className={styles.chart}>
|
||||
<div className={styles.title}>{propertyName}</div>
|
||||
<PieChart key={propertyName} type="doughnut" data={chartData} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</LoadingPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export default EventProperties;
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { useWebsiteEvents } from 'components/hooks';
|
||||
import EventsTable from './EventsTable';
|
||||
import DataTable from 'components/common/DataTable';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export default function EventsDataTable({
|
||||
websiteId,
|
||||
}: {
|
||||
websiteId?: string;
|
||||
teamId?: string;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
const queryResult = useWebsiteEvents(websiteId);
|
||||
|
||||
return (
|
||||
<DataTable queryResult={queryResult} allowSearch={true}>
|
||||
{({ data }) => <EventsTable data={data} />}
|
||||
</DataTable>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import { useMessages } from 'components/hooks';
|
||||
import useWebsiteSessionStats from 'components/hooks/queries/useWebsiteSessionStats';
|
||||
import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
|
||||
import MetricCard from 'components/metrics/MetricCard';
|
||||
import MetricsBar from 'components/metrics/MetricsBar';
|
||||
import { formatLongNumber } from 'lib/format';
|
||||
import { Flexbox } from 'react-basics';
|
||||
|
||||
export function EventsMetricsBar({ websiteId }: { websiteId: string }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { data, isLoading, isFetched, error } = useWebsiteSessionStats(websiteId);
|
||||
|
||||
return (
|
||||
<Flexbox direction="row" justifyContent="space-between" style={{ minHeight: 120 }}>
|
||||
<MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}>
|
||||
<MetricCard
|
||||
value={data?.visitors?.value}
|
||||
label={formatMessage(labels.visitors)}
|
||||
formatValue={formatLongNumber}
|
||||
/>
|
||||
<MetricCard
|
||||
value={data?.visits?.value}
|
||||
label={formatMessage(labels.visits)}
|
||||
formatValue={formatLongNumber}
|
||||
/>
|
||||
<MetricCard
|
||||
value={data?.pageviews?.value}
|
||||
label={formatMessage(labels.views)}
|
||||
formatValue={formatLongNumber}
|
||||
/>
|
||||
<MetricCard
|
||||
value={data?.events?.value}
|
||||
label={formatMessage(labels.events)}
|
||||
formatValue={formatLongNumber}
|
||||
/>
|
||||
</MetricsBar>
|
||||
<WebsiteDateFilter websiteId={websiteId} />
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
||||
export default EventsMetricsBar;
|
||||
44
src/app/(main)/websites/[websiteId]/events/EventsPage.tsx
Normal file
44
src/app/(main)/websites/[websiteId]/events/EventsPage.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
'use client';
|
||||
import WebsiteHeader from '../WebsiteHeader';
|
||||
import EventsDataTable from './EventsDataTable';
|
||||
import EventsMetricsBar from './EventsMetricsBar';
|
||||
import EventsChart from 'components/metrics/EventsChart';
|
||||
import { GridRow } from 'components/layout/Grid';
|
||||
import MetricsTable from 'components/metrics/MetricsTable';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { Item, Tabs } from 'react-basics';
|
||||
import { useState } from 'react';
|
||||
import EventProperties from './EventProperties';
|
||||
|
||||
export default function EventsPage({ websiteId }) {
|
||||
const [tab, setTab] = useState('activity');
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<>
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<EventsMetricsBar websiteId={websiteId} />
|
||||
<GridRow columns="two-one">
|
||||
<EventsChart websiteId={websiteId} />
|
||||
<MetricsTable
|
||||
websiteId={websiteId}
|
||||
type="event"
|
||||
title={formatMessage(labels.events)}
|
||||
metric={formatMessage(labels.actions)}
|
||||
/>
|
||||
</GridRow>
|
||||
<div>
|
||||
<Tabs
|
||||
selectedKey={tab}
|
||||
onSelect={(value: any) => setTab(value)}
|
||||
style={{ marginBottom: 30 }}
|
||||
>
|
||||
<Item key="activity">{formatMessage(labels.activity)}</Item>
|
||||
<Item key="properties">{formatMessage(labels.properties)}</Item>
|
||||
</Tabs>
|
||||
{tab === 'activity' && <EventsDataTable websiteId={websiteId} />}
|
||||
{tab === 'properties' && <EventProperties websiteId={websiteId} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
44
src/app/(main)/websites/[websiteId]/events/EventsTable.tsx
Normal file
44
src/app/(main)/websites/[websiteId]/events/EventsTable.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { GridTable, GridColumn, Icon } from 'react-basics';
|
||||
import { useMessages, useTeamUrl, useTimezone } from 'components/hooks';
|
||||
import Empty from 'components/common/Empty';
|
||||
import Avatar from 'components/common/Avatar';
|
||||
import Link from 'next/link';
|
||||
import Icons from 'components/icons';
|
||||
|
||||
export function EventsTable({ data = [] }) {
|
||||
const { formatTimezoneDate } = useTimezone();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { renderTeamUrl } = useTeamUrl();
|
||||
|
||||
if (data.length === 0) {
|
||||
return <Empty />;
|
||||
}
|
||||
|
||||
return (
|
||||
<GridTable data={data}>
|
||||
<GridColumn name="session" label={formatMessage(labels.session)} width={'100px'}>
|
||||
{row => (
|
||||
<Link href={renderTeamUrl(`/websites/${row.websiteId}/sessions/${row.sessionId}`)}>
|
||||
<Avatar seed={row.sessionId} size={64} />
|
||||
</Link>
|
||||
)}
|
||||
</GridColumn>
|
||||
<GridColumn name="event" label={formatMessage(labels.event)}>
|
||||
{row => {
|
||||
return (
|
||||
<>
|
||||
<Icon>{row.eventName ? <Icons.Bolt /> : <Icons.Eye />}</Icon>
|
||||
{formatMessage(row.eventName ? labels.triggeredEvent : labels.viewedPage)}
|
||||
<strong>{row.eventName || row.urlPath}</strong>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</GridColumn>
|
||||
<GridColumn name="created" label={formatMessage(labels.created)} width={'300px'}>
|
||||
{row => formatTimezoneDate(row.createdAt, 'PPPpp')}
|
||||
</GridColumn>
|
||||
</GridTable>
|
||||
);
|
||||
}
|
||||
|
||||
export default EventsTable;
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { Metadata } from 'next';
|
||||
import EventDataPage from './EventDataPage';
|
||||
import EventsPage from './EventsPage';
|
||||
|
||||
export default async function ({ params: { websiteId } }) {
|
||||
return <EventDataPage websiteId={websiteId} />;
|
||||
return <EventsPage websiteId={websiteId} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
13
src/app/(main)/websites/[websiteId]/layout.tsx
Normal file
13
src/app/(main)/websites/[websiteId]/layout.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Metadata } from 'next';
|
||||
import WebsiteProvider from './WebsiteProvider';
|
||||
|
||||
export default function ({ children, params: { websiteId } }) {
|
||||
return <WebsiteProvider websiteId={websiteId}>{children}</WebsiteProvider>;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
template: '%s | Umami',
|
||||
default: 'Websites | Umami',
|
||||
},
|
||||
};
|
||||
|
|
@ -3,19 +3,17 @@ import ListTable from 'components/metrics/ListTable';
|
|||
import { useLocale, useCountryNames, useMessages } from 'components/hooks';
|
||||
import classNames from 'classnames';
|
||||
import styles from './RealtimeCountries.module.css';
|
||||
import TypeIcon from 'components/common/TypeIcon';
|
||||
|
||||
export function RealtimeCountries({ data }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { locale } = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
const { countryNames } = useCountryNames(locale);
|
||||
|
||||
const renderCountryName = useCallback(
|
||||
({ x: code }) => (
|
||||
<span className={classNames(locale, styles.row)}>
|
||||
<img
|
||||
src={`${process.env.basePath || ''}/images/flags/${code?.toLowerCase() || 'xx'}.png`}
|
||||
alt={code}
|
||||
/>
|
||||
<TypeIcon type="country" value={code?.toLowerCase()} />
|
||||
{countryNames[code]}
|
||||
</span>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
import { useContext, useMemo, useState } from 'react';
|
||||
import { StatusLight, Icon, Text, SearchField } from 'react-basics';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { format } from 'date-fns';
|
||||
import thenby from 'thenby';
|
||||
import { safeDecodeURI } from 'next-basics';
|
||||
import FilterButtons from 'components/common/FilterButtons';
|
||||
import Empty from 'components/common/Empty';
|
||||
import { useLocale, useCountryNames, useMessages } from 'components/hooks';
|
||||
import Icons from 'components/icons';
|
||||
import useFormat from 'components//hooks/useFormat';
|
||||
import Empty from 'components/common/Empty';
|
||||
import FilterButtons from 'components/common/FilterButtons';
|
||||
import { useCountryNames, useLocale, useMessages, useTimezone } from 'components/hooks';
|
||||
import Icons from 'components/icons';
|
||||
import { BROWSERS } from 'lib/constants';
|
||||
import { stringToColor } from 'lib/format';
|
||||
import { RealtimeData } from 'lib/types';
|
||||
import { safeDecodeURI } from 'next-basics';
|
||||
import { useContext, useMemo, useState } from 'react';
|
||||
import { Icon, SearchField, StatusLight, Text } from 'react-basics';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { WebsiteContext } from '../WebsiteProvider';
|
||||
import styles from './RealtimeLog.module.css';
|
||||
|
||||
|
|
@ -32,7 +30,8 @@ export function RealtimeLog({ data }: { data: RealtimeData }) {
|
|||
const { formatMessage, labels, messages, FormattedMessage } = useMessages();
|
||||
const { formatValue } = useFormat();
|
||||
const { locale } = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
const { formatTimezoneDate } = useTimezone();
|
||||
const { countryNames } = useCountryNames(locale);
|
||||
const [filter, setFilter] = useState(TYPE_ALL);
|
||||
|
||||
const buttons = [
|
||||
|
|
@ -54,7 +53,7 @@ export function RealtimeLog({ data }: { data: RealtimeData }) {
|
|||
},
|
||||
];
|
||||
|
||||
const getTime = ({ timestamp }) => format(timestamp * 1000, 'h:mm:ss');
|
||||
const getTime = ({ createdAt, firstAt }) => formatTimezoneDate(firstAt || createdAt, 'h:mm:ss');
|
||||
|
||||
const getColor = ({ id, sessionId }) => stringToColor(sessionId || id);
|
||||
|
||||
|
|
@ -141,12 +140,7 @@ export function RealtimeLog({ data }: { data: RealtimeData }) {
|
|||
return [];
|
||||
}
|
||||
|
||||
const { events, visitors } = data;
|
||||
|
||||
let logs = [
|
||||
...events.map(e => ({ __type: e.eventName ? TYPE_EVENT : TYPE_PAGEVIEW, ...e })),
|
||||
...visitors.map(v => ({ __type: TYPE_SESSION, ...v })),
|
||||
].sort(thenby.firstBy('timestamp', -1));
|
||||
let logs = data.events;
|
||||
|
||||
if (search) {
|
||||
logs = logs.filter(({ eventName, urlPath, browser, os, country, device }) => {
|
||||
|
|
@ -178,7 +172,7 @@ export function RealtimeLog({ data }: { data: RealtimeData }) {
|
|||
<SearchField className={styles.search} value={search} onSearch={setSearch} />
|
||||
<FilterButtons items={buttons} selectedKey={filter} onSelect={setFilter} />
|
||||
</div>
|
||||
<div className={styles.header}>{formatMessage(labels.activityLog)}</div>
|
||||
<div className={styles.header}>{formatMessage(labels.activity)}</div>
|
||||
<div className={styles.body}>
|
||||
{logs?.length === 0 && <Empty />}
|
||||
{logs?.length > 0 && (
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import RealtimeHeader from './RealtimeHeader';
|
|||
import RealtimeUrls from './RealtimeUrls';
|
||||
import RealtimeCountries from './RealtimeCountries';
|
||||
import WebsiteHeader from '../WebsiteHeader';
|
||||
import WebsiteProvider from '../WebsiteProvider';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
|
||||
export function WebsiteRealtimePage({ websiteId }) {
|
||||
|
|
@ -27,7 +26,7 @@ export function WebsiteRealtimePage({ websiteId }) {
|
|||
);
|
||||
|
||||
return (
|
||||
<WebsiteProvider websiteId={websiteId}>
|
||||
<>
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<RealtimeHeader data={data} />
|
||||
<RealtimeChart data={data} unit="minute" />
|
||||
|
|
@ -41,7 +40,7 @@ export function WebsiteRealtimePage({ websiteId }) {
|
|||
<WorldMap data={countries} />
|
||||
</GridRow>
|
||||
</Grid>
|
||||
</WebsiteProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
|
||||
gap: 60px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.table {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
cursor: pointer;
|
||||
color: var(--primary400);
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.chart {
|
||||
min-height: 620px;
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import { GridColumn, GridTable } from 'react-basics';
|
||||
import { useSessionDataProperties, useSessionDataValues, useMessages } from 'components/hooks';
|
||||
import { LoadingPanel } from 'components/common/LoadingPanel';
|
||||
import PieChart from 'components/charts/PieChart';
|
||||
import { useState } from 'react';
|
||||
import { CHART_COLORS } from 'lib/constants';
|
||||
import styles from './SessionProperties.module.css';
|
||||
|
||||
export function SessionProperties({ websiteId }: { websiteId: string }) {
|
||||
const [propertyName, setPropertyName] = useState('');
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { data, isLoading, isFetched, error } = useSessionDataProperties(websiteId);
|
||||
const { data: values } = useSessionDataValues(websiteId, propertyName);
|
||||
const chartData =
|
||||
propertyName && values
|
||||
? {
|
||||
labels: values.map(({ value }) => value),
|
||||
datasets: [
|
||||
{
|
||||
data: values.map(({ total }) => total),
|
||||
backgroundColor: CHART_COLORS,
|
||||
borderWidth: 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
: null;
|
||||
|
||||
return (
|
||||
<LoadingPanel isLoading={isLoading} isFetched={isFetched} data={data} error={error}>
|
||||
<div className={styles.container}>
|
||||
<GridTable data={data} cardMode={false} className={styles.table}>
|
||||
<GridColumn name="propertyName" label={formatMessage(labels.property)}>
|
||||
{row => (
|
||||
<div className={styles.link} onClick={() => setPropertyName(row.propertyName)}>
|
||||
{row.propertyName}
|
||||
</div>
|
||||
)}
|
||||
</GridColumn>
|
||||
<GridColumn name="total" label={formatMessage(labels.count)} alignment="end" />
|
||||
</GridTable>
|
||||
{propertyName && (
|
||||
<div className={styles.chart}>
|
||||
<div className={styles.title}>{propertyName}</div>
|
||||
<PieChart key={propertyName} type="doughnut" data={chartData} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</LoadingPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export default SessionProperties;
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { useWebsiteSessions } from 'components/hooks';
|
||||
import SessionsTable from './SessionsTable';
|
||||
import DataTable from 'components/common/DataTable';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export default function SessionsDataTable({
|
||||
websiteId,
|
||||
children,
|
||||
}: {
|
||||
websiteId?: string;
|
||||
teamId?: string;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
const queryResult = useWebsiteSessions(websiteId);
|
||||
|
||||
return (
|
||||
<DataTable queryResult={queryResult} allowSearch={false} renderEmpty={() => children}>
|
||||
{({ data }) => <SessionsTable data={data} showDomain={!websiteId} />}
|
||||
</DataTable>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import { useMessages } from 'components/hooks';
|
||||
import useWebsiteSessionStats from 'components/hooks/queries/useWebsiteSessionStats';
|
||||
import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
|
||||
import MetricCard from 'components/metrics/MetricCard';
|
||||
import MetricsBar from 'components/metrics/MetricsBar';
|
||||
import { formatLongNumber } from 'lib/format';
|
||||
import { Flexbox } from 'react-basics';
|
||||
|
||||
export function SessionsMetricsBar({ websiteId }: { websiteId: string }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { data, isLoading, isFetched, error } = useWebsiteSessionStats(websiteId);
|
||||
|
||||
return (
|
||||
<Flexbox direction="row" justifyContent="space-between" style={{ minHeight: 120 }}>
|
||||
<MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}>
|
||||
<MetricCard
|
||||
value={data?.visitors?.value}
|
||||
label={formatMessage(labels.visitors)}
|
||||
formatValue={formatLongNumber}
|
||||
/>
|
||||
<MetricCard
|
||||
value={data?.visits?.value}
|
||||
label={formatMessage(labels.visits)}
|
||||
formatValue={formatLongNumber}
|
||||
/>
|
||||
<MetricCard
|
||||
value={data?.pageviews?.value}
|
||||
label={formatMessage(labels.views)}
|
||||
formatValue={formatLongNumber}
|
||||
/>
|
||||
<MetricCard
|
||||
value={data?.countries?.value}
|
||||
label={formatMessage(labels.countries)}
|
||||
formatValue={formatLongNumber}
|
||||
/>
|
||||
</MetricsBar>
|
||||
<WebsiteDateFilter websiteId={websiteId} />
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
||||
export default SessionsMetricsBar;
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
'use client';
|
||||
import WebsiteHeader from '../WebsiteHeader';
|
||||
import SessionsDataTable from './SessionsDataTable';
|
||||
import SessionsMetricsBar from './SessionsMetricsBar';
|
||||
import SessionProperties from './SessionProperties';
|
||||
import WorldMap from 'components/metrics/WorldMap';
|
||||
import { GridRow } from 'components/layout/Grid';
|
||||
import { Item, Tabs } from 'react-basics';
|
||||
import { useState } from 'react';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import SessionsWeekly from './SessionsWeekly';
|
||||
|
||||
export function SessionsPage({ websiteId }) {
|
||||
const [tab, setTab] = useState('activity');
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<>
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<SessionsMetricsBar websiteId={websiteId} />
|
||||
<GridRow columns="two-one">
|
||||
<WorldMap websiteId={websiteId} />
|
||||
<SessionsWeekly websiteId={websiteId} />
|
||||
</GridRow>
|
||||
<Tabs selectedKey={tab} onSelect={(value: any) => setTab(value)} style={{ marginBottom: 30 }}>
|
||||
<Item key="activity">{formatMessage(labels.activity)}</Item>
|
||||
<Item key="properties">{formatMessage(labels.properties)}</Item>
|
||||
</Tabs>
|
||||
{tab === 'activity' && <SessionsDataTable websiteId={websiteId} />}
|
||||
{tab === 'properties' && <SessionProperties websiteId={websiteId} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SessionsPage;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import Link from 'next/link';
|
||||
import { GridColumn, GridTable } from 'react-basics';
|
||||
import { useFormat, useMessages, useTimezone } from 'components/hooks';
|
||||
import Avatar from 'components/common/Avatar';
|
||||
import styles from './SessionsTable.module.css';
|
||||
import TypeIcon from 'components/common/TypeIcon';
|
||||
|
||||
export function SessionsTable({ data = [] }: { data: any[]; showDomain?: boolean }) {
|
||||
const { formatTimezoneDate } = useTimezone();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { formatValue } = useFormat();
|
||||
|
||||
return (
|
||||
<GridTable data={data}>
|
||||
<GridColumn name="id" label={formatMessage(labels.session)} width="100px">
|
||||
{row => (
|
||||
<Link href={`sessions/${row.id}`} className={styles.link}>
|
||||
<Avatar key={row.id} seed={row.id} size={64} />
|
||||
</Link>
|
||||
)}
|
||||
</GridColumn>
|
||||
<GridColumn name="visits" label={formatMessage(labels.visits)} width="100px" />
|
||||
<GridColumn name="views" label={formatMessage(labels.views)} width="100px" />
|
||||
<GridColumn name="country" label={formatMessage(labels.country)}>
|
||||
{row => (
|
||||
<TypeIcon type="country" value={row.country}>
|
||||
{formatValue(row.country, 'country')}
|
||||
</TypeIcon>
|
||||
)}
|
||||
</GridColumn>
|
||||
<GridColumn name="city" label={formatMessage(labels.city)} />
|
||||
<GridColumn name="browser" label={formatMessage(labels.browser)}>
|
||||
{row => (
|
||||
<TypeIcon type="browser" value={row.browser}>
|
||||
{formatValue(row.browser, 'browser')}
|
||||
</TypeIcon>
|
||||
)}
|
||||
</GridColumn>
|
||||
<GridColumn name="os" label={formatMessage(labels.os)}>
|
||||
{row => (
|
||||
<TypeIcon type="os" value={row.os}>
|
||||
{formatValue(row.os, 'os')}
|
||||
</TypeIcon>
|
||||
)}
|
||||
</GridColumn>
|
||||
<GridColumn name="device" label={formatMessage(labels.device)}>
|
||||
{row => (
|
||||
<TypeIcon type="device" value={row.device}>
|
||||
{formatValue(row.device, 'device')}
|
||||
</TypeIcon>
|
||||
)}
|
||||
</GridColumn>
|
||||
<GridColumn name="lastAt" label={formatMessage(labels.lastSeen)}>
|
||||
{row => formatTimezoneDate(row.createdAt, 'PPPpp')}
|
||||
</GridColumn>
|
||||
</GridTable>
|
||||
);
|
||||
}
|
||||
|
||||
export default SessionsTable;
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
.week {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.day {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
gap: 1px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cell {
|
||||
display: flex;
|
||||
background-color: var(--base75);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: auto;
|
||||
border-radius: 100%;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.hour {
|
||||
font-weight: 700;
|
||||
color: var(--font-color300);
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.block {
|
||||
background-color: var(--primary400);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import { format, startOfDay, addHours } from 'date-fns';
|
||||
import { useLocale, useMessages, useWebsiteSessionsWeekly } from 'components/hooks';
|
||||
import { LoadingPanel } from 'components/common/LoadingPanel';
|
||||
import { getDayOfWeekAsDate } from 'lib/date';
|
||||
import styles from './SessionsWeekly.module.css';
|
||||
import classNames from 'classnames';
|
||||
import { TooltipPopup } from 'react-basics';
|
||||
|
||||
export function SessionsWeekly({ websiteId }: { websiteId: string }) {
|
||||
const { data, ...props } = useWebsiteSessionsWeekly(websiteId);
|
||||
const { dateLocale } = useLocale();
|
||||
const { labels, formatMessage } = useMessages();
|
||||
|
||||
const [, max] = data
|
||||
? data.reduce((arr: number[], hours: number[], index: number) => {
|
||||
const min = Math.min(...hours);
|
||||
const max = Math.max(...hours);
|
||||
|
||||
if (index === 0) {
|
||||
return [min, max];
|
||||
}
|
||||
|
||||
if (min < arr[0]) {
|
||||
arr[0] = min;
|
||||
}
|
||||
|
||||
if (max > arr[1]) {
|
||||
arr[1] = max;
|
||||
}
|
||||
|
||||
return arr;
|
||||
}, [])
|
||||
: [];
|
||||
|
||||
return (
|
||||
<LoadingPanel {...(props as any)} data={data}>
|
||||
<div key={data} className={styles.week}>
|
||||
<div className={styles.day}>
|
||||
<div className={styles.header}> </div>
|
||||
{Array(24)
|
||||
.fill(null)
|
||||
.map((_, i) => {
|
||||
const label = format(addHours(startOfDay(new Date()), i), 'haaa');
|
||||
return (
|
||||
<div key={i} className={styles.hour}>
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{data?.map((day: number[], index: number) => {
|
||||
return (
|
||||
<div key={index} className={styles.day}>
|
||||
<div className={styles.header}>
|
||||
{format(getDayOfWeekAsDate(index), 'EEE', { locale: dateLocale })}
|
||||
</div>
|
||||
{day?.map((hour: number) => {
|
||||
const pct = hour / max;
|
||||
return (
|
||||
<div key={hour} className={classNames(styles.cell)}>
|
||||
{hour > 0 && (
|
||||
<TooltipPopup
|
||||
label={`${formatMessage(labels.visitors)}: ${hour}`}
|
||||
position="right"
|
||||
>
|
||||
<div
|
||||
className={styles.block}
|
||||
style={{ opacity: pct, transform: `scale(${pct})` }}
|
||||
/>
|
||||
</TooltipPopup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</LoadingPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export default SessionsWeekly;
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
.timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.time {
|
||||
color: var(--font-color200);
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.value {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import { isSameDay } from 'date-fns';
|
||||
import { Loading, Icon, StatusLight } from 'react-basics';
|
||||
import Icons from 'components/icons';
|
||||
import { useSessionActivity, useTimezone } from 'components/hooks';
|
||||
import styles from './SessionActivity.module.css';
|
||||
|
||||
export function SessionActivity({
|
||||
websiteId,
|
||||
sessionId,
|
||||
startDate,
|
||||
endDate,
|
||||
}: {
|
||||
websiteId: string;
|
||||
sessionId: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
}) {
|
||||
const { formatTimezoneDate } = useTimezone();
|
||||
const { data, isLoading } = useSessionActivity(websiteId, sessionId, startDate, endDate);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading position="page" />;
|
||||
}
|
||||
|
||||
let lastDay = null;
|
||||
|
||||
return (
|
||||
<div className={styles.timeline}>
|
||||
{data.map(({ eventId, createdAt, urlPath, eventName, visitId }) => {
|
||||
const showHeader = !lastDay || !isSameDay(new Date(lastDay), new Date(createdAt));
|
||||
lastDay = createdAt;
|
||||
|
||||
return (
|
||||
<>
|
||||
{showHeader && (
|
||||
<div className={styles.header}>{formatTimezoneDate(createdAt, 'EEEE, PPP')}</div>
|
||||
)}
|
||||
<div key={eventId} className={styles.row}>
|
||||
<div className={styles.time}>
|
||||
<StatusLight color={`#${visitId?.substring(0, 6)}`}>
|
||||
{formatTimezoneDate(createdAt, 'h:mm:ss aaa')}
|
||||
</StatusLight>
|
||||
</div>
|
||||
<Icon>{eventName ? <Icons.Bolt /> : <Icons.Eye />}</Icon>
|
||||
<div className={styles.value}>{eventName || urlPath}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
.data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--font-color300);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.type {
|
||||
font-size: 11px;
|
||||
padding: 0 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--base400);
|
||||
}
|
||||
|
||||
.name {
|
||||
color: var(--font-color200);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.value {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { TextOverflow } from 'react-basics';
|
||||
import { useMessages, useSessionData } from 'components/hooks';
|
||||
import Empty from 'components/common/Empty';
|
||||
import { DATA_TYPES } from 'lib/constants';
|
||||
import styles from './SessionData.module.css';
|
||||
import { LoadingPanel } from 'components/common/LoadingPanel';
|
||||
|
||||
export function SessionData({ websiteId, sessionId }: { websiteId: string; sessionId: string }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { data, ...query } = useSessionData(websiteId, sessionId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.header}>{formatMessage(labels.properties)}</div>
|
||||
<LoadingPanel className={styles.data} {...query} data={data}>
|
||||
{!data?.length && <Empty className={styles.empty} />}
|
||||
{data?.map(({ dataKey, dataType, stringValue }) => {
|
||||
return (
|
||||
<div key={dataKey}>
|
||||
<div className={styles.label}>
|
||||
<div className={styles.name}>
|
||||
<TextOverflow>{dataKey}</TextOverflow>
|
||||
</div>
|
||||
<div className={styles.type}>{DATA_TYPES[dataType]}</div>
|
||||
</div>
|
||||
<div className={styles.value}>{stringValue}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</LoadingPanel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
.page {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr max-content;
|
||||
margin-bottom: 40px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 20px;
|
||||
width: 300px;
|
||||
padding-right: 20px;
|
||||
border-right: 1px solid var(--base300);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
padding: 0 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.data {
|
||||
width: 300px;
|
||||
border-left: 1px solid var(--base300);
|
||||
padding-left: 20px;
|
||||
position: relative;
|
||||
transition: width 200ms ease-in-out;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.page {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.sidebar,
|
||||
.data {
|
||||
border: 0;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
'use client';
|
||||
import Avatar from 'components/common/Avatar';
|
||||
import { LoadingPanel } from 'components/common/LoadingPanel';
|
||||
import { useWebsiteSession } from 'components/hooks';
|
||||
import WebsiteHeader from '../../WebsiteHeader';
|
||||
import { SessionActivity } from './SessionActivity';
|
||||
import { SessionData } from './SessionData';
|
||||
import styles from './SessionDetailsPage.module.css';
|
||||
import SessionInfo from './SessionInfo';
|
||||
import { SessionStats } from './SessionStats';
|
||||
|
||||
export default function SessionDetailsPage({
|
||||
websiteId,
|
||||
sessionId,
|
||||
}: {
|
||||
websiteId: string;
|
||||
sessionId: string;
|
||||
}) {
|
||||
const { data, ...query } = useWebsiteSession(websiteId, sessionId);
|
||||
|
||||
return (
|
||||
<LoadingPanel {...query} loadingIcon="spinner" data={data}>
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<div className={styles.page}>
|
||||
<div className={styles.sidebar}>
|
||||
<Avatar seed={data?.id} />
|
||||
<SessionInfo data={data} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<SessionStats data={data} />
|
||||
<SessionActivity
|
||||
websiteId={websiteId}
|
||||
sessionId={sessionId}
|
||||
startDate={data?.firstAt}
|
||||
endDate={data?.lastAt}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.data}>
|
||||
<SessionData websiteId={websiteId} sessionId={sessionId} />
|
||||
</div>
|
||||
</div>
|
||||
</LoadingPanel>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
.info {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.info dl {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.info dt {
|
||||
color: var(--font-color200);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.info dd {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin: 5px 0 28px;
|
||||
text-align: left;
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import { useFormat, useLocale, useMessages, useRegionNames, useTimezone } from 'components/hooks';
|
||||
import TypeIcon from 'components/common/TypeIcon';
|
||||
import { Icon, CopyIcon } from 'react-basics';
|
||||
import Icons from 'components/icons';
|
||||
import styles from './SessionInfo.module.css';
|
||||
|
||||
export default function SessionInfo({ data }) {
|
||||
const { locale } = useLocale();
|
||||
const { formatTimezoneDate } = useTimezone();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { formatValue } = useFormat();
|
||||
const { getRegionName } = useRegionNames(locale);
|
||||
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
<dl>
|
||||
<dt>ID</dt>
|
||||
<dd>
|
||||
{data?.id} <CopyIcon value={data?.id} />
|
||||
</dd>
|
||||
|
||||
<dt>{formatMessage(labels.lastSeen)}</dt>
|
||||
<dd>{formatTimezoneDate(data?.lastAt, 'EEEE, PPPpp')}</dd>
|
||||
|
||||
<dt>{formatMessage(labels.firstSeen)}</dt>
|
||||
<dd>{formatTimezoneDate(data?.firstAt, 'EEEE, PPPpp')}</dd>
|
||||
|
||||
<dt>{formatMessage(labels.country)}</dt>
|
||||
<dd>
|
||||
<TypeIcon type="country" value={data?.country} />
|
||||
{formatValue(data?.country, 'country')}
|
||||
</dd>
|
||||
|
||||
<dt>{formatMessage(labels.region)}</dt>
|
||||
<dd>
|
||||
<Icon>
|
||||
<Icons.Location />
|
||||
</Icon>
|
||||
{getRegionName(data?.subdivision1)}
|
||||
</dd>
|
||||
|
||||
<dt>{formatMessage(labels.city)}</dt>
|
||||
<dd>
|
||||
<Icon>
|
||||
<Icons.Location />
|
||||
</Icon>
|
||||
{data?.city}
|
||||
</dd>
|
||||
|
||||
<dt>{formatMessage(labels.os)}</dt>
|
||||
<dd>
|
||||
<TypeIcon type="os" value={data?.os?.toLowerCase()?.replaceAll(/\W/g, '-')} />
|
||||
{formatValue(data?.os, 'os')}
|
||||
</dd>
|
||||
|
||||
<dt>{formatMessage(labels.device)}</dt>
|
||||
<dd>
|
||||
<TypeIcon type="device" value={data?.device} />
|
||||
{formatValue(data?.device, 'device')}
|
||||
</dd>
|
||||
|
||||
<dt>{formatMessage(labels.browser)}</dt>
|
||||
<dd>
|
||||
<TypeIcon type="browser" value={data?.browser} />
|
||||
{formatValue(data?.browser, 'browser')}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { useMessages } from 'components/hooks';
|
||||
import MetricCard from 'components/metrics/MetricCard';
|
||||
import MetricsBar from 'components/metrics/MetricsBar';
|
||||
import { formatShortTime } from 'lib/format';
|
||||
|
||||
export function SessionStats({ data }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<MetricsBar isFetched={true}>
|
||||
<MetricCard label={formatMessage(labels.visits)} value={data?.visits} />
|
||||
<MetricCard label={formatMessage(labels.views)} value={data?.views} />
|
||||
<MetricCard label={formatMessage(labels.events)} value={data?.events} />
|
||||
<MetricCard
|
||||
label={formatMessage(labels.visitDuration)}
|
||||
value={data?.totaltime / data?.visits}
|
||||
formatValue={n => `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`}
|
||||
/>
|
||||
</MetricsBar>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import SessionDetailsPage from './SessionDetailsPage';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function WebsitePage({ params: { websiteId, sessionId } }) {
|
||||
return <SessionDetailsPage websiteId={websiteId} sessionId={sessionId} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Websites',
|
||||
};
|
||||
10
src/app/(main)/websites/[websiteId]/sessions/page.tsx
Normal file
10
src/app/(main)/websites/[websiteId]/sessions/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import SessionsPage from './SessionsPage';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function ({ params: { websiteId } }) {
|
||||
return <SessionsPage websiteId={websiteId} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Sessions',
|
||||
};
|
||||
28
src/app/api/scripts/telemetry/route.ts
Normal file
28
src/app/api/scripts/telemetry/route.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { CURRENT_VERSION, TELEMETRY_PIXEL } from 'lib/constants';
|
||||
|
||||
export async function GET() {
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
process.env.DISABLE_TELEMETRY &&
|
||||
process.env.PRIVATE_MODE
|
||||
) {
|
||||
const script = `
|
||||
(()=>{const i=document.createElement('img');
|
||||
i.setAttribute('src','${TELEMETRY_PIXEL}?v=${CURRENT_VERSION}');
|
||||
i.setAttribute('style','width:0;height:0;position:absolute;pointer-events:none;');
|
||||
document.body.appendChild(i);})();
|
||||
`;
|
||||
|
||||
return new Response(script.replace(/\s\s+/g, ''), {
|
||||
headers: {
|
||||
'content-type': 'text/javascript',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return new Response('/* telemetry disabled */', {
|
||||
headers: {
|
||||
'content-type': 'text/javascript',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -21,7 +21,6 @@ export default function ({ children }) {
|
|||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="theme-color" content="#fafafa" media="(prefers-color-scheme: light)" />
|
||||
<meta name="theme-color" content="#2f2f2f" media="(prefers-color-scheme: dark)" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
.container {
|
||||
flex: 1;
|
||||
min-height: calc(100vh - 200px);
|
||||
min-height: calc(100dvh - 200px);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import Page from 'components/layout/Page';
|
|||
import Header from './Header';
|
||||
import Footer from './Footer';
|
||||
import styles from './SharePage.module.css';
|
||||
import { WebsiteProvider } from 'app/(main)/websites/[websiteId]/WebsiteProvider';
|
||||
|
||||
export default function SharePage({ shareId }) {
|
||||
const { shareToken, isLoading } = useShareToken(shareId);
|
||||
|
|
@ -17,7 +18,9 @@ export default function SharePage({ shareId }) {
|
|||
<div className={styles.container}>
|
||||
<Page>
|
||||
<Header />
|
||||
<WebsiteDetailsPage websiteId={shareToken.websiteId} />
|
||||
<WebsiteProvider websiteId={shareToken.websiteId}>
|
||||
<WebsiteDetailsPage websiteId={shareToken.websiteId} />
|
||||
</WebsiteProvider>
|
||||
<Footer />
|
||||
</Page>
|
||||
</div>
|
||||
|
|
|
|||
1
src/assets/lightning.svg
Normal file
1
src/assets/lightning.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xml:space="preserve" viewBox="0 0 682.667 682.667" xmlns="http://www.w3.org/2000/svg"><defs><clipPath clipPathUnits="userSpaceOnUse" id="a"><path d="M0 512h512V0H0Z"/></clipPath></defs><g clip-path="url(#a)" transform="matrix(1.33333 0 0 -1.33333 0 682.667)"><path d="M0 0h137.962L69.319-155.807h140.419L.242-482l55.349 222.794h-155.853z" style="fill:none;stroke:currentColor;stroke-width:30;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" transform="translate(201.262 496.994)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 551 B |
1
src/assets/location.svg
Normal file
1
src/assets/location.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg height="512" viewBox="0 0 64 64" width="512" xmlns="http://www.w3.org/2000/svg"><path d="M32 0A24.032 24.032 0 0 0 8 24c0 17.23 22.36 38.81 23.31 39.72a.99.99 0 0 0 1.38 0C33.64 62.81 56 41.23 56 24A24.032 24.032 0 0 0 32 0zm0 35a11 11 0 1 1 11-11 11.007 11.007 0 0 1-11 11z"/></svg>
|
||||
|
After Width: | Height: | Size: 288 B |
|
|
@ -1,4 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 428 389.11">
|
||||
<circle cx="214.15" cy="181" r="171" fill="none" stroke="white" stroke-miterlimit="10" stroke-width="20"/>
|
||||
<path d="M413 134.11H15.29a15 15 0 0 0-15 15v15.3C.12 168 0 171.52 0 175.11c0 118.19 95.81 214 214 214 116.4 0 211.1-92.94 213.93-208.67 0-.44.07-.88.07-1.33v-30a15 15 0 0 0-15-15Z" fill="white"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 428 389.11"><circle cx="214.15" cy="181" r="171" fill="none" stroke="#fff" stroke-miterlimit="10" stroke-width="20"/><path d="M413 134.11H15.29a15 15 0 0 0-15 15v15.3C.12 168 0 171.52 0 175.11c0 118.19 95.81 214 214 214 116.4 0 211.1-92.94 213.93-208.67 0-.44.07-.88.07-1.33v-30a15 15 0 0 0-15-15Z" fill="#fff"/></svg>
|
||||
|
Before Width: | Height: | Size: 404 B After Width: | Height: | Size: 394 B |
1
src/assets/money.svg
Normal file
1
src/assets/money.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve"><path d="M347 302c8.271 0 15 6.639 15 14.8h30c0-19.468-12.541-36.067-30-42.231V242h-30v32.58c-17.459 6.192-30 22.865-30 42.42 0 24.813 20.187 45 45 45 8.271 0 15 6.729 15 15s-6.729 15-15 15-15-6.729-15-15h-30c0 19.555 12.541 36.228 30 42.42v32.38h30v-32.38c17.459-6.192 30-22.865 30-42.42 0-24.813-20.187-45-45-45-8.271 0-15-6.729-15-15s6.729-15 15-15z"/><path d="M347 182c-5.057 0-10.058.242-15 .689V90c0-26.011-18.548-49.61-52.226-66.449C249.4 8.364 209.35 0 167 0 124.564 0 84.193 8.347 53.323 23.502 18.938 40.385 0 64 0 90v272c0 26 18.938 49.616 53.323 66.498C84.193 443.653 124.564 452 167 452c17.009 0 33.647-1.358 49.615-4.004C246.826 486.909 294.035 512 347 512c90.981 0 165-74.019 165-165s-74.019-165-165-165zM66.545 50.432C92.992 37.447 129.606 30 167 30c79.558 0 135 31.621 135 60s-55.442 60-135 60c-37.394 0-74.008-7.447-100.455-20.432C43.32 118.166 30 103.744 30 90s13.32-28.166 36.545-39.568zM30 142.265c6.724 5.137 14.512 9.907 23.323 14.233C84.193 171.653 124.564 180 167 180c42.35 0 82.4-8.364 112.774-23.551 8.359-4.18 15.783-8.776 22.226-13.722v45.51c-29.896 8.485-56.359 25.209-76.778 47.548C206.946 239.908 187.386 242 167 242c-37.394 0-74.008-7.447-100.455-20.432C43.32 210.166 30 195.744 30 182v-39.735zm0 92c6.724 5.137 14.512 9.907 23.323 14.233C84.193 263.653 124.564 272 167 272c11.581 0 22.942-.621 34.021-1.839a163.743 163.743 0 0 0-18.293 61.395c-5.211.286-10.465.444-15.728.444-37.394 0-74.008-7.447-100.455-20.432C43.32 300.166 30 285.744 30 272v-37.735zM167 422c-37.394 0-74.008-7.447-100.455-20.432C43.32 390.166 30 375.744 30 362v-37.736c6.724 5.137 14.512 9.907 23.323 14.233C84.193 353.653 124.564 362 167 362c5.23 0 10.459-.132 15.654-.388a163.726 163.726 0 0 0 16.486 58.557A280.559 280.559 0 0 1 167 422zm180 60c-74.439 0-135-60.561-135-135s60.561-135 135-135 135 60.561 135 135-60.561 135-135 135z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -12,6 +12,8 @@ export interface BarChartProps extends ChartProps {
|
|||
renderYLabel?: (label: string, index: number, values: any[]) => string;
|
||||
XAxisType?: string;
|
||||
YAxisType?: string;
|
||||
minDate?: number | string;
|
||||
maxDate?: number | string;
|
||||
}
|
||||
|
||||
export function BarChart(props: BarChartProps) {
|
||||
|
|
@ -24,6 +26,8 @@ export function BarChart(props: BarChartProps) {
|
|||
XAxisType = 'time',
|
||||
YAxisType = 'linear',
|
||||
stacked = false,
|
||||
minDate,
|
||||
maxDate,
|
||||
} = props;
|
||||
|
||||
const options: any = useMemo(() => {
|
||||
|
|
@ -32,6 +36,8 @@ export function BarChart(props: BarChartProps) {
|
|||
x: {
|
||||
type: XAxisType,
|
||||
stacked: true,
|
||||
min: minDate,
|
||||
max: maxDate,
|
||||
time: {
|
||||
unit,
|
||||
},
|
||||
|
|
|
|||
27
src/components/charts/BubbleChart.tsx
Normal file
27
src/components/charts/BubbleChart.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { Chart, ChartProps } from 'components/charts/Chart';
|
||||
import { useState } from 'react';
|
||||
import { StatusLight } from 'react-basics';
|
||||
import { formatLongNumber } from 'lib/format';
|
||||
|
||||
export interface BubbleChartProps extends ChartProps {
|
||||
type?: 'bubble';
|
||||
}
|
||||
|
||||
export default function BubbleChart(props: BubbleChartProps) {
|
||||
const [tooltip, setTooltip] = useState(null);
|
||||
const { type = 'bubble' } = props;
|
||||
|
||||
const handleTooltip = ({ tooltip }) => {
|
||||
const { labelColors, dataPoints } = tooltip;
|
||||
|
||||
setTooltip(
|
||||
tooltip.opacity ? (
|
||||
<StatusLight color={labelColors?.[0]?.backgroundColor}>
|
||||
{formatLongNumber(dataPoints?.[0]?.raw)} {dataPoints?.[0]?.label}
|
||||
</StatusLight>
|
||||
) : null,
|
||||
);
|
||||
};
|
||||
|
||||
return <Chart {...props} type={type} tooltip={tooltip} onTooltip={handleTooltip} />;
|
||||
}
|
||||
|
|
@ -79,18 +79,20 @@ export function Chart({
|
|||
};
|
||||
|
||||
const updateChart = (data: any) => {
|
||||
if (data.datasets.length === chart.current.data.datasets.length) {
|
||||
chart.current.data.datasets.forEach((dataset: { data: any }, index: string | number) => {
|
||||
if (data?.datasets[index]) {
|
||||
dataset.data = data?.datasets[index]?.data;
|
||||
if (data.datasets) {
|
||||
if (data.datasets.length === chart.current.data.datasets.length) {
|
||||
chart.current.data.datasets.forEach((dataset: { data: any }, index: string | number) => {
|
||||
if (data?.datasets[index]) {
|
||||
dataset.data = data?.datasets[index]?.data;
|
||||
|
||||
if (chart.current.legend.legendItems[index]) {
|
||||
chart.current.legend.legendItems[index].text = data?.datasets[index]?.label;
|
||||
if (chart.current.legend.legendItems[index]) {
|
||||
chart.current.legend.legendItems[index].text = data?.datasets[index]?.label;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
chart.current.data.datasets = data.datasets;
|
||||
});
|
||||
} else {
|
||||
chart.current.data.datasets = data.datasets;
|
||||
}
|
||||
}
|
||||
|
||||
chart.current.options = options;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export interface PieChartProps extends ChartProps {
|
|||
|
||||
export default function PieChart(props: PieChartProps) {
|
||||
const [tooltip, setTooltip] = useState(null);
|
||||
const { type } = props;
|
||||
const { type = 'pie' } = props;
|
||||
|
||||
const handleTooltip = ({ tooltip }) => {
|
||||
const { labelColors, dataPoints } = tooltip;
|
||||
|
|
@ -23,5 +23,5 @@ export default function PieChart(props: PieChartProps) {
|
|||
);
|
||||
};
|
||||
|
||||
return <Chart {...props} type={type || 'pie'} tooltip={tooltip} onTooltip={handleTooltip} />;
|
||||
return <Chart {...props} type={type} tooltip={tooltip} onTooltip={handleTooltip} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,71 +1,23 @@
|
|||
import md5 from 'md5';
|
||||
import { colord, extend } from 'colord';
|
||||
import harmoniesPlugin from 'colord/plugins/harmonies';
|
||||
import mixPlugin from 'colord/plugins/mix';
|
||||
import { useMemo } from 'react';
|
||||
import { createAvatar } from '@dicebear/core';
|
||||
import { lorelei } from '@dicebear/collection';
|
||||
import { getColor, getPastel } from 'lib/colors';
|
||||
|
||||
extend([harmoniesPlugin, mixPlugin]);
|
||||
const lib = lorelei;
|
||||
|
||||
const harmonies = [
|
||||
//'analogous',
|
||||
//'complementary',
|
||||
'double-split-complementary',
|
||||
//'rectangle',
|
||||
'split-complementary',
|
||||
'tetradic',
|
||||
//'triadic',
|
||||
];
|
||||
function Avatar({ seed, size = 128, ...props }: { seed: string; size?: number }) {
|
||||
const backgroundColor = getPastel(getColor(seed), 4);
|
||||
|
||||
const color = (value: string, invert: boolean = false) => {
|
||||
const c = colord(value.startsWith('#') ? value : `#${value}`);
|
||||
const avatar = useMemo(() => {
|
||||
return createAvatar(lib, {
|
||||
...props,
|
||||
seed,
|
||||
size,
|
||||
backgroundColor: [backgroundColor],
|
||||
}).toDataUri();
|
||||
}, []);
|
||||
|
||||
if (invert && c.isDark()) {
|
||||
return c.invert();
|
||||
}
|
||||
|
||||
return c;
|
||||
};
|
||||
|
||||
const remix = (hash: string) => {
|
||||
const a = hash.substring(0, 6);
|
||||
const b = hash.substring(6, 12);
|
||||
const c = hash.substring(12, 18);
|
||||
const d = hash.substring(18, 24);
|
||||
const e = hash.substring(24, 30);
|
||||
const f = hash.substring(30, 32);
|
||||
|
||||
const base = [b, c, d, e]
|
||||
.reduce((acc, val) => {
|
||||
return acc.mix(color(val), 0.05);
|
||||
}, color(a))
|
||||
.saturate(0.1)
|
||||
.toHex();
|
||||
|
||||
const harmony = pick(parseInt(f, 16), harmonies);
|
||||
|
||||
return color(base, true)
|
||||
.harmonies(harmony)
|
||||
.map(c => c.toHex());
|
||||
};
|
||||
|
||||
const pick = (num: number, arr: any[]) => {
|
||||
return arr[num % arr.length];
|
||||
};
|
||||
|
||||
export function Avatar({ value }: { value: string }) {
|
||||
const hash = md5(value);
|
||||
const colors = remix(hash);
|
||||
|
||||
return (
|
||||
<svg viewBox="0 0 100 100">
|
||||
<defs>
|
||||
<linearGradient id={`color-${hash}`} gradientTransform="rotate(90)">
|
||||
<stop offset="0%" stopColor={colors[1]} />
|
||||
<stop offset="100%" stopColor={colors[2]} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="50" fill={`url(#color-${hash})`} />
|
||||
</svg>
|
||||
);
|
||||
return <img src={avatar} alt="Avatar" style={{ borderRadius: '100%' }} />;
|
||||
}
|
||||
|
||||
export default Avatar;
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
import { ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Banner, Loading, SearchField } from 'react-basics';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { Loading, SearchField } from 'react-basics';
|
||||
import { useMessages, useNavigation } from 'components/hooks';
|
||||
import Empty from 'components/common/Empty';
|
||||
import Pager from 'components/common/Pager';
|
||||
import { FilterQueryResult } from 'lib/types';
|
||||
import { PagedQueryResult } from 'lib/types';
|
||||
import styles from './DataTable.module.css';
|
||||
import { LoadingPanel } from 'components/common/LoadingPanel';
|
||||
|
||||
const DEFAULT_SEARCH_DELAY = 600;
|
||||
|
||||
export interface DataTableProps {
|
||||
queryResult: FilterQueryResult<any>;
|
||||
queryResult: PagedQueryResult<any>;
|
||||
searchDelay?: number;
|
||||
allowSearch?: boolean;
|
||||
allowPaging?: boolean;
|
||||
renderEmpty?: () => ReactNode;
|
||||
children: ReactNode | ((data: any) => ReactNode);
|
||||
}
|
||||
|
||||
|
|
@ -22,6 +24,7 @@ export function DataTable({
|
|||
searchDelay = 600,
|
||||
allowSearch = true,
|
||||
allowPaging = true,
|
||||
renderEmpty,
|
||||
children,
|
||||
}: DataTableProps) {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
|
|
@ -29,12 +32,13 @@ export function DataTable({
|
|||
result,
|
||||
params,
|
||||
setParams,
|
||||
query: { error, isLoading },
|
||||
query: { error, isLoading, isFetched },
|
||||
} = queryResult || {};
|
||||
const { page, pageSize, count, data } = result || {};
|
||||
const { query } = params || {};
|
||||
const hasData = Boolean(!isLoading && data?.length);
|
||||
const noResults = Boolean(!isLoading && query && !hasData);
|
||||
const noResults = Boolean(query && !hasData);
|
||||
const { router, renderUrl } = useNavigation();
|
||||
|
||||
const handleSearch = (query: string) => {
|
||||
setParams({ ...params, query, page: params.page ? page : 1 });
|
||||
|
|
@ -42,12 +46,9 @@ export function DataTable({
|
|||
|
||||
const handlePageChange = (page: number) => {
|
||||
setParams({ ...params, query, page });
|
||||
router.push(renderUrl({ page }));
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return <Banner variant="error">{formatMessage(messages.error)}</Banner>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{allowSearch && (hasData || query) && (
|
||||
|
|
@ -60,23 +61,27 @@ export function DataTable({
|
|||
placeholder={formatMessage(labels.search)}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={classNames(styles.body, { [styles.status]: isLoading || noResults || !hasData })}
|
||||
>
|
||||
{hasData ? (typeof children === 'function' ? children(result) : children) : null}
|
||||
{isLoading && <Loading position="page" />}
|
||||
{!isLoading && !hasData && !query && <Empty />}
|
||||
{noResults && <Empty message={formatMessage(messages.noResultsFound)} />}
|
||||
</div>
|
||||
{allowPaging && hasData && (
|
||||
<Pager
|
||||
className={styles.pager}
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
count={count}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
)}
|
||||
<LoadingPanel data={data} isLoading={isLoading} isFetched={isFetched} error={error}>
|
||||
<div
|
||||
className={classNames(styles.body, {
|
||||
[styles.status]: isLoading || noResults || !hasData,
|
||||
})}
|
||||
>
|
||||
{hasData ? (typeof children === 'function' ? children(result) : children) : null}
|
||||
{isLoading && <Loading position="page" />}
|
||||
{!isLoading && !hasData && !query && (renderEmpty ? renderEmpty() : <Empty />)}
|
||||
{!isLoading && noResults && <Empty message={formatMessage(messages.noResultsFound)} />}
|
||||
</div>
|
||||
{allowPaging && hasData && (
|
||||
<Pager
|
||||
className={styles.pager}
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
count={count}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
)}
|
||||
</LoadingPanel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
16
src/components/common/LoadingPanel.module.css
Normal file
16
src/components/common/LoadingPanel.module.css
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
}
|
||||
36
src/components/common/LoadingPanel.tsx
Normal file
36
src/components/common/LoadingPanel.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Loading } from 'react-basics';
|
||||
import ErrorMessage from 'components/common/ErrorMessage';
|
||||
import Empty from 'components/common/Empty';
|
||||
import styles from './LoadingPanel.module.css';
|
||||
|
||||
export function LoadingPanel({
|
||||
data,
|
||||
error,
|
||||
isFetched,
|
||||
isLoading,
|
||||
loadingIcon = 'dots',
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
data?: any;
|
||||
error?: Error;
|
||||
isFetched?: boolean;
|
||||
isLoading?: boolean;
|
||||
loadingIcon?: 'dots' | 'spinner';
|
||||
isEmpty?: boolean;
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const isEmpty = !isLoading && isFetched && data && Array.isArray(data) && data.length === 0;
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.panel, className)}>
|
||||
{isLoading && !isFetched && <Loading className={styles.loading} icon={loadingIcon} />}
|
||||
{error && <ErrorMessage />}
|
||||
{!error && isEmpty && <Empty />}
|
||||
{!error && !isEmpty && data && children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -27,6 +27,6 @@
|
|||
}
|
||||
|
||||
.nav {
|
||||
justify-content: end;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
src/components/common/TypeIcon.tsx
Normal file
27
src/components/common/TypeIcon.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { ReactNode } from 'react';
|
||||
|
||||
export function TypeIcon({
|
||||
type,
|
||||
value,
|
||||
children,
|
||||
}: {
|
||||
type: 'browser' | 'country' | 'device' | 'os';
|
||||
value: string;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<img
|
||||
src={`${process.env.basePath || ''}/images/${type}/${
|
||||
value?.replaceAll(' ', '-').toLowerCase() || 'unknown'
|
||||
}.png`}
|
||||
alt={value}
|
||||
width={type === 'country' ? undefined : 16}
|
||||
height={type === 'country' ? undefined : 16}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default TypeIcon;
|
||||
|
|
@ -1,10 +1,20 @@
|
|||
export * from './queries/useApi';
|
||||
export * from './queries/useConfig';
|
||||
export * from './queries/useFilterQuery';
|
||||
export * from './queries/useEventDataEvents';
|
||||
export * from './queries/useEventDataProperties';
|
||||
export * from './queries/useEventDataValues';
|
||||
export * from './queries/usePagedQuery';
|
||||
export * from './queries/useLogin';
|
||||
export * from './queries/useRealtime';
|
||||
export * from './queries/useReport';
|
||||
export * from './queries/useReports';
|
||||
export * from './queries/useSessionActivity';
|
||||
export * from './queries/useSessionData';
|
||||
export * from './queries/useSessionDataProperties';
|
||||
export * from './queries/useSessionDataValues';
|
||||
export * from './queries/useWebsiteSession';
|
||||
export * from './queries/useWebsiteSessions';
|
||||
export * from './queries/useWebsiteSessionsWeekly';
|
||||
export * from './queries/useShareToken';
|
||||
export * from './queries/useTeam';
|
||||
export * from './queries/useTeams';
|
||||
|
|
@ -15,6 +25,7 @@ export * from './queries/useUsers';
|
|||
export * from './queries/useWebsite';
|
||||
export * from './queries/useWebsites';
|
||||
export * from './queries/useWebsiteEvents';
|
||||
export * from './queries/useWebsiteEventsSeries';
|
||||
export * from './queries/useWebsiteMetrics';
|
||||
export * from './queries/useWebsiteValues';
|
||||
export * from './useCountryNames';
|
||||
|
|
@ -30,6 +41,7 @@ export * from './useLocale';
|
|||
export * from './useMessages';
|
||||
export * from './useModified';
|
||||
export * from './useNavigation';
|
||||
export * from './useRegionNames';
|
||||
export * from './useSticky';
|
||||
export * from './useTeamUrl';
|
||||
export * from './useTheme';
|
||||
|
|
|
|||
20
src/components/hooks/queries/useEventDataEvents.ts
Normal file
20
src/components/hooks/queries/useEventDataEvents.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import useApi from './useApi';
|
||||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
|
||||
export function useEventDataEvents(
|
||||
websiteId: string,
|
||||
options?: Omit<UseQueryOptions, 'queryKey' | 'queryFn'>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const params = useFilterParams(websiteId);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['websites:event-data:events', { websiteId, ...params }],
|
||||
queryFn: () => get(`/websites/${websiteId}/event-data/events`, { ...params }),
|
||||
enabled: !!websiteId,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export default useEventDataEvents;
|
||||
20
src/components/hooks/queries/useEventDataProperties.ts
Normal file
20
src/components/hooks/queries/useEventDataProperties.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import useApi from './useApi';
|
||||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
|
||||
export function useEventDataProperties(
|
||||
websiteId: string,
|
||||
options?: Omit<UseQueryOptions, 'queryKey' | 'queryFn'>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const params = useFilterParams(websiteId);
|
||||
|
||||
return useQuery<any>({
|
||||
queryKey: ['websites:event-data:properties', { websiteId, ...params }],
|
||||
queryFn: () => get(`/websites/${websiteId}/event-data/properties`, { ...params }),
|
||||
enabled: !!websiteId,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export default useEventDataProperties;
|
||||
23
src/components/hooks/queries/useEventDataValues.ts
Normal file
23
src/components/hooks/queries/useEventDataValues.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import useApi from './useApi';
|
||||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
|
||||
export function useEventDataValues(
|
||||
websiteId: string,
|
||||
eventName: string,
|
||||
propertyName: string,
|
||||
options?: Omit<UseQueryOptions, 'queryKey' | 'queryFn'>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const params = useFilterParams(websiteId);
|
||||
|
||||
return useQuery<any>({
|
||||
queryKey: ['websites:event-data:values', { websiteId, propertyName, ...params }],
|
||||
queryFn: () =>
|
||||
get(`/websites/${websiteId}/event-data/values`, { ...params, eventName, propertyName }),
|
||||
enabled: !!(websiteId && propertyName),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export default useEventDataValues;
|
||||
|
|
@ -1,16 +1,18 @@
|
|||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import { useApi } from './useApi';
|
||||
import { PageResult, PageParams, FilterQueryResult } from 'lib/types';
|
||||
import { PageResult, PageParams, PagedQueryResult } from 'lib/types';
|
||||
import { useNavigation } from '../useNavigation';
|
||||
|
||||
export function useFilterQuery<T = any>({
|
||||
export function usePagedQuery<T = any>({
|
||||
queryKey,
|
||||
queryFn,
|
||||
...options
|
||||
}: Omit<UseQueryOptions, 'queryFn'> & { queryFn: (params?: object) => any }): FilterQueryResult<T> {
|
||||
const [params, setParams] = useState<T | PageParams>({
|
||||
}: Omit<UseQueryOptions, 'queryFn'> & { queryFn: (params?: object) => any }): PagedQueryResult<T> {
|
||||
const { query: queryParams } = useNavigation();
|
||||
const [params, setParams] = useState<PageParams>({
|
||||
query: '',
|
||||
page: 1,
|
||||
page: +queryParams.page || 1,
|
||||
});
|
||||
|
||||
const { useQuery } = useApi();
|
||||
|
|
@ -21,11 +23,11 @@ export function useFilterQuery<T = any>({
|
|||
});
|
||||
|
||||
return {
|
||||
result: data as PageResult<any>,
|
||||
result: data as PageResult<T>,
|
||||
query,
|
||||
params,
|
||||
setParams,
|
||||
};
|
||||
}
|
||||
|
||||
export default useFilterQuery;
|
||||
export default usePagedQuery;
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import { useTimezone } from 'components/hooks';
|
||||
import { REALTIME_INTERVAL } from 'lib/constants';
|
||||
import { RealtimeData } from 'lib/types';
|
||||
import { useApi } from './useApi';
|
||||
import { REALTIME_INTERVAL } from 'lib/constants';
|
||||
import { useTimezone } from 'components/hooks';
|
||||
|
||||
export function useRealtime(websiteId: string) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { timezone } = useTimezone();
|
||||
const { data, isLoading, error } = useQuery<RealtimeData>({
|
||||
queryKey: ['realtime', websiteId],
|
||||
queryKey: ['realtime', { websiteId, timezone }],
|
||||
queryFn: async () => {
|
||||
return get(`/realtime/${websiteId}`, { timezone });
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import useApi from './useApi';
|
||||
import useFilterQuery from './useFilterQuery';
|
||||
import usePagedQuery from './usePagedQuery';
|
||||
import useModified from '../useModified';
|
||||
|
||||
export function useReports({ websiteId, teamId }: { websiteId?: string; teamId?: string }) {
|
||||
const { modified } = useModified(`reports`);
|
||||
const { get, del, useMutation } = useApi();
|
||||
const queryResult = useFilterQuery({
|
||||
const queryResult = usePagedQuery({
|
||||
queryKey: ['reports', { websiteId, teamId, modified }],
|
||||
queryFn: (params: any) => {
|
||||
return get('/reports', { websiteId, teamId, ...params });
|
||||
|
|
|
|||
21
src/components/hooks/queries/useSessionActivity.ts
Normal file
21
src/components/hooks/queries/useSessionActivity.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { useApi } from './useApi';
|
||||
|
||||
export function useSessionActivity(
|
||||
websiteId: string,
|
||||
sessionId: string,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['session:activity', { websiteId, sessionId, startDate, endDate }],
|
||||
queryFn: () => {
|
||||
return get(`/websites/${websiteId}/sessions/${sessionId}/activity`, {
|
||||
startAt: +new Date(startDate),
|
||||
endAt: +new Date(endDate),
|
||||
});
|
||||
},
|
||||
enabled: Boolean(websiteId && sessionId && startDate && endDate),
|
||||
});
|
||||
}
|
||||
12
src/components/hooks/queries/useSessionData.ts
Normal file
12
src/components/hooks/queries/useSessionData.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { useApi } from './useApi';
|
||||
|
||||
export function useSessionData(websiteId: string, sessionId: string) {
|
||||
const { get, useQuery } = useApi();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['session:data', { websiteId, sessionId }],
|
||||
queryFn: () => {
|
||||
return get(`/websites/${websiteId}/sessions/${sessionId}/properties`, { websiteId });
|
||||
},
|
||||
});
|
||||
}
|
||||
20
src/components/hooks/queries/useSessionDataProperties.ts
Normal file
20
src/components/hooks/queries/useSessionDataProperties.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import useApi from './useApi';
|
||||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
|
||||
export function useSessionDataProperties(
|
||||
websiteId: string,
|
||||
options?: Omit<UseQueryOptions, 'queryKey' | 'queryFn'>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const params = useFilterParams(websiteId);
|
||||
|
||||
return useQuery<any>({
|
||||
queryKey: ['websites:event-data:properties', { websiteId, ...params }],
|
||||
queryFn: () => get(`/websites/${websiteId}/session-data/properties`, { ...params }),
|
||||
enabled: !!websiteId,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export default useSessionDataProperties;
|
||||
21
src/components/hooks/queries/useSessionDataValues.ts
Normal file
21
src/components/hooks/queries/useSessionDataValues.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import useApi from './useApi';
|
||||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
|
||||
export function useSessionDataValues(
|
||||
websiteId: string,
|
||||
propertyName: string,
|
||||
options?: Omit<UseQueryOptions, 'queryKey' | 'queryFn'>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const params = useFilterParams(websiteId);
|
||||
|
||||
return useQuery<any>({
|
||||
queryKey: ['websites:session-data:values', { websiteId, propertyName, ...params }],
|
||||
queryFn: () => get(`/websites/${websiteId}/session-data/values`, { ...params, propertyName }),
|
||||
enabled: !!(websiteId && propertyName),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export default useSessionDataValues;
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import useApi from './useApi';
|
||||
import useFilterQuery from './useFilterQuery';
|
||||
import usePagedQuery from './usePagedQuery';
|
||||
import useModified from '../useModified';
|
||||
|
||||
export function useTeamMembers(teamId: string) {
|
||||
const { get } = useApi();
|
||||
const { modified } = useModified(`teams:members`);
|
||||
|
||||
return useFilterQuery({
|
||||
return usePagedQuery({
|
||||
queryKey: ['teams:members', { teamId, modified }],
|
||||
queryFn: (params: any) => {
|
||||
return get(`/teams/${teamId}/users`, params);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import useApi from './useApi';
|
||||
import useFilterQuery from './useFilterQuery';
|
||||
import usePagedQuery from './usePagedQuery';
|
||||
import useModified from '../useModified';
|
||||
|
||||
export function useTeamWebsites(teamId: string) {
|
||||
const { get } = useApi();
|
||||
const { modified } = useModified(`websites`);
|
||||
|
||||
return useFilterQuery({
|
||||
return usePagedQuery({
|
||||
queryKey: ['teams:websites', { teamId, modified }],
|
||||
queryFn: (params: any) => {
|
||||
return get(`/teams/${teamId}/websites`, params);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import useApi from './useApi';
|
||||
import useFilterQuery from './useFilterQuery';
|
||||
import usePagedQuery from './usePagedQuery';
|
||||
import useModified from '../useModified';
|
||||
|
||||
export function useTeams(userId: string) {
|
||||
const { get } = useApi();
|
||||
const { modified } = useModified(`teams`);
|
||||
|
||||
return useFilterQuery({
|
||||
return usePagedQuery({
|
||||
queryKey: ['teams', { userId, modified }],
|
||||
queryFn: (params: any) => {
|
||||
return get(`/users/${userId}/teams`, params);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import useApi from './useApi';
|
||||
import useFilterQuery from './useFilterQuery';
|
||||
import usePagedQuery from './usePagedQuery';
|
||||
import useModified from '../useModified';
|
||||
|
||||
export function useUsers() {
|
||||
const { get } = useApi();
|
||||
const { modified } = useModified(`users`);
|
||||
|
||||
return useFilterQuery({
|
||||
return usePagedQuery({
|
||||
queryKey: ['users', { modified }],
|
||||
queryFn: (params: any) => {
|
||||
return get('/admin/users', {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
import useApi from './useApi';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
import { usePagedQuery } from './usePagedQuery';
|
||||
|
||||
export function useWebsiteEvents(
|
||||
websiteId: string,
|
||||
options?: Omit<UseQueryOptions, 'queryKey' | 'queryFn'>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { get } = useApi();
|
||||
const params = useFilterParams(websiteId);
|
||||
|
||||
return useQuery({
|
||||
return usePagedQuery({
|
||||
queryKey: ['websites:events', { websiteId, ...params }],
|
||||
queryFn: () => get(`/websites/${websiteId}/events`, params),
|
||||
queryFn: pageParams =>
|
||||
get(`/websites/${websiteId}/events`, { ...params, ...pageParams, pageSize: 20 }),
|
||||
enabled: !!websiteId,
|
||||
...options,
|
||||
});
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue