From bf4e6ea96f4449d88c797aad60f3b850a1081a4e Mon Sep 17 00:00:00 2001 From: Gavin Mogan Date: Sat, 6 Sep 2025 00:49:55 -0700 Subject: [PATCH 1/7] 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 --- .github/workflows/cd-cloud.yml | 2 +- .github/workflows/cd-manual.yml | 58 ----------------- .github/workflows/cd.yml | 109 +++++++++++++++++++++++--------- 3 files changed, 81 insertions(+), 88 deletions(-) delete mode 100644 .github/workflows/cd-manual.yml diff --git a/.github/workflows/cd-cloud.yml b/.github/workflows/cd-cloud.yml index b155624a7..90a09dab0 100644 --- a/.github/workflows/cd-cloud.yml +++ b/.github/workflows/cd-cloud.yml @@ -1,4 +1,4 @@ -name: Create docker images +name: Create docker images (cloud) on: push: diff --git a/.github/workflows/cd-manual.yml b/.github/workflows/cd-manual.yml deleted file mode 100644 index 1f8651fa2..000000000 --- a/.github/workflows/cd-manual.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Create docker images (manual) - -on: - workflow_dispatch: - inputs: - version: - type: string - description: Version - required: true - -jobs: - build: - name: Build, push, and deploy - runs-on: ubuntu-latest - - strategy: - matrix: - db-type: [postgresql, mysql] - - steps: - - uses: actions/checkout@v3 - - - name: Extract version parts from input - id: extract_version - run: | - echo "version=$(echo ${{ github.event.inputs.version }})" >> $GITHUB_ENV - echo "major=$(echo ${{ github.event.inputs.version }} | cut -d. -f1)" >> $GITHUB_ENV - echo "minor=$(echo ${{ github.event.inputs.version }} | cut -d. -f2)" >> $GITHUB_ENV - - - name: Generate tags - id: generate_tags - run: | - echo "tag_major=$(echo ${{ matrix.db-type }}-${{ env.major }})" >> $GITHUB_ENV - echo "tag_minor=$(echo ${{ matrix.db-type }}-${{ env.major }}.${{ env.minor }})" >> $GITHUB_ENV - echo "tag_patch=$(echo ${{ matrix.db-type }}-${{ env.version }})" >> $GITHUB_ENV - echo "tag_latest=$(echo ${{ matrix.db-type }}-latest)" >> $GITHUB_ENV - - - uses: mr-smithers-excellent/docker-build-push@v6 - name: Build & push Docker image to ghcr.io for ${{ matrix.db-type }} - 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 }} \ No newline at end of file diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index f67f51c38..051d24a22 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -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, mysql] 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: + 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: - 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 }} + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 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 }} \ No newline at end of file + 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}" From b2c829a077fc388bb17e7c045a702be507b08dc0 Mon Sep 17 00:00:00 2001 From: Nick Maynard Date: Wed, 17 Sep 2025 21:37:57 +0100 Subject: [PATCH 2/7] Add setup-pnpm to hopefully fix CI tests etc. --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 478e5ad16..d2e027cfb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,12 +19,18 @@ jobs: matrix: include: - node-version: 18.18 + pnpm-version: 10 db-type: postgresql - node-version: 18.18 + pnpm-version: 10 db-type: mysql 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: From 7d9fe30626a0251884cc33a31d408cd93117e2ed Mon Sep 17 00:00:00 2001 From: Panagiotis Date: Sun, 21 Sep 2025 22:56:59 +0300 Subject: [PATCH 3/7] Resolve IPv6 address destruction --- src/lib/detect.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/lib/detect.ts b/src/lib/detect.ts index ee9d2603c..17fb574c0 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -108,6 +108,14 @@ function decodeHeader(s: string | undefined | null): string | undefined | null { return Buffer.from(s, 'latin1').toString('utf-8'); } +function removePortFromIP(ip: string = "") { + const split = ip.split(":"); + + // Assuming ip is a valid IPv4/IPv6 address, 3 colons is the minumum for IPv6 + const ipv4 = split.length - 1 < 3; + return ipv4 ? split[0] : ip; +} + export async function getLocation(ip: string = '', headers: Headers, hasPayloadIP: boolean) { // Ignore local ips if (await isLocalhost(ip)) { @@ -141,7 +149,7 @@ export async function getLocation(ip: string = '', headers: Headers, hasPayloadI } // When the client IP is extracted from headers, sometimes the value includes a port - const cleanIp = ip?.split(':')[0]; + const cleanIp = removePortFromIP(ip); const result = global[MAXMIND].get(cleanIp); if (result) { From cb209eee8162d3cf80edbc5d5b8a2472e42eb9b4 Mon Sep 17 00:00:00 2001 From: Tobias Kronthaler Date: Tue, 23 Sep 2025 10:09:58 +0200 Subject: [PATCH 4/7] Fix map display for DACH --- src/lib/constants.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 6192f3423..75691d057 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -448,8 +448,10 @@ export const MAP_FILE = '/datamaps.world.json'; export const ISO_COUNTRIES = { ANT: 'AN', ARE: 'AE', + AUT: 'AT', BLM: 'BL', CHE: 'CH', + DEU: 'DE', ESH: 'EH', ESP: 'ES', FSM: 'FM', From 3afe843461fdf51ce6256f11ca4730d7b3c0e2bc Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 23 Sep 2025 17:49:36 -0700 Subject: [PATCH 5/7] Fixed ISO countries. --- src/lib/constants.ts | 109 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index af6174589..10a5660cd 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -359,16 +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', @@ -376,6 +476,7 @@ export const ISO_COUNTRIES = { KAZ: 'KZ', KEN: 'KE', KGZ: 'KG', + KHM: 'KH', KIR: 'KI', KNA: 'KN', KOR: 'KR', @@ -440,6 +541,7 @@ export const ISO_COUNTRIES = { PRT: 'PT', PRY: 'PY', PSE: 'PS', + PYF: 'PF', QAT: 'QA', REU: 'RE', ROU: 'RO', @@ -454,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', @@ -468,6 +570,7 @@ export const ISO_COUNTRIES = { SYC: 'SC', SYR: 'SY', TCA: 'TC', + TCD: 'TD', TGO: 'TG', THA: 'TH', TJK: 'TJ', @@ -487,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', From 83a014e8846f8e04e93788c0c9e4263f26cd9be8 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 23 Sep 2025 23:08:40 -0700 Subject: [PATCH 6/7] Use FormattedMessage. Updated icons. Fixed bugs. --- src/app/(main)/links/LinkDeleteButton.tsx | 13 +++-- src/app/(main)/pixels/PixelDeleteButton.tsx | 13 +++-- src/app/(main)/teams/TeamLeaveForm.tsx | 13 +++-- src/app/(main)/teams/TeamsDataTable.tsx | 11 +--- src/app/(main)/teams/TeamsJoinButton.tsx | 4 +- .../teams/[teamId]/TeamMemberRemoveButton.tsx | 13 +++-- .../(main)/teams/[teamId]/TeamSettings.tsx | 4 +- src/app/(main)/websites/WebsitesPage.tsx | 2 +- .../[websiteId]/WebsiteFilterButton.tsx | 4 +- .../cohorts/CohortDeleteButton.tsx | 13 +++-- .../[websiteId]/events/EventsTable.tsx | 4 +- .../[websiteId]/realtime/RealtimeLog.tsx | 58 +++++++++++-------- .../segments/SegmentDeleteButton.tsx | 13 +++-- .../sessions/[sessionId]/SessionActivity.tsx | 4 +- src/components/common/PageHeader.tsx | 5 +- src/components/hooks/useMessages.ts | 28 ++++++--- src/components/icons.ts | 4 -- src/components/input/WebsiteDateFilter.tsx | 3 +- src/components/metrics/WeeklyTraffic.tsx | 2 +- src/lib/request.ts | 2 +- 20 files changed, 129 insertions(+), 84 deletions(-) diff --git a/src/app/(main)/links/LinkDeleteButton.tsx b/src/app/(main)/links/LinkDeleteButton.tsx index 05a3217f7..6754074f2 100644 --- a/src/app/(main)/links/LinkDeleteButton.tsx +++ b/src/app/(main)/links/LinkDeleteButton.tsx @@ -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({ {({ close }) => ( {name}, + }} + /> + } isLoading={isPending} error={getErrorMessage(error)} onConfirm={handleConfirm.bind(null, close)} diff --git a/src/app/(main)/pixels/PixelDeleteButton.tsx b/src/app/(main)/pixels/PixelDeleteButton.tsx index c90261e55..98e34c1f9 100644 --- a/src/app/(main)/pixels/PixelDeleteButton.tsx +++ b/src/app/(main)/pixels/PixelDeleteButton.tsx @@ -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({ {({ close }) => ( {name}, + }} + /> + } isLoading={isPending} error={getErrorMessage(error)} onConfirm={handleConfirm.bind(null, close)} diff --git a/src/app/(main)/teams/TeamLeaveForm.tsx b/src/app/(main)/teams/TeamLeaveForm.tsx index c8063365b..1c7846b8d 100644 --- a/src/app/(main)/teams/TeamLeaveForm.tsx +++ b/src/app/(main)/teams/TeamLeaveForm.tsx @@ -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 ( {teamName}, + }} + /> + } onConfirm={handleConfirm} onClose={onClose} isLoading={isPending} diff --git a/src/app/(main)/teams/TeamsDataTable.tsx b/src/app/(main)/teams/TeamsDataTable.tsx index 6475c456a..5f81bca17 100644 --- a/src/app/(main)/teams/TeamsDataTable.tsx +++ b/src/app/(main)/teams/TeamsDataTable.tsx @@ -1,16 +1,9 @@ -import { ReactNode } from 'react'; import Link from 'next/link'; import { DataGrid } from '@/components/common/DataGrid'; import { TeamsTable } from './TeamsTable'; import { useLoginQuery, useNavigation, useUserTeamsQuery } from '@/components/hooks'; -export function TeamsDataTable({ - showActions, -}: { - allowEdit?: boolean; - showActions?: boolean; - children?: ReactNode; -}) { +export function TeamsDataTable() { const { user } = useLoginQuery(); const query = useUserTeamsQuery(user.id); const { pathname } = useNavigation(); @@ -27,7 +20,7 @@ export function TeamsDataTable({ return ( {({ data }) => { - return ; + return ; }} ); diff --git a/src/app/(main)/teams/TeamsJoinButton.tsx b/src/app/(main)/teams/TeamsJoinButton.tsx index 7e1e24e7c..742954970 100644 --- a/src/app/(main)/teams/TeamsJoinButton.tsx +++ b/src/app/(main)/teams/TeamsJoinButton.tsx @@ -1,5 +1,5 @@ import { Button, Icon, Modal, DialogTrigger, Dialog, Text, useToast } from '@umami/react-zen'; -import { AddUser } 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() { diff --git a/src/app/(main)/teams/[teamId]/TeamMemberRemoveButton.tsx b/src/app/(main)/teams/[teamId]/TeamMemberRemoveButton.tsx index 22c871a30..065f637bd 100644 --- a/src/app/(main)/teams/[teamId]/TeamMemberRemoveButton.tsx +++ b/src/app/(main)/teams/[teamId]/TeamMemberRemoveButton.tsx @@ -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({ {({ close }) => ( {userName}, + }} + /> + } isLoading={isPending} error={error} onConfirm={handleConfirm.bind(null, close)} diff --git a/src/app/(main)/teams/[teamId]/TeamSettings.tsx b/src/app/(main)/teams/[teamId]/TeamSettings.tsx index c5994c0d3..9649c4f7c 100644 --- a/src/app/(main)/teams/[teamId]/TeamSettings.tsx +++ b/src/app/(main)/teams/[teamId]/TeamSettings.tsx @@ -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, Arrow } from '@/components/icons'; +import { Users, ArrowRight } from '@/components/icons'; import { TeamLeaveButton } from '@/app/(main)/teams/TeamLeaveButton'; import { TeamManage } from './TeamManage'; import { TeamEditForm } from './TeamEditForm'; @@ -35,7 +35,7 @@ export function TeamSettings({ teamId }: { teamId: string }) { - + {formatMessage(labels.teams)} diff --git a/src/app/(main)/websites/WebsitesPage.tsx b/src/app/(main)/websites/WebsitesPage.tsx index 313dccc7d..5f1aaae8d 100644 --- a/src/app/(main)/websites/WebsitesPage.tsx +++ b/src/app/(main)/websites/WebsitesPage.tsx @@ -14,7 +14,7 @@ export function WebsitesPage() { return ( - + diff --git a/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx b/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx index a4a0c1897..723c0249b 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteFilterButton.tsx @@ -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); }; diff --git a/src/app/(main)/websites/[websiteId]/cohorts/CohortDeleteButton.tsx b/src/app/(main)/websites/[websiteId]/cohorts/CohortDeleteButton.tsx index 239193b0d..32a9180db 100644 --- a/src/app/(main)/websites/[websiteId]/cohorts/CohortDeleteButton.tsx +++ b/src/app/(main)/websites/[websiteId]/cohorts/CohortDeleteButton.tsx @@ -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({ {({ close }) => ( {name}, + }} + /> + } isLoading={isPending} error={error} onConfirm={handleConfirm.bind(null, close)} diff --git a/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx b/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx index df4329012..7150f43f8 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventsTable.tsx @@ -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 = [] }) { - {row.eventName ? : } + {row.eventName ? : } {formatMessage(row.eventName ? labels.triggeredEvent : labels.viewedPage)} diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx index c7bd13476..e994831d6 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx @@ -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]: , - [TYPE_SESSION]: , - [TYPE_EVENT]: , + [TYPE_SESSION]: , + [TYPE_EVENT]: , }; 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: {eventName || formatMessage(labels.unknown)}, - url: ( - - {urlPath} - - ), - }); + return ( + {eventName || formatMessage(labels.unknown)}, + url: ( + + {urlPath} + + ), + }} + /> + ); } if (__type === TYPE_PAGEVIEW) { @@ -104,12 +109,17 @@ export function RealtimeLog({ data }: { data: any }) { } if (__type === TYPE_SESSION) { - return formatMessage(messages.visitorLog, { - country: {countryNames[country] || formatMessage(labels.unknown)}, - browser: {BROWSERS[browser]}, - os: {OS_NAMES[os] || os}, - device: {formatMessage(labels[device] || labels.unknown)}, - }); + return ( + {countryNames[country] || formatMessage(labels.unknown)}, + browser: {BROWSERS[browser]}, + os: {OS_NAMES[os] || os}, + device: {formatMessage(labels[device] || labels.unknown)}, + }} + /> + ); } }; diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton.tsx index a588d11a3..6cbc24bd8 100644 --- a/src/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton.tsx +++ b/src/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton.tsx @@ -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({ {({ close }) => ( {name}, + }} + /> + } isLoading={isPending} error={error} onConfirm={handleConfirm.bind(null, close)} diff --git a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx index d88fff7ab..4ef120f71 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/[sessionId]/SessionActivity.tsx @@ -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')} - {eventName ? : } + {eventName ? : } {eventName ? formatMessage(labels.triggeredEvent) diff --git a/src/components/common/PageHeader.tsx b/src/components/common/PageHeader.tsx index 4d0dac5c2..a5850f732 100644 --- a/src/components/common/PageHeader.tsx +++ b/src/components/common/PageHeader.tsx @@ -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} > - + + {label} {icon && ( diff --git a/src/components/hooks/useMessages.ts b/src/components/hooks/useMessages.ts index 48b1ba60f..42bbeb01e 100644 --- a/src/components/hooks/useMessages.ts +++ b/src/components/hooks/useMessages.ts @@ -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, + 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, + descriptor: MessageDescriptor, + values?: Record, opts?: any, ) => { return descriptor ? intl.formatMessage(descriptor, values, opts) : null; }; - return { formatMessage, messages, labels, getMessage, getErrorMessage }; + return { formatMessage, messages, labels, getMessage, getErrorMessage, FormattedMessage }; } diff --git a/src/components/icons.ts b/src/components/icons.ts index 0280c31a5..bdba73673 100644 --- a/src/components/icons.ts +++ b/src/components/icons.ts @@ -1,17 +1,13 @@ 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, } from '@/components/svg'; diff --git a/src/components/input/WebsiteDateFilter.tsx b/src/components/input/WebsiteDateFilter.tsx index 475fce44a..1045de211 100644 --- a/src/components/input/WebsiteDateFilter.tsx +++ b/src/components/input/WebsiteDateFilter.tsx @@ -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 })); }; diff --git a/src/components/metrics/WeeklyTraffic.tsx b/src/components/metrics/WeeklyTraffic.tsx index ff6147392..82d1c1c1f 100644 --- a/src/components/metrics/WeeklyTraffic.tsx +++ b/src/components/metrics/WeeklyTraffic.tsx @@ -85,7 +85,7 @@ export function WeeklyTraffic({ websiteId }: { websiteId: string }) { height="16px" borderRadius="full" style={{ margin: '0 auto' }} - role="cell" + role="button" > Date: Wed, 24 Sep 2025 00:07:17 -0700 Subject: [PATCH 7/7] Added label to PageHeader. Style fixes. --- package.components.json | 2 +- src/app/(main)/SideNav.tsx | 2 +- src/app/(main)/links/[linkId]/LinkHeader.tsx | 2 +- src/app/(main)/pixels/[pixelId]/PixelHeader.tsx | 2 +- src/app/(main)/websites/WebsitesPage.tsx | 2 +- src/components/common/PageHeader.tsx | 6 +++++- 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/package.components.json b/package.components.json index ba951df99..927e06604 100644 --- a/package.components.json +++ b/package.components.json @@ -1,6 +1,6 @@ { "name": "@umami/components", - "version": "0.125.0", + "version": "0.127.0", "description": "Umami React components.", "author": "Mike Cao ", "license": "MIT", diff --git a/src/app/(main)/SideNav.tsx b/src/app/(main)/SideNav.tsx index c351ef47f..6e67465cd 100644 --- a/src/app/(main)/SideNav.tsx +++ b/src/app/(main)/SideNav.tsx @@ -69,7 +69,7 @@ export function SideNav(props: SidebarProps) { diff --git a/src/app/(main)/links/[linkId]/LinkHeader.tsx b/src/app/(main)/links/[linkId]/LinkHeader.tsx index 0d325ebb4..ba1de3ee2 100644 --- a/src/app/(main)/links/[linkId]/LinkHeader.tsx +++ b/src/app/(main)/links/[linkId]/LinkHeader.tsx @@ -10,7 +10,7 @@ export function LinkHeader() { const link = useLink(); return ( - }> + } marginBottom="3"> diff --git a/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx b/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx index 991d807d5..7e1a62b87 100644 --- a/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx +++ b/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx @@ -10,7 +10,7 @@ export function PixelHeader() { const pixel = usePixel(); return ( - }> + } marginBottom="3"> diff --git a/src/app/(main)/websites/WebsitesPage.tsx b/src/app/(main)/websites/WebsitesPage.tsx index 5f1aaae8d..313dccc7d 100644 --- a/src/app/(main)/websites/WebsitesPage.tsx +++ b/src/app/(main)/websites/WebsitesPage.tsx @@ -14,7 +14,7 @@ export function WebsitesPage() { return ( - + diff --git a/src/components/common/PageHeader.tsx b/src/components/common/PageHeader.tsx index a5850f732..4049e0c6e 100644 --- a/src/components/common/PageHeader.tsx +++ b/src/components/common/PageHeader.tsx @@ -38,7 +38,11 @@ export function PageHeader({ )} {title && {title}} - {description && {description}} + {description && ( + + {description} + + )} {children}