mirror of
https://github.com/umami-software/umami.git
synced 2025-12-06 01:18:00 +01:00
Fixed editing and navigation issues.
This commit is contained in:
parent
bf6c9395c6
commit
8c26e310f7
52 changed files with 118 additions and 122 deletions
|
|
@ -199,18 +199,6 @@ export default {
|
|||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
functions: {
|
||||
'app/api/**/*.js': {
|
||||
maxDuration: 30,
|
||||
},
|
||||
},
|
||||
outputFileTracing: {
|
||||
include: [
|
||||
'src/generated/prisma/**/*',
|
||||
'node_modules/@prisma/client/**/*',
|
||||
'node_modules/.prisma/client/**/*',
|
||||
],
|
||||
},
|
||||
async headers() {
|
||||
return headers;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import Script from 'next/script';
|
|||
import { usePathname } from 'next/navigation';
|
||||
import { UpdateNotice } from './UpdateNotice';
|
||||
import { SideNav } from '@/app/(main)/SideNav';
|
||||
import { MenuBar } from '@/app/(main)/MenuBar';
|
||||
import { TopNav } from '@/app/(main)/TopNav';
|
||||
import { useLoginQuery, useConfig } from '@/components/hooks';
|
||||
|
||||
export function App({ children }) {
|
||||
|
|
@ -35,7 +35,7 @@ export function App({ children }) {
|
|||
<SideNav />
|
||||
</Column>
|
||||
<Row gridColumn="2 / 3" gridRow="1 / 2">
|
||||
<MenuBar />
|
||||
<TopNav />
|
||||
</Row>
|
||||
<Column
|
||||
gridColumn="2 / 3"
|
||||
|
|
|
|||
|
|
@ -6,11 +6,10 @@ import { WebsiteSelect } from '@/components/input/WebsiteSelect';
|
|||
import { PanelLeft, Slash } from '@/components/icons';
|
||||
import { useNavigation, useGlobalState } from '@/components/hooks';
|
||||
|
||||
export function MenuBar() {
|
||||
export function TopNav() {
|
||||
const [isCollapsed, setCollapsed] = useGlobalState('sidenav-collapsed');
|
||||
const { teamId, websiteId } = useNavigation();
|
||||
|
||||
const handleSelect = () => {};
|
||||
const { teamId, websiteId, pathname } = useNavigation();
|
||||
const isSettings = pathname.includes('/settings');
|
||||
|
||||
return (
|
||||
<Row
|
||||
|
|
@ -30,17 +29,12 @@ export function MenuBar() {
|
|||
</Button>
|
||||
<Row alignItems="center" gap="1">
|
||||
<TeamsButton />
|
||||
{websiteId && (
|
||||
{websiteId && !isSettings && (
|
||||
<>
|
||||
<Icon strokeColor="7" rotate={-25}>
|
||||
<Slash />
|
||||
</Icon>
|
||||
<WebsiteSelect
|
||||
variant="quiet"
|
||||
websiteId={websiteId}
|
||||
teamId={teamId}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
<WebsiteSelect variant="quiet" websiteId={websiteId} teamId={teamId} />
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
|
|
@ -21,6 +21,7 @@ export function UserDeleteForm({
|
|||
mutate(null, {
|
||||
onSuccess: async () => {
|
||||
touch('users');
|
||||
touch(`users:${userId}`);
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
|
|
@ -35,9 +36,7 @@ export function UserDeleteForm({
|
|||
confirmLabel={formatMessage(labels.delete)}
|
||||
isDanger
|
||||
>
|
||||
<Row gap="1">
|
||||
{formatMessage(messages.confirmDelete, { target: <b key={username}>{username}</b> })}
|
||||
</Row>
|
||||
<Row gap="1">{formatMessage(messages.confirmDelete, { target: username })}</Row>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Row, Text, Icon, DataTable, DataColumn, MenuItem, Modal } from '@umami/react-zen';
|
||||
import { Row, Text, Icon, DataTable, DataColumn, MenuItem, Modal, Dialog } from '@umami/react-zen';
|
||||
import { Trash, Users } from '@/components/icons';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { Edit } from '@/components/icons';
|
||||
import { MenuButton } from '@/components/input/MenuButton';
|
||||
import { DateDistance } from '@/components/common/DateDistance';
|
||||
import { WebsiteDeleteForm } from '@/app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm';
|
||||
|
||||
export function AdminWebsitesTable({ data = [] }: { data: any[] }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [deleteUser, setDeleteUser] = useState(null);
|
||||
const [deleteWebsite, setDeleteWebsite] = useState(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -64,7 +65,7 @@ export function AdminWebsitesTable({ data = [] }: { data: any[] }) {
|
|||
</MenuItem>
|
||||
<MenuItem
|
||||
id="delete"
|
||||
onAction={() => setDeleteUser(row)}
|
||||
onAction={() => setDeleteWebsite(id)}
|
||||
data-test="link-button-delete"
|
||||
>
|
||||
<Row alignItems="center" gap>
|
||||
|
|
@ -79,7 +80,11 @@ export function AdminWebsitesTable({ data = [] }: { data: any[] }) {
|
|||
}}
|
||||
</DataColumn>
|
||||
</DataTable>
|
||||
<Modal isOpen={!!deleteUser}></Modal>
|
||||
<Modal isOpen={!!deleteWebsite}>
|
||||
<Dialog style={{ width: 400 }}>
|
||||
<WebsiteDeleteForm websiteId={deleteWebsite} onClose={() => setDeleteWebsite(null)} />
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export function SettingsLayout({ children }: { children: ReactNode }) {
|
|||
<SideMenu items={items} selectedKey={value} />
|
||||
</Column>
|
||||
<Column>
|
||||
<Panel>{children}</Panel>
|
||||
<Panel minHeight="300px">{children}</Panel>
|
||||
</Column>
|
||||
</Grid>
|
||||
</Column>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useContext, useState } from 'react';
|
||||
import { Column, Tabs, TabList, Tab, TabPanel } from '@umami/react-zen';
|
||||
import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider';
|
||||
import { useLoginQuery, useMessages } from '@/components/hooks';
|
||||
import { useLoginQuery, useMessages, useNavigation } from '@/components/hooks';
|
||||
import { SectionHeader } from '@/components/common/SectionHeader';
|
||||
import { ROLES } from '@/lib/constants';
|
||||
import { Users } from '@/components/icons';
|
||||
|
|
@ -15,7 +15,10 @@ export function TeamDetails({ teamId }: { teamId: string }) {
|
|||
const team = useContext(TeamContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useLoginQuery();
|
||||
const [tab, setTab] = useState('details');
|
||||
const { query, pathname } = useNavigation();
|
||||
const [tab, setTab] = useState(query?.tab || 'details');
|
||||
|
||||
const isAdmin = pathname.includes('/admin');
|
||||
|
||||
const isTeamOwner =
|
||||
!!team?.teamUser?.find(({ userId, role }) => role === ROLES.teamOwner && userId === user.id) &&
|
||||
|
|
@ -32,7 +35,7 @@ export function TeamDetails({ teamId }: { teamId: string }) {
|
|||
return (
|
||||
<Column gap>
|
||||
<SectionHeader title={team?.name} icon={<Users />}>
|
||||
{!isTeamOwner && <TeamLeaveButton teamId={team.id} teamName={team.name} />}
|
||||
{!isTeamOwner && !isAdmin && <TeamLeaveButton teamId={team.id} teamName={team.name} />}
|
||||
</SectionHeader>
|
||||
<Tabs selectedKey={tab} onSelectionChange={(value: any) => setTab(value)}>
|
||||
<TabList>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ export function TeamMembersTable({
|
|||
{allowEdit && (
|
||||
<DataColumn id="action" align="end">
|
||||
{(row: any) => {
|
||||
if (row?.role === ROLES.teamOwner) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Row alignItems="center">
|
||||
<TeamMemberEditButton teamId={teamId} userId={row?.user?.id} role={row?.role} />
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export function WebsitesTable({
|
|||
</MenuItem>
|
||||
)}
|
||||
{allowEdit && (
|
||||
<MenuItem href={renderUrl(`/settings/websites/${websiteId}`)}>
|
||||
<MenuItem href={`/settings/websites/${websiteId}`}>
|
||||
<Row alignItems="center" gap>
|
||||
<Icon data-test="link-button-edit">
|
||||
<SquarePen />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useApi, useMessages } from '@/components/hooks';
|
||||
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||
import { TypeConfirmationForm } from '@/components/common/TypeConfirmationForm';
|
||||
|
||||
const CONFIRM_VALUE = 'DELETE';
|
||||
|
|
@ -17,10 +17,13 @@ export function WebsiteDeleteForm({
|
|||
const { mutate, isPending, error } = useMutation({
|
||||
mutationFn: () => del(`/websites/${websiteId}`),
|
||||
});
|
||||
const { touch } = useModified();
|
||||
|
||||
const handleConfirm = async () => {
|
||||
mutate(null, {
|
||||
onSuccess: async () => {
|
||||
touch('websites');
|
||||
touch(`websites:${websiteId}`);
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ export function WebsiteShareForm({ websiteId, shareId, onSave, onClose }: Websit
|
|||
<Row>
|
||||
{id && <Button onPress={handleGenerate}>{formatMessage(labels.regenerate)}</Button>}
|
||||
</Row>
|
||||
<Row>
|
||||
<Row alignItems="center" gap>
|
||||
{onClose && <Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>}
|
||||
<FormSubmitButton isDisabled={false} isLoading={isPending}>
|
||||
{formatMessage(labels.save)}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export function WebsitesPage() {
|
|||
<WebsiteAddButton teamId={teamId} />
|
||||
</PageHeader>
|
||||
<Panel>
|
||||
<WebsitesDataTable teamId={teamId} allowEdit={false} />
|
||||
<WebsitesDataTable teamId={teamId} />
|
||||
</Panel>
|
||||
</Column>
|
||||
</PageBody>
|
||||
|
|
|
|||
|
|
@ -5,12 +5,11 @@ import { Share, Edit } from '@/components/icons';
|
|||
import { Favicon } from '@/components/common/Favicon';
|
||||
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
|
||||
import { WebsiteShareForm } from '@/app/(main)/settings/websites/[websiteId]/WebsiteShareForm';
|
||||
import { useMessages, useNavigation } from '@/components/hooks';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { LinkButton } from '@/components/common/LinkButton';
|
||||
|
||||
export function WebsiteHeader() {
|
||||
const website = useWebsite();
|
||||
const { renderUrl } = useNavigation();
|
||||
|
||||
return (
|
||||
<PageHeader title={website.name} icon={<Favicon domain={website.domain} />} showBorder={false}>
|
||||
|
|
@ -18,7 +17,7 @@ export function WebsiteHeader() {
|
|||
<ActiveUsers websiteId={website.id} />
|
||||
<Row alignItems="center" gap>
|
||||
<ShareButton websiteId={website.id} shareId={website.shareId} />
|
||||
<LinkButton href={renderUrl(`/settings/websites/${website.id}`)}>
|
||||
<LinkButton href={`/settings/websites/${website.id}`}>
|
||||
<Icon>
|
||||
<Edit />
|
||||
</Icon>
|
||||
|
|
@ -42,7 +41,7 @@ const ShareButton = ({ websiteId, shareId }) => {
|
|||
<Text>Share</Text>
|
||||
</Button>
|
||||
<Modal>
|
||||
<Dialog title={formatMessage(labels.share)} style={{ width: 400 }}>
|
||||
<Dialog title={formatMessage(labels.share)} style={{ width: 600 }}>
|
||||
{({ close }) => {
|
||||
return <WebsiteShareForm websiteId={websiteId} shareId={shareId} onClose={close} />;
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export async function GET(
|
|||
return unauthorized();
|
||||
}
|
||||
|
||||
const filters = getQueryFilters({
|
||||
const filters = await getQueryFilters({
|
||||
...query,
|
||||
websiteId,
|
||||
startAt: subMinutes(startOfMinute(new Date()), REALTIME_RANGE).getTime(),
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export async function POST(request: Request) {
|
|||
}
|
||||
|
||||
const parameters = await setWebsiteDate(websiteId, body.parameters);
|
||||
const filters = getQueryFilters(body.filters);
|
||||
const filters = await getQueryFilters(body.filters);
|
||||
|
||||
const data = await getAttribution(websiteId, parameters as AttributionParameters, filters);
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export async function POST(request: Request) {
|
|||
}
|
||||
|
||||
const parameters = await setWebsiteDate(websiteId, body.parameters);
|
||||
const filters = getQueryFilters(body.filters);
|
||||
const filters = await getQueryFilters(body.filters);
|
||||
|
||||
const data = await getBreakdown(websiteId, parameters as BreakdownParameters, filters);
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export async function POST(request: Request) {
|
|||
}
|
||||
|
||||
const parameters = await setWebsiteDate(websiteId, body.parameters);
|
||||
const filters = getQueryFilters(body.filters);
|
||||
const filters = await getQueryFilters(body.filters);
|
||||
|
||||
const data = await getFunnel(websiteId, parameters as FunnelParameters, filters);
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export async function POST(request: Request) {
|
|||
}
|
||||
|
||||
const parameters = await setWebsiteDate(websiteId, body.parameters);
|
||||
const filters = getQueryFilters(body.filters);
|
||||
const filters = await getQueryFilters(body.filters);
|
||||
|
||||
const data = await getGoal(websiteId, parameters as GoalParameters, filters);
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export async function POST(request: Request) {
|
|||
return unauthorized();
|
||||
}
|
||||
|
||||
const queryFilters = await setWebsiteDate(websiteId, getQueryFilters(filters));
|
||||
const queryFilters = await setWebsiteDate(websiteId, await getQueryFilters(filters));
|
||||
|
||||
const data = await getJourney(websiteId, parameters, queryFilters);
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export async function POST(request: Request) {
|
|||
return unauthorized();
|
||||
}
|
||||
|
||||
const filters = getQueryFilters(body.filters);
|
||||
const filters = await getQueryFilters(body.filters);
|
||||
const parameters = await setWebsiteDate(websiteId, body.parameters);
|
||||
|
||||
const data = await getRetention(websiteId, parameters as RetentionParameters, filters);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export async function POST(request: Request) {
|
|||
}
|
||||
|
||||
const parameters = await setWebsiteDate(websiteId, body.parameters);
|
||||
const filters = getQueryFilters(body.filters);
|
||||
const filters = await getQueryFilters(body.filters);
|
||||
|
||||
const data = await getRevenue(websiteId, parameters as RevenuParameters, filters);
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export async function POST(request: Request) {
|
|||
}
|
||||
|
||||
const parameters = await setWebsiteDate(websiteId, body.parameters);
|
||||
const filters = getQueryFilters(body.filters);
|
||||
const filters = await getQueryFilters(body.filters);
|
||||
|
||||
const data = await getUTM(websiteId, parameters as UTMParameters, filters);
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ team
|
|||
return unauthorized('You must be the owner of this team.');
|
||||
}
|
||||
|
||||
const filters = getQueryFilters(query);
|
||||
const filters = await getQueryFilters(query);
|
||||
|
||||
const users = await getTeamUsers(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ user
|
|||
}
|
||||
|
||||
const { userId } = await params;
|
||||
const filters = getQueryFilters(query);
|
||||
const filters = await getQueryFilters(query);
|
||||
|
||||
const websites = await getAllUserWebsitesIncludingTeamOwner(userId);
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export async function GET(
|
|||
}
|
||||
|
||||
const { event } = query;
|
||||
const filters = getQueryFilters(query);
|
||||
const filters = await getQueryFilters(query);
|
||||
|
||||
const data = await getEventDataEvents(websiteId, {
|
||||
...filters,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export async function GET(
|
|||
return unauthorized();
|
||||
}
|
||||
|
||||
const filters = getQueryFilters(query);
|
||||
const filters = await getQueryFilters(query);
|
||||
|
||||
const data = await getEventDataFields(websiteId, filters);
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export async function GET(
|
|||
}
|
||||
|
||||
const { propertyName } = query;
|
||||
const filters = getQueryFilters(query);
|
||||
const filters = await getQueryFilters(query);
|
||||
|
||||
const data = await getEventDataProperties(websiteId, { ...filters, propertyName });
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export async function GET(
|
|||
return unauthorized();
|
||||
}
|
||||
|
||||
const filters = getQueryFilters(query);
|
||||
const filters = await getQueryFilters(query);
|
||||
|
||||
const data = await getEventDataStats(websiteId, filters);
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export async function GET(
|
|||
}
|
||||
|
||||
const { eventName, propertyName } = query;
|
||||
const filters = getQueryFilters(query);
|
||||
const filters = await getQueryFilters(query);
|
||||
|
||||
const data = await getEventDataValues(websiteId, {
|
||||
...filters,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export async function GET(
|
|||
return unauthorized();
|
||||
}
|
||||
|
||||
const filters = getQueryFilters(query);
|
||||
const filters = await getQueryFilters(query);
|
||||
|
||||
const data = await getWebsiteEvents(websiteId, filters);
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export async function GET(
|
|||
return unauthorized();
|
||||
}
|
||||
|
||||
const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
|
||||
const filters = await setWebsiteDate(websiteId, await getQueryFilters(query));
|
||||
|
||||
const data = await getEventMetrics(websiteId, filters);
|
||||
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ import {
|
|||
VIDEO_DOMAINS,
|
||||
PAID_AD_PARAMS,
|
||||
} from '@/lib/constants';
|
||||
import { parseRequest, getQueryFilters, setWebsiteDate } from '@/lib/request';
|
||||
import { parseRequest, getQueryFilters } from '@/lib/request';
|
||||
import { json, unauthorized, badRequest } from '@/lib/response';
|
||||
import { getPageviewMetrics, getSessionMetrics, getChannelMetrics } from '@/queries';
|
||||
import { filterParams } from '@/lib/schema';
|
||||
import { dateRangeParams, filterParams, searchParams } from '@/lib/schema';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
|
|
@ -22,11 +22,10 @@ export async function GET(
|
|||
) {
|
||||
const schema = z.object({
|
||||
type: z.string(),
|
||||
startAt: z.coerce.number().int(),
|
||||
endAt: z.coerce.number().int(),
|
||||
limit: z.coerce.number().optional(),
|
||||
offset: z.coerce.number().optional(),
|
||||
search: z.string().optional(),
|
||||
...dateRangeParams,
|
||||
...searchParams,
|
||||
...filterParams,
|
||||
});
|
||||
|
||||
|
|
@ -37,13 +36,13 @@ export async function GET(
|
|||
}
|
||||
|
||||
const { websiteId } = await params;
|
||||
const { type, limit, offset, search } = query;
|
||||
|
||||
if (!(await canViewWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
|
||||
const { type, limit, offset, search } = query;
|
||||
const filters = await getQueryFilters(query, websiteId);
|
||||
|
||||
if (search) {
|
||||
filters[type] = `c.${search}`;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { z } from 'zod';
|
||||
import { canViewWebsite } from '@/lib/auth';
|
||||
import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request';
|
||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||
import { dateRangeParams, filterParams } from '@/lib/schema';
|
||||
import { getCompareDate } from '@/lib/date';
|
||||
import { unauthorized, json } from '@/lib/response';
|
||||
|
|
@ -27,7 +27,7 @@ export async function GET(
|
|||
return unauthorized();
|
||||
}
|
||||
|
||||
const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
|
||||
const filters = await getQueryFilters(query, websiteId);
|
||||
|
||||
const [pageviews, sessions] = await Promise.all([
|
||||
getPageviewStats(websiteId, filters),
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export async function GET(
|
|||
|
||||
const { websiteId } = await params;
|
||||
const { propertyName } = query;
|
||||
const filters = getQueryFilters(query);
|
||||
const filters = await getQueryFilters(query);
|
||||
|
||||
if (!(await canViewWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export async function GET(
|
|||
|
||||
const { propertyName } = query;
|
||||
const { websiteId } = await params;
|
||||
const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
|
||||
const filters = await setWebsiteDate(websiteId, await getQueryFilters(query));
|
||||
|
||||
if (!(await canViewWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export async function GET(
|
|||
return unauthorized();
|
||||
}
|
||||
|
||||
const filters = getQueryFilters(query);
|
||||
const filters = await getQueryFilters(query);
|
||||
|
||||
const data = await getSessionActivity(websiteId, sessionId, filters);
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export async function GET(
|
|||
return unauthorized();
|
||||
}
|
||||
|
||||
const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
|
||||
const filters = await setWebsiteDate(websiteId, await getQueryFilters(query));
|
||||
|
||||
const data = await getWebsiteSessions(websiteId, filters);
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export async function GET(
|
|||
return unauthorized();
|
||||
}
|
||||
|
||||
const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
|
||||
const filters = await setWebsiteDate(websiteId, await getQueryFilters(query));
|
||||
|
||||
const metrics = await getWebsiteSessionStats(websiteId, filters);
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export async function GET(
|
|||
return unauthorized();
|
||||
}
|
||||
|
||||
const filters = getQueryFilters(query);
|
||||
const filters = await getQueryFilters(query);
|
||||
|
||||
const data = await getWebsiteSessionsWeekly(websiteId, filters);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { z } from 'zod';
|
||||
import { parseRequest, getQueryFilters, setWebsiteDate } from '@/lib/request';
|
||||
import { parseRequest, getQueryFilters } from '@/lib/request';
|
||||
import { unauthorized, json } from '@/lib/response';
|
||||
import { canViewWebsite } from '@/lib/auth';
|
||||
import { filterParams } from '@/lib/schema';
|
||||
import { dateRangeParams, filterParams } from '@/lib/schema';
|
||||
import { getWebsiteStats } from '@/queries';
|
||||
import { getCompareDate } from '@/lib/date';
|
||||
|
||||
|
|
@ -11,9 +11,8 @@ export async function GET(
|
|||
{ params }: { params: Promise<{ websiteId: string }> },
|
||||
) {
|
||||
const schema = z.object({
|
||||
startAt: z.coerce.number().int(),
|
||||
endAt: z.coerce.number().int(),
|
||||
compare: z.string().optional(),
|
||||
...dateRangeParams,
|
||||
...filterParams,
|
||||
});
|
||||
|
||||
|
|
@ -29,7 +28,7 @@ export async function GET(
|
|||
return unauthorized();
|
||||
}
|
||||
|
||||
const filters = await setWebsiteDate(websiteId, getQueryFilters(query));
|
||||
const filters = await getQueryFilters(query, websiteId);
|
||||
|
||||
const data = await getWebsiteStats(websiteId, filters);
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export async function GET(
|
|||
if (FILTER_GROUPS[type]) {
|
||||
values = (await getWebsiteSegments(websiteId, type)).map(segment => ({ value: segment.name }));
|
||||
} else {
|
||||
const filters = getQueryFilters(query);
|
||||
const filters = await getQueryFilters(query);
|
||||
values = await getValues(websiteId, FILTER_COLUMNS[type], filters);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,16 +42,18 @@ export function Pager({ page, pageSize, count, onPageChange }: PagerProps) {
|
|||
total: maxPage.toLocaleString(),
|
||||
})}
|
||||
</Text>
|
||||
<Button onPress={() => handlePageChange(-1)} isDisabled={firstPage}>
|
||||
<Icon size="sm" rotate={180}>
|
||||
<Chevron />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Button onPress={() => handlePageChange(1)} isDisabled={lastPage}>
|
||||
<Icon size="sm">
|
||||
<Chevron />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Row gap="1">
|
||||
<Button variant="outline" onPress={() => handlePageChange(-1)} isDisabled={firstPage}>
|
||||
<Icon size="sm" rotate={180}>
|
||||
<Chevron />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Button variant="outline" onPress={() => handlePageChange(1)} isDisabled={lastPage}>
|
||||
<Icon size="sm">
|
||||
<Chevron />
|
||||
</Icon>
|
||||
</Button>
|
||||
</Row>
|
||||
</Row>
|
||||
</Row>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ export function useTeamsQuery(params?: Record<string, any>, options?: ReactQuery
|
|||
queryKey: ['teams:admin', { modified, ...params }],
|
||||
queryFn: pageParams => {
|
||||
return get(`/admin/teams`, {
|
||||
...params,
|
||||
...pageParams,
|
||||
...params,
|
||||
});
|
||||
},
|
||||
...options,
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ export function useUserWebsitesQuery(
|
|||
queryKey: ['websites', { userId, teamId, modified, ...params }],
|
||||
queryFn: pageParams => {
|
||||
return get(teamId ? `/teams/${teamId}/websites` : `/users/${userId || user.id}/websites`, {
|
||||
...params,
|
||||
...pageParams,
|
||||
...params,
|
||||
});
|
||||
},
|
||||
...options,
|
||||
|
|
|
|||
|
|
@ -1,28 +1,24 @@
|
|||
import { useState } from 'react';
|
||||
import { Select, SelectProps, ListItem } from '@umami/react-zen';
|
||||
import { useUserWebsitesQuery, useMessages } from '@/components/hooks';
|
||||
import { useUserWebsitesQuery, useWebsiteQuery, useNavigation } from '@/components/hooks';
|
||||
|
||||
export function WebsiteSelect({
|
||||
websiteId,
|
||||
teamId,
|
||||
variant,
|
||||
onSelect,
|
||||
...props
|
||||
}: {
|
||||
websiteId?: string;
|
||||
teamId?: string;
|
||||
variant?: 'primary' | 'outline' | 'quiet' | 'danger' | 'zero';
|
||||
onSelect?: (key: any) => void;
|
||||
} & SelectProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { router, renderUrl } = useNavigation();
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedId, setSelectedId] = useState(websiteId);
|
||||
|
||||
const { data: website } = useWebsiteQuery(websiteId);
|
||||
const { data, isLoading } = useUserWebsitesQuery({ teamId }, { search, pageSize: 5 });
|
||||
|
||||
const handleSelect = (value: any) => {
|
||||
setSelectedId(value);
|
||||
onSelect?.(value);
|
||||
router.push(renderUrl(`/websites/${value}`));
|
||||
};
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
|
|
@ -33,14 +29,14 @@ export function WebsiteSelect({
|
|||
<Select
|
||||
{...props}
|
||||
items={data?.['data'] || []}
|
||||
value={selectedId}
|
||||
placeholder={formatMessage(labels.selectWebsite)}
|
||||
value={websiteId}
|
||||
isLoading={isLoading}
|
||||
buttonProps={{ variant }}
|
||||
allowSearch={true}
|
||||
searchValue={search}
|
||||
onSearch={handleSearch}
|
||||
onChange={handleSelect}
|
||||
renderValue={() => website?.name}
|
||||
>
|
||||
{({ id, name }: any) => <ListItem key={id}>{name}</ListItem>}
|
||||
</Select>
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export function getRequestDateRange(query: Record<string, string>) {
|
|||
};
|
||||
}
|
||||
|
||||
export async function getRequestFilters(query: Record<string, any>, websiteId?: string) {
|
||||
export function getRequestFilters(query: Record<string, any>) {
|
||||
const result: Record<string, any> = {};
|
||||
|
||||
for (const key of Object.keys(FILTER_COLUMNS)) {
|
||||
|
|
@ -76,20 +76,17 @@ export async function getRequestFilters(query: Record<string, any>, websiteId?:
|
|||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getRequestSegments(websiteId: string, query: Record<string, any>) {
|
||||
for (const key of Object.keys(FILTER_GROUPS)) {
|
||||
const value = query[key];
|
||||
|
||||
if (value !== undefined) {
|
||||
const segment = await getWebsiteSegment(websiteId, key, value);
|
||||
if (key === 'segment') {
|
||||
// merge filters into result
|
||||
Object.assign(result, segment.parameters);
|
||||
} else {
|
||||
result[key] = segment.parameters;
|
||||
}
|
||||
return getWebsiteSegment(websiteId, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function setWebsiteDate(websiteId: string, data: Record<string, any>) {
|
||||
|
|
@ -102,13 +99,18 @@ export async function setWebsiteDate(websiteId: string, data: Record<string, any
|
|||
return data;
|
||||
}
|
||||
|
||||
export function getQueryFilters(params: Record<string, any>): QueryFilters {
|
||||
const dateRange = getRequestDateRange(params);
|
||||
export async function getQueryFilters(
|
||||
params: Record<string, any>,
|
||||
websiteId?: string,
|
||||
): Promise<QueryFilters> {
|
||||
const dateRange = await setWebsiteDate(websiteId, getRequestDateRange(params));
|
||||
const filters = getRequestFilters(params);
|
||||
const segments = await getRequestSegments(websiteId, params);
|
||||
|
||||
return {
|
||||
...dateRange,
|
||||
...filters,
|
||||
...segments,
|
||||
page: params?.page,
|
||||
pageSize: params?.page ? params?.pageSize || DEFAULT_PAGE_SIZE : undefined,
|
||||
orderBy: params?.orderBy,
|
||||
|
|
|
|||
|
|
@ -71,6 +71,9 @@ export interface FilterParams {
|
|||
search?: string;
|
||||
tag?: string;
|
||||
eventType?: number;
|
||||
segment?: string;
|
||||
cohort?: string;
|
||||
compare?: string;
|
||||
}
|
||||
|
||||
export interface SortParams {
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ async function clickhouseQuery(
|
|||
from (
|
||||
select arrayJoin(event_name) as event_name,
|
||||
created_at
|
||||
from website_event_stats_hourly website_event
|
||||
from website_event_stats_hourly as website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = {eventType:UInt32}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ async function clickhouseQuery(
|
|||
select
|
||||
${getDateSQL('website_event.created_at', unit, timezone)} as t,
|
||||
sum(views) as y
|
||||
from website_event_stats_hourly website_event
|
||||
from website_event_stats_hourly as website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ async function clickhouseQuery(
|
|||
${column} x,
|
||||
uniq(session_id) y
|
||||
${includeCountry ? ', country' : ''}
|
||||
from website_event_stats_hourly website_event
|
||||
from website_event_stats_hourly as website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ async function clickhouseQuery(
|
|||
select
|
||||
${getDateSQL('website_event.created_at', unit, timezone)} as t,
|
||||
uniq(session_id) as y
|
||||
from website_event_stats_hourly website_event
|
||||
from website_event_stats_hourly as website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = {eventType:UInt32}
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
|
|||
uniq(visit_id) as visits,
|
||||
sumIf(views, event_type = 1) as views,
|
||||
lastAt as createdAt
|
||||
from website_event_stats_hourly website_event
|
||||
from website_event_stats_hourly as website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
${dateQuery}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue