mirror of
https://github.com/umami-software/umami.git
synced 2026-02-08 22:57:12 +01:00
Merge branch 'dev' into feature/table-view-events
This commit is contained in:
commit
31f9b17942
60 changed files with 2522 additions and 1972 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ export async function POST(request: Request) {
|
|||
type,
|
||||
name,
|
||||
description,
|
||||
parameters: JSON.stringify(parameters),
|
||||
parameters: parameters,
|
||||
} as any);
|
||||
|
||||
return json(result);
|
||||
|
|
|
|||
|
|
@ -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 + currentUrl.hash;
|
||||
let urlPath =
|
||||
currentUrl.pathname === '/undefined' ? '' : currentUrl.pathname + currentUrl.hash;
|
||||
const urlQuery = currentUrl.search.substring(1);
|
||||
const urlDomain = currentUrl.hostname.replace(/^www./, '');
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export async function GET(
|
|||
}
|
||||
|
||||
const filters = {
|
||||
...getRequestFilters(query),
|
||||
...(await getRequestFilters(query)),
|
||||
startDate,
|
||||
endDate,
|
||||
timezone,
|
||||
|
|
|
|||
|
|
@ -48,7 +48,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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
67
src/app/api/websites/[websiteId]/segments/route.ts
Normal file
67
src/app/api/websites/[websiteId]/segments/route.ts
Normal 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);
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export async function GET(
|
|||
endDate,
|
||||
);
|
||||
|
||||
const filters = getRequestFilters(query);
|
||||
const filters = await getRequestFilters(query);
|
||||
|
||||
const metrics = await getWebsiteStats(websiteId, {
|
||||
...filters,
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue