From 2c0b7a64084d23c71a9809a0701e7ce3fac9f70c Mon Sep 17 00:00:00 2001 From: vicke4 Date: Tue, 1 Jul 2025 14:32:17 +0530 Subject: [PATCH 1/9] feat: persistent event tab selection --- .../websites/[websiteId]/events/EventsPage.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx index 285a230e..259e77be 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx @@ -9,16 +9,22 @@ import { useMessages } from '@/components/hooks'; import { Item, Tabs } from 'react-basics'; import { useState } from 'react'; import EventProperties from './EventProperties'; +import { getItem, setItem } from '@/lib/storage'; export default function EventsPage({ websiteId }) { const [label, setLabel] = useState(null); - const [tab, setTab] = useState('activity'); + const [tab, setTab] = useState(getItem('eventTab') || 'activity'); const { formatMessage, labels } = useMessages(); const handleLabelClick = (value: string) => { setLabel(value !== label ? value : ''); }; + const onSelect = (value: any) => { + setItem('eventTab', value); + setTab(value); + }; + return ( <> @@ -34,11 +40,7 @@ export default function EventsPage({ websiteId }) { />
- setTab(value)} - style={{ marginBottom: 30 }} - > + {formatMessage(labels.activity)} {formatMessage(labels.properties)} From da7f4cb2d0dec2a810e789ed53fceb77cb659a58 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Fri, 18 Jul 2025 10:26:24 -0700 Subject: [PATCH 2/9] create and apply mysql migrations --- .../migrations/11_add_segment/migration.sql | 14 ++++ .../12_update_report_parameter/migration.sql | 2 + .../migrations/13_add_revenue/migration.sql | 18 +++++ db/mysql/schema.prisma | 46 +++++++++++-- pnpm-lock.yaml | 68 +++++++++---------- .../populate-revenue-table.sql | 41 +++++++++++ 6 files changed, 151 insertions(+), 38 deletions(-) create mode 100644 db/mysql/migrations/11_add_segment/migration.sql create mode 100644 db/mysql/migrations/12_update_report_parameter/migration.sql create mode 100644 db/mysql/migrations/13_add_revenue/migration.sql create mode 100644 scripts/data-migrations/populate-revenue-table.sql diff --git a/db/mysql/migrations/11_add_segment/migration.sql b/db/mysql/migrations/11_add_segment/migration.sql new file mode 100644 index 00000000..c79e916d --- /dev/null +++ b/db/mysql/migrations/11_add_segment/migration.sql @@ -0,0 +1,14 @@ +-- CreateTable +CREATE TABLE `segment` ( + `segment_id` VARCHAR(36) NOT NULL, + `website_id` VARCHAR(36) NOT NULL, + `type` VARCHAR(200) NOT NULL, + `name` VARCHAR(200) NOT NULL, + `parameters` JSON NOT NULL, + `created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0), + `updated_at` TIMESTAMP(0) NULL, + + UNIQUE INDEX `segment_segment_id_key`(`segment_id`), + INDEX `segment_website_id_idx`(`website_id`), + PRIMARY KEY (`segment_id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/db/mysql/migrations/12_update_report_parameter/migration.sql b/db/mysql/migrations/12_update_report_parameter/migration.sql new file mode 100644 index 00000000..f6a99c3f --- /dev/null +++ b/db/mysql/migrations/12_update_report_parameter/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `report` MODIFY `parameters` JSON NOT NULL; diff --git a/db/mysql/migrations/13_add_revenue/migration.sql b/db/mysql/migrations/13_add_revenue/migration.sql new file mode 100644 index 00000000..96115a33 --- /dev/null +++ b/db/mysql/migrations/13_add_revenue/migration.sql @@ -0,0 +1,18 @@ +-- CreateTable +CREATE TABLE `revenue` ( + `revenue_id` VARCHAR(36) NOT NULL, + `website_id` VARCHAR(36) NOT NULL, + `session_id` VARCHAR(36) NOT NULL, + `event_id` VARCHAR(36) NOT NULL, + `event_name` VARCHAR(50) NOT NULL, + `currency` VARCHAR(100) NOT NULL, + `revenue` DECIMAL(19, 4) NULL, + `created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0), + + UNIQUE INDEX `revenue_revenue_id_key`(`revenue_id`), + INDEX `revenue_website_id_idx`(`website_id`), + INDEX `revenue_session_id_idx`(`session_id`), + INDEX `revenue_website_id_created_at_idx`(`website_id`, `created_at`), + INDEX `revenue_website_id_session_id_created_at_idx`(`website_id`, `session_id`, `created_at`), + PRIMARY KEY (`revenue_id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/db/mysql/schema.prisma b/db/mysql/schema.prisma index 2a5513ab..67bd24d2 100644 --- a/db/mysql/schema.prisma +++ b/db/mysql/schema.prisma @@ -43,6 +43,7 @@ model Session { websiteEvent WebsiteEvent[] sessionData SessionData[] + revenue Revenue[] @@index([createdAt]) @@index([websiteId]) @@ -76,7 +77,9 @@ model Website { team Team? @relation(fields: [teamId], references: [id]) eventData EventData[] report Report[] + revenue Revenue[] sessionData SessionData[] + segment Segment[] @@index([userId]) @@index([teamId]) @@ -215,10 +218,10 @@ model Report { id String @id() @unique() @map("report_id") @db.VarChar(36) userId String @map("user_id") @db.VarChar(36) websiteId String @map("website_id") @db.VarChar(36) - type String @map("type") @db.VarChar(200) - name String @map("name") @db.VarChar(200) - description String @map("description") @db.VarChar(500) - parameters String @map("parameters") @db.VarChar(6000) + type String @db.VarChar(200) + name String @db.VarChar(200) + description String @db.VarChar(500) + parameters Json createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0) @@ -231,3 +234,38 @@ model Report { @@index([name]) @@map("report") } + +model Segment { + id String @id() @unique() @map("segment_id") @db.VarChar(36) + websiteId String @map("website_id") @db.VarChar(36) + type String @db.VarChar(200) + name String @db.VarChar(200) + parameters Json + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) + updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0) + + website Website @relation(fields: [websiteId], references: [id]) + + @@index([websiteId]) + @@map("segment") +} + +model Revenue { + id String @id() @unique() @map("revenue_id") @db.VarChar(36) + websiteId String @map("website_id") @db.VarChar(36) + sessionId String @map("session_id") @db.VarChar(36) + eventId String @map("event_id") @db.VarChar(36) + eventName String @map("event_name") @db.VarChar(50) + currency String @db.VarChar(100) + revenue Decimal? @db.Decimal(19, 4) + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) + + website Website @relation(fields: [websiteId], references: [id]) + session Session @relation(fields: [sessionId], references: [id]) + + @@index([websiteId]) + @@index([sessionId]) + @@index([websiteId, createdAt]) + @@index([websiteId, sessionId, createdAt]) + @@map("revenue") +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a25a6594..bd8c46f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -248,7 +248,7 @@ importers: version: 14.2.30(eslint@8.57.1)(typescript@5.8.3) eslint-config-prettier: specifier: ^8.5.0 - version: 8.10.0(eslint@8.57.1) + version: 8.10.1(eslint@8.57.1) eslint-import-resolver-alias: specifier: ^1.1.2 version: 1.1.2(eslint-plugin-import@2.32.0) @@ -266,7 +266,7 @@ importers: version: 27.9.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)))(typescript@5.8.3) eslint-plugin-prettier: specifier: ^4.0.0 - version: 4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.1))(eslint@8.57.1)(prettier@2.8.8) + version: 4.2.3(eslint-config-prettier@8.10.1(eslint@8.57.1))(eslint@8.57.1)(prettier@2.8.8) extract-react-intl-messages: specifier: ^4.1.1 version: 4.1.1(ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.6)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)))(typescript@5.8.3)) @@ -1308,14 +1308,14 @@ packages: peerDependencies: '@dicebear/core': ^9.0.0 - '@emnapi/core@1.4.4': - resolution: {integrity: sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g==} + '@emnapi/core@1.4.5': + resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} - '@emnapi/runtime@1.4.4': - resolution: {integrity: sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==} + '@emnapi/runtime@1.4.5': + resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} - '@emnapi/wasi-threads@1.0.3': - resolution: {integrity: sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw==} + '@emnapi/wasi-threads@1.0.4': + resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} '@esbuild/aix-ppc64@0.25.6': resolution: {integrity: sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==} @@ -3377,8 +3377,8 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-to-chromium@1.5.186: - resolution: {integrity: sha512-lur7L4BFklgepaJxj4DqPk7vKbTEl0pajNlg2QjE5shefmlmBLm2HvQ7PMf1R/GvlevT/581cop33/quQcfX3A==} + electron-to-chromium@1.5.187: + resolution: {integrity: sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==} emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} @@ -3477,8 +3477,8 @@ packages: typescript: optional: true - eslint-config-prettier@8.10.0: - resolution: {integrity: sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==} + eslint-config-prettier@8.10.1: + resolution: {integrity: sha512-mXi3I2ghYZv02pKsUS5C2IRcYlOs3WFNYzYtLKX5s9mFju7BIAjxJqA4UG2qN2DAC0yUECdnfs5iGnyUdgOWzA==} hasBin: true peerDependencies: eslint: '>=7.0.0' @@ -3566,8 +3566,8 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - eslint-plugin-prettier@4.2.1: - resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} + eslint-plugin-prettier@4.2.3: + resolution: {integrity: sha512-HOT5QrFj0ioo/USxnMvj9+E1kwJHg7HDpY9sf6mxNqisHbSegMnmdan/wfUtIPVZ8hwcfGGEJpyG1/TpeR5R1g==} engines: {node: '>=12.0.0'} peerDependencies: eslint: '>=7.28.0' @@ -4796,8 +4796,8 @@ packages: mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} - mdn-data@2.22.1: - resolution: {integrity: sha512-u9Xnc9zLuF/CL2IHPow7HcXPpb8okQyzYpwL5wFsY//JRedSWYglYRg3PYWoQCu1zO+tBTmWOJN/iM0mPC5CRQ==} + mdn-data@2.23.0: + resolution: {integrity: sha512-786vq1+4079JSeu2XdcDjrhi/Ry7BWtjDl9WtGPWLiIHb2T66GvIVflZTBoSNZ5JqTtJGYEVMuFA/lbQlMOyDQ==} memoize-one@5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} @@ -4914,8 +4914,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - napi-postinstall@0.3.0: - resolution: {integrity: sha512-M7NqKyhODKV1gRLdkwE7pDsZP2/SC2a2vHkOYh9MCpKMbWVfyVfUw5MaH83Fv6XMjxr5jryUp3IDDL9rlxsTeA==} + napi-postinstall@0.3.1: + resolution: {integrity: sha512-is9eGpjKpRg+Z7ECny6NSOekea7+1eTs9+jTt5jJx43ez0o1BYRUKEBIf+ZZn15oQf5wvPnRf0Uat6MAtvVz+A==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} hasBin: true @@ -7840,18 +7840,18 @@ snapshots: dependencies: '@dicebear/core': 9.2.3 - '@emnapi/core@1.4.4': + '@emnapi/core@1.4.5': dependencies: - '@emnapi/wasi-threads': 1.0.3 + '@emnapi/wasi-threads': 1.0.4 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.4.4': + '@emnapi/runtime@1.4.5': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.0.3': + '@emnapi/wasi-threads@1.0.4': dependencies: tslib: 2.8.1 optional: true @@ -8180,7 +8180,7 @@ snapshots: '@img/sharp-wasm32@0.34.3': dependencies: - '@emnapi/runtime': 1.4.4 + '@emnapi/runtime': 1.4.5 optional: true '@img/sharp-win32-arm64@0.34.3': @@ -8407,8 +8407,8 @@ snapshots: '@napi-rs/wasm-runtime@0.2.12': dependencies: - '@emnapi/core': 1.4.4 - '@emnapi/runtime': 1.4.4 + '@emnapi/core': 1.4.5 + '@emnapi/runtime': 1.4.5 '@tybys/wasm-util': 0.10.0 optional: true @@ -9458,7 +9458,7 @@ snapshots: browserslist@4.25.1: dependencies: caniuse-lite: 1.0.30001727 - electron-to-chromium: 1.5.186 + electron-to-chromium: 1.5.187 node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.1) @@ -10119,7 +10119,7 @@ snapshots: dependencies: jake: 10.9.2 - electron-to-chromium@1.5.186: {} + electron-to-chromium@1.5.187: {} emittery@0.13.1: {} @@ -10311,7 +10311,7 @@ snapshots: - eslint-plugin-import-x - supports-color - eslint-config-prettier@8.10.0(eslint@8.57.1): + eslint-config-prettier@8.10.1(eslint@8.57.1): dependencies: eslint: 8.57.1 @@ -10423,13 +10423,13 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.1))(eslint@8.57.1)(prettier@2.8.8): + eslint-plugin-prettier@4.2.3(eslint-config-prettier@8.10.1(eslint@8.57.1))(eslint@8.57.1)(prettier@2.8.8): dependencies: eslint: 8.57.1 prettier: 2.8.8 prettier-linter-helpers: 1.0.0 optionalDependencies: - eslint-config-prettier: 8.10.0(eslint@8.57.1) + eslint-config-prettier: 8.10.1(eslint@8.57.1) eslint-plugin-promise@6.6.0(eslint@8.57.1): dependencies: @@ -11914,7 +11914,7 @@ snapshots: mdn-data@2.12.2: optional: true - mdn-data@2.22.1: + mdn-data@2.23.0: optional: true memoize-one@5.2.1: {} @@ -12029,7 +12029,7 @@ snapshots: nanoid@3.3.11: {} - napi-postinstall@0.3.0: {} + napi-postinstall@0.3.1: {} natural-compare@1.4.0: {} @@ -13465,7 +13465,7 @@ snapshots: css-tree: 3.1.0 is-plain-object: 5.0.0 known-css-properties: 0.36.0 - mdn-data: 2.22.1 + mdn-data: 2.23.0 postcss-media-query-parser: 0.2.3 postcss-resolve-nested-selector: 0.1.6 postcss-selector-parser: 7.1.0 @@ -13789,7 +13789,7 @@ snapshots: unrs-resolver@1.11.1: dependencies: - napi-postinstall: 0.3.0 + napi-postinstall: 0.3.1 optionalDependencies: '@unrs/resolver-binding-android-arm-eabi': 1.11.1 '@unrs/resolver-binding-android-arm64': 1.11.1 diff --git a/scripts/data-migrations/populate-revenue-table.sql b/scripts/data-migrations/populate-revenue-table.sql new file mode 100644 index 00000000..9df75189 --- /dev/null +++ b/scripts/data-migrations/populate-revenue-table.sql @@ -0,0 +1,41 @@ +----------------------------------------------------- +-- PostgreSQL +----------------------------------------------------- +INSERT INTO "revenue" +SELECT gen_random_uuid() revenue_id, + ed.website_id, + we.session_id, + we.event_id, + we.event_name, + currency.string_value currency, + coalesce(ed.number_value, cast(ed.string_value as numeric(19,4))) revenue, + ed.created_at +FROM event_data ed +JOIN website_event we +ON we.event_id = ed.website_event_id +JOIN (SELECT website_event_id, string_value + FROM event_data + WHERE data_key ilike '%currency%') currency +ON currency.website_event_id = ed.website_event_id +WHERE ed.data_key ilike '%revenue%'; + +----------------------------------------------------- +-- MySQL +----------------------------------------------------- +INSERT INTO `revenue` +SELECT UUID() revenue_id, + ed.website_id, + we.session_id, + we.event_id, + we.event_name, + currency.string_value currency, + coalesce(ed.number_value, cast(ed.string_value as decimal(19,4))) revenue, + ed.created_at +FROM event_data ed +JOIN website_event we +ON we.event_id = ed.website_event_id +JOIN (SELECT website_event_id, string_value + FROM event_data + WHERE data_key like '%currency%') currency +ON currency.website_event_id = ed.website_event_id +WHERE ed.data_key like '%revenue%'; \ No newline at end of file From 60c310fd7732efb0ae3937eaf08bdae41791c13a Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 21 Jul 2025 00:46:12 -0700 Subject: [PATCH 3/9] Apply suggestion from @greptile-apps[bot] Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- src/app/(main)/websites/[websiteId]/events/EventsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx index 259e77be..46970b51 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx @@ -20,7 +20,7 @@ export default function EventsPage({ websiteId }) { setLabel(value !== label ? value : ''); }; - const onSelect = (value: any) => { + const onSelect = (value: 'activity' | 'properties') => { setItem('eventTab', value); setTab(value); }; From a81b144cd66abe0b668e0b545f8c0209054d341a Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Mon, 21 Jul 2025 10:49:48 -0700 Subject: [PATCH 4/9] fix get event metrics card and create it's own query --- .../[websiteId]/events/series/route.ts | 4 +- .../api/websites/[websiteId]/metrics/route.ts | 15 ++- src/queries/index.ts | 1 + src/queries/sql/events/getEventMetrics.ts | 100 +++++++++--------- src/queries/sql/events/getEventStats.ts | 92 ++++++++++++++++ 5 files changed, 157 insertions(+), 55 deletions(-) create mode 100644 src/queries/sql/events/getEventStats.ts diff --git a/src/app/api/websites/[websiteId]/events/series/route.ts b/src/app/api/websites/[websiteId]/events/series/route.ts index 5b5bc88c..a30741c6 100644 --- a/src/app/api/websites/[websiteId]/events/series/route.ts +++ b/src/app/api/websites/[websiteId]/events/series/route.ts @@ -3,7 +3,7 @@ import { parseRequest, getRequestDateRange, getRequestFilters } from '@/lib/requ import { unauthorized, json } from '@/lib/response'; import { canViewWebsite } from '@/lib/auth'; import { filterParams, timezoneParam, unitParam } from '@/lib/schema'; -import { getEventMetrics } from '@/queries'; +import { getEventStats } from '@/queries'; export async function GET( request: Request, @@ -39,7 +39,7 @@ export async function GET( unit, }; - const data = await getEventMetrics(websiteId, filters); + const data = await getEventStats(websiteId, filters); return json(data); } diff --git a/src/app/api/websites/[websiteId]/metrics/route.ts b/src/app/api/websites/[websiteId]/metrics/route.ts index 5bc4e522..488f85fc 100644 --- a/src/app/api/websites/[websiteId]/metrics/route.ts +++ b/src/app/api/websites/[websiteId]/metrics/route.ts @@ -15,7 +15,12 @@ import { } from '@/lib/constants'; import { getRequestFilters, getRequestDateRange, parseRequest } from '@/lib/request'; import { json, unauthorized, badRequest } from '@/lib/response'; -import { getPageviewMetrics, getSessionMetrics, getChannelMetrics } from '@/queries'; +import { + getPageviewMetrics, + getSessionMetrics, + getEventMetrics, + getChannelMetrics, +} from '@/queries'; import { filterParams } from '@/lib/schema'; export async function GET( @@ -85,7 +90,13 @@ export async function GET( } if (EVENT_COLUMNS.includes(type)) { - const data = await getPageviewMetrics(websiteId, type, filters, limit, offset); + let data; + + if (type === 'event') { + data = await getEventMetrics(websiteId, type, filters, limit, offset); + } else { + data = await getPageviewMetrics(websiteId, type, filters, limit, offset); + } return json(data); } diff --git a/src/queries/index.ts b/src/queries/index.ts index b9495bcd..0d57b294 100644 --- a/src/queries/index.ts +++ b/src/queries/index.ts @@ -11,6 +11,7 @@ export * from '@/queries/sql/events/getEventDataValues'; export * from '@/queries/sql/events/getEventDataStats'; export * from '@/queries/sql/events/getEventDataUsage'; export * from '@/queries/sql/events/getEventMetrics'; +export * from '@/queries/sql/events/getEventStats'; export * from '@/queries/sql/events/getWebsiteEvents'; export * from '@/queries/sql/events/getEventUsage'; export * from '@/queries/sql/events/saveEvent'; diff --git a/src/queries/sql/events/getEventMetrics.ts b/src/queries/sql/events/getEventMetrics.ts index 42dc8862..b028d759 100644 --- a/src/queries/sql/events/getEventMetrics.ts +++ b/src/queries/sql/events/getEventMetrics.ts @@ -1,32 +1,46 @@ import clickhouse from '@/lib/clickhouse'; -import { EVENT_TYPE } from '@/lib/constants'; +import { EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import prisma from '@/lib/prisma'; -import { QueryFilters, WebsiteEventMetric } from '@/lib/types'; +import { QueryFilters } from '@/lib/types'; export async function getEventMetrics( - ...args: [websiteId: string, filters: QueryFilters] -): Promise { + ...args: [ + websiteId: string, + type: string, + filters: QueryFilters, + limit?: number | string, + offset?: number | string, + ] +) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), }); } -async function relationalQuery(websiteId: string, filters: QueryFilters) { - const { timezone = 'utc', unit = 'day' } = filters; - const { rawQuery, getDateSQL, parseFilters } = prisma; - const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(websiteId, { - ...filters, - eventType: EVENT_TYPE.customEvent, - }); +async function relationalQuery( + websiteId: string, + type: string, + filters: QueryFilters, + limit: number | string = 500, + offset: number | string = 0, +) { + const column = FILTER_COLUMNS[type] || type; + const { rawQuery, parseFilters } = prisma; + const { filterQuery, cohortQuery, joinSession, params } = await parseFilters( + websiteId, + { + ...filters, + eventType: EVENT_TYPE.customEvent, + }, + { joinSession: SESSION_COLUMNS.includes(type) }, + ); return rawQuery( ` - select - event_name x, - ${getDateSQL('website_event.created_at', unit, timezone)} t, - count(*) y + select ${column} x, + count(*) as y from website_event ${cohortQuery} ${joinSession} @@ -34,8 +48,10 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { and website_event.created_at between {{startDate}} and {{endDate}} and event_type = {{eventType}} ${filterQuery} - group by 1, 2 - order by 2 + group by 1 + order by 2 desc + limit ${limit} + offset ${offset} `, params, ); @@ -43,50 +59,32 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { async function clickhouseQuery( websiteId: string, + type: string, filters: QueryFilters, -): Promise<{ x: string; t: string; y: number }[]> { - const { timezone = 'UTC', unit = 'day' } = filters; - const { rawQuery, getDateSQL, parseFilters } = clickhouse; + limit: number | string = 500, + offset: number | string = 0, +): Promise<{ x: string; y: number }[]> { + const column = FILTER_COLUMNS[type] || type; + const { rawQuery, parseFilters } = clickhouse; const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, { ...filters, eventType: EVENT_TYPE.customEvent, }); - let sql = ''; - - if (filterQuery || cohortQuery) { - sql = ` - select - event_name x, - ${getDateSQL('created_at', unit, timezone)} t, - count(*) y + return rawQuery( + `select ${column} x, + count(*) as y from website_event ${cohortQuery} where website_id = {websiteId:UUID} and created_at between {startDate:DateTime64} and {endDate:DateTime64} and event_type = {eventType:UInt32} ${filterQuery} - group by x, t - order by t - `; - } else { - sql = ` - select - event_name x, - ${getDateSQL('created_at', unit, timezone)} t, - count(*) y - from ( - select arrayJoin(event_name) as event_name, - created_at - from website_event_stats_hourly website_event - where website_id = {websiteId:UUID} - and created_at between {startDate:DateTime64} and {endDate:DateTime64} - and event_type = {eventType:UInt32} - ) as g - group by x, t - order by t - `; - } - - return rawQuery(sql, params); + group by x + order by y desc + limit ${limit} + offset ${offset} + `, + params, + ); } diff --git a/src/queries/sql/events/getEventStats.ts b/src/queries/sql/events/getEventStats.ts new file mode 100644 index 00000000..56f31363 --- /dev/null +++ b/src/queries/sql/events/getEventStats.ts @@ -0,0 +1,92 @@ +import clickhouse from '@/lib/clickhouse'; +import { EVENT_TYPE } from '@/lib/constants'; +import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; +import prisma from '@/lib/prisma'; +import { QueryFilters, WebsiteEventMetric } from '@/lib/types'; + +export async function getEventStats( + ...args: [websiteId: string, filters: QueryFilters] +): Promise { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery(websiteId: string, filters: QueryFilters) { + const { timezone = 'utc', unit = 'day' } = filters; + const { rawQuery, getDateSQL, parseFilters } = prisma; + const { filterQuery, cohortQuery, joinSession, params } = await parseFilters(websiteId, { + ...filters, + eventType: EVENT_TYPE.customEvent, + }); + + return rawQuery( + ` + select + event_name x, + ${getDateSQL('website_event.created_at', unit, timezone)} t, + count(*) y + from website_event + ${cohortQuery} + ${joinSession} + where website_event.website_id = {{websiteId::uuid}} + and website_event.created_at between {{startDate}} and {{endDate}} + and event_type = {{eventType}} + ${filterQuery} + group by 1, 2 + order by 2 + `, + params, + ); +} + +async function clickhouseQuery( + websiteId: string, + filters: QueryFilters, +): Promise<{ x: string; t: string; y: number }[]> { + const { timezone = 'UTC', unit = 'day' } = filters; + const { rawQuery, getDateSQL, parseFilters } = clickhouse; + const { filterQuery, cohortQuery, params } = await parseFilters(websiteId, { + ...filters, + eventType: EVENT_TYPE.customEvent, + }); + + let sql = ''; + + if (filterQuery || cohortQuery) { + sql = ` + select + event_name x, + ${getDateSQL('created_at', unit, timezone)} t, + count(*) y + from website_event + ${cohortQuery} + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_type = {eventType:UInt32} + ${filterQuery} + group by x, t + order by t + `; + } else { + sql = ` + select + event_name x, + ${getDateSQL('created_at', unit, timezone)} t, + count(*) y + from ( + select arrayJoin(event_name) as event_name, + created_at + from website_event_stats_hourly website_event + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_type = {eventType:UInt32} + ) as g + group by x, t + order by t + `; + } + + return rawQuery(sql, params); +} From 58284453131f26eb747ebeb5532d26114870a955 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 21 Jul 2025 10:58:58 -0700 Subject: [PATCH 5/9] Updated packages. --- package.json | 5 +- pnpm-lock.yaml | 458 ++++++++++++++++++++++++++++--------------------- 2 files changed, 267 insertions(+), 196 deletions(-) diff --git a/package.json b/package.json index 448fcb10..26ac6683 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "2.18.1", + "version": "2.19.0", "description": "A modern, privacy-focused alternative to Google Analytics.", "author": "Umami Software, Inc. ", "license": "MIT", @@ -11,7 +11,7 @@ }, "scripts": { "dev": "next dev", - "dev-turbo": "next dev -p 3001 --turbopack", + "dev-turbo": "next dev -p 3000 --turbopack", "build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app", "start": "next start", "build-docker": "npm-run-all build-db build-tracker build-geo build-app", @@ -100,6 +100,7 @@ "is-localhost-ip": "^1.4.0", "isbot": "^5.1.16", "jsonwebtoken": "^9.0.2", + "jszip": "^3.10.1", "kafkajs": "^2.1.0", "maxmind": "^4.3.24", "md5": "^2.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd8c46f0..10fdbff0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,6 +110,9 @@ importers: jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 + jszip: + specifier: ^3.10.1 + version: 3.10.1 kafkajs: specifier: ^2.1.0 version: 2.2.4 @@ -182,7 +185,7 @@ importers: devDependencies: '@formatjs/cli': specifier: ^4.2.29 - version: 4.8.4(ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.6)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)))(typescript@5.8.3)) + version: 4.8.4(ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.8)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)))(typescript@5.8.3)) '@netlify/plugin-nextjs': specifier: ^5.10.6 version: 5.11.6 @@ -215,7 +218,7 @@ importers: version: 29.5.14 '@types/node': specifier: ^22.13.4 - version: 22.16.4 + version: 22.16.5 '@types/react': specifier: ^19.0.8 version: 19.1.8 @@ -239,7 +242,7 @@ importers: version: 13.17.0 esbuild: specifier: ^0.25.0 - version: 0.25.6 + version: 0.25.8 eslint: specifier: ^8.33.0 version: 8.57.1 @@ -248,7 +251,7 @@ importers: version: 14.2.30(eslint@8.57.1)(typescript@5.8.3) eslint-config-prettier: specifier: ^8.5.0 - version: 8.10.1(eslint@8.57.1) + version: 8.10.2(eslint@8.57.1) eslint-import-resolver-alias: specifier: ^1.1.2 version: 1.1.2(eslint-plugin-import@2.32.0) @@ -263,19 +266,19 @@ importers: version: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-jest: specifier: ^27.9.0 - version: 27.9.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)))(typescript@5.8.3) + version: 27.9.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(jest@29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)))(typescript@5.8.3) eslint-plugin-prettier: specifier: ^4.0.0 - version: 4.2.3(eslint-config-prettier@8.10.1(eslint@8.57.1))(eslint@8.57.1)(prettier@2.8.8) + version: 4.2.5(eslint-config-prettier@8.10.2(eslint@8.57.1))(eslint@8.57.1)(prettier@2.8.8) extract-react-intl-messages: specifier: ^4.1.1 - version: 4.1.1(ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.6)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)))(typescript@5.8.3)) + version: 4.1.1(ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.8)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)))(typescript@5.8.3)) husky: specifier: ^8.0.3 version: 8.0.3 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)) + version: 29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)) lint-staged: specifier: ^14.0.1 version: 14.0.1(enquirer@2.4.1) @@ -311,13 +314,13 @@ importers: version: 5.3.1(rollup@3.29.5)(typescript@5.8.3) rollup-plugin-esbuild: specifier: ^5.0.0 - version: 5.0.0(esbuild@0.25.6)(rollup@3.29.5) + version: 5.0.0(esbuild@0.25.8)(rollup@3.29.5) rollup-plugin-node-externals: specifier: ^6.1.1 version: 6.1.2(rollup@3.29.5) rollup-plugin-postcss: specifier: ^4.0.2 - version: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)) + version: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)) stylelint: specifier: ^15.10.1 version: 15.11.0(typescript@5.8.3) @@ -335,10 +338,10 @@ importers: version: 6.2.1 ts-jest: specifier: ^29.1.2 - version: 29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.6)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)))(typescript@5.8.3) + version: 29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.8)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)))(typescript@5.8.3) ts-node: specifier: ^10.9.1 - version: 10.9.2(@types/node@22.16.4)(typescript@5.8.3) + version: 10.9.2(@types/node@22.16.5)(typescript@5.8.3) typescript: specifier: ^5.5.3 version: 5.8.3 @@ -1317,158 +1320,158 @@ packages: '@emnapi/wasi-threads@1.0.4': resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} - '@esbuild/aix-ppc64@0.25.6': - resolution: {integrity: sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==} + '@esbuild/aix-ppc64@0.25.8': + resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.6': - resolution: {integrity: sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==} + '@esbuild/android-arm64@0.25.8': + resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.6': - resolution: {integrity: sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==} + '@esbuild/android-arm@0.25.8': + resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.6': - resolution: {integrity: sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==} + '@esbuild/android-x64@0.25.8': + resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.6': - resolution: {integrity: sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==} + '@esbuild/darwin-arm64@0.25.8': + resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.6': - resolution: {integrity: sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==} + '@esbuild/darwin-x64@0.25.8': + resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.6': - resolution: {integrity: sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==} + '@esbuild/freebsd-arm64@0.25.8': + resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.6': - resolution: {integrity: sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==} + '@esbuild/freebsd-x64@0.25.8': + resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.6': - resolution: {integrity: sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==} + '@esbuild/linux-arm64@0.25.8': + resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.6': - resolution: {integrity: sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==} + '@esbuild/linux-arm@0.25.8': + resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.6': - resolution: {integrity: sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==} + '@esbuild/linux-ia32@0.25.8': + resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.6': - resolution: {integrity: sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==} + '@esbuild/linux-loong64@0.25.8': + resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.6': - resolution: {integrity: sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==} + '@esbuild/linux-mips64el@0.25.8': + resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.6': - resolution: {integrity: sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==} + '@esbuild/linux-ppc64@0.25.8': + resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.6': - resolution: {integrity: sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==} + '@esbuild/linux-riscv64@0.25.8': + resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.6': - resolution: {integrity: sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==} + '@esbuild/linux-s390x@0.25.8': + resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.6': - resolution: {integrity: sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==} + '@esbuild/linux-x64@0.25.8': + resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.6': - resolution: {integrity: sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==} + '@esbuild/netbsd-arm64@0.25.8': + resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.6': - resolution: {integrity: sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==} + '@esbuild/netbsd-x64@0.25.8': + resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.6': - resolution: {integrity: sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==} + '@esbuild/openbsd-arm64@0.25.8': + resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.6': - resolution: {integrity: sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==} + '@esbuild/openbsd-x64@0.25.8': + resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.6': - resolution: {integrity: sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==} + '@esbuild/openharmony-arm64@0.25.8': + resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.6': - resolution: {integrity: sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==} + '@esbuild/sunos-x64@0.25.8': + resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.6': - resolution: {integrity: sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==} + '@esbuild/win32-arm64@0.25.8': + resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.6': - resolution: {integrity: sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==} + '@esbuild/win32-ia32@0.25.8': + resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.6': - resolution: {integrity: sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==} + '@esbuild/win32-x64@0.25.8': + resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -2243,8 +2246,8 @@ packages: '@types/node@14.18.63': resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} - '@types/node@22.16.4': - resolution: {integrity: sha512-PYRhNtZdm2wH/NT2k/oAJ6/f2VD2N2Dag0lGlx2vWgMSJXGNmlce5MiTQzoWAiIJtso30mjnfQCOKVH+kAQC/g==} + '@types/node@22.16.5': + resolution: {integrity: sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -3447,8 +3450,8 @@ packages: peerDependencies: esbuild: '>=0.12 <1' - esbuild@0.25.6: - resolution: {integrity: sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==} + esbuild@0.25.8: + resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} engines: {node: '>=18'} hasBin: true @@ -3477,8 +3480,8 @@ packages: typescript: optional: true - eslint-config-prettier@8.10.1: - resolution: {integrity: sha512-mXi3I2ghYZv02pKsUS5C2IRcYlOs3WFNYzYtLKX5s9mFju7BIAjxJqA4UG2qN2DAC0yUECdnfs5iGnyUdgOWzA==} + eslint-config-prettier@8.10.2: + resolution: {integrity: sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==} hasBin: true peerDependencies: eslint: '>=7.0.0' @@ -3566,8 +3569,8 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - eslint-plugin-prettier@4.2.3: - resolution: {integrity: sha512-HOT5QrFj0ioo/USxnMvj9+E1kwJHg7HDpY9sf6mxNqisHbSegMnmdan/wfUtIPVZ8hwcfGGEJpyG1/TpeR5R1g==} + eslint-plugin-prettier@4.2.5: + resolution: {integrity: sha512-9Ni+xgemM2IWLq6aXEpP2+V/V30GeA/46Ar629vcMqVPodFFWC9skHu/D1phvuqtS8bJCFnNf01/qcmqYEwNfg==} engines: {node: '>=12.0.0'} peerDependencies: eslint: '>=7.28.0' @@ -4041,6 +4044,9 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immer@9.0.21: resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} @@ -4299,6 +4305,9 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -4567,6 +4576,9 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + jwa@1.4.2: resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} @@ -4613,6 +4625,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -4914,8 +4929,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - napi-postinstall@0.3.1: - resolution: {integrity: sha512-is9eGpjKpRg+Z7ECny6NSOekea7+1eTs9+jTt5jJx43ez0o1BYRUKEBIf+ZZn15oQf5wvPnRf0Uat6MAtvVz+A==} + napi-postinstall@0.3.2: + resolution: {integrity: sha512-tWVJxJHmBWLy69PvO96TZMZDrzmw5KeiZBz3RHmiM2XZ9grBJ2WgMAFVVg25nqp3ZjTFUs2Ftw1JhscL3Teliw==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} hasBin: true @@ -5089,6 +5104,9 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -5632,6 +5650,9 @@ packages: typescript: optional: true + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} @@ -5785,6 +5806,9 @@ packages: resolution: {integrity: sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==} engines: {node: '>=12'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -5941,6 +5965,9 @@ packages: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -5997,6 +6024,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + sharp@0.34.3: resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -6175,6 +6205,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -7856,82 +7889,82 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.25.6': + '@esbuild/aix-ppc64@0.25.8': optional: true - '@esbuild/android-arm64@0.25.6': + '@esbuild/android-arm64@0.25.8': optional: true - '@esbuild/android-arm@0.25.6': + '@esbuild/android-arm@0.25.8': optional: true - '@esbuild/android-x64@0.25.6': + '@esbuild/android-x64@0.25.8': optional: true - '@esbuild/darwin-arm64@0.25.6': + '@esbuild/darwin-arm64@0.25.8': optional: true - '@esbuild/darwin-x64@0.25.6': + '@esbuild/darwin-x64@0.25.8': optional: true - '@esbuild/freebsd-arm64@0.25.6': + '@esbuild/freebsd-arm64@0.25.8': optional: true - '@esbuild/freebsd-x64@0.25.6': + '@esbuild/freebsd-x64@0.25.8': optional: true - '@esbuild/linux-arm64@0.25.6': + '@esbuild/linux-arm64@0.25.8': optional: true - '@esbuild/linux-arm@0.25.6': + '@esbuild/linux-arm@0.25.8': optional: true - '@esbuild/linux-ia32@0.25.6': + '@esbuild/linux-ia32@0.25.8': optional: true - '@esbuild/linux-loong64@0.25.6': + '@esbuild/linux-loong64@0.25.8': optional: true - '@esbuild/linux-mips64el@0.25.6': + '@esbuild/linux-mips64el@0.25.8': optional: true - '@esbuild/linux-ppc64@0.25.6': + '@esbuild/linux-ppc64@0.25.8': optional: true - '@esbuild/linux-riscv64@0.25.6': + '@esbuild/linux-riscv64@0.25.8': optional: true - '@esbuild/linux-s390x@0.25.6': + '@esbuild/linux-s390x@0.25.8': optional: true - '@esbuild/linux-x64@0.25.6': + '@esbuild/linux-x64@0.25.8': optional: true - '@esbuild/netbsd-arm64@0.25.6': + '@esbuild/netbsd-arm64@0.25.8': optional: true - '@esbuild/netbsd-x64@0.25.6': + '@esbuild/netbsd-x64@0.25.8': optional: true - '@esbuild/openbsd-arm64@0.25.6': + '@esbuild/openbsd-arm64@0.25.8': optional: true - '@esbuild/openbsd-x64@0.25.6': + '@esbuild/openbsd-x64@0.25.8': optional: true - '@esbuild/openharmony-arm64@0.25.6': + '@esbuild/openharmony-arm64@0.25.8': optional: true - '@esbuild/sunos-x64@0.25.6': + '@esbuild/sunos-x64@0.25.8': optional: true - '@esbuild/win32-arm64@0.25.6': + '@esbuild/win32-arm64@0.25.8': optional: true - '@esbuild/win32-ia32@0.25.6': + '@esbuild/win32-ia32@0.25.8': optional: true - '@esbuild/win32-x64@0.25.6': + '@esbuild/win32-x64@0.25.8': optional: true '@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)': @@ -7959,10 +7992,10 @@ snapshots: '@fontsource/inter@4.5.15': {} - '@formatjs/cli@4.8.4(ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.6)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)))(typescript@5.8.3))': + '@formatjs/cli@4.8.4(ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.8)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)))(typescript@5.8.3))': dependencies: '@formatjs/icu-messageformat-parser': 2.1.0 - '@formatjs/ts-transformer': 3.9.4(ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.6)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)))(typescript@5.8.3)) + '@formatjs/ts-transformer': 3.9.4(ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.8)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)))(typescript@5.8.3)) '@types/estree': 0.0.50 '@types/fs-extra': 9.0.13 '@types/json-stable-stringify': 1.2.0 @@ -8062,15 +8095,15 @@ snapshots: optionalDependencies: typescript: 5.8.3 - '@formatjs/ts-transformer@2.13.0(ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.6)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)))(typescript@5.8.3))': + '@formatjs/ts-transformer@2.13.0(ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.8)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)))(typescript@5.8.3))': dependencies: intl-messageformat-parser: 6.1.2 tslib: 2.8.1 typescript: 4.9.5 optionalDependencies: - ts-jest: 29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.6)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)))(typescript@5.8.3) + ts-jest: 29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.8)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)))(typescript@5.8.3) - '@formatjs/ts-transformer@3.9.4(ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.6)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)))(typescript@5.8.3))': + '@formatjs/ts-transformer@3.9.4(ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.8)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)))(typescript@5.8.3))': dependencies: '@formatjs/icu-messageformat-parser': 2.1.0 '@types/node': 14.18.63 @@ -8078,7 +8111,7 @@ snapshots: tslib: 2.8.1 typescript: 4.9.5 optionalDependencies: - ts-jest: 29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.6)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)))(typescript@5.8.3) + ts-jest: 29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.8)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)))(typescript@5.8.3) '@hello-pangea/dnd@17.0.0(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: @@ -8220,27 +8253,27 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 22.16.4 + '@types/node': 22.16.5 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3))': + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.16.4 + '@types/node': 22.16.5 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)) + jest-config: 29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -8265,7 +8298,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.16.4 + '@types/node': 22.16.5 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -8283,7 +8316,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.16.4 + '@types/node': 22.16.5 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -8305,7 +8338,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.29 - '@types/node': 22.16.4 + '@types/node': 22.16.5 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -8375,7 +8408,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.16.4 + '@types/node': 22.16.5 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -8468,8 +8501,8 @@ snapshots: '@prisma/config@6.7.0': dependencies: - esbuild: 0.25.6 - esbuild-register: 3.6.0(esbuild@0.25.6) + esbuild: 0.25.8 + esbuild-register: 3.6.0(esbuild@0.25.8) transitivePeerDependencies: - supports-color @@ -8788,20 +8821,20 @@ snapshots: '@types/fs-extra@8.1.5': dependencies: - '@types/node': 22.16.4 + '@types/node': 22.16.5 '@types/fs-extra@9.0.13': dependencies: - '@types/node': 22.16.4 + '@types/node': 22.16.5 '@types/glob@7.2.0': dependencies: '@types/minimatch': 6.0.0 - '@types/node': 22.16.4 + '@types/node': 22.16.5 '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.16.4 + '@types/node': 22.16.5 '@types/hoist-non-react-statics@3.3.6': dependencies: @@ -8839,7 +8872,7 @@ snapshots: '@types/node@14.18.63': {} - '@types/node@22.16.4': + '@types/node@22.16.5': dependencies: undici-types: 6.21.0 @@ -8888,7 +8921,7 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 22.16.4 + '@types/node': 22.16.5 optional: true '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)': @@ -9383,12 +9416,12 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-react-intl@7.9.4(ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.6)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)))(typescript@5.8.3)): + babel-plugin-react-intl@7.9.4(ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.8)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)))(typescript@5.8.3)): dependencies: '@babel/core': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 '@babel/types': 7.28.1 - '@formatjs/ts-transformer': 2.13.0(ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.6)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)))(typescript@5.8.3)) + '@formatjs/ts-transformer': 2.13.0(ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.8)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)))(typescript@5.8.3)) '@types/babel__core': 7.20.5 '@types/fs-extra': 9.0.13 '@types/schema-utils': 2.4.0 @@ -9688,13 +9721,13 @@ snapshots: optionalDependencies: typescript: 5.8.3 - create-jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)): + create-jest@29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)) + jest-config: 29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -10247,41 +10280,41 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild-register@3.6.0(esbuild@0.25.6): + esbuild-register@3.6.0(esbuild@0.25.8): dependencies: debug: 4.4.1(supports-color@8.1.1) - esbuild: 0.25.6 + esbuild: 0.25.8 transitivePeerDependencies: - supports-color - esbuild@0.25.6: + esbuild@0.25.8: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.6 - '@esbuild/android-arm': 0.25.6 - '@esbuild/android-arm64': 0.25.6 - '@esbuild/android-x64': 0.25.6 - '@esbuild/darwin-arm64': 0.25.6 - '@esbuild/darwin-x64': 0.25.6 - '@esbuild/freebsd-arm64': 0.25.6 - '@esbuild/freebsd-x64': 0.25.6 - '@esbuild/linux-arm': 0.25.6 - '@esbuild/linux-arm64': 0.25.6 - '@esbuild/linux-ia32': 0.25.6 - '@esbuild/linux-loong64': 0.25.6 - '@esbuild/linux-mips64el': 0.25.6 - '@esbuild/linux-ppc64': 0.25.6 - '@esbuild/linux-riscv64': 0.25.6 - '@esbuild/linux-s390x': 0.25.6 - '@esbuild/linux-x64': 0.25.6 - '@esbuild/netbsd-arm64': 0.25.6 - '@esbuild/netbsd-x64': 0.25.6 - '@esbuild/openbsd-arm64': 0.25.6 - '@esbuild/openbsd-x64': 0.25.6 - '@esbuild/openharmony-arm64': 0.25.6 - '@esbuild/sunos-x64': 0.25.6 - '@esbuild/win32-arm64': 0.25.6 - '@esbuild/win32-ia32': 0.25.6 - '@esbuild/win32-x64': 0.25.6 + '@esbuild/aix-ppc64': 0.25.8 + '@esbuild/android-arm': 0.25.8 + '@esbuild/android-arm64': 0.25.8 + '@esbuild/android-x64': 0.25.8 + '@esbuild/darwin-arm64': 0.25.8 + '@esbuild/darwin-x64': 0.25.8 + '@esbuild/freebsd-arm64': 0.25.8 + '@esbuild/freebsd-x64': 0.25.8 + '@esbuild/linux-arm': 0.25.8 + '@esbuild/linux-arm64': 0.25.8 + '@esbuild/linux-ia32': 0.25.8 + '@esbuild/linux-loong64': 0.25.8 + '@esbuild/linux-mips64el': 0.25.8 + '@esbuild/linux-ppc64': 0.25.8 + '@esbuild/linux-riscv64': 0.25.8 + '@esbuild/linux-s390x': 0.25.8 + '@esbuild/linux-x64': 0.25.8 + '@esbuild/netbsd-arm64': 0.25.8 + '@esbuild/netbsd-x64': 0.25.8 + '@esbuild/openbsd-arm64': 0.25.8 + '@esbuild/openbsd-x64': 0.25.8 + '@esbuild/openharmony-arm64': 0.25.8 + '@esbuild/sunos-x64': 0.25.8 + '@esbuild/win32-arm64': 0.25.8 + '@esbuild/win32-ia32': 0.25.8 + '@esbuild/win32-x64': 0.25.8 escalade@3.2.0: {} @@ -10311,7 +10344,7 @@ snapshots: - eslint-plugin-import-x - supports-color - eslint-config-prettier@8.10.1(eslint@8.57.1): + eslint-config-prettier@8.10.2(eslint@8.57.1): dependencies: eslint: 8.57.1 @@ -10393,13 +10426,13 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)))(typescript@5.8.3): + eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(jest@29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)))(typescript@5.8.3): dependencies: '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.8.3) eslint: 8.57.1 optionalDependencies: '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) - jest: 29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)) + jest: 29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)) transitivePeerDependencies: - supports-color - typescript @@ -10423,13 +10456,13 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-prettier@4.2.3(eslint-config-prettier@8.10.1(eslint@8.57.1))(eslint@8.57.1)(prettier@2.8.8): + eslint-plugin-prettier@4.2.5(eslint-config-prettier@8.10.2(eslint@8.57.1))(eslint@8.57.1)(prettier@2.8.8): dependencies: eslint: 8.57.1 prettier: 2.8.8 prettier-linter-helpers: 1.0.0 optionalDependencies: - eslint-config-prettier: 8.10.1(eslint@8.57.1) + eslint-config-prettier: 8.10.2(eslint@8.57.1) eslint-plugin-promise@6.6.0(eslint@8.57.1): dependencies: @@ -10600,10 +10633,10 @@ snapshots: extend@3.0.2: {} - extract-react-intl-messages@4.1.1(ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.6)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)))(typescript@5.8.3)): + extract-react-intl-messages@4.1.1(ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.8)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)))(typescript@5.8.3)): dependencies: '@babel/core': 7.28.0 - babel-plugin-react-intl: 7.9.4(ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.6)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)))(typescript@5.8.3)) + babel-plugin-react-intl: 7.9.4(ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.8)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)))(typescript@5.8.3)) flat: 5.0.2 glob: 7.2.3 js-yaml: 3.14.1 @@ -10986,6 +11019,8 @@ snapshots: ignore@5.3.2: {} + immediate@3.0.6: {} + immer@9.0.21: {} import-cwd@3.0.0: @@ -11214,6 +11249,8 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + isarray@1.0.0: {} + isarray@2.0.5: {} isbot@5.1.28: {} @@ -11297,7 +11334,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.16.4 + '@types/node': 22.16.5 chalk: 4.1.2 co: 4.6.0 dedent: 1.6.0 @@ -11317,16 +11354,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)): + jest-cli@29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)) + create-jest: 29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)) + jest-config: 29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -11336,7 +11373,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)): + jest-config@29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)): dependencies: '@babel/core': 7.28.0 '@jest/test-sequencer': 29.7.0 @@ -11361,8 +11398,8 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 22.16.4 - ts-node: 10.9.2(@types/node@22.16.4)(typescript@5.8.3) + '@types/node': 22.16.5 + ts-node: 10.9.2(@types/node@22.16.5)(typescript@5.8.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -11391,7 +11428,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.16.4 + '@types/node': 22.16.5 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -11401,7 +11438,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 22.16.4 + '@types/node': 22.16.5 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -11440,7 +11477,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.16.4 + '@types/node': 22.16.5 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -11475,7 +11512,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.16.4 + '@types/node': 22.16.5 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -11503,7 +11540,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.16.4 + '@types/node': 22.16.5 chalk: 4.1.2 cjs-module-lexer: 1.4.3 collect-v8-coverage: 1.0.2 @@ -11549,7 +11586,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.16.4 + '@types/node': 22.16.5 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -11568,7 +11605,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.16.4 + '@types/node': 22.16.5 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -11577,17 +11614,17 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 22.16.4 + '@types/node': 22.16.5 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)): + jest@29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)) + jest-cli: 29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -11684,6 +11721,13 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + jwa@1.4.2: dependencies: buffer-equal-constant-time: 1.0.1 @@ -11725,6 +11769,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lilconfig@2.1.0: {} lines-and-columns@1.2.4: {} @@ -12029,7 +12077,7 @@ snapshots: nanoid@3.3.11: {} - napi-postinstall@0.3.1: {} + napi-postinstall@0.3.2: {} natural-compare@1.4.0: {} @@ -12225,6 +12273,8 @@ snapshots: p-try@2.2.0: {} + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -12428,13 +12478,13 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)): + postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)): dependencies: lilconfig: 2.1.0 yaml: 1.10.2 optionalDependencies: postcss: 8.5.6 - ts-node: 10.9.2(@types/node@22.16.4)(typescript@5.8.3) + ts-node: 10.9.2(@types/node@22.16.5)(typescript@5.8.3) postcss-logical@5.0.4(postcss@8.5.6): dependencies: @@ -12739,6 +12789,8 @@ snapshots: transitivePeerDependencies: - supports-color + process-nextick-args@2.0.1: {} + process@0.11.10: {} promise.series@0.2.0: {} @@ -12900,6 +12952,16 @@ snapshots: parse-json: 5.2.0 type-fest: 1.4.0 + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.2 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + redent@3.0.0: dependencies: indent-string: 4.0.0 @@ -13035,12 +13097,12 @@ snapshots: optionalDependencies: '@babel/code-frame': 7.27.1 - rollup-plugin-esbuild@5.0.0(esbuild@0.25.6)(rollup@3.29.5): + rollup-plugin-esbuild@5.0.0(esbuild@0.25.8)(rollup@3.29.5): dependencies: '@rollup/pluginutils': 5.2.0(rollup@3.29.5) debug: 4.4.1(supports-color@8.1.1) es-module-lexer: 1.7.0 - esbuild: 0.25.6 + esbuild: 0.25.8 joycon: 3.1.1 jsonc-parser: 3.3.1 rollup: 3.29.5 @@ -13051,7 +13113,7 @@ snapshots: dependencies: rollup: 3.29.5 - rollup-plugin-postcss@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)): + rollup-plugin-postcss@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)): dependencies: chalk: 4.1.2 concat-with-sourcemaps: 1.1.0 @@ -13060,7 +13122,7 @@ snapshots: p-queue: 6.6.2 pify: 5.0.0 postcss: 8.5.6 - postcss-load-config: 3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)) + postcss-load-config: 3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)) postcss-modules: 4.3.1(postcss@8.5.6) promise.series: 0.2.0 resolve: 1.22.10 @@ -13094,6 +13156,8 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} safe-identifier@0.4.2: {} @@ -13155,6 +13219,8 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + setimmediate@1.0.5: {} + sharp@0.34.3: dependencies: color: 4.2.3 @@ -13403,6 +13469,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -13641,12 +13711,12 @@ snapshots: dependencies: typescript: 5.8.3 - ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.6)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)))(typescript@5.8.3): + ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.8)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)))(typescript@5.8.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3)) + jest: 29.7.0(@types/node@22.16.5)(ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -13659,17 +13729,17 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.28.0) - esbuild: 0.25.6 + esbuild: 0.25.8 jest-util: 29.7.0 - ts-node@10.9.2(@types/node@22.16.4)(typescript@5.8.3): + ts-node@10.9.2(@types/node@22.16.5)(typescript@5.8.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 22.16.4 + '@types/node': 22.16.5 acorn: 8.15.0 acorn-walk: 8.3.4 arg: 4.1.3 @@ -13789,7 +13859,7 @@ snapshots: unrs-resolver@1.11.1: dependencies: - napi-postinstall: 0.3.1 + napi-postinstall: 0.3.2 optionalDependencies: '@unrs/resolver-binding-android-arm-eabi': 1.11.1 '@unrs/resolver-binding-android-arm64': 1.11.1 From 7670ec4136d5e94cd5f92b0c4bba997fe963bfcc Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 22 Jul 2025 00:24:37 -0700 Subject: [PATCH 6/9] Added download functionality. --- package.json | 1 + pnpm-lock.yaml | 8 ++ .../(main)/reports/[reportId]/ReportBody.tsx | 8 +- .../(main)/reports/[reportId]/ReportPage.tsx | 4 + .../[websiteId]/WebsiteMetricsBar.tsx | 11 ++- .../websites/[websiteId]/WebsiteTableView.tsx | 1 + .../api/websites/[websiteId]/export/route.ts | 75 +++++++++++++++++++ src/assets/download.svg | 1 + src/assets/export.svg | 1 + src/components/hooks/queries/useReport.ts | 2 +- src/components/icons.ts | 4 + src/components/input/DownloadButton.tsx | 41 ++++++++++ src/components/input/ExportButton.tsx | 47 ++++++++++++ src/components/messages.ts | 1 + src/components/metrics/EventsTable.tsx | 1 + .../metrics/MetricsTable.module.css | 7 ++ src/components/metrics/MetricsTable.tsx | 8 +- 17 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 src/app/api/websites/[websiteId]/export/route.ts create mode 100644 src/assets/download.svg create mode 100644 src/assets/export.svg create mode 100644 src/components/input/DownloadButton.tsx create mode 100644 src/components/input/ExportButton.tsx diff --git a/package.json b/package.json index 26ac6683..4e775a59 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "next": "15.3.3", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", + "papaparse": "^5.5.3", "prisma": "6.7.0", "pure-rand": "^6.1.0", "react": "^19.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10fdbff0..6f5e4bef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,6 +131,9 @@ importers: npm-run-all: specifier: ^4.1.5 version: 4.1.5 + papaparse: + specifier: ^5.5.3 + version: 5.5.3 prisma: specifier: 6.7.0 version: 6.7.0(typescript@5.8.3) @@ -5107,6 +5110,9 @@ packages: pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + papaparse@5.5.3: + resolution: {integrity: sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -12275,6 +12281,8 @@ snapshots: pako@1.0.11: {} + papaparse@5.5.3: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 diff --git a/src/app/(main)/reports/[reportId]/ReportBody.tsx b/src/app/(main)/reports/[reportId]/ReportBody.tsx index 9a740c5e..7b47c848 100644 --- a/src/app/(main)/reports/[reportId]/ReportBody.tsx +++ b/src/app/(main)/reports/[reportId]/ReportBody.tsx @@ -1,6 +1,7 @@ import { useContext } from 'react'; import { ReportContext } from './Report'; import styles from './ReportBody.module.css'; +import { DownloadButton } from '@/components/input/DownloadButton'; export function ReportBody({ children }) { const { report } = useContext(ReportContext); @@ -9,7 +10,12 @@ export function ReportBody({ children }) { return null; } - return
{children}
; + return ( +
+ + {children} +
+ ); } export default ReportBody; diff --git a/src/app/(main)/reports/[reportId]/ReportPage.tsx b/src/app/(main)/reports/[reportId]/ReportPage.tsx index 5e215cd2..aea5c0de 100644 --- a/src/app/(main)/reports/[reportId]/ReportPage.tsx +++ b/src/app/(main)/reports/[reportId]/ReportPage.tsx @@ -31,5 +31,9 @@ export default function ReportPage({ reportId }: { reportId: string }) { const ReportComponent = reports[report.type]; + if (!ReportComponent) { + return null; + } + return ; } diff --git a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx index f206d3c9..37e6861e 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx @@ -1,6 +1,6 @@ import { Dropdown, Item } from 'react-basics'; import classNames from 'classnames'; -import { useDateRange, useMessages, useSticky } from '@/components/hooks'; +import { useDateRange, useMessages, useNavigation, useSticky } from '@/components/hooks'; import WebsiteDateFilter from '@/components/input/WebsiteDateFilter'; import MetricCard from '@/components/metrics/MetricCard'; import MetricsBar from '@/components/metrics/MetricsBar'; @@ -8,6 +8,7 @@ import { formatShortTime, formatLongNumber } from '@/lib/format'; import useWebsiteStats from '@/components/hooks/queries/useWebsiteStats'; import useStore, { setWebsiteDateCompare } from '@/store/websites'; import WebsiteFilterButton from './WebsiteFilterButton'; +import { ExportButton } from '@/components/input/ExportButton'; import styles from './WebsiteMetricsBar.module.css'; export function WebsiteMetricsBar({ @@ -31,6 +32,9 @@ export function WebsiteMetricsBar({ websiteId, compareMode && dateCompare, ); + const { + query: { view }, + } = useNavigation(); const isAllTime = dateRange.value === 'all'; const { pageviews, visitors, visits, bounces, totaltime } = data || {}; @@ -109,7 +113,10 @@ export function WebsiteMetricsBar({
- {showFilter && } +
+ {showFilter && } + {!view && } +
{compareMode && (
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteTableView.tsx b/src/app/(main)/websites/[websiteId]/WebsiteTableView.tsx index 02422075..9b505140 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteTableView.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteTableView.tsx @@ -14,6 +14,7 @@ export default function WebsiteTableView({ websiteId }: { websiteId: string }) { const pathname = usePathname(); const tableProps = { websiteId, + allowDownload: false, limit: 10, }; const isSharePage = pathname.includes('/share/'); diff --git a/src/app/api/websites/[websiteId]/export/route.ts b/src/app/api/websites/[websiteId]/export/route.ts new file mode 100644 index 00000000..d8b403b4 --- /dev/null +++ b/src/app/api/websites/[websiteId]/export/route.ts @@ -0,0 +1,75 @@ +import { z } from 'zod'; +import JSZip from 'jszip'; +import Papa from 'papaparse'; +import { getRequestFilters, parseRequest } from '@/lib/request'; +import { unauthorized, json } from '@/lib/response'; +import { canViewWebsite } from '@/lib/auth'; +import { pagingParams } from '@/lib/schema'; +import { getEventMetrics, getWebsiteEvents } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + startAt: z.coerce.number().int(), + endAt: z.coerce.number().int(), + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { startAt, endAt } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + const limit = 10; + const offset = 0; + + const filters = { + ...(await getRequestFilters(query)), + startDate, + endDate, + }; + + const [events, pages, referrers, browsers, os, devices, countries] = await Promise.all([ + getWebsiteEvents(websiteId, { startDate, endDate }, query), + getEventMetrics(websiteId, 'url', filters, limit, offset), + getEventMetrics(websiteId, 'referrer', filters, limit, offset), + getEventMetrics(websiteId, 'browser', filters, limit, offset), + getEventMetrics(websiteId, 'os', filters, limit, offset), + getEventMetrics(websiteId, 'device', filters, limit, offset), + getEventMetrics(websiteId, 'country', filters, limit, offset), + ]); + + const zip = new JSZip(); + + const parse = (data: any) => { + return Papa.unparse(data, { + header: true, + skipEmptyLines: true, + }); + }; + + zip.file('events.csv', parse(events?.data)); + zip.file('pages.csv', parse(pages)); + zip.file('referrers.csv', parse(referrers)); + zip.file('browsers.csv', parse(browsers)); + zip.file('os.csv', parse(os)); + zip.file('devices.csv', parse(devices)); + zip.file('countries.csv', parse(countries)); + + const content = await zip.generateAsync({ type: 'nodebuffer' }); + const base64 = content.toString('base64'); + + return json({ zip: base64 }); +} diff --git a/src/assets/download.svg b/src/assets/download.svg new file mode 100644 index 00000000..b2482c9b --- /dev/null +++ b/src/assets/download.svg @@ -0,0 +1 @@ + diff --git a/src/assets/export.svg b/src/assets/export.svg new file mode 100644 index 00000000..d7585b15 --- /dev/null +++ b/src/assets/export.svg @@ -0,0 +1 @@ + diff --git a/src/components/hooks/queries/useReport.ts b/src/components/hooks/queries/useReport.ts index 45aea19c..7e9d81b0 100644 --- a/src/components/hooks/queries/useReport.ts +++ b/src/components/hooks/queries/useReport.ts @@ -29,7 +29,7 @@ export function useReport( data.parameters = { ...defaultParameters?.parameters, ...data.parameters, - dateRange: parseDateRange(dateRange.value), + dateRange: dateRange ? parseDateRange(dateRange?.value) : {}, }; setReport(data); diff --git a/src/components/icons.ts b/src/components/icons.ts index e952e500..2667e70e 100644 --- a/src/components/icons.ts +++ b/src/components/icons.ts @@ -8,6 +8,8 @@ import Change from '@/assets/change.svg'; import Clock from '@/assets/clock.svg'; import Compare from '@/assets/compare.svg'; import Dashboard from '@/assets/dashboard.svg'; +import Download from '@/assets/download.svg'; +import Export from '@/assets/export.svg'; import Eye from '@/assets/eye.svg'; import Gear from '@/assets/gear.svg'; import Globe from '@/assets/globe.svg'; @@ -37,6 +39,8 @@ const icons = { Clock, Compare, Dashboard, + Download, + Export, Eye, Gear, Globe, diff --git a/src/components/input/DownloadButton.tsx b/src/components/input/DownloadButton.tsx new file mode 100644 index 00000000..9a0e7994 --- /dev/null +++ b/src/components/input/DownloadButton.tsx @@ -0,0 +1,41 @@ +import Papa from 'papaparse'; +import { Button, Icon, TooltipPopup } from 'react-basics'; +import Icons from '@/components/icons'; +import { useMessages } from '@/components/hooks'; + +export function DownloadButton({ + filename = 'data', + data, +}: { + filename?: string; + data?: any; + onClick?: () => void; +}) { + const { formatMessage, labels } = useMessages(); + + const handleClick = async () => { + downloadCsv(`${filename}.csv`, Papa.unparse(data)); + }; + + return ( + + + + ); +} + +function downloadCsv(filename: string, data: any) { + const blob = new Blob([data], { type: 'text/csv' }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + + URL.revokeObjectURL(url); +} diff --git a/src/components/input/ExportButton.tsx b/src/components/input/ExportButton.tsx new file mode 100644 index 00000000..2b17a629 --- /dev/null +++ b/src/components/input/ExportButton.tsx @@ -0,0 +1,47 @@ +import { useState } from 'react'; +import { Icon, TooltipPopup, LoadingButton } from 'react-basics'; +import Icons from '@/components/icons'; +import { useMessages, useApi } from '@/components/hooks'; +import { useFilterParams } from '@/components/hooks/useFilterParams'; +import { useSearchParams } from 'next/navigation'; + +export function ExportButton({ websiteId }: { websiteId: string }) { + const { formatMessage, labels } = useMessages(); + const [isLoading, setIsLoading] = useState(false); + const params = useFilterParams(websiteId); + const searchParams = useSearchParams(); + const { get } = useApi(); + + const handleClick = async () => { + setIsLoading(true); + + const { zip } = await get(`/websites/${websiteId}/export`, { ...params, ...searchParams }); + + const binary = atob(zip); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + + const blob = new Blob([bytes], { type: 'application/zip' }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = 'download.zip'; + a.click(); + URL.revokeObjectURL(url); + + setIsLoading(false); + }; + + return ( + + + + + + + + ); +} diff --git a/src/components/messages.ts b/src/components/messages.ts index 1a7a1858..27086b49 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -316,6 +316,7 @@ export const labels = defineMessages({ other: { id: 'label.other', defaultMessage: 'Other' }, chart: { id: 'label.chart', defaultMessage: 'Chart' }, table: { id: 'label.table', defaultMessage: 'Table' }, + download: { id: 'label.download', defaultMessage: 'Download' }, }); export const messages = defineMessages({ diff --git a/src/components/metrics/EventsTable.tsx b/src/components/metrics/EventsTable.tsx index 45b81094..2be6f013 100644 --- a/src/components/metrics/EventsTable.tsx +++ b/src/components/metrics/EventsTable.tsx @@ -32,6 +32,7 @@ export function EventsTable({ onLabelClick, ...props }: EventsTableProps) { metric={formatMessage(labels.actions)} onDataLoad={handleDataLoad} renderLabel={renderLabel} + allowDownload={false} /> ); } diff --git a/src/components/metrics/MetricsTable.module.css b/src/components/metrics/MetricsTable.module.css index f04d9ae4..112cf382 100644 --- a/src/components/metrics/MetricsTable.module.css +++ b/src/components/metrics/MetricsTable.module.css @@ -14,6 +14,13 @@ margin-bottom: 10px; } +.buttons { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; +} + .footer { display: flex; justify-content: center; diff --git a/src/components/metrics/MetricsTable.tsx b/src/components/metrics/MetricsTable.tsx index 616262cb..39ba712d 100644 --- a/src/components/metrics/MetricsTable.tsx +++ b/src/components/metrics/MetricsTable.tsx @@ -15,6 +15,7 @@ import { import Icons from '@/components/icons'; import ListTable, { ListTableProps } from './ListTable'; import styles from './MetricsTable.module.css'; +import { DownloadButton } from '@/components/input/DownloadButton'; export interface MetricsTableProps extends ListTableProps { websiteId: string; @@ -29,6 +30,7 @@ export interface MetricsTableProps extends ListTableProps { searchFormattedValues?: boolean; showMore?: boolean; params?: { [key: string]: any }; + allowDownload?: boolean; children?: ReactNode; } @@ -44,6 +46,7 @@ export function MetricsTable({ searchFormattedValues = false, showMore = true, params, + allowDownload = true, children, ...props }: MetricsTableProps) { @@ -104,7 +107,10 @@ export function MetricsTable({ autoFocus={true} /> )} - {children} +
+ {children} + {allowDownload && } +
{data && !error && ( From 6d124e7bb0bd7b566d74e9b1101c625db526a742 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Tue, 22 Jul 2025 11:10:37 -0700 Subject: [PATCH 7/9] fix export report route --- .../(main)/reports/[reportId]/ReportBody.tsx | 4 +++- .../api/websites/[websiteId]/export/route.ts | 20 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/app/(main)/reports/[reportId]/ReportBody.tsx b/src/app/(main)/reports/[reportId]/ReportBody.tsx index 7b47c848..fe2f8c91 100644 --- a/src/app/(main)/reports/[reportId]/ReportBody.tsx +++ b/src/app/(main)/reports/[reportId]/ReportBody.tsx @@ -12,7 +12,9 @@ export function ReportBody({ children }) { return (
- + {report.type !== 'revenue' && report.type !== 'attribution' && ( + + )} {children}
); diff --git a/src/app/api/websites/[websiteId]/export/route.ts b/src/app/api/websites/[websiteId]/export/route.ts index d8b403b4..534fd229 100644 --- a/src/app/api/websites/[websiteId]/export/route.ts +++ b/src/app/api/websites/[websiteId]/export/route.ts @@ -5,7 +5,7 @@ import { getRequestFilters, parseRequest } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; import { canViewWebsite } from '@/lib/auth'; import { pagingParams } from '@/lib/schema'; -import { getEventMetrics, getWebsiteEvents } from '@/queries'; +import { getEventMetrics, getPageviewMetrics, getSessionMetrics } from '@/queries'; export async function GET( request: Request, @@ -32,8 +32,6 @@ export async function GET( const startDate = new Date(+startAt); const endDate = new Date(+endAt); - const limit = 10; - const offset = 0; const filters = { ...(await getRequestFilters(query)), @@ -42,13 +40,13 @@ export async function GET( }; const [events, pages, referrers, browsers, os, devices, countries] = await Promise.all([ - getWebsiteEvents(websiteId, { startDate, endDate }, query), - getEventMetrics(websiteId, 'url', filters, limit, offset), - getEventMetrics(websiteId, 'referrer', filters, limit, offset), - getEventMetrics(websiteId, 'browser', filters, limit, offset), - getEventMetrics(websiteId, 'os', filters, limit, offset), - getEventMetrics(websiteId, 'device', filters, limit, offset), - getEventMetrics(websiteId, 'country', filters, limit, offset), + getEventMetrics(websiteId, 'event', filters), + getPageviewMetrics(websiteId, 'url', filters), + getPageviewMetrics(websiteId, 'referrer', filters), + getSessionMetrics(websiteId, 'browser', filters), + getSessionMetrics(websiteId, 'os', filters), + getSessionMetrics(websiteId, 'device', filters), + getSessionMetrics(websiteId, 'country', filters), ]); const zip = new JSZip(); @@ -60,7 +58,7 @@ export async function GET( }); }; - zip.file('events.csv', parse(events?.data)); + zip.file('events.csv', parse(events)); zip.file('pages.csv', parse(pages)); zip.file('referrers.csv', parse(referrers)); zip.file('browsers.csv', parse(browsers)); From 2203ba1fe7707c735ce6c7a6b30a08391c4af6cf Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Wed, 23 Jul 2025 09:38:39 -0700 Subject: [PATCH 8/9] move parseParameters to UTMView --- src/app/(main)/reports/utm/UTMView.tsx | 27 +++++++++++++++++++++++- src/queries/sql/reports/getUTM.ts | 29 ++------------------------ 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/app/(main)/reports/utm/UTMView.tsx b/src/app/(main)/reports/utm/UTMView.tsx index ba025824..a8200c2c 100644 --- a/src/app/(main)/reports/utm/UTMView.tsx +++ b/src/app/(main)/reports/utm/UTMView.tsx @@ -15,6 +15,31 @@ function toArray(data: { [key: string]: number } = {}) { .sort(firstBy('value', -1)); } +function parseParameters(data: any[]) { + return data.reduce((obj, { url_query, num }) => { + try { + const searchParams = new URLSearchParams(url_query); + + for (const [key, value] of searchParams) { + if (key.match(/^utm_(\w+)$/)) { + const name = value; + if (!obj[key]) { + obj[key] = { [name]: Number(num) }; + } else if (!obj[key][name]) { + obj[key][name] = Number(num); + } else { + obj[key][name] += Number(num); + } + } + } + } catch { + // Ignore + } + + return obj; + }, {}); +} + export default function UTMView() { const { formatMessage, labels } = useMessages(); const { report } = useContext(ReportContext); @@ -27,7 +52,7 @@ export default function UTMView() { return (
{UTM_PARAMS.map(param => { - const items = toArray(data[param]); + const items = toArray(parseParameters(data)[param]); const chartData = { labels: items.map(({ name }) => name), datasets: [ diff --git a/src/queries/sql/reports/getUTM.ts b/src/queries/sql/reports/getUTM.ts index 5463815b..f96c62d3 100644 --- a/src/queries/sql/reports/getUTM.ts +++ b/src/queries/sql/reports/getUTM.ts @@ -44,7 +44,7 @@ async function relationalQuery( startDate, endDate, }, - ).then(result => parseParameters(result as any[])); + ); } async function clickhouseQuery( @@ -73,30 +73,5 @@ async function clickhouseQuery( startDate, endDate, }, - ).then(result => parseParameters(result as any[])); -} - -function parseParameters(data: any[]) { - return data.reduce((obj, { url_query, num }) => { - try { - const searchParams = new URLSearchParams(url_query); - - for (const [key, value] of searchParams) { - if (key.match(/^utm_(\w+)$/)) { - const name = value; - if (!obj[key]) { - obj[key] = { [name]: Number(num) }; - } else if (!obj[key][name]) { - obj[key][name] = Number(num); - } else { - obj[key][name] += Number(num); - } - } - } - } catch { - // Ignore - } - - return obj; - }, {}); + ); } From 038d3d6000f2c02bd42a2d0d2056b178ba30a1cc Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Wed, 23 Jul 2025 10:18:43 -0700 Subject: [PATCH 9/9] remove stringify json on updatereport --- src/app/api/reports/[reportId]/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/api/reports/[reportId]/route.ts b/src/app/api/reports/[reportId]/route.ts index 252aa2ec..24c32b57 100644 --- a/src/app/api/reports/[reportId]/route.ts +++ b/src/app/api/reports/[reportId]/route.ts @@ -60,7 +60,7 @@ export async function POST( type, name, description, - parameters: JSON.stringify(parameters), + parameters: parameters, } as any); return json(result);