diff --git a/db/clickhouse/migrations/02_add_visit_id.sql b/db/clickhouse/migrations/02_add_visit_id.sql new file mode 100644 index 000000000..202c0fd30 --- /dev/null +++ b/db/clickhouse/migrations/02_add_visit_id.sql @@ -0,0 +1,90 @@ +CREATE TABLE umami.website_event_join +( + session_id UUID, + visit_id UUID, + created_at DateTime('UTC') +) + engine = MergeTree + ORDER BY (session_id, created_at) + SETTINGS index_granularity = 8192; + +INSERT INTO umami.website_event_join +SELECT DISTINCT + s.session_id, + generateUUIDv4() visit_id, + s.created_at +FROM (SELECT DISTINCT session_id, + date_trunc('hour', created_at) created_at + FROM website_event) s; + +-- create new table +CREATE TABLE umami.website_event_new +( + website_id UUID, + session_id UUID, + visit_id UUID, + event_id UUID, + 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, + url_path String, + url_query String, + referrer_path String, + referrer_query String, + referrer_domain String, + page_title String, + event_type UInt32, + event_name String, + created_at DateTime('UTC'), + job_id UUID +) + engine = MergeTree + ORDER BY (website_id, session_id, created_at) + SETTINGS index_granularity = 8192; + +INSERT INTO umami.website_event_new +SELECT we.website_id, + we.session_id, + j.visit_id, + we.event_id, + we.hostname, + we.browser, + we.os, + we.device, + we.screen, + we.language, + we.country, + we.subdivision1, + we.subdivision2, + we.city, + we.url_path, + we.url_query, + we.referrer_path, + we.referrer_query, + we.referrer_domain, + we.page_title, + we.event_type, + we.event_name, + we.created_at, + we.job_id +FROM umami.website_event we +JOIN umami.website_event_join j + ON we.session_id = j.session_id + and date_trunc('hour', we.created_at) = j.created_at + +RENAME TABLE umami.website_event TO umami.website_event_old; +RENAME TABLE umami.website_event_new TO umami.website_event; + +/* + + DROP TABLE umami.website_event_old + DROP TABLE umami.website_event_join + + */ \ No newline at end of file diff --git a/db/clickhouse/schema.sql b/db/clickhouse/schema.sql index 741f06ad2..dad4f4af3 100644 --- a/db/clickhouse/schema.sql +++ b/db/clickhouse/schema.sql @@ -3,6 +3,7 @@ CREATE TABLE umami.website_event ( website_id UUID, session_id UUID, + visit_id UUID, event_id UUID, --sessions hostname LowCardinality(String), diff --git a/db/mysql/migrations/05_add_visit_id/migration.sql b/db/mysql/migrations/05_add_visit_id/migration.sql new file mode 100644 index 000000000..7a833a885 --- /dev/null +++ b/db/mysql/migrations/05_add_visit_id/migration.sql @@ -0,0 +1,22 @@ +-- AlterTable +ALTER TABLE `website_event` ADD COLUMN `visit_id` VARCHAR(36) NULL; + +UPDATE `website_event` we +JOIN (SELECT DISTINCT + s.session_id, + s.visit_time, + BIN_TO_UUID(RANDOM_BYTES(16) & 0xffffffffffff0fff3fffffffffffffff | 0x00000000000040008000000000000000) uuid + FROM (SELECT DISTINCT session_id, + DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00') visit_time + FROM `website_event`) s) a + ON we.session_id = a.session_id and DATE_FORMAT(we.created_at, '%Y-%m-%d %H:00:00') = a.visit_time +SET we.visit_id = a.uuid +WHERE we.visit_id IS NULL; + +ALTER TABLE `website_event` MODIFY `visit_id` VARCHAR(36) NOT NULL; + +-- CreateIndex +CREATE INDEX `website_event_visit_id_idx` ON `website_event`(`visit_id`); + +-- CreateIndex +CREATE INDEX `website_event_website_id_visit_id_created_at_idx` ON `website_event`(`website_id`, `visit_id`, `created_at`); diff --git a/db/mysql/schema.prisma b/db/mysql/schema.prisma index 8e5cbbc33..152ca265b 100644 --- a/db/mysql/schema.prisma +++ b/db/mysql/schema.prisma @@ -92,6 +92,7 @@ model WebsiteEvent { id String @id() @map("event_id") @db.VarChar(36) websiteId String @map("website_id") @db.VarChar(36) sessionId String @map("session_id") @db.VarChar(36) + visitId String @map("visit_id") @db.VarChar(36) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) urlPath String @map("url_path") @db.VarChar(500) urlQuery String? @map("url_query") @db.VarChar(500) @@ -107,6 +108,7 @@ model WebsiteEvent { @@index([createdAt]) @@index([sessionId]) + @@index([visitId]) @@index([websiteId]) @@index([websiteId, createdAt]) @@index([websiteId, createdAt, urlPath]) @@ -115,6 +117,7 @@ model WebsiteEvent { @@index([websiteId, createdAt, pageTitle]) @@index([websiteId, createdAt, eventName]) @@index([websiteId, sessionId, createdAt]) + @@index([websiteId, visitId, createdAt]) @@map("website_event") } diff --git a/db/postgresql/migrations/05_add_visit_id/migration.sql b/db/postgresql/migrations/05_add_visit_id/migration.sql new file mode 100644 index 000000000..fd2f1b905 --- /dev/null +++ b/db/postgresql/migrations/05_add_visit_id/migration.sql @@ -0,0 +1,22 @@ +-- AlterTable +ALTER TABLE "website_event" ADD COLUMN "visit_id" UUID NULL; + +UPDATE "website_event" we +SET visit_id = a.uuid +FROM (SELECT DISTINCT + s.session_id, + s.visit_time, + gen_random_uuid() uuid + FROM (SELECT DISTINCT session_id, + date_trunc('hour', created_at) visit_time + FROM "website_event") s) a +WHERE we.session_id = a.session_id + and date_trunc('hour', we.created_at) = a.visit_time; + +ALTER TABLE "website_event" ALTER COLUMN "visit_id" SET NOT NULL; + +-- CreateIndex +CREATE INDEX "website_event_visit_id_idx" ON "website_event"("visit_id"); + +-- CreateIndex +CREATE INDEX "website_event_website_id_visit_id_created_at_idx" ON "website_event"("website_id", "visit_id", "created_at"); \ No newline at end of file diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index 31cc7616d..0cb8ae8a2 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -92,6 +92,7 @@ model WebsiteEvent { id String @id() @map("event_id") @db.Uuid websiteId String @map("website_id") @db.Uuid sessionId String @map("session_id") @db.Uuid + visitId String @map("visit_id") @db.Uuid createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) urlPath String @map("url_path") @db.VarChar(500) urlQuery String? @map("url_query") @db.VarChar(500) @@ -107,6 +108,7 @@ model WebsiteEvent { @@index([createdAt]) @@index([sessionId]) + @@index([visitId]) @@index([websiteId]) @@index([websiteId, createdAt]) @@index([websiteId, createdAt, urlPath]) @@ -115,6 +117,7 @@ model WebsiteEvent { @@index([websiteId, createdAt, pageTitle]) @@index([websiteId, createdAt, eventName]) @@index([websiteId, sessionId, createdAt]) + @@index([websiteId, visitId, createdAt]) @@map("website_event") } diff --git a/next.config.js b/next.config.js index dce49100e..f8850c60c 100644 --- a/next.config.js +++ b/next.config.js @@ -14,6 +14,7 @@ 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 privateMode = process.env.PRIVATE_MODE || ''; const contentSecurityPolicy = [ `default-src 'self'`, @@ -120,6 +121,7 @@ const config = { disableLogin, disableUI, hostURL, + privateMode, }, basePath, output: 'standalone', diff --git a/package.json b/package.json index 03f0f79aa..bd27a4cfe 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "dependencies": { "@clickhouse/client": "^0.2.2", "@fontsource/inter": "^4.5.15", - "@prisma/client": "5.10.2", + "@prisma/client": "5.11.0", "@prisma/extension-read-replicas": "^0.3.0", "@react-spring/web": "^9.7.3", "@tanstack/react-query": "^5.28.6", @@ -98,11 +98,11 @@ "maxmind": "^4.3.6", "md5": "^2.3.0", "moment-timezone": "^0.5.35", - "next": "14.1.3", + "next": "14.1.4", "next-basics": "^0.39.0", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", - "prisma": "5.10.2", + "prisma": "5.11.0", "react": "^18.2.0", "react-basics": "^0.123.0", "react-beautiful-dnd": "^13.1.0", @@ -115,7 +115,6 @@ "request-ip": "^3.3.0", "semver": "^7.5.4", "thenby": "^1.3.4", - "timezone-support": "^2.0.2", "uuid": "^9.0.0", "yup": "^0.32.11", "zustand": "^4.3.8" diff --git a/public/intl/messages/am-ET.json b/public/intl/messages/am-ET.json index 21a8e79ff..e17f35c77 100644 --- a/public/intl/messages/am-ET.json +++ b/public/intl/messages/am-ET.json @@ -41,7 +41,7 @@ "value": "Add website" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Administrator" diff --git a/public/intl/messages/ar-SA.json b/public/intl/messages/ar-SA.json index 08b7e51f2..42dea85be 100644 --- a/public/intl/messages/ar-SA.json +++ b/public/intl/messages/ar-SA.json @@ -41,7 +41,7 @@ "value": "إضافة موقع" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "مدير" diff --git a/public/intl/messages/be-BY.json b/public/intl/messages/be-BY.json index def58386c..4fd681e17 100644 --- a/public/intl/messages/be-BY.json +++ b/public/intl/messages/be-BY.json @@ -41,7 +41,7 @@ "value": "Дадаць сайт" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Адміністратар" diff --git a/public/intl/messages/bn-BD.json b/public/intl/messages/bn-BD.json index 0f32ba3bc..301ce73b1 100644 --- a/public/intl/messages/bn-BD.json +++ b/public/intl/messages/bn-BD.json @@ -41,7 +41,7 @@ "value": "ওয়েবসাইট যুক্ত করুন" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "অ্যাডমিন" diff --git a/public/intl/messages/ca-ES.json b/public/intl/messages/ca-ES.json index 0f441e0c9..4de900c3a 100644 --- a/public/intl/messages/ca-ES.json +++ b/public/intl/messages/ca-ES.json @@ -41,7 +41,7 @@ "value": "Afegeix lloc web" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Administrador" diff --git a/public/intl/messages/cs-CZ.json b/public/intl/messages/cs-CZ.json index 1b91d2472..9037654bf 100644 --- a/public/intl/messages/cs-CZ.json +++ b/public/intl/messages/cs-CZ.json @@ -41,7 +41,7 @@ "value": "Přidat web" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Administrátor" diff --git a/public/intl/messages/da-DK.json b/public/intl/messages/da-DK.json index a53d93a3f..ecc26e878 100644 --- a/public/intl/messages/da-DK.json +++ b/public/intl/messages/da-DK.json @@ -41,7 +41,7 @@ "value": "Tilføj hjemmeside" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Administrator" diff --git a/public/intl/messages/de-CH.json b/public/intl/messages/de-CH.json index e7abed6bd..b908b4b28 100644 --- a/public/intl/messages/de-CH.json +++ b/public/intl/messages/de-CH.json @@ -41,7 +41,7 @@ "value": "Websiite hinzuefüege" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Administrator" diff --git a/public/intl/messages/de-DE.json b/public/intl/messages/de-DE.json index 2477f0e64..0dc62bb61 100644 --- a/public/intl/messages/de-DE.json +++ b/public/intl/messages/de-DE.json @@ -41,7 +41,7 @@ "value": "Website hinzufügen" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Administrator" diff --git a/public/intl/messages/el-GR.json b/public/intl/messages/el-GR.json index dd78c9714..3ececb2c1 100644 --- a/public/intl/messages/el-GR.json +++ b/public/intl/messages/el-GR.json @@ -41,7 +41,7 @@ "value": "Προσθήκη ιστότοπου" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Διαχειριστής" diff --git a/public/intl/messages/en-GB.json b/public/intl/messages/en-GB.json index e035770eb..5f37a5400 100644 --- a/public/intl/messages/en-GB.json +++ b/public/intl/messages/en-GB.json @@ -41,7 +41,7 @@ "value": "Add website" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Administrator" diff --git a/public/intl/messages/en-US.json b/public/intl/messages/en-US.json index 68a3e1f73..8946c90eb 100644 --- a/public/intl/messages/en-US.json +++ b/public/intl/messages/en-US.json @@ -41,7 +41,7 @@ "value": "Add website" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Administrator" diff --git a/public/intl/messages/es-ES.json b/public/intl/messages/es-ES.json index b1ce8d8a8..9b0ba5bf4 100644 --- a/public/intl/messages/es-ES.json +++ b/public/intl/messages/es-ES.json @@ -41,7 +41,7 @@ "value": "Nuevo sitio web" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Administrador" diff --git a/public/intl/messages/fa-IR.json b/public/intl/messages/fa-IR.json index a680492cc..9168ff1a9 100644 --- a/public/intl/messages/fa-IR.json +++ b/public/intl/messages/fa-IR.json @@ -41,7 +41,7 @@ "value": "افزودن وب‌سایت" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "مدیر" diff --git a/public/intl/messages/fi-FI.json b/public/intl/messages/fi-FI.json index 5c9b76111..16cf8db66 100644 --- a/public/intl/messages/fi-FI.json +++ b/public/intl/messages/fi-FI.json @@ -41,7 +41,7 @@ "value": "Lisää verkkosivu" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Järjestelmänvalvoja" diff --git a/public/intl/messages/fo-FO.json b/public/intl/messages/fo-FO.json index 29d1eb5fb..b05faf6cf 100644 --- a/public/intl/messages/fo-FO.json +++ b/public/intl/messages/fo-FO.json @@ -41,7 +41,7 @@ "value": "Legg heimasíðu afturat" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Fyrisitari" diff --git a/public/intl/messages/fr-FR.json b/public/intl/messages/fr-FR.json index c8ba44c3b..c09732497 100644 --- a/public/intl/messages/fr-FR.json +++ b/public/intl/messages/fr-FR.json @@ -41,7 +41,7 @@ "value": "Ajouter un site" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Administrateur" diff --git a/public/intl/messages/ga-ES.json b/public/intl/messages/ga-ES.json index 926294eab..9e8252288 100644 --- a/public/intl/messages/ga-ES.json +++ b/public/intl/messages/ga-ES.json @@ -41,7 +41,7 @@ "value": "Engadir sitio web" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Administradora" diff --git a/public/intl/messages/he-IL.json b/public/intl/messages/he-IL.json index b3cdfeebb..596922931 100644 --- a/public/intl/messages/he-IL.json +++ b/public/intl/messages/he-IL.json @@ -41,7 +41,7 @@ "value": "הוספת אתר" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "מנהל" diff --git a/public/intl/messages/hi-IN.json b/public/intl/messages/hi-IN.json index 985ed5371..ef4deb6f9 100644 --- a/public/intl/messages/hi-IN.json +++ b/public/intl/messages/hi-IN.json @@ -41,7 +41,7 @@ "value": "वेबसाइट" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "प्रशासक" diff --git a/public/intl/messages/hr-HR.json b/public/intl/messages/hr-HR.json index ff3ca36c2..7259dd029 100644 --- a/public/intl/messages/hr-HR.json +++ b/public/intl/messages/hr-HR.json @@ -41,7 +41,7 @@ "value": "Dodaj web stranicu" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Administrator" diff --git a/public/intl/messages/hu-HU.json b/public/intl/messages/hu-HU.json index 28450892c..f6342e3c1 100644 --- a/public/intl/messages/hu-HU.json +++ b/public/intl/messages/hu-HU.json @@ -41,7 +41,7 @@ "value": "Weboldal hozzáadása" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Adminisztrátor" diff --git a/public/intl/messages/id-ID.json b/public/intl/messages/id-ID.json index d2b5a98cf..13385bd8f 100644 --- a/public/intl/messages/id-ID.json +++ b/public/intl/messages/id-ID.json @@ -41,7 +41,7 @@ "value": "Tambah situs web" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Pengelola" diff --git a/public/intl/messages/it-IT.json b/public/intl/messages/it-IT.json index 10942ffff..4008ef049 100644 --- a/public/intl/messages/it-IT.json +++ b/public/intl/messages/it-IT.json @@ -41,7 +41,7 @@ "value": "Aggiungi sito" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Amministratore" diff --git a/public/intl/messages/ja-JP.json b/public/intl/messages/ja-JP.json index 06ef769c1..e1882cd3f 100644 --- a/public/intl/messages/ja-JP.json +++ b/public/intl/messages/ja-JP.json @@ -41,7 +41,7 @@ "value": "Webサイトの追加" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "管理者" diff --git a/public/intl/messages/km-KH.json b/public/intl/messages/km-KH.json index 40a280f99..712bed110 100644 --- a/public/intl/messages/km-KH.json +++ b/public/intl/messages/km-KH.json @@ -41,7 +41,7 @@ "value": "បន្ថែមគេហទំព័រ" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "អ្នកគ្រប់គ្រង" diff --git a/public/intl/messages/ko-KR.json b/public/intl/messages/ko-KR.json index b95b0e320..4d19a3a26 100644 --- a/public/intl/messages/ko-KR.json +++ b/public/intl/messages/ko-KR.json @@ -41,7 +41,7 @@ "value": "웹사이트 추가" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "관리자" diff --git a/public/intl/messages/lt-LT.json b/public/intl/messages/lt-LT.json index 0d3817dad..e7d468387 100644 --- a/public/intl/messages/lt-LT.json +++ b/public/intl/messages/lt-LT.json @@ -41,7 +41,7 @@ "value": "Pridėti svetainę" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Administratorius" diff --git a/public/intl/messages/mn-MN.json b/public/intl/messages/mn-MN.json index 2c8a01e7d..509d8b5bb 100644 --- a/public/intl/messages/mn-MN.json +++ b/public/intl/messages/mn-MN.json @@ -41,7 +41,7 @@ "value": "Веб нэмэх" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Админ" diff --git a/public/intl/messages/ms-MY.json b/public/intl/messages/ms-MY.json index 4039bea13..997a09511 100644 --- a/public/intl/messages/ms-MY.json +++ b/public/intl/messages/ms-MY.json @@ -41,7 +41,7 @@ "value": "Tambah laman web" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Pentadbir" diff --git a/public/intl/messages/my-MM.json b/public/intl/messages/my-MM.json index 729dec130..9a586ac2f 100644 --- a/public/intl/messages/my-MM.json +++ b/public/intl/messages/my-MM.json @@ -41,7 +41,7 @@ "value": "ဝက်ဘ်ဆိုဒ်ထည့်မည်" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "အက်ဒမင်" diff --git a/public/intl/messages/nb-NO.json b/public/intl/messages/nb-NO.json index fadc11f81..b2b8715d4 100644 --- a/public/intl/messages/nb-NO.json +++ b/public/intl/messages/nb-NO.json @@ -41,7 +41,7 @@ "value": "Legg til nettsted" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Administrator" diff --git a/public/intl/messages/nl-NL.json b/public/intl/messages/nl-NL.json index 34777f935..c992b6c83 100644 --- a/public/intl/messages/nl-NL.json +++ b/public/intl/messages/nl-NL.json @@ -41,7 +41,7 @@ "value": "Website koppelen" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Beheerder" diff --git a/public/intl/messages/pl-PL.json b/public/intl/messages/pl-PL.json index 89ab2dcdb..923534487 100644 --- a/public/intl/messages/pl-PL.json +++ b/public/intl/messages/pl-PL.json @@ -41,7 +41,7 @@ "value": "Dodaj witrynę" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Administrator" diff --git a/public/intl/messages/pt-BR.json b/public/intl/messages/pt-BR.json index 3c6bec5bf..e926a9a2d 100644 --- a/public/intl/messages/pt-BR.json +++ b/public/intl/messages/pt-BR.json @@ -41,7 +41,7 @@ "value": "Adicionar site" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Administrador" diff --git a/public/intl/messages/pt-PT.json b/public/intl/messages/pt-PT.json index bcc7fae4b..ae3fb472c 100644 --- a/public/intl/messages/pt-PT.json +++ b/public/intl/messages/pt-PT.json @@ -41,7 +41,7 @@ "value": "Adicionar website" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Administrador" diff --git a/public/intl/messages/ro-RO.json b/public/intl/messages/ro-RO.json index 50ae21663..8236eba6b 100644 --- a/public/intl/messages/ro-RO.json +++ b/public/intl/messages/ro-RO.json @@ -41,7 +41,7 @@ "value": "Adăugare site web" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Administrator" diff --git a/public/intl/messages/ru-RU.json b/public/intl/messages/ru-RU.json index 7d3bd9ea4..e39299021 100644 --- a/public/intl/messages/ru-RU.json +++ b/public/intl/messages/ru-RU.json @@ -41,7 +41,7 @@ "value": "Добавить сайт" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Администратор" diff --git a/public/intl/messages/si-LK.json b/public/intl/messages/si-LK.json index eab794e6c..04d627fbc 100644 --- a/public/intl/messages/si-LK.json +++ b/public/intl/messages/si-LK.json @@ -41,7 +41,7 @@ "value": "වෙබ් අඩවිය එක් කරන්න" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Administrator" diff --git a/public/intl/messages/sk-SK.json b/public/intl/messages/sk-SK.json index b1c300eb1..690ee4c5a 100644 --- a/public/intl/messages/sk-SK.json +++ b/public/intl/messages/sk-SK.json @@ -41,7 +41,7 @@ "value": "Pridať web" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Administrátor" diff --git a/public/intl/messages/sl-SI.json b/public/intl/messages/sl-SI.json index 67524f57d..7773c813c 100644 --- a/public/intl/messages/sl-SI.json +++ b/public/intl/messages/sl-SI.json @@ -41,7 +41,7 @@ "value": "Dodaj spletno mesto" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Administrator" diff --git a/public/intl/messages/sv-SE.json b/public/intl/messages/sv-SE.json index 5e3b9021e..8b8ccf5a9 100644 --- a/public/intl/messages/sv-SE.json +++ b/public/intl/messages/sv-SE.json @@ -41,7 +41,7 @@ "value": "Lägg till webbplats" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Administratör" diff --git a/public/intl/messages/ta-IN.json b/public/intl/messages/ta-IN.json index e5f4f36bf..3566ed5cd 100644 --- a/public/intl/messages/ta-IN.json +++ b/public/intl/messages/ta-IN.json @@ -41,7 +41,7 @@ "value": "வலைத்தளத்தைச் சேர்க்க" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "நிர்வாகியைச் சேர்க்க" diff --git a/public/intl/messages/th-TH.json b/public/intl/messages/th-TH.json index f4356a50e..451e8e7a9 100644 --- a/public/intl/messages/th-TH.json +++ b/public/intl/messages/th-TH.json @@ -41,7 +41,7 @@ "value": "เพิ่มเว็บไซต์" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "ผู้ดูแลระบบ" diff --git a/public/intl/messages/tr-TR.json b/public/intl/messages/tr-TR.json index 1bd71b562..91d8d8dbe 100644 --- a/public/intl/messages/tr-TR.json +++ b/public/intl/messages/tr-TR.json @@ -41,7 +41,7 @@ "value": "Web sitesi ekle" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Yönetici" diff --git a/public/intl/messages/uk-UA.json b/public/intl/messages/uk-UA.json index 4dd2e86d5..d9b5bd1e2 100644 --- a/public/intl/messages/uk-UA.json +++ b/public/intl/messages/uk-UA.json @@ -41,7 +41,7 @@ "value": "Додати сайт" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Адміністратор" diff --git a/public/intl/messages/ur-PK.json b/public/intl/messages/ur-PK.json index 1f9ad9959..883992dc8 100644 --- a/public/intl/messages/ur-PK.json +++ b/public/intl/messages/ur-PK.json @@ -41,7 +41,7 @@ "value": "ویب سائٹ کا اضافہ کریں" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "منتظم" diff --git a/public/intl/messages/vi-VN.json b/public/intl/messages/vi-VN.json index 254f50705..cc76182d6 100644 --- a/public/intl/messages/vi-VN.json +++ b/public/intl/messages/vi-VN.json @@ -41,7 +41,7 @@ "value": "Thêm website" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "Quản trị" diff --git a/public/intl/messages/zh-CN.json b/public/intl/messages/zh-CN.json index a66d7db3a..41e748b0d 100644 --- a/public/intl/messages/zh-CN.json +++ b/public/intl/messages/zh-CN.json @@ -41,7 +41,7 @@ "value": "添加网站" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "管理员" diff --git a/public/intl/messages/zh-TW.json b/public/intl/messages/zh-TW.json index f0455d433..bd9a7d3aa 100644 --- a/public/intl/messages/zh-TW.json +++ b/public/intl/messages/zh-TW.json @@ -41,7 +41,7 @@ "value": "新增網站" } ], - "label.administrator": [ + "label.admin": [ { "type": 0, "value": "管理員" diff --git a/src/app/(main)/NavBar.tsx b/src/app/(main)/NavBar.tsx index 08007b1c4..5e0e3da26 100644 --- a/src/app/(main)/NavBar.tsx +++ b/src/app/(main)/NavBar.tsx @@ -14,7 +14,7 @@ import styles from './NavBar.module.css'; export function NavBar() { const { formatMessage, labels } = useMessages(); const { pathname, router } = useNavigation(); - const { renderTeamUrl } = useTeamUrl(); + const { teamId, renderTeamUrl } = useTeamUrl(); const cloudMode = !!process.env.cloudMode; @@ -34,25 +34,38 @@ export function NavBar() { label: formatMessage(labels.settings), url: renderTeamUrl('/settings'), children: [ + ...(teamId + ? [ + { + label: formatMessage(labels.team), + url: renderTeamUrl('/settings/team'), + }, + ] + : []), { label: formatMessage(labels.websites), - url: '/settings/websites', - }, - { - label: formatMessage(labels.teams), - url: '/settings/teams', - }, - { - label: formatMessage(labels.users), - url: '/settings/users', - }, - { - label: formatMessage(labels.profile), - url: '/profile', + url: renderTeamUrl('/settings/websites'), }, + ...(!teamId + ? [ + { + label: formatMessage(labels.teams), + url: renderTeamUrl('/settings/teams'), + }, + { + label: formatMessage(labels.users), + url: '/settings/users', + }, + ] + : [ + { + label: formatMessage(labels.members), + url: renderTeamUrl('/settings/members'), + }, + ]), ], }, - cloudMode && { + { label: formatMessage(labels.profile), url: '/profile', }, @@ -94,6 +107,7 @@ export function NavBar() {
+
diff --git a/src/app/(main)/UpdateNotice.module.css b/src/app/(main)/UpdateNotice.module.css index 261a31698..fec0962cd 100644 --- a/src/app/(main)/UpdateNotice.module.css +++ b/src/app/(main)/UpdateNotice.module.css @@ -1,14 +1,17 @@ .notice { position: absolute; + display: flex; + justify-content: space-between; + width: 100%; max-width: 800px; gap: 20px; - margin: 80px auto; + margin: 60px auto; align-self: center; background: var(--base50); padding: 20px; border: 1px solid var(--base300); border-radius: var(--border-radius); - z-index: var(--z-index-popup); + z-index: 9999; box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.1); } diff --git a/src/app/(main)/UpdateNotice.tsx b/src/app/(main)/UpdateNotice.tsx index 54ad05c9a..553e1138b 100644 --- a/src/app/(main)/UpdateNotice.tsx +++ b/src/app/(main)/UpdateNotice.tsx @@ -4,9 +4,9 @@ import { Button } from 'react-basics'; 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'; import { usePathname } from 'next/navigation'; +import styles from './UpdateNotice.module.css'; export function UpdateNotice({ user, config }) { const { formatMessage, labels, messages } = useMessages(); @@ -16,8 +16,9 @@ export function UpdateNotice({ user, config }) { const allowUpdate = user?.isAdmin && !config?.updatesDisabled && - !config?.cloudMode && !pathname.includes('/share/') && + !process.env.cloudMode && + !process.env.privateMode && !dismissed; const updateCheck = useCallback(() => { diff --git a/src/app/(main)/profile/DateRangeSetting.module.css b/src/app/(main)/profile/DateRangeSetting.module.css new file mode 100644 index 000000000..9de13efe9 --- /dev/null +++ b/src/app/(main)/profile/DateRangeSetting.module.css @@ -0,0 +1,3 @@ +.field { + width: 200px; +} diff --git a/src/app/(main)/profile/DateRangeSetting.tsx b/src/app/(main)/profile/DateRangeSetting.tsx index a1ae7bc79..c57a209a6 100644 --- a/src/app/(main)/profile/DateRangeSetting.tsx +++ b/src/app/(main)/profile/DateRangeSetting.tsx @@ -3,6 +3,7 @@ import { Button, Flexbox } from 'react-basics'; import { useDateRange, useMessages } from 'components/hooks'; import { DEFAULT_DATE_RANGE } from 'lib/constants'; import { DateRange } from 'lib/types'; +import styles from './DateRangeSetting.module.css'; export function DateRangeSetting() { const { formatMessage, labels } = useMessages(); @@ -13,8 +14,9 @@ export function DateRangeSetting() { const handleReset = () => setDateRange(DEFAULT_DATE_RANGE); return ( - + n.toLowerCase().includes(search.toLowerCase())) - : listTimeZones(); + ? timezones.filter(n => n.toLowerCase().includes(search.toLowerCase())) + : timezones; const handleReset = () => saveTimezone(getTimezone()); return ( saveTimezone(value)} menuProps={{ className: styles.menu }} allowSearch={true} onSearch={setSearch} diff --git a/src/app/(main)/reports/[reportId]/FieldAddForm.module.css b/src/app/(main)/reports/[reportId]/FieldAddForm.module.css deleted file mode 100644 index 5c5aaa4f0..000000000 --- a/src/app/(main)/reports/[reportId]/FieldAddForm.module.css +++ /dev/null @@ -1,38 +0,0 @@ -.menu { - width: 360px; - max-height: 300px; - overflow: auto; -} - -.item { - display: flex; - flex-direction: row; - justify-content: space-between; - border-radius: var(--border-radius); -} - -.item:hover { - background: var(--base75); -} - -.type { - color: var(--font-color300); -} - -.selected { - font-weight: bold; -} - -.popup { - display: flex; -} - -.filter { - display: flex; - flex-direction: column; - gap: 20px; -} - -.dropdown { - min-width: 60px; -} diff --git a/src/app/(main)/reports/[reportId]/FieldAddForm.tsx b/src/app/(main)/reports/[reportId]/FieldAddForm.tsx index 9db472d8d..9217ce4df 100644 --- a/src/app/(main)/reports/[reportId]/FieldAddForm.tsx +++ b/src/app/(main)/reports/[reportId]/FieldAddForm.tsx @@ -3,9 +3,6 @@ import { createPortal } from 'react-dom'; import { REPORT_PARAMETERS } from 'lib/constants'; import PopupForm from './PopupForm'; import FieldSelectForm from './FieldSelectForm'; -import FieldAggregateForm from './FieldAggregateForm'; -import FieldFilterForm from './FieldFilterForm'; -import styles from './FieldAddForm.module.css'; export function FieldAddForm({ fields = [], @@ -18,7 +15,11 @@ export function FieldAddForm({ onAdd: (group: string, value: string) => void; onClose: () => void; }) { - const [selected, setSelected] = useState<{ name: string; type: string; value: string }>(); + const [selected, setSelected] = useState<{ + name: string; + type: string; + value: string; + }>(); const handleSelect = (value: any) => { const { type } = value; @@ -38,14 +39,8 @@ export function FieldAddForm({ }; return createPortal( - + {!selected && } - {selected && group === REPORT_PARAMETERS.fields && ( - - )} - {selected && group === REPORT_PARAMETERS.filters && ( - - )} , document.body, ); diff --git a/src/app/(main)/reports/[reportId]/FieldFilterEditForm.module.css b/src/app/(main)/reports/[reportId]/FieldFilterEditForm.module.css new file mode 100644 index 000000000..43a34438c --- /dev/null +++ b/src/app/(main)/reports/[reportId]/FieldFilterEditForm.module.css @@ -0,0 +1,36 @@ +.menu { + position: absolute; + max-width: 300px; + max-height: 210px; +} + +.filter { + display: flex; + flex-direction: column; + gap: 20px; +} + +.dropdown { + min-width: 200px; +} + +.text { + min-width: 200px; +} + +.selected { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 16px; + white-space: nowrap; + min-width: 200px; + font-weight: 900; + background: var(--base100); + border-radius: var(--border-radius); + cursor: pointer; +} + +.search { + position: relative; +} diff --git a/src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx b/src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx new file mode 100644 index 000000000..dc10b724d --- /dev/null +++ b/src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx @@ -0,0 +1,224 @@ +import { useState, useMemo } from 'react'; +import { + Form, + FormRow, + Item, + Flexbox, + Dropdown, + Button, + SearchField, + TextField, + Text, + Icon, + Icons, + Menu, + Loading, +} from 'react-basics'; +import { useMessages, useFilters, useFormat, useLocale, useWebsiteValues } from 'components/hooks'; +import { OPERATORS } from 'lib/constants'; +import { isEqualsOperator } from 'lib/params'; +import styles from './FieldFilterEditForm.module.css'; + +export interface FieldFilterFormProps { + websiteId?: string; + name: string; + label?: string; + type: string; + startDate: Date; + endDate: Date; + operator?: string; + defaultValue?: string; + onChange?: (filter: { name: string; type: string; operator: string; value: string }) => void; + allowFilterSelect?: boolean; + isNew?: boolean; +} + +export default function FieldFilterEditForm({ + websiteId, + name, + label, + type, + startDate, + endDate, + operator: defaultOperator = 'eq', + defaultValue = '', + onChange, + allowFilterSelect = true, + isNew, +}: FieldFilterFormProps) { + const { formatMessage, labels } = useMessages(); + const [operator, setOperator] = useState(defaultOperator); + const [value, setValue] = useState(defaultValue); + const [showMenu, setShowMenu] = useState(false); + const isEquals = isEqualsOperator(operator); + const [search, setSearch] = useState(''); + const [selected, setSelected] = useState(isEquals ? value : ''); + const { filters } = useFilters(); + const { formatValue } = useFormat(); + const { locale } = useLocale(); + const isDisabled = !operator || (isEquals && !selected) || (!isEquals && !value); + const { + data: values = [], + isLoading, + refetch, + } = useWebsiteValues({ + websiteId, + type: name, + startDate, + endDate, + search, + }); + + const formattedValues = useMemo(() => { + if (!values) { + return {}; + } + const formatted = {}; + const format = (val: string) => { + formatted[val] = formatValue(val, name); + return formatted[val]; + }; + + if (values?.length !== 1) { + const { compare } = new Intl.Collator(locale, { numeric: true }); + values.sort((a, b) => compare(formatted[a] ?? format(a), formatted[b] ?? format(b))); + } else { + format(values[0]); + } + + return formatted; + }, [formatValue, locale, name, values]); + + const filteredValues = useMemo(() => { + return value + ? values.filter((n: string | number) => + formattedValues[n].toLowerCase().includes(value.toLowerCase()), + ) + : values; + }, [value, formattedValues]); + + const renderFilterValue = (value: any) => { + return filters.find((filter: { value: any }) => filter.value === value)?.label; + }; + + const handleAdd = () => { + onChange({ name, type, operator, value: isEquals ? selected : value }); + }; + + const handleMenuSelect = (value: string) => { + setSelected(value); + setShowMenu(false); + }; + + const handleSearch = (value: string) => { + setSearch(value); + }; + + const handleReset = () => { + setSelected(''); + setValue(''); + setSearch(''); + refetch(); + }; + + const handleOperatorChange = (value: any) => { + setOperator(value); + + if ([OPERATORS.equals, OPERATORS.notEquals].includes(value)) { + setValue(''); + } else { + setSelected(''); + } + }; + + const handleBlur = () => { + window.setTimeout(() => setShowMenu(false), 500); + }; + + return ( +
+ + + {allowFilterSelect && ( + f.type === type)} + value={operator} + renderValue={renderFilterValue} + onChange={handleOperatorChange} + > + {({ value, label }) => { + return {label}; + }} + + )} + {selected && isEquals && ( +
+ {formatValue(selected, name)} + + + +
+ )} + {!selected && isEquals && ( +
+ setValue(e.target.value)} + onSearch={handleSearch} + delay={500} + onFocus={() => setShowMenu(true)} + onBlur={handleBlur} + /> + {showMenu && ( + + )} +
+ )} + {!selected && !isEquals && ( + setValue(e.target.value)} + /> + )} +
+ +
+
+ ); +} + +const ResultsMenu = ({ values, type, isLoading, onSelect }) => { + const { formatValue } = useFormat(); + if (isLoading) { + return ( + + + + + + ); + } + + if (!values?.length) { + return null; + } + + return ( + + {values?.map((value: any) => { + return {formatValue(value, type)}; + })} + + ); +}; diff --git a/src/app/(main)/reports/[reportId]/FieldFilterForm.module.css b/src/app/(main)/reports/[reportId]/FieldFilterForm.module.css deleted file mode 100644 index be7bb9546..000000000 --- a/src/app/(main)/reports/[reportId]/FieldFilterForm.module.css +++ /dev/null @@ -1,24 +0,0 @@ -.popup { - display: flex; - max-width: 300px; - max-height: 210px; - overflow-x: hidden; -} - -.popup > div { - overflow-y: auto; -} - -.filter { - display: flex; - flex-direction: column; - gap: 20px; -} - -.dropdown { - min-width: 180px; -} - -.text { - min-width: 180px; -} diff --git a/src/app/(main)/reports/[reportId]/FieldFilterForm.tsx b/src/app/(main)/reports/[reportId]/FieldFilterForm.tsx deleted file mode 100644 index e38f3d656..000000000 --- a/src/app/(main)/reports/[reportId]/FieldFilterForm.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { useState, useMemo } from 'react'; -import { - Form, - FormRow, - Item, - Flexbox, - Dropdown, - Button, - TextField, - Menu, - Popup, - PopupTrigger, -} from 'react-basics'; -import { useMessages, useFilters, useFormat, useLocale } from 'components/hooks'; -import { safeDecodeURIComponent } from 'next-basics'; -import { OPERATORS } from 'lib/constants'; -import styles from './FieldFilterForm.module.css'; - -export interface FieldFilterFormProps { - name: string; - label?: string; - type: string; - values?: any[]; - onSelect?: (key: any) => void; - allowFilterSelect?: boolean; -} - -export default function FieldFilterForm({ - name, - label, - type, - values, - onSelect, - allowFilterSelect = true, -}: FieldFilterFormProps) { - const { formatMessage, labels } = useMessages(); - const [filter, setFilter] = useState('eq'); - const [value, setValue] = useState(''); - const { getFilters } = useFilters(); - const { formatValue } = useFormat(); - const { locale } = useLocale(); - const filters = getFilters(type); - - const formattedValues = useMemo(() => { - const formatted = {}; - const format = (val: string) => { - formatted[val] = formatValue(val, name); - return formatted[val]; - }; - if (values.length !== 1) { - const { compare } = new Intl.Collator(locale, { numeric: true }); - values.sort((a, b) => compare(formatted[a] ?? format(a), formatted[b] ?? format(b))); - } else { - format(values[0]); - } - return formatted; - }, [formatValue, locale, name, values]); - - const filteredValues = useMemo(() => { - return value ? values.filter(n => n.includes(value)) : values; - }, [value, formattedValues]); - - const renderFilterValue = value => { - return filters.find(f => f.value === value)?.label; - }; - - const handleAdd = () => { - onSelect({ name, type, filter, value }); - }; - - const handleMenuSelect = value => { - setValue(value); - }; - - const showMenu = - [OPERATORS.equals, OPERATORS.notEquals].includes(filter as any) && - !(filteredValues.length === 1 && filteredValues[0] === value); - - return ( -
- - - {allowFilterSelect && ( - setFilter(key)} - > - {({ value, label }) => { - return {label}; - }} - - )} - - setValue(e.target.value)} - /> - {showMenu && ( - - {filteredValues.length > 0 && ( - - {filteredValues.map(value => { - return {safeDecodeURIComponent(value)}; - })} - - )} - - )} - - - - -
- ); -} diff --git a/src/app/(main)/reports/insights/InsightsFieldParameters.tsx b/src/app/(main)/reports/[reportId]/FieldParameters.tsx similarity index 64% rename from src/app/(main)/reports/insights/InsightsFieldParameters.tsx rename to src/app/(main)/reports/[reportId]/FieldParameters.tsx index 798a828cd..36cfbda9b 100644 --- a/src/app/(main)/reports/insights/InsightsFieldParameters.tsx +++ b/src/app/(main)/reports/[reportId]/FieldParameters.tsx @@ -1,30 +1,18 @@ -import { useMessages } from 'components/hooks'; +import { useFields, useMessages } from 'components/hooks'; import Icons from 'components/icons'; import { useContext } from 'react'; import { Button, FormRow, Icon, Popup, PopupTrigger } from 'react-basics'; import FieldSelectForm from '../[reportId]/FieldSelectForm'; import ParameterList from '../[reportId]/ParameterList'; import PopupForm from '../[reportId]/PopupForm'; -import { ReportContext } from '../[reportId]/Report'; +import { ReportContext } from './Report'; -export function InsightsFieldParameters() { +export function FieldParameters() { const { report, updateReport } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); const { parameters } = report || {}; const { fields } = parameters || {}; - - const fieldOptions = [ - { name: 'url', type: 'string', label: formatMessage(labels.url) }, - { name: 'title', type: 'string', label: formatMessage(labels.pageTitle) }, - { name: 'referrer', type: 'string', label: formatMessage(labels.referrer) }, - { name: 'query', type: 'string', label: formatMessage(labels.query) }, - { name: 'browser', type: 'string', label: formatMessage(labels.browser) }, - { name: 'os', type: 'string', label: formatMessage(labels.os) }, - { name: 'device', type: 'string', label: formatMessage(labels.device) }, - { name: 'country', type: 'string', label: formatMessage(labels.country) }, - { name: 'region', type: 'string', label: formatMessage(labels.region) }, - { name: 'city', type: 'string', label: formatMessage(labels.city) }, - ]; + const { fields: fieldOptions } = useFields(); const handleAdd = (value: { name: any }) => { if (!fields.find(({ name }) => name === value.name)) { @@ -72,4 +60,4 @@ export function InsightsFieldParameters() { ); } -export default InsightsFieldParameters; +export default FieldParameters; diff --git a/src/app/(main)/reports/insights/InsightsFilterParameters.module.css b/src/app/(main)/reports/[reportId]/FilterParameters.module.css similarity index 94% rename from src/app/(main)/reports/insights/InsightsFilterParameters.module.css rename to src/app/(main)/reports/[reportId]/FilterParameters.module.css index 8b1795d21..939d0652d 100644 --- a/src/app/(main)/reports/insights/InsightsFilterParameters.module.css +++ b/src/app/(main)/reports/[reportId]/FilterParameters.module.css @@ -15,7 +15,7 @@ white-space: nowrap; } -.filter { +.op { color: var(--blue900); background-color: var(--blue100); font-size: 12px; @@ -34,3 +34,7 @@ border-radius: 5px; white-space: nowrap; } + +.edit { + margin-top: 20px; +} diff --git a/src/app/(main)/reports/[reportId]/FilterParameters.tsx b/src/app/(main)/reports/[reportId]/FilterParameters.tsx new file mode 100644 index 000000000..3118a6f4b --- /dev/null +++ b/src/app/(main)/reports/[reportId]/FilterParameters.tsx @@ -0,0 +1,136 @@ +import { useContext } from 'react'; +import { useMessages, useFormat, useFilters, useFields } from 'components/hooks'; +import Icons from 'components/icons'; +import { Button, FormRow, Icon, Popup, PopupTrigger } from 'react-basics'; +import FilterSelectForm from '../[reportId]/FilterSelectForm'; +import ParameterList from '../[reportId]/ParameterList'; +import PopupForm from '../[reportId]/PopupForm'; +import { ReportContext } from './Report'; +import FieldFilterEditForm from '../[reportId]/FieldFilterEditForm'; +import { isSearchOperator } from 'lib/params'; +import styles from './FilterParameters.module.css'; + +export function FilterParameters() { + const { report, updateReport } = useContext(ReportContext); + const { formatMessage, labels } = useMessages(); + const { formatValue } = useFormat(); + const { parameters } = report || {}; + const { websiteId, filters, dateRange } = parameters || {}; + const { fields } = useFields(); + + const handleAdd = (value: { name: any }) => { + if (!filters.find(({ name }) => name === value.name)) { + updateReport({ parameters: { filters: filters.concat(value) } }); + } + }; + + const handleRemove = (name: string) => { + updateReport({ parameters: { filters: filters.filter(f => f.name !== name) } }); + }; + + const handleChange = (close: () => void, filter: { name: any }) => { + updateReport({ + parameters: { + filters: filters.map(f => { + if (filter.name === f.name) { + return filter; + } + return f; + }), + }, + }); + close(); + }; + + const AddButton = () => { + return ( + + + + + !filters.find(f => f.name === name))} + onChange={handleAdd} + /> + + + + ); + }; + + return ( + }> + + {filters.map( + ({ name, operator, value }: { name: string; operator: string; value: string }) => { + const label = fields.find(f => f.name === name)?.label; + const isSearch = isSearchOperator(operator); + + return ( + handleRemove(name)}> + + + ); + }, + )} + + + ); +} + +const FilterParameter = ({ + websiteId, + name, + label, + operator, + value, + type = 'string', + startDate, + endDate, + onChange, +}) => { + const { operatorLabels } = useFilters(); + + return ( + +
+
{label}
+
{operatorLabels[operator]}
+
{value}
+
+ + {(close: any) => ( + + + + )} + +
+ ); +}; + +export default FilterParameters; diff --git a/src/app/(main)/reports/[reportId]/FilterSelectForm.tsx b/src/app/(main)/reports/[reportId]/FilterSelectForm.tsx index 3d209fc14..b81c85767 100644 --- a/src/app/(main)/reports/[reportId]/FilterSelectForm.tsx +++ b/src/app/(main)/reports/[reportId]/FilterSelectForm.tsx @@ -1,59 +1,41 @@ import { useState } from 'react'; -import { Loading } from 'react-basics'; -import { subDays } from 'date-fns'; import FieldSelectForm from './FieldSelectForm'; -import FieldFilterForm from './FieldFilterForm'; -import { useApi } from 'components/hooks'; - -function useValues(websiteId: string, type: string) { - const now = Date.now(); - const { get, useQuery } = useApi(); - const { data, error, isLoading } = useQuery({ - queryKey: ['websites:values', websiteId, type], - queryFn: () => - get(`/websites/${websiteId}/values`, { - type, - startAt: +subDays(now, 90), - endAt: now, - }), - enabled: !!(websiteId && type), - }); - - return { data, error, isLoading }; -} +import FieldFilterEditForm from './FieldFilterEditForm'; +import { useDateRange } from 'components/hooks'; export interface FilterSelectFormProps { - websiteId: string; + websiteId?: string; fields: any[]; - onSelect?: (key: any) => void; + onChange?: (filter: { name: string; type: string; operator: string; value: string }) => void; allowFilterSelect?: boolean; } export default function FilterSelectForm({ websiteId, fields, - onSelect, + onChange, allowFilterSelect, }: FilterSelectFormProps) { const [field, setField] = useState<{ name: string; label: string; type: string }>(); - const { data, isLoading } = useValues(websiteId, field?.name); + const [{ startDate, endDate }] = useDateRange(websiteId); if (!field) { return ; } - if (isLoading) { - return ; - } + const { name, label, type } = field; return ( - ); } diff --git a/src/app/(main)/reports/[reportId]/ParameterList.tsx b/src/app/(main)/reports/[reportId]/ParameterList.tsx index 7fe123818..f2ac988fc 100644 --- a/src/app/(main)/reports/[reportId]/ParameterList.tsx +++ b/src/app/(main)/reports/[reportId]/ParameterList.tsx @@ -4,6 +4,7 @@ import Icons from 'components/icons'; import Empty from 'components/common/Empty'; import { useMessages } from 'components/hooks'; import styles from './ParameterList.module.css'; +import classNames from 'classnames'; export interface ParameterListProps { children?: ReactNode; @@ -20,9 +21,19 @@ export function ParameterList({ children }: ParameterListProps) { ); } -const Item = ({ children, onRemove }: { children?: ReactNode; onRemove?: () => void }) => { +const Item = ({ + children, + className, + onClick, + onRemove, +}: { + children?: ReactNode; + className?: string; + onClick?: () => void; + onRemove?: () => void; +}) => { return ( -
+
{children} diff --git a/src/app/(main)/reports/[reportId]/PopupForm.module.css b/src/app/(main)/reports/[reportId]/PopupForm.module.css index 94d98b38c..5d069dd46 100644 --- a/src/app/(main)/reports/[reportId]/PopupForm.module.css +++ b/src/app/(main)/reports/[reportId]/PopupForm.module.css @@ -1,5 +1,4 @@ .form { - position: absolute; background: var(--base50); min-width: 300px; padding: 20px; diff --git a/src/app/(main)/reports/[reportId]/Report.tsx b/src/app/(main)/reports/[reportId]/Report.tsx index 76f735952..d6de9d425 100644 --- a/src/app/(main)/reports/[reportId]/Report.tsx +++ b/src/app/(main)/reports/[reportId]/Report.tsx @@ -13,7 +13,7 @@ export function Report({ className, }: { reportId: string; - defaultParameters: { [key: string]: any }; + defaultParameters: { type: string; parameters: { [key: string]: any } }; children: ReactNode; className?: string; }) { diff --git a/src/app/(main)/reports/event-data/EventDataParameters.tsx b/src/app/(main)/reports/event-data/EventDataParameters.tsx index efa9fb675..adc182748 100644 --- a/src/app/(main)/reports/event-data/EventDataParameters.tsx +++ b/src/app/(main)/reports/event-data/EventDataParameters.tsx @@ -60,10 +60,9 @@ export function EventDataParameters() { } }; - const handleRemove = (group: string, index: number) => { + const handleRemove = (group: string) => { const data = [...parameterData[group]]; - data.splice(index, 1); - updateReport({ parameters: { [group]: data } }); + updateReport({ parameters: { [group]: data.filter(({ name }) => name !== group) } }); }; const AddButton = ({ group, onAdd }) => { @@ -104,29 +103,28 @@ export function EventDataParameters() { label={label} action={} > - handleRemove(group, index)} - > - {({ name, value }) => { + + {parameterData[group].map(({ name, value }) => { return ( -
- {group === REPORT_PARAMETERS.fields && ( - <> -
{name}
-
{value}
- - )} - {group === REPORT_PARAMETERS.filters && ( - <> -
{name}
-
{value[0]}
-
{value[1]}
- - )} -
+ handleRemove(group)}> +
+ {group === REPORT_PARAMETERS.fields && ( + <> +
{name}
+
{value}
+ + )} + {group === REPORT_PARAMETERS.filters && ( + <> +
{name}
+
{value[0]}
+
{value[1]}
+ + )} +
+
); - }} + })}
); diff --git a/src/app/(main)/reports/funnel/FunnelChart.module.css b/src/app/(main)/reports/funnel/FunnelChart.module.css index 0279ea038..81b22c787 100644 --- a/src/app/(main)/reports/funnel/FunnelChart.module.css +++ b/src/app/(main)/reports/funnel/FunnelChart.module.css @@ -37,12 +37,12 @@ .card { display: grid; gap: 20px; + margin-top: 14px; } .header { display: flex; - align-items: center; - font-weight: 700; + flex-direction: column; gap: 20px; } @@ -51,19 +51,16 @@ align-items: center; justify-content: flex-end; background: var(--base900); - height: 50px; + height: 30px; border-radius: 5px; overflow: hidden; position: relative; } .label { - color: var(--base700); -} - -.value { - color: var(--base50); - margin-inline-end: 20px; + color: var(--base600); + font-weight: 700; + text-transform: uppercase; } .track { @@ -72,13 +69,33 @@ } .info { - display: flex; - justify-content: space-between; text-transform: lowercase; } .item { - padding: 6px 10px; - border-radius: 4px; - border: 1px solid var(--base300); + font-size: 20px; + color: var(--base900); + font-weight: 700; +} + +.metric { + color: var(--base700); + display: flex; + justify-content: space-between; + gap: 10px; + margin: 10px 0; + text-transform: lowercase; +} + +.visitors { + color: var(--base900); + font-size: 24px; + font-weight: 900; + margin-right: 10px; +} + +.percent { + font-size: 20px; + font-weight: 700; + align-self: flex-end; } diff --git a/src/app/(main)/reports/funnel/FunnelChart.tsx b/src/app/(main)/reports/funnel/FunnelChart.tsx index 6207a1777..0da71d6f5 100644 --- a/src/app/(main)/reports/funnel/FunnelChart.tsx +++ b/src/app/(main)/reports/funnel/FunnelChart.tsx @@ -2,8 +2,8 @@ import { useContext } from 'react'; import classNames from 'classnames'; import { useMessages } from 'components/hooks'; import { ReportContext } from '../[reportId]/Report'; -import styles from './FunnelChart.module.css'; import { formatLongNumber } from 'lib/format'; +import styles from './FunnelChart.module.css'; export interface FunnelChartProps { className?: string; @@ -18,35 +18,33 @@ export function FunnelChart({ className }: FunnelChartProps) { return (
- {data?.map(({ url, visitors, dropped, dropoff, remaining }, index: number) => { + {data?.map(({ type, value, visitors, dropped, dropoff, remaining }, index: number) => { return ( -
+
{index + 1}
- {formatMessage(labels.viewedPage)}: - {url} + + {formatMessage(type === 'url' ? labels.viewedPage : labels.triggeredEvent)} + + {value} +
+
+
+ {formatLongNumber(visitors)} + {formatMessage(labels.visitors)} +
+
{(remaining * 100).toFixed(2)}%
-
- - {remaining > 0.1 && `${(remaining * 100).toFixed(2)}%`} - -
+
-
-
- {formatLongNumber(visitors)} - {formatMessage(labels.visitors)} - ({(remaining * 100).toFixed(2)}%) + {dropoff > 0 && ( +
+ {formatLongNumber(dropped)} {formatMessage(labels.visitorsDroppedOff)} ( + {(dropoff * 100).toFixed(2)}%)
- {dropoff > 0 && ( -
- {formatLongNumber(dropped)} {formatMessage(labels.visitorsDroppedOff)} ( - {(dropoff * 100).toFixed(2)}%) -
- )} -
+ )}
); diff --git a/src/app/(main)/reports/funnel/FunnelParameters.module.css b/src/app/(main)/reports/funnel/FunnelParameters.module.css new file mode 100644 index 000000000..219b98076 --- /dev/null +++ b/src/app/(main)/reports/funnel/FunnelParameters.module.css @@ -0,0 +1,16 @@ +.item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; +} + +.type { + color: var(--base700); +} + +.value { + display: flex; + align-self: center; + gap: 20px; +} diff --git a/src/app/(main)/reports/funnel/FunnelParameters.tsx b/src/app/(main)/reports/funnel/FunnelParameters.tsx index 6eefbaae4..7b1fb0c81 100644 --- a/src/app/(main)/reports/funnel/FunnelParameters.tsx +++ b/src/app/(main)/reports/funnel/FunnelParameters.tsx @@ -10,50 +10,65 @@ import { Popup, SubmitButton, TextField, + Button, } from 'react-basics'; import Icons from 'components/icons'; -import UrlAddForm from './UrlAddForm'; +import FunnelStepAddForm from './FunnelStepAddForm'; import { ReportContext } from '../[reportId]/Report'; import BaseParameters from '../[reportId]/BaseParameters'; import ParameterList from '../[reportId]/ParameterList'; import PopupForm from '../[reportId]/PopupForm'; +import styles from './FunnelParameters.module.css'; export function FunnelParameters() { const { report, runReport, updateReport, isRunning } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); const { id, parameters } = report || {}; - const { websiteId, dateRange, urls } = parameters || {}; - const queryDisabled = !websiteId || !dateRange || urls?.length < 2; + const { websiteId, dateRange, steps } = parameters || {}; + const queryDisabled = !websiteId || !dateRange || steps?.length < 2; const handleSubmit = (data: any, e: any) => { e.stopPropagation(); e.preventDefault(); + if (!queryDisabled) { runReport(data); } }; - const handleAddUrl = (url: string) => { - updateReport({ parameters: { urls: parameters.urls.concat(url) } }); + const handleAddStep = (step: { type: string; value: string }) => { + updateReport({ parameters: { steps: parameters.steps.concat(step) } }); }; - const handleRemoveUrl = (index: number, e: any) => { - e.stopPropagation(); - const urls = [...parameters.urls]; - urls.splice(index, 1); - updateReport({ parameters: { urls } }); + const handleUpdateStep = ( + close: () => void, + index: number, + step: { type: string; value: string }, + ) => { + const steps = [...parameters.steps]; + steps[index] = step; + updateReport({ parameters: { steps } }); + close(); }; - const AddUrlButton = () => { + const handleRemoveStep = (index: number) => { + const steps = [...parameters.steps]; + delete steps[index]; + updateReport({ parameters: { steps: steps.filter(n => n) } }); + }; + + const AddStepButton = () => { return ( - - - - + + - + @@ -71,11 +86,37 @@ export function FunnelParameters() { - }> - handleRemoveUrl(index, e)} - /> + }> + + {steps.map((step: { type: string; value: string }, index: number) => { + return ( + + handleRemoveStep(index)} + > +
+
+ {step.type === 'url' ? : } +
+
{step.value}
+
+
+ + {(close: () => void) => ( + + + + )} + +
+ ); + })} +
diff --git a/src/app/(main)/reports/funnel/FunnelReport.tsx b/src/app/(main)/reports/funnel/FunnelReport.tsx index 7b9a6677a..850bbd906 100644 --- a/src/app/(main)/reports/funnel/FunnelReport.tsx +++ b/src/app/(main)/reports/funnel/FunnelReport.tsx @@ -9,7 +9,7 @@ import { REPORT_TYPES } from 'lib/constants'; const defaultParameters = { type: REPORT_TYPES.funnel, - parameters: { window: 60, urls: [] }, + parameters: { window: 60, steps: [] }, }; export default function FunnelReport({ reportId }: { reportId?: string }) { diff --git a/src/app/(main)/reports/funnel/FunnelStepAddForm.module.css b/src/app/(main)/reports/funnel/FunnelStepAddForm.module.css new file mode 100644 index 000000000..a254ff088 --- /dev/null +++ b/src/app/(main)/reports/funnel/FunnelStepAddForm.module.css @@ -0,0 +1,7 @@ +.dropdown { + width: 140px; +} + +.input { + width: 200px; +} diff --git a/src/app/(main)/reports/funnel/FunnelStepAddForm.tsx b/src/app/(main)/reports/funnel/FunnelStepAddForm.tsx new file mode 100644 index 000000000..7d77b0c72 --- /dev/null +++ b/src/app/(main)/reports/funnel/FunnelStepAddForm.tsx @@ -0,0 +1,80 @@ +import { useState } from 'react'; +import { useMessages } from 'components/hooks'; +import { Button, FormRow, TextField, Flexbox, Dropdown, Item } from 'react-basics'; +import styles from './FunnelStepAddForm.module.css'; + +export interface FunnelStepAddFormProps { + type?: string; + value?: string; + onChange?: (step: { type: string; value: string }) => void; +} + +export function FunnelStepAddForm({ + type: defaultType = 'url', + value: defaultValue = '', + onChange, +}: FunnelStepAddFormProps) { + const [type, setType] = useState(defaultType); + const [value, setValue] = useState(defaultValue); + const { formatMessage, labels } = useMessages(); + const items = [ + { label: formatMessage(labels.url), value: 'url' }, + { label: formatMessage(labels.event), value: 'event' }, + ]; + const isDisabled = !type || !value; + + const handleSave = () => { + onChange({ type, value }); + setValue(''); + }; + + const handleChange = e => { + setValue(e.target.value); + }; + + const handleKeyDown = e => { + if (e.key === 'Enter') { + e.stopPropagation(); + handleSave(); + } + }; + + const renderTypeValue = (value: any) => { + return items.find(item => item.value === value)?.label; + }; + + return ( + + + + setType(value)} + > + {({ value, label }) => { + return {label}; + }} + + + + + + + + + ); +} + +export default FunnelStepAddForm; diff --git a/src/app/(main)/reports/funnel/UrlAddForm.module.css b/src/app/(main)/reports/funnel/UrlAddForm.module.css deleted file mode 100644 index 6a3e03b5e..000000000 --- a/src/app/(main)/reports/funnel/UrlAddForm.module.css +++ /dev/null @@ -1,14 +0,0 @@ -.form { - position: absolute; - background: var(--base50); - width: 300px; - padding: 30px; - margin-top: 10px; - border: 1px solid var(--base400); - border-radius: var(--border-radius); - box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.1); -} - -.input { - width: 100%; -} diff --git a/src/app/(main)/reports/funnel/UrlAddForm.tsx b/src/app/(main)/reports/funnel/UrlAddForm.tsx deleted file mode 100644 index 88c27ae91..000000000 --- a/src/app/(main)/reports/funnel/UrlAddForm.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { useState } from 'react'; -import { useMessages } from 'components/hooks'; -import { Button, Form, FormRow, TextField, Flexbox } from 'react-basics'; -import styles from './UrlAddForm.module.css'; - -export interface UrlAddFormProps { - defaultValue?: string; - onAdd?: (url: string) => void; -} - -export function UrlAddForm({ defaultValue = '', onAdd }: UrlAddFormProps) { - const [url, setUrl] = useState(defaultValue); - const { formatMessage, labels } = useMessages(); - - const handleSave = () => { - onAdd(url); - setUrl(''); - }; - - const handleChange = e => { - setUrl(e.target.value); - }; - - const handleKeyDown = e => { - if (e.key === 'Enter') { - e.stopPropagation(); - handleSave(); - } - }; - - return ( -
- - - - - - -
- ); -} - -export default UrlAddForm; diff --git a/src/app/(main)/reports/insights/InsightsFilterParameters.tsx b/src/app/(main)/reports/insights/InsightsFilterParameters.tsx deleted file mode 100644 index 47554469c..000000000 --- a/src/app/(main)/reports/insights/InsightsFilterParameters.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useMessages, useFormat, useFilters } from 'components/hooks'; -import Icons from 'components/icons'; -import { useContext } from 'react'; -import { Button, FormRow, Icon, Popup, PopupTrigger } from 'react-basics'; -import FilterSelectForm from '../[reportId]/FilterSelectForm'; -import ParameterList from '../[reportId]/ParameterList'; -import PopupForm from '../[reportId]/PopupForm'; -import { ReportContext } from '../[reportId]/Report'; -import styles from './InsightsFilterParameters.module.css'; -import { safeDecodeURIComponent } from 'next-basics'; -import { OPERATORS } from 'lib/constants'; - -export function InsightsFilterParameters() { - const { report, updateReport } = useContext(ReportContext); - const { formatMessage, labels } = useMessages(); - const { formatValue } = useFormat(); - const { filterLabels } = useFilters(); - const { parameters } = report || {}; - const { websiteId, filters } = parameters || {}; - - const fieldOptions = [ - { name: 'url', type: 'string', label: formatMessage(labels.url) }, - { name: 'title', type: 'string', label: formatMessage(labels.pageTitle) }, - { name: 'referrer', type: 'string', label: formatMessage(labels.referrer) }, - { name: 'query', type: 'string', label: formatMessage(labels.query) }, - { name: 'browser', type: 'string', label: formatMessage(labels.browser) }, - { name: 'os', type: 'string', label: formatMessage(labels.os) }, - { name: 'device', type: 'string', label: formatMessage(labels.device) }, - { name: 'country', type: 'string', label: formatMessage(labels.country) }, - { name: 'region', type: 'string', label: formatMessage(labels.region) }, - { name: 'city', type: 'string', label: formatMessage(labels.city) }, - ]; - - const handleAdd = (value: { name: any }) => { - if (!filters.find(({ name }) => name === value.name)) { - updateReport({ parameters: { filters: filters.concat(value) } }); - } - }; - - const handleRemove = (name: string) => { - updateReport({ parameters: { filters: filters.filter(f => f.name !== name) } }); - }; - - const AddButton = () => { - return ( - - - - - !filters.find(f => f.name === name))} - onSelect={handleAdd} - /> - - - - ); - }; - - return ( - }> - - {filters.map(({ name, filter, value }) => { - const label = fieldOptions.find(f => f.name === name)?.label; - const isEquals = [OPERATORS.equals, OPERATORS.notEquals].includes(filter); - return ( - handleRemove(name)}> -
-
{label}
-
{filterLabels[filter]}
-
- {safeDecodeURIComponent(isEquals ? formatValue(value, name) : value)} -
-
-
- ); - })} -
-
- ); -} - -export default InsightsFilterParameters; diff --git a/src/app/(main)/reports/insights/InsightsParameters.tsx b/src/app/(main)/reports/insights/InsightsParameters.tsx index 22c57ff0c..7f58de6a4 100644 --- a/src/app/(main)/reports/insights/InsightsParameters.tsx +++ b/src/app/(main)/reports/insights/InsightsParameters.tsx @@ -3,8 +3,8 @@ import { useContext } from 'react'; import { Form, FormButtons, SubmitButton } from 'react-basics'; import BaseParameters from '../[reportId]/BaseParameters'; import { ReportContext } from '../[reportId]/Report'; -import InsightsFieldParameters from './InsightsFieldParameters'; -import InsightsFilterParameters from './InsightsFilterParameters'; +import FieldParameters from '../[reportId]/FieldParameters'; +import FilterParameters from '../[reportId]/FilterParameters'; export function InsightsParameters() { const { report, runReport, isRunning } = useContext(ReportContext); @@ -22,8 +22,8 @@ export function InsightsParameters() { return (
- {parametersSelected && } - {parametersSelected && } + {parametersSelected && } + {parametersSelected && } {formatMessage(labels.runQuery)} diff --git a/src/app/(main)/settings/teams/[teamId]/websites/TeamWebsitesTable.tsx b/src/app/(main)/settings/teams/[teamId]/websites/TeamWebsitesTable.tsx index dc6760a6b..c733e3e36 100644 --- a/src/app/(main)/settings/teams/[teamId]/websites/TeamWebsitesTable.tsx +++ b/src/app/(main)/settings/teams/[teamId]/websites/TeamWebsitesTable.tsx @@ -1,4 +1,4 @@ -import { GridColumn, GridTable, Icon, Text } from 'react-basics'; +import { GridColumn, GridTable, Icon, Text, useBreakpoint } from 'react-basics'; import { useLogin, useMessages } from 'components/hooks'; import Icons from 'components/icons'; import LinkButton from 'components/common/LinkButton'; @@ -14,9 +14,10 @@ export function TeamWebsitesTable({ }) { const { user } = useLogin(); const { formatMessage, labels } = useMessages(); + const breakpoint = useBreakpoint(); return ( - + diff --git a/src/app/(main)/settings/users/UserAddForm.tsx b/src/app/(main)/settings/users/UserAddForm.tsx index 7ea720072..979f399fe 100644 --- a/src/app/(main)/settings/users/UserAddForm.tsx +++ b/src/app/(main)/settings/users/UserAddForm.tsx @@ -34,7 +34,7 @@ export function UserAddForm({ onSave, onClose }) { return formatMessage(labels.user); } if (value === ROLES.admin) { - return formatMessage(labels.administrator); + return formatMessage(labels.admin); } if (value === ROLES.viewOnly) { return formatMessage(labels.viewOnly); @@ -58,7 +58,7 @@ export function UserAddForm({ onSave, onClose }) { {formatMessage(labels.viewOnly)} {formatMessage(labels.user)} - {formatMessage(labels.administrator)} + {formatMessage(labels.admin)} diff --git a/src/app/(main)/settings/users/[userId]/UserEditForm.tsx b/src/app/(main)/settings/users/[userId]/UserEditForm.tsx index 369b4ff29..1acfc581a 100644 --- a/src/app/(main)/settings/users/[userId]/UserEditForm.tsx +++ b/src/app/(main)/settings/users/[userId]/UserEditForm.tsx @@ -46,7 +46,7 @@ export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () = return formatMessage(labels.user); } if (value === ROLES.admin) { - return formatMessage(labels.administrator); + return formatMessage(labels.admin); } if (value === ROLES.viewOnly) { return formatMessage(labels.viewOnly); @@ -76,7 +76,7 @@ export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () = {formatMessage(labels.viewOnly)} {formatMessage(labels.user)} - {formatMessage(labels.administrator)} + {formatMessage(labels.admin)} diff --git a/src/app/(main)/settings/websites/WebsitesTable.module.css b/src/app/(main)/settings/websites/WebsitesTable.module.css deleted file mode 100644 index a26c349fd..000000000 --- a/src/app/(main)/settings/websites/WebsitesTable.module.css +++ /dev/null @@ -1,13 +0,0 @@ -@media screen and (max-width: 992px) { - .row { - flex-wrap: wrap; - } - - .header .actions { - display: none; - } - - .actions { - flex-basis: 100%; - } -} diff --git a/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx b/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx index 640c519b1..989f4def3 100644 --- a/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/ShareUrl.tsx @@ -35,7 +35,7 @@ export function ShareUrl({ const url = `${hostUrl || process.env.hostUrl || window?.location.origin}${ process.env.basePath - }/share/${id}/${encodeURIComponent(domain)}`; + }/share/${id}/${domain}`; const handleGenerate = () => { setId(generateId()); diff --git a/src/app/(main)/websites/[websiteId]/WebsiteChartList.tsx b/src/app/(main)/websites/[websiteId]/WebsiteChartList.tsx index b35b6f1f0..6484e3835 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteChartList.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteChartList.tsx @@ -6,7 +6,7 @@ import WebsiteChart from './WebsiteChart'; import useDashboard from 'store/dashboard'; import WebsiteHeader from './WebsiteHeader'; import { WebsiteMetricsBar } from './WebsiteMetricsBar'; -import { useMessages, useLocale } from 'components/hooks'; +import { useMessages, useLocale, useTeamUrl } from 'components/hooks'; export default function WebsiteChartList({ websites, @@ -19,6 +19,7 @@ export default function WebsiteChartList({ }) { const { formatMessage, labels } = useMessages(); const { websiteOrder } = useDashboard(); + const { renderTeamUrl } = useTeamUrl(); const { dir } = useLocale(); const ordered = useMemo( @@ -35,7 +36,7 @@ export default function WebsiteChartList({ return index < limit ? (
- + - + {(close: () => void) => { return ( { + fields={fields} + onChange={value => { handleAddFilter(value); close(); }} - allowFilterSelect={false} /> ); diff --git a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx index 2dcb2b813..e4acea3bf 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx @@ -21,11 +21,12 @@ export function WebsiteMetricsBar({ const { ref, isSticky } = useSticky({ enabled: sticky }); const { data, isLoading, isFetched, error } = useWebsiteStats(websiteId); - const { pageviews, uniques, bounces, totaltime } = data || {}; - const num = Math.min(data && uniques.value, data && bounces.value); + const { pageviews, visitors, visits, bounces, totaltime } = data || {}; + const num = Math.min(data && visitors.value, data && bounces.value); const diffs = data && { pageviews: pageviews.value - pageviews.change, - uniques: uniques.value - uniques.change, + visitors: visitors.value - visitors.change, + visits: visits.value - visits.change, bounces: bounces.value - bounces.change, totaltime: totaltime.value - totaltime.change, }; @@ -39,25 +40,30 @@ export function WebsiteMetricsBar({ })} > - {pageviews && uniques && ( + {pageviews && visitors && ( <> + Number(n).toFixed(0) + '%'} diff --git a/src/components/charts/BarChart.tsx b/src/components/charts/BarChart.tsx index 09ff51548..635bb10de 100644 --- a/src/components/charts/BarChart.tsx +++ b/src/components/charts/BarChart.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { useTheme } from 'components/hooks'; import Chart, { ChartProps } from 'components/charts/Chart'; import { renderNumberLabels } from 'lib/charts'; @@ -25,45 +26,47 @@ export function BarChart(props: BarChartProps) { stacked = false, } = props; - const options = { - scales: { - x: { - type: XAxisType, - stacked: true, - time: { - unit, + const options = useMemo(() => { + return { + scales: { + x: { + type: XAxisType, + stacked: true, + time: { + unit, + }, + grid: { + display: false, + }, + border: { + color: colors.chart.line, + }, + ticks: { + color: colors.chart.text, + autoSkip: false, + maxRotation: 0, + callback: renderXLabel, + }, }, - grid: { - display: false, - }, - border: { - color: colors.chart.line, - }, - ticks: { - color: colors.chart.text, - autoSkip: false, - maxRotation: 0, - callback: renderXLabel, + y: { + type: YAxisType, + min: 0, + beginAtZero: true, + stacked, + grid: { + color: colors.chart.line, + }, + border: { + color: colors.chart.line, + }, + ticks: { + color: colors.chart.text, + callback: renderYLabel || renderNumberLabels, + }, }, }, - y: { - type: YAxisType, - min: 0, - beginAtZero: true, - stacked, - grid: { - color: colors.chart.line, - }, - border: { - color: colors.chart.line, - }, - ticks: { - color: colors.chart.text, - callback: renderYLabel || renderNumberLabels, - }, - }, - }, - }; + }; + }, [colors, unit, stacked, renderXLabel, renderYLabel]); const handleTooltip = ({ tooltip }: { tooltip: any }) => { const { opacity } = tooltip; diff --git a/src/components/charts/Chart.tsx b/src/components/charts/Chart.tsx index ba49796eb..993618c2a 100644 --- a/src/components/charts/Chart.tsx +++ b/src/components/charts/Chart.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect, ReactNode } from 'react'; +import { useState, useRef, useEffect, useMemo, ReactNode } from 'react'; import { Loading } from 'react-basics'; import classNames from 'classnames'; import ChartJS, { LegendItem } from 'chart.js/auto'; @@ -38,29 +38,31 @@ export function Chart({ const chart = useRef(null); const [legendItems, setLegendItems] = useState([]); - const options = { - responsive: true, - maintainAspectRatio: false, - animation: { - duration: animationDuration, - resize: { - duration: 0, + const options = useMemo(() => { + return { + responsive: true, + maintainAspectRatio: false, + animation: { + duration: animationDuration, + resize: { + duration: 0, + }, + active: { + duration: 0, + }, }, - active: { - duration: 0, + plugins: { + legend: { + display: false, + }, + tooltip: { + enabled: false, + external: onTooltip, + }, }, - }, - plugins: { - legend: { - display: false, - }, - tooltip: { - enabled: false, - external: onTooltip, - }, - }, - ...chartOptions, - }; + ...chartOptions, + }; + }, [chartOptions]); const createChart = (data: any) => { ChartJS.defaults.font.family = 'Inter'; @@ -79,6 +81,7 @@ export function Chart({ const updateChart = (data: any) => { chart.current.data.datasets.forEach((dataset: { data: any }, index: string | number) => { dataset.data = data?.datasets[index]?.data; + chart.current.legend.legendItems[index].text = data?.datasets[index].label; }); chart.current.options = options; @@ -86,9 +89,9 @@ export function Chart({ // Allow config changes before update onUpdate?.(chart.current); - chart.current.update(updateMode); - setLegendItems(chart.current.legend.legendItems); + + chart.current.update(updateMode); }; useEffect(() => { @@ -99,7 +102,7 @@ export function Chart({ updateChart(data); } } - }, [data]); + }, [data, options]); const handleLegendClick = (item: LegendItem) => { if (type === 'bar') { diff --git a/src/components/common/Favicon.tsx b/src/components/common/Favicon.tsx index 2bf43c771..cdaeaf4bf 100644 --- a/src/components/common/Favicon.tsx +++ b/src/components/common/Favicon.tsx @@ -6,13 +6,18 @@ function getHostName(url: string) { } export function Favicon({ domain, ...props }) { + if (process.env.privateMode) { + return null; + } + const hostName = domain ? getHostName(domain) : null; return hostName ? ( diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts index a737ba204..df4fbd88f 100644 --- a/src/components/hooks/index.ts +++ b/src/components/hooks/index.ts @@ -16,10 +16,12 @@ export * from './queries/useWebsite'; export * from './queries/useWebsites'; export * from './queries/useWebsiteEvents'; export * from './queries/useWebsiteMetrics'; +export * from './queries/useWebsiteValues'; export * from './useCountryNames'; export * from './useDateRange'; export * from './useDocumentClick'; export * from './useEscapeKey'; +export * from './useFields'; export * from './useFilters'; export * from './useForceUpdate'; export * from './useFormat'; diff --git a/src/components/hooks/queries/useReport.ts b/src/components/hooks/queries/useReport.ts index ef571d008..3aacabb40 100644 --- a/src/components/hooks/queries/useReport.ts +++ b/src/components/hooks/queries/useReport.ts @@ -4,11 +4,14 @@ import { useApi } from './useApi'; import { useTimezone } from '../useTimezone'; import { useMessages } from '../useMessages'; -export function useReport(reportId: string, defaultParameters: { [key: string]: any } = {}) { +export function useReport( + reportId: string, + defaultParameters: { type: string; parameters: { [key: string]: any } }, +) { const [report, setReport] = useState(null); const [isRunning, setIsRunning] = useState(false); const { get, post } = useApi(); - const [timezone] = useTimezone(); + const { timezone } = useTimezone(); const { formatMessage, labels } = useMessages(); const baseParameters = { @@ -28,6 +31,8 @@ export function useReport(reportId: string, defaultParameters: { [key: string]: dateRange.endDate = new Date(endDate); } + data.parameters = { ...defaultParameters?.parameters, ...data.parameters }; + setReport(data); }; @@ -41,7 +46,7 @@ export function useReport(reportId: string, defaultParameters: { [key: string]: setReport( produce((state: any) => { - state.parameters = parameters; + state.parameters = { ...defaultParameters?.parameters, ...parameters }; state.data = data; return state; @@ -60,7 +65,11 @@ export function useReport(reportId: string, defaultParameters: { [key: string]: const { parameters, ...rest } = data; if (parameters) { - state.parameters = { ...state.parameters, ...parameters }; + state.parameters = { + ...defaultParameters?.parameters, + ...state.parameters, + ...parameters, + }; } for (const key in rest) { @@ -80,7 +89,7 @@ export function useReport(reportId: string, defaultParameters: { [key: string]: } else { loadReport(reportId); } - }, []); + }, [reportId]); return { report, runReport, updateReport, isRunning }; } diff --git a/src/components/hooks/queries/useWebsiteEvents.ts b/src/components/hooks/queries/useWebsiteEvents.ts index de18a1f90..992814702 100644 --- a/src/components/hooks/queries/useWebsiteEvents.ts +++ b/src/components/hooks/queries/useWebsiteEvents.ts @@ -1,12 +1,29 @@ import useApi from './useApi'; import { UseQueryOptions } from '@tanstack/react-query'; +import { useDateRange, useNavigation, useTimezone } from 'components/hooks'; +import { zonedTimeToUtc } from 'date-fns-tz'; export function useWebsiteEvents( websiteId: string, - params?: { [key: string]: any }, options?: Omit, ) { const { get, useQuery } = useApi(); + const [dateRange] = useDateRange(websiteId); + const { startDate, endDate, unit, offset } = dateRange; + const { timezone } = useTimezone(); + const { + query: { url, event }, + } = useNavigation(); + + const params = { + startAt: +zonedTimeToUtc(startDate, timezone), + endAt: +zonedTimeToUtc(endDate, timezone), + unit, + offset, + timezone, + url, + event, + }; return useQuery({ queryKey: ['events', { ...params }], diff --git a/src/components/hooks/queries/useWebsitePageviews.ts b/src/components/hooks/queries/useWebsitePageviews.ts index f5502f8ab..7262708ac 100644 --- a/src/components/hooks/queries/useWebsitePageviews.ts +++ b/src/components/hooks/queries/useWebsitePageviews.ts @@ -1,17 +1,18 @@ +import { zonedTimeToUtc } from 'date-fns-tz'; import { useApi, useDateRange, useNavigation, useTimezone } from 'components/hooks'; export function useWebsitePageviews(websiteId: string, options?: { [key: string]: string }) { const { get, useQuery } = useApi(); const [dateRange] = useDateRange(websiteId); const { startDate, endDate, unit } = dateRange; - const [timezone] = useTimezone(); + const { timezone } = useTimezone(); const { query: { url, referrer, query, os, browser, device, country, region, city, title }, } = useNavigation(); const params = { - startAt: +startDate, - endAt: +endDate, + startAt: +zonedTimeToUtc(startDate, timezone), + endAt: +zonedTimeToUtc(endDate, timezone), unit, timezone, url, diff --git a/src/components/hooks/queries/useWebsiteValues.ts b/src/components/hooks/queries/useWebsiteValues.ts new file mode 100644 index 000000000..02e26fc33 --- /dev/null +++ b/src/components/hooks/queries/useWebsiteValues.ts @@ -0,0 +1,31 @@ +import { useApi } from 'components/hooks'; + +export function useWebsiteValues({ + websiteId, + type, + startDate, + endDate, + search, +}: { + websiteId: string; + type: string; + startDate: Date; + endDate: Date; + search?: string; +}) { + const { get, useQuery } = useApi(); + + return useQuery({ + queryKey: ['websites:values', { websiteId, type, startDate, endDate, search }], + queryFn: () => + get(`/websites/${websiteId}/values`, { + type, + startAt: +startDate, + endAt: +endDate, + search, + }), + enabled: !!(websiteId && type && startDate && endDate), + }); +} + +export default useWebsiteValues; diff --git a/src/components/hooks/useFields.ts b/src/components/hooks/useFields.ts new file mode 100644 index 000000000..05d2b4588 --- /dev/null +++ b/src/components/hooks/useFields.ts @@ -0,0 +1,22 @@ +import { useMessages } from './useMessages'; + +export function useFields() { + const { formatMessage, labels } = useMessages(); + + const fields = [ + { name: 'url', type: 'string', label: formatMessage(labels.url) }, + { name: 'title', type: 'string', label: formatMessage(labels.pageTitle) }, + { name: 'referrer', type: 'string', label: formatMessage(labels.referrer) }, + { name: 'query', type: 'string', label: formatMessage(labels.query) }, + { name: 'browser', type: 'string', label: formatMessage(labels.browser) }, + { name: 'os', type: 'string', label: formatMessage(labels.os) }, + { name: 'device', type: 'string', label: formatMessage(labels.device) }, + { name: 'country', type: 'string', label: formatMessage(labels.country) }, + { name: 'region', type: 'string', label: formatMessage(labels.region) }, + { name: 'city', type: 'string', label: formatMessage(labels.city) }, + ]; + + return { fields }; +} + +export default useFields; diff --git a/src/components/hooks/useFilters.ts b/src/components/hooks/useFilters.ts index 714a8c97a..5f89eca40 100644 --- a/src/components/hooks/useFilters.ts +++ b/src/components/hooks/useFilters.ts @@ -4,7 +4,7 @@ import { OPERATORS } from 'lib/constants'; export function useFilters() { const { formatMessage, labels } = useMessages(); - const filterLabels = { + const operatorLabels = { [OPERATORS.equals]: formatMessage(labels.is), [OPERATORS.notEquals]: formatMessage(labels.isNot), [OPERATORS.set]: formatMessage(labels.isSet), @@ -37,11 +37,17 @@ export function useFilters() { uuid: [OPERATORS.equals], }; + const filters = Object.keys(typeFilters).flatMap(key => { + return ( + typeFilters[key]?.map(value => ({ type: key, value, label: operatorLabels[value] })) ?? [] + ); + }); + const getFilters = type => { - return typeFilters[type]?.map(key => ({ type, value: key, label: filterLabels[key] })) ?? []; + return typeFilters[type]?.map(key => ({ type, value: key, label: operatorLabels[key] })) ?? []; }; - return { getFilters, filterLabels, typeFilters }; + return { filters, operatorLabels, typeFilters, getFilters }; } export default useFilters; diff --git a/src/components/hooks/useTheme.ts b/src/components/hooks/useTheme.ts index f2a2d448b..aa2b1d381 100644 --- a/src/components/hooks/useTheme.ts +++ b/src/components/hooks/useTheme.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import useStore, { setTheme } from 'store/app'; import { getItem, setItem } from 'next-basics'; import { DEFAULT_THEME, THEME_COLORS, THEME_CONFIG } from 'lib/constants'; @@ -10,38 +10,40 @@ export function useTheme() { const theme = useStore(selector) || getItem(THEME_CONFIG) || DEFAULT_THEME; const primaryColor = colord(THEME_COLORS[theme].primary); - const colors = { - theme: { - ...THEME_COLORS[theme], - }, - chart: { - text: THEME_COLORS[theme].gray700, - line: THEME_COLORS[theme].gray200, - views: { - hoverBackgroundColor: primaryColor.alpha(0.7).toRgbString(), - backgroundColor: primaryColor.alpha(0.4).toRgbString(), - borderColor: primaryColor.alpha(0.7).toRgbString(), - hoverBorderColor: primaryColor.toRgbString(), + const colors = useMemo(() => { + return { + theme: { + ...THEME_COLORS[theme], }, - visitors: { - hoverBackgroundColor: primaryColor.alpha(0.9).toRgbString(), - backgroundColor: primaryColor.alpha(0.6).toRgbString(), - borderColor: primaryColor.alpha(0.9).toRgbString(), - hoverBorderColor: primaryColor.toRgbString(), + chart: { + text: THEME_COLORS[theme].gray700, + line: THEME_COLORS[theme].gray200, + views: { + hoverBackgroundColor: primaryColor.alpha(0.7).toRgbString(), + backgroundColor: primaryColor.alpha(0.4).toRgbString(), + borderColor: primaryColor.alpha(0.7).toRgbString(), + hoverBorderColor: primaryColor.toRgbString(), + }, + visitors: { + hoverBackgroundColor: primaryColor.alpha(0.9).toRgbString(), + backgroundColor: primaryColor.alpha(0.6).toRgbString(), + borderColor: primaryColor.alpha(0.9).toRgbString(), + hoverBorderColor: primaryColor.toRgbString(), + }, }, - }, - map: { - baseColor: THEME_COLORS[theme].primary, - fillColor: THEME_COLORS[theme].gray100, - strokeColor: THEME_COLORS[theme].primary, - hoverColor: THEME_COLORS[theme].primary, - }, - }; + map: { + baseColor: THEME_COLORS[theme].primary, + fillColor: THEME_COLORS[theme].gray100, + strokeColor: THEME_COLORS[theme].primary, + hoverColor: THEME_COLORS[theme].primary, + }, + }; + }, [theme]); - function saveTheme(value) { + const saveTheme = (value: string) => { setItem(THEME_CONFIG, value); setTheme(value); - } + }; useEffect(() => { document.body.setAttribute('data-theme', theme); diff --git a/src/components/hooks/useTimezone.ts b/src/components/hooks/useTimezone.ts index 3dbb52b3b..8bd76504b 100644 --- a/src/components/hooks/useTimezone.ts +++ b/src/components/hooks/useTimezone.ts @@ -1,20 +1,18 @@ -import { useState, useCallback } from 'react'; -import { getTimezone } from 'lib/date'; -import { getItem, setItem } from 'next-basics'; +import { setItem } from 'next-basics'; import { TIMEZONE_CONFIG } from 'lib/constants'; +import useStore, { setTimezone } from 'store/app'; + +const selector = (state: { timezone: string }) => state.timezone; export function useTimezone() { - const [timezone, setTimezone] = useState(getItem(TIMEZONE_CONFIG) || getTimezone()); + const timezone = useStore(selector); - const saveTimezone = useCallback( - (value: string) => { - setItem(TIMEZONE_CONFIG, value); - setTimezone(value); - }, - [setTimezone], - ); + const saveTimezone = (value: string) => { + setItem(TIMEZONE_CONFIG, value); + setTimezone(value); + }; - return [timezone, saveTimezone]; + return { timezone, saveTimezone }; } export default useTimezone; diff --git a/src/components/input/DateFilter.tsx b/src/components/input/DateFilter.tsx index 353480163..6e7c099a5 100644 --- a/src/components/input/DateFilter.tsx +++ b/src/components/input/DateFilter.tsx @@ -117,7 +117,7 @@ export function DateFilter({ ); } - return options.find(e => e.value === value).label; + return options.find(e => e.value === value)?.label; }; return ( diff --git a/src/components/input/ProfileButton.tsx b/src/components/input/ProfileButton.tsx index ff3ee63ec..b1875165a 100644 --- a/src/components/input/ProfileButton.tsx +++ b/src/components/input/ProfileButton.tsx @@ -11,7 +11,7 @@ export function ProfileButton() { const { user } = useLogin(); const router = useRouter(); const { dir } = useLocale(); - const cloudMode = Boolean(process.env.cloudMode); + const cloudMode = !!process.env.cloudMode; const handleSelect = (key: Key, close: () => void) => { if (key === 'profile') { diff --git a/src/components/input/TeamsButton.tsx b/src/components/input/TeamsButton.tsx index e3b5a3a8d..1f6270b42 100644 --- a/src/components/input/TeamsButton.tsx +++ b/src/components/input/TeamsButton.tsx @@ -7,9 +7,11 @@ import styles from './TeamsButton.module.css'; export function TeamsButton({ className, + showText = true, onChange, }: { className?: string; + showText?: boolean; onChange?: (value: string) => void; }) { const { user } = useLogin(); @@ -31,7 +33,7 @@ export function TeamsButton({