mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Compare commits
13 commits
8c703eff93
...
27c342811e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27c342811e | ||
|
|
8115709a8b | ||
|
|
83a014e884 | ||
|
|
3afe843461 | ||
|
|
c51dd7e606 | ||
|
|
050df528a6 | ||
|
|
cb209eee81 | ||
|
|
6497cd0cd4 | ||
|
|
3a4f4c1e27 | ||
|
|
7d9fe30626 | ||
|
|
9c36f76e1b | ||
|
|
b2c829a077 | ||
|
|
bf4e6ea96f |
27 changed files with 349 additions and 118 deletions
2
.github/workflows/cd-cloud.yml
vendored
2
.github/workflows/cd-cloud.yml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
name: Create docker images
|
||||
name: Create docker images (cloud)
|
||||
|
||||
on:
|
||||
push:
|
||||
|
|
|
|||
113
.github/workflows/cd.yml
vendored
113
.github/workflows/cd.yml
vendored
|
|
@ -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}"
|
||||
|
|
|
|||
9
.github/workflows/ci.yml
vendored
9
.github/workflows/ci.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 />}
|
||||
|
|
|
|||
|
|
@ -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 }));
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue