Compare commits

...

13 commits

Author SHA1 Message Date
Mike Cao
27c342811e Added label to PageHeader. Style fixes.
Some checks are pending
Create docker images / Build, push, and deploy (push) Waiting to run
Node.js CI / build (postgresql, 18.18, 10) (push) Waiting to run
2025-09-24 00:07:17 -07:00
Mike Cao
8115709a8b Merge remote-tracking branch 'origin/dev' into dev
# Conflicts:
#	src/app/(main)/teams/TeamsDataTable.tsx
#	src/app/(main)/teams/TeamsJoinButton.tsx
#	src/app/(main)/teams/[teamId]/TeamSettings.tsx
2025-09-23 23:10:05 -07:00
Mike Cao
83a014e884 Use FormattedMessage. Updated icons. Fixed bugs. 2025-09-23 23:08:40 -07:00
Mike Cao
3afe843461 Fixed ISO countries. 2025-09-23 17:49:36 -07:00
Mike Cao
c51dd7e606 Merge branch 'master' into dev
# Conflicts:
#	.github/workflows/cd-manual.yml
#	.github/workflows/cd.yml
#	.github/workflows/ci.yml
#	src/lib/detect.ts
2025-09-23 17:43:24 -07:00
Mike Cao
050df528a6
Merge pull request #3629 from kronthto/patch-1
Incomplete ISO Countries Constants
2025-09-23 17:39:41 -07:00
Tobias Kronthaler
cb209eee81
Fix map display for DACH 2025-09-23 10:09:58 +02:00
Mike Cao
6497cd0cd4
Merge pull request #3611 from halkeye/migrate-docker-gha
Some checks are pending
Create docker images / Build, push, and deploy (push) Waiting to run
Create docker images / Build, push, and deploy-1 (push) Waiting to run
Node.js CI / build (mysql, 18.18, 10) (push) Waiting to run
Node.js CI / build (postgresql, 18.18, 10) (push) Waiting to run
Migrate to docker actions
2025-09-22 22:29:46 -07:00
Mike Cao
3a4f4c1e27
Merge pull request #3627 from malwarepad/master
URGENT! Resolve IPv6 address destruction on GeoIP query
2025-09-22 22:26:33 -07:00
Panagiotis
7d9fe30626
Resolve IPv6 address destruction 2025-09-21 22:56:59 +03:00
Mike Cao
9c36f76e1b
Merge pull request #3623 from nickcmaynard/fix-ci
Some checks failed
Node.js CI / build (mysql, 18.18, 10) (push) Has been cancelled
Node.js CI / build (postgresql, 18.18, 10) (push) Has been cancelled
Add setup-pnpm to hopefully fix CI tests etc.
2025-09-17 21:43:28 -07:00
Nick Maynard
b2c829a077 Add setup-pnpm to hopefully fix CI tests etc. 2025-09-17 21:37:57 +01:00
Gavin Mogan
bf4e6ea96f Migrate to docker actions
Originally just wanted to add the standard opencontainer labels that
docker/metadata provide

but with "mr-smithers-excellent" seemed to only half implement docker
support, and a higher risk than docker for supply chain issues, so I
went all out and also added cosign to sign the images.

Docker metadata tags supports all the custom code to create version
tags, out of the box and fully maintained

Also dropped the manual workflow, just merged it into cd.yml since you
can select tags when you manual dispatch, and thats less to maintain
2025-09-06 07:06:23 -07:00
27 changed files with 349 additions and 118 deletions

View file

@ -1,4 +1,4 @@
name: Create docker images
name: Create docker images (cloud)
on:
push:

View file

@ -1,50 +1,101 @@
name: Create docker images
on: [create]
on:
push:
branches:
- master
- main
- dev
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
pull_request:
branches:
- master
- main
- dev
workflow_dispatch:
jobs:
build:
name: Build, push, and deploy
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
strategy:
matrix:
db-type: [postgresql]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- name: Set env
run: |
echo "NOW=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
- name: Generate tags
id: generate_tags
run: |
echo "tag_patch=$(echo ${{ matrix.db-type }})-${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
echo "tag_minor=$(echo ${{ matrix.db-type }})-$(echo ${GITHUB_REF#refs/tags/} | cut -d. -f1,2)" >> $GITHUB_ENV
echo "tag_major=$(echo ${{ matrix.db-type }})-$(echo ${GITHUB_REF#refs/tags/} | cut -d. -f1)" >> $GITHUB_ENV
echo "tag_latest=$(echo ${{ matrix.db-type }})-latest" >> $GITHUB_ENV
# Install the cosign tool except on PR
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@v3
- uses: mr-smithers-excellent/docker-build-push@v6
name: Build & push Docker image to ghcr.io for ${{ matrix.db-type }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log into registry docker.io
if: github.event_name != 'pull_request' && github.repository == 'umami-software/umami'
uses: docker/login-action@v3
with:
image: umami
tags: ${{ env.tag_major }}, ${{ env.tag_minor }}, ${{ env.tag_patch }}, ${{ env.tag_latest }}
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
registry: ghcr.io
multiPlatform: true
platform: linux/amd64,linux/arm64
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: mr-smithers-excellent/docker-build-push@v6
name: Build & push Docker image to docker.io for ${{ matrix.db-type }}
with:
image: umamisoftware/umami
tags: ${{ env.tag_major }}, ${{ env.tag_minor }}, ${{ env.tag_patch }}, ${{ env.tag_latest }}
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
registry: docker.io
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log into ghcr registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: |
umamisoftware/umami,enable=${{ github.repository == 'umami-software/umami' }}
ghcr.io/${{ github.repository }}
flavor: |
latest=auto
prefix=${{ matrix.db-type }}-
tags: |
type=ref,event=branch
type=ref,event=pr
# output 1.1.2
type=semver,pattern={{version}}
# output 1.1
type=semver,pattern={{major}}.{{minor}}
# output 1
type=semver,pattern={{major}}
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
build-args: DATABASE_TYPE=${{ matrix.db-type }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Sign the resulting Docker image digest except on PRs.
- name: Sign the published Docker image
if: ${{ github.event_name != 'pull_request' }}
env:
TAGS: ${{ steps.meta.outputs.tags }}
DIGEST: ${{ steps.build-and-push.outputs.digest }}
run: echo "${TAGS}" | xargs -I {} cosign sign --yes "{}@${DIGEST}"

View file

@ -14,11 +14,16 @@ jobs:
strategy:
matrix:
include:
- node-version: 18.18
db-type: postgresql
- node-version: 18.18
pnpm-version: 10
db-type: postgresql
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4 # required so that setup-node will work
with:
version: ${{ matrix.pnpm-version }}
run_install: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:

View file

@ -1,6 +1,6 @@
{
"name": "@umami/components",
"version": "0.125.0",
"version": "0.127.0",
"description": "Umami React components.",
"author": "Mike Cao <mike@mikecao.com>",
"license": "MIT",

View file

@ -69,7 +69,7 @@ export function SideNav(props: SidebarProps) {
<SidebarItem
label={label}
icon={icon}
isSelected={pathname.endsWith(path)}
isSelected={pathname.includes(path)}
role="button"
/>
</Link>

View file

@ -15,7 +15,7 @@ export function LinkDeleteButton({
name: string;
onSave?: () => void;
}) {
const { formatMessage, labels, getErrorMessage } = useMessages();
const { formatMessage, labels, getErrorMessage, FormattedMessage } = useMessages();
const { mutateAsync, isPending, error, touch } = useDeleteQuery(`/links/${linkId}`);
const handleConfirm = async (close: () => void) => {
@ -33,9 +33,14 @@ export function LinkDeleteButton({
<Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}>
{({ close }) => (
<ConfirmationForm
message={formatMessage(messages.confirmRemove, {
target: name,
})}
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending}
error={getErrorMessage(error)}
onConfirm={handleConfirm.bind(null, close)}

View file

@ -10,7 +10,7 @@ export function LinkHeader() {
const link = useLink();
return (
<PageHeader title={link.name} description={link.url} icon={<Link />}>
<PageHeader title={link.name} description={link.url} icon={<Link />} marginBottom="3">
<LinkButton href={getSlugUrl(link.slug)} target="_blank">
<Icon>
<ExternalLink />

View file

@ -13,7 +13,7 @@ export function PixelDeleteButton({
name: string;
onSave?: () => void;
}) {
const { formatMessage, labels, getErrorMessage } = useMessages();
const { formatMessage, labels, getErrorMessage, FormattedMessage } = useMessages();
const { mutateAsync, isPending, error } = useDeleteQuery(`/pixels/${pixelId}`);
const { touch } = useModified();
@ -32,9 +32,14 @@ export function PixelDeleteButton({
<Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}>
{({ close }) => (
<ConfirmationForm
message={formatMessage(messages.confirmRemove, {
target: name,
})}
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending}
error={getErrorMessage(error)}
onConfirm={handleConfirm.bind(null, close)}

View file

@ -10,7 +10,7 @@ export function PixelHeader() {
const pixel = usePixel();
return (
<PageHeader title={pixel.name} description={pixel.slug} icon={<Grid2x2 />}>
<PageHeader title={pixel.name} icon={<Grid2x2 />} marginBottom="3">
<LinkButton href={getSlugUrl(pixel.slug)} target="_blank">
<Icon>
<ExternalLink />

View file

@ -14,7 +14,7 @@ export function TeamLeaveForm({
onSave: () => void;
onClose: () => void;
}) {
const { formatMessage, labels, messages, getErrorMessage } = useMessages();
const { formatMessage, labels, messages, getErrorMessage, FormattedMessage } = useMessages();
const { mutateAsync, error, isPending } = useDeleteQuery(`/teams/${teamId}/users/${userId}`);
const { touch } = useModified();
@ -31,9 +31,14 @@ export function TeamLeaveForm({
return (
<ConfirmationForm
buttonLabel={formatMessage(labels.leave)}
message={formatMessage(messages.confirmLeave, {
target: teamName,
})}
message={
<FormattedMessage
{...messages.confirmLeave}
values={{
target: <b>{teamName}</b>,
}}
/>
}
onConfirm={handleConfirm}
onClose={onClose}
isLoading={isPending}

View file

@ -1,5 +1,5 @@
import { Button, Icon, Modal, DialogTrigger, Dialog, Text, useToast } from '@umami/react-zen';
import { AddUserSvg } from '@/components/icons';
import { UserPlus } from '@/components/icons';
import { useMessages, useModified } from '@/components/hooks';
import { TeamJoinForm } from './TeamJoinForm';
@ -17,7 +17,7 @@ export function TeamsJoinButton() {
<DialogTrigger>
<Button>
<Icon>
<AddUserSvg />
<UserPlus />
</Icon>
<Text>{formatMessage(labels.joinTeam)}</Text>
</Button>

View file

@ -17,7 +17,7 @@ export function TeamMemberRemoveButton({
disabled?: boolean;
onSave?: () => void;
}) {
const { formatMessage, labels } = useMessages();
const { formatMessage, labels, FormattedMessage } = useMessages();
const { mutateAsync, isPending, error } = useDeleteQuery(`/teams/${teamId}/users/${userId}`);
const { touch } = useModified();
@ -36,9 +36,14 @@ export function TeamMemberRemoveButton({
<Dialog title={formatMessage(labels.removeMember)} style={{ width: 400 }}>
{({ close }) => (
<ConfirmationForm
message={formatMessage(messages.confirmRemove, {
target: userName,
})}
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{userName}</b>,
}}
/>
}
isLoading={isPending}
error={error}
onConfirm={handleConfirm.bind(null, close)}

View file

@ -2,7 +2,7 @@ import Link from 'next/link';
import { Column, Icon, Text, Row } from '@umami/react-zen';
import { useLoginQuery, useMessages, useNavigation, useTeam } from '@/components/hooks';
import { ROLES } from '@/lib/constants';
import { Users, ArrowLeft } from '@/components/icons';
import { Users, ArrowRight } from '@/components/icons';
import { TeamLeaveButton } from '@/app/(main)/teams/TeamLeaveButton';
import { TeamManage } from './TeamManage';
import { TeamEditForm } from './TeamEditForm';
@ -34,8 +34,8 @@ export function TeamSettings({ teamId }: { teamId: string }) {
<>
<Link href="/settings/teams">
<Row marginTop="2" alignItems="center" gap>
<Icon>
<ArrowLeft />
<Icon rotate={180}>
<ArrowRight />
</Icon>
<Text>{formatMessage(labels.teams)}</Text>
</Row>

View file

@ -14,12 +14,12 @@ export function WebsiteFilterButton({
showText?: boolean;
}) {
const { formatMessage, labels } = useMessages();
const { replaceParams, router } = useNavigation();
const { updateParams, router } = useNavigation();
const handleChange = ({ filters, segment, cohort }: any) => {
const params = filtersArrayToObject(filters);
const url = replaceParams({ ...params, segment, cohort });
const url = updateParams({ ...params, segment, cohort });
router.push(url);
};

View file

@ -16,7 +16,7 @@ export function CohortDeleteButton({
name: string;
onSave?: () => void;
}) {
const { formatMessage, labels } = useMessages();
const { formatMessage, labels, FormattedMessage } = useMessages();
const { mutateAsync, isPending, error, touch } = useDeleteQuery(
`/websites/${websiteId}/segments/${cohortId}`,
);
@ -36,9 +36,14 @@ export function CohortDeleteButton({
<Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}>
{({ close }) => (
<ConfirmationForm
message={formatMessage(messages.confirmRemove, {
target: name,
})}
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending}
error={error}
onConfirm={handleConfirm.bind(null, close)}

View file

@ -3,7 +3,7 @@ import { useFormat, useMessages, useNavigation } from '@/components/hooks';
import { Empty } from '@/components/common/Empty';
import { Avatar } from '@/components/common/Avatar';
import Link from 'next/link';
import { Bolt, Eye } from '@/components/icons';
import { LightningSvg, Eye } from '@/components/icons';
import { DateDistance } from '@/components/common/DateDistance';
import { TypeIcon } from '@/components/common/TypeIcon';
@ -25,7 +25,7 @@ export function EventsTable({ data = [] }) {
<Link href={renderUrl(`/websites/${row.websiteId}/sessions/${row.sessionId}`)}>
<Avatar seed={row.sessionId} size={32} />
</Link>
<Icon>{row.eventName ? <Bolt /> : <Eye />}</Icon>
<Icon>{row.eventName ? <LightningSvg /> : <Eye />}</Icon>
<Text>
{formatMessage(row.eventName ? labels.triggeredEvent : labels.viewedPage)}
</Text>

View file

@ -8,7 +8,7 @@ import {
useTimezone,
useWebsite,
} from '@/components/hooks';
import { Eye, Visitor, Bolt } from '@/components/icons';
import { Eye, User, LightningSvg } from '@/components/icons';
import { BROWSERS, OS_NAMES } from '@/lib/constants';
import { stringToColor } from '@/lib/format';
import { useMemo, useState } from 'react';
@ -23,14 +23,14 @@ const TYPE_EVENT = 'event';
const icons = {
[TYPE_PAGEVIEW]: <Eye />,
[TYPE_SESSION]: <Visitor />,
[TYPE_EVENT]: <Bolt />,
[TYPE_SESSION]: <User />,
[TYPE_EVENT]: <LightningSvg />,
};
export function RealtimeLog({ data }: { data: any }) {
const website = useWebsite();
const [search, setSearch] = useState('');
const { formatMessage, labels, messages } = useMessages();
const { formatMessage, labels, messages, FormattedMessage } = useMessages();
const { formatValue } = useFormat();
const { locale } = useLocale();
const { formatTimezoneDate } = useTimezone();
@ -74,20 +74,25 @@ export function RealtimeLog({ data }: { data: any }) {
const { __type, eventName, urlPath, browser, os, country, device } = log;
if (__type === TYPE_EVENT) {
return formatMessage(messages.eventLog, {
event: <b key="b">{eventName || formatMessage(labels.unknown)}</b>,
url: (
<a
key="a"
href={`//${website?.domain}${urlPath}`}
className={styles.link}
target="_blank"
rel="noreferrer noopener"
>
{urlPath}
</a>
),
});
return (
<FormattedMessage
{...messages.eventLog}
values={{
event: <b key="b">{eventName || formatMessage(labels.unknown)}</b>,
url: (
<a
key="a"
href={`//${website?.domain}${urlPath}`}
className={styles.link}
target="_blank"
rel="noreferrer noopener"
>
{urlPath}
</a>
),
}}
/>
);
}
if (__type === TYPE_PAGEVIEW) {
@ -104,12 +109,17 @@ export function RealtimeLog({ data }: { data: any }) {
}
if (__type === TYPE_SESSION) {
return formatMessage(messages.visitorLog, {
country: <b key="country">{countryNames[country] || formatMessage(labels.unknown)}</b>,
browser: <b key="browser">{BROWSERS[browser]}</b>,
os: <b key="os">{OS_NAMES[os] || os}</b>,
device: <b key="device">{formatMessage(labels[device] || labels.unknown)}</b>,
});
return (
<FormattedMessage
{...messages.visitorLog}
values={{
country: <b key="country">{countryNames[country] || formatMessage(labels.unknown)}</b>,
browser: <b key="browser">{BROWSERS[browser]}</b>,
os: <b key="os">{OS_NAMES[os] || os}</b>,
device: <b key="device">{formatMessage(labels[device] || labels.unknown)}</b>,
}}
/>
);
}
};

View file

@ -16,7 +16,7 @@ export function SegmentDeleteButton({
name: string;
onSave?: () => void;
}) {
const { formatMessage, labels } = useMessages();
const { formatMessage, labels, FormattedMessage } = useMessages();
const { mutateAsync, isPending, error, touch } = useDeleteQuery(
`/websites/${websiteId}/segments/${segmentId}`,
);
@ -36,9 +36,14 @@ export function SegmentDeleteButton({
<Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}>
{({ close }) => (
<ConfirmationForm
message={formatMessage(messages.confirmRemove, {
target: name,
})}
message={
<FormattedMessage
{...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending}
error={error}
onConfirm={handleConfirm.bind(null, close)}

View file

@ -12,7 +12,7 @@ import {
Dialog,
} from '@umami/react-zen';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { Bolt, Eye, FileText } from '@/components/icons';
import { LightningSvg, Eye, FileText } from '@/components/icons';
import { useMessages, useSessionActivityQuery, useTimezone } from '@/components/hooks';
import { EventData } from '@/components/metrics/EventData';
@ -52,7 +52,7 @@ export function SessionActivity({
{formatTimezoneDate(createdAt, 'pp')}
</StatusLight>
<Row alignItems="center" gap="2">
<Icon>{eventName ? <Bolt /> : <Eye />}</Icon>
<Icon>{eventName ? <LightningSvg /> : <Eye />}</Icon>
<Text>
{eventName
? formatMessage(labels.triggeredEvent)

View file

@ -4,6 +4,7 @@ import { Heading, Icon, Row, RowProps, Text, Column } from '@umami/react-zen';
export function PageHeader({
title,
description,
label,
icon,
showBorder = true,
children,
@ -11,6 +12,7 @@ export function PageHeader({
}: {
title: string;
description?: string;
label?: ReactNode;
icon?: ReactNode;
showBorder?: boolean;
allowEdit?: boolean;
@ -26,7 +28,8 @@ export function PageHeader({
width="100%"
{...props}
>
<Column>
<Column gap="2">
{label}
<Row alignItems="center" gap="3">
{icon && (
<Icon size="md" color="muted">
@ -35,7 +38,11 @@ export function PageHeader({
)}
{title && <Heading size="4">{title}</Heading>}
</Row>
{description && <Text color="muted">{description}</Text>}
{description && (
<Text color="muted" truncate style={{ maxWidth: 600 }} title={description}>
{description}
</Text>
)}
</Column>
<Row justifyContent="flex-end">{children}</Row>
</Row>

View file

@ -1,7 +1,22 @@
import { useIntl } from 'react-intl';
import { useIntl, FormattedMessage, type MessageDescriptor } from 'react-intl';
import { messages, labels } from '@/components/messages';
export function useMessages() {
type FormatMessage = (
descriptor: MessageDescriptor,
values?: Record<string, string | number | boolean | null | undefined>,
opts?: any,
) => string | null;
interface UseMessages {
formatMessage: FormatMessage;
messages: typeof messages;
labels: typeof labels;
getMessage: (id: string) => string;
getErrorMessage: (error: unknown) => string | undefined;
FormattedMessage: typeof FormattedMessage;
}
export function useMessages(): UseMessages {
const intl = useIntl();
const getMessage = (id: string) => {
@ -21,15 +36,12 @@ export function useMessages() {
};
const formatMessage = (
descriptor: {
id: string;
defaultMessage: string;
},
values?: Record<string, string>,
descriptor: MessageDescriptor,
values?: Record<string, string | number | boolean | null | undefined>,
opts?: any,
) => {
return descriptor ? intl.formatMessage(descriptor, values, opts) : null;
};
return { formatMessage, messages, labels, getMessage, getErrorMessage };
return { formatMessage, messages, labels, getMessage, getErrorMessage, FormattedMessage };
}

View file

@ -1,18 +1,14 @@
export * from 'lucide-react';
export {
Logo as LogoSvg,
Bolt as BoltSvg,
Change as ChangeSvg,
Compare as CompareSvg,
Funnel as FunnelSvg,
Lightbulb as LightbulbSvg,
Lightning as LightningSvg,
Location as LocationSvg,
Magnet as MagnetSvg,
Money as MoneySvg,
Network as NetworkSvg,
Path as PathSvg,
Tag as TagSvg,
Target as TargetSvg,
AddUser as AddUserSvg,
} from '@/components/svg';

View file

@ -10,7 +10,15 @@ import {
MenuSection,
} from '@umami/react-zen';
import { useMessages, useLoginQuery, useNavigation, useConfig } from '@/components/hooks';
import { LogOut, LockKeyhole, Settings, UserCircle, LifeBuoy, BookText } from '@/components/icons';
import {
LogOut,
LockKeyhole,
Settings,
UserCircle,
LifeBuoy,
BookText,
ExternalLink,
} from '@/components/icons';
import { DOCS_URL } from '@/lib/constants';
export function SettingsButton() {
@ -54,7 +62,11 @@ export function SettingsButton() {
id="/docs"
icon={<BookText />}
label={formatMessage(labels.documentation)}
/>
>
<Icon color="muted">
<ExternalLink />
</Icon>
</MenuItem>
<MenuItem
id="/settings/support"
icon={<LifeBuoy />}

View file

@ -18,7 +18,7 @@ export function WebsiteDateFilter({
showButtons = true,
allowCompare,
}: WebsiteDateFilterProps) {
const { dateRange } = useDateRange(websiteId);
const { dateRange, saveDateRange } = useDateRange(websiteId);
const { value, endDate } = dateRange;
const { formatMessage, labels } = useMessages();
const {
@ -32,6 +32,7 @@ export function WebsiteDateFilter({
const disableForward = value === 'all' || isAfter(endDate, new Date());
const handleChange = (date: string) => {
saveDateRange(date);
router.push(updateParams({ date, offset: undefined }));
};

View file

@ -85,7 +85,7 @@ export function WeeklyTraffic({ websiteId }: { websiteId: string }) {
height="16px"
borderRadius="full"
style={{ margin: '0 auto' }}
role="cell"
role="button"
>
<Row
backgroundColor="primary"

View file

@ -359,14 +359,116 @@ export const GROUPED_DOMAINS = [
export const MAP_FILE = '/datamaps.world.json';
export const ISO_COUNTRIES = {
ABW: 'AW',
AFG: 'AF',
AGO: 'AO',
AIA: 'AI',
ALA: 'AX',
ALB: 'AL',
AND: 'AD',
ANT: 'AN',
ARE: 'AE',
ARG: 'AR',
ARM: 'AM',
ASM: 'AS',
ATF: 'TF',
ATG: 'AG',
AUS: 'AU',
AUT: 'AT',
AZE: 'AZ',
BDI: 'BI',
BEL: 'BE',
BEN: 'BJ',
BFA: 'BF',
BGD: 'BD',
BGR: 'BG',
BHR: 'BH',
BHS: 'BS',
BIH: 'BA',
BLR: 'BY',
BLZ: 'BZ',
BLM: 'BL',
BMU: 'BM',
BOL: 'BO',
BRA: 'BR',
BRB: 'BB',
BRN: 'BN',
BTN: 'BT',
BVT: 'BV',
BWA: 'BW',
CAF: 'CF',
CAN: 'CA',
CCK: 'CC',
CHE: 'CH',
CHL: 'CL',
CHN: 'CN',
CIV: 'CI',
CMR: 'CM',
COD: 'CD',
COG: 'CG',
COK: 'CK',
COL: 'CO',
COM: 'KM',
CPV: 'CV',
CRI: 'CR',
CUB: 'CU',
CXR: 'CX',
CYM: 'KY',
CYP: 'CY',
CZE: 'CZ',
DEU: 'DE',
DJI: 'DJ',
DMA: 'DM',
DNK: 'DK',
DOM: 'DO',
DZA: 'DZ',
ECU: 'EC',
EGY: 'EG',
ERI: 'ER',
ESH: 'EH',
ESP: 'ES',
EST: 'EE',
ETH: 'ET',
FIN: 'FI',
FJI: 'FJ',
FLK: 'FK',
FRA: 'FR',
FRO: 'FO',
FSM: 'FM',
GAB: 'GA',
GBR: 'GB',
GEO: 'GE',
GGY: 'GG',
GHA: 'GH',
GIB: 'GI',
GIN: 'GN',
GLP: 'GP',
GMB: 'GM',
GNB: 'GW',
GNQ: 'GQ',
GRC: 'GR',
GRD: 'GD',
GRL: 'GL',
GTM: 'GT',
GUF: 'GF',
GUM: 'GU',
GUY: 'GY',
HKG: 'HK',
HMD: 'HM',
HND: 'HN',
HRV: 'HR',
HTI: 'HT',
HUN: 'HU',
IDN: 'ID',
IMN: 'IM',
IND: 'IN',
IOT: 'IO',
IRL: 'IE',
IRN: 'IR',
IRQ: 'IQ',
ISL: 'IS',
ISR: 'IL',
ITA: 'IT',
JAM: 'JM',
JEY: 'JE',
JOR: 'JO',
@ -374,6 +476,7 @@ export const ISO_COUNTRIES = {
KAZ: 'KZ',
KEN: 'KE',
KGZ: 'KG',
KHM: 'KH',
KIR: 'KI',
KNA: 'KN',
KOR: 'KR',
@ -438,6 +541,7 @@ export const ISO_COUNTRIES = {
PRT: 'PT',
PRY: 'PY',
PSE: 'PS',
PYF: 'PF',
QAT: 'QA',
REU: 'RE',
ROU: 'RO',
@ -452,13 +556,13 @@ export const ISO_COUNTRIES = {
SJM: 'SJ',
SLB: 'SB',
SLE: 'SL',
SLV: 'SV',
SMR: 'SM',
SOM: 'SO',
SPM: 'PM',
SRB: 'RS',
SSD: 'SS',
STP: 'ST',
SUR: 'SR',
STP: 'ST',
SVK: 'SK',
SVN: 'SI',
SWE: 'SE',
@ -466,6 +570,7 @@ export const ISO_COUNTRIES = {
SYC: 'SC',
SYR: 'SY',
TCA: 'TC',
TCD: 'TD',
TGO: 'TG',
THA: 'TH',
TJK: 'TJ',
@ -485,8 +590,10 @@ export const ISO_COUNTRIES = {
URY: 'UY',
USA: 'US',
UZB: 'UZ',
VAT: 'VA',
VCT: 'VC',
VEN: 'VE',
VGB: 'VG',
VIR: 'VI',
VNM: 'VN',
VUT: 'VU',

View file

@ -136,7 +136,7 @@ export async function getQueryFilters(
...dateRange,
...filters,
page: params?.page,
pageSize: params?.page ? params?.pageSize || DEFAULT_PAGE_SIZE : undefined,
pageSize: params?.pageSize ? params?.pageSize || DEFAULT_PAGE_SIZE : undefined,
orderBy: params?.orderBy,
sortDescending: params?.sortDescending,
search: params?.search,