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: on:
push: push:

View file

@ -1,50 +1,101 @@
name: Create docker images 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: jobs:
build: build:
name: Build, push, and deploy name: Build, push, and deploy
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
runs-on: ubuntu-latest 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: strategy:
matrix: matrix:
db-type: [postgresql] db-type: [postgresql]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v5
- name: Set env # Install the cosign tool except on PR
run: | # https://github.com/sigstore/cosign-installer
echo "NOW=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV - name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@v3
- name: Generate tags - name: Set up Docker Buildx
id: generate_tags uses: docker/setup-buildx-action@v3
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
- uses: mr-smithers-excellent/docker-build-push@v6 - name: Log into registry docker.io
name: Build & push Docker image to ghcr.io for ${{ matrix.db-type }} if: github.event_name != 'pull_request' && github.repository == 'umami-software/umami'
uses: docker/login-action@v3
with: 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 registry: docker.io
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} 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: strategy:
matrix: matrix:
include: include:
- node-version: 18.18 - node-version: 18.18
db-type: postgresql pnpm-version: 10
db-type: postgresql
steps: steps:
- uses: actions/checkout@v4 - 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 }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:

View file

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

View file

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

View file

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

View file

@ -10,7 +10,7 @@ export function LinkHeader() {
const link = useLink(); const link = useLink();
return ( 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"> <LinkButton href={getSlugUrl(link.slug)} target="_blank">
<Icon> <Icon>
<ExternalLink /> <ExternalLink />

View file

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

View file

@ -10,7 +10,7 @@ export function PixelHeader() {
const pixel = usePixel(); const pixel = usePixel();
return ( 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"> <LinkButton href={getSlugUrl(pixel.slug)} target="_blank">
<Icon> <Icon>
<ExternalLink /> <ExternalLink />

View file

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

View file

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

View file

@ -17,7 +17,7 @@ export function TeamMemberRemoveButton({
disabled?: boolean; disabled?: boolean;
onSave?: () => void; onSave?: () => void;
}) { }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels, FormattedMessage } = useMessages();
const { mutateAsync, isPending, error } = useDeleteQuery(`/teams/${teamId}/users/${userId}`); const { mutateAsync, isPending, error } = useDeleteQuery(`/teams/${teamId}/users/${userId}`);
const { touch } = useModified(); const { touch } = useModified();
@ -36,9 +36,14 @@ export function TeamMemberRemoveButton({
<Dialog title={formatMessage(labels.removeMember)} style={{ width: 400 }}> <Dialog title={formatMessage(labels.removeMember)} style={{ width: 400 }}>
{({ close }) => ( {({ close }) => (
<ConfirmationForm <ConfirmationForm
message={formatMessage(messages.confirmRemove, { message={
target: userName, <FormattedMessage
})} {...messages.confirmRemove}
values={{
target: <b>{userName}</b>,
}}
/>
}
isLoading={isPending} isLoading={isPending}
error={error} error={error}
onConfirm={handleConfirm.bind(null, close)} 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 { Column, Icon, Text, Row } from '@umami/react-zen';
import { useLoginQuery, useMessages, useNavigation, useTeam } from '@/components/hooks'; import { useLoginQuery, useMessages, useNavigation, useTeam } from '@/components/hooks';
import { ROLES } from '@/lib/constants'; 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 { TeamLeaveButton } from '@/app/(main)/teams/TeamLeaveButton';
import { TeamManage } from './TeamManage'; import { TeamManage } from './TeamManage';
import { TeamEditForm } from './TeamEditForm'; import { TeamEditForm } from './TeamEditForm';
@ -34,8 +34,8 @@ export function TeamSettings({ teamId }: { teamId: string }) {
<> <>
<Link href="/settings/teams"> <Link href="/settings/teams">
<Row marginTop="2" alignItems="center" gap> <Row marginTop="2" alignItems="center" gap>
<Icon> <Icon rotate={180}>
<ArrowLeft /> <ArrowRight />
</Icon> </Icon>
<Text>{formatMessage(labels.teams)}</Text> <Text>{formatMessage(labels.teams)}</Text>
</Row> </Row>

View file

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

View file

@ -16,7 +16,7 @@ export function CohortDeleteButton({
name: string; name: string;
onSave?: () => void; onSave?: () => void;
}) { }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels, FormattedMessage } = useMessages();
const { mutateAsync, isPending, error, touch } = useDeleteQuery( const { mutateAsync, isPending, error, touch } = useDeleteQuery(
`/websites/${websiteId}/segments/${cohortId}`, `/websites/${websiteId}/segments/${cohortId}`,
); );
@ -36,9 +36,14 @@ export function CohortDeleteButton({
<Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}> <Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}>
{({ close }) => ( {({ close }) => (
<ConfirmationForm <ConfirmationForm
message={formatMessage(messages.confirmRemove, { message={
target: name, <FormattedMessage
})} {...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending} isLoading={isPending}
error={error} error={error}
onConfirm={handleConfirm.bind(null, close)} 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 { Empty } from '@/components/common/Empty';
import { Avatar } from '@/components/common/Avatar'; import { Avatar } from '@/components/common/Avatar';
import Link from 'next/link'; import Link from 'next/link';
import { Bolt, Eye } from '@/components/icons'; import { LightningSvg, Eye } from '@/components/icons';
import { DateDistance } from '@/components/common/DateDistance'; import { DateDistance } from '@/components/common/DateDistance';
import { TypeIcon } from '@/components/common/TypeIcon'; import { TypeIcon } from '@/components/common/TypeIcon';
@ -25,7 +25,7 @@ export function EventsTable({ data = [] }) {
<Link href={renderUrl(`/websites/${row.websiteId}/sessions/${row.sessionId}`)}> <Link href={renderUrl(`/websites/${row.websiteId}/sessions/${row.sessionId}`)}>
<Avatar seed={row.sessionId} size={32} /> <Avatar seed={row.sessionId} size={32} />
</Link> </Link>
<Icon>{row.eventName ? <Bolt /> : <Eye />}</Icon> <Icon>{row.eventName ? <LightningSvg /> : <Eye />}</Icon>
<Text> <Text>
{formatMessage(row.eventName ? labels.triggeredEvent : labels.viewedPage)} {formatMessage(row.eventName ? labels.triggeredEvent : labels.viewedPage)}
</Text> </Text>

View file

@ -8,7 +8,7 @@ import {
useTimezone, useTimezone,
useWebsite, useWebsite,
} from '@/components/hooks'; } 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 { BROWSERS, OS_NAMES } from '@/lib/constants';
import { stringToColor } from '@/lib/format'; import { stringToColor } from '@/lib/format';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
@ -23,14 +23,14 @@ const TYPE_EVENT = 'event';
const icons = { const icons = {
[TYPE_PAGEVIEW]: <Eye />, [TYPE_PAGEVIEW]: <Eye />,
[TYPE_SESSION]: <Visitor />, [TYPE_SESSION]: <User />,
[TYPE_EVENT]: <Bolt />, [TYPE_EVENT]: <LightningSvg />,
}; };
export function RealtimeLog({ data }: { data: any }) { export function RealtimeLog({ data }: { data: any }) {
const website = useWebsite(); const website = useWebsite();
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages, FormattedMessage } = useMessages();
const { formatValue } = useFormat(); const { formatValue } = useFormat();
const { locale } = useLocale(); const { locale } = useLocale();
const { formatTimezoneDate } = useTimezone(); const { formatTimezoneDate } = useTimezone();
@ -74,20 +74,25 @@ export function RealtimeLog({ data }: { data: any }) {
const { __type, eventName, urlPath, browser, os, country, device } = log; const { __type, eventName, urlPath, browser, os, country, device } = log;
if (__type === TYPE_EVENT) { if (__type === TYPE_EVENT) {
return formatMessage(messages.eventLog, { return (
event: <b key="b">{eventName || formatMessage(labels.unknown)}</b>, <FormattedMessage
url: ( {...messages.eventLog}
<a values={{
key="a" event: <b key="b">{eventName || formatMessage(labels.unknown)}</b>,
href={`//${website?.domain}${urlPath}`} url: (
className={styles.link} <a
target="_blank" key="a"
rel="noreferrer noopener" href={`//${website?.domain}${urlPath}`}
> className={styles.link}
{urlPath} target="_blank"
</a> rel="noreferrer noopener"
), >
}); {urlPath}
</a>
),
}}
/>
);
} }
if (__type === TYPE_PAGEVIEW) { if (__type === TYPE_PAGEVIEW) {
@ -104,12 +109,17 @@ export function RealtimeLog({ data }: { data: any }) {
} }
if (__type === TYPE_SESSION) { if (__type === TYPE_SESSION) {
return formatMessage(messages.visitorLog, { return (
country: <b key="country">{countryNames[country] || formatMessage(labels.unknown)}</b>, <FormattedMessage
browser: <b key="browser">{BROWSERS[browser]}</b>, {...messages.visitorLog}
os: <b key="os">{OS_NAMES[os] || os}</b>, values={{
device: <b key="device">{formatMessage(labels[device] || labels.unknown)}</b>, 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; name: string;
onSave?: () => void; onSave?: () => void;
}) { }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels, FormattedMessage } = useMessages();
const { mutateAsync, isPending, error, touch } = useDeleteQuery( const { mutateAsync, isPending, error, touch } = useDeleteQuery(
`/websites/${websiteId}/segments/${segmentId}`, `/websites/${websiteId}/segments/${segmentId}`,
); );
@ -36,9 +36,14 @@ export function SegmentDeleteButton({
<Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}> <Dialog title={formatMessage(labels.confirm)} style={{ width: 400 }}>
{({ close }) => ( {({ close }) => (
<ConfirmationForm <ConfirmationForm
message={formatMessage(messages.confirmRemove, { message={
target: name, <FormattedMessage
})} {...messages.confirmRemove}
values={{
target: <b>{name}</b>,
}}
/>
}
isLoading={isPending} isLoading={isPending}
error={error} error={error}
onConfirm={handleConfirm.bind(null, close)} onConfirm={handleConfirm.bind(null, close)}

View file

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

View file

@ -4,6 +4,7 @@ import { Heading, Icon, Row, RowProps, Text, Column } from '@umami/react-zen';
export function PageHeader({ export function PageHeader({
title, title,
description, description,
label,
icon, icon,
showBorder = true, showBorder = true,
children, children,
@ -11,6 +12,7 @@ export function PageHeader({
}: { }: {
title: string; title: string;
description?: string; description?: string;
label?: ReactNode;
icon?: ReactNode; icon?: ReactNode;
showBorder?: boolean; showBorder?: boolean;
allowEdit?: boolean; allowEdit?: boolean;
@ -26,7 +28,8 @@ export function PageHeader({
width="100%" width="100%"
{...props} {...props}
> >
<Column> <Column gap="2">
{label}
<Row alignItems="center" gap="3"> <Row alignItems="center" gap="3">
{icon && ( {icon && (
<Icon size="md" color="muted"> <Icon size="md" color="muted">
@ -35,7 +38,11 @@ export function PageHeader({
)} )}
{title && <Heading size="4">{title}</Heading>} {title && <Heading size="4">{title}</Heading>}
</Row> </Row>
{description && <Text color="muted">{description}</Text>} {description && (
<Text color="muted" truncate style={{ maxWidth: 600 }} title={description}>
{description}
</Text>
)}
</Column> </Column>
<Row justifyContent="flex-end">{children}</Row> <Row justifyContent="flex-end">{children}</Row>
</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'; 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 intl = useIntl();
const getMessage = (id: string) => { const getMessage = (id: string) => {
@ -21,15 +36,12 @@ export function useMessages() {
}; };
const formatMessage = ( const formatMessage = (
descriptor: { descriptor: MessageDescriptor,
id: string; values?: Record<string, string | number | boolean | null | undefined>,
defaultMessage: string;
},
values?: Record<string, string>,
opts?: any, opts?: any,
) => { ) => {
return descriptor ? intl.formatMessage(descriptor, values, opts) : null; 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 * from 'lucide-react';
export { export {
Logo as LogoSvg, Logo as LogoSvg,
Bolt as BoltSvg,
Change as ChangeSvg,
Compare as CompareSvg, Compare as CompareSvg,
Funnel as FunnelSvg, Funnel as FunnelSvg,
Lightbulb as LightbulbSvg,
Lightning as LightningSvg, Lightning as LightningSvg,
Location as LocationSvg, Location as LocationSvg,
Magnet as MagnetSvg, Magnet as MagnetSvg,
Money as MoneySvg, Money as MoneySvg,
Network as NetworkSvg, Network as NetworkSvg,
Path as PathSvg, Path as PathSvg,
Tag as TagSvg,
Target as TargetSvg, Target as TargetSvg,
AddUser as AddUserSvg, AddUser as AddUserSvg,
} from '@/components/svg'; } from '@/components/svg';

View file

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

View file

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

View file

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

View file

@ -359,14 +359,116 @@ export const GROUPED_DOMAINS = [
export const MAP_FILE = '/datamaps.world.json'; export const MAP_FILE = '/datamaps.world.json';
export const ISO_COUNTRIES = { export const ISO_COUNTRIES = {
ABW: 'AW',
AFG: 'AF',
AGO: 'AO',
AIA: 'AI',
ALA: 'AX',
ALB: 'AL',
AND: 'AD',
ANT: 'AN', ANT: 'AN',
ARE: 'AE', 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', 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', 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', ESH: 'EH',
ESP: 'ES', ESP: 'ES',
EST: 'EE',
ETH: 'ET',
FIN: 'FI',
FJI: 'FJ',
FLK: 'FK',
FRA: 'FR',
FRO: 'FO',
FSM: 'FM', FSM: 'FM',
GAB: 'GA',
GBR: 'GB', 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', JAM: 'JM',
JEY: 'JE', JEY: 'JE',
JOR: 'JO', JOR: 'JO',
@ -374,6 +476,7 @@ export const ISO_COUNTRIES = {
KAZ: 'KZ', KAZ: 'KZ',
KEN: 'KE', KEN: 'KE',
KGZ: 'KG', KGZ: 'KG',
KHM: 'KH',
KIR: 'KI', KIR: 'KI',
KNA: 'KN', KNA: 'KN',
KOR: 'KR', KOR: 'KR',
@ -438,6 +541,7 @@ export const ISO_COUNTRIES = {
PRT: 'PT', PRT: 'PT',
PRY: 'PY', PRY: 'PY',
PSE: 'PS', PSE: 'PS',
PYF: 'PF',
QAT: 'QA', QAT: 'QA',
REU: 'RE', REU: 'RE',
ROU: 'RO', ROU: 'RO',
@ -452,13 +556,13 @@ export const ISO_COUNTRIES = {
SJM: 'SJ', SJM: 'SJ',
SLB: 'SB', SLB: 'SB',
SLE: 'SL', SLE: 'SL',
SLV: 'SV',
SMR: 'SM', SMR: 'SM',
SOM: 'SO', SOM: 'SO',
SPM: 'PM', SPM: 'PM',
SRB: 'RS', SRB: 'RS',
SSD: 'SS',
STP: 'ST',
SUR: 'SR', SUR: 'SR',
STP: 'ST',
SVK: 'SK', SVK: 'SK',
SVN: 'SI', SVN: 'SI',
SWE: 'SE', SWE: 'SE',
@ -466,6 +570,7 @@ export const ISO_COUNTRIES = {
SYC: 'SC', SYC: 'SC',
SYR: 'SY', SYR: 'SY',
TCA: 'TC', TCA: 'TC',
TCD: 'TD',
TGO: 'TG', TGO: 'TG',
THA: 'TH', THA: 'TH',
TJK: 'TJ', TJK: 'TJ',
@ -485,8 +590,10 @@ export const ISO_COUNTRIES = {
URY: 'UY', URY: 'UY',
USA: 'US', USA: 'US',
UZB: 'UZ', UZB: 'UZ',
VAT: 'VA',
VCT: 'VC', VCT: 'VC',
VEN: 'VE', VEN: 'VE',
VGB: 'VG',
VIR: 'VI', VIR: 'VI',
VNM: 'VN', VNM: 'VN',
VUT: 'VU', VUT: 'VU',

View file

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