- {ordered.map(({ id, name, domain }, index) => {
+ {ordered.map(({ id }, index) => {
return index < limit ? (
-
+
+
+
+
+
+
+
) : null;
})}
diff --git a/components/pages/websites/WebsiteDetails.js b/components/pages/websites/WebsiteDetails.js
deleted file mode 100644
index ba80bcf8..00000000
--- a/components/pages/websites/WebsiteDetails.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import { useState } from 'react';
-import { Loading } from 'react-basics';
-import Page from 'components/layout/Page';
-import WebsiteChart from 'components/metrics/WebsiteChart';
-import useApi from 'hooks/useApi';
-import usePageQuery from 'hooks/usePageQuery';
-import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
-import WebsiteTableView from './WebsiteTableView';
-import WebsiteMenuView from './WebsiteMenuView';
-
-export default function WebsiteDetails({ websiteId }) {
- const { get, useQuery } = useApi();
- const { data, isLoading, error } = useQuery(['websites', websiteId], () =>
- get(`/websites/${websiteId}`),
- );
- const [chartLoaded, setChartLoaded] = useState(false);
-
- const {
- query: { view },
- } = usePageQuery();
-
- function handleDataLoad() {
- if (!chartLoaded) {
- setTimeout(() => setChartLoaded(true), DEFAULT_ANIMATION_DURATION);
- }
- }
-
- return (
-
-
- {!chartLoaded && }
- {chartLoaded && (
- <>
- {!view && }
- {view && }
- >
- )}
-
- );
-}
diff --git a/components/pages/websites/WebsiteDetails.module.css b/components/pages/websites/WebsiteDetails.module.css
deleted file mode 100644
index b0632be6..00000000
--- a/components/pages/websites/WebsiteDetails.module.css
+++ /dev/null
@@ -1,31 +0,0 @@
-.chart {
- margin-bottom: 30px;
-}
-
-.view {
- border-top: 1px solid var(--base300);
-}
-
-.menu {
- font-size: var(--font-size-sm);
-}
-
-.content {
- min-height: 600px;
- padding: 20px 0;
-}
-
-.backButton {
- display: flex;
- justify-content: center;
- align-items: center;
- margin-bottom: 16px;
-}
-
-.backButton svg {
- transform: rotate(180deg);
-}
-
-.hidden {
- display: none;
-}
diff --git a/components/pages/websites/WebsiteDetailsPage.js b/components/pages/websites/WebsiteDetailsPage.js
new file mode 100644
index 00000000..e6545ae2
--- /dev/null
+++ b/components/pages/websites/WebsiteDetailsPage.js
@@ -0,0 +1,37 @@
+import { Loading } from 'react-basics';
+import Page from 'components/layout/Page';
+import WebsiteChart from 'components/pages/websites/WebsiteChart';
+import FilterTags from 'components/metrics/FilterTags';
+import usePageQuery from 'hooks/usePageQuery';
+import WebsiteTableView from './WebsiteTableView';
+import WebsiteMenuView from './WebsiteMenuView';
+import { useWebsite } from 'hooks';
+import WebsiteHeader from './WebsiteHeader';
+import { WebsiteMetricsBar } from './WebsiteMetricsBar';
+
+export default function WebsiteDetailsPage({ websiteId }) {
+ const { data: website, isLoading, error } = useWebsite(websiteId);
+
+ const {
+ query: { view, url, referrer, os, browser, device, country, region, city, title },
+ } = usePageQuery();
+
+ return (
+
+
+
+
+
+ {!website && }
+ {website && (
+ <>
+ {!view && }
+ {view && }
+ >
+ )}
+
+ );
+}
diff --git a/components/pages/websites/WebsiteEventData.js b/components/pages/websites/WebsiteEventData.js
new file mode 100644
index 00000000..105995f9
--- /dev/null
+++ b/components/pages/websites/WebsiteEventData.js
@@ -0,0 +1,35 @@
+import EventDataTable from 'components/pages/event-data/EventDataTable';
+import { EventDataMetricsBar } from 'components/pages/event-data/EventDataMetricsBar';
+import { useDateRange, useApi, usePageQuery } from 'hooks';
+import styles from './WebsiteEventData.module.css';
+
+function useFields(websiteId, field) {
+ const [dateRange] = useDateRange(websiteId);
+ const { startDate, endDate } = dateRange;
+ const { get, useQuery } = useApi();
+ const { data, error, isLoading } = useQuery(
+ ['event-data:fields', websiteId, startDate, endDate],
+ () =>
+ get('/event-data', {
+ websiteId,
+ startAt: +startDate,
+ endAt: +endDate,
+ field,
+ }),
+ { enabled: !!(websiteId && startDate && endDate) },
+ );
+
+ return { data, error, isLoading };
+}
+
+export default function WebsiteEventData({ websiteId }) {
+ const { data } = useFields(websiteId);
+ const { query } = usePageQuery();
+
+ return (
+
+
+
+
+ );
+}
diff --git a/components/pages/websites/WebsiteEventData.module.css b/components/pages/websites/WebsiteEventData.module.css
new file mode 100644
index 00000000..e835da71
--- /dev/null
+++ b/components/pages/websites/WebsiteEventData.module.css
@@ -0,0 +1,9 @@
+.container {
+ display: flex;
+ flex-direction: column;
+}
+
+.header {
+ display: flex;
+ justify-content: flex-end;
+}
diff --git a/components/pages/websites/WebsiteEventDataPage.js b/components/pages/websites/WebsiteEventDataPage.js
new file mode 100644
index 00000000..08acafb5
--- /dev/null
+++ b/components/pages/websites/WebsiteEventDataPage.js
@@ -0,0 +1,12 @@
+import Page from 'components/layout/Page';
+import WebsiteHeader from './WebsiteHeader';
+import WebsiteEventData from './WebsiteEventData';
+
+export default function WebsiteEventDataPage({ websiteId }) {
+ return (
+
+
+
+
+ );
+}
diff --git a/components/pages/websites/WebsiteHeader.js b/components/pages/websites/WebsiteHeader.js
new file mode 100644
index 00000000..8802c320
--- /dev/null
+++ b/components/pages/websites/WebsiteHeader.js
@@ -0,0 +1,78 @@
+import classNames from 'classnames';
+import { Flexbox, Row, Column, Text, Button, Icon } from 'react-basics';
+import Link from 'next/link';
+import { useRouter } from 'next/router';
+import Favicon from 'components/common/Favicon';
+import ActiveUsers from 'components/metrics/ActiveUsers';
+import styles from './WebsiteHeader.module.css';
+import Icons from 'components/icons';
+import { useMessages, useWebsite } from 'hooks';
+
+export function WebsiteHeader({ websiteId, showLinks = true, children }) {
+ const { formatMessage, labels } = useMessages();
+ const { asPath, pathname } = useRouter();
+ const { data: website } = useWebsite(websiteId);
+ const { name, domain } = website || {};
+
+ const links = [
+ {
+ label: formatMessage(labels.overview),
+ icon:
,
+ path: '',
+ },
+ {
+ label: formatMessage(labels.realtime),
+ icon:
,
+ path: '/realtime',
+ },
+ {
+ label: formatMessage(labels.reports),
+ icon:
,
+ path: '/reports',
+ },
+ {
+ label: formatMessage(labels.eventData),
+ icon:
,
+ path: '/event-data',
+ },
+ ];
+
+ return (
+
+
+
+ {name}
+
+
+
+ {showLinks && (
+
+ {links.map(({ label, icon, path }) => {
+ const query = path.indexOf('?');
+ const selected = path
+ ? asPath.endsWith(query >= 0 ? path.substring(0, query) : path)
+ : pathname === '/websites/[id]';
+
+ return (
+
+
+
+ );
+ })}
+
+ )}
+ {children}
+
+
+ );
+}
+
+export default WebsiteHeader;
diff --git a/components/metrics/WebsiteHeader.module.css b/components/pages/websites/WebsiteHeader.module.css
similarity index 91%
rename from components/metrics/WebsiteHeader.module.css
rename to components/pages/websites/WebsiteHeader.module.css
index 408da0ec..89f78e52 100644
--- a/components/metrics/WebsiteHeader.module.css
+++ b/components/pages/websites/WebsiteHeader.module.css
@@ -23,3 +23,7 @@
gap: 30px;
min-height: 0;
}
+
+.selected {
+ font-weight: bold;
+}
diff --git a/components/pages/websites/WebsiteMetricsBar.js b/components/pages/websites/WebsiteMetricsBar.js
new file mode 100644
index 00000000..9114e8f4
--- /dev/null
+++ b/components/pages/websites/WebsiteMetricsBar.js
@@ -0,0 +1,138 @@
+import { useState } from 'react';
+import classNames from 'classnames';
+import { Row, Column } from 'react-basics';
+import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
+import MetricCard from 'components/metrics/MetricCard';
+import RefreshButton from 'components/input/RefreshButton';
+import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
+import MetricsBar from 'components/metrics/MetricsBar';
+import { useApi, useDateRange, usePageQuery, useMessages, useSticky } from 'hooks';
+import styles from './WebsiteMetricsBar.module.css';
+
+export function WebsiteMetricsBar({ websiteId, sticky }) {
+ const { formatMessage, labels } = useMessages();
+ const { get, useQuery } = useApi();
+ const [dateRange] = useDateRange(websiteId);
+ const { startDate, endDate, modified } = dateRange;
+ const [format, setFormat] = useState(true);
+ const { ref, isSticky } = useSticky({ enabled: sticky });
+ const {
+ query: { url, referrer, title, os, browser, device, country, region, city },
+ } = usePageQuery();
+
+ const { data, error, isLoading, isFetched } = useQuery(
+ [
+ 'websites:stats',
+ { websiteId, modified, url, referrer, title, os, browser, device, country, region, city },
+ ],
+ () =>
+ get(`/websites/${websiteId}/stats`, {
+ startAt: +startDate,
+ endAt: +endDate,
+ url,
+ referrer,
+ title,
+ os,
+ browser,
+ device,
+ country,
+ region,
+ city,
+ }),
+ );
+
+ const formatFunc = format
+ ? n => (n >= 0 ? formatLongNumber(n) : `-${formatLongNumber(Math.abs(n))}`)
+ : formatNumber;
+
+ function handleSetFormat() {
+ setFormat(state => !state);
+ }
+
+ const { pageviews, uniques, bounces, totaltime } = data || {};
+ const num = Math.min(data && uniques.value, data && bounces.value);
+ const diffs = data && {
+ pageviews: pageviews.value - pageviews.change,
+ uniques: uniques.value - uniques.change,
+ bounces: bounces.value - bounces.change,
+ totaltime: totaltime.value - totaltime.change,
+ };
+
+ return (
+
+
+
+ {!error && isFetched && (
+ <>
+
+
+ Number(n).toFixed(0) + '%'}
+ reverseColors
+ />
+
+ `${n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`
+ }
+ />
+ >
+ )}
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default WebsiteMetricsBar;
diff --git a/components/metrics/WebsiteChart.module.css b/components/pages/websites/WebsiteMetricsBar.module.css
similarity index 67%
rename from components/metrics/WebsiteChart.module.css
rename to components/pages/websites/WebsiteMetricsBar.module.css
index c9334a27..52decfc6 100644
--- a/components/metrics/WebsiteChart.module.css
+++ b/components/pages/websites/WebsiteMetricsBar.module.css
@@ -1,22 +1,4 @@
.container {
- position: relative;
- display: flex;
- flex-direction: column;
- align-self: stretch;
-}
-
-.chart {
- position: relative;
- overflow: hidden;
-}
-
-.title {
- font-size: var(--font-size-lg);
- line-height: 60px;
- font-weight: 600;
-}
-
-.header {
display: flex;
justify-content: space-between;
align-items: center;
@@ -35,8 +17,10 @@
gap: 10px;
}
-.dropdown {
- min-width: 200px;
+@media only screen and (max-width: 1200px) {
+ .actions {
+ margin-top: 40px;
+ }
}
@media only screen and (min-width: 992px) {
@@ -49,9 +33,3 @@
border-bottom: 1px solid var(--base300);
}
}
-
-@media only screen and (max-width: 1200px) {
- .actions {
- margin-top: 40px;
- }
-}
diff --git a/components/pages/websites/WebsiteReportsPage.js b/components/pages/websites/WebsiteReportsPage.js
new file mode 100644
index 00000000..b6f41bac
--- /dev/null
+++ b/components/pages/websites/WebsiteReportsPage.js
@@ -0,0 +1,30 @@
+import Page from 'components/layout/Page';
+import Link from 'next/link';
+import { Button, Icon, Icons, Text, Flexbox } from 'react-basics';
+import { useMessages, useReports } from 'hooks';
+import ReportsTable from 'components/pages/reports/ReportsTable';
+import WebsiteHeader from './WebsiteHeader';
+
+export function WebsiteReportsPage({ websiteId }) {
+ const { formatMessage, labels } = useMessages();
+ const { reports, error, isLoading } = useReports(websiteId);
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default WebsiteReportsPage;
diff --git a/hooks/index.js b/hooks/index.js
index 892d52e4..6a9b3b35 100644
--- a/hooks/index.js
+++ b/hooks/index.js
@@ -18,3 +18,4 @@ export * from './useSticky';
export * from './useTheme';
export * from './useTimezone';
export * from './useUser';
+export * from './useWebsite';
diff --git a/hooks/useReports.js b/hooks/useReports.js
index 0b5e60d0..90aa5cf5 100644
--- a/hooks/useReports.js
+++ b/hooks/useReports.js
@@ -1,8 +1,8 @@
import useApi from './useApi';
-export function useReports() {
+export function useReports(websiteId) {
const { get, useQuery } = useApi();
- const { data, error, isLoading } = useQuery(['reports'], () => get(`/reports`));
+ const { data, error, isLoading } = useQuery(['reports'], () => get(`/reports`, { websiteId }));
return { reports: data, error, isLoading };
}
diff --git a/hooks/useWebsite.js b/hooks/useWebsite.js
new file mode 100644
index 00000000..5315f0dc
--- /dev/null
+++ b/hooks/useWebsite.js
@@ -0,0 +1,10 @@
+import useApi from './useApi';
+
+export function useWebsite(websiteId) {
+ const { get, useQuery } = useApi();
+ return useQuery(['websites', websiteId], () => get(`/websites/${websiteId}`), {
+ enabled: !!websiteId,
+ });
+}
+
+export default useWebsite;
diff --git a/package.json b/package.json
index 6d5ed0fe..c965e77c 100644
--- a/package.json
+++ b/package.json
@@ -95,7 +95,7 @@
"node-fetch": "^3.2.8",
"npm-run-all": "^4.1.5",
"react": "^18.2.0",
- "react-basics": "^0.89.0",
+ "react-basics": "^0.91.0",
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.4",
diff --git a/pages/api/event-data/fields.ts b/pages/api/event-data/fields.ts
new file mode 100644
index 00000000..f94d6c54
--- /dev/null
+++ b/pages/api/event-data/fields.ts
@@ -0,0 +1,36 @@
+import { canViewWebsite } from 'lib/auth';
+import { useCors, useAuth } from 'lib/middleware';
+import { NextApiRequestQueryBody } from 'lib/types';
+import { NextApiResponse } from 'next';
+import { ok, methodNotAllowed, unauthorized } from 'next-basics';
+import { getEventDataFields } from 'queries';
+
+export interface EventDataFieldsRequestBody {
+ websiteId: string;
+ dateRange: {
+ startDate: string;
+ endDate: string;
+ };
+}
+
+export default async (
+ req: NextApiRequestQueryBody
,
+ res: NextApiResponse,
+) => {
+ await useCors(req, res);
+ await useAuth(req, res);
+
+ if (req.method === 'GET') {
+ const { websiteId, startAt, endAt } = req.query;
+
+ if (!(await canViewWebsite(req.auth, websiteId))) {
+ return unauthorized(res);
+ }
+
+ const data = await getEventDataFields(websiteId, new Date(+startAt), new Date(+endAt));
+
+ return ok(res, data);
+ }
+
+ return methodNotAllowed(res);
+};
diff --git a/pages/api/event-data/index.ts b/pages/api/event-data/index.ts
new file mode 100644
index 00000000..d683156f
--- /dev/null
+++ b/pages/api/event-data/index.ts
@@ -0,0 +1,37 @@
+import { canViewWebsite } from 'lib/auth';
+import { useCors, useAuth } from 'lib/middleware';
+import { NextApiRequestQueryBody } from 'lib/types';
+import { NextApiResponse } from 'next';
+import { ok, methodNotAllowed, unauthorized } from 'next-basics';
+import { getEventData } from 'queries';
+
+export interface EventDataRequestBody {
+ websiteId: string;
+ dateRange: {
+ startDate: string;
+ endDate: string;
+ };
+ field?: string;
+}
+
+export default async (
+ req: NextApiRequestQueryBody,
+ res: NextApiResponse,
+) => {
+ await useCors(req, res);
+ await useAuth(req, res);
+
+ if (req.method === 'GET') {
+ const { websiteId, startAt, endAt, field } = req.query;
+
+ if (!(await canViewWebsite(req.auth, websiteId))) {
+ return unauthorized(res);
+ }
+
+ const data = await getEventData(websiteId, new Date(+startAt), new Date(+endAt), field);
+
+ return ok(res, data);
+ }
+
+ return methodNotAllowed(res);
+};
diff --git a/pages/api/reports/event-data.ts b/pages/api/reports/event-data.ts
deleted file mode 100644
index e9135f81..00000000
--- a/pages/api/reports/event-data.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { canViewWebsite } from 'lib/auth';
-import { useCors, useAuth } from 'lib/middleware';
-import { NextApiRequestQueryBody } from 'lib/types';
-import { NextApiResponse } from 'next';
-import { ok, methodNotAllowed, unauthorized } from 'next-basics';
-import { getEventDataFields } from 'queries/analytics/eventData/getEventDataFields';
-import { getEventData } from 'queries';
-
-export interface EventDataRequestBody {
- websiteId: string;
- dateRange: {
- startDate: string;
- endDate: string;
- };
- fields: [
- {
- name: string;
- type: string;
- value: string;
- },
- ];
- filters: [
- {
- name: string;
- type: string;
- value: string;
- },
- ];
- groups: [
- {
- name: string;
- type: string;
- },
- ];
-}
-
-export default async (
- req: NextApiRequestQueryBody,
- res: NextApiResponse,
-) => {
- await useCors(req, res);
- await useAuth(req, res);
-
- if (req.method === 'GET') {
- const { websiteId, startAt, endAt } = req.query;
-
- if (!(await canViewWebsite(req.auth, websiteId))) {
- return unauthorized(res);
- }
-
- const data = await getEventDataFields(websiteId, new Date(+startAt), new Date(+endAt));
-
- return ok(res, data);
- }
-
- if (req.method === 'POST') {
- const {
- websiteId,
- dateRange: { startDate, endDate },
- ...criteria
- } = req.body;
-
- if (!(await canViewWebsite(req.auth, websiteId))) {
- return unauthorized(res);
- }
-
- const data = await getEventData(
- websiteId,
- new Date(startDate),
- new Date(endDate),
- criteria as any,
- );
-
- return ok(res, data);
- }
-
- return methodNotAllowed(res);
-};
diff --git a/pages/api/reports/index.ts b/pages/api/reports/index.ts
index 55dc4bf5..b2c5da9e 100644
--- a/pages/api/reports/index.ts
+++ b/pages/api/reports/index.ts
@@ -2,8 +2,9 @@ import { uuid } from 'lib/crypto';
import { useAuth, useCors } from 'lib/middleware';
import { NextApiRequestQueryBody } from 'lib/types';
import { NextApiResponse } from 'next';
-import { methodNotAllowed, ok } from 'next-basics';
+import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createReport, getReports } from 'queries';
+import { canViewWebsite } from 'lib/auth';
export interface ReportRequestBody {
websiteId: string;
@@ -23,12 +24,18 @@ export default async (
await useCors(req, res);
await useAuth(req, res);
+ const { websiteId } = req.query;
+
const {
user: { id: userId },
} = req.auth;
if (req.method === 'GET') {
- const data = await getReports(userId);
+ if (!(websiteId && (await canViewWebsite(req.auth, websiteId)))) {
+ return unauthorized(res);
+ }
+
+ const data = await getReports({ websiteId });
return ok(res, data);
}
diff --git a/pages/api/websites/[id]/eventData.ts b/pages/api/websites/[id]/eventData.ts
deleted file mode 100644
index 04a6d83b..00000000
--- a/pages/api/websites/[id]/eventData.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { canViewWebsite } from 'lib/auth';
-import { useAuth, useCors } from 'lib/middleware';
-import { NextApiRequestQueryBody, WebsiteEventDataMetric } from 'lib/types';
-import { NextApiResponse } from 'next';
-import { methodNotAllowed, ok, unauthorized } from 'next-basics';
-import { getEventData } from 'queries';
-
-export interface WebsiteEventDataRequestQuery {
- id: string;
-}
-
-export interface WebsiteEventDataRequestBody {
- startAt: string;
- endAt: string;
- eventName?: string;
- urlPath?: string;
- timeSeries?: {
- unit: string;
- timezone: string;
- };
- filters: [
- {
- eventKey?: string;
- eventValue?: string | number | boolean | Date;
- },
- ];
-}
-
-export default async (
- req: NextApiRequestQueryBody,
- res: NextApiResponse,
-) => {
- await useCors(req, res);
- await useAuth(req, res);
-
- const { id: websiteId } = req.query;
-
- if (req.method === 'GET') {
- if (!(await canViewWebsite(req.auth, websiteId))) {
- return unauthorized(res);
- }
-
- const { startAt, endAt, eventName, urlPath, filters } = req.body;
-
- const startDate = new Date(+startAt);
- const endDate = new Date(+endAt);
-
- const events = await getEventData(websiteId, {
- startDate,
- endDate,
- eventName,
- urlPath,
- filters,
- });
-
- return ok(res, events);
- }
-
- return methodNotAllowed(res);
-};
diff --git a/pages/reports/index.js b/pages/reports/index.js
deleted file mode 100644
index e74bc05f..00000000
--- a/pages/reports/index.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import AppLayout from 'components/layout/AppLayout';
-import useMessages from 'hooks/useMessages';
-import ReportsPage from 'components/pages/reports/ReportsPage';
-
-export default function () {
- const { formatMessage, labels } = useMessages();
-
- return (
-
-
-
- );
-}
diff --git a/pages/websites/[id]/event-data.js b/pages/websites/[id]/event-data.js
index 7b060d20..8b44616d 100644
--- a/pages/websites/[id]/event-data.js
+++ b/pages/websites/[id]/event-data.js
@@ -1,6 +1,6 @@
import { useRouter } from 'next/router';
import AppLayout from 'components/layout/AppLayout';
-import WebsiteEventData from 'components/pages/websites/WebsiteEventData';
+import WebsiteEventDataPage from 'components/pages/websites/WebsiteEventDataPage';
import useMessages from 'hooks/useMessages';
export default function () {
@@ -14,7 +14,7 @@ export default function () {
return (
-
+
);
}
diff --git a/pages/websites/[id]/realtime.js b/pages/websites/[id]/realtime.js
index ceab7ad2..efe486a5 100644
--- a/pages/websites/[id]/realtime.js
+++ b/pages/websites/[id]/realtime.js
@@ -1,6 +1,6 @@
import { useRouter } from 'next/router';
import AppLayout from 'components/layout/AppLayout';
-import RealtimeDashboard from 'components/pages/realtime/RealtimeDashboard';
+import RealtimePage from 'components/pages/realtime/RealtimePage';
export default function () {
const router = useRouter();
@@ -12,7 +12,7 @@ export default function () {
return (
-
+
);
}
diff --git a/pages/websites/[id]/reports.js b/pages/websites/[id]/reports.js
index 2035b539..ccd88081 100644
--- a/pages/websites/[id]/reports.js
+++ b/pages/websites/[id]/reports.js
@@ -1,6 +1,6 @@
import { useRouter } from 'next/router';
import AppLayout from 'components/layout/AppLayout';
-import WebsiteReports from 'components/pages/websites/WebsiteReports';
+import WebsiteReportsPage from 'components/pages/websites/WebsiteReportsPage';
export default function () {
const router = useRouter();
@@ -12,7 +12,7 @@ export default function () {
return (
-
+
);
}
diff --git a/public/images/os/mac-os.png b/public/images/os/mac-os.png
index 357e3eac..1972abe7 100644
Binary files a/public/images/os/mac-os.png and b/public/images/os/mac-os.png differ
diff --git a/queries/admin/report.ts b/queries/admin/report.ts
index 506902f5..47fe4eb4 100644
--- a/queries/admin/report.ts
+++ b/queries/admin/report.ts
@@ -13,11 +13,9 @@ export async function getReportById(reportId: string): Promise {
});
}
-export async function getReports(userId: string): Promise {
+export async function getReports(where: Prisma.ReportWhereUniqueInput): Promise {
return prisma.client.report.findMany({
- where: {
- userId,
- },
+ where,
});
}
diff --git a/queries/analytics/eventData/getEventData.ts b/queries/analytics/eventData/getEventData.ts
index 2f3b04eb..2f8b6992 100644
--- a/queries/analytics/eventData/getEventData.ts
+++ b/queries/analytics/eventData/getEventData.ts
@@ -3,26 +3,10 @@ import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import { WebsiteEventDataMetric } from 'lib/types';
import { loadWebsite } from 'lib/query';
import { DEFAULT_CREATED_AT } from 'lib/constants';
-
-export interface EventDataCriteria {
- fields: [{ name: string; type: string; value: string }];
- filters: [
- {
- name: string;
- type: string;
- value: [string, string];
- },
- ];
- groups: [
- {
- name: string;
- type: string;
- },
- ];
-}
+import prisma from '../../../lib/prisma';
export async function getEventData(
- ...args: [websiteId: string, startDate: Date, endDate: Date, criteria: EventDataCriteria]
+ ...args: [websiteId: string, startDate: Date, endDate: Date, field?: string]
): Promise {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -30,76 +14,79 @@ export async function getEventData(
});
}
-async function relationalQuery() {
- return null;
+async function relationalQuery(websiteId: string, startDate: Date, endDate: Date, field: string) {
+ const { toUuid, rawQuery } = prisma;
+ const website = await loadWebsite(websiteId);
+ const resetDate = new Date(website?.resetAt || DEFAULT_CREATED_AT);
+
+ if (field) {
+ return rawQuery(
+ `select event_key as field,
+ count(*) as total
+ from event_data
+ where website_id = $1${toUuid()}
+ and event_key = $2
+ and created_at >= $3
+ and created_at between $4 and $5
+ group by event_key
+ order by 2 desc, 1 asc
+ limit 1000
+ `,
+ [websiteId, field, resetDate, startDate, endDate] as any,
+ );
+ }
+
+ return rawQuery(
+ `select
+ event_key as field,
+ count(*) as total
+ from event_data
+ where website_id = $1${toUuid()}
+ and created_at >= $2
+ and created_at between $3 and $4
+ group by event_key
+ order by 2 desc, 1 asc
+ limit 1000
+ `,
+ [websiteId, resetDate, startDate, endDate] as any,
+ );
}
-async function clickhouseQuery(
- websiteId: string,
- startDate: Date,
- endDate: Date,
- criteria: EventDataCriteria,
-) {
- const { fields, filters } = criteria;
+async function clickhouseQuery(websiteId: string, startDate: Date, endDate: Date, field: string) {
const { rawQuery, getDateFormat, getBetweenDates } = clickhouse;
const website = await loadWebsite(websiteId);
const resetDate = new Date(website?.resetAt || DEFAULT_CREATED_AT);
- const uniqueFields = fields.reduce((obj, { name, type, value }) => {
- const prefix = type === 'array' ? 'string' : type;
-
- if (!obj[name]) {
- obj[name] = {
- columns: [
- 'event_key as field',
- `count(*) as total`,
- value === 'unique' ? `${prefix}_value as value` : null,
- ].filter(n => n),
- groups: ['event_key', value === 'unique' ? `${prefix}_value` : null].filter(n => n),
- };
- }
- return obj;
- }, {});
-
- const queries = Object.keys(uniqueFields).reduce((arr, key) => {
- const field = uniqueFields[key];
- const params = { websiteId, name: key };
-
- return arr.concat(
- rawQuery(
- `select
- ${field.columns.join(',')}
+ if (field) {
+ return rawQuery(
+ `select
+ event_key as field,
+ count(*) as total
from event_data
where website_id = {websiteId:UUID}
- and event_key = {name:String}
+ and event_key = {field:String}
and created_at >= ${getDateFormat(resetDate)}
and ${getBetweenDates('created_at', startDate, endDate)}
- group by ${field.groups.join(',')}
- limit 100
+ group by event_key
+ order by 2 desc, 1 asc
+ limit 1000
`,
- params,
- ),
+ { websiteId, field },
);
- }, []);
+ }
- const results = (await Promise.all(queries)).flatMap(n => n);
-
- const columns = results.reduce((arr, row) => {
- const keys = Object.keys(row);
- for (const key of keys) {
- if (!arr.includes(key)) {
- arr.push(key);
- }
- }
- return arr;
- }, []);
-
- return results.reduce((arr, row) => {
- return arr.concat(
- columns.reduce((obj, key) => {
- obj[key] = row[key];
- return obj;
- }, {}),
- );
- }, []);
+ return rawQuery(
+ `select
+ event_key as field,
+ count(*) as total
+ from event_data
+ where website_id = {websiteId:UUID}
+ and created_at >= ${getDateFormat(resetDate)}
+ and ${getBetweenDates('created_at', startDate, endDate)}
+ group by event_key
+ order by 2 desc, 1 asc
+ limit 1000
+ `,
+ { websiteId },
+ );
}
diff --git a/yarn.lock b/yarn.lock
index fc94a0f7..737c14e4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7344,10 +7344,10 @@ rc@^1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
-react-basics@^0.89.0:
- version "0.89.0"
- resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.89.0.tgz#672a14448818fc7f20a3f7d73d0340d2165f94f2"
- integrity sha512-nsYZCCfAjEy/fVt+5te3kQEyqA+4dEFutI9n7ol36eWmWbBJjZXCF1NgSHsosMYN2wlrpsrI7HoMTgL68FQnUg==
+react-basics@^0.91.0:
+ version "0.91.0"
+ resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.91.0.tgz#2970529a22a455ec73a1be884eb93a109c9dafc0"
+ integrity sha512-vP8LYWiFwA+eguMEuHvHct4Jl5R/2GUjWc1tMujDG0CsAAUGhx68tAJr0K3gBrWjmpJrTPVfX8SdBNKSDAjQsw==
dependencies:
classnames "^2.3.1"
date-fns "^2.29.3"