mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Merge branch 'dev' of https://github.com/umami-software/umami into dev
This commit is contained in:
commit
1483241494
93 changed files with 3147 additions and 1296 deletions
|
|
@ -1,8 +1,13 @@
|
|||
import { Icon, Row, Text } from '@umami/react-zen';
|
||||
import Link from 'next/link';
|
||||
import Link, { type LinkProps } from 'next/link';
|
||||
import type { ReactNode } from 'react';
|
||||
import { ExternalLink as LinkIcon } from '@/components/icons';
|
||||
|
||||
export function ExternalLink({ href, children, ...props }) {
|
||||
export function ExternalLink({
|
||||
href,
|
||||
children,
|
||||
...props
|
||||
}: LinkProps & { href: string; children: ReactNode }) {
|
||||
return (
|
||||
<Row alignItems="center" overflow="hidden" gap>
|
||||
<Text title={href} truncate>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export interface LinkButtonProps extends ButtonProps {
|
|||
scroll?: boolean;
|
||||
variant?: any;
|
||||
prefetch?: boolean;
|
||||
asAnchor?: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
|
|
@ -19,15 +20,22 @@ export function LinkButton({
|
|||
target,
|
||||
prefetch,
|
||||
children,
|
||||
asAnchor,
|
||||
...props
|
||||
}: LinkButtonProps) {
|
||||
const { dir } = useLocale();
|
||||
|
||||
return (
|
||||
<Button {...props} variant={variant} asChild>
|
||||
<Link href={href} dir={dir} scroll={scroll} target={target} prefetch={prefetch}>
|
||||
{children}
|
||||
</Link>
|
||||
{asAnchor ? (
|
||||
<a href={href} target={target}>
|
||||
{children}
|
||||
</a>
|
||||
) : (
|
||||
<Link href={href} dir={dir} scroll={scroll} target={target} prefetch={prefetch}>
|
||||
{children}
|
||||
</Link>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Column, Grid, Heading, Icon, Row, Text } from '@umami/react-zen';
|
||||
import type { ReactNode } from 'react';
|
||||
import { LinkButton } from './LinkButton';
|
||||
|
||||
export function PageHeader({
|
||||
title,
|
||||
|
|
@ -7,6 +8,7 @@ export function PageHeader({
|
|||
label,
|
||||
icon,
|
||||
showBorder = true,
|
||||
titleHref,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
|
|
@ -14,6 +16,7 @@ export function PageHeader({
|
|||
label?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
showBorder?: boolean;
|
||||
titleHref?: string;
|
||||
allowEdit?: boolean;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
|
|
@ -33,7 +36,13 @@ export function PageHeader({
|
|||
{icon}
|
||||
</Icon>
|
||||
)}
|
||||
{title && <Heading size={{ xs: '2', md: '3', lg: '4' }}>{title}</Heading>}
|
||||
{title && titleHref ? (
|
||||
<LinkButton href={titleHref} variant="quiet">
|
||||
<Heading size={{ xs: '2', md: '3', lg: '4' }}>{title}</Heading>
|
||||
</LinkButton>
|
||||
) : (
|
||||
title && <Heading size={{ xs: '2', md: '3', lg: '4' }}>{title}</Heading>
|
||||
)}
|
||||
</Row>
|
||||
{description && (
|
||||
<Text color="muted" truncate style={{ maxWidth: 600 }} title={description}>
|
||||
|
|
@ -41,7 +50,9 @@ export function PageHeader({
|
|||
</Text>
|
||||
)}
|
||||
</Column>
|
||||
<Row justifyContent="flex-end">{children}</Row>
|
||||
<Row justifyContent="flex-end" alignItems="center">
|
||||
{children}
|
||||
</Row>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
Text,
|
||||
} from '@umami/react-zen';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import type { Key } from 'react';
|
||||
import {
|
||||
useConfig,
|
||||
useLoginQuery,
|
||||
|
|
@ -33,7 +34,8 @@ import {
|
|||
Users,
|
||||
} from '@/components/icons';
|
||||
import { Switch } from '@/components/svg';
|
||||
import { DOCS_URL } from '@/lib/constants';
|
||||
import { DOCS_URL, LAST_TEAM_CONFIG } from '@/lib/constants';
|
||||
import { removeItem } from '@/lib/storage';
|
||||
|
||||
export interface TeamsButtonProps {
|
||||
showText?: boolean;
|
||||
|
|
@ -44,7 +46,7 @@ export function NavButton({ showText = true }: TeamsButtonProps) {
|
|||
const { user } = useLoginQuery();
|
||||
const { cloudMode } = useConfig();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { teamId } = useNavigation();
|
||||
const { teamId, router } = useNavigation();
|
||||
const { isMobile } = useMobile();
|
||||
const team = user?.teams?.find(({ id }) => id === teamId);
|
||||
const selectedKeys = new Set([teamId || 'user']);
|
||||
|
|
@ -54,7 +56,16 @@ export function NavButton({ showText = true }: TeamsButtonProps) {
|
|||
return cloudMode ? `${process.env.cloudUrl}${url}` : url;
|
||||
};
|
||||
|
||||
const handleAction = async () => {};
|
||||
const handleAction = async (key: Key) => {
|
||||
if (key === 'user') {
|
||||
removeItem(LAST_TEAM_CONFIG);
|
||||
if (cloudMode) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
router.push('/');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<MenuTrigger>
|
||||
|
|
@ -84,16 +95,16 @@ export function NavButton({ showText = true }: TeamsButtonProps) {
|
|||
</Pressable>
|
||||
<Popover placement="bottom start">
|
||||
<Column minWidth="300px">
|
||||
<Menu autoFocus="last" onAction={handleAction}>
|
||||
<Menu autoFocus="last">
|
||||
<SubmenuTrigger>
|
||||
<MenuItem id="teams" showChecked={false} showSubMenuIcon>
|
||||
<IconLabel icon={<Switch />} label={formatMessage(labels.switchAccount)} />
|
||||
</MenuItem>
|
||||
<Popover placement={isMobile ? 'bottom start' : 'right top'}>
|
||||
<Column minWidth="300px">
|
||||
<Menu selectionMode="single" selectedKeys={selectedKeys}>
|
||||
<Menu selectionMode="single" selectedKeys={selectedKeys} onAction={handleAction}>
|
||||
<MenuSection title={formatMessage(labels.myAccount)}>
|
||||
<MenuItem id="user" href={getUrl('/')}>
|
||||
<MenuItem id="user">
|
||||
<IconLabel icon={<User />} label={user.username} />
|
||||
</MenuItem>
|
||||
</MenuSection>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Button, Icon, ListItem, Row, Select, Text } from '@umami/react-zen';
|
||||
import { isAfter } from 'date-fns';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useDateRange, useDateRangeQuery, useMessages, useNavigation } from '@/components/hooks';
|
||||
import { ChevronRight } from '@/components/icons';
|
||||
import { getDateRangeValue } from '@/lib/date';
|
||||
|
|
@ -45,13 +45,9 @@ export function WebsiteDateFilter({
|
|||
}
|
||||
};
|
||||
|
||||
const handleIncrement = useCallback(
|
||||
(increment: number) => {
|
||||
router.push(updateParams({ offset: +offset + increment }));
|
||||
},
|
||||
[offset],
|
||||
);
|
||||
|
||||
const handleIncrement = increment => {
|
||||
router.push(updateParams({ offset: Number(offset) + increment }));
|
||||
};
|
||||
const handleSelect = (compare: any) => {
|
||||
router.push(updateParams({ compare }));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { StatusLight, Text } from '@umami/react-zen';
|
||||
import { useMemo } from 'react';
|
||||
import { LinkButton } from '@/components/common/LinkButton';
|
||||
import { useActyiveUsersQuery, useMessages } from '@/components/hooks';
|
||||
|
||||
export function ActiveUsers({
|
||||
|
|
@ -27,10 +28,12 @@ export function ActiveUsers({
|
|||
}
|
||||
|
||||
return (
|
||||
<StatusLight variant="success">
|
||||
<Text size="2" weight="medium">
|
||||
{count} {formatMessage(labels.online)}
|
||||
</Text>
|
||||
</StatusLight>
|
||||
<LinkButton href={`/websites/${websiteId}/realtime`} variant="quiet">
|
||||
<StatusLight variant="success">
|
||||
<Text size="2" weight="medium">
|
||||
{count} {formatMessage(labels.online)}
|
||||
</Text>
|
||||
</StatusLight>
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,12 @@ export function Legend({
|
|||
return (
|
||||
<Row key={text} onClick={() => onClick(item)}>
|
||||
<StatusLight color={color.alpha(color.alpha() + 0.2).toHex()}>
|
||||
<Text size="2" color={hidden ? 'disabled' : undefined} wrap="nowrap">
|
||||
<Text
|
||||
size="2"
|
||||
color={hidden ? 'disabled' : undefined}
|
||||
truncate={true}
|
||||
style={{ maxWidth: '300px' }}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
</StatusLight>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export function RealtimeChart({ data, unit, ...props }: RealtimeChartProps) {
|
|||
const endDate = startOfMinute(new Date());
|
||||
const startDate = subMinutes(endDate, REALTIME_RANGE);
|
||||
const prevEndDate = useRef(endDate);
|
||||
const prevData = useRef<string | null>(null);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (!data) {
|
||||
|
|
@ -28,14 +29,22 @@ export function RealtimeChart({ data, unit, ...props }: RealtimeChartProps) {
|
|||
};
|
||||
}, [data, startDate, endDate, unit]);
|
||||
|
||||
// Don't animate the bars shifting over because it looks weird
|
||||
const animationDuration = useMemo(() => {
|
||||
// Don't animate the bars shifting over because it looks weird
|
||||
if (isBefore(prevEndDate.current, endDate)) {
|
||||
prevEndDate.current = endDate;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Don't animate when data hasn't changed
|
||||
const serialized = JSON.stringify(chartData);
|
||||
if (prevData.current === serialized) {
|
||||
return 0;
|
||||
}
|
||||
prevData.current = serialized;
|
||||
|
||||
return DEFAULT_ANIMATION_DURATION;
|
||||
}, [endDate]);
|
||||
}, [endDate, chartData]);
|
||||
|
||||
return (
|
||||
<PageviewsChart
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue