Merge branch 'master' into feat/events-page-filters

This commit is contained in:
Cătălin Pîrvu 2025-09-03 18:58:12 +03:00 committed by GitHub
commit 737a323c91
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
113 changed files with 4827 additions and 3380 deletions

View file

@ -22,10 +22,6 @@ export function App({ children }) {
return null;
}
if (config.uiDisabled) {
return null;
}
return (
<>
{children}

View file

@ -13,13 +13,14 @@ export function UpdateNotice({ user, config }) {
const { latest, checked, hasUpdate, releaseUrl } = useStore();
const pathname = usePathname();
const [dismissed, setDismissed] = useState(checked);
const allowUpdate =
process.env.NODE_ENV === 'production' &&
user?.isAdmin &&
!config?.updatesDisabled &&
!config?.privateMode &&
!pathname.includes('/share/') &&
!process.env.cloudMode &&
!process.env.privateMode &&
!dismissed;
const updateCheck = useCallback(() => {

View file

@ -9,6 +9,7 @@ export function LanguageSetting() {
const [search, setSearch] = useState('');
const { formatMessage, labels } = useMessages();
const { locale, saveLocale } = useLocale();
const options = search
? Object.keys(languages).filter(n => {
return (

View file

@ -1,6 +1,7 @@
import { useContext } from 'react';
import { ReportContext } from './Report';
import styles from './ReportBody.module.css';
import { DownloadButton } from '@/components/input/DownloadButton';
export function ReportBody({ children }) {
const { report } = useContext(ReportContext);
@ -9,7 +10,14 @@ export function ReportBody({ children }) {
return null;
}
return <div className={styles.body}>{children}</div>;
return (
<div className={styles.body}>
{report.type !== 'revenue' && report.type !== 'attribution' && (
<DownloadButton filename={report.name} data={report.data} />
)}
{children}
</div>
);
}
export default ReportBody;

View file

@ -31,5 +31,9 @@ export default function ReportPage({ reportId }: { reportId: string }) {
const ReportComponent = reports[report.type];
if (!ReportComponent) {
return null;
}
return <ReportComponent reportId={reportId} />;
}

View file

@ -15,6 +15,31 @@ function toArray(data: { [key: string]: number } = {}) {
.sort(firstBy('value', -1));
}
function parseParameters(data: any[]) {
return data.reduce((obj, { url_query, num }) => {
try {
const searchParams = new URLSearchParams(url_query);
for (const [key, value] of searchParams) {
if (key.match(/^utm_(\w+)$/)) {
const name = value;
if (!obj[key]) {
obj[key] = { [name]: Number(num) };
} else if (!obj[key][name]) {
obj[key][name] = Number(num);
} else {
obj[key][name] += Number(num);
}
}
}
} catch {
// Ignore
}
return obj;
}, {});
}
export default function UTMView() {
const { formatMessage, labels } = useMessages();
const { report } = useContext(ReportContext);
@ -27,7 +52,7 @@ export default function UTMView() {
return (
<div>
{UTM_PARAMS.map(param => {
const items = toArray(data[param]);
const items = toArray(parseParameters(data)[param]);
const chartData = {
labels: items.map(({ name }) => name),
datasets: [

View file

@ -7,7 +7,7 @@ import WebsiteExpandedView from './WebsiteExpandedView';
import WebsiteHeader from './WebsiteHeader';
import WebsiteMetricsBar from './WebsiteMetricsBar';
import WebsiteTableView from './WebsiteTableView';
import { FILTER_COLUMNS } from '@/lib/constants';
import { FILTER_COLUMNS, FILTER_GROUPS } from '@/lib/constants';
export default function WebsiteDetailsPage({ websiteId }: { websiteId: string }) {
const pathname = usePathname();
@ -17,7 +17,7 @@ export default function WebsiteDetailsPage({ websiteId }: { websiteId: string })
const { view } = query;
const params = Object.keys(query).reduce((obj, key) => {
if (FILTER_COLUMNS[key]) {
if (FILTER_COLUMNS[key] || FILTER_GROUPS[key]) {
obj[key] = query[key];
}
return obj;

View file

@ -1,6 +1,6 @@
import { Dropdown, Item } from 'react-basics';
import classNames from 'classnames';
import { useDateRange, useMessages, useSticky } from '@/components/hooks';
import { useDateRange, useMessages, useNavigation, useSticky } from '@/components/hooks';
import WebsiteDateFilter from '@/components/input/WebsiteDateFilter';
import MetricCard from '@/components/metrics/MetricCard';
import MetricsBar from '@/components/metrics/MetricsBar';
@ -8,6 +8,7 @@ import { formatShortTime, formatLongNumber } from '@/lib/format';
import useWebsiteStats from '@/components/hooks/queries/useWebsiteStats';
import useStore, { setWebsiteDateCompare } from '@/store/websites';
import WebsiteFilterButton from './WebsiteFilterButton';
import { ExportButton } from '@/components/input/ExportButton';
import styles from './WebsiteMetricsBar.module.css';
export function WebsiteMetricsBar({
@ -31,6 +32,9 @@ export function WebsiteMetricsBar({
websiteId,
compareMode && dateCompare,
);
const {
query: { view },
} = useNavigation();
const isAllTime = dateRange.value === 'all';
const { pageviews, visitors, visits, bounces, totaltime } = data || {};
@ -109,7 +113,10 @@ export function WebsiteMetricsBar({
</MetricsBar>
</div>
<div className={styles.actions}>
{showFilter && <WebsiteFilterButton websiteId={websiteId} />}
<div>
{showFilter && <WebsiteFilterButton websiteId={websiteId} />}
{!view && <ExportButton websiteId={websiteId} />}
</div>
<WebsiteDateFilter websiteId={websiteId} showAllTime={!compareMode} />
{compareMode && (
<div className={styles.vs}>

View file

@ -14,6 +14,7 @@ export default function WebsiteTableView({ websiteId }: { websiteId: string }) {
const pathname = usePathname();
const tableProps = {
websiteId,
allowDownload: false,
limit: 10,
};
const isSharePage = pathname.includes('/share/');

View file

@ -3,7 +3,7 @@ import WebsiteHeader from '../WebsiteHeader';
import WebsiteMetricsBar from '../WebsiteMetricsBar';
import FilterTags from '@/components/metrics/FilterTags';
import { useNavigation } from '@/components/hooks';
import { FILTER_COLUMNS } from '@/lib/constants';
import { FILTER_COLUMNS, FILTER_GROUPS } from '@/lib/constants';
import WebsiteChart from '../WebsiteChart';
import WebsiteCompareTables from './WebsiteCompareTables';
@ -11,7 +11,7 @@ export function WebsiteComparePage({ websiteId }) {
const { query } = useNavigation();
const params = Object.keys(query).reduce((obj, key) => {
if (FILTER_COLUMNS[key]) {
if (FILTER_COLUMNS[key] || FILTER_GROUPS[key]) {
obj[key] = query[key];
}
return obj;

View file

@ -14,12 +14,14 @@
color: var(--primary400);
}
.title {
text-align: center;
font-weight: bold;
margin: 20px 0;
.header {
margin-bottom: 40px;
}
.chart {
.title {
font-weight: bold;
}
.data {
min-height: 620px;
}

View file

@ -1,7 +1,9 @@
import { GridColumn, GridTable } from 'react-basics';
import { useMemo } from 'react';
import { GridColumn, GridTable, Flexbox, Button, ButtonGroup, Loading } from 'react-basics';
import { useEventDataProperties, useEventDataValues, useMessages } from '@/components/hooks';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import PieChart from '@/components/charts/PieChart';
import ListTable from '@/components/metrics/ListTable';
import { useState } from 'react';
import { CHART_COLORS } from '@/lib/constants';
import styles from './EventProperties.module.css';
@ -9,22 +11,38 @@ import styles from './EventProperties.module.css';
export function EventProperties({ websiteId }: { websiteId: string }) {
const [propertyName, setPropertyName] = useState('');
const [eventName, setEventName] = useState('');
const [propertyView, setPropertyView] = useState('table');
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 propertySum = useMemo(() => {
return values?.reduce((sum, { total }) => sum + total, 0) ?? 0;
}, [values]);
const chartData = useMemo(() => {
if (!propertyName || !values) return null;
return {
labels: values.map(({ value }) => value),
datasets: [
{
data: values.map(({ total }) => total),
backgroundColor: CHART_COLORS,
borderWidth: 0,
},
],
};
}, [propertyName, values]);
const tableData = useMemo(() => {
if (!propertyName || !values || propertySum === 0) return [];
return values.map(({ value, total }) => ({
x: value,
y: total,
z: 100 * (total / propertySum),
}));
}, [propertyName, values, propertySum]);
const handleRowClick = row => {
setEventName(row.eventName);
@ -52,9 +70,25 @@ export function EventProperties({ websiteId }: { websiteId: string }) {
<GridColumn name="total" label={formatMessage(labels.count)} alignment="end" />
</GridTable>
{propertyName && (
<div className={styles.chart}>
<div className={styles.title}>{propertyName}</div>
<PieChart key={propertyName + eventName} type="doughnut" data={chartData} />
<div className={styles.data}>
<Flexbox className={styles.header} gap={12} justifyContent="space-between">
<div className={styles.title}>{`${eventName}: ${propertyName}`}</div>
<ButtonGroup
selectedKey={propertyView}
onSelect={key => setPropertyView(key as string)}
>
<Button key="table">{formatMessage(labels.table)}</Button>
<Button key="chart">{formatMessage(labels.chart)}</Button>
</ButtonGroup>
</Flexbox>
{!values ? (
<Loading icon="dots" />
) : propertyView === 'table' ? (
<ListTable data={tableData} />
) : (
<PieChart key={propertyName + eventName} type="doughnut" data={chartData} />
)}
</div>
)}
</div>

View file

@ -11,10 +11,11 @@ import { Item, Tabs } from 'react-basics';
import { useState } from 'react';
import EventProperties from './EventProperties';
import { FILTER_COLUMNS } from '@/lib/constants';
import { getItem, setItem } from '@/lib/storage';
export default function EventsPage({ websiteId }) {
const [label, setLabel] = useState(null);
const [tab, setTab] = useState('activity');
const [tab, setTab] = useState(getItem('eventTab') || 'activity');
const { formatMessage, labels } = useMessages();
const { query } = useNavigation();
@ -28,6 +29,10 @@ export default function EventsPage({ websiteId }) {
}
return obj;
}, {});
const onSelect = (value: 'activity' | 'properties') => {
setItem('eventTab', value);
setTab(value);
};
return (
<>
@ -45,11 +50,7 @@ export default function EventsPage({ websiteId }) {
/>
</GridRow>
<div>
<Tabs
selectedKey={tab}
onSelect={(value: any) => setTab(value)}
style={{ marginBottom: 30 }}
>
<Tabs selectedKey={tab} onSelect={onSelect} style={{ marginBottom: 30 }}>
<Item key="activity">{formatMessage(labels.activity)}</Item>
<Item key="properties">{formatMessage(labels.properties)}</Item>
</Tabs>

View file

@ -1,10 +1,19 @@
'use server';
export async function getConfig() {
export type Config = {
faviconUrl: string | undefined;
privateMode: boolean;
telemetryDisabled: boolean;
trackerScriptName: string | undefined;
updatesDisabled: boolean;
};
export async function getConfig(): Promise<Config> {
return {
faviconUrl: process.env.FAVICON_URL,
privateMode: !!process.env.PRIVATE_MODE,
telemetryDisabled: !!process.env.DISABLE_TELEMETRY,
trackerScriptName: process.env.TRACKER_SCRIPT_NAME,
uiDisabled: !!process.env.DISABLE_UI,
updatesDisabled: !!process.env.DISABLE_UPDATES,
};
}

View file

@ -20,8 +20,6 @@ export async function GET(request: Request, { params }: { params: Promise<{ repo
return unauthorized();
}
report.parameters = JSON.parse(report.parameters);
return json(report);
}
@ -62,7 +60,7 @@ export async function POST(
type,
name,
description,
parameters: JSON.stringify(parameters),
parameters: parameters,
} as any);
return json(result);

View file

@ -103,7 +103,7 @@ export async function POST(request: Request) {
type,
name,
description,
parameters: JSON.stringify(parameters),
parameters: parameters,
} as any);
return json(result);

View file

@ -2,25 +2,25 @@ import { CURRENT_VERSION, TELEMETRY_PIXEL } from '@/lib/constants';
export async function GET() {
if (
process.env.NODE_ENV !== 'production' &&
process.env.DISABLE_TELEMETRY &&
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, ''), {
return new Response('/* telemetry disabled */', {
headers: {
'content-type': 'text/javascript',
},
});
}
return new Response('/* telemetry disabled */', {
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',
},

View file

@ -4,7 +4,7 @@ import { startOfHour, startOfMonth } from 'date-fns';
import clickhouse from '@/lib/clickhouse';
import { parseRequest } from '@/lib/request';
import { badRequest, json, forbidden, serverError } from '@/lib/response';
import { fetchSession, fetchWebsite } from '@/lib/load';
import { fetchWebsite } from '@/lib/load';
import { getClientInfo, hasBlockedIp } from '@/lib/detect';
import { createToken, parseToken } from '@/lib/jwt';
import { secret, uuid, hash } from '@/lib/crypto';
@ -103,32 +103,24 @@ export async function POST(request: Request) {
const sessionId = id ? uuid(websiteId, id) : uuid(websiteId, ip, userAgent, sessionSalt);
// Find session
// Create a session if not found
if (!clickhouse.enabled && !cache?.sessionId) {
const session = await fetchSession(websiteId, sessionId);
// Create a session if not found
if (!session) {
try {
await createSession({
id: sessionId,
websiteId,
browser,
os,
device,
screen,
language,
country,
region,
city,
distinctId: id,
});
} catch (e: any) {
if (!e.message.toLowerCase().includes('unique constraint')) {
return serverError(e);
}
}
}
await createSession(
{
id: sessionId,
websiteId,
browser,
os,
device,
screen,
language,
country,
region,
city,
distinctId: id,
},
{ skipDuplicates: true },
);
}
// Visit info
@ -145,7 +137,8 @@ export async function POST(request: Request) {
const base = hostname ? `https://${hostname}` : 'https://localhost';
const currentUrl = new URL(url, base);
let urlPath = currentUrl.pathname === '/undefined' ? '' : currentUrl.pathname;
let urlPath =
currentUrl.pathname === '/undefined' ? '' : currentUrl.pathname + currentUrl.hash;
const urlQuery = currentUrl.search.substring(1);
const urlDomain = currentUrl.hostname.replace(/^www./, '');
@ -169,7 +162,7 @@ export async function POST(request: Request) {
const twclid = currentUrl.searchParams.get('twclid');
if (process.env.REMOVE_TRAILING_SLASH) {
urlPath = urlPath.replace(/(.+)\/$/, '$1');
urlPath = urlPath.replace(/\/(?=(#.*)?$)/, '');
}
if (referrer) {

View file

@ -3,7 +3,7 @@ import { parseRequest, getRequestDateRange, getRequestFilters } from '@/lib/requ
import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth';
import { filterParams, timezoneParam, unitParam } from '@/lib/schema';
import { getEventMetrics } from '@/queries';
import { getEventStats } from '@/queries';
export async function GET(
request: Request,
@ -32,14 +32,14 @@ export async function GET(
}
const filters = {
...getRequestFilters(query),
...(await getRequestFilters(query)),
startDate,
endDate,
timezone,
unit,
};
const data = await getEventMetrics(websiteId, filters);
const data = await getEventStats(websiteId, filters);
return json(data);
}

View file

@ -0,0 +1,73 @@
import { z } from 'zod';
import JSZip from 'jszip';
import Papa from 'papaparse';
import { getRequestFilters, parseRequest } from '@/lib/request';
import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth';
import { pagingParams } from '@/lib/schema';
import { getEventMetrics, getPageviewMetrics, getSessionMetrics } from '@/queries';
export async function GET(
request: Request,
{ params }: { params: Promise<{ websiteId: string }> },
) {
const schema = z.object({
startAt: z.coerce.number().int(),
endAt: z.coerce.number().int(),
...pagingParams,
});
const { auth, query, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const { websiteId } = await params;
const { startAt, endAt } = query;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const startDate = new Date(+startAt);
const endDate = new Date(+endAt);
const filters = {
...(await getRequestFilters(query)),
startDate,
endDate,
};
const [events, pages, referrers, browsers, os, devices, countries] = await Promise.all([
getEventMetrics(websiteId, 'event', filters),
getPageviewMetrics(websiteId, 'url', filters),
getPageviewMetrics(websiteId, 'referrer', filters),
getSessionMetrics(websiteId, 'browser', filters),
getSessionMetrics(websiteId, 'os', filters),
getSessionMetrics(websiteId, 'device', filters),
getSessionMetrics(websiteId, 'country', filters),
]);
const zip = new JSZip();
const parse = (data: any) => {
return Papa.unparse(data, {
header: true,
skipEmptyLines: true,
});
};
zip.file('events.csv', parse(events));
zip.file('pages.csv', parse(pages));
zip.file('referrers.csv', parse(referrers));
zip.file('browsers.csv', parse(browsers));
zip.file('os.csv', parse(os));
zip.file('devices.csv', parse(devices));
zip.file('countries.csv', parse(countries));
const content = await zip.generateAsync({ type: 'nodebuffer' });
const base64 = content.toString('base64');
return json({ zip: base64 });
}

View file

@ -15,7 +15,12 @@ import {
} from '@/lib/constants';
import { getRequestFilters, getRequestDateRange, parseRequest } from '@/lib/request';
import { json, unauthorized, badRequest } from '@/lib/response';
import { getPageviewMetrics, getSessionMetrics, getChannelMetrics } from '@/queries';
import {
getPageviewMetrics,
getSessionMetrics,
getEventMetrics,
getChannelMetrics,
} from '@/queries';
import { filterParams } from '@/lib/schema';
export async function GET(
@ -48,7 +53,7 @@ export async function GET(
const { startDate, endDate } = await getRequestDateRange(query);
const column = FILTER_COLUMNS[type] || type;
const filters = {
...getRequestFilters(query),
...(await getRequestFilters(query)),
startDate,
endDate,
};
@ -85,7 +90,13 @@ export async function GET(
}
if (EVENT_COLUMNS.includes(type)) {
const data = await getPageviewMetrics(websiteId, type, filters, limit, offset);
let data;
if (type === 'event') {
data = await getEventMetrics(websiteId, type, filters, limit, offset);
} else {
data = await getPageviewMetrics(websiteId, type, filters, limit, offset);
}
return json(data);
}

View file

@ -35,7 +35,7 @@ export async function GET(
const { startDate, endDate, unit } = await getRequestDateRange(query);
const filters = {
...getRequestFilters(query),
...(await getRequestFilters(query)),
startDate,
endDate,
timezone,

View file

@ -0,0 +1,92 @@
import { canDeleteWebsite, canUpdateWebsite, canViewWebsite } from '@/lib/auth';
import { parseRequest } from '@/lib/request';
import { json, notFound, ok, unauthorized } from '@/lib/response';
import { segmentTypeParam } from '@/lib/schema';
import { deleteSegment, getSegment, updateSegment } from '@/queries';
import { z } from 'zod';
export async function GET(
request: Request,
{ params }: { params: Promise<{ websiteId: string; segmentId: string }> },
) {
const { auth, error } = await parseRequest(request);
if (error) {
return error();
}
const { websiteId, segmentId } = await params;
const segment = await getSegment(segmentId);
if (websiteId && !(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
return json(segment);
}
export async function POST(
request: Request,
{ params }: { params: Promise<{ websiteId: string; segmentId: string }> },
) {
const schema = z.object({
type: segmentTypeParam,
name: z.string().max(200),
parameters: z.object({}).passthrough(),
});
const { auth, body, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const { websiteId, segmentId } = await params;
const { type, name, parameters } = body;
const segment = await getSegment(segmentId);
if (!segment) {
return notFound();
}
if (!(await canUpdateWebsite(auth, websiteId))) {
return unauthorized();
}
const result = await updateSegment(segmentId, {
type,
name,
parameters,
} as any);
return json(result);
}
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ websiteId: string; segmentId: string }> },
) {
const { auth, error } = await parseRequest(request);
if (error) {
return error();
}
const { websiteId, segmentId } = await params;
const segment = await getSegment(segmentId);
if (!segment) {
return notFound();
}
if (!(await canDeleteWebsite(auth, websiteId))) {
return unauthorized();
}
await deleteSegment(segmentId);
return ok();
}

View file

@ -0,0 +1,67 @@
import { canUpdateWebsite, canViewWebsite } from '@/lib/auth';
import { uuid } from '@/lib/crypto';
import { parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { segmentTypeParam } from '@/lib/schema';
import { createSegment, getWebsiteSegments } from '@/queries';
import { z } from 'zod';
export async function GET(
request: Request,
{ params }: { params: Promise<{ websiteId: string }> },
) {
const schema = z.object({
type: segmentTypeParam,
});
const { auth, query, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const { websiteId } = await params;
const { type } = query;
if (websiteId && !(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const segments = await getWebsiteSegments(websiteId, type);
return json(segments);
}
export async function POST(
request: Request,
{ params }: { params: Promise<{ websiteId: string }> },
) {
const schema = z.object({
type: segmentTypeParam,
name: z.string().max(200),
parameters: z.object({}).passthrough(),
});
const { auth, body, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const { websiteId } = await params;
const { type, name, parameters } = body;
if (!(await canUpdateWebsite(auth, websiteId))) {
return unauthorized();
}
const result = await createSegment({
id: uuid(),
websiteId,
type,
name,
parameters,
} as any);
return json(result);
}

View file

@ -29,7 +29,7 @@ export async function GET(
const { startDate, endDate } = await getRequestDateRange(query);
const filters = getRequestFilters(query);
const filters = await getRequestFilters(query);
const metrics = await getWebsiteSessionStats(websiteId, {
...filters,

View file

@ -37,7 +37,7 @@ export async function GET(
endDate,
);
const filters = getRequestFilters(query);
const filters = await getRequestFilters(query);
const metrics = await getWebsiteStats(websiteId, {
...filters,

View file

@ -1,9 +1,9 @@
import { z } from 'zod';
import { canViewWebsite } from '@/lib/auth';
import { EVENT_COLUMNS, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants';
import { getValues } from '@/queries';
import { parseRequest, getRequestDateRange } from '@/lib/request';
import { EVENT_COLUMNS, FILTER_COLUMNS, FILTER_GROUPS, SESSION_COLUMNS } from '@/lib/constants';
import { getRequestDateRange, parseRequest } from '@/lib/request';
import { badRequest, json, unauthorized } from '@/lib/response';
import { getWebsiteSegments, getValues } from '@/queries';
import { z } from 'zod';
export async function GET(
request: Request,
@ -30,11 +30,17 @@ export async function GET(
return unauthorized();
}
if (!SESSION_COLUMNS.includes(type) && !EVENT_COLUMNS.includes(type)) {
if (!SESSION_COLUMNS.includes(type) && !EVENT_COLUMNS.includes(type) && !FILTER_GROUPS[type]) {
return badRequest('Invalid type.');
}
const values = await getValues(websiteId, FILTER_COLUMNS[type], startDate, endDate, search);
let values;
if (FILTER_GROUPS[type]) {
values = (await getWebsiteSegments(websiteId, type)).map(segment => ({ value: segment.name }));
} else {
values = await getValues(websiteId, FILTER_COLUMNS[type], startDate, endDate, search);
}
return json(values.filter(n => n).sort());
}

View file

@ -26,6 +26,7 @@ export async function POST(request: Request) {
domain: z.string().max(500),
shareId: z.string().max(50).nullable().optional(),
teamId: z.string().nullable().optional(),
id: z.string().uuid().nullable().optional(),
});
const { auth, body, error } = await parseRequest(request, schema);
@ -34,14 +35,14 @@ export async function POST(request: Request) {
return error();
}
const { name, domain, shareId, teamId } = body;
const { id, name, domain, shareId, teamId } = body;
if ((teamId && !(await canCreateTeamWebsite(auth, teamId))) || !(await canCreateWebsite(auth))) {
return unauthorized();
}
const data: any = {
id: uuid(),
id: id ?? uuid(),
createdBy: auth.user.id,
name,
domain,

View file

@ -9,6 +9,14 @@ import '@/styles/index.css';
import '@/styles/variables.css';
export default function ({ children }) {
if (process.env.DISABLE_UI) {
return (
<html>
<body></body>
</html>
);
}
return (
<html lang="en" data-scroll="0">
<head>

View file

@ -3,10 +3,6 @@ import LoginForm from './LoginForm';
import styles from './LoginPage.module.css';
export function LoginPage() {
if (process.env.disableLogin) {
return null;
}
return (
<div className={styles.page}>
<LoginForm />

View file

@ -2,6 +2,10 @@ import { Metadata } from 'next';
import LoginPage from './LoginPage';
export default async function () {
if (process.env.DISABLE_LOGIN) {
return null;
}
return <LoginPage />;
}

View file

@ -6,9 +6,9 @@ import { setUser } from '@/store/app';
import { removeClientAuthToken } from '@/lib/client';
export function LogoutPage() {
const disabled = !!(process.env.disableLogin || process.env.cloudMode);
const router = useRouter();
const { post } = useApi();
const disabled = process.env.cloudMode;
useEffect(() => {
async function logout() {

View file

@ -2,6 +2,10 @@ import LogoutPage from './LogoutPage';
import { Metadata } from 'next';
export default function () {
if (process.env.DISABLE_LOGIN) {
return null;
}
return <LogoutPage />;
}