Merge branch 'dev' into codex/fix-umami-4062
This commit is contained in:
@@ -24,13 +24,13 @@ body:
|
|||||||
render: shell
|
render: shell
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: Which Umami version are you using? (if relevant)
|
label: Which Umami version are you using?
|
||||||
description: 'For example: 2.18.0, 2.15.1, 1.39.0, etc'
|
description: 'For example: 2.18.0, 2.15.1, 1.39.0, etc'
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: Which browser are you using? (if relevant)
|
label: How are you deploying your application?
|
||||||
description: 'For example: Chrome, Edge, Firefox, etc'
|
description: 'For example: Vercel, Railway, Docker, etc'
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: How are you deploying your application? (if relevant)
|
label: Which browser are you using?
|
||||||
description: 'For example: Vercel, Railway, Docker, etc'
|
description: 'For example: Chrome, Edge, Firefox, etc'
|
||||||
+32
-18
@@ -23,6 +23,15 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- type: standard
|
||||||
|
base_path: ""
|
||||||
|
suffix: ""
|
||||||
|
- type: base
|
||||||
|
base_path: "/umami"
|
||||||
|
suffix: "-base"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
@@ -55,6 +64,7 @@ jobs:
|
|||||||
REF_TYPE="${{ github.ref_type }}"
|
REF_TYPE="${{ github.ref_type }}"
|
||||||
REF_NAME="${{ github.ref_name }}"
|
REF_NAME="${{ github.ref_name }}"
|
||||||
INCLUDE_LATEST="${{ github.event.inputs.include_latest }}"
|
INCLUDE_LATEST="${{ github.event.inputs.include_latest }}"
|
||||||
|
SUFFIX="${{ matrix.suffix }}"
|
||||||
|
|
||||||
# Determine version source
|
# Determine version source
|
||||||
if [[ -n "$INPUT" ]]; then
|
if [[ -n "$INPUT" ]]; then
|
||||||
@@ -74,30 +84,30 @@ jobs:
|
|||||||
|
|
||||||
if [[ "$VERSION" == *-* ]]; then
|
if [[ "$VERSION" == *-* ]]; then
|
||||||
# prerelease: only version tag
|
# prerelease: only version tag
|
||||||
GHCR_TAGS="ghcr.io/${{ github.repository }}:$VERSION"
|
GHCR_TAGS="ghcr.io/${{ github.repository }}:$VERSION$SUFFIX"
|
||||||
DOCKER_TAGS="umamisoftware/umami:$VERSION"
|
DOCKER_TAGS="umamisoftware/umami:$VERSION$SUFFIX"
|
||||||
else
|
else
|
||||||
# stable release: version + hierarchy
|
# stable release: version + hierarchy
|
||||||
GHCR_TAGS="ghcr.io/${{ github.repository }}:$VERSION"
|
GHCR_TAGS="ghcr.io/${{ github.repository }}:$VERSION$SUFFIX"
|
||||||
GHCR_TAGS="$GHCR_TAGS,ghcr.io/${{ github.repository }}:${MAJOR}.${MINOR}"
|
GHCR_TAGS="$GHCR_TAGS,ghcr.io/${{ github.repository }}:${MAJOR}.${MINOR}$SUFFIX"
|
||||||
GHCR_TAGS="$GHCR_TAGS,ghcr.io/${{ github.repository }}:${MAJOR}"
|
GHCR_TAGS="$GHCR_TAGS,ghcr.io/${{ github.repository }}:${MAJOR}$SUFFIX"
|
||||||
GHCR_TAGS="$GHCR_TAGS,ghcr.io/${{ github.repository }}:postgresql-latest"
|
GHCR_TAGS="$GHCR_TAGS,ghcr.io/${{ github.repository }}:postgresql-latest$SUFFIX"
|
||||||
|
|
||||||
DOCKER_TAGS="umamisoftware/umami:$VERSION"
|
DOCKER_TAGS="umamisoftware/umami:$VERSION$SUFFIX"
|
||||||
DOCKER_TAGS="$DOCKER_TAGS,umamisoftware/umami:${MAJOR}.${MINOR}"
|
DOCKER_TAGS="$DOCKER_TAGS,umamisoftware/umami:${MAJOR}.${MINOR}$SUFFIX"
|
||||||
DOCKER_TAGS="$DOCKER_TAGS,umamisoftware/umami:${MAJOR}"
|
DOCKER_TAGS="$DOCKER_TAGS,umamisoftware/umami:${MAJOR}$SUFFIX"
|
||||||
DOCKER_TAGS="$DOCKER_TAGS,umamisoftware/umami:postgresql-latest"
|
DOCKER_TAGS="$DOCKER_TAGS,umamisoftware/umami:postgresql-latest$SUFFIX"
|
||||||
|
|
||||||
# Add latest tag based on trigger and input
|
# Add latest tag based on trigger and input
|
||||||
if [[ "$REF_TYPE" == "tag" ]] || [[ "$INCLUDE_LATEST" == "true" ]]; then
|
if [[ "$REF_TYPE" == "tag" ]] || [[ "$INCLUDE_LATEST" == "true" ]]; then
|
||||||
GHCR_TAGS="$GHCR_TAGS,ghcr.io/${{ github.repository }}:latest"
|
GHCR_TAGS="$GHCR_TAGS,ghcr.io/${{ github.repository }}:latest$SUFFIX"
|
||||||
DOCKER_TAGS="$DOCKER_TAGS,umamisoftware/umami:latest"
|
DOCKER_TAGS="$DOCKER_TAGS,umamisoftware/umami:latest$SUFFIX"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# Non-tag build (e.g. from main branch)
|
# Non-tag build (e.g. from main branch)
|
||||||
GHCR_TAGS="ghcr.io/${{ github.repository }}:${REF_NAME}"
|
GHCR_TAGS="ghcr.io/${{ github.repository }}:${REF_NAME}$SUFFIX"
|
||||||
DOCKER_TAGS="umamisoftware/umami:${REF_NAME}"
|
DOCKER_TAGS="umamisoftware/umami:${REF_NAME}$SUFFIX"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "ghcr_tags=$GHCR_TAGS" >> $GITHUB_OUTPUT
|
echo "ghcr_tags=$GHCR_TAGS" >> $GITHUB_OUTPUT
|
||||||
@@ -112,8 +122,10 @@ jobs:
|
|||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.compute.outputs.ghcr_tags }}
|
tags: ${{ steps.compute.outputs.ghcr_tags }}
|
||||||
cache-from: type=gha
|
build-args: |
|
||||||
cache-to: type=gha,mode=max
|
BASE_PATH=${{ matrix.base_path }}
|
||||||
|
cache-from: type=gha,scope=${{ matrix.type }}
|
||||||
|
cache-to: type=gha,mode=max,scope=${{ matrix.type }}
|
||||||
|
|
||||||
- name: Build and push to Docker Hub
|
- name: Build and push to Docker Hub
|
||||||
if: github.repository == 'umami-software/umami'
|
if: github.repository == 'umami-software/umami'
|
||||||
@@ -123,5 +135,7 @@ jobs:
|
|||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.compute.outputs.docker_tags }}
|
tags: ${{ steps.compute.outputs.docker_tags }}
|
||||||
cache-from: type=gha
|
build-args: |
|
||||||
cache-to: type=gha,mode=max
|
BASE_PATH=${{ matrix.base_path }}
|
||||||
|
cache-from: type=gha,scope=${{ matrix.type }}
|
||||||
|
cache-to: type=gha,mode=max,scope=${{ matrix.type }}
|
||||||
+8
-1
@@ -11,16 +11,19 @@ package-lock.json
|
|||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
|
next-env.d.ts
|
||||||
/.next
|
/.next
|
||||||
/out
|
/out
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
/public/script.js
|
/public/script.js
|
||||||
|
/public/recorder.js
|
||||||
/geo
|
/geo
|
||||||
/dist
|
/dist
|
||||||
/generated
|
/generated
|
||||||
/src/generated
|
/src/generated
|
||||||
|
pm2.yml
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -30,6 +33,11 @@ package-lock.json
|
|||||||
*.log
|
*.log
|
||||||
.vscode
|
.vscode
|
||||||
.tool-versions
|
.tool-versions
|
||||||
|
.claude
|
||||||
|
.agents
|
||||||
|
tmpclaude*
|
||||||
|
CLAUDE.md
|
||||||
|
nul
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
@@ -42,4 +50,3 @@ yarn-error.log*
|
|||||||
*.env.*
|
*.env.*
|
||||||
|
|
||||||
*.dev.yml
|
*.dev.yml
|
||||||
|
|
||||||
+1
-1
@@ -16,7 +16,7 @@ COPY --from=deps /app/node_modules ./node_modules
|
|||||||
COPY . .
|
COPY . .
|
||||||
COPY docker/middleware.ts ./src
|
COPY docker/middleware.ts ./src
|
||||||
|
|
||||||
ARG BASE_PATH
|
ARG BASE_PATH=""
|
||||||
|
|
||||||
ENV BASE_PATH=$BASE_PATH
|
ENV BASE_PATH=$BASE_PATH
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
-- Add performance columns to website_event
|
||||||
|
ALTER TABLE umami.website_event ADD COLUMN lcp Nullable(Decimal(10, 1)) AFTER twclid;
|
||||||
|
ALTER TABLE umami.website_event ADD COLUMN inp Nullable(Decimal(10, 1)) AFTER lcp;
|
||||||
|
ALTER TABLE umami.website_event ADD COLUMN cls Nullable(Decimal(10, 4)) AFTER inp;
|
||||||
|
ALTER TABLE umami.website_event ADD COLUMN fcp Nullable(Decimal(10, 1)) AFTER cls;
|
||||||
|
ALTER TABLE umami.website_event ADD COLUMN ttfb Nullable(Decimal(10, 1)) AFTER fcp;
|
||||||
|
|
||||||
|
-- Update materialized view to exclude performance events from view counts
|
||||||
|
DROP TABLE umami.website_event_stats_hourly_mv;
|
||||||
|
|
||||||
|
CREATE MATERIALIZED VIEW umami.website_event_stats_hourly_mv
|
||||||
|
TO umami.website_event_stats_hourly
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
website_id,
|
||||||
|
session_id,
|
||||||
|
visit_id,
|
||||||
|
hostnames as hostname,
|
||||||
|
browser,
|
||||||
|
os,
|
||||||
|
device,
|
||||||
|
screen,
|
||||||
|
language,
|
||||||
|
country,
|
||||||
|
region,
|
||||||
|
city,
|
||||||
|
entry_url,
|
||||||
|
exit_url,
|
||||||
|
url_paths as url_path,
|
||||||
|
url_query,
|
||||||
|
utm_source,
|
||||||
|
utm_medium,
|
||||||
|
utm_campaign,
|
||||||
|
utm_content,
|
||||||
|
utm_term,
|
||||||
|
referrer_domain,
|
||||||
|
page_title,
|
||||||
|
gclid,
|
||||||
|
fbclid,
|
||||||
|
msclkid,
|
||||||
|
ttclid,
|
||||||
|
li_fat_id,
|
||||||
|
twclid,
|
||||||
|
event_type,
|
||||||
|
event_name,
|
||||||
|
views,
|
||||||
|
min_time,
|
||||||
|
max_time,
|
||||||
|
tag,
|
||||||
|
distinct_id,
|
||||||
|
timestamp as created_at
|
||||||
|
FROM (SELECT
|
||||||
|
website_id,
|
||||||
|
session_id,
|
||||||
|
visit_id,
|
||||||
|
arrayFilter(x -> x != '', groupArray(hostname)) hostnames,
|
||||||
|
browser,
|
||||||
|
os,
|
||||||
|
device,
|
||||||
|
screen,
|
||||||
|
language,
|
||||||
|
country,
|
||||||
|
region,
|
||||||
|
city,
|
||||||
|
argMinState(url_path, created_at) entry_url,
|
||||||
|
argMaxState(url_path, created_at) exit_url,
|
||||||
|
arrayFilter(x -> x != '', groupArray(url_path)) as url_paths,
|
||||||
|
arrayFilter(x -> x != '', groupArray(url_query)) url_query,
|
||||||
|
arrayFilter(x -> x != '', groupArray(utm_source)) utm_source,
|
||||||
|
arrayFilter(x -> x != '', groupArray(utm_medium)) utm_medium,
|
||||||
|
arrayFilter(x -> x != '', groupArray(utm_campaign)) utm_campaign,
|
||||||
|
arrayFilter(x -> x != '', groupArray(utm_content)) utm_content,
|
||||||
|
arrayFilter(x -> x != '', groupArray(utm_term)) utm_term,
|
||||||
|
arrayFilter(x -> x != '' and x != hostname, groupArray(referrer_domain)) referrer_domain,
|
||||||
|
arrayFilter(x -> x != '', groupArray(page_title)) page_title,
|
||||||
|
arrayFilter(x -> x != '', groupArray(gclid)) gclid,
|
||||||
|
arrayFilter(x -> x != '', groupArray(fbclid)) fbclid,
|
||||||
|
arrayFilter(x -> x != '', groupArray(msclkid)) msclkid,
|
||||||
|
arrayFilter(x -> x != '', groupArray(ttclid)) ttclid,
|
||||||
|
arrayFilter(x -> x != '', groupArray(li_fat_id)) li_fat_id,
|
||||||
|
arrayFilter(x -> x != '', groupArray(twclid)) twclid,
|
||||||
|
event_type,
|
||||||
|
if(event_type = 2, groupArray(event_name), []) event_name,
|
||||||
|
sumIf(1, event_type NOT IN (2, 5)) views,
|
||||||
|
min(created_at) min_time,
|
||||||
|
max(created_at) max_time,
|
||||||
|
arrayFilter(x -> x != '', groupArray(tag)) tag,
|
||||||
|
distinct_id,
|
||||||
|
toStartOfHour(created_at) timestamp
|
||||||
|
FROM umami.website_event
|
||||||
|
GROUP BY website_id,
|
||||||
|
session_id,
|
||||||
|
visit_id,
|
||||||
|
hostname,
|
||||||
|
browser,
|
||||||
|
os,
|
||||||
|
device,
|
||||||
|
screen,
|
||||||
|
language,
|
||||||
|
country,
|
||||||
|
region,
|
||||||
|
city,
|
||||||
|
event_type,
|
||||||
|
distinct_id,
|
||||||
|
timestamp);
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- Create session_replay
|
||||||
|
CREATE TABLE umami.session_replay
|
||||||
|
(
|
||||||
|
replay_id UUID,
|
||||||
|
website_id UUID,
|
||||||
|
session_id UUID,
|
||||||
|
visit_id UUID,
|
||||||
|
chunk_index UInt32,
|
||||||
|
events String CODEC(ZSTD(3)),
|
||||||
|
event_count UInt32,
|
||||||
|
started_at DateTime64(6),
|
||||||
|
ended_at DateTime64(6),
|
||||||
|
created_at DateTime64(6) DEFAULT now64(6)
|
||||||
|
)
|
||||||
|
ENGINE = MergeTree()
|
||||||
|
PARTITION BY toYYYYMM(created_at)
|
||||||
|
ORDER BY (replay_id, website_id, session_id, visit_id, chunk_index)
|
||||||
|
SETTINGS index_granularity = 8192;
|
||||||
@@ -34,6 +34,12 @@ CREATE TABLE umami.website_event
|
|||||||
ttclid String,
|
ttclid String,
|
||||||
li_fat_id String,
|
li_fat_id String,
|
||||||
twclid String,
|
twclid String,
|
||||||
|
--performance
|
||||||
|
lcp Nullable(Decimal(10, 1)),
|
||||||
|
inp Nullable(Decimal(10, 1)),
|
||||||
|
cls Nullable(Decimal(10, 4)),
|
||||||
|
fcp Nullable(Decimal(10, 1)),
|
||||||
|
ttfb Nullable(Decimal(10, 1)),
|
||||||
--events
|
--events
|
||||||
event_type UInt32,
|
event_type UInt32,
|
||||||
event_name String,
|
event_name String,
|
||||||
@@ -209,7 +215,7 @@ FROM (SELECT
|
|||||||
arrayFilter(x -> x != '', groupArray(twclid)) twclid,
|
arrayFilter(x -> x != '', groupArray(twclid)) twclid,
|
||||||
event_type,
|
event_type,
|
||||||
if(event_type = 2, groupArray(event_name), []) event_name,
|
if(event_type = 2, groupArray(event_name), []) event_name,
|
||||||
sumIf(1, event_type != 2) views,
|
sumIf(1, event_type NOT IN (2, 5)) views,
|
||||||
min(created_at) min_time,
|
min(created_at) min_time,
|
||||||
max(created_at) max_time,
|
max(created_at) max_time,
|
||||||
arrayFilter(x -> x != '', groupArray(tag)) tag,
|
arrayFilter(x -> x != '', groupArray(tag)) tag,
|
||||||
@@ -281,3 +287,22 @@ JOIN (SELECT event_id, string_value as currency
|
|||||||
WHERE positionCaseInsensitive(data_key, 'currency') > 0) c
|
WHERE positionCaseInsensitive(data_key, 'currency') > 0) c
|
||||||
ON c.event_id = ed.event_id
|
ON c.event_id = ed.event_id
|
||||||
WHERE positionCaseInsensitive(data_key, 'revenue') > 0;
|
WHERE positionCaseInsensitive(data_key, 'revenue') > 0;
|
||||||
|
|
||||||
|
-- Create session_replay
|
||||||
|
CREATE TABLE umami.session_replay
|
||||||
|
(
|
||||||
|
replay_id UUID,
|
||||||
|
website_id UUID,
|
||||||
|
session_id UUID,
|
||||||
|
visit_id UUID,
|
||||||
|
chunk_index UInt32,
|
||||||
|
events String CODEC(ZSTD(3)),
|
||||||
|
event_count UInt32,
|
||||||
|
started_at DateTime64(6),
|
||||||
|
ended_at DateTime64(6),
|
||||||
|
created_at DateTime64(6) DEFAULT now64(6)
|
||||||
|
)
|
||||||
|
ENGINE = MergeTree()
|
||||||
|
PARTITION BY toYYYYMM(created_at)
|
||||||
|
ORDER BY (replay_id, website_id, session_id, visit_id, chunk_index)
|
||||||
|
SETTINGS index_granularity = 8192;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { type NextRequest, NextResponse } from 'next/server';
|
import { type NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { matchesConfiguredPath } from '@/lib/match-configured-path';
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: '/:path*',
|
matcher: '/:path*',
|
||||||
@@ -7,6 +8,7 @@ export const config = {
|
|||||||
const TRACKER_PATH = '/script.js';
|
const TRACKER_PATH = '/script.js';
|
||||||
const COLLECT_PATH = '/api/send';
|
const COLLECT_PATH = '/api/send';
|
||||||
const LOGIN_PATH = '/login';
|
const LOGIN_PATH = '/login';
|
||||||
|
const BASE_PATH = process.env.BASE_PATH || '';
|
||||||
|
|
||||||
const apiHeaders = {
|
const apiHeaders = {
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
@@ -27,7 +29,7 @@ function customCollectEndpoint(request: NextRequest) {
|
|||||||
if (collectEndpoint) {
|
if (collectEndpoint) {
|
||||||
const url = request.nextUrl.clone();
|
const url = request.nextUrl.clone();
|
||||||
|
|
||||||
if (url.pathname.endsWith(collectEndpoint)) {
|
if (matchesConfiguredPath(url.pathname, collectEndpoint, BASE_PATH)) {
|
||||||
url.pathname = COLLECT_PATH;
|
url.pathname = COLLECT_PATH;
|
||||||
return NextResponse.rewrite(url, { headers: apiHeaders });
|
return NextResponse.rewrite(url, { headers: apiHeaders });
|
||||||
}
|
}
|
||||||
@@ -41,7 +43,7 @@ function customScriptName(request: NextRequest) {
|
|||||||
const url = request.nextUrl.clone();
|
const url = request.nextUrl.clone();
|
||||||
const names = scriptName.split(',').map(name => name.trim().replace(/^\/+/, ''));
|
const names = scriptName.split(',').map(name => name.trim().replace(/^\/+/, ''));
|
||||||
|
|
||||||
if (names.find(name => url.pathname.endsWith(name))) {
|
if (names.find(name => matchesConfiguredPath(url.pathname, name, BASE_PATH))) {
|
||||||
url.pathname = TRACKER_PATH;
|
url.pathname = TRACKER_PATH;
|
||||||
return NextResponse.rewrite(url, { headers: trackerHeaders });
|
return NextResponse.rewrite(url, { headers: trackerHeaders });
|
||||||
}
|
}
|
||||||
@@ -51,7 +53,7 @@ function customScriptName(request: NextRequest) {
|
|||||||
function customScriptUrl(request: NextRequest) {
|
function customScriptUrl(request: NextRequest) {
|
||||||
const scriptUrl = process.env.TRACKER_SCRIPT_URL;
|
const scriptUrl = process.env.TRACKER_SCRIPT_URL;
|
||||||
|
|
||||||
if (scriptUrl && request.nextUrl.pathname.endsWith(TRACKER_PATH)) {
|
if (scriptUrl && matchesConfiguredPath(request.nextUrl.pathname, TRACKER_PATH, BASE_PATH)) {
|
||||||
return NextResponse.rewrite(scriptUrl, { headers: trackerHeaders });
|
return NextResponse.rewrite(scriptUrl, { headers: trackerHeaders });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,7 +61,7 @@ function customScriptUrl(request: NextRequest) {
|
|||||||
function disableLogin(request: NextRequest) {
|
function disableLogin(request: NextRequest) {
|
||||||
const loginDisabled = process.env.DISABLE_LOGIN;
|
const loginDisabled = process.env.DISABLE_LOGIN;
|
||||||
|
|
||||||
if (loginDisabled && request.nextUrl.pathname.endsWith(LOGIN_PATH)) {
|
if (loginDisabled && matchesConfiguredPath(request.nextUrl.pathname, LOGIN_PATH, BASE_PATH)) {
|
||||||
return new NextResponse('Access denied', { status: 403 });
|
return new NextResponse('Access denied', { status: 403 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
-6
@@ -1,6 +0,0 @@
|
|||||||
/// <reference types="next" />
|
|
||||||
/// <reference types="next/image-types/global" />
|
|
||||||
/// <reference path="./.next/types/routes.d.ts" />
|
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
||||||
+31
-9
@@ -1,18 +1,26 @@
|
|||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
|
import createNextIntlPlugin from 'next-intl/plugin';
|
||||||
import pkg from './package.json' with { type: 'json' };
|
import pkg from './package.json' with { type: 'json' };
|
||||||
|
|
||||||
|
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
|
||||||
|
|
||||||
const TRACKER_SCRIPT = '/script.js';
|
const TRACKER_SCRIPT = '/script.js';
|
||||||
|
|
||||||
|
const isProd = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
const basePath = process.env.BASE_PATH || '';
|
const basePath = process.env.BASE_PATH || '';
|
||||||
const cloudMode = process.env.CLOUD_MODE || '';
|
const cloudMode = process.env.CLOUD_MODE || '';
|
||||||
const cloudUrl = process.env.CLOUD_URL || '';
|
const cloudUrl = process.env.CLOUD_URL || '';
|
||||||
const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT || '';
|
const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT || '';
|
||||||
const corsMaxAge = process.env.CORS_MAX_AGE || '';
|
const corsMaxAge = process.env.CORS_MAX_AGE || '';
|
||||||
|
const defaultCurrency = process.env.DEFAULT_CURRENCY || '';
|
||||||
const defaultLocale = process.env.DEFAULT_LOCALE || '';
|
const defaultLocale = process.env.DEFAULT_LOCALE || '';
|
||||||
const forceSSL = process.env.FORCE_SSL || '';
|
const forceSSL = process.env.FORCE_SSL || '';
|
||||||
const frameAncestors = process.env.ALLOWED_FRAME_URLS || '';
|
const frameAncestors = process.env.ALLOWED_FRAME_URLS || '';
|
||||||
const trackerScriptName = process.env.TRACKER_SCRIPT_NAME || '';
|
const trackerScriptName = process.env.TRACKER_SCRIPT_NAME || '';
|
||||||
const trackerScriptURL = process.env.TRACKER_SCRIPT_URL || '';
|
const trackerScriptURL = process.env.TRACKER_SCRIPT_URL || '';
|
||||||
|
const selfTrack = process.env.UMAMI_SELF_TRACK || '';
|
||||||
|
const selfRecord = process.env.UMAMI_SELF_RECORD || '';
|
||||||
|
|
||||||
const contentSecurityPolicy = `
|
const contentSecurityPolicy = `
|
||||||
default-src 'self';
|
default-src 'self';
|
||||||
@@ -84,11 +92,14 @@ const headers = [
|
|||||||
source: '/:path*',
|
source: '/:path*',
|
||||||
headers: defaultHeaders,
|
headers: defaultHeaders,
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
if (isProd) {
|
||||||
|
headers.push({
|
||||||
source: TRACKER_SCRIPT,
|
source: TRACKER_SCRIPT,
|
||||||
headers: trackerHeaders,
|
headers: trackerHeaders,
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
const rewrites = [];
|
const rewrites = [];
|
||||||
|
|
||||||
@@ -112,6 +123,16 @@ if (collectApiEndpoint) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const redirects = [
|
const redirects = [
|
||||||
|
{
|
||||||
|
source: '/teams/:id/dashboard/edit',
|
||||||
|
destination: '/dashboard/edit',
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/teams/:id/dashboard',
|
||||||
|
destination: '/dashboard',
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
source: '/settings',
|
source: '/settings',
|
||||||
destination: '/settings/preferences',
|
destination: '/settings/preferences',
|
||||||
@@ -155,7 +176,7 @@ if (trackerScriptName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cloudMode) {
|
if (isProd && cloudMode) {
|
||||||
rewrites.push({
|
rewrites.push({
|
||||||
source: '/script.js',
|
source: '/script.js',
|
||||||
destination: 'https://cloud.umami.is/script.js',
|
destination: 'https://cloud.umami.is/script.js',
|
||||||
@@ -163,23 +184,24 @@ if (cloudMode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
export default {
|
export default withNextIntl({
|
||||||
reactStrictMode: false,
|
reactStrictMode: false,
|
||||||
env: {
|
env: {
|
||||||
basePath,
|
basePath,
|
||||||
cloudMode,
|
cloudMode,
|
||||||
cloudUrl,
|
cloudUrl,
|
||||||
currentVersion: pkg.version,
|
currentVersion: pkg.version,
|
||||||
|
defaultCurrency,
|
||||||
defaultLocale,
|
defaultLocale,
|
||||||
|
selfTrack,
|
||||||
|
selfRecord,
|
||||||
},
|
},
|
||||||
basePath,
|
basePath,
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
eslint: {
|
|
||||||
ignoreDuringBuilds: true,
|
|
||||||
},
|
|
||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
|
devIndicators: false,
|
||||||
async headers() {
|
async headers() {
|
||||||
return headers;
|
return headers;
|
||||||
},
|
},
|
||||||
@@ -199,4 +221,4 @@ export default {
|
|||||||
async redirects() {
|
async redirects() {
|
||||||
return [...redirects];
|
return [...redirects];
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
+46
-49
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "umami",
|
"name": "umami",
|
||||||
"version": "3.0.3",
|
"version": "3.1.0",
|
||||||
"description": "A modern, privacy-focused alternative to Google Analytics.",
|
"description": "A modern, privacy-focused alternative to Google Analytics.",
|
||||||
"author": "Umami Software, Inc. <hello@umami.is>",
|
"author": "Umami Software, Inc. <hello@umami.is>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -11,8 +11,8 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3001 --turbo",
|
"dev": "dotenv next dev --turbo",
|
||||||
"build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app",
|
"build": "npm-run-all check-env build-db check-db build-tracker build-recorder build-geo build-app",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"build-docker": "npm-run-all build-db build-tracker build-geo build-app",
|
"build-docker": "npm-run-all build-db build-tracker build-geo build-app",
|
||||||
"start-docker": "npm-run-all check-db update-tracker start-server",
|
"start-docker": "npm-run-all check-db update-tracker start-server",
|
||||||
@@ -20,10 +20,11 @@
|
|||||||
"start-server": "node server.js",
|
"start-server": "node server.js",
|
||||||
"build-app": "next build --turbo",
|
"build-app": "next build --turbo",
|
||||||
"build-icons": "svgr ./src/assets --out-dir src/components/svg --typescript",
|
"build-icons": "svgr ./src/assets --out-dir src/components/svg --typescript",
|
||||||
"build-components": "tsup",
|
"build-components": "node scripts/bump-components.js && tsup",
|
||||||
"build-tracker": "rollup -c rollup.tracker.config.js",
|
"build-tracker": "rollup -c rollup.tracker.config.js",
|
||||||
|
"build-recorder": "rollup -c rollup.recorder.config.js",
|
||||||
"build-prisma-client": "node scripts/build-prisma-client.js",
|
"build-prisma-client": "node scripts/build-prisma-client.js",
|
||||||
"build-lang": "npm-run-all format-lang compile-lang download-country-names download-language-names clean-lang",
|
"build-lang": "npm-run-all download-country-names download-language-names",
|
||||||
"build-geo": "node scripts/build-geo.js",
|
"build-geo": "node scripts/build-geo.js",
|
||||||
"build-db": "npm-run-all build-db-client build-prisma-client",
|
"build-db": "npm-run-all build-db-client build-prisma-client",
|
||||||
"build-db-schema": "prisma db pull",
|
"build-db-schema": "prisma db pull",
|
||||||
@@ -32,13 +33,8 @@
|
|||||||
"update-db": "prisma migrate deploy",
|
"update-db": "prisma migrate deploy",
|
||||||
"check-db": "node scripts/check-db.js",
|
"check-db": "node scripts/check-db.js",
|
||||||
"check-env": "node scripts/check-env.js",
|
"check-env": "node scripts/check-env.js",
|
||||||
|
"check-missing-messages": "node scripts/check-missing-messages.js",
|
||||||
"copy-db-files": "node scripts/copy-db-files.js",
|
"copy-db-files": "node scripts/copy-db-files.js",
|
||||||
"extract-messages": "formatjs extract \"src/components/messages.ts\" --out-file build/extracted-messages.json",
|
|
||||||
"merge-messages": "node scripts/merge-messages.js",
|
|
||||||
"generate-lang": "npm-run-all extract-messages merge-messages",
|
|
||||||
"format-lang": "node scripts/format-lang.js",
|
|
||||||
"compile-lang": "formatjs compile-folder --ast build/messages public/intl/messages",
|
|
||||||
"clean-lang": "prettier --write ./public/intl/**/*.json",
|
|
||||||
"download-country-names": "node scripts/download-country-names.js",
|
"download-country-names": "node scripts/download-country-names.js",
|
||||||
"download-language-names": "node scripts/download-language-names.js",
|
"download-language-names": "node scripts/download-language-names.js",
|
||||||
"change-password": "node scripts/change-password.js",
|
"change-password": "node scripts/change-password.js",
|
||||||
@@ -61,74 +57,75 @@
|
|||||||
".next/cache"
|
".next/cache"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clickhouse/client": "^1.12.0",
|
"@clickhouse/client": "^1.17.0",
|
||||||
"@date-fns/utc": "^1.2.0",
|
"@date-fns/utc": "^1.2.0",
|
||||||
"@dicebear/collection": "^9.2.3",
|
"@dicebear/collection": "^9.3.1",
|
||||||
"@dicebear/core": "^9.2.3",
|
"@dicebear/core": "^9.3.1",
|
||||||
"@fontsource/inter": "^5.2.8",
|
|
||||||
"@hello-pangea/dnd": "^17.0.0",
|
"@hello-pangea/dnd": "^17.0.0",
|
||||||
"@prisma/adapter-pg": "^6.18.0",
|
"@prisma/adapter-pg": "^7.3.0",
|
||||||
"@prisma/client": "^6.18.0",
|
"@prisma/client": "^7.3.0",
|
||||||
"@prisma/extension-read-replicas": "^0.4.1",
|
"@prisma/extension-read-replicas": "^0.5.0",
|
||||||
"@react-spring/web": "^10.0.3",
|
"@react-spring/web": "^10.0.3",
|
||||||
"@svgr/cli": "^8.1.0",
|
"@svgr/cli": "^8.1.0",
|
||||||
"@tanstack/react-query": "^5.90.11",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
"@umami/react-zen": "^0.211.0",
|
"@umami/react-zen": "^0.245.0",
|
||||||
"@umami/redis-client": "^0.29.0",
|
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"chartjs-adapter-date-fns": "^3.0.0",
|
"chartjs-adapter-date-fns": "^3.0.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"colord": "^2.9.2",
|
"colord": "^2.9.2",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.6",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.3",
|
||||||
"date-fns": "^2.23.0",
|
"date-fns": "^2.23.0",
|
||||||
"date-fns-tz": "^1.1.4",
|
"date-fns-tz": "^1.1.4",
|
||||||
"debug": "^4.4.3",
|
"debug": "^4.4.3",
|
||||||
"del": "^6.0.0",
|
"del": "^6.0.0",
|
||||||
"detect-browser": "^5.2.0",
|
"detect-browser": "^5.2.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.3.1",
|
||||||
"esbuild": "^0.25.11",
|
"esbuild": "^0.25.11",
|
||||||
"fs-extra": "^11.3.2",
|
"fs-extra": "^11.3.3",
|
||||||
"immer": "^10.2.0",
|
"immer": "^10.2.0",
|
||||||
"ipaddr.js": "^2.3.0",
|
"ipaddr.js": "^2.3.0",
|
||||||
"is-ci": "^3.0.1",
|
"is-ci": "^3.0.1",
|
||||||
"is-docker": "^3.0.0",
|
"is-docker": "^3.0.0",
|
||||||
"is-localhost-ip": "^2.0.0",
|
"is-localhost-ip": "^2.0.0",
|
||||||
"isbot": "^5.1.31",
|
"isbot": "^5.1.35",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"kafkajs": "^2.1.0",
|
"kafkajs": "^2.1.0",
|
||||||
"lucide-react": "^0.543.0",
|
"lucide-react": "^0.543.0",
|
||||||
"maxmind": "^5.0.0",
|
"maxmind": "^5.0.5",
|
||||||
"next": "^15.5.9",
|
"next": "^16.1.6",
|
||||||
|
"next-intl": "^4.8.3",
|
||||||
"node-fetch": "^3.2.8",
|
"node-fetch": "^3.2.8",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"papaparse": "^5.5.3",
|
"papaparse": "^5.5.3",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.18.0",
|
||||||
"prisma": "^6.18.0",
|
"prisma": "^7.3.0",
|
||||||
"pure-rand": "^7.0.1",
|
"pure-rand": "^7.0.1",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.4",
|
||||||
"react-error-boundary": "^4.0.4",
|
"react-error-boundary": "^4.0.4",
|
||||||
"react-intl": "^7.1.14",
|
"react-resizable-panels": "^4.6.4",
|
||||||
"react-simple-maps": "^2.3.0",
|
"react-simple-maps": "^2.3.0",
|
||||||
"react-use-measure": "^2.0.4",
|
"react-use-measure": "^2.0.4",
|
||||||
"react-window": "^1.8.6",
|
"react-window": "^1.8.6",
|
||||||
|
"redis": "^4.5.1",
|
||||||
"request-ip": "^3.3.0",
|
"request-ip": "^3.3.0",
|
||||||
"semver": "^7.7.3",
|
"rrweb": "2.0.0-alpha.4",
|
||||||
|
"rrweb-player": "1.0.0-alpha.4",
|
||||||
|
"semver": "^7.7.4",
|
||||||
"serialize-error": "^12.0.0",
|
"serialize-error": "^12.0.0",
|
||||||
"thenby": "^1.3.4",
|
"thenby": "^1.3.4",
|
||||||
"ua-parser-js": "^2.0.6",
|
"ua-parser-js": "^2.0.9",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^13.0.0",
|
||||||
"zod": "^4.1.13",
|
"zod": "^4.3.6",
|
||||||
"zustand": "^5.0.9"
|
"zustand": "^5.0.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.3.8",
|
"@biomejs/biome": "^2.4.2",
|
||||||
"@formatjs/cli": "^4.2.29",
|
"@netlify/plugin-nextjs": "^5.15.8",
|
||||||
"@netlify/plugin-nextjs": "^5.15.1",
|
|
||||||
"@rollup/plugin-alias": "^5.0.0",
|
"@rollup/plugin-alias": "^5.0.0",
|
||||||
"@rollup/plugin-commonjs": "^25.0.4",
|
"@rollup/plugin-commonjs": "^25.0.4",
|
||||||
"@rollup/plugin-json": "^6.0.0",
|
"@rollup/plugin-json": "^6.0.0",
|
||||||
@@ -137,14 +134,13 @@
|
|||||||
"@rollup/plugin-terser": "^0.4.4",
|
"@rollup/plugin-terser": "^0.4.4",
|
||||||
"@rollup/plugin-typescript": "^12.3.0",
|
"@rollup/plugin-typescript": "^12.3.0",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^24.9.2",
|
"@types/node": "^24.10.13",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.2",
|
||||||
"@types/react-window": "^1.8.8",
|
|
||||||
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"cypress": "^13.6.6",
|
"cypress": "^13.6.6",
|
||||||
"extract-react-intl-messages": "^4.1.1",
|
"dotenv-cli": "^11.0.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"lint-staged": "^16.2.6",
|
"lint-staged": "^16.2.6",
|
||||||
@@ -153,19 +149,20 @@
|
|||||||
"postcss-import": "^15.1.0",
|
"postcss-import": "^15.1.0",
|
||||||
"postcss-preset-env": "7.8.3",
|
"postcss-preset-env": "7.8.3",
|
||||||
"prompts": "2.4.2",
|
"prompts": "2.4.2",
|
||||||
"rollup": "^4.52.5",
|
"rollup": "^4.57.1",
|
||||||
"rollup-plugin-copy": "^3.4.0",
|
"rollup-plugin-copy": "^3.4.0",
|
||||||
"rollup-plugin-delete": "^3.0.1",
|
"rollup-plugin-delete": "^3.0.2",
|
||||||
"rollup-plugin-dts": "^6.3.0",
|
"rollup-plugin-dts": "^6.3.0",
|
||||||
"rollup-plugin-node-externals": "^8.1.1",
|
"rollup-plugin-node-externals": "^8.1.1",
|
||||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||||
"rollup-plugin-postcss": "^4.0.2",
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
"stylelint": "^15.10.1",
|
"stylelint": "^15.10.1",
|
||||||
"stylelint-config-css-modules": "^4.5.1",
|
"stylelint-config-css-modules": "^4.6.0",
|
||||||
"stylelint-config-prettier": "^9.0.3",
|
"stylelint-config-prettier": "^9.0.3",
|
||||||
"stylelint-config-recommended": "^14.0.0",
|
"stylelint-config-recommended": "^14.0.0",
|
||||||
"tar": "^6.1.2",
|
"tar": "^7.5.9",
|
||||||
"ts-jest": "^29.4.6",
|
"ts-jest": "^29.4.6",
|
||||||
|
"ts-morph": "^27.0.2",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tsup": "^8.5.0",
|
"tsup": "^8.5.0",
|
||||||
"tsx": "^4.19.0",
|
"tsx": "^4.19.0",
|
||||||
|
|||||||
Generated
+2900
-3134
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "share" (
|
||||||
|
"share_id" UUID NOT NULL,
|
||||||
|
"entity_id" UUID NOT NULL,
|
||||||
|
"name" VARCHAR(200) NOT NULL,
|
||||||
|
"share_type" INTEGER NOT NULL,
|
||||||
|
"slug" VARCHAR(100) NOT NULL,
|
||||||
|
"parameters" JSONB NOT NULL,
|
||||||
|
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ(6),
|
||||||
|
|
||||||
|
CONSTRAINT "share_pkey" PRIMARY KEY ("share_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "share_slug_key" ON "share"("slug");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "share_entity_id_idx" ON "share"("entity_id");
|
||||||
|
|
||||||
|
-- MigrateData
|
||||||
|
INSERT INTO "share" (share_id, entity_id, name, share_type, slug, parameters, created_at)
|
||||||
|
SELECT gen_random_uuid(),
|
||||||
|
website_id,
|
||||||
|
name,
|
||||||
|
1,
|
||||||
|
share_id,
|
||||||
|
'{"overview":true}'::jsonb,
|
||||||
|
now()
|
||||||
|
FROM "website"
|
||||||
|
WHERE share_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "website_share_id_idx";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "website_share_id_key";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "website" DROP COLUMN "share_id";
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "board" (
|
||||||
|
"board_id" UUID NOT NULL,
|
||||||
|
"type" VARCHAR(50) NOT NULL,
|
||||||
|
"name" VARCHAR(200) NOT NULL,
|
||||||
|
"description" VARCHAR(500) NOT NULL,
|
||||||
|
"parameters" JSONB NOT NULL,
|
||||||
|
"slug" VARCHAR(100) NOT NULL,
|
||||||
|
"user_id" UUID,
|
||||||
|
"team_id" UUID,
|
||||||
|
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ(6),
|
||||||
|
|
||||||
|
CONSTRAINT "board_pkey" PRIMARY KEY ("board_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "board_board_id_key" ON "board"("board_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "board_slug_key" ON "board"("slug");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "board_slug_idx" ON "board"("slug");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "board_user_id_idx" ON "board"("user_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "board_team_id_idx" ON "board"("team_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "board_created_at_idx" ON "board"("created_at");
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "link_link_id_key";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "pixel_pixel_id_key";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "report_report_id_key";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "revenue_revenue_id_key";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "segment_segment_id_key";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "session_session_id_key";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "team_team_id_key";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "team_user_team_user_id_key";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "user_user_id_key";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "website_website_id_key";
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "website_event" ADD COLUMN "cls" DECIMAL(10,4),
|
||||||
|
ADD COLUMN "fcp" DECIMAL(10,1),
|
||||||
|
ADD COLUMN "inp" DECIMAL(10,1),
|
||||||
|
ADD COLUMN "lcp" DECIMAL(10,1),
|
||||||
|
ADD COLUMN "ttfb" DECIMAL(10,1);
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "website" ADD COLUMN "replay_enabled" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
ALTER TABLE "website" ADD COLUMN "replay_config" JSONB;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "session_replay" (
|
||||||
|
"replay_id" UUID NOT NULL,
|
||||||
|
"website_id" UUID NOT NULL,
|
||||||
|
"session_id" UUID NOT NULL,
|
||||||
|
"visit_id" UUID NOT NULL,
|
||||||
|
"chunk_index" INTEGER NOT NULL,
|
||||||
|
"events" BYTEA NOT NULL,
|
||||||
|
"event_count" INTEGER NOT NULL,
|
||||||
|
"started_at" TIMESTAMPTZ(6) NOT NULL,
|
||||||
|
"ended_at" TIMESTAMPTZ(6) NOT NULL,
|
||||||
|
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "session_replay_pkey" PRIMARY KEY ("replay_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "session_replay_website_id_idx" ON "session_replay"("website_id");
|
||||||
|
CREATE INDEX "session_replay_session_id_idx" ON "session_replay"("session_id");
|
||||||
|
CREATE INDEX "session_replay_website_id_session_id_idx" ON "session_replay"("website_id", "session_id");
|
||||||
|
CREATE INDEX "session_replay_website_id_visit_id_idx" ON "session_replay"("website_id", "visit_id");
|
||||||
|
CREATE INDEX "session_replay_website_id_created_at_idx" ON "session_replay"("website_id", "created_at");
|
||||||
|
CREATE INDEX "session_replay_session_id_chunk_index_idx" ON "session_replay"("session_id", "chunk_index");
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "session_replay_saved" (
|
||||||
|
"saved_replay_id" UUID NOT NULL,
|
||||||
|
"name" VARCHAR(100) NOT NULL,
|
||||||
|
"website_id" UUID NOT NULL,
|
||||||
|
"visit_id" UUID NOT NULL,
|
||||||
|
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ(6),
|
||||||
|
|
||||||
|
CONSTRAINT "session_replay_saved_pkey" PRIMARY KEY ("saved_replay_id"),
|
||||||
|
CONSTRAINT "session_replay_saved_website_id_visit_id_key" UNIQUE ("website_id", "visit_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "session_replay_saved_website_id_idx" ON "session_replay_saved"("website_id");
|
||||||
|
CREATE INDEX "session_replay_saved_visit_id_idx" ON "session_replay_saved"("visit_id");
|
||||||
|
CREATE INDEX "session_replay_saved_website_id_created_at_idx" ON "session_replay_saved"("website_id", "created_at");
|
||||||
+98
-13
@@ -6,12 +6,11 @@ generator client {
|
|||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
url = env("DATABASE_URL")
|
|
||||||
relationMode = "prisma"
|
relationMode = "prisma"
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @unique @map("user_id") @db.Uuid
|
id String @id() @map("user_id") @db.Uuid
|
||||||
username String @unique @db.VarChar(255)
|
username String @unique @db.VarChar(255)
|
||||||
password String @db.VarChar(60)
|
password String @db.VarChar(60)
|
||||||
role String @map("role") @db.VarChar(50)
|
role String @map("role") @db.VarChar(50)
|
||||||
@@ -27,12 +26,13 @@ model User {
|
|||||||
pixels Pixel[] @relation("user")
|
pixels Pixel[] @relation("user")
|
||||||
teams TeamUser[]
|
teams TeamUser[]
|
||||||
reports Report[]
|
reports Report[]
|
||||||
|
boards Board[] @relation("user")
|
||||||
|
|
||||||
@@map("user")
|
@@map("user")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Session {
|
model Session {
|
||||||
id String @id @unique @map("session_id") @db.Uuid
|
id String @id() @map("session_id") @db.Uuid
|
||||||
websiteId String @map("website_id") @db.Uuid
|
websiteId String @map("website_id") @db.Uuid
|
||||||
browser String? @db.VarChar(20)
|
browser String? @db.VarChar(20)
|
||||||
os String? @db.VarChar(20)
|
os String? @db.VarChar(20)
|
||||||
@@ -64,10 +64,9 @@ model Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Website {
|
model Website {
|
||||||
id String @id @unique @map("website_id") @db.Uuid
|
id String @id() @map("website_id") @db.Uuid
|
||||||
name String @db.VarChar(100)
|
name String @db.VarChar(100)
|
||||||
domain String? @db.VarChar(500)
|
domain String? @db.VarChar(500)
|
||||||
shareId String? @unique @map("share_id") @db.VarChar(50)
|
|
||||||
resetAt DateTime? @map("reset_at") @db.Timestamptz(6)
|
resetAt DateTime? @map("reset_at") @db.Timestamptz(6)
|
||||||
userId String? @map("user_id") @db.Uuid
|
userId String? @map("user_id") @db.Uuid
|
||||||
teamId String? @map("team_id") @db.Uuid
|
teamId String? @map("team_id") @db.Uuid
|
||||||
@@ -76,6 +75,9 @@ model Website {
|
|||||||
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||||
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
|
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
|
||||||
|
|
||||||
|
replayEnabled Boolean @default(false) @map("replay_enabled")
|
||||||
|
replayConfig Json? @map("replay_config")
|
||||||
|
|
||||||
user User? @relation("user", fields: [userId], references: [id])
|
user User? @relation("user", fields: [userId], references: [id])
|
||||||
createUser User? @relation("createUser", fields: [createdBy], references: [id])
|
createUser User? @relation("createUser", fields: [createdBy], references: [id])
|
||||||
team Team? @relation(fields: [teamId], references: [id])
|
team Team? @relation(fields: [teamId], references: [id])
|
||||||
@@ -84,11 +86,12 @@ model Website {
|
|||||||
revenue Revenue[]
|
revenue Revenue[]
|
||||||
segments Segment[]
|
segments Segment[]
|
||||||
sessionData SessionData[]
|
sessionData SessionData[]
|
||||||
|
sessionReplays SessionReplay[]
|
||||||
|
sessionReplaysSaved SessionReplaySaved[]
|
||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@@index([teamId])
|
@@index([teamId])
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([shareId])
|
|
||||||
@@index([createdBy])
|
@@index([createdBy])
|
||||||
@@map("website")
|
@@map("website")
|
||||||
}
|
}
|
||||||
@@ -120,6 +123,11 @@ model WebsiteEvent {
|
|||||||
eventName String? @map("event_name") @db.VarChar(50)
|
eventName String? @map("event_name") @db.VarChar(50)
|
||||||
tag String? @db.VarChar(50)
|
tag String? @db.VarChar(50)
|
||||||
hostname String? @db.VarChar(100)
|
hostname String? @db.VarChar(100)
|
||||||
|
lcp Decimal? @db.Decimal(10, 1)
|
||||||
|
inp Decimal? @db.Decimal(10, 1)
|
||||||
|
cls Decimal? @db.Decimal(10, 4)
|
||||||
|
fcp Decimal? @db.Decimal(10, 1)
|
||||||
|
ttfb Decimal? @db.Decimal(10, 1)
|
||||||
|
|
||||||
eventData EventData[]
|
eventData EventData[]
|
||||||
session Session @relation(fields: [sessionId], references: [id])
|
session Session @relation(fields: [sessionId], references: [id])
|
||||||
@@ -187,7 +195,7 @@ model SessionData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Team {
|
model Team {
|
||||||
id String @id() @unique() @map("team_id") @db.Uuid
|
id String @id() @map("team_id") @db.Uuid
|
||||||
name String @db.VarChar(50)
|
name String @db.VarChar(50)
|
||||||
accessCode String? @unique @map("access_code") @db.VarChar(50)
|
accessCode String? @unique @map("access_code") @db.VarChar(50)
|
||||||
logoUrl String? @map("logo_url") @db.VarChar(2183)
|
logoUrl String? @map("logo_url") @db.VarChar(2183)
|
||||||
@@ -199,13 +207,14 @@ model Team {
|
|||||||
members TeamUser[]
|
members TeamUser[]
|
||||||
links Link[]
|
links Link[]
|
||||||
pixels Pixel[]
|
pixels Pixel[]
|
||||||
|
boards Board[]
|
||||||
|
|
||||||
@@index([accessCode])
|
@@index([accessCode])
|
||||||
@@map("team")
|
@@map("team")
|
||||||
}
|
}
|
||||||
|
|
||||||
model TeamUser {
|
model TeamUser {
|
||||||
id String @id() @unique() @map("team_user_id") @db.Uuid
|
id String @id() @map("team_user_id") @db.Uuid
|
||||||
teamId String @map("team_id") @db.Uuid
|
teamId String @map("team_id") @db.Uuid
|
||||||
userId String @map("user_id") @db.Uuid
|
userId String @map("user_id") @db.Uuid
|
||||||
role String @db.VarChar(50)
|
role String @db.VarChar(50)
|
||||||
@@ -221,7 +230,7 @@ model TeamUser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Report {
|
model Report {
|
||||||
id String @id() @unique() @map("report_id") @db.Uuid
|
id String @id() @map("report_id") @db.Uuid
|
||||||
userId String @map("user_id") @db.Uuid
|
userId String @map("user_id") @db.Uuid
|
||||||
websiteId String @map("website_id") @db.Uuid
|
websiteId String @map("website_id") @db.Uuid
|
||||||
type String @db.VarChar(50)
|
type String @db.VarChar(50)
|
||||||
@@ -242,7 +251,7 @@ model Report {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Segment {
|
model Segment {
|
||||||
id String @id() @unique() @map("segment_id") @db.Uuid
|
id String @id() @map("segment_id") @db.Uuid
|
||||||
websiteId String @map("website_id") @db.Uuid
|
websiteId String @map("website_id") @db.Uuid
|
||||||
type String @db.VarChar(50)
|
type String @db.VarChar(50)
|
||||||
name String @db.VarChar(200)
|
name String @db.VarChar(200)
|
||||||
@@ -257,7 +266,7 @@ model Segment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Revenue {
|
model Revenue {
|
||||||
id String @id() @unique() @map("revenue_id") @db.Uuid
|
id String @id() @map("revenue_id") @db.Uuid
|
||||||
websiteId String @map("website_id") @db.Uuid
|
websiteId String @map("website_id") @db.Uuid
|
||||||
sessionId String @map("session_id") @db.Uuid
|
sessionId String @map("session_id") @db.Uuid
|
||||||
eventId String @map("event_id") @db.Uuid
|
eventId String @map("event_id") @db.Uuid
|
||||||
@@ -277,7 +286,7 @@ model Revenue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Link {
|
model Link {
|
||||||
id String @id() @unique() @map("link_id") @db.Uuid
|
id String @id() @map("link_id") @db.Uuid
|
||||||
name String @db.VarChar(100)
|
name String @db.VarChar(100)
|
||||||
url String @db.VarChar(500)
|
url String @db.VarChar(500)
|
||||||
slug String @unique() @db.VarChar(100)
|
slug String @unique() @db.VarChar(100)
|
||||||
@@ -298,7 +307,7 @@ model Link {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Pixel {
|
model Pixel {
|
||||||
id String @id() @unique() @map("pixel_id") @db.Uuid
|
id String @id() @map("pixel_id") @db.Uuid
|
||||||
name String @db.VarChar(100)
|
name String @db.VarChar(100)
|
||||||
slug String @unique() @db.VarChar(100)
|
slug String @unique() @db.VarChar(100)
|
||||||
userId String? @map("user_id") @db.Uuid
|
userId String? @map("user_id") @db.Uuid
|
||||||
@@ -316,3 +325,79 @@ model Pixel {
|
|||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@map("pixel")
|
@@map("pixel")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Board {
|
||||||
|
id String @id() @map("board_id") @db.Uuid
|
||||||
|
type String @db.VarChar(50)
|
||||||
|
name String @db.VarChar(200)
|
||||||
|
description String @db.VarChar(500)
|
||||||
|
parameters Json
|
||||||
|
slug String @unique() @db.VarChar(100)
|
||||||
|
userId String? @map("user_id") @db.Uuid
|
||||||
|
teamId String? @map("team_id") @db.Uuid
|
||||||
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
|
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||||
|
|
||||||
|
user User? @relation("user", fields: [userId], references: [id])
|
||||||
|
team Team? @relation(fields: [teamId], references: [id])
|
||||||
|
|
||||||
|
@@index([slug])
|
||||||
|
@@index([userId])
|
||||||
|
@@index([teamId])
|
||||||
|
@@index([createdAt])
|
||||||
|
@@map("board")
|
||||||
|
}
|
||||||
|
model Share {
|
||||||
|
id String @id() @map("share_id") @db.Uuid
|
||||||
|
entityId String @map("entity_id") @db.Uuid
|
||||||
|
name String @db.VarChar(200)
|
||||||
|
shareType Int @map("share_type") @db.Integer
|
||||||
|
slug String @unique() @db.VarChar(100)
|
||||||
|
parameters Json
|
||||||
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
|
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||||
|
|
||||||
|
@@index([entityId])
|
||||||
|
@@map("share")
|
||||||
|
}
|
||||||
|
|
||||||
|
model SessionReplay {
|
||||||
|
id String @id() @map("replay_id") @db.Uuid
|
||||||
|
websiteId String @map("website_id") @db.Uuid
|
||||||
|
sessionId String @map("session_id") @db.Uuid
|
||||||
|
visitId String @map("visit_id") @db.Uuid
|
||||||
|
chunkIndex Int @map("chunk_index") @db.Integer
|
||||||
|
events Bytes @map("events")
|
||||||
|
eventCount Int @map("event_count") @db.Integer
|
||||||
|
startedAt DateTime @map("started_at") @db.Timestamptz(6)
|
||||||
|
endedAt DateTime @map("ended_at") @db.Timestamptz(6)
|
||||||
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
|
|
||||||
|
website Website @relation(fields: [websiteId], references: [id])
|
||||||
|
|
||||||
|
@@index([websiteId])
|
||||||
|
@@index([sessionId])
|
||||||
|
@@index([visitId])
|
||||||
|
@@index([websiteId, sessionId])
|
||||||
|
@@index([websiteId, visitId])
|
||||||
|
@@index([websiteId, createdAt])
|
||||||
|
@@index([sessionId, chunkIndex])
|
||||||
|
@@map("session_replay")
|
||||||
|
}
|
||||||
|
|
||||||
|
model SessionReplaySaved {
|
||||||
|
id String @id() @map("saved_replay_id") @db.Uuid
|
||||||
|
name String @db.VarChar(100)
|
||||||
|
websiteId String @map("website_id") @db.Uuid
|
||||||
|
visitId String @map("visit_id") @db.Uuid
|
||||||
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
|
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||||
|
|
||||||
|
website Website @relation(fields: [websiteId], references: [id])
|
||||||
|
|
||||||
|
@@unique([websiteId, visitId])
|
||||||
|
@@index([websiteId])
|
||||||
|
@@index([visitId])
|
||||||
|
@@index([websiteId, createdAt])
|
||||||
|
@@map("session_replay_saved")
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
File diff suppressed because it is too large
Load Diff
+418
-2210
File diff suppressed because it is too large
Load Diff
+418
-2218
File diff suppressed because it is too large
Load Diff
+418
-2224
File diff suppressed because it is too large
Load Diff
+418
-2226
File diff suppressed because it is too large
Load Diff
+418
-2224
File diff suppressed because it is too large
Load Diff
+418
-2218
File diff suppressed because it is too large
Load Diff
+418
-2218
File diff suppressed because it is too large
Load Diff
+418
-2218
File diff suppressed because it is too large
Load Diff
+418
-2210
File diff suppressed because it is too large
Load Diff
+418
-2206
File diff suppressed because it is too large
Load Diff
+418
-2218
File diff suppressed because it is too large
Load Diff
+418
-2218
File diff suppressed because it is too large
Load Diff
+427
-2224
File diff suppressed because it is too large
Load Diff
+420
-2224
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+418
-2198
File diff suppressed because it is too large
Load Diff
+418
-2218
File diff suppressed because it is too large
Load Diff
+418
-2218
File diff suppressed because it is too large
Load Diff
+418
-2202
File diff suppressed because it is too large
Load Diff
+418
-2226
File diff suppressed because it is too large
Load Diff
+418
-2202
File diff suppressed because it is too large
Load Diff
+418
-2210
File diff suppressed because it is too large
Load Diff
+418
-2232
File diff suppressed because it is too large
Load Diff
+418
-2222
File diff suppressed because it is too large
Load Diff
+418
-2182
File diff suppressed because it is too large
Load Diff
+418
-2222
File diff suppressed because it is too large
Load Diff
+418
-2208
File diff suppressed because it is too large
Load Diff
+418
-2208
File diff suppressed because it is too large
Load Diff
+418
-2164
File diff suppressed because it is too large
Load Diff
+419
-2344
File diff suppressed because it is too large
Load Diff
+418
-2232
File diff suppressed because it is too large
Load Diff
+418
-2210
File diff suppressed because it is too large
Load Diff
+418
-2198
File diff suppressed because it is too large
Load Diff
+418
-2222
File diff suppressed because it is too large
Load Diff
+418
-2218
File diff suppressed because it is too large
Load Diff
+418
-2218
File diff suppressed because it is too large
Load Diff
+418
-2202
File diff suppressed because it is too large
Load Diff
+418
-2222
File diff suppressed because it is too large
Load Diff
+418
-2218
File diff suppressed because it is too large
Load Diff
+418
-2186
File diff suppressed because it is too large
Load Diff
+418
-2224
File diff suppressed because it is too large
Load Diff
+418
-2218
File diff suppressed because it is too large
Load Diff
+422
-2092
File diff suppressed because it is too large
Load Diff
+418
-2222
File diff suppressed because it is too large
Load Diff
+418
-2214
File diff suppressed because it is too large
Load Diff
+418
-2214
File diff suppressed because it is too large
Load Diff
+418
-2174
File diff suppressed because it is too large
Load Diff
+418
-2194
File diff suppressed because it is too large
Load Diff
+418
-2218
File diff suppressed because it is too large
Load Diff
+424
-1854
File diff suppressed because it is too large
Load Diff
+424
-1856
File diff suppressed because it is too large
Load Diff
+418
-2210
File diff suppressed because it is too large
Load Diff
+418
-2196
File diff suppressed because it is too large
Load Diff
@@ -6,13 +6,13 @@
|
|||||||
"AD-06": "Sant Julia de Loria",
|
"AD-06": "Sant Julia de Loria",
|
||||||
"AD-07": "Andorra la Vella",
|
"AD-07": "Andorra la Vella",
|
||||||
"AD-08": "Escaldes-Engordany",
|
"AD-08": "Escaldes-Engordany",
|
||||||
"AE-AJ": "'Ajman",
|
"AE-AJ": "Ajman",
|
||||||
"AE-AZ": "Abu Zaby",
|
"AE-AZ": "Abu Dhabi",
|
||||||
"AE-DU": "Dubayy",
|
"AE-DU": "Dubai",
|
||||||
"AE-FU": "Al Fujayrah",
|
"AE-FU": "Al Fujairah",
|
||||||
"AE-RK": "Ra's al Khaymah",
|
"AE-RK": "Ras al Khaimah",
|
||||||
"AE-SH": "Ash Shariqah",
|
"AE-SH": "Sharjah",
|
||||||
"AE-UQ": "Umm al Qaywayn",
|
"AE-UQ": "Umm al Quwain",
|
||||||
"AF-BAL": "Balkh",
|
"AF-BAL": "Balkh",
|
||||||
"AF-BAM": "Bamyan",
|
"AF-BAM": "Bamyan",
|
||||||
"AF-BDG": "Badghis",
|
"AF-BDG": "Badghis",
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
|
import resolve from '@rollup/plugin-node-resolve';
|
||||||
|
import replace from '@rollup/plugin-replace';
|
||||||
|
import terser from '@rollup/plugin-terser';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input: 'src/recorder/index.js',
|
||||||
|
output: {
|
||||||
|
file: 'public/recorder.js',
|
||||||
|
format: 'iife',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
resolve({ browser: true }),
|
||||||
|
commonjs(),
|
||||||
|
replace({
|
||||||
|
__COLLECT_API_HOST__: process.env.COLLECT_API_HOST || '',
|
||||||
|
__COLLECT_REPLAY_ENDPOINT__: process.env.COLLECT_REPLAY_ENDPOINT || '/api/record',
|
||||||
|
delimiters: ['', ''],
|
||||||
|
preventAssignment: true,
|
||||||
|
}),
|
||||||
|
terser({ compress: { evaluate: false } }),
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -3,9 +3,14 @@ import 'dotenv/config';
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import https from 'https';
|
import https from 'https';
|
||||||
import tar from 'tar';
|
import { list } from 'tar';
|
||||||
import zlib from 'zlib';
|
import zlib from 'zlib';
|
||||||
|
|
||||||
|
if (process.env.SKIP_BUILD_GEO) {
|
||||||
|
console.log('SKIP_BUILD_GEO is set. Skipping geo setup.');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
if (process.env.VERCEL && !process.env.BUILD_GEO) {
|
if (process.env.VERCEL && !process.env.BUILD_GEO) {
|
||||||
console.log('Vercel environment detected. Skipping geo setup.');
|
console.log('Vercel environment detected. Skipping geo setup.');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
@@ -40,7 +45,7 @@ const isDirectMmdb = url.endsWith('.mmdb');
|
|||||||
const downloadCompressed = url =>
|
const downloadCompressed = url =>
|
||||||
new Promise(resolve => {
|
new Promise(resolve => {
|
||||||
https.get(url, res => {
|
https.get(url, res => {
|
||||||
resolve(res.pipe(zlib.createGunzip({})).pipe(tar.t()));
|
resolve(res.pipe(zlib.createGunzip({})).pipe(list()));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const distDir = path.resolve(process.cwd(), 'dist');
|
||||||
|
const packageFile = path.join(distDir, 'package.json');
|
||||||
|
|
||||||
|
const defaultPackage = {
|
||||||
|
name: '@umami/components',
|
||||||
|
version: '0.0.0',
|
||||||
|
description: 'Umami React components.',
|
||||||
|
author: 'Mike Cao <mike@mikecao.com>',
|
||||||
|
license: 'MIT',
|
||||||
|
type: 'module',
|
||||||
|
main: './index.js',
|
||||||
|
types: './index.d.ts',
|
||||||
|
dependencies: {
|
||||||
|
'chart.js': '^4.5.0',
|
||||||
|
'chartjs-adapter-date-fns': '^3.0.0',
|
||||||
|
colord: '^2.9.2',
|
||||||
|
jsonwebtoken: '^9.0.2',
|
||||||
|
'lucide-react': '^0.542.0',
|
||||||
|
'pure-rand': '^7.0.1',
|
||||||
|
'react-simple-maps': '^2.3.0',
|
||||||
|
'react-use-measure': '^2.0.4',
|
||||||
|
'react-window': '^1.8.6',
|
||||||
|
'serialize-error': '^12.0.0',
|
||||||
|
thenby: '^1.3.4',
|
||||||
|
uuid: '^11.1.0',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!fs.existsSync(distDir)) {
|
||||||
|
fs.mkdirSync(distDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pkg = fs.existsSync(packageFile)
|
||||||
|
? JSON.parse(fs.readFileSync(packageFile, 'utf8'))
|
||||||
|
: defaultPackage;
|
||||||
|
|
||||||
|
const published = execSync(`npm view ${pkg.name} version`, { encoding: 'utf8' }).trim();
|
||||||
|
const [major, minor] = published.split('.').map(Number);
|
||||||
|
const next = `${major}.${minor + 1}.0`;
|
||||||
|
|
||||||
|
pkg.version = next;
|
||||||
|
|
||||||
|
fs.writeFileSync(packageFile, `${JSON.stringify(pkg, null, 2)}\n`);
|
||||||
|
|
||||||
|
console.log(`Bumped ${pkg.name} version: ${published} -> ${next}`);
|
||||||
+1
-1
@@ -48,7 +48,7 @@ async function checkConnection() {
|
|||||||
|
|
||||||
success('Database connection successful.');
|
success('Database connection successful.');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error('Unable to connect to the database: ' + e.message);
|
throw new Error(`Unable to connect to the database: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { readdirSync, readFileSync } from 'node:fs';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const messagesDir = join(__dirname, '..', 'public', 'intl', 'messages');
|
||||||
|
|
||||||
|
const en = JSON.parse(readFileSync(join(messagesDir, 'en-US.json'), 'utf8'));
|
||||||
|
|
||||||
|
// Flatten nested structure: { label: { foo: 'bar' } } -> { 'label.foo': 'bar' }
|
||||||
|
function flatten(obj, prefix = '') {
|
||||||
|
const result = {};
|
||||||
|
for (const [k, v] of Object.entries(obj)) {
|
||||||
|
const key = prefix ? `${prefix}.${k}` : k;
|
||||||
|
if (typeof v === 'object' && v !== null) {
|
||||||
|
Object.assign(result, flatten(v, key));
|
||||||
|
} else {
|
||||||
|
result[key] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enFlat = flatten(en);
|
||||||
|
const enKeys = Object.keys(enFlat);
|
||||||
|
console.log(`en-US.json has ${enKeys.length} keys`);
|
||||||
|
|
||||||
|
const files = readdirSync(messagesDir)
|
||||||
|
.filter(f => f.endsWith('.json') && f !== 'en-US.json')
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
const allMissing = {};
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
for (const fname of files) {
|
||||||
|
const data = JSON.parse(readFileSync(join(messagesDir, fname), 'utf8'));
|
||||||
|
const flat = flatten(data);
|
||||||
|
const missing = enKeys.filter(k => !(k in flat));
|
||||||
|
if (missing.length) {
|
||||||
|
allMissing[fname] = missing;
|
||||||
|
console.log(`${fname}: ${missing.length} missing`);
|
||||||
|
total += missing.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`\nTotal missing across all locales: ${total}`);
|
||||||
|
|
||||||
|
const keyCounts = {};
|
||||||
|
for (const missing of Object.values(allMissing)) {
|
||||||
|
for (const k of missing) {
|
||||||
|
keyCounts[k] = (keyCounts[k] || 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const sorted = Object.entries(keyCounts).sort((a, b) => b[1] - a[1]);
|
||||||
|
if (sorted.length) {
|
||||||
|
console.log('\nMost commonly missing keys:');
|
||||||
|
for (const [k, count] of sorted) {
|
||||||
|
console.log(` "${k}": missing from ${count} files (en value: "${enFlat[k]}")`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
import https from 'node:https';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import https from 'https';
|
|
||||||
|
|
||||||
const src = path.resolve(process.cwd(), 'src/lang');
|
const src = path.resolve(process.cwd(), 'public/intl/messages');
|
||||||
const dest = path.resolve(process.cwd(), 'public/intl/country');
|
const dest = path.resolve(process.cwd(), 'public/intl/country');
|
||||||
const files = fs.readdirSync(src);
|
const files = fs.readdirSync(src);
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
import https from 'node:https';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import https from 'https';
|
|
||||||
|
|
||||||
const src = path.resolve(process.cwd(), 'src/lang');
|
const src = path.resolve(process.cwd(), 'public/intl/messages');
|
||||||
const dest = path.resolve(process.cwd(), 'public/intl/language');
|
const dest = path.resolve(process.cwd(), 'public/intl/language');
|
||||||
const files = fs.readdirSync(src);
|
const files = fs.readdirSync(src);
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import path from 'node:path';
|
|
||||||
import del from 'del';
|
|
||||||
import fs from 'fs-extra';
|
|
||||||
import { createRequire } from 'module';
|
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
|
||||||
const src = path.resolve(process.cwd(), 'src/lang');
|
|
||||||
const dest = path.resolve(process.cwd(), 'build/messages');
|
|
||||||
const files = fs.readdirSync(src);
|
|
||||||
|
|
||||||
del.sync([path.join(dest)]);
|
|
||||||
|
|
||||||
/*
|
|
||||||
This script takes the files from the `lang` folder and formats them into
|
|
||||||
the format that format-js expects.
|
|
||||||
*/
|
|
||||||
async function run() {
|
|
||||||
await fs.ensureDir(dest);
|
|
||||||
|
|
||||||
files.forEach(file => {
|
|
||||||
const lang = require(path.resolve(process.cwd(), `src/lang/${file}`));
|
|
||||||
const keys = Object.keys(lang).sort();
|
|
||||||
|
|
||||||
const formatted = keys.reduce((obj, key) => {
|
|
||||||
obj[key] = { defaultMessage: lang[key] };
|
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const json = JSON.stringify(formatted, null, 2);
|
|
||||||
|
|
||||||
fs.writeFileSync(path.resolve(dest, file), json);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
run();
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
/* eslint-disable no-console */
|
|
||||||
import fs from 'node:fs';
|
|
||||||
import path from 'node:path';
|
|
||||||
import { createRequire } from 'module';
|
|
||||||
import prettier from 'prettier';
|
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
|
||||||
|
|
||||||
const messages = require('../build/extracted-messages.json');
|
|
||||||
const dest = path.resolve(process.cwd(), 'src/lang');
|
|
||||||
const files = fs.readdirSync(dest);
|
|
||||||
const keys = Object.keys(messages).sort();
|
|
||||||
|
|
||||||
/*
|
|
||||||
This script takes extracted messages and merges them
|
|
||||||
with the existing files under `lang`. Any newly added
|
|
||||||
keys will be printed to the console.
|
|
||||||
*/
|
|
||||||
files.forEach(file => {
|
|
||||||
const lang = require(path.resolve(process.cwd(), `src/lang/${file}`));
|
|
||||||
|
|
||||||
console.log(`Merging ${file}`);
|
|
||||||
|
|
||||||
const merged = keys.reduce((obj, key) => {
|
|
||||||
const message = lang[key];
|
|
||||||
|
|
||||||
if (file === 'en-US.json') {
|
|
||||||
obj[key] = messages[key].defaultMessage;
|
|
||||||
} else {
|
|
||||||
obj[key] = message || messages[key].defaultMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!message) {
|
|
||||||
console.log(`* Added key ${key}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const json = prettier.format(JSON.stringify(merged), { parser: 'json' });
|
|
||||||
|
|
||||||
fs.writeFileSync(path.resolve(dest, file), json);
|
|
||||||
});
|
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* npm run seed-data -- --verbose # Show detailed progress
|
* npm run seed-data -- --verbose # Show detailed progress
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { seed, type SeedConfig } from './seed/index.js';
|
import { type SeedConfig, seed } from './seed/index.js';
|
||||||
|
|
||||||
function parseArgs(): SeedConfig {
|
function parseArgs(): SeedConfig {
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { weightedRandom, pickRandom, type WeightedOption } from '../utils.js';
|
import { pickRandom, type WeightedOption, weightedRandom } from '../utils.js';
|
||||||
|
|
||||||
export type DeviceType = 'desktop' | 'mobile' | 'tablet';
|
export type DeviceType = 'desktop' | 'mobile' | 'tablet';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { weightedRandom, pickRandom, type WeightedOption } from '../utils.js';
|
import { pickRandom, type WeightedOption, weightedRandom } from '../utils.js';
|
||||||
|
|
||||||
interface GeoLocation {
|
interface GeoLocation {
|
||||||
country: string;
|
country: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { weightedRandom, pickRandom, randomInt, type WeightedOption } from '../utils.js';
|
import { pickRandom, randomInt, type WeightedOption, weightedRandom } from '../utils.js';
|
||||||
|
|
||||||
export type ReferrerType = 'direct' | 'organic' | 'social' | 'paid' | 'referral';
|
export type ReferrerType = 'direct' | 'organic' | 'social' | 'paid' | 'referral';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { weightedRandom, randomInt, type WeightedOption } from '../utils.js';
|
import { randomInt, type WeightedOption, weightedRandom } from '../utils.js';
|
||||||
|
|
||||||
const hourlyWeights: WeightedOption<number>[] = [
|
const hourlyWeights: WeightedOption<number>[] = [
|
||||||
{ value: 0, weight: 0.02 },
|
{ value: 0, weight: 0.02 },
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { uuid, addSeconds, randomInt } from '../utils.js';
|
|
||||||
import { getRandomReferrer } from '../distributions/referrers.js';
|
import { getRandomReferrer } from '../distributions/referrers.js';
|
||||||
|
import { addSeconds, randomInt, uuid } from '../utils.js';
|
||||||
import type { SessionData } from './sessions.js';
|
import type { SessionData } from './sessions.js';
|
||||||
|
|
||||||
export const EVENT_TYPE = {
|
export const EVENT_TYPE = {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { uuid, randomFloat } from '../utils.js';
|
import { randomFloat, uuid } from '../utils.js';
|
||||||
import type { EventData } from './events.js';
|
import type { EventData } from './events.js';
|
||||||
|
|
||||||
export interface RevenueConfig {
|
export interface RevenueConfig {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { uuid } from '../utils.js';
|
|
||||||
import { getRandomDevice } from '../distributions/devices.js';
|
import { getRandomDevice } from '../distributions/devices.js';
|
||||||
import { getRandomGeo, getRandomLanguage } from '../distributions/geographic.js';
|
import { getRandomGeo, getRandomLanguage } from '../distributions/geographic.js';
|
||||||
import { generateTimestampForDay } from '../distributions/temporal.js';
|
import { generateTimestampForDay } from '../distributions/temporal.js';
|
||||||
|
import { uuid } from '../utils.js';
|
||||||
|
|
||||||
export interface SessionData {
|
export interface SessionData {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
+13
-13
@@ -1,35 +1,35 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
import { PrismaPg } from '@prisma/adapter-pg';
|
import { PrismaPg } from '@prisma/adapter-pg';
|
||||||
import { PrismaClient, Prisma } from '../../src/generated/prisma/client.js';
|
import { type Prisma, PrismaClient } from '../../src/generated/prisma/client.js';
|
||||||
import { uuid, generateDatesBetween, subDays, formatNumber, progressBar } from './utils.js';
|
import { getSessionCountForDay } from './distributions/temporal.js';
|
||||||
import { createSessions, type SessionData } from './generators/sessions.js';
|
|
||||||
import {
|
import {
|
||||||
generateEventsForSession,
|
|
||||||
type EventData,
|
type EventData,
|
||||||
type EventDataEntry,
|
type EventDataEntry,
|
||||||
|
generateEventsForSession,
|
||||||
} from './generators/events.js';
|
} from './generators/events.js';
|
||||||
import {
|
import {
|
||||||
generateRevenueForEvents,
|
generateRevenueForEvents,
|
||||||
type RevenueData,
|
|
||||||
type RevenueConfig,
|
type RevenueConfig,
|
||||||
|
type RevenueData,
|
||||||
} from './generators/revenue.js';
|
} from './generators/revenue.js';
|
||||||
import { getSessionCountForDay } from './distributions/temporal.js';
|
import { createSessions, type SessionData } from './generators/sessions.js';
|
||||||
import {
|
import {
|
||||||
BLOG_WEBSITE_NAME,
|
|
||||||
BLOG_WEBSITE_DOMAIN,
|
|
||||||
BLOG_SESSIONS_PER_DAY,
|
BLOG_SESSIONS_PER_DAY,
|
||||||
getBlogSiteConfig,
|
BLOG_WEBSITE_DOMAIN,
|
||||||
|
BLOG_WEBSITE_NAME,
|
||||||
getBlogJourney,
|
getBlogJourney,
|
||||||
|
getBlogSiteConfig,
|
||||||
} from './sites/blog.js';
|
} from './sites/blog.js';
|
||||||
import {
|
import {
|
||||||
SAAS_WEBSITE_NAME,
|
|
||||||
SAAS_WEBSITE_DOMAIN,
|
|
||||||
SAAS_SESSIONS_PER_DAY,
|
|
||||||
getSaasSiteConfig,
|
|
||||||
getSaasJourney,
|
getSaasJourney,
|
||||||
|
getSaasSiteConfig,
|
||||||
|
SAAS_SESSIONS_PER_DAY,
|
||||||
|
SAAS_WEBSITE_DOMAIN,
|
||||||
|
SAAS_WEBSITE_NAME,
|
||||||
saasRevenueConfigs,
|
saasRevenueConfigs,
|
||||||
} from './sites/saas.js';
|
} from './sites/saas.js';
|
||||||
|
import { formatNumber, generateDatesBetween, progressBar, subDays, uuid } from './utils.js';
|
||||||
|
|
||||||
const BATCH_SIZE = 1000;
|
const BATCH_SIZE = 1000;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { weightedRandom, type WeightedOption } from '../utils.js';
|
|
||||||
import type {
|
import type {
|
||||||
SiteConfig,
|
CustomEventConfig,
|
||||||
JourneyConfig,
|
JourneyConfig,
|
||||||
PageConfig,
|
PageConfig,
|
||||||
CustomEventConfig,
|
SiteConfig,
|
||||||
} from '../generators/events.js';
|
} from '../generators/events.js';
|
||||||
|
import { type WeightedOption, weightedRandom } from '../utils.js';
|
||||||
|
|
||||||
export const BLOG_WEBSITE_NAME = 'Demo Blog';
|
export const BLOG_WEBSITE_NAME = 'Demo Blog';
|
||||||
export const BLOG_WEBSITE_DOMAIN = 'blog.example.com';
|
export const BLOG_WEBSITE_DOMAIN = 'blog.example.com';
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { weightedRandom, type WeightedOption } from '../utils.js';
|
|
||||||
import type {
|
import type {
|
||||||
SiteConfig,
|
CustomEventConfig,
|
||||||
JourneyConfig,
|
JourneyConfig,
|
||||||
PageConfig,
|
PageConfig,
|
||||||
CustomEventConfig,
|
SiteConfig,
|
||||||
} from '../generators/events.js';
|
} from '../generators/events.js';
|
||||||
import type { RevenueConfig } from '../generators/revenue.js';
|
import type { RevenueConfig } from '../generators/revenue.js';
|
||||||
|
import { type WeightedOption, weightedRandom } from '../utils.js';
|
||||||
|
|
||||||
export const SAAS_WEBSITE_NAME = 'Demo SaaS';
|
export const SAAS_WEBSITE_NAME = 'Demo SaaS';
|
||||||
export const SAAS_WEBSITE_DOMAIN = 'app.example.com';
|
export const SAAS_WEBSITE_DOMAIN = 'app.example.com';
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug
|
|||||||
payload: {
|
payload: {
|
||||||
pixel: pixel.id,
|
pixel: pixel.id,
|
||||||
url: request.url,
|
url: request.url,
|
||||||
referrer: request.headers.get('referer'),
|
referrer: request.headers.get("referer") || undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug
|
|||||||
payload: {
|
payload: {
|
||||||
link: link.id,
|
link: link.id,
|
||||||
url: request.url,
|
url: request.url,
|
||||||
referrer: request.headers.get('referer'),
|
referrer: request.headers.get("referer") || undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+26
-8
@@ -4,6 +4,7 @@ import Script from 'next/script';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { MobileNav } from '@/app/(main)/MobileNav';
|
import { MobileNav } from '@/app/(main)/MobileNav';
|
||||||
import { SideNav } from '@/app/(main)/SideNav';
|
import { SideNav } from '@/app/(main)/SideNav';
|
||||||
|
import { TopNav } from '@/app/(main)/TopNav';
|
||||||
import { useConfig, useLoginQuery, useNavigation } from '@/components/hooks';
|
import { useConfig, useLoginQuery, useNavigation } from '@/components/hooks';
|
||||||
import { LAST_TEAM_CONFIG } from '@/lib/constants';
|
import { LAST_TEAM_CONFIG } from '@/lib/constants';
|
||||||
import { removeItem, setItem } from '@/lib/storage';
|
import { removeItem, setItem } from '@/lib/storage';
|
||||||
@@ -39,24 +40,41 @@ export function App({ children }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid
|
<Grid
|
||||||
columns={{ xs: '1fr', lg: 'auto 1fr' }}
|
columns={{ base: '1fr', lg: 'auto 1fr' }}
|
||||||
rows={{ xs: 'auto 1fr', lg: '1fr' }}
|
rows={{ base: 'auto 1fr', lg: '1fr' }}
|
||||||
height={{ xs: 'auto', lg: '100vh' }}
|
height="screen"
|
||||||
width="100%"
|
|
||||||
>
|
>
|
||||||
<Row display={{ xs: 'flex', lg: 'none' }} alignItems="center" gap padding="3">
|
<Row display={{ base: 'flex', lg: 'none' }} alignItems="center" gap padding="3">
|
||||||
<MobileNav />
|
<MobileNav />
|
||||||
</Row>
|
</Row>
|
||||||
<Column display={{ xs: 'none', lg: 'flex' }}>
|
<Column display={{ base: 'none', lg: 'flex' }} minHeight="0" style={{ overflow: 'hidden' }}>
|
||||||
<SideNav />
|
<SideNav />
|
||||||
</Column>
|
</Column>
|
||||||
<Column alignItems="center" overflowY="auto" overflowX="hidden" position="relative">
|
<Column overflowX="hidden" minHeight="0" position="relative">
|
||||||
{children}
|
<TopNav />
|
||||||
|
<Column alignItems="center">{children}</Column>
|
||||||
</Column>
|
</Column>
|
||||||
<UpdateNotice user={user} config={config} />
|
<UpdateNotice user={user} config={config} />
|
||||||
{process.env.NODE_ENV === 'production' && !pathname.includes('/share/') && (
|
{process.env.NODE_ENV === 'production' && !pathname.includes('/share/') && (
|
||||||
<Script src={`${process.env.basePath || ''}/telemetry.js`} />
|
<Script src={`${process.env.basePath || ''}/telemetry.js`} />
|
||||||
)}
|
)}
|
||||||
|
{process.env.selfTrack && (
|
||||||
|
<Script
|
||||||
|
async
|
||||||
|
data-website-id={process.env.selfTrack}
|
||||||
|
src={`${process.env.basePath || ''}/script.js`}
|
||||||
|
data-cache="true"
|
||||||
|
data-performance="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{process.env.selfRecord && (
|
||||||
|
<Script
|
||||||
|
async
|
||||||
|
data-website-id={process.env.selfRecord}
|
||||||
|
data-sample-rate="1"
|
||||||
|
src={`${process.env.basePath || ''}/recorder.js`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,63 +1,78 @@
|
|||||||
import { Grid, IconLabel, NavMenu, NavMenuItem, Row, Text } from '@umami/react-zen';
|
import { Column, Grid, Row, Text } from '@umami/react-zen';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
|
import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
|
||||||
|
import { IconLabel } from '@/components/common/IconLabel';
|
||||||
import { useMessages, useNavigation } from '@/components/hooks';
|
import { useMessages, useNavigation } from '@/components/hooks';
|
||||||
import { Globe, Grid2x2, LinkIcon } from '@/components/icons';
|
import { Globe, Grid2x2, LayoutDashboard, LinkIcon } from '@/components/icons';
|
||||||
import { MobileMenuButton } from '@/components/input/MobileMenuButton';
|
import { MobileMenuButton } from '@/components/input/MobileMenuButton';
|
||||||
import { NavButton } from '@/components/input/NavButton';
|
import { UserButton } from '@/components/input/UserButton';
|
||||||
import { Logo } from '@/components/svg';
|
import { Logo } from '@/components/svg';
|
||||||
import { AdminNav } from './admin/AdminNav';
|
import { AdminNav } from './admin/AdminNav';
|
||||||
import { SettingsNav } from './settings/SettingsNav';
|
import { SettingsNav } from './settings/SettingsNav';
|
||||||
|
|
||||||
export function MobileNav() {
|
export function MobileNav() {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { t, labels } = useMessages();
|
||||||
const { pathname, websiteId, renderUrl } = useNavigation();
|
const { pathname, websiteId, renderUrl } = useNavigation();
|
||||||
const isAdmin = pathname.includes('/admin');
|
const isAdmin = pathname.includes('/admin');
|
||||||
const isSettings = pathname.includes('/settings');
|
const isSettings = pathname.includes('/settings');
|
||||||
|
const isMain = !websiteId && !isAdmin && !isSettings;
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
|
{
|
||||||
|
id: 'boards',
|
||||||
|
label: t(labels.boards),
|
||||||
|
path: '/boards',
|
||||||
|
icon: <LayoutDashboard />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'websites',
|
id: 'websites',
|
||||||
label: formatMessage(labels.websites),
|
label: t(labels.websites),
|
||||||
path: '/websites',
|
path: '/websites',
|
||||||
icon: <Globe />,
|
icon: <Globe />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'links',
|
id: 'links',
|
||||||
label: formatMessage(labels.links),
|
label: t(labels.links),
|
||||||
path: '/links',
|
path: '/links',
|
||||||
icon: <LinkIcon />,
|
icon: <LinkIcon />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'pixels',
|
id: 'pixels',
|
||||||
label: formatMessage(labels.pixels),
|
label: t(labels.pixels),
|
||||||
path: '/pixels',
|
path: '/pixels',
|
||||||
icon: <Grid2x2 />,
|
icon: <Grid2x2 />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid columns="auto 1fr" flexGrow={1} backgroundColor="3" borderRadius>
|
<Grid columns="auto 1fr" flexGrow={1} backgroundColor="surface-sunken" borderRadius>
|
||||||
<MobileMenuButton>
|
<MobileMenuButton>
|
||||||
{({ close }) => {
|
{({ close }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<Column
|
||||||
<NavMenu padding="3" onItemClick={close} border="bottom">
|
gap="2"
|
||||||
<NavButton />
|
display="flex"
|
||||||
{links.map(link => {
|
flex-direction="column"
|
||||||
|
padding="1"
|
||||||
|
style={{ height: '100dvh' }}
|
||||||
|
>
|
||||||
|
{isMain &&
|
||||||
|
links.map(link => {
|
||||||
return (
|
return (
|
||||||
<Link key={link.id} href={renderUrl(link.path)}>
|
<Row key={link.id} padding>
|
||||||
<NavMenuItem>
|
<Link href={renderUrl(link.path)} onClick={close}>
|
||||||
<IconLabel icon={link.icon} label={link.label} />
|
<IconLabel icon={link.icon} label={link.label} />
|
||||||
</NavMenuItem>
|
|
||||||
</Link>
|
</Link>
|
||||||
|
</Row>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</NavMenu>
|
|
||||||
{websiteId && <WebsiteNav websiteId={websiteId} onItemClick={close} />}
|
{websiteId && <WebsiteNav websiteId={websiteId} onItemClick={close} />}
|
||||||
{isAdmin && <AdminNav onItemClick={close} />}
|
{isAdmin && <AdminNav onItemClick={close} />}
|
||||||
{isSettings && <SettingsNav onItemClick={close} />}
|
{isSettings && <SettingsNav onItemClick={close} />}
|
||||||
</>
|
<Row style={{ marginTop: 'auto' }}>
|
||||||
|
<UserButton onClose={close} />
|
||||||
|
</Row>
|
||||||
|
</Column>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</MobileMenuButton>
|
</MobileMenuButton>
|
||||||
|
|||||||
+119
-48
@@ -1,87 +1,158 @@
|
|||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
|
type ButtonProps,
|
||||||
|
Column,
|
||||||
|
Focusable,
|
||||||
|
Icon,
|
||||||
Row,
|
Row,
|
||||||
Sidebar,
|
Text,
|
||||||
SidebarHeader,
|
Tooltip,
|
||||||
SidebarItem,
|
TooltipTrigger,
|
||||||
type SidebarProps,
|
|
||||||
SidebarSection,
|
|
||||||
ThemeButton,
|
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import type { Key } from 'react';
|
import { SettingsNav } from '@/app/(main)/settings/SettingsNav';
|
||||||
|
import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
|
||||||
|
import { IconLabel } from '@/components/common/IconLabel';
|
||||||
import { useGlobalState, useMessages, useNavigation } from '@/components/hooks';
|
import { useGlobalState, useMessages, useNavigation } from '@/components/hooks';
|
||||||
import { Globe, Grid2x2, LinkIcon, PanelLeft } from '@/components/icons';
|
import {
|
||||||
import { LanguageButton } from '@/components/input/LanguageButton';
|
Globe,
|
||||||
import { NavButton } from '@/components/input/NavButton';
|
Grid2x2,
|
||||||
import { PanelButton } from '@/components/input/PanelButton';
|
LayoutDashboard,
|
||||||
|
LinkIcon,
|
||||||
|
PanelLeft,
|
||||||
|
PanelsLeftBottom,
|
||||||
|
} from '@/components/icons';
|
||||||
|
import { UserButton } from '@/components/input/UserButton';
|
||||||
import { Logo } from '@/components/svg';
|
import { Logo } from '@/components/svg';
|
||||||
|
|
||||||
export function SideNav(props: SidebarProps) {
|
export function SideNav(props: any) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { t, labels } = useMessages();
|
||||||
const { pathname, renderUrl, websiteId, router } = useNavigation();
|
const { pathname, renderUrl, websiteId, teamId } = useNavigation();
|
||||||
const [isCollapsed, setIsCollapsed] = useGlobalState('sidenav-collapsed');
|
const [isCollapsed] = useGlobalState('sidenav-collapsed', false);
|
||||||
|
|
||||||
const hasNav = !!(websiteId || pathname.startsWith('/admin') || pathname.includes('/settings'));
|
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
|
...(!teamId
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: 'dashboard',
|
||||||
|
label: t(labels.dashboard),
|
||||||
|
path: '/dashboard',
|
||||||
|
icon: <PanelsLeftBottom />,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
id: 'boards',
|
||||||
|
label: t(labels.boards),
|
||||||
|
path: '/boards',
|
||||||
|
icon: <LayoutDashboard />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'websites',
|
id: 'websites',
|
||||||
label: formatMessage(labels.websites),
|
label: t(labels.websites),
|
||||||
path: '/websites',
|
path: '/websites',
|
||||||
icon: <Globe />,
|
icon: <Globe />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'links',
|
id: 'links',
|
||||||
label: formatMessage(labels.links),
|
label: t(labels.links),
|
||||||
path: '/links',
|
path: '/links',
|
||||||
icon: <LinkIcon />,
|
icon: <LinkIcon />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'pixels',
|
id: 'pixels',
|
||||||
label: formatMessage(labels.pixels),
|
label: t(labels.pixels),
|
||||||
path: '/pixels',
|
path: '/pixels',
|
||||||
icon: <Grid2x2 />,
|
icon: <Grid2x2 />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleSelect = (id: Key) => {
|
|
||||||
router.push(id === 'user' ? '/websites' : `/teams/${id}/websites`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar {...props} isCollapsed={isCollapsed || hasNav} backgroundColor>
|
<Column
|
||||||
<SidebarSection onClick={() => setIsCollapsed(false)}>
|
{...props}
|
||||||
<SidebarHeader
|
backgroundColor="surface-base"
|
||||||
label="umami"
|
border
|
||||||
icon={isCollapsed && !hasNav ? <PanelLeft /> : <Logo />}
|
borderRadius
|
||||||
style={{ maxHeight: 40 }}
|
paddingX="2"
|
||||||
|
flexGrow="1"
|
||||||
|
minHeight="0"
|
||||||
|
margin="2"
|
||||||
|
style={{
|
||||||
|
width: isCollapsed ? '55px' : '240px',
|
||||||
|
transition: 'width 0.2s ease-in-out',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{!isCollapsed && !hasNav && <PanelButton />}
|
<Row
|
||||||
</SidebarHeader>
|
alignItems="center"
|
||||||
</SidebarSection>
|
justifyContent="space-between"
|
||||||
<SidebarSection paddingTop="0" paddingBottom="0" justifyContent="center">
|
height="60px"
|
||||||
<NavButton showText={!hasNav && !isCollapsed} onAction={handleSelect} />
|
style={{ flexShrink: 0 }}
|
||||||
</SidebarSection>
|
>
|
||||||
<SidebarSection flexGrow={1}>
|
<Row paddingX="3" alignItems="center" justifyContent="space-between" flexGrow="1">
|
||||||
|
{!isCollapsed && (
|
||||||
|
<IconLabel icon={<Logo />}>
|
||||||
|
<Text weight="bold">umami</Text>
|
||||||
|
</IconLabel>
|
||||||
|
)}
|
||||||
|
<PanelButton />
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
<Column flexGrow="1" minHeight="0" style={{ overflowY: 'auto', overflowX: 'hidden' }}>
|
||||||
|
{websiteId ? (
|
||||||
|
<WebsiteNav websiteId={websiteId} isCollapsed={isCollapsed} />
|
||||||
|
) : pathname.includes('/settings') ? (
|
||||||
|
<SettingsNav isCollapsed={isCollapsed} />
|
||||||
|
) : (
|
||||||
|
<Column gap="2">
|
||||||
{links.map(({ id, path, label, icon }) => {
|
{links.map(({ id, path, label, icon }) => {
|
||||||
|
const isSelected = pathname.startsWith(renderUrl(path, false));
|
||||||
return (
|
return (
|
||||||
<Link key={id} href={renderUrl(path, false)} role="button">
|
<Link key={id} href={renderUrl(path, false)} role="button">
|
||||||
<SidebarItem
|
<TooltipTrigger isDisabled={!isCollapsed} delay={0}>
|
||||||
label={label}
|
<Focusable>
|
||||||
|
<Row
|
||||||
|
alignItems="center"
|
||||||
|
hover={{ backgroundColor: 'surface-sunken' }}
|
||||||
|
backgroundColor={isSelected ? 'surface-sunken' : undefined}
|
||||||
|
borderRadius
|
||||||
|
minHeight="40px"
|
||||||
|
>
|
||||||
|
<IconLabel
|
||||||
icon={icon}
|
icon={icon}
|
||||||
isSelected={pathname.includes(path)}
|
label={isCollapsed ? '' : label}
|
||||||
role="button"
|
weight={isSelected ? 'bold' : undefined}
|
||||||
|
padding
|
||||||
/>
|
/>
|
||||||
|
</Row>
|
||||||
|
</Focusable>
|
||||||
|
<Tooltip placement="right">{label}</Tooltip>
|
||||||
|
</TooltipTrigger>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</SidebarSection>
|
</Column>
|
||||||
<SidebarSection justifyContent="flex-start">
|
)}
|
||||||
<Row wrap="wrap">
|
</Column>
|
||||||
<LanguageButton />
|
<Row marginBottom="4" paddingTop="2">
|
||||||
<ThemeButton />
|
<UserButton showText={!isCollapsed} />
|
||||||
</Row>
|
</Row>
|
||||||
</SidebarSection>
|
</Column>
|
||||||
</Sidebar>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PanelButton = (props: ButtonProps) => {
|
||||||
|
const [isCollapsed, setIsCollapsed] = useGlobalState('sidenav-collapsed', false);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onPress={() => setIsCollapsed(!isCollapsed)}
|
||||||
|
variant="zero"
|
||||||
|
{...props}
|
||||||
|
style={{ padding: 0 }}
|
||||||
|
>
|
||||||
|
<Icon strokeColor="muted">
|
||||||
|
<PanelLeft />
|
||||||
|
</Icon>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
+95
-11
@@ -1,26 +1,110 @@
|
|||||||
import { Row, ThemeButton } from '@umami/react-zen';
|
'use client';
|
||||||
import { LanguageButton } from '@/components/input/LanguageButton';
|
import { Icon, Row } from '@umami/react-zen';
|
||||||
import { ProfileButton } from '@/components/input/ProfileButton';
|
import { useNavigation } from '@/components/hooks';
|
||||||
|
import { Slash } from '@/components/icons';
|
||||||
|
import { BoardSelect } from '@/components/input/BoardSelect';
|
||||||
|
import { LinkSelect } from '@/components/input/LinkSelect';
|
||||||
|
import { PixelSelect } from '@/components/input/PixelSelect';
|
||||||
|
import { TeamsButton } from '@/components/input/TeamsButton';
|
||||||
|
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
|
||||||
|
|
||||||
export function TopNav() {
|
export function TopNav() {
|
||||||
|
const { websiteId, linkId, pixelId, boardId, teamId, router, renderUrl } = useNavigation();
|
||||||
|
|
||||||
|
const handleWebsiteChange = (value: string) => {
|
||||||
|
router.push(renderUrl(`/websites/${value}`));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLinkChange = (value: string) => {
|
||||||
|
router.push(renderUrl(`/links/${value}`));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePixelChange = (value: string) => {
|
||||||
|
router.push(renderUrl(`/pixels/${value}`));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBoardChange = (value: string) => {
|
||||||
|
router.push(renderUrl(`/boards/${value}`));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row
|
<Row
|
||||||
position="absolute"
|
position="sticky"
|
||||||
top="0"
|
top="0"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="flex-end"
|
justifyContent="flex-start"
|
||||||
paddingY="2"
|
paddingY="2"
|
||||||
paddingX="3"
|
paddingX="3"
|
||||||
paddingRight="5"
|
paddingRight="5"
|
||||||
width="100%"
|
width="100%"
|
||||||
style={{ position: 'sticky', top: 0 }}
|
zIndex={100}
|
||||||
zIndex={1}
|
backgroundColor="surface-raised"
|
||||||
>
|
>
|
||||||
<Row alignItems="center" justifyContent="flex-end" backgroundColor="2" borderRadius>
|
<Row alignItems="center">
|
||||||
<ThemeButton />
|
<TeamsButton />
|
||||||
<LanguageButton />
|
{(websiteId || linkId || pixelId || boardId) && (
|
||||||
<ProfileButton />
|
<>
|
||||||
|
<Icon size="sm" color="muted" style={{ opacity: 0.7, margin: '0 6px' }}>
|
||||||
|
<Slash />
|
||||||
|
</Icon>
|
||||||
|
{websiteId && (
|
||||||
|
<WebsiteSelect
|
||||||
|
websiteId={websiteId}
|
||||||
|
teamId={teamId}
|
||||||
|
onChange={handleWebsiteChange}
|
||||||
|
buttonProps={{
|
||||||
|
variant: 'quiet',
|
||||||
|
style: { minHeight: 40, minWidth: 200, maxWidth: 200 },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{linkId && (
|
||||||
|
<LinkSelect
|
||||||
|
linkId={linkId}
|
||||||
|
teamId={teamId}
|
||||||
|
onChange={handleLinkChange}
|
||||||
|
buttonProps={{
|
||||||
|
variant: 'quiet',
|
||||||
|
style: { minHeight: 40, minWidth: 200, maxWidth: 200 },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{pixelId && (
|
||||||
|
<PixelSelect
|
||||||
|
pixelId={pixelId}
|
||||||
|
teamId={teamId}
|
||||||
|
onChange={handlePixelChange}
|
||||||
|
buttonProps={{
|
||||||
|
variant: 'quiet',
|
||||||
|
style: { minHeight: 40, minWidth: 200, maxWidth: 200 },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{boardId && (
|
||||||
|
<BoardSelect
|
||||||
|
boardId={boardId}
|
||||||
|
teamId={teamId}
|
||||||
|
onChange={handleBoardChange}
|
||||||
|
buttonProps={{
|
||||||
|
variant: 'quiet',
|
||||||
|
style: { minHeight: 40, minWidth: 200, maxWidth: 200 },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: -16,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: 16,
|
||||||
|
background: 'linear-gradient(to bottom, var(--surface-raised), transparent)',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user