mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-22 01:46:58 +01:00
commit
88da20ea7f
1
.gitignore
vendored
1
.gitignore
vendored
@ -22,6 +22,7 @@ node_modules
|
|||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea
|
.idea
|
||||||
|
.yarn
|
||||||
*.iml
|
*.iml
|
||||||
*.log
|
*.log
|
||||||
.vscode
|
.vscode
|
||||||
|
@ -4,4 +4,9 @@ export default defineConfig({
|
|||||||
e2e: {
|
e2e: {
|
||||||
baseUrl: 'http://localhost:3000',
|
baseUrl: 'http://localhost:3000',
|
||||||
},
|
},
|
||||||
|
// default username / password on init
|
||||||
|
env: {
|
||||||
|
umami_user: 'admin',
|
||||||
|
umami_password: 'umami',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
@ -43,9 +43,10 @@ services:
|
|||||||
- CYPRESS_umami_user=admin
|
- CYPRESS_umami_user=admin
|
||||||
- CYPRESS_umami_password=umami
|
- CYPRESS_umami_password=umami
|
||||||
volumes:
|
volumes:
|
||||||
- ../tsconfig.json:/tsconfig.json
|
- ./tsconfig.json:/tsconfig.json
|
||||||
- ../cypress.config.ts:/cypress.config.ts
|
- ../cypress.config.ts:/cypress.config.ts
|
||||||
- ./:/cypress
|
- ./:/cypress
|
||||||
- ../node_modules/:/node_modules
|
- ../node_modules/:/node_modules
|
||||||
|
- ../src/lib/crypto.ts:/src/lib/crypto.ts
|
||||||
volumes:
|
volumes:
|
||||||
umami-db-data:
|
umami-db-data:
|
||||||
|
@ -6,8 +6,12 @@ describe('Login tests', () => {
|
|||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
cy.visit('/login');
|
cy.visit('/login');
|
||||||
cy.getDataTest('input-username').find('input').type(Cypress.env('umami_user'));
|
cy.getDataTest('input-username').find('input').click();
|
||||||
cy.getDataTest('input-password').find('input').type(Cypress.env('umami_password'));
|
cy.getDataTest('input-username').find('input').type(Cypress.env('umami_user'), { delay: 50 });
|
||||||
|
cy.getDataTest('input-password').find('input').click();
|
||||||
|
cy.getDataTest('input-password')
|
||||||
|
.find('input')
|
||||||
|
.type(Cypress.env('umami_password'), { delay: 50 });
|
||||||
cy.getDataTest('button-submit').click();
|
cy.getDataTest('button-submit').click();
|
||||||
cy.url().should('eq', Cypress.config().baseUrl + '/dashboard');
|
cy.url().should('eq', Cypress.config().baseUrl + '/dashboard');
|
||||||
cy.getDataTest('button-profile').click();
|
cy.getDataTest('button-profile').click();
|
||||||
|
@ -10,8 +10,10 @@ describe('Website tests', () => {
|
|||||||
cy.visit('/settings/websites');
|
cy.visit('/settings/websites');
|
||||||
cy.getDataTest('button-website-add').click();
|
cy.getDataTest('button-website-add').click();
|
||||||
cy.contains(/Add website/i).should('be.visible');
|
cy.contains(/Add website/i).should('be.visible');
|
||||||
cy.getDataTest('input-name').find('input').wait(500).type('Add test', { delay: 50 });
|
cy.getDataTest('input-name').find('input').click();
|
||||||
cy.getDataTest('input-domain').find('input').wait(500).type('addtest.com', { delay: 50 });
|
cy.getDataTest('input-name').find('input').type('Add test', { delay: 50 });
|
||||||
|
cy.getDataTest('input-domain').find('input').click();
|
||||||
|
cy.getDataTest('input-domain').find('input').type('addtest.com', { delay: 50 });
|
||||||
cy.getDataTest('button-submit').click();
|
cy.getDataTest('button-submit').click();
|
||||||
cy.get('td[label="Name"]').should('contain.text', 'Add test');
|
cy.get('td[label="Name"]').should('contain.text', 'Add test');
|
||||||
cy.get('td[label="Domain"]').should('contain.text', 'addtest.com');
|
cy.get('td[label="Domain"]').should('contain.text', 'addtest.com');
|
||||||
@ -26,10 +28,10 @@ describe('Website tests', () => {
|
|||||||
cy.deleteWebsite(websiteId);
|
cy.deleteWebsite(websiteId);
|
||||||
});
|
});
|
||||||
cy.visit('/settings/websites');
|
cy.visit('/settings/websites');
|
||||||
cy.contains('Add test').should('not.exist');
|
cy.contains(/Add test/i).should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.only('Edit a website', () => {
|
it('Edit a website', () => {
|
||||||
// prep data
|
// prep data
|
||||||
cy.addWebsite('Update test', 'updatetest.com');
|
cy.addWebsite('Update test', 'updatetest.com');
|
||||||
cy.visit('/settings/websites');
|
cy.visit('/settings/websites');
|
||||||
@ -37,16 +39,12 @@ describe('Website tests', () => {
|
|||||||
// edit website
|
// edit website
|
||||||
cy.getDataTest('link-button-edit').first().click();
|
cy.getDataTest('link-button-edit').first().click();
|
||||||
cy.contains(/Details/i).should('be.visible');
|
cy.contains(/Details/i).should('be.visible');
|
||||||
cy.getDataTest('input-name')
|
cy.getDataTest('input-name').find('input').click();
|
||||||
.find('input')
|
cy.getDataTest('input-name').find('input').clear();
|
||||||
.wait(500)
|
cy.getDataTest('input-name').find('input').type('Updated website', { delay: 50 });
|
||||||
.clear()
|
cy.getDataTest('input-domain').find('input').click();
|
||||||
.type('Updated website', { delay: 50 });
|
cy.getDataTest('input-domain').find('input').clear();
|
||||||
cy.getDataTest('input-domain')
|
cy.getDataTest('input-domain').find('input').type('updatedwebsite.com', { delay: 50 });
|
||||||
.find('input')
|
|
||||||
.wait(500)
|
|
||||||
.clear()
|
|
||||||
.type('updatedwebsite.com', { delay: 50 });
|
|
||||||
cy.getDataTest('button-submit').click({ force: true });
|
cy.getDataTest('button-submit').click({ force: true });
|
||||||
cy.getDataTest('input-name').find('input').should('have.value', 'Updated website');
|
cy.getDataTest('input-name').find('input').should('have.value', 'Updated website');
|
||||||
cy.getDataTest('input-domain').find('input').should('have.value', 'updatedwebsite.com');
|
cy.getDataTest('input-domain').find('input').should('have.value', 'updatedwebsite.com');
|
||||||
@ -69,7 +67,7 @@ describe('Website tests', () => {
|
|||||||
cy.deleteWebsite(websiteId);
|
cy.deleteWebsite(websiteId);
|
||||||
});
|
});
|
||||||
cy.visit('/settings/websites');
|
cy.visit('/settings/websites');
|
||||||
cy.contains('Add test').should('not.exist');
|
cy.contains(/Add test/i).should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Delete a website', () => {
|
it('Delete a website', () => {
|
||||||
@ -86,6 +84,6 @@ describe('Website tests', () => {
|
|||||||
cy.contains(/Type DELETE in the box below to confirm./i).should('be.visible');
|
cy.contains(/Type DELETE in the box below to confirm./i).should('be.visible');
|
||||||
cy.get('input[name="confirm"').type('DELETE');
|
cy.get('input[name="confirm"').type('DELETE');
|
||||||
cy.get('button[type="submit"]').click();
|
cy.get('button[type="submit"]').click();
|
||||||
cy.contains('Delete test').should('not.exist');
|
cy.contains(/Delete test/i).should('not.exist');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
90
db/clickhouse/migrations/02_add_visit_id.sql
Normal file
90
db/clickhouse/migrations/02_add_visit_id.sql
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
CREATE TABLE umami.website_event_join
|
||||||
|
(
|
||||||
|
session_id UUID,
|
||||||
|
visit_id UUID,
|
||||||
|
created_at DateTime('UTC')
|
||||||
|
)
|
||||||
|
engine = MergeTree
|
||||||
|
ORDER BY (session_id, created_at)
|
||||||
|
SETTINGS index_granularity = 8192;
|
||||||
|
|
||||||
|
INSERT INTO umami.website_event_join
|
||||||
|
SELECT DISTINCT
|
||||||
|
s.session_id,
|
||||||
|
generateUUIDv4() visit_id,
|
||||||
|
s.created_at
|
||||||
|
FROM (SELECT DISTINCT session_id,
|
||||||
|
date_trunc('hour', created_at) created_at
|
||||||
|
FROM website_event) s;
|
||||||
|
|
||||||
|
-- create new table
|
||||||
|
CREATE TABLE umami.website_event_new
|
||||||
|
(
|
||||||
|
website_id UUID,
|
||||||
|
session_id UUID,
|
||||||
|
visit_id UUID,
|
||||||
|
event_id UUID,
|
||||||
|
hostname LowCardinality(String),
|
||||||
|
browser LowCardinality(String),
|
||||||
|
os LowCardinality(String),
|
||||||
|
device LowCardinality(String),
|
||||||
|
screen LowCardinality(String),
|
||||||
|
language LowCardinality(String),
|
||||||
|
country LowCardinality(String),
|
||||||
|
subdivision1 LowCardinality(String),
|
||||||
|
subdivision2 LowCardinality(String),
|
||||||
|
city String,
|
||||||
|
url_path String,
|
||||||
|
url_query String,
|
||||||
|
referrer_path String,
|
||||||
|
referrer_query String,
|
||||||
|
referrer_domain String,
|
||||||
|
page_title String,
|
||||||
|
event_type UInt32,
|
||||||
|
event_name String,
|
||||||
|
created_at DateTime('UTC'),
|
||||||
|
job_id UUID
|
||||||
|
)
|
||||||
|
engine = MergeTree
|
||||||
|
ORDER BY (website_id, session_id, created_at)
|
||||||
|
SETTINGS index_granularity = 8192;
|
||||||
|
|
||||||
|
INSERT INTO umami.website_event_new
|
||||||
|
SELECT we.website_id,
|
||||||
|
we.session_id,
|
||||||
|
j.visit_id,
|
||||||
|
we.event_id,
|
||||||
|
we.hostname,
|
||||||
|
we.browser,
|
||||||
|
we.os,
|
||||||
|
we.device,
|
||||||
|
we.screen,
|
||||||
|
we.language,
|
||||||
|
we.country,
|
||||||
|
we.subdivision1,
|
||||||
|
we.subdivision2,
|
||||||
|
we.city,
|
||||||
|
we.url_path,
|
||||||
|
we.url_query,
|
||||||
|
we.referrer_path,
|
||||||
|
we.referrer_query,
|
||||||
|
we.referrer_domain,
|
||||||
|
we.page_title,
|
||||||
|
we.event_type,
|
||||||
|
we.event_name,
|
||||||
|
we.created_at,
|
||||||
|
we.job_id
|
||||||
|
FROM umami.website_event we
|
||||||
|
JOIN umami.website_event_join j
|
||||||
|
ON we.session_id = j.session_id
|
||||||
|
and date_trunc('hour', we.created_at) = j.created_at
|
||||||
|
|
||||||
|
RENAME TABLE umami.website_event TO umami.website_event_old;
|
||||||
|
RENAME TABLE umami.website_event_new TO umami.website_event;
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
DROP TABLE umami.website_event_old
|
||||||
|
DROP TABLE umami.website_event_join
|
||||||
|
|
||||||
|
*/
|
@ -3,6 +3,7 @@ CREATE TABLE umami.website_event
|
|||||||
(
|
(
|
||||||
website_id UUID,
|
website_id UUID,
|
||||||
session_id UUID,
|
session_id UUID,
|
||||||
|
visit_id UUID,
|
||||||
event_id UUID,
|
event_id UUID,
|
||||||
--sessions
|
--sessions
|
||||||
hostname LowCardinality(String),
|
hostname LowCardinality(String),
|
||||||
|
22
db/mysql/migrations/05_add_visit_id/migration.sql
Normal file
22
db/mysql/migrations/05_add_visit_id/migration.sql
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `website_event` ADD COLUMN `visit_id` VARCHAR(36) NULL;
|
||||||
|
|
||||||
|
UPDATE `website_event` we
|
||||||
|
JOIN (SELECT DISTINCT
|
||||||
|
s.session_id,
|
||||||
|
s.visit_time,
|
||||||
|
BIN_TO_UUID(RANDOM_BYTES(16) & 0xffffffffffff0fff3fffffffffffffff | 0x00000000000040008000000000000000) uuid
|
||||||
|
FROM (SELECT DISTINCT session_id,
|
||||||
|
DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00') visit_time
|
||||||
|
FROM `website_event`) s) a
|
||||||
|
ON we.session_id = a.session_id and DATE_FORMAT(we.created_at, '%Y-%m-%d %H:00:00') = a.visit_time
|
||||||
|
SET we.visit_id = a.uuid
|
||||||
|
WHERE we.visit_id IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE `website_event` MODIFY `visit_id` VARCHAR(36) NOT NULL;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `website_event_visit_id_idx` ON `website_event`(`visit_id`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `website_event_website_id_visit_id_created_at_idx` ON `website_event`(`website_id`, `visit_id`, `created_at`);
|
@ -92,6 +92,7 @@ model WebsiteEvent {
|
|||||||
id String @id() @map("event_id") @db.VarChar(36)
|
id String @id() @map("event_id") @db.VarChar(36)
|
||||||
websiteId String @map("website_id") @db.VarChar(36)
|
websiteId String @map("website_id") @db.VarChar(36)
|
||||||
sessionId String @map("session_id") @db.VarChar(36)
|
sessionId String @map("session_id") @db.VarChar(36)
|
||||||
|
visitId String @map("visit_id") @db.VarChar(36)
|
||||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||||
urlPath String @map("url_path") @db.VarChar(500)
|
urlPath String @map("url_path") @db.VarChar(500)
|
||||||
urlQuery String? @map("url_query") @db.VarChar(500)
|
urlQuery String? @map("url_query") @db.VarChar(500)
|
||||||
@ -107,6 +108,7 @@ model WebsiteEvent {
|
|||||||
|
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([sessionId])
|
@@index([sessionId])
|
||||||
|
@@index([visitId])
|
||||||
@@index([websiteId])
|
@@index([websiteId])
|
||||||
@@index([websiteId, createdAt])
|
@@index([websiteId, createdAt])
|
||||||
@@index([websiteId, createdAt, urlPath])
|
@@index([websiteId, createdAt, urlPath])
|
||||||
@ -115,6 +117,7 @@ model WebsiteEvent {
|
|||||||
@@index([websiteId, createdAt, pageTitle])
|
@@index([websiteId, createdAt, pageTitle])
|
||||||
@@index([websiteId, createdAt, eventName])
|
@@index([websiteId, createdAt, eventName])
|
||||||
@@index([websiteId, sessionId, createdAt])
|
@@index([websiteId, sessionId, createdAt])
|
||||||
|
@@index([websiteId, visitId, createdAt])
|
||||||
@@map("website_event")
|
@@map("website_event")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
22
db/postgresql/migrations/05_add_visit_id/migration.sql
Normal file
22
db/postgresql/migrations/05_add_visit_id/migration.sql
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "website_event" ADD COLUMN "visit_id" UUID NULL;
|
||||||
|
|
||||||
|
UPDATE "website_event" we
|
||||||
|
SET visit_id = a.uuid
|
||||||
|
FROM (SELECT DISTINCT
|
||||||
|
s.session_id,
|
||||||
|
s.visit_time,
|
||||||
|
gen_random_uuid() uuid
|
||||||
|
FROM (SELECT DISTINCT session_id,
|
||||||
|
date_trunc('hour', created_at) visit_time
|
||||||
|
FROM "website_event") s) a
|
||||||
|
WHERE we.session_id = a.session_id
|
||||||
|
and date_trunc('hour', we.created_at) = a.visit_time;
|
||||||
|
|
||||||
|
ALTER TABLE "website_event" ALTER COLUMN "visit_id" SET NOT NULL;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "website_event_visit_id_idx" ON "website_event"("visit_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "website_event_website_id_visit_id_created_at_idx" ON "website_event"("website_id", "visit_id", "created_at");
|
@ -92,6 +92,7 @@ model WebsiteEvent {
|
|||||||
id String @id() @map("event_id") @db.Uuid
|
id String @id() @map("event_id") @db.Uuid
|
||||||
websiteId String @map("website_id") @db.Uuid
|
websiteId String @map("website_id") @db.Uuid
|
||||||
sessionId String @map("session_id") @db.Uuid
|
sessionId String @map("session_id") @db.Uuid
|
||||||
|
visitId String @map("visit_id") @db.Uuid
|
||||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
urlPath String @map("url_path") @db.VarChar(500)
|
urlPath String @map("url_path") @db.VarChar(500)
|
||||||
urlQuery String? @map("url_query") @db.VarChar(500)
|
urlQuery String? @map("url_query") @db.VarChar(500)
|
||||||
@ -107,6 +108,7 @@ model WebsiteEvent {
|
|||||||
|
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([sessionId])
|
@@index([sessionId])
|
||||||
|
@@index([visitId])
|
||||||
@@index([websiteId])
|
@@index([websiteId])
|
||||||
@@index([websiteId, createdAt])
|
@@index([websiteId, createdAt])
|
||||||
@@index([websiteId, createdAt, urlPath])
|
@@index([websiteId, createdAt, urlPath])
|
||||||
@ -115,6 +117,7 @@ model WebsiteEvent {
|
|||||||
@@index([websiteId, createdAt, pageTitle])
|
@@index([websiteId, createdAt, pageTitle])
|
||||||
@@index([websiteId, createdAt, eventName])
|
@@index([websiteId, createdAt, eventName])
|
||||||
@@index([websiteId, sessionId, createdAt])
|
@@index([websiteId, sessionId, createdAt])
|
||||||
|
@@index([websiteId, visitId, createdAt])
|
||||||
@@map("website_event")
|
@@map("website_event")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ const frameAncestors = process.env.ALLOWED_FRAME_URLS || '';
|
|||||||
const disableLogin = process.env.DISABLE_LOGIN || '';
|
const disableLogin = process.env.DISABLE_LOGIN || '';
|
||||||
const disableUI = process.env.DISABLE_UI || '';
|
const disableUI = process.env.DISABLE_UI || '';
|
||||||
const hostURL = process.env.HOST_URL || '';
|
const hostURL = process.env.HOST_URL || '';
|
||||||
|
const privateMode = process.env.PRIVATE_MODE || '';
|
||||||
|
|
||||||
const contentSecurityPolicy = [
|
const contentSecurityPolicy = [
|
||||||
`default-src 'self'`,
|
`default-src 'self'`,
|
||||||
@ -120,6 +121,7 @@ const config = {
|
|||||||
disableLogin,
|
disableLogin,
|
||||||
disableUI,
|
disableUI,
|
||||||
hostURL,
|
hostURL,
|
||||||
|
privateMode,
|
||||||
},
|
},
|
||||||
basePath,
|
basePath,
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
|
15
package.json
15
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "umami",
|
"name": "umami",
|
||||||
"version": "2.10.2",
|
"version": "2.11.0",
|
||||||
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
|
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
|
||||||
"author": "Umami Software, Inc. <hello@umami.is>",
|
"author": "Umami Software, Inc. <hello@umami.is>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -66,14 +66,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clickhouse/client": "^0.2.2",
|
"@clickhouse/client": "^0.2.2",
|
||||||
"@fontsource/inter": "^4.5.15",
|
"@fontsource/inter": "^4.5.15",
|
||||||
"@prisma/client": "5.9.1",
|
"@prisma/client": "5.11.0",
|
||||||
"@prisma/extension-read-replicas": "^0.3.0",
|
"@prisma/extension-read-replicas": "^0.3.0",
|
||||||
"@react-spring/web": "^9.7.3",
|
"@react-spring/web": "^9.7.3",
|
||||||
"@tanstack/react-query": "^5.12.2",
|
"@tanstack/react-query": "^5.28.6",
|
||||||
"@umami/prisma-client": "^0.14.0",
|
"@umami/prisma-client": "^0.14.0",
|
||||||
"@umami/redis-client": "^0.18.0",
|
"@umami/redis-client": "^0.18.0",
|
||||||
"chalk": "^4.1.1",
|
"chalk": "^4.1.1",
|
||||||
"chart.js": "^4.2.1",
|
"chart.js": "^4.4.2",
|
||||||
"chartjs-adapter-date-fns": "^3.0.0",
|
"chartjs-adapter-date-fns": "^3.0.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"colord": "^2.9.2",
|
"colord": "^2.9.2",
|
||||||
@ -98,11 +98,11 @@
|
|||||||
"maxmind": "^4.3.6",
|
"maxmind": "^4.3.6",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"moment-timezone": "^0.5.35",
|
"moment-timezone": "^0.5.35",
|
||||||
"next": "14.1.0",
|
"next": "14.1.4",
|
||||||
"next-basics": "^0.39.0",
|
"next-basics": "^0.39.0",
|
||||||
"node-fetch": "^3.2.8",
|
"node-fetch": "^3.2.8",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prisma": "5.9.1",
|
"prisma": "5.11.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-basics": "^0.123.0",
|
"react-basics": "^0.123.0",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
@ -115,7 +115,6 @@
|
|||||||
"request-ip": "^3.3.0",
|
"request-ip": "^3.3.0",
|
||||||
"semver": "^7.5.4",
|
"semver": "^7.5.4",
|
||||||
"thenby": "^1.3.4",
|
"thenby": "^1.3.4",
|
||||||
"timezone-support": "^2.0.2",
|
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"yup": "^0.32.11",
|
"yup": "^0.32.11",
|
||||||
"zustand": "^4.3.8"
|
"zustand": "^4.3.8"
|
||||||
@ -176,6 +175,6 @@
|
|||||||
"tar": "^6.1.2",
|
"tar": "^6.1.2",
|
||||||
"ts-jest": "^29.1.2",
|
"ts-jest": "^29.1.2",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Erstellt"
|
"value": "Erstellt"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Erstellt"
|
"value": "Erstellt"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
"label.add-member": [
|
"label.add-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Add member"
|
"value": "Añadir miembro"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.add-website": [
|
"label.add-website": [
|
||||||
@ -215,6 +215,12 @@
|
|||||||
"value": "Creado"
|
"value": "Creado"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
@ -272,7 +278,7 @@
|
|||||||
"label.delete-report": [
|
"label.delete-report": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Delete report"
|
"value": "Eliminar reporte"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.delete-team": [
|
"label.delete-team": [
|
||||||
@ -464,7 +470,7 @@
|
|||||||
"label.insights-description": [
|
"label.insights-description": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Dive deeper into your data by using segments and filters."
|
"value": "Profundice en sus datos mediante el uso de segmentos y filtros."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.is": [
|
"label.is": [
|
||||||
@ -482,7 +488,7 @@
|
|||||||
"label.is-not-set": [
|
"label.is-not-set": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Is not set"
|
"value": "No está establecido"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.is-set": [
|
"label.is-set": [
|
||||||
@ -588,7 +594,7 @@
|
|||||||
"label.manage": [
|
"label.manage": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Manage"
|
"value": "Administrar"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.max": [
|
"label.max": [
|
||||||
@ -600,7 +606,7 @@
|
|||||||
"label.member": [
|
"label.member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Member"
|
"value": "Miembro"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.members": [
|
"label.members": [
|
||||||
@ -630,7 +636,7 @@
|
|||||||
"label.my-account": [
|
"label.my-account": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "My account"
|
"value": "Mi cuenta"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.my-websites": [
|
"label.my-websites": [
|
||||||
@ -842,7 +848,7 @@
|
|||||||
"label.remove-member": [
|
"label.remove-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Remove member"
|
"value": "Eliminar miembro"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.reports": [
|
"label.reports": [
|
||||||
@ -926,7 +932,7 @@
|
|||||||
"label.select-role": [
|
"label.select-role": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Select role"
|
"value": "Seleccionar rol"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.select-website": [
|
"label.select-website": [
|
||||||
@ -1004,7 +1010,7 @@
|
|||||||
"label.team-view-only": [
|
"label.team-view-only": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Team view only"
|
"value": "Vista solo del equipo"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.team-websites": [
|
"label.team-websites": [
|
||||||
@ -1088,13 +1094,13 @@
|
|||||||
"label.transfer": [
|
"label.transfer": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer"
|
"value": "Transferir"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.transfer-website": [
|
"label.transfer-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer website"
|
"value": "Transferir sitio web"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.true": [
|
"label.true": [
|
||||||
@ -1232,7 +1238,7 @@
|
|||||||
"message.action-confirmation": [
|
"message.action-confirmation": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Type "
|
"value": "Escriba "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1240,7 +1246,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": " in the box below to confirm."
|
"value": " en el cuadro a continuación para confirmar."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.active-users": [
|
"message.active-users": [
|
||||||
@ -1308,7 +1314,7 @@
|
|||||||
"message.confirm-remove": [
|
"message.confirm-remove": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Are you sure you want to remove "
|
"value": "¿Estás seguro de que desea eliminar "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1336,7 +1342,7 @@
|
|||||||
"message.delete-team-warning": [
|
"message.delete-team-warning": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Deleting a team will also delete all team websites."
|
"value": "Al eliminar un equipo, también se eliminarán todos los sitios web del equipo."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.delete-website-warning": [
|
"message.delete-website-warning": [
|
||||||
@ -1532,7 +1538,7 @@
|
|||||||
"message.transfer-team-website-to-user": [
|
"message.transfer-team-website-to-user": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer this website to your account?"
|
"value": "¿Transferir este sitio web a su cuenta?"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.transfer-user-website-to-team": [
|
"message.transfer-user-website-to-team": [
|
||||||
@ -1544,13 +1550,13 @@
|
|||||||
"message.transfer-website": [
|
"message.transfer-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer website ownership to your account or another team."
|
"value": "Seleccione el equipo al que transferir este sitio web."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.triggered-event": [
|
"message.triggered-event": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Triggered event"
|
"value": "Evento lanzado"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.user-deleted": [
|
"message.user-deleted": [
|
||||||
@ -1562,7 +1568,7 @@
|
|||||||
"message.viewed-page": [
|
"message.viewed-page": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Viewed page"
|
"value": "Página vista"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.visitor-log": [
|
"message.visitor-log": [
|
||||||
@ -1602,7 +1608,7 @@
|
|||||||
"message.visitors-dropped-off": [
|
"message.visitors-dropped-off": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Visitors dropped off"
|
"value": "Los visitantes salieron"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
"label.add-member": [
|
"label.add-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Add member"
|
"value": "Ajouter un membre"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.add-website": [
|
"label.add-website": [
|
||||||
@ -215,6 +215,12 @@
|
|||||||
"value": "Créé"
|
"value": "Créé"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Crée par"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
@ -272,7 +278,7 @@
|
|||||||
"label.delete-report": [
|
"label.delete-report": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Delete report"
|
"value": "Supprimer le rapport"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.delete-team": [
|
"label.delete-team": [
|
||||||
@ -362,7 +368,7 @@
|
|||||||
"label.edit-member": [
|
"label.edit-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Edit member"
|
"value": "Modifier le membre"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.enable-share-url": [
|
"label.enable-share-url": [
|
||||||
@ -580,7 +586,7 @@
|
|||||||
"label.manage": [
|
"label.manage": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Manage"
|
"value": "Gérer"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.max": [
|
"label.max": [
|
||||||
@ -592,7 +598,7 @@
|
|||||||
"label.member": [
|
"label.member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Member"
|
"value": "Membre"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.members": [
|
"label.members": [
|
||||||
@ -622,7 +628,7 @@
|
|||||||
"label.my-account": [
|
"label.my-account": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "My account"
|
"value": "Mon compte"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.my-websites": [
|
"label.my-websites": [
|
||||||
@ -646,7 +652,7 @@
|
|||||||
"label.none": [
|
"label.none": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Aucun·e"
|
"value": "Aucun"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.number-of-records": [
|
"label.number-of-records": [
|
||||||
@ -834,7 +840,7 @@
|
|||||||
"label.remove-member": [
|
"label.remove-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Remove member"
|
"value": "Retirer le membre"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.reports": [
|
"label.reports": [
|
||||||
@ -918,7 +924,7 @@
|
|||||||
"label.select-role": [
|
"label.select-role": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Select role"
|
"value": "Choisir un rôle"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.select-website": [
|
"label.select-website": [
|
||||||
@ -1080,13 +1086,13 @@
|
|||||||
"label.transfer": [
|
"label.transfer": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer"
|
"value": "Transférer"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.transfer-website": [
|
"label.transfer-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer website"
|
"value": "Transférer le site"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.true": [
|
"label.true": [
|
||||||
@ -1224,7 +1230,7 @@
|
|||||||
"message.action-confirmation": [
|
"message.action-confirmation": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Type "
|
"value": "Taper "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1232,7 +1238,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": " in the box below to confirm."
|
"value": " ci-dessous pour confirmer."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.active-users": [
|
"message.active-users": [
|
||||||
@ -1304,7 +1310,7 @@
|
|||||||
"message.confirm-remove": [
|
"message.confirm-remove": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Are you sure you want to remove "
|
"value": "Êtes-vous sûr de vouloir retirer "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1332,7 +1338,7 @@
|
|||||||
"message.delete-team-warning": [
|
"message.delete-team-warning": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Deleting a team will also delete all team websites."
|
"value": "Supprimer une équipe supprimera aussi tous les sites de cette équipe."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.delete-website-warning": [
|
"message.delete-website-warning": [
|
||||||
@ -1520,19 +1526,19 @@
|
|||||||
"message.transfer-team-website-to-user": [
|
"message.transfer-team-website-to-user": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer this website to your account?"
|
"value": "Transférer ce site sur votre compte ?"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.transfer-user-website-to-team": [
|
"message.transfer-user-website-to-team": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Select the team to transfer this website to."
|
"value": "Choisir l'équipe à laquelle transférer ce site."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.transfer-website": [
|
"message.transfer-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer website ownership to your account or another team."
|
"value": "Transférer la propriété du site sur votre compte ou à une autre équipe."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.triggered-event": [
|
"message.triggered-event": [
|
||||||
@ -1550,7 +1556,7 @@
|
|||||||
"message.viewed-page": [
|
"message.viewed-page": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Viewed page"
|
"value": "Page vue"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.visitor-log": [
|
"message.visitor-log": [
|
||||||
@ -1590,7 +1596,7 @@
|
|||||||
"message.visitors-dropped-off": [
|
"message.visitors-dropped-off": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Visitors dropped off"
|
"value": "Visiteurs ont abandonné"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
"label.add-member": [
|
"label.add-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Add member"
|
"value": "メンバーの追加"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.add-website": [
|
"label.add-website": [
|
||||||
@ -188,7 +188,7 @@
|
|||||||
"label.create": [
|
"label.create": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Create"
|
"value": "作成"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.create-report": [
|
"label.create-report": [
|
||||||
@ -215,6 +215,12 @@
|
|||||||
"value": "作成されました"
|
"value": "作成されました"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
@ -272,7 +278,7 @@
|
|||||||
"label.delete-report": [
|
"label.delete-report": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Delete report"
|
"value": "レポートの削除"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.delete-team": [
|
"label.delete-team": [
|
||||||
@ -362,7 +368,7 @@
|
|||||||
"label.edit-member": [
|
"label.edit-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Edit member"
|
"value": "メンバーの編集"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.enable-share-url": [
|
"label.enable-share-url": [
|
||||||
@ -410,13 +416,13 @@
|
|||||||
"label.filter": [
|
"label.filter": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Filter"
|
"value": "フィルター"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.filter-combined": [
|
"label.filter-combined": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "統合"
|
"value": "結合"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.filter-raw": [
|
"label.filter-raw": [
|
||||||
@ -434,13 +440,13 @@
|
|||||||
"label.funnel": [
|
"label.funnel": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "分析"
|
"value": "ファネル"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.funnel-description": [
|
"label.funnel-description": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Understand the conversion and drop-off rate of users."
|
"value": "ユーザーのコンバージョン率と離脱率を分析します。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.greater-than": [
|
"label.greater-than": [
|
||||||
@ -458,13 +464,13 @@
|
|||||||
"label.insights": [
|
"label.insights": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "見通し"
|
"value": "インサイト"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.insights-description": [
|
"label.insights-description": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Dive deeper into your data by using segments and filters."
|
"value": "セグメントとフィルタを使用して、データをさらに詳しく分析します。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.is": [
|
"label.is": [
|
||||||
@ -588,7 +594,7 @@
|
|||||||
"label.manage": [
|
"label.manage": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Manage"
|
"value": "管理"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.max": [
|
"label.max": [
|
||||||
@ -600,7 +606,7 @@
|
|||||||
"label.member": [
|
"label.member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Member"
|
"value": "メンバー"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.members": [
|
"label.members": [
|
||||||
@ -630,7 +636,7 @@
|
|||||||
"label.my-account": [
|
"label.my-account": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "My account"
|
"value": "マイアカウント"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.my-websites": [
|
"label.my-websites": [
|
||||||
@ -842,7 +848,7 @@
|
|||||||
"label.remove-member": [
|
"label.remove-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Remove member"
|
"value": "メンバーの削除"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.reports": [
|
"label.reports": [
|
||||||
@ -872,13 +878,13 @@
|
|||||||
"label.retention": [
|
"label.retention": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "保持"
|
"value": "リテンション"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.retention-description": [
|
"label.retention-description": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Measure your website stickiness by tracking how often users return."
|
"value": "ユーザーの再訪問回数を記録して、Webサイトのリテンション率を計測します。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.role": [
|
"label.role": [
|
||||||
@ -908,13 +914,13 @@
|
|||||||
"label.search": [
|
"label.search": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Search"
|
"value": "検索"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.select": [
|
"label.select": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Select"
|
"value": "選択"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.select-date": [
|
"label.select-date": [
|
||||||
@ -926,7 +932,7 @@
|
|||||||
"label.select-role": [
|
"label.select-role": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Select role"
|
"value": "ロールを選択"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.select-website": [
|
"label.select-website": [
|
||||||
@ -1004,7 +1010,7 @@
|
|||||||
"label.team-view-only": [
|
"label.team-view-only": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Team view only"
|
"value": "チーム表示のみ"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.team-websites": [
|
"label.team-websites": [
|
||||||
@ -1088,13 +1094,13 @@
|
|||||||
"label.transfer": [
|
"label.transfer": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer"
|
"value": "移管"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.transfer-website": [
|
"label.transfer-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer website"
|
"value": "Webサイトの移管"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.true": [
|
"label.true": [
|
||||||
@ -1232,7 +1238,7 @@
|
|||||||
"message.action-confirmation": [
|
"message.action-confirmation": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Type "
|
"value": "承認する場合は、下のフォームに「"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1240,7 +1246,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": " in the box below to confirm."
|
"value": "」と入力してください。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.active-users": [
|
"message.active-users": [
|
||||||
@ -1298,17 +1304,13 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.confirm-remove": [
|
"message.confirm-remove": [
|
||||||
{
|
|
||||||
"type": 0,
|
|
||||||
"value": "Are you sure you want to remove "
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
"value": "target"
|
"value": "target"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "?"
|
"value": "を削除してもよろしいですか?"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.confirm-reset": [
|
"message.confirm-reset": [
|
||||||
@ -1324,7 +1326,7 @@
|
|||||||
"message.delete-team-warning": [
|
"message.delete-team-warning": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Deleting a team will also delete all team websites."
|
"value": "チームを削除すると、そのチームが管理しているWebサイトもすべて削除されます。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.delete-website-warning": [
|
"message.delete-website-warning": [
|
||||||
@ -1526,25 +1528,25 @@
|
|||||||
"message.transfer-team-website-to-user": [
|
"message.transfer-team-website-to-user": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer this website to your account?"
|
"value": "このWebサイトをあなたのアカウントに移管しますか?"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.transfer-user-website-to-team": [
|
"message.transfer-user-website-to-team": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Select the team to transfer this website to."
|
"value": "このWebサイトを移管するチームを選択してください。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.transfer-website": [
|
"message.transfer-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer website ownership to your account or another team."
|
"value": "Webサイトの所有権を自分のアカウントまたは別のチームへ移管します。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.triggered-event": [
|
"message.triggered-event": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Triggered event"
|
"value": "トリガーされたイベント"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.user-deleted": [
|
"message.user-deleted": [
|
||||||
@ -1556,7 +1558,7 @@
|
|||||||
"message.viewed-page": [
|
"message.viewed-page": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Viewed page"
|
"value": "閲覧されたページ"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.visitor-log": [
|
"message.visitor-log": [
|
||||||
@ -1596,7 +1598,7 @@
|
|||||||
"message.visitors-dropped-off": [
|
"message.visitors-dropped-off": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Visitors dropped off"
|
"value": "訪問者の離脱率"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Үүсгэсэн"
|
"value": "Үүсгэсэн"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "ပြုလုပ်ပြီးသော"
|
"value": "ပြုလုပ်ပြီးသော"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Gemaakt"
|
"value": "Gemaakt"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Utworzony"
|
"value": "Utworzony"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Criado"
|
"value": "Criado"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -215,6 +215,12 @@
|
|||||||
"value": "Создано"
|
"value": "Создано"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Ustvarjeno"
|
"value": "Ustvarjeno"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Skapad"
|
"value": "Skapad"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
"label.add-member": [
|
"label.add-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Add member"
|
"value": "添加成员"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.add-website": [
|
"label.add-website": [
|
||||||
@ -215,6 +215,12 @@
|
|||||||
"value": "已创建"
|
"value": "已创建"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "创建者"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
@ -272,7 +278,7 @@
|
|||||||
"label.delete-report": [
|
"label.delete-report": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Delete report"
|
"value": "删除报告"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.delete-team": [
|
"label.delete-team": [
|
||||||
@ -362,7 +368,7 @@
|
|||||||
"label.edit-member": [
|
"label.edit-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Edit member"
|
"value": "编辑成员"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.enable-share-url": [
|
"label.enable-share-url": [
|
||||||
@ -588,7 +594,7 @@
|
|||||||
"label.manage": [
|
"label.manage": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Manage"
|
"value": "管理"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.max": [
|
"label.max": [
|
||||||
@ -600,7 +606,7 @@
|
|||||||
"label.member": [
|
"label.member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Member"
|
"value": "成员"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.members": [
|
"label.members": [
|
||||||
@ -630,7 +636,7 @@
|
|||||||
"label.my-account": [
|
"label.my-account": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "My account"
|
"value": "我的账户"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.my-websites": [
|
"label.my-websites": [
|
||||||
@ -700,7 +706,7 @@
|
|||||||
"label.os": [
|
"label.os": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "OS"
|
"value": "操作系统"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.overview": [
|
"label.overview": [
|
||||||
@ -850,7 +856,7 @@
|
|||||||
"label.remove-member": [
|
"label.remove-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Remove member"
|
"value": "移除成员"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.reports": [
|
"label.reports": [
|
||||||
@ -922,7 +928,7 @@
|
|||||||
"label.select": [
|
"label.select": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Select"
|
"value": "选择"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.select-date": [
|
"label.select-date": [
|
||||||
@ -934,7 +940,7 @@
|
|||||||
"label.select-role": [
|
"label.select-role": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Select role"
|
"value": "选择角色"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.select-website": [
|
"label.select-website": [
|
||||||
@ -1012,7 +1018,7 @@
|
|||||||
"label.team-view-only": [
|
"label.team-view-only": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Team view only"
|
"value": "仅团队视图"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.team-websites": [
|
"label.team-websites": [
|
||||||
@ -1096,13 +1102,13 @@
|
|||||||
"label.transfer": [
|
"label.transfer": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer"
|
"value": "转移"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.transfer-website": [
|
"label.transfer-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer website"
|
"value": "转移网站"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.true": [
|
"label.true": [
|
||||||
@ -1240,7 +1246,7 @@
|
|||||||
"message.action-confirmation": [
|
"message.action-confirmation": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Type "
|
"value": "在下面的框中输入 "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1248,7 +1254,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": " in the box below to confirm."
|
"value": " 以确认。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.active-users": [
|
"message.active-users": [
|
||||||
@ -1296,7 +1302,7 @@
|
|||||||
"message.confirm-remove": [
|
"message.confirm-remove": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Are you sure you want to remove "
|
"value": "您确定要移除 "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1304,7 +1310,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "?"
|
"value": " ?"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.confirm-reset": [
|
"message.confirm-reset": [
|
||||||
@ -1318,13 +1324,13 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": " 的数据吗?"
|
"value": " 的数据吗?"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.delete-team-warning": [
|
"message.delete-team-warning": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Deleting a team will also delete all team websites."
|
"value": "删除团队也会删除所有团队的网站。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.delete-website-warning": [
|
"message.delete-website-warning": [
|
||||||
@ -1456,7 +1462,7 @@
|
|||||||
"message.reset-website": [
|
"message.reset-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "如果确定重置该网站, 请在下面的输入框中输入 "
|
"value": "如果确定重置该网站,请在下面的输入框中输入 "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1520,25 +1526,25 @@
|
|||||||
"message.transfer-team-website-to-user": [
|
"message.transfer-team-website-to-user": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer this website to your account?"
|
"value": "将该网站转入您的账户?"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.transfer-user-website-to-team": [
|
"message.transfer-user-website-to-team": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Select the team to transfer this website to."
|
"value": "选择要将该网站转移到哪个团队。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.transfer-website": [
|
"message.transfer-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer website ownership to your account or another team."
|
"value": "将网站所有权转移到您的账户或其他团队。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.triggered-event": [
|
"message.triggered-event": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Triggered event"
|
"value": "触发事件"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.user-deleted": [
|
"message.user-deleted": [
|
||||||
@ -1550,7 +1556,7 @@
|
|||||||
"message.viewed-page": [
|
"message.viewed-page": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Viewed page"
|
"value": "已浏览页面"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.visitor-log": [
|
"message.visitor-log": [
|
||||||
@ -1594,7 +1600,7 @@
|
|||||||
"message.visitors-dropped-off": [
|
"message.visitors-dropped-off": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Visitors dropped off"
|
"value": "访客减少"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "已建立"
|
"value": "已建立"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -10,7 +10,8 @@ export default {
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
replace({
|
replace({
|
||||||
'/api/send': process.env.COLLECT_API_ENDPOINT || '/api/send',
|
'__COLLECT_API_HOST__': process.env.COLLECT_API_HOST || '',
|
||||||
|
'__COLLECT_API_ENDPOINT__': process.env.COLLECT_API_ENDPOINT || '/api/send',
|
||||||
delimiters: ['', ''],
|
delimiters: ['', ''],
|
||||||
preventAssignment: true,
|
preventAssignment: true,
|
||||||
}),
|
}),
|
||||||
|
@ -14,7 +14,7 @@ import styles from './NavBar.module.css';
|
|||||||
export function NavBar() {
|
export function NavBar() {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { pathname, router } = useNavigation();
|
const { pathname, router } = useNavigation();
|
||||||
const { renderTeamUrl } = useTeamUrl();
|
const { teamId, renderTeamUrl } = useTeamUrl();
|
||||||
|
|
||||||
const cloudMode = !!process.env.cloudMode;
|
const cloudMode = !!process.env.cloudMode;
|
||||||
|
|
||||||
@ -34,25 +34,38 @@ export function NavBar() {
|
|||||||
label: formatMessage(labels.settings),
|
label: formatMessage(labels.settings),
|
||||||
url: renderTeamUrl('/settings'),
|
url: renderTeamUrl('/settings'),
|
||||||
children: [
|
children: [
|
||||||
|
...(teamId
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: formatMessage(labels.team),
|
||||||
|
url: renderTeamUrl('/settings/team'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
label: formatMessage(labels.websites),
|
label: formatMessage(labels.websites),
|
||||||
url: '/settings/websites',
|
url: renderTeamUrl('/settings/websites'),
|
||||||
},
|
},
|
||||||
|
...(!teamId
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
label: formatMessage(labels.teams),
|
label: formatMessage(labels.teams),
|
||||||
url: '/settings/teams',
|
url: renderTeamUrl('/settings/teams'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: formatMessage(labels.users),
|
label: formatMessage(labels.users),
|
||||||
url: '/settings/users',
|
url: '/settings/users',
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
{
|
{
|
||||||
label: formatMessage(labels.profile),
|
label: formatMessage(labels.members),
|
||||||
url: '/profile',
|
url: renderTeamUrl('/settings/members'),
|
||||||
},
|
},
|
||||||
|
]),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
cloudMode && {
|
{
|
||||||
label: formatMessage(labels.profile),
|
label: formatMessage(labels.profile),
|
||||||
url: '/profile',
|
url: '/profile',
|
||||||
},
|
},
|
||||||
@ -94,6 +107,7 @@ export function NavBar() {
|
|||||||
<ProfileButton />
|
<ProfileButton />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.mobile}>
|
<div className={styles.mobile}>
|
||||||
|
<TeamsButton onChange={handleTeamChange} showText={false} />
|
||||||
<HamburgerButton menuItems={menuItems} />
|
<HamburgerButton menuItems={menuItems} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
.notice {
|
.notice {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
margin: 80px auto;
|
margin: 60px auto;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
background: var(--base50);
|
background: var(--base50);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border: 1px solid var(--base300);
|
border: 1px solid var(--base300);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
z-index: var(--z-index-popup);
|
z-index: 9999;
|
||||||
box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,9 +4,9 @@ import { Button } from 'react-basics';
|
|||||||
import { setItem } from 'next-basics';
|
import { setItem } from 'next-basics';
|
||||||
import useStore, { checkVersion } from 'store/version';
|
import useStore, { checkVersion } from 'store/version';
|
||||||
import { REPO_URL, VERSION_CHECK } from 'lib/constants';
|
import { REPO_URL, VERSION_CHECK } from 'lib/constants';
|
||||||
import styles from './UpdateNotice.module.css';
|
|
||||||
import { useMessages } from 'components/hooks';
|
import { useMessages } from 'components/hooks';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
import styles from './UpdateNotice.module.css';
|
||||||
|
|
||||||
export function UpdateNotice({ user, config }) {
|
export function UpdateNotice({ user, config }) {
|
||||||
const { formatMessage, labels, messages } = useMessages();
|
const { formatMessage, labels, messages } = useMessages();
|
||||||
@ -16,8 +16,9 @@ export function UpdateNotice({ user, config }) {
|
|||||||
const allowUpdate =
|
const allowUpdate =
|
||||||
user?.isAdmin &&
|
user?.isAdmin &&
|
||||||
!config?.updatesDisabled &&
|
!config?.updatesDisabled &&
|
||||||
!config?.cloudMode &&
|
|
||||||
!pathname.includes('/share/') &&
|
!pathname.includes('/share/') &&
|
||||||
|
!process.env.cloudMode &&
|
||||||
|
!process.env.privateMode &&
|
||||||
!dismissed;
|
!dismissed;
|
||||||
|
|
||||||
const updateCheck = useCallback(() => {
|
const updateCheck = useCallback(() => {
|
||||||
|
3
src/app/(main)/profile/DateRangeSetting.module.css
Normal file
3
src/app/(main)/profile/DateRangeSetting.module.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.field {
|
||||||
|
width: 200px;
|
||||||
|
}
|
@ -3,6 +3,7 @@ import { Button, Flexbox } from 'react-basics';
|
|||||||
import { useDateRange, useMessages } from 'components/hooks';
|
import { useDateRange, useMessages } from 'components/hooks';
|
||||||
import { DEFAULT_DATE_RANGE } from 'lib/constants';
|
import { DEFAULT_DATE_RANGE } from 'lib/constants';
|
||||||
import { DateRange } from 'lib/types';
|
import { DateRange } from 'lib/types';
|
||||||
|
import styles from './DateRangeSetting.module.css';
|
||||||
|
|
||||||
export function DateRangeSetting() {
|
export function DateRangeSetting() {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
@ -13,8 +14,9 @@ export function DateRangeSetting() {
|
|||||||
const handleReset = () => setDateRange(DEFAULT_DATE_RANGE);
|
const handleReset = () => setDateRange(DEFAULT_DATE_RANGE);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flexbox gap={10}>
|
<Flexbox gap={10} width={300}>
|
||||||
<DateFilter
|
<DateFilter
|
||||||
|
className={styles.field}
|
||||||
value={value}
|
value={value}
|
||||||
startDate={dateRange.startDate}
|
startDate={dateRange.startDate}
|
||||||
endDate={dateRange.endDate}
|
endDate={dateRange.endDate}
|
||||||
|
@ -20,7 +20,7 @@ export function LanguageSetting() {
|
|||||||
|
|
||||||
const handleReset = () => saveLocale(DEFAULT_LOCALE);
|
const handleReset = () => saveLocale(DEFAULT_LOCALE);
|
||||||
|
|
||||||
const renderValue = (value: string | number) => languages[value].label;
|
const renderValue = (value: string | number) => languages?.[value]?.label;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flexbox gap={10}>
|
<Flexbox gap={10}>
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
.dropdown {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
div.menu {
|
div.menu {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
@ -1,26 +1,29 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Dropdown, Item, Button, Flexbox } from 'react-basics';
|
import { Dropdown, Item, Button, Flexbox } from 'react-basics';
|
||||||
import { listTimeZones } from 'timezone-support';
|
import moment from 'moment-timezone';
|
||||||
import { useTimezone, useMessages } from 'components/hooks';
|
import { useTimezone, useMessages } from 'components/hooks';
|
||||||
import { getTimezone } from 'lib/date';
|
import { getTimezone } from 'lib/date';
|
||||||
import styles from './TimezoneSetting.module.css';
|
import styles from './TimezoneSetting.module.css';
|
||||||
|
|
||||||
|
const timezones = moment.tz.names();
|
||||||
|
|
||||||
export function TimezoneSetting() {
|
export function TimezoneSetting() {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const [timezone, saveTimezone] = useTimezone();
|
const { timezone, saveTimezone } = useTimezone();
|
||||||
const options = search
|
const options = search
|
||||||
? listTimeZones().filter(n => n.toLowerCase().includes(search.toLowerCase()))
|
? timezones.filter(n => n.toLowerCase().includes(search.toLowerCase()))
|
||||||
: listTimeZones();
|
: timezones;
|
||||||
|
|
||||||
const handleReset = () => saveTimezone(getTimezone());
|
const handleReset = () => saveTimezone(getTimezone());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flexbox gap={10}>
|
<Flexbox gap={10}>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
className={styles.dropdown}
|
||||||
items={options}
|
items={options}
|
||||||
value={timezone}
|
value={timezone}
|
||||||
onChange={saveTimezone}
|
onChange={(value: any) => saveTimezone(value)}
|
||||||
menuProps={{ className: styles.menu }}
|
menuProps={{ className: styles.menu }}
|
||||||
allowSearch={true}
|
allowSearch={true}
|
||||||
onSearch={setSearch}
|
onSearch={setSearch}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
import { Metadata } from 'next';
|
||||||
import ReportsHeader from './ReportsHeader';
|
import ReportsHeader from './ReportsHeader';
|
||||||
import ReportsDataTable from './ReportsDataTable';
|
import ReportsDataTable from './ReportsDataTable';
|
||||||
|
|
||||||
@ -10,6 +11,7 @@ export default function ReportsPage({ teamId }: { teamId: string }) {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export const metadata = {
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
title: 'Reports',
|
title: 'Reports',
|
||||||
};
|
};
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
.menu {
|
|
||||||
width: 360px;
|
|
||||||
max-height: 300px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:hover {
|
|
||||||
background: var(--base75);
|
|
||||||
}
|
|
||||||
|
|
||||||
.type {
|
|
||||||
color: var(--font-color300);
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown {
|
|
||||||
min-width: 60px;
|
|
||||||
}
|
|
@ -3,9 +3,6 @@ import { createPortal } from 'react-dom';
|
|||||||
import { REPORT_PARAMETERS } from 'lib/constants';
|
import { REPORT_PARAMETERS } from 'lib/constants';
|
||||||
import PopupForm from './PopupForm';
|
import PopupForm from './PopupForm';
|
||||||
import FieldSelectForm from './FieldSelectForm';
|
import FieldSelectForm from './FieldSelectForm';
|
||||||
import FieldAggregateForm from './FieldAggregateForm';
|
|
||||||
import FieldFilterForm from './FieldFilterForm';
|
|
||||||
import styles from './FieldAddForm.module.css';
|
|
||||||
|
|
||||||
export function FieldAddForm({
|
export function FieldAddForm({
|
||||||
fields = [],
|
fields = [],
|
||||||
@ -18,7 +15,11 @@ export function FieldAddForm({
|
|||||||
onAdd: (group: string, value: string) => void;
|
onAdd: (group: string, value: string) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [selected, setSelected] = useState<{ name: string; type: string; value: string }>();
|
const [selected, setSelected] = useState<{
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
value: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
const handleSelect = (value: any) => {
|
const handleSelect = (value: any) => {
|
||||||
const { type } = value;
|
const { type } = value;
|
||||||
@ -38,14 +39,8 @@ export function FieldAddForm({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<PopupForm className={styles.popup}>
|
<PopupForm>
|
||||||
{!selected && <FieldSelectForm fields={fields} onSelect={handleSelect} />}
|
{!selected && <FieldSelectForm fields={fields} onSelect={handleSelect} />}
|
||||||
{selected && group === REPORT_PARAMETERS.fields && (
|
|
||||||
<FieldAggregateForm {...selected} onSelect={handleSave} />
|
|
||||||
)}
|
|
||||||
{selected && group === REPORT_PARAMETERS.filters && (
|
|
||||||
<FieldFilterForm {...selected} onSelect={handleSave} />
|
|
||||||
)}
|
|
||||||
</PopupForm>,
|
</PopupForm>,
|
||||||
document.body,
|
document.body,
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
.menu {
|
||||||
|
position: absolute;
|
||||||
|
max-width: 300px;
|
||||||
|
max-height: 210px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 16px;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 200px;
|
||||||
|
font-weight: 900;
|
||||||
|
background: var(--base100);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
position: relative;
|
||||||
|
}
|
224
src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx
Normal file
224
src/app/(main)/reports/[reportId]/FieldFilterEditForm.tsx
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormRow,
|
||||||
|
Item,
|
||||||
|
Flexbox,
|
||||||
|
Dropdown,
|
||||||
|
Button,
|
||||||
|
SearchField,
|
||||||
|
TextField,
|
||||||
|
Text,
|
||||||
|
Icon,
|
||||||
|
Icons,
|
||||||
|
Menu,
|
||||||
|
Loading,
|
||||||
|
} from 'react-basics';
|
||||||
|
import { useMessages, useFilters, useFormat, useLocale, useWebsiteValues } from 'components/hooks';
|
||||||
|
import { OPERATORS } from 'lib/constants';
|
||||||
|
import { isEqualsOperator } from 'lib/params';
|
||||||
|
import styles from './FieldFilterEditForm.module.css';
|
||||||
|
|
||||||
|
export interface FieldFilterFormProps {
|
||||||
|
websiteId?: string;
|
||||||
|
name: string;
|
||||||
|
label?: string;
|
||||||
|
type: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
operator?: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
onChange?: (filter: { name: string; type: string; operator: string; value: string }) => void;
|
||||||
|
allowFilterSelect?: boolean;
|
||||||
|
isNew?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FieldFilterEditForm({
|
||||||
|
websiteId,
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
type,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
operator: defaultOperator = 'eq',
|
||||||
|
defaultValue = '',
|
||||||
|
onChange,
|
||||||
|
allowFilterSelect = true,
|
||||||
|
isNew,
|
||||||
|
}: FieldFilterFormProps) {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const [operator, setOperator] = useState(defaultOperator);
|
||||||
|
const [value, setValue] = useState(defaultValue);
|
||||||
|
const [showMenu, setShowMenu] = useState(false);
|
||||||
|
const isEquals = isEqualsOperator(operator);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [selected, setSelected] = useState(isEquals ? value : '');
|
||||||
|
const { filters } = useFilters();
|
||||||
|
const { formatValue } = useFormat();
|
||||||
|
const { locale } = useLocale();
|
||||||
|
const isDisabled = !operator || (isEquals && !selected) || (!isEquals && !value);
|
||||||
|
const {
|
||||||
|
data: values = [],
|
||||||
|
isLoading,
|
||||||
|
refetch,
|
||||||
|
} = useWebsiteValues({
|
||||||
|
websiteId,
|
||||||
|
type: name,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
search,
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedValues = useMemo(() => {
|
||||||
|
if (!values) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const formatted = {};
|
||||||
|
const format = (val: string) => {
|
||||||
|
formatted[val] = formatValue(val, name);
|
||||||
|
return formatted[val];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (values?.length !== 1) {
|
||||||
|
const { compare } = new Intl.Collator(locale, { numeric: true });
|
||||||
|
values.sort((a, b) => compare(formatted[a] ?? format(a), formatted[b] ?? format(b)));
|
||||||
|
} else {
|
||||||
|
format(values[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
}, [formatValue, locale, name, values]);
|
||||||
|
|
||||||
|
const filteredValues = useMemo(() => {
|
||||||
|
return value
|
||||||
|
? values.filter((n: string | number) =>
|
||||||
|
formattedValues[n].toLowerCase().includes(value.toLowerCase()),
|
||||||
|
)
|
||||||
|
: values;
|
||||||
|
}, [value, formattedValues]);
|
||||||
|
|
||||||
|
const renderFilterValue = (value: any) => {
|
||||||
|
return filters.find((filter: { value: any }) => filter.value === value)?.label;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
onChange({ name, type, operator, value: isEquals ? selected : value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuSelect = (value: string) => {
|
||||||
|
setSelected(value);
|
||||||
|
setShowMenu(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = (value: string) => {
|
||||||
|
setSearch(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setSelected('');
|
||||||
|
setValue('');
|
||||||
|
setSearch('');
|
||||||
|
refetch();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOperatorChange = (value: any) => {
|
||||||
|
setOperator(value);
|
||||||
|
|
||||||
|
if ([OPERATORS.equals, OPERATORS.notEquals].includes(value)) {
|
||||||
|
setValue('');
|
||||||
|
} else {
|
||||||
|
setSelected('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
window.setTimeout(() => setShowMenu(false), 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form>
|
||||||
|
<FormRow label={label} className={styles.filter}>
|
||||||
|
<Flexbox gap={10}>
|
||||||
|
{allowFilterSelect && (
|
||||||
|
<Dropdown
|
||||||
|
className={styles.dropdown}
|
||||||
|
items={filters.filter(f => f.type === type)}
|
||||||
|
value={operator}
|
||||||
|
renderValue={renderFilterValue}
|
||||||
|
onChange={handleOperatorChange}
|
||||||
|
>
|
||||||
|
{({ value, label }) => {
|
||||||
|
return <Item key={value}>{label}</Item>;
|
||||||
|
}}
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
|
{selected && isEquals && (
|
||||||
|
<div className={styles.selected} onClick={handleReset}>
|
||||||
|
<Text>{formatValue(selected, name)}</Text>
|
||||||
|
<Icon>
|
||||||
|
<Icons.Close />
|
||||||
|
</Icon>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!selected && isEquals && (
|
||||||
|
<div className={styles.search}>
|
||||||
|
<SearchField
|
||||||
|
className={styles.text}
|
||||||
|
value={value}
|
||||||
|
placeholder={formatMessage(labels.enter)}
|
||||||
|
onChange={e => setValue(e.target.value)}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
delay={500}
|
||||||
|
onFocus={() => setShowMenu(true)}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
/>
|
||||||
|
{showMenu && (
|
||||||
|
<ResultsMenu
|
||||||
|
values={filteredValues}
|
||||||
|
type={name}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onSelect={handleMenuSelect}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!selected && !isEquals && (
|
||||||
|
<TextField
|
||||||
|
className={styles.text}
|
||||||
|
value={value}
|
||||||
|
onChange={e => setValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Flexbox>
|
||||||
|
<Button variant="primary" onClick={handleAdd} disabled={isDisabled}>
|
||||||
|
{formatMessage(isNew ? labels.add : labels.update)}
|
||||||
|
</Button>
|
||||||
|
</FormRow>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResultsMenu = ({ values, type, isLoading, onSelect }) => {
|
||||||
|
const { formatValue } = useFormat();
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Menu className={styles.menu} variant="popup">
|
||||||
|
<Item>
|
||||||
|
<Loading icon="dots" position="center" />
|
||||||
|
</Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu className={styles.menu} variant="popup" onSelect={onSelect}>
|
||||||
|
{values?.map((value: any) => {
|
||||||
|
return <Item key={value}>{formatValue(value, type)}</Item>;
|
||||||
|
})}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
@ -1,24 +0,0 @@
|
|||||||
.popup {
|
|
||||||
display: flex;
|
|
||||||
max-width: 300px;
|
|
||||||
max-height: 400px;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup > div {
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown {
|
|
||||||
min-width: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu {
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
@ -1,102 +0,0 @@
|
|||||||
import { useState, useMemo } from 'react';
|
|
||||||
import { Form, FormRow, Item, Flexbox, Dropdown, Button } from 'react-basics';
|
|
||||||
import { useMessages, useFilters, useFormat, useLocale } from 'components/hooks';
|
|
||||||
import styles from './FieldFilterForm.module.css';
|
|
||||||
|
|
||||||
export interface FieldFilterFormProps {
|
|
||||||
name: string;
|
|
||||||
label?: string;
|
|
||||||
type: string;
|
|
||||||
values?: any[];
|
|
||||||
onSelect?: (key: any) => void;
|
|
||||||
allowFilterSelect?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function FieldFilterForm({
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
type,
|
|
||||||
values,
|
|
||||||
onSelect,
|
|
||||||
allowFilterSelect = true,
|
|
||||||
}: FieldFilterFormProps) {
|
|
||||||
const { formatMessage, labels } = useMessages();
|
|
||||||
const [filter, setFilter] = useState('eq');
|
|
||||||
const [value, setValue] = useState();
|
|
||||||
const { getFilters } = useFilters();
|
|
||||||
const { formatValue } = useFormat();
|
|
||||||
const { locale } = useLocale();
|
|
||||||
const filters = getFilters(type);
|
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
|
|
||||||
const formattedValues = useMemo(() => {
|
|
||||||
const formatted = {};
|
|
||||||
const format = (val: string) => {
|
|
||||||
formatted[val] = formatValue(val, name);
|
|
||||||
return formatted[val];
|
|
||||||
};
|
|
||||||
if (values.length !== 1) {
|
|
||||||
const { compare } = new Intl.Collator(locale, { numeric: true });
|
|
||||||
values.sort((a, b) => compare(formatted[a] ?? format(a), formatted[b] ?? format(b)));
|
|
||||||
} else {
|
|
||||||
format(values[0]);
|
|
||||||
}
|
|
||||||
return formatted;
|
|
||||||
}, [formatValue, locale, name, values]);
|
|
||||||
|
|
||||||
const filteredValues = useMemo(() => {
|
|
||||||
return search ? values.filter(n => n.includes(search)) : values;
|
|
||||||
}, [search, formattedValues]);
|
|
||||||
|
|
||||||
const renderFilterValue = value => {
|
|
||||||
return filters.find(f => f.value === value)?.label;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderValue = value => {
|
|
||||||
return formattedValues[value];
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAdd = () => {
|
|
||||||
onSelect({ name, type, filter, value });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form>
|
|
||||||
<FormRow label={label} className={styles.filter}>
|
|
||||||
<Flexbox gap={10}>
|
|
||||||
{allowFilterSelect && (
|
|
||||||
<Dropdown
|
|
||||||
className={styles.dropdown}
|
|
||||||
items={filters}
|
|
||||||
value={filter}
|
|
||||||
renderValue={renderFilterValue}
|
|
||||||
onChange={(key: any) => setFilter(key)}
|
|
||||||
>
|
|
||||||
{({ value, label }) => {
|
|
||||||
return <Item key={value}>{label}</Item>;
|
|
||||||
}}
|
|
||||||
</Dropdown>
|
|
||||||
)}
|
|
||||||
<Dropdown
|
|
||||||
className={styles.dropdown}
|
|
||||||
popupProps={{ className: styles.popup }}
|
|
||||||
menuProps={{ className: styles.menu }}
|
|
||||||
items={filteredValues}
|
|
||||||
value={value}
|
|
||||||
renderValue={renderValue}
|
|
||||||
onChange={(key: any) => setValue(key)}
|
|
||||||
allowSearch={true}
|
|
||||||
onSearch={setSearch}
|
|
||||||
>
|
|
||||||
{(value: string) => {
|
|
||||||
return <Item key={value}>{formattedValues[value]}</Item>;
|
|
||||||
}}
|
|
||||||
</Dropdown>
|
|
||||||
</Flexbox>
|
|
||||||
<Button variant="primary" onClick={handleAdd} disabled={!filter || !value}>
|
|
||||||
{formatMessage(labels.add)}
|
|
||||||
</Button>
|
|
||||||
</FormRow>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
63
src/app/(main)/reports/[reportId]/FieldParameters.tsx
Normal file
63
src/app/(main)/reports/[reportId]/FieldParameters.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { useFields, useMessages } from 'components/hooks';
|
||||||
|
import Icons from 'components/icons';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { Button, FormRow, Icon, Popup, PopupTrigger } from 'react-basics';
|
||||||
|
import FieldSelectForm from '../[reportId]/FieldSelectForm';
|
||||||
|
import ParameterList from '../[reportId]/ParameterList';
|
||||||
|
import PopupForm from '../[reportId]/PopupForm';
|
||||||
|
import { ReportContext } from './Report';
|
||||||
|
|
||||||
|
export function FieldParameters() {
|
||||||
|
const { report, updateReport } = useContext(ReportContext);
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { parameters } = report || {};
|
||||||
|
const { fields } = parameters || {};
|
||||||
|
const { fields: fieldOptions } = useFields();
|
||||||
|
|
||||||
|
const handleAdd = (value: { name: any }) => {
|
||||||
|
if (!fields.find(({ name }) => name === value.name)) {
|
||||||
|
updateReport({ parameters: { fields: fields.concat(value) } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = (name: string) => {
|
||||||
|
updateReport({ parameters: { fields: fields.filter(f => f.name !== name) } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddButton = () => {
|
||||||
|
return (
|
||||||
|
<PopupTrigger>
|
||||||
|
<Button size="sm">
|
||||||
|
<Icon>
|
||||||
|
<Icons.Plus />
|
||||||
|
</Icon>
|
||||||
|
</Button>
|
||||||
|
<Popup position="bottom" alignment="start">
|
||||||
|
<PopupForm>
|
||||||
|
<FieldSelectForm
|
||||||
|
fields={fieldOptions.filter(({ name }) => !fields.find(f => f.name === name))}
|
||||||
|
onSelect={handleAdd}
|
||||||
|
showType={false}
|
||||||
|
/>
|
||||||
|
</PopupForm>
|
||||||
|
</Popup>
|
||||||
|
</PopupTrigger>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormRow label={formatMessage(labels.fields)} action={<AddButton />}>
|
||||||
|
<ParameterList>
|
||||||
|
{fields.map(({ name }) => {
|
||||||
|
return (
|
||||||
|
<ParameterList.Item key={name} onRemove={() => handleRemove(name)}>
|
||||||
|
{fieldOptions.find(f => f.name === name)?.label}
|
||||||
|
</ParameterList.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ParameterList>
|
||||||
|
</FormRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FieldParameters;
|
@ -0,0 +1,40 @@
|
|||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: var(--base800);
|
||||||
|
border: 1px solid var(--base300);
|
||||||
|
font-weight: 900;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op {
|
||||||
|
color: var(--blue900);
|
||||||
|
background-color: var(--blue100);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 900;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: var(--base900);
|
||||||
|
background-color: var(--base100);
|
||||||
|
font-weight: 900;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
136
src/app/(main)/reports/[reportId]/FilterParameters.tsx
Normal file
136
src/app/(main)/reports/[reportId]/FilterParameters.tsx
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { useMessages, useFormat, useFilters, useFields } from 'components/hooks';
|
||||||
|
import Icons from 'components/icons';
|
||||||
|
import { Button, FormRow, Icon, Popup, PopupTrigger } from 'react-basics';
|
||||||
|
import FilterSelectForm from '../[reportId]/FilterSelectForm';
|
||||||
|
import ParameterList from '../[reportId]/ParameterList';
|
||||||
|
import PopupForm from '../[reportId]/PopupForm';
|
||||||
|
import { ReportContext } from './Report';
|
||||||
|
import FieldFilterEditForm from '../[reportId]/FieldFilterEditForm';
|
||||||
|
import { isSearchOperator } from 'lib/params';
|
||||||
|
import styles from './FilterParameters.module.css';
|
||||||
|
|
||||||
|
export function FilterParameters() {
|
||||||
|
const { report, updateReport } = useContext(ReportContext);
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { formatValue } = useFormat();
|
||||||
|
const { parameters } = report || {};
|
||||||
|
const { websiteId, filters, dateRange } = parameters || {};
|
||||||
|
const { fields } = useFields();
|
||||||
|
|
||||||
|
const handleAdd = (value: { name: any }) => {
|
||||||
|
if (!filters.find(({ name }) => name === value.name)) {
|
||||||
|
updateReport({ parameters: { filters: filters.concat(value) } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = (name: string) => {
|
||||||
|
updateReport({ parameters: { filters: filters.filter(f => f.name !== name) } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (close: () => void, filter: { name: any }) => {
|
||||||
|
updateReport({
|
||||||
|
parameters: {
|
||||||
|
filters: filters.map(f => {
|
||||||
|
if (filter.name === f.name) {
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddButton = () => {
|
||||||
|
return (
|
||||||
|
<PopupTrigger>
|
||||||
|
<Button size="sm">
|
||||||
|
<Icon>
|
||||||
|
<Icons.Plus />
|
||||||
|
</Icon>
|
||||||
|
</Button>
|
||||||
|
<Popup position="bottom" alignment="start">
|
||||||
|
<PopupForm>
|
||||||
|
<FilterSelectForm
|
||||||
|
websiteId={websiteId}
|
||||||
|
fields={fields.filter(({ name }) => !filters.find(f => f.name === name))}
|
||||||
|
onChange={handleAdd}
|
||||||
|
/>
|
||||||
|
</PopupForm>
|
||||||
|
</Popup>
|
||||||
|
</PopupTrigger>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormRow label={formatMessage(labels.filters)} action={<AddButton />}>
|
||||||
|
<ParameterList>
|
||||||
|
{filters.map(
|
||||||
|
({ name, operator, value }: { name: string; operator: string; value: string }) => {
|
||||||
|
const label = fields.find(f => f.name === name)?.label;
|
||||||
|
const isSearch = isSearchOperator(operator);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ParameterList.Item key={name} onRemove={() => handleRemove(name)}>
|
||||||
|
<FilterParameter
|
||||||
|
startDate={dateRange.startDate}
|
||||||
|
endDate={dateRange.endDate}
|
||||||
|
websiteId={websiteId}
|
||||||
|
name={name}
|
||||||
|
label={label}
|
||||||
|
operator={operator}
|
||||||
|
value={isSearch ? value : formatValue(value, name)}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</ParameterList.Item>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</ParameterList>
|
||||||
|
</FormRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FilterParameter = ({
|
||||||
|
websiteId,
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
operator,
|
||||||
|
value,
|
||||||
|
type = 'string',
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const { operatorLabels } = useFilters();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopupTrigger>
|
||||||
|
<div className={styles.item}>
|
||||||
|
<div className={styles.label}>{label}</div>
|
||||||
|
<div className={styles.op}>{operatorLabels[operator]}</div>
|
||||||
|
<div className={styles.value}>{value}</div>
|
||||||
|
</div>
|
||||||
|
<Popup className={styles.edit} alignment="start">
|
||||||
|
{(close: any) => (
|
||||||
|
<PopupForm>
|
||||||
|
<FieldFilterEditForm
|
||||||
|
websiteId={websiteId}
|
||||||
|
name={name}
|
||||||
|
label={label}
|
||||||
|
type={type}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
operator={operator}
|
||||||
|
defaultValue={value}
|
||||||
|
onChange={onChange.bind(null, close)}
|
||||||
|
/>
|
||||||
|
</PopupForm>
|
||||||
|
)}
|
||||||
|
</Popup>
|
||||||
|
</PopupTrigger>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilterParameters;
|
@ -1,59 +1,41 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Loading } from 'react-basics';
|
|
||||||
import { subDays } from 'date-fns';
|
|
||||||
import FieldSelectForm from './FieldSelectForm';
|
import FieldSelectForm from './FieldSelectForm';
|
||||||
import FieldFilterForm from './FieldFilterForm';
|
import FieldFilterEditForm from './FieldFilterEditForm';
|
||||||
import { useApi } from 'components/hooks';
|
import { useDateRange } from 'components/hooks';
|
||||||
|
|
||||||
function useValues(websiteId: string, type: string) {
|
|
||||||
const now = Date.now();
|
|
||||||
const { get, useQuery } = useApi();
|
|
||||||
const { data, error, isLoading } = useQuery({
|
|
||||||
queryKey: ['websites:values', websiteId, type],
|
|
||||||
queryFn: () =>
|
|
||||||
get(`/websites/${websiteId}/values`, {
|
|
||||||
type,
|
|
||||||
startAt: +subDays(now, 90),
|
|
||||||
endAt: now,
|
|
||||||
}),
|
|
||||||
enabled: !!(websiteId && type),
|
|
||||||
});
|
|
||||||
|
|
||||||
return { data, error, isLoading };
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FilterSelectFormProps {
|
export interface FilterSelectFormProps {
|
||||||
websiteId: string;
|
websiteId?: string;
|
||||||
items: any[];
|
fields: any[];
|
||||||
onSelect?: (key: any) => void;
|
onChange?: (filter: { name: string; type: string; operator: string; value: string }) => void;
|
||||||
allowFilterSelect?: boolean;
|
allowFilterSelect?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FilterSelectForm({
|
export default function FilterSelectForm({
|
||||||
websiteId,
|
websiteId,
|
||||||
items,
|
fields,
|
||||||
onSelect,
|
onChange,
|
||||||
allowFilterSelect,
|
allowFilterSelect,
|
||||||
}: FilterSelectFormProps) {
|
}: FilterSelectFormProps) {
|
||||||
const [field, setField] = useState<{ name: string; label: string; type: string }>();
|
const [field, setField] = useState<{ name: string; label: string; type: string }>();
|
||||||
const { data, isLoading } = useValues(websiteId, field?.name);
|
const [{ startDate, endDate }] = useDateRange(websiteId);
|
||||||
|
|
||||||
if (!field) {
|
if (!field) {
|
||||||
return <FieldSelectForm fields={items} onSelect={setField} showType={false} />;
|
return <FieldSelectForm fields={fields} onSelect={setField} showType={false} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
const { name, label, type } = field;
|
||||||
return <Loading position="center" icon="dots" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldFilterForm
|
<FieldFilterEditForm
|
||||||
name={field?.name}
|
websiteId={websiteId}
|
||||||
label={field?.label}
|
name={name}
|
||||||
type={field?.type}
|
label={label}
|
||||||
values={data}
|
type={type}
|
||||||
onSelect={onSelect}
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
onChange={onChange}
|
||||||
allowFilterSelect={allowFilterSelect}
|
allowFilterSelect={allowFilterSelect}
|
||||||
|
isNew={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -13,9 +13,4 @@
|
|||||||
border: 1px solid var(--base400);
|
border: 1px solid var(--base400);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
box-shadow: 1px 1px 1px var(--base400);
|
box-shadow: 1px 1px 1px var(--base400);
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
align-self: center;
|
|
||||||
}
|
}
|
||||||
|
@ -1,40 +1,47 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Icon, TooltipPopup } from 'react-basics';
|
import { Icon } from 'react-basics';
|
||||||
import Icons from 'components/icons';
|
import Icons from 'components/icons';
|
||||||
import Empty from 'components/common/Empty';
|
import Empty from 'components/common/Empty';
|
||||||
import { useMessages } from 'components/hooks';
|
import { useMessages } from 'components/hooks';
|
||||||
import styles from './ParameterList.module.css';
|
import styles from './ParameterList.module.css';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
export interface ParameterListProps {
|
export interface ParameterListProps {
|
||||||
items: any[];
|
children?: ReactNode;
|
||||||
children?: ReactNode | ((item: any) => ReactNode);
|
|
||||||
onRemove: (index: number, e: any) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ParameterList({ items = [], children, onRemove }: ParameterListProps) {
|
export function ParameterList({ children }: ParameterListProps) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.list}>
|
<div className={styles.list}>
|
||||||
{!items.length && <Empty message={formatMessage(labels.none)} />}
|
{!children && <Empty message={formatMessage(labels.none)} />}
|
||||||
{items.map((item, index) => {
|
{children}
|
||||||
return (
|
|
||||||
<div key={index} className={styles.item}>
|
|
||||||
{typeof children === 'function' ? children(item) : item}
|
|
||||||
<TooltipPopup
|
|
||||||
className={styles.icon}
|
|
||||||
label={formatMessage(labels.remove)}
|
|
||||||
position="right"
|
|
||||||
>
|
|
||||||
<Icon onClick={onRemove.bind(null, index)}>
|
|
||||||
<Icons.Close />
|
|
||||||
</Icon>
|
|
||||||
</TooltipPopup>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Item = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
onClick,
|
||||||
|
onRemove,
|
||||||
|
}: {
|
||||||
|
children?: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
onRemove?: () => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={classNames(styles.item, className)} onClick={onClick}>
|
||||||
|
{children}
|
||||||
|
<Icon onClick={onRemove}>
|
||||||
|
<Icons.Close />
|
||||||
|
</Icon>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ParameterList.Item = Item;
|
||||||
|
|
||||||
export default ParameterList;
|
export default ParameterList;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
.form {
|
.form {
|
||||||
position: absolute;
|
|
||||||
background: var(--base50);
|
background: var(--base50);
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
@ -2,4 +2,5 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: max-content 1fr;
|
grid-template-rows: max-content 1fr;
|
||||||
grid-template-columns: max-content 1fr;
|
grid-template-columns: max-content 1fr;
|
||||||
|
margin-bottom: 60px;
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ export function Report({
|
|||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
reportId: string;
|
reportId: string;
|
||||||
defaultParameters: { [key: string]: any };
|
defaultParameters: { type: string; parameters: { [key: string]: any } };
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
import FunnelReport from '../funnel/FunnelReport';
|
|
||||||
import EventDataReport from '../event-data/EventDataReport';
|
|
||||||
import InsightsReport from '../insights/InsightsReport';
|
|
||||||
import RetentionReport from '../retention/RetentionReport';
|
|
||||||
import { useApi } from 'components/hooks';
|
|
||||||
|
|
||||||
const reports = {
|
|
||||||
funnel: FunnelReport,
|
|
||||||
'event-data': EventDataReport,
|
|
||||||
insights: InsightsReport,
|
|
||||||
retention: RetentionReport,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ReportDetails({ reportId }: { reportId: string }) {
|
|
||||||
const { get, useQuery } = useApi();
|
|
||||||
const { data: report } = useQuery({
|
|
||||||
queryKey: ['reports', reportId],
|
|
||||||
queryFn: () => get(`/reports/${reportId}`),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!report) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReportComponent = reports[report.type];
|
|
||||||
|
|
||||||
return <ReportComponent reportId={reportId} />;
|
|
||||||
}
|
|
@ -1,6 +1,27 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import ReportDetails from './ReportDetails';
|
import FunnelReport from '../funnel/FunnelReport';
|
||||||
|
import EventDataReport from '../event-data/EventDataReport';
|
||||||
|
import InsightsReport from '../insights/InsightsReport';
|
||||||
|
import RetentionReport from '../retention/RetentionReport';
|
||||||
|
import UTMReport from '../utm/UTMReport';
|
||||||
|
import { useReport } from 'components/hooks';
|
||||||
|
|
||||||
export default function ReportPage({ reportId }) {
|
const reports = {
|
||||||
return <ReportDetails reportId={reportId} />;
|
funnel: FunnelReport,
|
||||||
|
'event-data': EventDataReport,
|
||||||
|
insights: InsightsReport,
|
||||||
|
retention: RetentionReport,
|
||||||
|
utm: UTMReport,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ReportPage({ reportId }: { reportId: string }) {
|
||||||
|
const { report } = useReport(reportId);
|
||||||
|
|
||||||
|
if (!report) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReportComponent = reports[report.type];
|
||||||
|
|
||||||
|
return <ReportComponent reportId={reportId} />;
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import PageHeader from 'components/layout/PageHeader';
|
|||||||
import Funnel from 'assets/funnel.svg';
|
import Funnel from 'assets/funnel.svg';
|
||||||
import Lightbulb from 'assets/lightbulb.svg';
|
import Lightbulb from 'assets/lightbulb.svg';
|
||||||
import Magnet from 'assets/magnet.svg';
|
import Magnet from 'assets/magnet.svg';
|
||||||
|
import Tag from 'assets/tag.svg';
|
||||||
import styles from './ReportTemplates.module.css';
|
import styles from './ReportTemplates.module.css';
|
||||||
import { useMessages, useTeamUrl } from 'components/hooks';
|
import { useMessages, useTeamUrl } from 'components/hooks';
|
||||||
|
|
||||||
@ -30,6 +31,12 @@ export function ReportTemplates({ showHeader = true }: { showHeader?: boolean })
|
|||||||
url: renderTeamUrl('/reports/retention'),
|
url: renderTeamUrl('/reports/retention'),
|
||||||
icon: <Magnet />,
|
icon: <Magnet />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: formatMessage(labels.utm),
|
||||||
|
description: formatMessage(labels.utmDescription),
|
||||||
|
url: renderTeamUrl('/reports/utm'),
|
||||||
|
icon: <Tag />,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -60,10 +60,9 @@ export function EventDataParameters() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemove = (group: string, index: number) => {
|
const handleRemove = (group: string) => {
|
||||||
const data = [...parameterData[group]];
|
const data = [...parameterData[group]];
|
||||||
data.splice(index, 1);
|
updateReport({ parameters: { [group]: data.filter(({ name }) => name !== group) } });
|
||||||
updateReport({ parameters: { [group]: data } });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddButton = ({ group, onAdd }) => {
|
const AddButton = ({ group, onAdd }) => {
|
||||||
@ -104,12 +103,10 @@ export function EventDataParameters() {
|
|||||||
label={label}
|
label={label}
|
||||||
action={<AddButton group={group} onAdd={handleAdd} />}
|
action={<AddButton group={group} onAdd={handleAdd} />}
|
||||||
>
|
>
|
||||||
<ParameterList
|
<ParameterList>
|
||||||
items={parameterData[group]}
|
{parameterData[group].map(({ name, value }) => {
|
||||||
onRemove={index => handleRemove(group, index)}
|
|
||||||
>
|
|
||||||
{({ name, value }) => {
|
|
||||||
return (
|
return (
|
||||||
|
<ParameterList.Item key={name} onRemove={() => handleRemove(group)}>
|
||||||
<div className={styles.parameter}>
|
<div className={styles.parameter}>
|
||||||
{group === REPORT_PARAMETERS.fields && (
|
{group === REPORT_PARAMETERS.fields && (
|
||||||
<>
|
<>
|
||||||
@ -125,8 +122,9 @@ export function EventDataParameters() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</ParameterList.Item>
|
||||||
);
|
);
|
||||||
}}
|
})}
|
||||||
</ParameterList>
|
</ParameterList>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
);
|
);
|
||||||
|
@ -37,12 +37,12 @@
|
|||||||
.card {
|
.card {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
margin-top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
font-weight: 700;
|
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,19 +51,16 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
background: var(--base900);
|
background: var(--base900);
|
||||||
height: 50px;
|
height: 30px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
color: var(--base700);
|
color: var(--base600);
|
||||||
}
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
.value {
|
|
||||||
color: var(--base50);
|
|
||||||
margin-inline-end: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.track {
|
.track {
|
||||||
@ -72,13 +69,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
padding: 6px 10px;
|
font-size: 20px;
|
||||||
border-radius: 4px;
|
color: var(--base900);
|
||||||
border: 1px solid var(--base300);
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric {
|
||||||
|
color: var(--base700);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
text-transform: lowercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visitors {
|
||||||
|
color: var(--base900);
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 900;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.percent {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
align-self: flex-end;
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,8 @@ import { useContext } from 'react';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useMessages } from 'components/hooks';
|
import { useMessages } from 'components/hooks';
|
||||||
import { ReportContext } from '../[reportId]/Report';
|
import { ReportContext } from '../[reportId]/Report';
|
||||||
import styles from './FunnelChart.module.css';
|
|
||||||
import { formatLongNumber } from 'lib/format';
|
import { formatLongNumber } from 'lib/format';
|
||||||
|
import styles from './FunnelChart.module.css';
|
||||||
|
|
||||||
export interface FunnelChartProps {
|
export interface FunnelChartProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -18,37 +18,35 @@ export function FunnelChart({ className }: FunnelChartProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.chart, className)}>
|
<div className={classNames(styles.chart, className)}>
|
||||||
{data?.map(({ url, visitors, dropped, dropoff, remaining }, index: number) => {
|
{data?.map(({ type, value, visitors, dropped, dropoff, remaining }, index: number) => {
|
||||||
return (
|
return (
|
||||||
<div key={url} className={styles.step}>
|
<div key={index} className={styles.step}>
|
||||||
<div className={styles.num}>{index + 1}</div>
|
<div className={styles.num}>{index + 1}</div>
|
||||||
<div className={styles.card}>
|
<div className={styles.card}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<span className={styles.label}>{formatMessage(labels.viewedPage)}:</span>
|
<span className={styles.label}>
|
||||||
<span className={styles.item}>{url}</span>
|
{formatMessage(type === 'url' ? labels.viewedPage : labels.triggeredEvent)}
|
||||||
|
</span>
|
||||||
|
<span className={styles.item}>{value}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.metric}>
|
||||||
|
<div>
|
||||||
|
<span className={styles.visitors}>{formatLongNumber(visitors)}</span>
|
||||||
|
{formatMessage(labels.visitors)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.percent}>{(remaining * 100).toFixed(2)}%</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.track}>
|
<div className={styles.track}>
|
||||||
<div className={styles.bar} style={{ width: `${remaining * 100}%` }}>
|
<div className={styles.bar} style={{ width: `${remaining * 100}%` }}></div>
|
||||||
<span className={styles.value}>
|
|
||||||
{remaining > 0.1 && `${(remaining * 100).toFixed(2)}%`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.info}>
|
|
||||||
<div>
|
|
||||||
<b>{formatLongNumber(visitors)}</b>
|
|
||||||
<span> {formatMessage(labels.visitors)}</span>
|
|
||||||
<span> ({(remaining * 100).toFixed(2)}%)</span>
|
|
||||||
</div>
|
</div>
|
||||||
{dropoff > 0 && (
|
{dropoff > 0 && (
|
||||||
<div>
|
<div className={styles.info}>
|
||||||
<b>{formatLongNumber(dropped)}</b> {formatMessage(labels.visitorsDroppedOff)} (
|
<b>{formatLongNumber(dropped)}</b> {formatMessage(labels.visitorsDroppedOff)} (
|
||||||
{(dropoff * 100).toFixed(2)}%)
|
{(dropoff * 100).toFixed(2)}%)
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
16
src/app/(main)/reports/funnel/FunnelParameters.module.css
Normal file
16
src/app/(main)/reports/funnel/FunnelParameters.module.css
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type {
|
||||||
|
color: var(--base700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
display: flex;
|
||||||
|
align-self: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
@ -10,50 +10,65 @@ import {
|
|||||||
Popup,
|
Popup,
|
||||||
SubmitButton,
|
SubmitButton,
|
||||||
TextField,
|
TextField,
|
||||||
|
Button,
|
||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
import Icons from 'components/icons';
|
import Icons from 'components/icons';
|
||||||
import UrlAddForm from './UrlAddForm';
|
import FunnelStepAddForm from './FunnelStepAddForm';
|
||||||
import { ReportContext } from '../[reportId]/Report';
|
import { ReportContext } from '../[reportId]/Report';
|
||||||
import BaseParameters from '../[reportId]/BaseParameters';
|
import BaseParameters from '../[reportId]/BaseParameters';
|
||||||
import ParameterList from '../[reportId]/ParameterList';
|
import ParameterList from '../[reportId]/ParameterList';
|
||||||
import PopupForm from '../[reportId]/PopupForm';
|
import PopupForm from '../[reportId]/PopupForm';
|
||||||
|
import styles from './FunnelParameters.module.css';
|
||||||
|
|
||||||
export function FunnelParameters() {
|
export function FunnelParameters() {
|
||||||
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
|
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
const { id, parameters } = report || {};
|
const { id, parameters } = report || {};
|
||||||
const { websiteId, dateRange, urls } = parameters || {};
|
const { websiteId, dateRange, steps } = parameters || {};
|
||||||
const queryDisabled = !websiteId || !dateRange || urls?.length < 2;
|
const queryDisabled = !websiteId || !dateRange || steps?.length < 2;
|
||||||
|
|
||||||
const handleSubmit = (data: any, e: any) => {
|
const handleSubmit = (data: any, e: any) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!queryDisabled) {
|
if (!queryDisabled) {
|
||||||
runReport(data);
|
runReport(data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddUrl = (url: string) => {
|
const handleAddStep = (step: { type: string; value: string }) => {
|
||||||
updateReport({ parameters: { urls: parameters.urls.concat(url) } });
|
updateReport({ parameters: { steps: parameters.steps.concat(step) } });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveUrl = (index: number, e: any) => {
|
const handleUpdateStep = (
|
||||||
e.stopPropagation();
|
close: () => void,
|
||||||
const urls = [...parameters.urls];
|
index: number,
|
||||||
urls.splice(index, 1);
|
step: { type: string; value: string },
|
||||||
updateReport({ parameters: { urls } });
|
) => {
|
||||||
|
const steps = [...parameters.steps];
|
||||||
|
steps[index] = step;
|
||||||
|
updateReport({ parameters: { steps } });
|
||||||
|
close();
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddUrlButton = () => {
|
const handleRemoveStep = (index: number) => {
|
||||||
|
const steps = [...parameters.steps];
|
||||||
|
delete steps[index];
|
||||||
|
updateReport({ parameters: { steps: steps.filter(n => n) } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddStepButton = () => {
|
||||||
return (
|
return (
|
||||||
<PopupTrigger>
|
<PopupTrigger>
|
||||||
|
<Button>
|
||||||
<Icon>
|
<Icon>
|
||||||
<Icons.Plus />
|
<Icons.Plus />
|
||||||
</Icon>
|
</Icon>
|
||||||
<Popup position="right" alignment="start">
|
</Button>
|
||||||
|
<Popup alignment="start">
|
||||||
<PopupForm>
|
<PopupForm>
|
||||||
<UrlAddForm onAdd={handleAddUrl} />
|
<FunnelStepAddForm onChange={handleAddStep} />
|
||||||
</PopupForm>
|
</PopupForm>
|
||||||
</Popup>
|
</Popup>
|
||||||
</PopupTrigger>
|
</PopupTrigger>
|
||||||
@ -71,11 +86,37 @@ export function FunnelParameters() {
|
|||||||
<TextField autoComplete="off" />
|
<TextField autoComplete="off" />
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label={formatMessage(labels.urls)} action={<AddUrlButton />}>
|
<FormRow label={formatMessage(labels.steps)} action={<AddStepButton />}>
|
||||||
<ParameterList
|
<ParameterList>
|
||||||
items={urls}
|
{steps.map((step: { type: string; value: string }, index: number) => {
|
||||||
onRemove={(index: number, e: any) => handleRemoveUrl(index, e)}
|
return (
|
||||||
|
<PopupTrigger key={index}>
|
||||||
|
<ParameterList.Item
|
||||||
|
className={styles.item}
|
||||||
|
onRemove={() => handleRemoveStep(index)}
|
||||||
|
>
|
||||||
|
<div className={styles.value}>
|
||||||
|
<div className={styles.type}>
|
||||||
|
<Icon>{step.type === 'url' ? <Icons.Eye /> : <Icons.Bolt />}</Icon>
|
||||||
|
</div>
|
||||||
|
<div>{step.value}</div>
|
||||||
|
</div>
|
||||||
|
</ParameterList.Item>
|
||||||
|
<Popup alignment="start">
|
||||||
|
{(close: () => void) => (
|
||||||
|
<PopupForm>
|
||||||
|
<FunnelStepAddForm
|
||||||
|
type={step.type}
|
||||||
|
value={step.value}
|
||||||
|
onChange={handleUpdateStep.bind(null, close, index)}
|
||||||
/>
|
/>
|
||||||
|
</PopupForm>
|
||||||
|
)}
|
||||||
|
</Popup>
|
||||||
|
</PopupTrigger>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ParameterList>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormButtons>
|
<FormButtons>
|
||||||
<SubmitButton variant="primary" disabled={queryDisabled} isLoading={isRunning}>
|
<SubmitButton variant="primary" disabled={queryDisabled} isLoading={isRunning}>
|
||||||
|
@ -9,7 +9,7 @@ import { REPORT_TYPES } from 'lib/constants';
|
|||||||
|
|
||||||
const defaultParameters = {
|
const defaultParameters = {
|
||||||
type: REPORT_TYPES.funnel,
|
type: REPORT_TYPES.funnel,
|
||||||
parameters: { window: 60, urls: [] },
|
parameters: { window: 60, steps: [] },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FunnelReport({ reportId }: { reportId?: string }) {
|
export default function FunnelReport({ reportId }: { reportId?: string }) {
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
.dropdown {
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 200px;
|
||||||
|
}
|
80
src/app/(main)/reports/funnel/FunnelStepAddForm.tsx
Normal file
80
src/app/(main)/reports/funnel/FunnelStepAddForm.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useMessages } from 'components/hooks';
|
||||||
|
import { Button, FormRow, TextField, Flexbox, Dropdown, Item } from 'react-basics';
|
||||||
|
import styles from './FunnelStepAddForm.module.css';
|
||||||
|
|
||||||
|
export interface FunnelStepAddFormProps {
|
||||||
|
type?: string;
|
||||||
|
value?: string;
|
||||||
|
onChange?: (step: { type: string; value: string }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FunnelStepAddForm({
|
||||||
|
type: defaultType = 'url',
|
||||||
|
value: defaultValue = '',
|
||||||
|
onChange,
|
||||||
|
}: FunnelStepAddFormProps) {
|
||||||
|
const [type, setType] = useState(defaultType);
|
||||||
|
const [value, setValue] = useState(defaultValue);
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const items = [
|
||||||
|
{ label: formatMessage(labels.url), value: 'url' },
|
||||||
|
{ label: formatMessage(labels.event), value: 'event' },
|
||||||
|
];
|
||||||
|
const isDisabled = !type || !value;
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
onChange({ type, value });
|
||||||
|
setValue('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = e => {
|
||||||
|
setValue(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = e => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleSave();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTypeValue = (value: any) => {
|
||||||
|
return items.find(item => item.value === value)?.label;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flexbox direction="column" gap={10}>
|
||||||
|
<FormRow label={formatMessage(defaultValue ? labels.update : labels.add)}>
|
||||||
|
<Flexbox gap={10}>
|
||||||
|
<Dropdown
|
||||||
|
className={styles.dropdown}
|
||||||
|
items={items}
|
||||||
|
value={type}
|
||||||
|
renderValue={renderTypeValue}
|
||||||
|
onChange={(value: any) => setType(value)}
|
||||||
|
>
|
||||||
|
{({ value, label }) => {
|
||||||
|
return <Item key={value}>{label}</Item>;
|
||||||
|
}}
|
||||||
|
</Dropdown>
|
||||||
|
<TextField
|
||||||
|
className={styles.input}
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
autoFocus={true}
|
||||||
|
autoComplete="off"
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
/>
|
||||||
|
</Flexbox>
|
||||||
|
</FormRow>
|
||||||
|
<FormRow>
|
||||||
|
<Button variant="primary" onClick={handleSave} disabled={isDisabled}>
|
||||||
|
{formatMessage(defaultValue ? labels.update : labels.add)}
|
||||||
|
</Button>
|
||||||
|
</FormRow>
|
||||||
|
</Flexbox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FunnelStepAddForm;
|
@ -1,14 +0,0 @@
|
|||||||
.form {
|
|
||||||
position: absolute;
|
|
||||||
background: var(--base50);
|
|
||||||
width: 300px;
|
|
||||||
padding: 30px;
|
|
||||||
margin-top: 10px;
|
|
||||||
border: 1px solid var(--base400);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { useMessages } from 'components/hooks';
|
|
||||||
import { Button, Form, FormRow, TextField, Flexbox } from 'react-basics';
|
|
||||||
import styles from './UrlAddForm.module.css';
|
|
||||||
|
|
||||||
export interface UrlAddFormProps {
|
|
||||||
defaultValue?: string;
|
|
||||||
onAdd?: (url: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UrlAddForm({ defaultValue = '', onAdd }: UrlAddFormProps) {
|
|
||||||
const [url, setUrl] = useState(defaultValue);
|
|
||||||
const { formatMessage, labels } = useMessages();
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
onAdd(url);
|
|
||||||
setUrl('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = e => {
|
|
||||||
setUrl(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = e => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleSave();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form>
|
|
||||||
<FormRow label={formatMessage(labels.url)}>
|
|
||||||
<Flexbox gap={10}>
|
|
||||||
<TextField
|
|
||||||
className={styles.input}
|
|
||||||
value={url}
|
|
||||||
onChange={handleChange}
|
|
||||||
autoFocus={true}
|
|
||||||
autoComplete="off"
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
/>
|
|
||||||
<Button variant="primary" onClick={handleSave}>
|
|
||||||
{formatMessage(labels.add)}
|
|
||||||
</Button>
|
|
||||||
</Flexbox>
|
|
||||||
</FormRow>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UrlAddForm;
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user