Merge branch 'dev' of https://github.com/umami-software/umami into dev
Some checks are pending
Create docker images (cloud) / Build, push, and deploy (push) Waiting to run
Node.js CI / build (postgresql, 18.18, 10) (push) Waiting to run

This commit is contained in:
Mike Cao 2025-12-03 18:39:45 -08:00
commit 1483241494
93 changed files with 3147 additions and 1296 deletions

View file

@ -21,7 +21,11 @@ export function LinksTable(props: DataTableProps) {
<DataColumn id="slug" label={formatMessage(labels.link)}>
{({ slug }: any) => {
const url = getSlugUrl(slug);
return <ExternalLink href={url}>{url}</ExternalLink>;
return (
<ExternalLink href={url} prefetch={false}>
{url}
</ExternalLink>
);
}}
</DataColumn>
<DataColumn id="url" label={formatMessage(labels.destinationUrl)}>

View file

@ -1,4 +1,4 @@
import { Icon, Text } from '@umami/react-zen';
import { IconLabel } from '@umami/react-zen';
import { LinkButton } from '@/components/common/LinkButton';
import { PageHeader } from '@/components/common/PageHeader';
import { useLink, useMessages, useSlug } from '@/components/hooks';
@ -10,12 +10,9 @@ export function LinkHeader() {
const link = useLink();
return (
<PageHeader title={link.name} description={link.url} icon={<Link />} marginBottom="3">
<LinkButton href={getSlugUrl(link.slug)} target="_blank">
<Icon>
<ExternalLink />
</Icon>
<Text>{formatMessage(labels.view)}</Text>
<PageHeader title={link.name} description={link.url} icon={<Link />}>
<LinkButton href={getSlugUrl(link.slug)} target="_blank" prefetch={false} asAnchor>
<IconLabel icon={<ExternalLink />} label={formatMessage(labels.view)} />
</LinkButton>
</PageHeader>
);

View file

@ -21,7 +21,11 @@ export function PixelsTable(props: DataTableProps) {
<DataColumn id="url" label="URL">
{({ slug }: any) => {
const url = getSlugUrl(slug);
return <ExternalLink href={url}>{url}</ExternalLink>;
return (
<ExternalLink href={url} prefetch={false}>
{url}
</ExternalLink>
);
}}
</DataColumn>
<DataColumn id="created" label={formatMessage(labels.created)}>

View file

@ -1,4 +1,4 @@
import { Icon, Text } from '@umami/react-zen';
import { IconLabel } from '@umami/react-zen';
import { LinkButton } from '@/components/common/LinkButton';
import { PageHeader } from '@/components/common/PageHeader';
import { useMessages, usePixel, useSlug } from '@/components/hooks';
@ -10,12 +10,9 @@ export function PixelHeader() {
const pixel = usePixel();
return (
<PageHeader title={pixel.name} icon={<Grid2x2 />} marginBottom="3">
<LinkButton href={getSlugUrl(pixel.slug)} target="_blank" prefetch={false}>
<Icon>
<ExternalLink />
</Icon>
<Text>{formatMessage(labels.view)}</Text>
<PageHeader title={pixel.name} icon={<Grid2x2 />}>
<LinkButton href={getSlugUrl(pixel.slug)} target="_blank" prefetch={false} asAnchor>
<IconLabel icon={<ExternalLink />} label={formatMessage(labels.view)} />
</LinkButton>
</PageHeader>
);

View file

@ -1,6 +1,8 @@
import Link from 'next/link';
import { DataGrid } from '@/components/common/DataGrid';
import { useLoginQuery, useNavigation, useUserWebsitesQuery } from '@/components/hooks';
import { Favicon } from '@/index';
import { Icon, Row } from '@umami/react-zen';
import { WebsitesTable } from './WebsitesTable';
export function WebsitesDataTable({
@ -21,7 +23,12 @@ export function WebsitesDataTable({
const { renderUrl } = useNavigation();
const renderLink = (row: any) => (
<Link href={renderUrl(`/websites/${row.id}`, false)}>{row.name}</Link>
<Row alignItems="center" gap="3">
<Icon size="md" color="muted">
<Favicon domain={row.domain} />
</Icon>
<Link href={renderUrl(`/websites/${row.id}`, false)}>{row.name}</Link>
</Row>
);
return (

View file

@ -13,12 +13,18 @@ export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
const { renderUrl, pathname } = useNavigation();
const isSettings = pathname.endsWith('/settings');
const { formatMessage, labels } = useMessages();
if (isSettings) {
return null;
}
return (
<PageHeader title={website.name} icon={<Favicon domain={website.domain} />} marginBottom="3">
<PageHeader
title={website.name}
icon={<Favicon domain={website.domain} />}
titleHref={renderUrl(`/websites/${website.id}`, false)}
>
<Row alignItems="center" gap="6" wrap="wrap">
<ActiveUsers websiteId={website.id} />
@ -29,7 +35,7 @@ export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
<Icon>
<Edit />
</Icon>
<Text>Edit</Text>
<Text>{formatMessage(labels.edit)}</Text>
</LinkButton>
</Row>
)}

View file

@ -7,7 +7,7 @@ import { checkPassword } from '@/lib/password';
import redis from '@/lib/redis';
import { parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { getUserByUsername } from '@/queries/prisma';
import { getAllUserTeams, getUserByUsername } from '@/queries/prisma';
export async function POST(request: Request) {
const schema = z.object({
@ -39,8 +39,10 @@ export async function POST(request: Request) {
token = createSecureToken({ userId: user.id, role }, secret());
}
const teams = await getAllUserTeams(id);
return json({
token,
user: { id, username, role, createdAt, isAdmin: role === ROLES.admin },
user: { id, username, role, createdAt, isAdmin: role === ROLES.admin, teams },
});
}

View file

@ -17,6 +17,7 @@ export async function POST(request: Request) {
const errors = [];
let index = 0;
let cache = null;
for (const data of body) {
// Recreate a fresh Request since `new Request(request)` will have the following error:
// > Cannot read private member #state from an object whose class did not declare it
@ -33,9 +34,12 @@ export async function POST(request: Request) {
});
const response = await send.POST(newRequest);
const responseJson = await response.json();
if (!response.ok) {
errors.push({ index, response: await response.json() });
errors.push({ index, response: responseJson });
} else {
cache ??= responseJson.cache;
}
index++;
@ -46,6 +50,7 @@ export async function POST(request: Request) {
processed: body.length - errors.length,
errors: errors.length,
details: errors,
cache,
});
} catch (e) {
return serverError(e);

View file

@ -41,6 +41,9 @@ const schema = z.object({
userAgent: z.string().optional(),
timestamp: z.coerce.number().int().optional(),
id: z.string().optional(),
browser: z.string().optional(),
os: z.string().optional(),
device: z.string().optional(),
})
.refine(
data => {

View file

@ -25,8 +25,7 @@ export function LoginForm() {
onSuccess: async ({ token, user }) => {
setClientAuthToken(token);
setUser(user);
router.push('/websites');
router.push('/');
},
});
};

View file

@ -2,7 +2,7 @@
import { redirect } from 'next/navigation';
import { useEffect } from 'react';
import { LAST_TEAM_CONFIG } from '@/lib/constants';
import { getItem, removeItem } from '@/lib/storage';
import { getItem } from '@/lib/storage';
export default function RootPage() {
useEffect(() => {
@ -11,8 +11,6 @@ export default function RootPage() {
if (lastTeam) {
redirect(`/teams/${lastTeam}/websites`);
} else {
removeItem(LAST_TEAM_CONFIG);
redirect(`/websites`);
}
}, []);

View file

@ -1,5 +1,6 @@
'use client';
import { Column } from '@umami/react-zen';
import { Column, useTheme } from '@umami/react-zen';
import { useEffect } from 'react';
import { WebsiteHeader } from '@/app/(main)/websites/[websiteId]/WebsiteHeader';
import { WebsitePage } from '@/app/(main)/websites/[websiteId]/WebsitePage';
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
@ -10,6 +11,16 @@ import { Header } from './Header';
export function SharePage({ shareId }) {
const { shareToken, isLoading } = useShareTokenQuery(shareId);
const { setTheme } = useTheme();
useEffect(() => {
const url = new URL(window?.location?.href);
const theme = url.searchParams.get('theme');
if (theme === 'light' || theme === 'dark') {
setTheme(theme);
}
}, []);
if (isLoading || !shareToken) {
return null;