Merge branch 'dev' into master
This commit is contained in:
476 files changed
+5604
-5121
No files matched your search
+3
-14
@@ -22,26 +22,15 @@
|
||||
"plugins": ["@typescript-eslint", "prettier"],
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"alias": {
|
||||
"map": [
|
||||
["assets", "./src/assets"],
|
||||
["components", "./src/components"],
|
||||
["db", "./db"],
|
||||
["hooks", "./src/components/hooks"],
|
||||
["lang", "./src/lang"],
|
||||
["lib", "./src/lib"],
|
||||
["public", "./public"],
|
||||
["queries", "./src/queries"],
|
||||
["store", "./src/store"],
|
||||
["styles", "./src/styles"]
|
||||
],
|
||||
"extensions": [".ts", ".tsx", ".js", ".jsx", ".json"]
|
||||
"node": {
|
||||
"moduleDirectory": ["node_modules", "src/"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"no-console": "error",
|
||||
"react/display-name": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/prop-types": "off",
|
||||
"import/no-anonymous-default-export": "off",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: "🐛 Bug Report"
|
||||
name: '🐛 Bug Report'
|
||||
description: Create a bug report for Umami.
|
||||
body:
|
||||
- type: textarea
|
||||
@@ -22,6 +22,10 @@ body:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
- type: input
|
||||
attributes:
|
||||
label: Which Umami version are you using? (if relevant)
|
||||
description: 'For example: Chrome, Edge, Firefox, etc'
|
||||
- type: input
|
||||
attributes:
|
||||
label: Which browser are you using? (if relevant)
|
||||
@@ -29,4 +33,4 @@ body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: How are you deploying your application? (if relevant)
|
||||
description: 'For example: Vercel, Railway, Docker, etc'
|
||||
description: 'For example: Vercel, Railway, Docker, etc'
|
||||
@@ -2,6 +2,21 @@ name: Create docker images
|
||||
|
||||
on: [create]
|
||||
|
||||
env:
|
||||
# set Docker OCI Labels
|
||||
DOCKER_LABELS: >
|
||||
org.opencontainers.image.title=${{github.event.repository.name}},
|
||||
org.opencontainers.image.description="Umami is a simple, fast, privacy-focused alternative to Google Analytics",
|
||||
org.opencontainers.image.vendor=${{github.repository_owner}},
|
||||
org.opencontainers.image.licenses="MIT",
|
||||
org.opencontainers.image.version=${{github.ref_name}},
|
||||
org.opencontainers.image.created=${{ env.NOW }},
|
||||
org.opencontainers.image.source=${{github.server_url}}/${{github.repository}},
|
||||
org.opencontainers.image.revision=${{github.sha}},
|
||||
org.opencontainers.image.url="https://umami.is/",
|
||||
org.opencontainers.image.documentation="https://umami.is/docs",
|
||||
org.opencontainers.image.base.name="docker.io/library/node:18-alpine"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build, push, and deploy
|
||||
@@ -16,13 +31,16 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
run: |
|
||||
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
echo "NOW=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
||||
|
||||
- uses: mr-smithers-excellent/docker-build-push@v6
|
||||
name: Build & push Docker image to ghcr.io for ${{ matrix.db-type }}
|
||||
with:
|
||||
image: umami
|
||||
tags: ${{ matrix.db-type }}-${{ env.RELEASE_VERSION }}, ${{ matrix.db-type }}-latest
|
||||
labels: $DOCKER_LABELS
|
||||
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
|
||||
registry: ghcr.io
|
||||
multiPlatform: true
|
||||
@@ -30,11 +48,13 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
- uses: mr-smithers-excellent/docker-build-push@v6
|
||||
name: Build & push Docker image to docker.io for ${{ matrix.db-type }}
|
||||
with:
|
||||
image: umamisoftware/umami
|
||||
tags: ${{ matrix.db-type }}-${{ env.RELEASE_VERSION }}, ${{ matrix.db-type }}-latest
|
||||
labels: $DOCKER_LABELS
|
||||
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
|
||||
@@ -16,15 +16,15 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- node-version: 18.x
|
||||
- node-version: 18.17
|
||||
db-type: postgresql
|
||||
- node-version: 18.x
|
||||
- node-version: 18.17
|
||||
db-type: mysql
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
|
||||
@@ -49,7 +49,7 @@ mysql://username:mypassword@localhost:3306/mydb
|
||||
yarn build
|
||||
```
|
||||
|
||||
The build step will also create tables in your database if you ae installing for the first time. It will also create a login user with username **admin** and password **umami**.
|
||||
The build step will also create tables in your database if you are installing for the first time. It will also create a login user with username **admin** and password **umami**.
|
||||
|
||||
### Start the application
|
||||
|
||||
|
||||
+1
-130
@@ -1,5 +1,3 @@
|
||||
SET allow_experimental_object_type = 1;
|
||||
|
||||
-- Create Event
|
||||
CREATE TABLE umami.website_event
|
||||
(
|
||||
@@ -34,82 +32,6 @@ CREATE TABLE umami.website_event
|
||||
ORDER BY (website_id, session_id, created_at)
|
||||
SETTINGS index_granularity = 8192;
|
||||
|
||||
CREATE TABLE umami.website_event_queue (
|
||||
website_id UUID,
|
||||
session_id UUID,
|
||||
event_id UUID,
|
||||
--sessions
|
||||
hostname LowCardinality(String),
|
||||
browser LowCardinality(String),
|
||||
os LowCardinality(String),
|
||||
device LowCardinality(String),
|
||||
screen LowCardinality(String),
|
||||
language LowCardinality(String),
|
||||
country LowCardinality(String),
|
||||
subdivision1 LowCardinality(String),
|
||||
subdivision2 LowCardinality(String),
|
||||
city String,
|
||||
--pageviews
|
||||
url_path String,
|
||||
url_query String,
|
||||
referrer_path String,
|
||||
referrer_query String,
|
||||
referrer_domain String,
|
||||
page_title String,
|
||||
--events
|
||||
event_type UInt32,
|
||||
event_name String,
|
||||
created_at DateTime('UTC'),
|
||||
--virtual columns
|
||||
_error String,
|
||||
_raw_message String
|
||||
)
|
||||
ENGINE = Kafka
|
||||
SETTINGS kafka_broker_list = 'domain:9092,domain:9093,domain:9094', -- input broker list
|
||||
kafka_topic_list = 'event',
|
||||
kafka_group_name = 'event_consumer_group',
|
||||
kafka_format = 'JSONEachRow',
|
||||
kafka_max_block_size = 1048576,
|
||||
kafka_handle_error_mode = 'stream';
|
||||
|
||||
CREATE MATERIALIZED VIEW umami.website_event_queue_mv TO umami.website_event AS
|
||||
SELECT website_id,
|
||||
session_id,
|
||||
event_id,
|
||||
hostname,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
subdivision1,
|
||||
subdivision2,
|
||||
city,
|
||||
url_path,
|
||||
url_query,
|
||||
referrer_path,
|
||||
referrer_query,
|
||||
referrer_domain,
|
||||
page_title,
|
||||
event_type,
|
||||
event_name,
|
||||
created_at
|
||||
FROM umami.website_event_queue;
|
||||
|
||||
CREATE MATERIALIZED VIEW umami.website_event_errors_mv
|
||||
(
|
||||
error String,
|
||||
raw String
|
||||
)
|
||||
ENGINE = MergeTree
|
||||
ORDER BY (error, raw)
|
||||
SETTINGS index_granularity = 8192 AS
|
||||
SELECT _error AS error,
|
||||
_raw_message AS raw
|
||||
FROM umami.website_event_queue
|
||||
WHERE length(_error) > 0;
|
||||
|
||||
CREATE TABLE umami.event_data
|
||||
(
|
||||
website_id UUID,
|
||||
@@ -127,55 +49,4 @@ CREATE TABLE umami.event_data
|
||||
)
|
||||
engine = MergeTree
|
||||
ORDER BY (website_id, event_id, event_key, created_at)
|
||||
SETTINGS index_granularity = 8192;
|
||||
|
||||
CREATE TABLE umami.event_data_queue (
|
||||
website_id UUID,
|
||||
session_id UUID,
|
||||
event_id UUID,
|
||||
url_path String,
|
||||
event_name String,
|
||||
event_key String,
|
||||
string_value Nullable(String),
|
||||
number_value Nullable(Decimal64(4)), --922337203685477.5625
|
||||
date_value Nullable(DateTime('UTC')),
|
||||
data_type UInt32,
|
||||
created_at DateTime('UTC'),
|
||||
--virtual columns
|
||||
_error String,
|
||||
_raw_message String
|
||||
)
|
||||
ENGINE = Kafka
|
||||
SETTINGS kafka_broker_list = 'domain:9092,domain:9093,domain:9094', -- input broker list
|
||||
kafka_topic_list = 'event_data',
|
||||
kafka_group_name = 'event_data_consumer_group',
|
||||
kafka_format = 'JSONEachRow',
|
||||
kafka_max_block_size = 1048576,
|
||||
kafka_handle_error_mode = 'stream';
|
||||
|
||||
CREATE MATERIALIZED VIEW umami.event_data_queue_mv TO umami.event_data AS
|
||||
SELECT website_id,
|
||||
session_id,
|
||||
event_id,
|
||||
url_path,
|
||||
event_name,
|
||||
event_key,
|
||||
string_value,
|
||||
number_value,
|
||||
date_value,
|
||||
data_type,
|
||||
created_at
|
||||
FROM umami.event_data_queue;
|
||||
|
||||
CREATE MATERIALIZED VIEW umami.event_data_errors_mv
|
||||
(
|
||||
error String,
|
||||
raw String
|
||||
)
|
||||
ENGINE = MergeTree
|
||||
ORDER BY (error, raw)
|
||||
SETTINGS index_granularity = 8192 AS
|
||||
SELECT _error AS error,
|
||||
_raw_message AS raw
|
||||
FROM umami.event_data_queue
|
||||
WHERE length(_error) > 0;
|
||||
SETTINGS index_granularity = 8192;
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `team_website` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE `team` ADD COLUMN `deleted_at` TIMESTAMP(0) NULL,
|
||||
ADD COLUMN `logo_url` VARCHAR(2183) NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `user` ADD COLUMN `display_name` VARCHAR(255) NULL,
|
||||
ADD COLUMN `logo_url` VARCHAR(2183) NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `website` ADD COLUMN `created_by` VARCHAR(36) NULL,
|
||||
ADD COLUMN `team_id` VARCHAR(36) NULL;
|
||||
|
||||
-- MigrateData
|
||||
UPDATE `website` SET created_by = user_id WHERE team_id IS NULL;
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE `team_website`;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX `website_team_id_idx` ON `website`(`team_id`);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX `website_created_by_idx` ON `website`(`created_by`);
|
||||
+21
-25
@@ -9,15 +9,18 @@ datasource db {
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @unique @map("user_id") @db.VarChar(36)
|
||||
username String @unique @db.VarChar(255)
|
||||
password String @db.VarChar(60)
|
||||
role String @map("role") @db.VarChar(50)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0)
|
||||
deletedAt DateTime? @map("deleted_at") @db.Timestamp(0)
|
||||
id String @id @unique @map("user_id") @db.VarChar(36)
|
||||
username String @unique @db.VarChar(255)
|
||||
password String @db.VarChar(60)
|
||||
role String @map("role") @db.VarChar(50)
|
||||
logoUrl String? @map("logo_url") @db.VarChar(2183)
|
||||
displayName String? @map("display_name") @db.VarChar(255)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0)
|
||||
deletedAt DateTime? @map("deleted_at") @db.Timestamp(0)
|
||||
|
||||
website Website[]
|
||||
websiteUser Website[] @relation("user")
|
||||
websiteCreateUser Website[] @relation("createUser")
|
||||
teamUser TeamUser[]
|
||||
report Report[]
|
||||
|
||||
@@ -64,19 +67,24 @@ model Website {
|
||||
shareId String? @unique @map("share_id") @db.VarChar(50)
|
||||
resetAt DateTime? @map("reset_at") @db.Timestamp(0)
|
||||
userId String? @map("user_id") @db.VarChar(36)
|
||||
teamId String? @map("team_id") @db.VarChar(36)
|
||||
createdBy String? @map("created_by") @db.VarChar(36)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0)
|
||||
deletedAt DateTime? @map("deleted_at") @db.Timestamp(0)
|
||||
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
teamWebsite TeamWebsite[]
|
||||
user User? @relation("user", fields: [userId], references: [id])
|
||||
createUser User? @relation("createUser", fields: [createdBy], references: [id])
|
||||
team Team? @relation(fields: [teamId], references: [id])
|
||||
eventData EventData[]
|
||||
report Report[]
|
||||
sessionData SessionData[]
|
||||
|
||||
@@index([userId])
|
||||
@@index([teamId])
|
||||
@@index([createdAt])
|
||||
@@index([shareId])
|
||||
@@index([createdBy])
|
||||
@@map("website")
|
||||
}
|
||||
|
||||
@@ -157,11 +165,13 @@ model Team {
|
||||
id String @id() @unique() @map("team_id") @db.VarChar(36)
|
||||
name String @db.VarChar(50)
|
||||
accessCode String? @unique @map("access_code") @db.VarChar(50)
|
||||
logoUrl String? @map("logo_url") @db.VarChar(2183)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0)
|
||||
deletedAt DateTime? @map("deleted_at") @db.Timestamp(0)
|
||||
|
||||
website Website[]
|
||||
teamUser TeamUser[]
|
||||
teamWebsite TeamWebsite[]
|
||||
|
||||
@@index([accessCode])
|
||||
@@map("team")
|
||||
@@ -183,20 +193,6 @@ model TeamUser {
|
||||
@@map("team_user")
|
||||
}
|
||||
|
||||
model TeamWebsite {
|
||||
id String @id() @unique() @map("team_website_id") @db.VarChar(36)
|
||||
teamId String @map("team_id") @db.VarChar(36)
|
||||
websiteId String @map("website_id") @db.VarChar(36)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||
|
||||
team Team @relation(fields: [teamId], references: [id])
|
||||
website Website @relation(fields: [websiteId], references: [id])
|
||||
|
||||
@@index([teamId])
|
||||
@@index([websiteId])
|
||||
@@map("team_website")
|
||||
}
|
||||
|
||||
model Report {
|
||||
id String @id() @unique() @map("report_id") @db.VarChar(36)
|
||||
userId String @map("user_id") @db.VarChar(36)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `team_website` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "team" ADD COLUMN "deleted_at" TIMESTAMPTZ(6),
|
||||
ADD COLUMN "logo_url" VARCHAR(2183);
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "user" ADD COLUMN "display_name" VARCHAR(255),
|
||||
ADD COLUMN "logo_url" VARCHAR(2183);
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "website" ADD COLUMN "created_by" UUID,
|
||||
ADD COLUMN "team_id" UUID;
|
||||
|
||||
-- MigrateData
|
||||
UPDATE "website" SET created_by = user_id WHERE team_id IS NULL;
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "team_website";
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "website_team_id_idx" ON "website"("team_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "website_created_by_idx" ON "website"("created_by");
|
||||
+24
-28
@@ -9,17 +9,20 @@ datasource db {
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @unique @map("user_id") @db.Uuid
|
||||
username String @unique @db.VarChar(255)
|
||||
password String @db.VarChar(60)
|
||||
role String @map("role") @db.VarChar(50)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
|
||||
id String @id @unique @map("user_id") @db.Uuid
|
||||
username String @unique @db.VarChar(255)
|
||||
password String @db.VarChar(60)
|
||||
role String @map("role") @db.VarChar(50)
|
||||
logoUrl String? @map("logo_url") @db.VarChar(2183)
|
||||
displayName String? @map("display_name") @db.VarChar(255)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
|
||||
|
||||
website Website[]
|
||||
teamUser TeamUser[]
|
||||
report Report[]
|
||||
websiteUser Website[] @relation("user")
|
||||
websiteCreateUser Website[] @relation("createUser")
|
||||
teamUser TeamUser[]
|
||||
report Report[]
|
||||
|
||||
@@map("user")
|
||||
}
|
||||
@@ -64,19 +67,24 @@ model Website {
|
||||
shareId String? @unique @map("share_id") @db.VarChar(50)
|
||||
resetAt DateTime? @map("reset_at") @db.Timestamptz(6)
|
||||
userId String? @map("user_id") @db.Uuid
|
||||
teamId String? @map("team_id") @db.Uuid
|
||||
createdBy String? @map("created_by") @db.Uuid
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
|
||||
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
teamWebsite TeamWebsite[]
|
||||
user User? @relation("user", fields: [userId], references: [id])
|
||||
createUser User? @relation("createUser", fields: [createdBy], references: [id])
|
||||
team Team? @relation(fields: [teamId], references: [id])
|
||||
eventData EventData[]
|
||||
report Report[]
|
||||
sessionData SessionData[]
|
||||
|
||||
@@index([userId])
|
||||
@@index([teamId])
|
||||
@@index([createdAt])
|
||||
@@index([shareId])
|
||||
@@index([createdBy])
|
||||
@@map("website")
|
||||
}
|
||||
|
||||
@@ -157,11 +165,13 @@ model Team {
|
||||
id String @id() @unique() @map("team_id") @db.Uuid
|
||||
name String @db.VarChar(50)
|
||||
accessCode String? @unique @map("access_code") @db.VarChar(50)
|
||||
logoUrl String? @map("logo_url") @db.VarChar(2183)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
|
||||
|
||||
teamUser TeamUser[]
|
||||
teamWebsite TeamWebsite[]
|
||||
website Website[]
|
||||
teamUser TeamUser[]
|
||||
|
||||
@@index([accessCode])
|
||||
@@map("team")
|
||||
@@ -183,20 +193,6 @@ model TeamUser {
|
||||
@@map("team_user")
|
||||
}
|
||||
|
||||
model TeamWebsite {
|
||||
id String @id() @unique() @map("team_website_id") @db.Uuid
|
||||
teamId String @map("team_id") @db.Uuid
|
||||
websiteId String @map("website_id") @db.Uuid
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
|
||||
team Team @relation(fields: [teamId], references: [id])
|
||||
website Website @relation(fields: [websiteId], references: [id])
|
||||
|
||||
@@index([teamId])
|
||||
@@index([websiteId])
|
||||
@@map("team_website")
|
||||
}
|
||||
|
||||
model Report {
|
||||
id String @id() @unique() @map("report_id") @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./src"
|
||||
}
|
||||
"baseUrl": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -36,6 +36,7 @@
|
||||
"metrics.referrers",
|
||||
"message.powered-by"
|
||||
],
|
||||
"mn-MN": ["label.max", "label.min", "label.os", "label.query", "label.url", "label.urls"],
|
||||
"nb-NO": ["label.administrator", "label.dashboard"],
|
||||
"nl-NL": [
|
||||
"label.analytics",
|
||||
|
||||
+54
-22
@@ -3,13 +3,25 @@ require('dotenv').config();
|
||||
const path = require('path');
|
||||
const pkg = require('./package.json');
|
||||
|
||||
const basePath = process.env.BASE_PATH || '';
|
||||
const forceSSL = process.env.FORCE_SSL || '';
|
||||
const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT || '';
|
||||
const defaultLocale = process.env.DEFAULT_LOCALE || '';
|
||||
const trackerScriptName = process.env.TRACKER_SCRIPT_NAME || '';
|
||||
const cloudMode = process.env.CLOUD_MODE || '';
|
||||
const cloudUrl = process.env.CLOUD_URL || '';
|
||||
const frameAncestors = process.env.ALLOWED_FRAME_URLS || '';
|
||||
const disableLogin = process.env.DISABLE_LOGIN || '';
|
||||
const disableUI = process.env.DISABLE_UI || '';
|
||||
const hostURL = process.env.HOST_URL || '';
|
||||
|
||||
const contentSecurityPolicy = [
|
||||
`default-src 'self'`,
|
||||
`img-src *`,
|
||||
`script-src 'self' 'unsafe-eval' 'unsafe-inline'`,
|
||||
`style-src 'self' 'unsafe-inline'`,
|
||||
`connect-src 'self' api.umami.is`,
|
||||
`frame-ancestors 'self' ${process.env.ALLOWED_FRAME_URLS || ''}`,
|
||||
`connect-src 'self' api.umami.is cloud.umami.is`,
|
||||
`frame-ancestors 'self' ${frameAncestors}`,
|
||||
];
|
||||
|
||||
const headers = [
|
||||
@@ -26,7 +38,7 @@ const headers = [
|
||||
},
|
||||
];
|
||||
|
||||
if (process.env.FORCE_SSL) {
|
||||
if (forceSSL) {
|
||||
headers.push({
|
||||
key: 'Strict-Transport-Security',
|
||||
value: 'max-age=63072000; includeSubDomains; preload',
|
||||
@@ -35,15 +47,15 @@ if (process.env.FORCE_SSL) {
|
||||
|
||||
const rewrites = [];
|
||||
|
||||
if (process.env.COLLECT_API_ENDPOINT) {
|
||||
if (collectApiEndpoint) {
|
||||
rewrites.push({
|
||||
source: process.env.COLLECT_API_ENDPOINT,
|
||||
source: collectApiEndpoint,
|
||||
destination: '/api/send',
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.TRACKER_SCRIPT_NAME) {
|
||||
const names = process.env.TRACKER_SCRIPT_NAME?.split(',').map(name => name.trim());
|
||||
if (trackerScriptName) {
|
||||
const names = trackerScriptName?.split(',').map(name => name.trim());
|
||||
|
||||
if (names) {
|
||||
names.forEach(name => {
|
||||
@@ -58,36 +70,56 @@ if (process.env.TRACKER_SCRIPT_NAME) {
|
||||
const redirects = [
|
||||
{
|
||||
source: '/settings',
|
||||
destination: process.env.CLOUD_MODE
|
||||
? `${process.env.CLOUD_URL}/settings/websites`
|
||||
: '/settings/websites',
|
||||
destination: '/settings/websites',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/teams/:id',
|
||||
destination: '/teams/:id/dashboard',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/teams/:id/settings',
|
||||
destination: '/teams/:id/settings/team',
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
|
||||
if (process.env.CLOUD_MODE && process.env.CLOUD_URL && process.env.DISABLE_LOGIN) {
|
||||
if (cloudMode && cloudUrl) {
|
||||
redirects.push({
|
||||
source: '/login',
|
||||
destination: process.env.CLOUD_URL,
|
||||
source: '/settings/:path*',
|
||||
destination: `${cloudUrl}/settings/:path*`,
|
||||
permanent: false,
|
||||
});
|
||||
}
|
||||
|
||||
const basePath = process.env.BASE_PATH;
|
||||
redirects.push({
|
||||
source: '/teams/:id/settings/:path*',
|
||||
destination: `${cloudUrl}/teams/:id/settings/:path*`,
|
||||
permanent: false,
|
||||
});
|
||||
|
||||
if (disableLogin) {
|
||||
redirects.push({
|
||||
source: '/login',
|
||||
destination: cloudUrl,
|
||||
permanent: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const config = {
|
||||
reactStrictMode: false,
|
||||
env: {
|
||||
basePath: basePath || '',
|
||||
cloudMode: process.env.CLOUD_MODE || '',
|
||||
cloudUrl: process.env.CLOUD_URL || '',
|
||||
basePath,
|
||||
cloudMode,
|
||||
cloudUrl,
|
||||
configUrl: '/config',
|
||||
currentVersion: pkg.version,
|
||||
defaultLocale: process.env.DEFAULT_LOCALE || '',
|
||||
disableLogin: process.env.DISABLE_LOGIN || '',
|
||||
disableUI: process.env.DISABLE_UI || '',
|
||||
hostUrl: process.env.HOST_URL || '',
|
||||
defaultLocale,
|
||||
disableLogin,
|
||||
disableUI,
|
||||
hostURL,
|
||||
},
|
||||
basePath,
|
||||
output: 'standalone',
|
||||
|
||||
+8
-8
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "umami",
|
||||
"version": "2.9.0",
|
||||
"version": "2.10.0",
|
||||
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
|
||||
"author": "Mike Cao <mike@mikecao.com>",
|
||||
"license": "MIT",
|
||||
@@ -63,11 +63,11 @@
|
||||
"dependencies": {
|
||||
"@clickhouse/client": "^0.2.2",
|
||||
"@fontsource/inter": "^4.5.15",
|
||||
"@prisma/client": "5.6.0",
|
||||
"@prisma/client": "5.7.0",
|
||||
"@prisma/extension-read-replicas": "^0.3.0",
|
||||
"@react-spring/web": "^9.7.3",
|
||||
"@tanstack/react-query": "^5.12.2",
|
||||
"@umami/prisma-client": "^0.8.0",
|
||||
"@umami/prisma-client": "^0.14.0",
|
||||
"@umami/redis-client": "^0.18.0",
|
||||
"chalk": "^4.1.1",
|
||||
"chart.js": "^4.2.1",
|
||||
@@ -92,14 +92,15 @@
|
||||
"isbot": "^3.4.5",
|
||||
"kafkajs": "^2.1.0",
|
||||
"maxmind": "^4.3.6",
|
||||
"md5": "^2.3.0",
|
||||
"moment-timezone": "^0.5.35",
|
||||
"next": "14.0.4",
|
||||
"next-basics": "^0.39.0",
|
||||
"node-fetch": "^3.2.8",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prisma": "5.6.0",
|
||||
"prisma": "5.7.0",
|
||||
"react": "^18.2.0",
|
||||
"react-basics": "^0.114.0",
|
||||
"react-basics": "^0.122.0",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^4.0.4",
|
||||
@@ -117,9 +118,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^4.2.29",
|
||||
"@netlify/plugin-nextjs": "^4.27.3",
|
||||
"@netlify/plugin-nextjs": "^4.41.3",
|
||||
"@rollup/plugin-alias": "^5.0.0",
|
||||
"@rollup/plugin-buble": "^1.0.2",
|
||||
"@rollup/plugin-commonjs": "^25.0.4",
|
||||
"@rollup/plugin-json": "^6.0.0",
|
||||
"@rollup/plugin-node-resolve": "^15.2.0",
|
||||
@@ -135,7 +135,7 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "^0.17.17",
|
||||
"eslint": "^8.33.0",
|
||||
"eslint-config-next": "^12.2.4",
|
||||
"eslint-config-next": "^14.0.4",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
|
||||
@@ -1382,7 +1382,7 @@
|
||||
"message.share-url": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Your website stats are publically available at the following URL:"
|
||||
"value": "Your website stats are publicly available at the following URL:"
|
||||
}
|
||||
],
|
||||
"message.team-already-member": [
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dotenv/config';
|
||||
import buble from '@rollup/plugin-buble';
|
||||
import replace from '@rollup/plugin-replace';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
|
||||
@@ -15,7 +14,6 @@ export default {
|
||||
delimiters: ['', ''],
|
||||
preventAssignment: true,
|
||||
}),
|
||||
buble({ objectAssign: true }),
|
||||
terser({ compress: { evaluate: false } }),
|
||||
],
|
||||
};
|
||||
@@ -27,7 +27,7 @@ export function App({ children }) {
|
||||
{children}
|
||||
<UpdateNotice user={user} config={config} />
|
||||
{process.env.NODE_ENV === 'production' && !pathname.includes('/share/') && (
|
||||
<Script src={`telemetry.js`} />
|
||||
<Script src={`${process.env.basePath}/telemetry.js`} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
padding: 0 40px;
|
||||
font-weight: 700;
|
||||
max-height: 60px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.links a,
|
||||
|
||||
+19
-14
@@ -2,35 +2,38 @@
|
||||
import { Icon, Text } from 'react-basics';
|
||||
import Link from 'next/link';
|
||||
import classNames from 'classnames';
|
||||
import Icons from 'components/icons';
|
||||
import HamburgerButton from 'components/common/HamburgerButton';
|
||||
import ThemeButton from 'components/input/ThemeButton';
|
||||
import LanguageButton from 'components/input/LanguageButton';
|
||||
import ProfileButton from 'components/input/ProfileButton';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import HamburgerButton from 'components/common/HamburgerButton';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import TeamsButton from 'components/input/TeamsButton';
|
||||
import Icons from 'components/icons';
|
||||
import { useLogin, useMessages, useNavigation, useTeamUrl } from 'components/hooks';
|
||||
import styles from './NavBar.module.css';
|
||||
|
||||
export function NavBar() {
|
||||
const pathname = usePathname();
|
||||
const { user } = useLogin();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const cloudMode = Boolean(process.env.cloudMode);
|
||||
const { pathname } = useNavigation();
|
||||
const { teamId, renderTeamUrl } = useTeamUrl();
|
||||
|
||||
const cloudMode = !!process.env.cloudMode;
|
||||
|
||||
const links = [
|
||||
{ label: formatMessage(labels.dashboard), url: '/dashboard' },
|
||||
{ label: formatMessage(labels.websites), url: '/websites' },
|
||||
{ label: formatMessage(labels.reports), url: '/reports' },
|
||||
{ label: formatMessage(labels.settings), url: '/settings' },
|
||||
{ label: formatMessage(labels.dashboard), url: renderTeamUrl('/dashboard') },
|
||||
{ label: formatMessage(labels.websites), url: renderTeamUrl('/websites') },
|
||||
{ label: formatMessage(labels.reports), url: renderTeamUrl('/reports') },
|
||||
{ label: formatMessage(labels.settings), url: renderTeamUrl('/settings') },
|
||||
].filter(n => n);
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
label: formatMessage(labels.dashboard),
|
||||
url: '/dashboard',
|
||||
url: renderTeamUrl('/dashboard'),
|
||||
},
|
||||
!cloudMode && {
|
||||
label: formatMessage(labels.settings),
|
||||
url: '/settings',
|
||||
url: renderTeamUrl('/settings'),
|
||||
children: [
|
||||
{
|
||||
label: formatMessage(labels.websites),
|
||||
@@ -46,13 +49,13 @@ export function NavBar() {
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.profile),
|
||||
url: '/settings/profile',
|
||||
url: '/profile',
|
||||
},
|
||||
],
|
||||
},
|
||||
cloudMode && {
|
||||
label: formatMessage(labels.profile),
|
||||
url: '/settings/profile',
|
||||
url: '/profile',
|
||||
},
|
||||
!cloudMode && { label: formatMessage(labels.logout), url: '/logout' },
|
||||
].filter(n => n);
|
||||
@@ -72,6 +75,7 @@ export function NavBar() {
|
||||
key={url}
|
||||
href={url}
|
||||
className={classNames({ [styles.selected]: pathname.startsWith(url) })}
|
||||
prefetch={url !== '/settings'}
|
||||
>
|
||||
<Text>{label}</Text>
|
||||
</Link>
|
||||
@@ -79,6 +83,7 @@ export function NavBar() {
|
||||
})}
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
{user?.teams?.length && <TeamsButton teamId={teamId} />}
|
||||
<ThemeButton />
|
||||
<LanguageButton />
|
||||
<ProfileButton />
|
||||
|
||||
@@ -6,7 +6,7 @@ import { setItem } from 'next-basics';
|
||||
import useStore, { checkVersion } from 'store/version';
|
||||
import { REPO_URL, VERSION_CHECK } from 'lib/constants';
|
||||
import styles from './UpdateNotice.module.css';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
export function UpdateNotice({ user, config }) {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
'use client';
|
||||
import TestConsole from './TestConsole';
|
||||
|
||||
export default function ConsolePage({ websiteId }) {
|
||||
return <TestConsole websiteId={websiteId} />;
|
||||
}
|
||||
@@ -1,7 +1,17 @@
|
||||
.test {
|
||||
.container {
|
||||
display: grid;
|
||||
gap: 30px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
border: 1px solid var(--base400);
|
||||
border-radius: 5px;
|
||||
padding: 0 20px 20px 20px;
|
||||
display: grid;
|
||||
gap: 40px;
|
||||
grid-template-columns: repeat(3, minmax(300px, 1fr));
|
||||
box-shadow: 0 0 0 10px var(--base100);
|
||||
}
|
||||
|
||||
.header {
|
||||
@@ -9,3 +19,15 @@
|
||||
font-weight: 700;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.wrapped {
|
||||
border: 1px solid var(--blue900);
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
'use client';
|
||||
import { Button } from 'react-basics';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
@@ -7,9 +6,8 @@ import WebsiteSelect from 'components/input/WebsiteSelect';
|
||||
import Page from 'components/layout/Page';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import EventsChart from 'components/metrics/EventsChart';
|
||||
import WebsiteChart from 'app/(main)/websites/[id]/WebsiteChart';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import useNavigation from 'components/hooks/useNavigation';
|
||||
import WebsiteChart from '../websites/[websiteId]/WebsiteChart';
|
||||
import { useApi, useNavigation } from 'components/hooks';
|
||||
import styles from './TestConsole.module.css';
|
||||
|
||||
export function TestConsole({ websiteId }: { websiteId: string }) {
|
||||
@@ -82,15 +80,15 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
|
||||
<WebsiteSelect websiteId={website?.id} onSelect={handleChange} />
|
||||
</PageHeader>
|
||||
{website && (
|
||||
<>
|
||||
<div className={styles.container}>
|
||||
<Script
|
||||
async
|
||||
data-website-id={websiteId}
|
||||
src={`${process.env.basePath}/script.js`}
|
||||
data-cache="true"
|
||||
/>
|
||||
<div className={styles.test}>
|
||||
<div>
|
||||
<div className={styles.actions}>
|
||||
<div className={styles.group}>
|
||||
<div className={styles.header}>Page links</div>
|
||||
<div>
|
||||
<Link href={`/console/${websiteId}/page/1/?q=abc`}>page one</Link>
|
||||
@@ -114,12 +112,11 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles.group}>
|
||||
<div className={styles.header}>Click events</div>
|
||||
<Button id="send-event-button" data-umami-event="button-click" variant="primary">
|
||||
Send event
|
||||
</Button>
|
||||
<p />
|
||||
<Button
|
||||
id="send-event-data-button"
|
||||
data-umami-event="button-click"
|
||||
@@ -129,23 +126,39 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
|
||||
>
|
||||
Send event with data
|
||||
</Button>
|
||||
<Button
|
||||
id="button-with-div-button"
|
||||
data-umami-event="button-click"
|
||||
data-umami-event-name="bob"
|
||||
data-umami-event-id="123"
|
||||
variant="primary"
|
||||
>
|
||||
<div className={styles.wrapped}>Button with div</div>
|
||||
</Button>
|
||||
<div data-umami-event="div-click" className={styles.wrapped}>
|
||||
DIV with attribute
|
||||
</div>
|
||||
<div data-umami-event="div-click-one" className={styles.wrapped}>
|
||||
<div data-umami-event="div-click-two" className={styles.wrapped}>
|
||||
<div data-umami-event="div-click-three" className={styles.wrapped}>
|
||||
Nested DIV
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles.group}>
|
||||
<div className={styles.header}>Javascript events</div>
|
||||
<Button id="manual-button" variant="primary" onClick={handleClick}>
|
||||
Run script
|
||||
</Button>
|
||||
<p />
|
||||
<Button id="manual-button" variant="primary" onClick={handleIdentifyClick}>
|
||||
Run identify
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<WebsiteChart websiteId={website.id} />
|
||||
<EventsChart websiteId={website.id} />
|
||||
</div>
|
||||
</>
|
||||
<WebsiteChart websiteId={website.id} />
|
||||
<EventsChart websiteId={website.id} />
|
||||
</div>
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
|
||||
+4
-4
@@ -1,20 +1,20 @@
|
||||
import TestConsole from '../TestConsole';
|
||||
import { Metadata } from 'next';
|
||||
import ConsolePage from '../ConsolePage';
|
||||
|
||||
async function getEnabled() {
|
||||
return !!process.env.ENABLE_TEST_CONSOLE;
|
||||
}
|
||||
|
||||
export default async function ({ params: { id } }) {
|
||||
export default async function ({ params: { websiteId } }) {
|
||||
const enabled = await getEnabled();
|
||||
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <TestConsole websiteId={id?.[0]} />;
|
||||
return <ConsolePage websiteId={websiteId?.[0]} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Test Console | umami',
|
||||
title: 'Test Console | Umami',
|
||||
};
|
||||
@@ -1,81 +0,0 @@
|
||||
'use client';
|
||||
import { Button, Icon, Icons, Loading, Text } from 'react-basics';
|
||||
import Link from 'next/link';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import Pager from 'components/common/Pager';
|
||||
import WebsiteChartList from '../../(main)/websites/[id]/WebsiteChartList';
|
||||
import DashboardSettingsButton from 'app/(main)/dashboard/DashboardSettingsButton';
|
||||
import DashboardEdit from 'app/(main)/dashboard/DashboardEdit';
|
||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import useDashboard from 'store/dashboard';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
import useFilterQuery from 'components/hooks/useFilterQuery';
|
||||
import { useUser } from 'components/hooks';
|
||||
|
||||
export function Dashboard() {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { user } = useUser();
|
||||
const { showCharts, editing } = useDashboard();
|
||||
const { dir } = useLocale();
|
||||
const { get } = useApi();
|
||||
const pageSize = 10;
|
||||
|
||||
const { query, params, setParams, result } = useFilterQuery({
|
||||
queryKey: ['dashboard:websites'],
|
||||
queryFn: (params: any) => {
|
||||
return get(`/users/${user.id}/websites`, { ...params, includeTeams: true, pageSize });
|
||||
},
|
||||
});
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setParams({ ...params, page });
|
||||
};
|
||||
|
||||
const { data, count } = result || {};
|
||||
const hasData = !!(data as any)?.length;
|
||||
const { page } = params;
|
||||
|
||||
if (query.isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={formatMessage(labels.dashboard)}>
|
||||
{!editing && hasData && <DashboardSettingsButton />}
|
||||
</PageHeader>
|
||||
{!hasData && (
|
||||
<EmptyPlaceholder message={formatMessage(messages.noWebsitesConfigured)}>
|
||||
<Link href="/settings/websites">
|
||||
<Button>
|
||||
<Icon rotate={dir === 'rtl' ? 180 : 0}>
|
||||
<Icons.ArrowRight />
|
||||
</Icon>
|
||||
<Text>{formatMessage(messages.goToSettings)}</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
</EmptyPlaceholder>
|
||||
)}
|
||||
{hasData && (
|
||||
<>
|
||||
{editing && <DashboardEdit />}
|
||||
{!editing && (
|
||||
<>
|
||||
<WebsiteChartList websites={data} showCharts={showCharts} limit={pageSize} />
|
||||
<Pager
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
count={count}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Dashboard;
|
||||
@@ -1,32 +1,30 @@
|
||||
'use client';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
|
||||
import classNames from 'classnames';
|
||||
import { Button } from 'react-basics';
|
||||
import { Button, Loading } from 'react-basics';
|
||||
import { firstBy } from 'thenby';
|
||||
import useDashboard, { saveDashboard } from 'store/dashboard';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import { useMessages, useWebsites } from 'components/hooks';
|
||||
import styles from './DashboardEdit.module.css';
|
||||
|
||||
const dragId = 'dashboard-website-ordering';
|
||||
const DRAG_ID = 'dashboard-website-ordering';
|
||||
|
||||
export function DashboardEdit() {
|
||||
export function DashboardEdit({ teamId }: { teamId: string }) {
|
||||
const settings = useDashboard();
|
||||
const { websiteOrder } = settings;
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [order, setOrder] = useState(websiteOrder || []);
|
||||
const { get, useQuery } = useApi();
|
||||
const { data: result } = useQuery({
|
||||
queryKey: ['websites'],
|
||||
queryFn: () => get('/websites', { includeTeams: 1 }),
|
||||
});
|
||||
const { data: websites } = result || {};
|
||||
const {
|
||||
result,
|
||||
query: { isLoading },
|
||||
} = useWebsites({ teamId });
|
||||
|
||||
const websites = result?.data;
|
||||
|
||||
const ordered = useMemo(() => {
|
||||
if (websites) {
|
||||
return websites
|
||||
.map(website => ({ ...website, order: order.indexOf(website.id) }))
|
||||
.map((website: { id: any }) => ({ ...website, order: order.indexOf(website.id) }))
|
||||
.sort(firstBy('order'));
|
||||
}
|
||||
return [];
|
||||
@@ -57,6 +55,10 @@ export function DashboardEdit() {
|
||||
setOrder([]);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.buttons}>
|
||||
@@ -72,7 +74,7 @@ export function DashboardEdit() {
|
||||
</div>
|
||||
<div className={styles.dragActive}>
|
||||
<DragDropContext onDragEnd={handleWebsiteDrag}>
|
||||
<Droppable droppableId={dragId}>
|
||||
<Droppable droppableId={DRAG_ID}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
@@ -80,7 +82,7 @@ export function DashboardEdit() {
|
||||
style={{ marginBottom: snapshot.isDraggingOver ? 260 : null }}
|
||||
>
|
||||
{ordered.map(({ id, name, domain }, index) => (
|
||||
<Draggable key={id} draggableId={`${dragId}-${id}`} index={index}>
|
||||
<Draggable key={id} draggableId={`${DRAG_ID}-${id}`} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
'use client';
|
||||
import { Icon, Icons, Loading, Text } from 'react-basics';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import Pager from 'components/common/Pager';
|
||||
import WebsiteChartList from '../websites/[websiteId]/WebsiteChartList';
|
||||
import DashboardSettingsButton from 'app/(main)/dashboard/DashboardSettingsButton';
|
||||
import DashboardEdit from 'app/(main)/dashboard/DashboardEdit';
|
||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||
import { useMessages, useLocale, useTeamUrl, useWebsites } from 'components/hooks';
|
||||
import useDashboard from 'store/dashboard';
|
||||
import LinkButton from 'components/common/LinkButton';
|
||||
|
||||
export function DashboardPage() {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { teamId, renderTeamUrl } = useTeamUrl();
|
||||
const { showCharts, editing } = useDashboard();
|
||||
const { dir } = useLocale();
|
||||
const pageSize = 10;
|
||||
|
||||
const { result, query, params, setParams } = useWebsites({ teamId }, { pageSize });
|
||||
const { page } = params;
|
||||
const hasData = !!result?.data?.length;
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setParams({ ...params, page });
|
||||
};
|
||||
|
||||
if (query.isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<section style={{ marginBottom: 60 }}>
|
||||
<PageHeader title={formatMessage(labels.dashboard)}>
|
||||
{!editing && hasData && <DashboardSettingsButton />}
|
||||
</PageHeader>
|
||||
{!hasData && (
|
||||
<EmptyPlaceholder message={formatMessage(messages.noWebsitesConfigured)}>
|
||||
<LinkButton href={renderTeamUrl('/settings')}>
|
||||
<Icon rotate={dir === 'rtl' ? 180 : 0}>
|
||||
<Icons.ArrowRight />
|
||||
</Icon>
|
||||
<Text>{formatMessage(messages.goToSettings)}</Text>
|
||||
</LinkButton>
|
||||
</EmptyPlaceholder>
|
||||
)}
|
||||
{hasData && (
|
||||
<>
|
||||
{editing && <DashboardEdit teamId={teamId} />}
|
||||
{!editing && (
|
||||
<>
|
||||
<WebsiteChartList
|
||||
websites={result?.data as any}
|
||||
showCharts={showCharts}
|
||||
limit={pageSize}
|
||||
/>
|
||||
<Pager
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
count={result?.count}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardPage;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TooltipPopup, Icon, Text, Flexbox, Button } from 'react-basics';
|
||||
import Icons from 'components/icons';
|
||||
import { saveDashboard } from 'store/dashboard';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useMessages } from 'components/hooks';
|
||||
|
||||
export function DashboardSettingsButton() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import Dashboard from 'app/(main)/dashboard/Dashboard';
|
||||
import DashboardPage from './DashboardPage';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function () {
|
||||
return <Dashboard />;
|
||||
return <DashboardPage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Dashboard | umami',
|
||||
title: 'Dashboard | Umami',
|
||||
};
|
||||
@@ -18,5 +18,16 @@
|
||||
min-height: 0;
|
||||
height: calc(100vh - 60px);
|
||||
overflow-y: auto;
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 1320px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
+3
-3
@@ -1,15 +1,15 @@
|
||||
import DateFilter from 'components/input/DateFilter';
|
||||
import { Button, Flexbox } from 'react-basics';
|
||||
import useDateRange from 'components/hooks/useDateRange';
|
||||
import { useDateRange, useMessages } from 'components/hooks';
|
||||
import { DEFAULT_DATE_RANGE } from 'lib/constants';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { DateRange } from 'lib/types';
|
||||
|
||||
export function DateRangeSetting() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [dateRange, setDateRange] = useDateRange();
|
||||
const { value } = dateRange;
|
||||
|
||||
const handleChange = value => setDateRange(value);
|
||||
const handleChange = (value: string | DateRange) => setDateRange(value);
|
||||
const handleReset = () => setDateRange(DEFAULT_DATE_RANGE);
|
||||
|
||||
return (
|
||||
@@ -0,0 +1,4 @@
|
||||
div.menu {
|
||||
max-height: 300px;
|
||||
width: 300px;
|
||||
}
|
||||
+17
-6
@@ -1,17 +1,26 @@
|
||||
import { useState } from 'react';
|
||||
import { Button, Dropdown, Item, Flexbox } from 'react-basics';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
import { useLocale, useMessages } from 'components/hooks';
|
||||
import { DEFAULT_LOCALE } from 'lib/constants';
|
||||
import { languages } from 'lib/lang';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import styles from './LanguageSetting.module.css';
|
||||
|
||||
export function LanguageSetting() {
|
||||
const [search, setSearch] = useState('');
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { locale, saveLocale } = useLocale();
|
||||
const options = Object.keys(languages);
|
||||
const options = search
|
||||
? Object.keys(languages).filter(n => {
|
||||
return (
|
||||
n.toLowerCase().includes(search.toLowerCase()) ||
|
||||
languages[n].label.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
})
|
||||
: Object.keys(languages);
|
||||
|
||||
const handleReset = () => saveLocale(DEFAULT_LOCALE);
|
||||
|
||||
const renderValue = value => languages[value].label;
|
||||
const renderValue = (value: string | number) => languages[value].label;
|
||||
|
||||
return (
|
||||
<Flexbox gap={10}>
|
||||
@@ -19,8 +28,10 @@ export function LanguageSetting() {
|
||||
items={options}
|
||||
value={locale}
|
||||
renderValue={renderValue}
|
||||
onChange={saveLocale}
|
||||
menuProps={{ style: { height: 300, width: 300 } }}
|
||||
onChange={val => saveLocale(val as string)}
|
||||
allowSearch={true}
|
||||
onSearch={setSearch}
|
||||
menuProps={{ className: styles.menu }}
|
||||
>
|
||||
{item => <Item key={item}>{languages[item].label}</Item>}
|
||||
</Dropdown>
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { Button, Icon, Text, useToasts, ModalTrigger, Modal } from 'react-basics';
|
||||
import PasswordEditForm from 'app/(main)/settings/profile/PasswordEditForm';
|
||||
import PasswordEditForm from 'app/(main)/profile/PasswordEditForm';
|
||||
import Icons from 'components/icons';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useMessages } from 'components/hooks';
|
||||
|
||||
export function PasswordChangeButton() {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
import { useRef } from 'react';
|
||||
import { Form, FormRow, FormInput, FormButtons, PasswordField, Button } from 'react-basics';
|
||||
import useApi from 'components/hooks/useApi';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useApi, useMessages } from 'components/hooks';
|
||||
|
||||
export function PasswordEditForm({ onSave, onClose }) {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
-1
@@ -1,4 +1,3 @@
|
||||
'use client';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import { useMessages } from 'components/hooks';
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.container {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,13 @@
|
||||
'use client';
|
||||
import ProfileHeader from './ProfileHeader';
|
||||
import ProfileSettings from './ProfileSettings';
|
||||
import { Metadata } from 'next';
|
||||
import styles from './ProfilePage.module.css';
|
||||
|
||||
export default function () {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.container}>
|
||||
<ProfileHeader />
|
||||
<ProfileSettings />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Profile Settings | umami',
|
||||
};
|
||||
+9
-11
@@ -1,18 +1,16 @@
|
||||
'use client';
|
||||
import { Form, FormRow } from 'react-basics';
|
||||
import TimezoneSetting from 'app/(main)/settings/profile/TimezoneSetting';
|
||||
import DateRangeSetting from 'app/(main)/settings/profile/DateRangeSetting';
|
||||
import LanguageSetting from 'app/(main)/settings/profile/LanguageSetting';
|
||||
import ThemeSetting from 'app/(main)/settings/profile/ThemeSetting';
|
||||
import TimezoneSetting from 'app/(main)/profile/TimezoneSetting';
|
||||
import DateRangeSetting from 'app/(main)/profile/DateRangeSetting';
|
||||
import LanguageSetting from 'app/(main)/profile/LanguageSetting';
|
||||
import ThemeSetting from 'app/(main)/profile/ThemeSetting';
|
||||
import PasswordChangeButton from './PasswordChangeButton';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useLogin, useMessages } from 'components/hooks';
|
||||
import { ROLES } from 'lib/constants';
|
||||
|
||||
export function ProfileSettings() {
|
||||
const { user } = useUser();
|
||||
const { user } = useLogin();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const cloudMode = Boolean(process.env.cloudMode);
|
||||
const cloudMode = !!process.env.cloudMode;
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
@@ -20,12 +18,12 @@ export function ProfileSettings() {
|
||||
|
||||
const { username, role } = user;
|
||||
|
||||
const renderRole = value => {
|
||||
const renderRole = (value: string) => {
|
||||
if (value === ROLES.user) {
|
||||
return formatMessage(labels.user);
|
||||
}
|
||||
if (value === ROLES.admin) {
|
||||
return formatMessage(labels.admin);
|
||||
return formatMessage(labels.administrator);
|
||||
}
|
||||
if (value === ROLES.viewOnly) {
|
||||
return formatMessage(labels.viewOnly);
|
||||
File renamed without changes.
+1
-1
@@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import { Button, Icon } from 'react-basics';
|
||||
import useTheme from 'components/hooks/useTheme';
|
||||
import { useTheme } from 'components/hooks';
|
||||
import Sun from 'assets/sun.svg';
|
||||
import Moon from 'assets/moon.svg';
|
||||
import styles from './ThemeSetting.module.css';
|
||||
@@ -0,0 +1,4 @@
|
||||
div.menu {
|
||||
max-height: 300px;
|
||||
width: 300px;
|
||||
}
|
||||
+10
-5
@@ -1,13 +1,17 @@
|
||||
import { useState } from 'react';
|
||||
import { Dropdown, Item, Button, Flexbox } from 'react-basics';
|
||||
import { listTimeZones } from 'timezone-support';
|
||||
import useTimezone from 'components/hooks/useTimezone';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { useTimezone, useMessages } from 'components/hooks';
|
||||
import { getTimezone } from 'lib/date';
|
||||
import styles from './TimezoneSetting.module.css';
|
||||
|
||||
export function TimezoneSetting() {
|
||||
const [search, setSearch] = useState('');
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [timezone, saveTimezone] = useTimezone();
|
||||
const options = listTimeZones();
|
||||
const options = search
|
||||
? listTimeZones().filter(n => n.toLowerCase().includes(search.toLowerCase()))
|
||||
: listTimeZones();
|
||||
|
||||
const handleReset = () => saveTimezone(getTimezone());
|
||||
|
||||
@@ -17,8 +21,9 @@ export function TimezoneSetting() {
|
||||
items={options}
|
||||
value={timezone}
|
||||
onChange={saveTimezone}
|
||||
style={{ flex: 1 }}
|
||||
menuProps={{ style: { height: 300 } }}
|
||||
menuProps={{ className: styles.menu }}
|
||||
allowSearch={true}
|
||||
onSearch={setSearch}
|
||||
>
|
||||
{item => <Item key={item}>{item}</Item>}
|
||||
</Dropdown>
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Metadata } from 'next';
|
||||
import ProfilePage from './ProfilePage';
|
||||
|
||||
export default function () {
|
||||
return <ProfilePage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Profile | Umami',
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
|
||||
import ConfirmDeleteForm from 'components/common/ConfirmDeleteForm';
|
||||
import { useApi, useMessages } from 'components/hooks';
|
||||
import { setValue } from 'store/cache';
|
||||
import { useApi, useMessages, useModified } from 'components/hooks';
|
||||
import ConfirmationForm from 'components/common/ConfirmationForm';
|
||||
|
||||
export function ReportDeleteButton({
|
||||
reportId,
|
||||
@@ -12,14 +11,17 @@ export function ReportDeleteButton({
|
||||
reportName: string;
|
||||
onDelete?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { formatMessage, labels, messages, FormattedMessage } = useMessages();
|
||||
const { del, useMutation } = useApi();
|
||||
const { mutate } = useMutation({ mutationFn: reportId => del(`/reports/${reportId}`) });
|
||||
const { mutate, isPending, error } = useMutation({
|
||||
mutationFn: reportId => del(`/reports/${reportId}`),
|
||||
});
|
||||
const { touch } = useModified();
|
||||
|
||||
const handleConfirm = (close: () => void) => {
|
||||
mutate(reportId as any, {
|
||||
onSuccess: () => {
|
||||
setValue('reports', Date.now());
|
||||
touch('reports');
|
||||
onDelete?.();
|
||||
close();
|
||||
},
|
||||
@@ -34,12 +36,20 @@ export function ReportDeleteButton({
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.delete)}</Text>
|
||||
</Button>
|
||||
<Modal>
|
||||
{close => (
|
||||
<ConfirmDeleteForm
|
||||
name={reportName}
|
||||
<Modal title={formatMessage(labels.deleteReport)}>
|
||||
{(close: () => void) => (
|
||||
<ConfirmationForm
|
||||
message={
|
||||
<FormattedMessage
|
||||
{...messages.confirmDelete}
|
||||
values={{ target: <b>{reportName}</b> }}
|
||||
/>
|
||||
}
|
||||
isLoading={isPending}
|
||||
error={error}
|
||||
onConfirm={handleConfirm.bind(null, close)}
|
||||
onClose={close}
|
||||
buttonLabel={formatMessage(labels.delete)}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
'use client';
|
||||
import { useReports } from 'components/hooks';
|
||||
import ReportsTable from './ReportsTable';
|
||||
import DataTable from 'components/common/DataTable';
|
||||
|
||||
export default function ReportsDataTable({ websiteId }: { websiteId?: string }) {
|
||||
const queryResult = useReports(websiteId);
|
||||
export default function ReportsDataTable({
|
||||
websiteId,
|
||||
teamId,
|
||||
}: {
|
||||
websiteId?: string;
|
||||
teamId?: string;
|
||||
}) {
|
||||
const queryResult = useReports({ websiteId, teamId });
|
||||
|
||||
return (
|
||||
<DataTable queryResult={queryResult}>
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
'use client';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import { Button, Icon, Icons, Text } from 'react-basics';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Icon, Icons, Text } from 'react-basics';
|
||||
import { useMessages, useTeamUrl } from 'components/hooks';
|
||||
import LinkButton from 'components/common/LinkButton';
|
||||
|
||||
export function ReportsHeader() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const router = useRouter();
|
||||
|
||||
const handleClick = () => router.push('/reports/create');
|
||||
const { renderTeamUrl } = useTeamUrl();
|
||||
|
||||
return (
|
||||
<PageHeader title={formatMessage(labels.reports)}>
|
||||
<Button variant="primary" onClick={handleClick}>
|
||||
<LinkButton href={renderTeamUrl('/reports/create')} variant="primary">
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.createReport)}</Text>
|
||||
</Button>
|
||||
</LinkButton>
|
||||
</PageHeader>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
'use client';
|
||||
import ReportsHeader from './ReportsHeader';
|
||||
import ReportsDataTable from './ReportsDataTable';
|
||||
|
||||
export default function ReportsPage({ teamId }: { teamId: string }) {
|
||||
return (
|
||||
<>
|
||||
<ReportsHeader />
|
||||
<ReportsDataTable teamId={teamId} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
export const metadata = {
|
||||
title: 'Reports | Umami',
|
||||
};
|
||||
@@ -1,14 +1,14 @@
|
||||
import { GridColumn, GridTable, Icon, Icons, Text, useBreakpoint } from 'react-basics';
|
||||
import LinkButton from 'components/common/LinkButton';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
import { useMessages, useLogin, useTeamUrl } from 'components/hooks';
|
||||
import { REPORT_TYPES } from 'lib/constants';
|
||||
import ReportDeleteButton from './ReportDeleteButton';
|
||||
|
||||
export function ReportsTable({ data = [], showDomain }: { data: any[]; showDomain?: boolean }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useUser();
|
||||
const { user } = useLogin();
|
||||
const breakpoint = useBreakpoint();
|
||||
const { renderTeamUrl } = useTeamUrl();
|
||||
|
||||
return (
|
||||
<GridTable data={data} cardMode={['xs', 'sm', 'md'].includes(breakpoint)}>
|
||||
@@ -34,7 +34,7 @@ export function ReportsTable({ data = [], showDomain }: { data: any[]; showDomai
|
||||
{(user.id === userId || user.id === website?.userId) && (
|
||||
<ReportDeleteButton reportId={id} reportName={name} />
|
||||
)}
|
||||
<LinkButton href={`/reports/${id}`}>
|
||||
<LinkButton href={renderTeamUrl(`/reports/${id}`)}>
|
||||
<Icon>
|
||||
<Icons.ArrowRight />
|
||||
</Icon>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import ReportDetails from './ReportDetails';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function ReportDetailsPage({ params: { id } }) {
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <ReportDetails reportId={id} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Reports | umami',
|
||||
};
|
||||
+8
-4
@@ -3,7 +3,7 @@ import { FormRow } from 'react-basics';
|
||||
import { parseDateRange } from 'lib/date';
|
||||
import DateFilter from 'components/input/DateFilter';
|
||||
import WebsiteSelect from 'components/input/WebsiteSelect';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { useMessages, useTeamUrl, useWebsite } from 'components/hooks';
|
||||
import { ReportContext } from './Report';
|
||||
|
||||
export interface BaseParametersProps {
|
||||
@@ -21,10 +21,12 @@ export function BaseParameters({
|
||||
}: BaseParametersProps) {
|
||||
const { report, updateReport } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const { teamId } = useTeamUrl();
|
||||
const { parameters } = report || {};
|
||||
const { websiteId, dateRange } = parameters || {};
|
||||
const { value, startDate, endDate } = dateRange || {};
|
||||
const { data: website } = useWebsite(websiteId);
|
||||
const { name } = website || {};
|
||||
|
||||
const handleWebsiteSelect = (websiteId: string) => {
|
||||
updateReport({ websiteId, parameters: { websiteId } });
|
||||
@@ -38,8 +40,10 @@ export function BaseParameters({
|
||||
<>
|
||||
{showWebsiteSelect && (
|
||||
<FormRow label={formatMessage(labels.website)}>
|
||||
{allowWebsiteSelect && (
|
||||
<WebsiteSelect websiteId={websiteId} onSelect={handleWebsiteSelect} />
|
||||
{allowWebsiteSelect ? (
|
||||
<WebsiteSelect teamId={teamId} websiteId={websiteId} onSelect={handleWebsiteSelect} />
|
||||
) : (
|
||||
name
|
||||
)}
|
||||
</FormRow>
|
||||
)}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -41,7 +41,7 @@ export default function FieldFilterForm({
|
||||
format(values[0]);
|
||||
}
|
||||
return formatted;
|
||||
}, [values]);
|
||||
}, [formatValue, locale, name, values]);
|
||||
|
||||
const renderFilterValue = value => {
|
||||
return filters.find(f => f.value === value)?.label;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+8
-6
@@ -1,20 +1,22 @@
|
||||
'use client';
|
||||
import { createContext, ReactNode } from 'react';
|
||||
import { Loading } from 'react-basics';
|
||||
import classNames from 'classnames';
|
||||
import { useReport } from 'components/hooks';
|
||||
import styles from './Report.module.css';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export const ReportContext = createContext(null);
|
||||
|
||||
export interface ReportProps {
|
||||
export function Report({
|
||||
reportId,
|
||||
defaultParameters,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
reportId: string;
|
||||
defaultParameters: { [key: string]: any };
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Report({ reportId, defaultParameters, children, className }: ReportProps) {
|
||||
}) {
|
||||
const report = useReport(reportId, defaultParameters);
|
||||
|
||||
if (!report) {
|
||||
File renamed without changes.
File renamed without changes.
-1
@@ -1,4 +1,3 @@
|
||||
'use client';
|
||||
import FunnelReport from '../funnel/FunnelReport';
|
||||
import EventDataReport from '../event-data/EventDataReport';
|
||||
import InsightsReport from '../insights/InsightsReport';
|
||||
File renamed without changes.
+5
-4
@@ -1,7 +1,6 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Icon, LoadingButton, InlineEditField, useToasts } from 'react-basics';
|
||||
import { useMessages, useApi } from 'components/hooks';
|
||||
import { useMessages, useApi, useNavigation, useTeamUrl } from 'components/hooks';
|
||||
import { ReportContext } from './Report';
|
||||
import styles from './ReportHeader.module.css';
|
||||
import { REPORT_TYPES } from 'lib/constants';
|
||||
@@ -10,8 +9,10 @@ export function ReportHeader({ icon }) {
|
||||
const { report, updateReport } = useContext(ReportContext);
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { showToast } = useToasts();
|
||||
const { router } = useNavigation();
|
||||
const { renderTeamUrl } = useTeamUrl();
|
||||
|
||||
const { post, useMutation } = useApi();
|
||||
const router = useRouter();
|
||||
const { mutate: create, isPending: isCreating } = useMutation({
|
||||
mutationFn: (data: any) => post(`/reports`, data),
|
||||
});
|
||||
@@ -28,7 +29,7 @@ export function ReportHeader({ icon }) {
|
||||
create(report, {
|
||||
onSuccess: async ({ id }) => {
|
||||
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||
router.push(`/reports/${id}`);
|
||||
router.push(renderTeamUrl(`/reports/${id}`));
|
||||
},
|
||||
});
|
||||
} else {
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,6 @@
|
||||
'use client';
|
||||
import ReportDetails from './ReportDetails';
|
||||
|
||||
export default function ReportPage({ reportId }) {
|
||||
return <ReportDetails reportId={reportId} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Metadata } from 'next';
|
||||
import ReportPage from './ReportPage';
|
||||
|
||||
export default function ({ params: { reportId } }) {
|
||||
return <ReportPage reportId={reportId} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Reports | Umami',
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
'use client';
|
||||
import ReportTemplates from './ReportTemplates';
|
||||
|
||||
export default function ReportCreatePage() {
|
||||
return <ReportTemplates />;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
'use client';
|
||||
import Link from 'next/link';
|
||||
import { Button, Icons, Text, Icon } from 'react-basics';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
@@ -6,7 +5,46 @@ import Funnel from 'assets/funnel.svg';
|
||||
import Lightbulb from 'assets/lightbulb.svg';
|
||||
import Magnet from 'assets/magnet.svg';
|
||||
import styles from './ReportTemplates.module.css';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { useMessages, useTeamUrl } from 'components/hooks';
|
||||
|
||||
export function ReportTemplates({ showHeader = true }: { showHeader?: boolean }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { renderTeamUrl } = useTeamUrl();
|
||||
|
||||
const reports = [
|
||||
{
|
||||
title: formatMessage(labels.insights),
|
||||
description: formatMessage(labels.insightsDescription),
|
||||
url: renderTeamUrl('/reports/insights'),
|
||||
icon: <Lightbulb />,
|
||||
},
|
||||
{
|
||||
title: formatMessage(labels.funnel),
|
||||
description: formatMessage(labels.funnelDescription),
|
||||
url: renderTeamUrl('/reports/funnel'),
|
||||
icon: <Funnel />,
|
||||
},
|
||||
{
|
||||
title: formatMessage(labels.retention),
|
||||
description: formatMessage(labels.retentionDescription),
|
||||
url: renderTeamUrl('/reports/retention'),
|
||||
icon: <Magnet />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{showHeader && <PageHeader title={formatMessage(labels.reports)} />}
|
||||
<div className={styles.reports}>
|
||||
{reports.map(({ title, description, url, icon }) => {
|
||||
return (
|
||||
<ReportItem key={title} icon={icon} title={title} description={description} url={url} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ReportItem({ title, description, url, icon }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
@@ -32,42 +70,4 @@ function ReportItem({ title, description, url, icon }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function ReportTemplates({ showHeader = true }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const reports = [
|
||||
{
|
||||
title: formatMessage(labels.insights),
|
||||
description: formatMessage(labels.insightsDescription),
|
||||
url: '/reports/insights',
|
||||
icon: <Lightbulb />,
|
||||
},
|
||||
{
|
||||
title: formatMessage(labels.funnel),
|
||||
description: formatMessage(labels.funnelDescription),
|
||||
url: '/reports/funnel',
|
||||
icon: <Funnel />,
|
||||
},
|
||||
{
|
||||
title: formatMessage(labels.retention),
|
||||
description: formatMessage(labels.retentionDescription),
|
||||
url: '/reports/retention',
|
||||
icon: <Magnet />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{showHeader && <PageHeader title={formatMessage(labels.reports)} />}
|
||||
<div className={styles.reports}>
|
||||
{reports.map(({ title, description, url, icon }) => {
|
||||
return (
|
||||
<ReportItem key={title} icon={icon} title={title} description={description} url={url} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReportTemplates;
|
||||
@@ -1,10 +1,10 @@
|
||||
import ReportTemplates from './ReportTemplates';
|
||||
import ReportCreatePage from './ReportCreatePage';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function ReportsCreatePage() {
|
||||
return <ReportTemplates />;
|
||||
export default function () {
|
||||
return <ReportCreatePage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Create Report | umami',
|
||||
title: 'Create Report | Umami',
|
||||
};
|
||||
@@ -4,10 +4,10 @@ import Empty from 'components/common/Empty';
|
||||
import Icons from 'components/icons';
|
||||
import { useApi, useMessages } from 'components/hooks';
|
||||
import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import FieldAddForm from '../[id]/FieldAddForm';
|
||||
import ParameterList from '../[id]/ParameterList';
|
||||
import BaseParameters from '../[id]/BaseParameters';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import FieldAddForm from '../[reportId]/FieldAddForm';
|
||||
import ParameterList from '../[reportId]/ParameterList';
|
||||
import BaseParameters from '../[reportId]/BaseParameters';
|
||||
import styles from './EventDataParameters.module.css';
|
||||
|
||||
function useFields(websiteId, startDate, endDate) {
|
||||
@@ -29,7 +29,7 @@ function useFields(websiteId, startDate, endDate) {
|
||||
export function EventDataParameters() {
|
||||
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { parameters } = report || {};
|
||||
const { id, parameters } = report || {};
|
||||
const { websiteId, dateRange, fields, filters, groups } = parameters || {};
|
||||
const { startDate, endDate } = dateRange || {};
|
||||
const queryEnabled = websiteId && dateRange && fields?.length;
|
||||
@@ -93,7 +93,7 @@ export function EventDataParameters() {
|
||||
|
||||
return (
|
||||
<Form values={parameters} error={error} onSubmit={handleSubmit}>
|
||||
<BaseParameters />
|
||||
<BaseParameters allowWebsiteSelect={!id} />
|
||||
{!hasData && <Empty message={formatMessage(messages.noEventData)} />}
|
||||
{parametersSelected &&
|
||||
hasData &&
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Report from '../[id]/Report';
|
||||
import ReportHeader from '../[id]/ReportHeader';
|
||||
import ReportMenu from '../[id]/ReportMenu';
|
||||
import ReportBody from '../[id]/ReportBody';
|
||||
import Report from '../[reportId]/Report';
|
||||
import ReportHeader from '../[reportId]/ReportHeader';
|
||||
import ReportMenu from '../[reportId]/ReportMenu';
|
||||
import ReportBody from '../[reportId]/ReportBody';
|
||||
import EventDataParameters from './EventDataParameters';
|
||||
import EventDataTable from './EventDataTable';
|
||||
import Nodes from 'assets/nodes.svg';
|
||||
@@ -11,7 +11,7 @@ const defaultParameters = {
|
||||
parameters: { fields: [], filters: [] },
|
||||
};
|
||||
|
||||
export default function EventDataReport({ reportId }: { reportId: string }) {
|
||||
export default function EventDataReport({ reportId }: { reportId?: string }) {
|
||||
return (
|
||||
<Report reportId={reportId} defaultParameters={defaultParameters}>
|
||||
<ReportHeader icon={<Nodes />} />
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
'use client';
|
||||
import EventDataReport from './EventDataReport';
|
||||
|
||||
export default function EventDataReportPage() {
|
||||
return <EventDataReport />;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useContext } from 'react';
|
||||
import { GridTable, GridColumn } from 'react-basics';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
|
||||
export function EventDataTable() {
|
||||
const { report } = useContext(ReportContext);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Metadata } from 'next';
|
||||
import EventDataReportPage from './EventDataReportPage';
|
||||
|
||||
export default function () {
|
||||
return <EventDataReportPage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Event Data Report | Umami',
|
||||
};
|
||||
@@ -1,10 +1,9 @@
|
||||
import { JSX, useCallback, useContext, useMemo } from 'react';
|
||||
import { Loading, StatusLight } from 'react-basics';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useTheme from 'components/hooks/useTheme';
|
||||
import { useMessages, useTheme } from 'components/hooks';
|
||||
import BarChart from 'components/metrics/BarChart';
|
||||
import { formatLongNumber } from 'lib/format';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import styles from './FunnelChart.module.css';
|
||||
|
||||
export interface FunnelChartProps {
|
||||
|
||||
@@ -13,16 +13,16 @@ import {
|
||||
} from 'react-basics';
|
||||
import Icons from 'components/icons';
|
||||
import UrlAddForm from './UrlAddForm';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import BaseParameters from '../[id]/BaseParameters';
|
||||
import ParameterList from '../[id]/ParameterList';
|
||||
import PopupForm from '../[id]/PopupForm';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import BaseParameters from '../[reportId]/BaseParameters';
|
||||
import ParameterList from '../[reportId]/ParameterList';
|
||||
import PopupForm from '../[reportId]/PopupForm';
|
||||
|
||||
export function FunnelParameters() {
|
||||
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const { parameters } = report || {};
|
||||
const { id, parameters } = report || {};
|
||||
const { websiteId, dateRange, urls } = parameters || {};
|
||||
const queryDisabled = !websiteId || !dateRange || urls?.length < 2;
|
||||
|
||||
@@ -62,7 +62,7 @@ export function FunnelParameters() {
|
||||
|
||||
return (
|
||||
<Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
|
||||
<BaseParameters />
|
||||
<BaseParameters allowWebsiteSelect={!id} />
|
||||
<FormRow label={formatMessage(labels.window)}>
|
||||
<FormInput
|
||||
name="window"
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
'use client';
|
||||
import FunnelChart from './FunnelChart';
|
||||
import FunnelTable from './FunnelTable';
|
||||
import FunnelParameters from './FunnelParameters';
|
||||
import Report from '../[id]/Report';
|
||||
import ReportHeader from '../[id]/ReportHeader';
|
||||
import ReportMenu from '../[id]/ReportMenu';
|
||||
import ReportBody from '../[id]/ReportBody';
|
||||
import Report from '../[reportId]/Report';
|
||||
import ReportHeader from '../[reportId]/ReportHeader';
|
||||
import ReportMenu from '../[reportId]/ReportMenu';
|
||||
import ReportBody from '../[reportId]/ReportBody';
|
||||
import Funnel from 'assets/funnel.svg';
|
||||
import { REPORT_TYPES } from 'lib/constants';
|
||||
|
||||
@@ -14,7 +13,7 @@ const defaultParameters = {
|
||||
parameters: { window: 60, urls: [] },
|
||||
};
|
||||
|
||||
export default function FunnelReport({ reportId }) {
|
||||
export default function FunnelReport({ reportId }: { reportId?: string }) {
|
||||
return (
|
||||
<Report reportId={reportId} defaultParameters={defaultParameters}>
|
||||
<ReportHeader icon={<Funnel />} />
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
'use client';
|
||||
import FunnelReport from './FunnelReport';
|
||||
|
||||
export default function FunnelReportPage() {
|
||||
return <FunnelReport />;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useContext } from 'react';
|
||||
import ListTable from 'components/metrics/ListTable';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
|
||||
export function FunnelTable() {
|
||||
const { report } = useContext(ReportContext);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import FunnelReport from './FunnelReport';
|
||||
import FunnelReportPage from './FunnelReportPage';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function FunnelReportPage() {
|
||||
return <FunnelReport reportId={null} />;
|
||||
export default function () {
|
||||
return <FunnelReportPage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Funnel Report | umami',
|
||||
title: 'Funnel Report | Umami',
|
||||
};
|
||||
@@ -1,22 +1,22 @@
|
||||
import { useFilters, useFormat, useMessages } from 'components/hooks';
|
||||
import Icons from 'components/icons';
|
||||
import { useContext } from 'react';
|
||||
import { useFormat, useMessages, useFilters } from 'components/hooks';
|
||||
import {
|
||||
Form,
|
||||
FormRow,
|
||||
FormButtons,
|
||||
SubmitButton,
|
||||
PopupTrigger,
|
||||
FormRow,
|
||||
Icon,
|
||||
Popup,
|
||||
PopupTrigger,
|
||||
SubmitButton,
|
||||
TooltipPopup,
|
||||
} from 'react-basics';
|
||||
import Icons from 'components/icons';
|
||||
import BaseParameters from '../[id]/BaseParameters';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import ParameterList from '../[id]/ParameterList';
|
||||
import FilterSelectForm from '../[id]/FilterSelectForm';
|
||||
import FieldSelectForm from '../[id]/FieldSelectForm';
|
||||
import PopupForm from '../[id]/PopupForm';
|
||||
import BaseParameters from '../[reportId]/BaseParameters';
|
||||
import FieldSelectForm from '../[reportId]/FieldSelectForm';
|
||||
import FilterSelectForm from '../[reportId]/FilterSelectForm';
|
||||
import ParameterList from '../[reportId]/ParameterList';
|
||||
import PopupForm from '../[reportId]/PopupForm';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import styles from './InsightsParameters.module.css';
|
||||
|
||||
export function InsightsParameters() {
|
||||
@@ -24,7 +24,7 @@ export function InsightsParameters() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { formatValue } = useFormat();
|
||||
const { filterLabels } = useFilters();
|
||||
const { parameters } = report || {};
|
||||
const { id, parameters } = report || {};
|
||||
const { websiteId, dateRange, fields, filters } = parameters || {};
|
||||
const { startDate, endDate } = dateRange || {};
|
||||
const parametersSelected = websiteId && startDate && endDate;
|
||||
@@ -103,7 +103,7 @@ export function InsightsParameters() {
|
||||
|
||||
return (
|
||||
<Form values={parameters} onSubmit={handleSubmit}>
|
||||
<BaseParameters />
|
||||
<BaseParameters allowWebsiteSelect={!id} />
|
||||
{parametersSelected &&
|
||||
parameterGroups.map(({ id, label }) => {
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
'use client';
|
||||
import Report from '../[id]/Report';
|
||||
import ReportHeader from '../[id]/ReportHeader';
|
||||
import ReportMenu from '../[id]/ReportMenu';
|
||||
import ReportBody from '../[id]/ReportBody';
|
||||
import Report from '../[reportId]/Report';
|
||||
import ReportHeader from '../[reportId]/ReportHeader';
|
||||
import ReportMenu from '../[reportId]/ReportMenu';
|
||||
import ReportBody from '../[reportId]/ReportBody';
|
||||
import InsightsParameters from './InsightsParameters';
|
||||
import InsightsTable from './InsightsTable';
|
||||
import Lightbulb from 'assets/lightbulb.svg';
|
||||
@@ -13,7 +12,7 @@ const defaultParameters = {
|
||||
parameters: { fields: [], filters: [] },
|
||||
};
|
||||
|
||||
export default function InsightsReport({ reportId }: { reportId: string }) {
|
||||
export default function InsightsReport({ reportId }: { reportId?: string }) {
|
||||
return (
|
||||
<Report reportId={reportId} defaultParameters={defaultParameters}>
|
||||
<ReportHeader icon={<Lightbulb />} />
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
'use client';
|
||||
import InsightsReport from './InsightsReport';
|
||||
|
||||
export default function InsightsReportPage() {
|
||||
return <InsightsReport />;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { GridTable, GridColumn } from 'react-basics';
|
||||
import { useFormat, useMessages } from 'components/hooks';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||
|
||||
export function InsightsTable() {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import InsightsReport from './InsightsReport';
|
||||
import InsightsReportPage from './InsightsReportPage';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function InsightsReportPage() {
|
||||
return <InsightsReport reportId={null} />;
|
||||
export default function () {
|
||||
return <InsightsReportPage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Insights Report | umami',
|
||||
title: 'Insights Report | Umami',
|
||||
};
|
||||
@@ -1,14 +1,10 @@
|
||||
import ReportsHeader from './ReportsHeader';
|
||||
import ReportsDataTable from './ReportsDataTable';
|
||||
import ReportsPage from './ReportsPage';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function () {
|
||||
return (
|
||||
<>
|
||||
<ReportsHeader />
|
||||
<ReportsDataTable />
|
||||
</>
|
||||
);
|
||||
export default function ({ params: { teamId } }: { params: { teamId: string } }) {
|
||||
return <ReportsPage teamId={teamId} />;
|
||||
}
|
||||
export const metadata = {
|
||||
title: 'Reports | umami',
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Reports | Umami',
|
||||
};
|
||||
@@ -1,16 +1,16 @@
|
||||
import { useContext } from 'react';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { Form, FormButtons, FormRow, SubmitButton } from 'react-basics';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import { MonthSelect } from 'components/input/MonthSelect';
|
||||
import BaseParameters from '../[id]/BaseParameters';
|
||||
import BaseParameters from '../[reportId]/BaseParameters';
|
||||
import { parseDateRange } from 'lib/date';
|
||||
|
||||
export function RetentionParameters() {
|
||||
const { report, runReport, isRunning, updateReport } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const { parameters } = report || {};
|
||||
const { id, parameters } = report || {};
|
||||
const { websiteId, dateRange } = parameters || {};
|
||||
const { startDate } = dateRange || {};
|
||||
const queryDisabled = !websiteId || !dateRange;
|
||||
@@ -30,7 +30,7 @@ export function RetentionParameters() {
|
||||
|
||||
return (
|
||||
<Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
|
||||
<BaseParameters showDateSelect={false} />
|
||||
<BaseParameters showDateSelect={false} allowWebsiteSelect={!id} />
|
||||
<FormRow label={formatMessage(labels.date)}>
|
||||
<MonthSelect date={startDate} onChange={handleDateChange} />
|
||||
</FormRow>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
'use client';
|
||||
import RetentionTable from './RetentionTable';
|
||||
import RetentionParameters from './RetentionParameters';
|
||||
import Report from '../[id]/Report';
|
||||
import ReportHeader from '../[id]/ReportHeader';
|
||||
import ReportMenu from '../[id]/ReportMenu';
|
||||
import ReportBody from '../[id]/ReportBody';
|
||||
import Report from '../[reportId]/Report';
|
||||
import ReportHeader from '../[reportId]/ReportHeader';
|
||||
import ReportMenu from '../[reportId]/ReportMenu';
|
||||
import ReportBody from '../[reportId]/ReportBody';
|
||||
import Magnet from 'assets/magnet.svg';
|
||||
import { REPORT_TYPES } from 'lib/constants';
|
||||
import { parseDateRange } from 'lib/date';
|
||||
@@ -19,7 +18,7 @@ const defaultParameters = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function RetentionReport({ reportId }: { reportId: string }) {
|
||||
export default function RetentionReport({ reportId }: { reportId?: string }) {
|
||||
return (
|
||||
<Report reportId={reportId} defaultParameters={defaultParameters}>
|
||||
<ReportHeader icon={<Magnet />} />
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
'use client';
|
||||
import RetentionReport from './RetentionReport';
|
||||
|
||||
export default function RetentionReportPage() {
|
||||
return <RetentionReport />;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useContext } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ReportContext } from '../[id]/Report';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||
import { useMessages, useLocale } from 'components/hooks';
|
||||
import { formatDate } from 'lib/date';
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import RetentionReport from './RetentionReport';
|
||||
|
||||
export default function RetentionReportPage() {
|
||||
return <RetentionReport reportId={null} />;
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: 'Create Report | umami',
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Metadata } from 'next';
|
||||
import RetentionReportPage from './RetentionReportPage';
|
||||
|
||||
export default function () {
|
||||
return <RetentionReportPage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Create Report | Umami',
|
||||
};
|
||||
Loaded 100 of 476 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user