Merge branch 'umami-software:master' into master

This commit is contained in:
Laurent de Lacerda 2025-03-25 10:27:06 +01:00 committed by GitHub
commit 94f47d1888
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
570 changed files with 24989 additions and 10829 deletions

View file

@ -3,6 +3,7 @@
"browser": true,
"es2020": true,
"node": true,
"jquery": true,
"jest": true
},
"parser": "@typescript-eslint/parser",
@ -13,14 +14,8 @@
"ecmaVersion": 11,
"sourceType": "module"
},
"settings": {
"import/resolver": {
"node": {
"moduleDirectory": ["node_modules", "src/"]
}
}
},
"extends": [
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"eslint:recommended",
"plugin:prettier/recommended",
@ -40,12 +35,14 @@
"react/prop-types": "off",
"import/no-anonymous-default-export": "off",
"import/no-named-as-default": "off",
"css-modules/no-unused-class": "off",
"@next/next/no-img-element": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }]
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }],
"@typescript-eslint/no-namespace": ["error", { "allowDeclarations": true }]
},
"globals": {
"React": "writable"

29
.github/workflows/cd-cloud.yml vendored Normal file
View file

@ -0,0 +1,29 @@
name: Create docker images
on:
push:
branches:
- analytics
- cloud
jobs:
build:
name: Build, push, and deploy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Generate random hash
id: random_hash
run: echo "hash=$(openssl rand -hex 4)" >> $GITHUB_OUTPUT
- uses: mr-smithers-excellent/docker-build-push@v6
name: Build & push Docker image to docker.io
with:
image: umamisoftware/umami
tags: cloud-${{ steps.random_hash.outputs.hash }}, cloud-latest
buildArgs: DATABASE_TYPE=postgresql
registry: docker.io
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

View file

@ -20,11 +20,26 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Extract version parts from input
id: extract_version
run: |
echo "version=$(echo ${{ github.event.inputs.version }})" >> $GITHUB_ENV
echo "major=$(echo ${{ github.event.inputs.version }} | cut -d. -f1)" >> $GITHUB_ENV
echo "minor=$(echo ${{ github.event.inputs.version }} | cut -d. -f2)" >> $GITHUB_ENV
- name: Generate tags
id: generate_tags
run: |
echo "tag_major=$(echo ${{ matrix.db-type }}-${{ env.major }})" >> $GITHUB_ENV
echo "tag_minor=$(echo ${{ matrix.db-type }}-${{ env.major }}.${{ env.minor }})" >> $GITHUB_ENV
echo "tag_patch=$(echo ${{ matrix.db-type }}-${{ env.version }})" >> $GITHUB_ENV
echo "tag_latest=$(echo ${{ matrix.db-type }}-latest)" >> $GITHUB_ENV
- uses: mr-smithers-excellent/docker-build-push@v6
name: Build & push Docker image to ghcr.io for ${{ matrix.db-type }}
with:
image: umami
tags: ${{ matrix.db-type }}-${{ inputs.version }}, ${{ matrix.db-type }}-latest
tags: ${{ env.tag_major }}, ${{ env.tag_minor }}, ${{ env.tag_patch }}, ${{ env.tag_latest }}
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
registry: ghcr.io
multiPlatform: true
@ -36,7 +51,7 @@ jobs:
name: Build & push Docker image to docker.io for ${{ matrix.db-type }}
with:
image: umamisoftware/umami
tags: ${{ matrix.db-type }}-${{ inputs.version }}, ${{ matrix.db-type }}-latest
tags: ${{ env.tag_major }}, ${{ env.tag_minor }}, ${{ env.tag_patch }}, ${{ env.tag_latest }}
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
registry: docker.io
username: ${{ secrets.DOCKER_USERNAME }}

View file

@ -17,14 +17,21 @@ jobs:
- name: Set env
run: |
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
echo "NOW=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
- name: Generate tags
id: generate_tags
run: |
echo "tag_patch=$(echo ${{ matrix.db-type }})-${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
echo "tag_minor=$(echo ${{ matrix.db-type }})-$(echo ${GITHUB_REF#refs/tags/} | cut -d. -f1,2)" >> $GITHUB_ENV
echo "tag_major=$(echo ${{ matrix.db-type }})-$(echo ${GITHUB_REF#refs/tags/} | cut -d. -f1)" >> $GITHUB_ENV
echo "tag_latest=$(echo ${{ matrix.db-type }})-latest" >> $GITHUB_ENV
- uses: mr-smithers-excellent/docker-build-push@v6
name: Build & push Docker image to ghcr.io for ${{ matrix.db-type }}
with:
image: umami
tags: ${{ matrix.db-type }}-${{ env.RELEASE_VERSION }}, ${{ matrix.db-type }}-latest
tags: ${{ env.tag_major }}, ${{ env.tag_minor }}, ${{ env.tag_patch }}, ${{ env.tag_latest }}
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
registry: ghcr.io
multiPlatform: true
@ -32,12 +39,11 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: mr-smithers-excellent/docker-build-push@v6
name: Build & push Docker image to docker.io for ${{ matrix.db-type }}
with:
image: umamisoftware/umami
tags: ${{ matrix.db-type }}-${{ env.RELEASE_VERSION }}, ${{ matrix.db-type }}-latest
tags: ${{ env.tag_major }}, ${{ env.tag_minor }}, ${{ env.tag_patch }}, ${{ env.tag_latest }}
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
registry: docker.io
username: ${{ secrets.DOCKER_USERNAME }}

View file

@ -16,9 +16,9 @@ jobs:
strategy:
matrix:
include:
- node-version: 18.17
- node-version: 18.18
db-type: postgresql
- node-version: 18.17
- node-version: 18.18
db-type: mysql
steps:

View file

@ -1,5 +1,5 @@
# Install dependencies only when needed
FROM node:18-alpine AS deps
FROM node:22-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
@ -9,7 +9,7 @@ RUN yarn config set network-timeout 300000
RUN yarn install --frozen-lockfile
# Rebuild the source code only when needed
FROM node:18-alpine AS builder
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
@ -26,18 +26,21 @@ ENV NEXT_TELEMETRY_DISABLED 1
RUN yarn build-docker
# Production image, copy all the files and run next
FROM node:18-alpine AS runner
FROM node:22-alpine AS runner
WORKDIR /app
ARG NODE_OPTIONS
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
ENV NODE_OPTIONS $NODE_OPTIONS
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
RUN set -x \
&& apk add --no-cache curl \
&& yarn add npm-run-all dotenv semver prisma@5.17.0
&& yarn add npm-run-all dotenv semver prisma@6.1.0
# You only need to copy next.config.js if you are NOT using the default configuration
COPY --from=builder /app/next.config.js .

View file

@ -1,5 +1,5 @@
<p align="center">
<img src="https://umami.is/images/umami-logo.png" alt="Umami Logo" width="100">
<img src="https://content.umami.is/website/images/umami-logo.png" alt="Umami Logo" width="100">
</p>
<h1 align="center">Umami</h1>
@ -35,8 +35,8 @@ A detailed getting started guide can be found at [umami.is/docs](https://umami.i
### Requirements
- A server with Node.js version 16.13 or newer
- A database. Umami supports [MySQL](https://www.mysql.com/) (minimum v8.0) and [PostgreSQL](https://www.postgresql.org/) (minimum v12.14) databases.
- A server with Node.js version 18.18 or newer
- A database. Umami supports [MariaDB](https://www.mariadb.org/) (minimum v10.5), [MySQL](https://www.mysql.com/) (minimum v8.0) and [PostgreSQL](https://www.postgresql.org/) (minimum v12.14) databases.
### Install Yarn
@ -121,7 +121,7 @@ To update the Docker image, simply pull the new images and rebuild:
```bash
docker compose pull
docker compose up --force-recreate
docker compose up --force-recreate -d
```
---

29
cypress/e2e/api.cy.ts Normal file
View file

@ -0,0 +1,29 @@
describe('Website tests', () => {
Cypress.session.clearAllSavedSessions();
beforeEach(() => {
cy.login(Cypress.env('umami_user'), Cypress.env('umami_password'));
});
//let userId;
it('creates a user.', () => {
cy.fixture('users').then(data => {
const userPost = data.userPost;
cy.request({
method: 'POST',
url: '/api/users',
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
body: userPost,
}).then(response => {
//userId = response.body.id;
expect(response.status).to.eq(200);
expect(response.body).to.have.property('username', 'cypress1');
expect(response.body).to.have.property('role', 'User');
});
});
});
});

View file

@ -1,22 +1,36 @@
describe('Login tests', () => {
beforeEach(() => {
cy.visit('/login');
});
it(
'logs user in with correct credentials and logs user out',
{
defaultCommandTimeout: 10000,
},
() => {
cy.visit('/login');
cy.getDataTest('input-username').find('input').click();
cy.getDataTest('input-username').find('input').type(Cypress.env('umami_user'), { delay: 50 });
cy.getDataTest('input-password').find('input').click();
cy.getDataTest('input-username').find('input').as('inputUsername').click();
cy.get('@inputUsername').type(Cypress.env('umami_user'), { delay: 0 });
cy.get('@inputUsername').click();
cy.getDataTest('input-password')
.find('input')
.type(Cypress.env('umami_password'), { delay: 50 });
.type(Cypress.env('umami_password'), { delay: 0 });
cy.getDataTest('button-submit').click();
cy.url().should('eq', Cypress.config().baseUrl + '/dashboard');
cy.getDataTest('button-profile').click();
cy.getDataTest('item-logout').click();
cy.url().should('eq', Cypress.config().baseUrl + '/login');
cy.logout();
},
);
it('login with blank inputs or incorrect credentials', () => {
cy.getDataTest('button-submit').click();
cy.contains(/Required/i).should('be.visible');
cy.getDataTest('input-username').find('input').as('inputUsername');
cy.get('@inputUsername').click();
cy.get('@inputUsername').type(Cypress.env('umami_user'), { delay: 0 });
cy.get('@inputUsername').click();
cy.getDataTest('input-password').find('input').type('wrongpassword', { delay: 0 });
cy.getDataTest('button-submit').click();
cy.contains(/Incorrect username and\/or password./i).should('be.visible');
});
});

65
cypress/e2e/user.cy.ts Normal file
View file

@ -0,0 +1,65 @@
describe('Website tests', () => {
Cypress.session.clearAllSavedSessions();
beforeEach(() => {
cy.login(Cypress.env('umami_user'), Cypress.env('umami_password'));
cy.visit('/settings/users');
});
it('Add a User', () => {
// add user
cy.contains(/Create user/i).should('be.visible');
cy.getDataTest('button-create-user').click();
cy.getDataTest('input-username').find('input').as('inputName').click();
cy.get('@inputName').type('Test-user', { delay: 0 });
cy.getDataTest('input-password').find('input').as('inputPassword').click();
cy.get('@inputPassword').type('testPasswordCypress', { delay: 0 });
cy.getDataTest('dropdown-role').click();
cy.getDataTest('dropdown-item-user').click();
cy.getDataTest('button-submit').click();
cy.get('td[label="Username"]').should('contain.text', 'Test-user');
cy.get('td[label="Role"]').should('contain.text', 'User');
});
it('Edit a User role and password', () => {
// edit user
cy.get('table tbody tr')
.contains('td', /Test-user/i)
.parent()
.within(() => {
cy.getDataTest('link-button-edit').click(); // Clicks the button inside the row
});
cy.getDataTest('input-password').find('input').as('inputPassword').click();
cy.get('@inputPassword').type('newPassword', { delay: 0 });
cy.getDataTest('dropdown-role').click();
cy.getDataTest('dropdown-item-viewOnly').click();
cy.getDataTest('button-submit').click();
cy.visit('/settings/users');
cy.get('table tbody tr')
.contains('td', /Test-user/i)
.parent()
.should('contain.text', 'View only');
cy.logout();
cy.url().should('eq', Cypress.config().baseUrl + '/login');
cy.getDataTest('input-username').find('input').as('inputUsername').click();
cy.get('@inputUsername').type('Test-user', { delay: 0 });
cy.get('@inputUsername').click();
cy.getDataTest('input-password').find('input').type('newPassword', { delay: 0 });
cy.getDataTest('button-submit').click();
cy.url().should('eq', Cypress.config().baseUrl + '/dashboard');
});
it('Delete a website', () => {
// delete user
cy.get('table tbody tr')
.contains('td', /Test-user/i)
.parent()
.within(() => {
cy.getDataTest('button-delete').click(); // Clicks the button inside the row
});
cy.contains(/Are you sure you want to delete Test-user?/i).should('be.visible');
cy.getDataTest('button-confirm').click();
});
});

View file

@ -10,10 +10,10 @@ describe('Website tests', () => {
cy.visit('/settings/websites');
cy.getDataTest('button-website-add').click();
cy.contains(/Add website/i).should('be.visible');
cy.getDataTest('input-name').find('input').click();
cy.getDataTest('input-name').find('input').type('Add test', { delay: 50 });
cy.getDataTest('input-name').find('input').as('inputUsername').click();
cy.getDataTest('input-name').find('input').type('Add test', { delay: 0 });
cy.getDataTest('input-domain').find('input').click();
cy.getDataTest('input-domain').find('input').type('addtest.com', { delay: 50 });
cy.getDataTest('input-domain').find('input').type('addtest.com', { delay: 0 });
cy.getDataTest('button-submit').click();
cy.get('td[label="Name"]').should('contain.text', 'Add test');
cy.get('td[label="Domain"]').should('contain.text', 'addtest.com');
@ -41,10 +41,10 @@ describe('Website tests', () => {
cy.contains(/Details/i).should('be.visible');
cy.getDataTest('input-name').find('input').click();
cy.getDataTest('input-name').find('input').clear();
cy.getDataTest('input-name').find('input').type('Updated website', { delay: 50 });
cy.getDataTest('input-name').find('input').type('Updated website', { delay: 0 });
cy.getDataTest('input-domain').find('input').click();
cy.getDataTest('input-domain').find('input').clear();
cy.getDataTest('input-domain').find('input').type('updatedwebsite.com', { delay: 50 });
cy.getDataTest('input-domain').find('input').type('updatedwebsite.com', { delay: 0 });
cy.getDataTest('button-submit').click({ force: true });
cy.getDataTest('input-name').find('input').should('have.value', 'Updated website');
cy.getDataTest('input-domain').find('input').should('have.value', 'updatedwebsite.com');

View file

@ -0,0 +1,17 @@
{
"userGet": {
"name": "cypress",
"email": "password",
"role": "User"
},
"userPost": {
"username": "cypress1",
"password": "password",
"role": "User"
},
"userDelete": {
"name": "Charlie",
"email": "charlie@example.com",
"age": 35
}
}

View file

@ -5,6 +5,12 @@ Cypress.Commands.add('getDataTest', (value: string) => {
return cy.get(`[data-test=${value}]`);
});
Cypress.Commands.add('logout', () => {
cy.getDataTest('button-profile').click();
cy.getDataTest('item-logout').click();
cy.url().should('eq', Cypress.config().baseUrl + '/login');
});
Cypress.Commands.add('login', (username: string, password: string) => {
cy.session([username, password], () => {
cy.request({

View file

@ -1,4 +1,5 @@
/// <reference types="cypress" />
/* global JQuery */
declare namespace Cypress {
interface Chainable {
@ -7,6 +8,11 @@ declare namespace Cypress {
* @example cy.getDataTest('greeting')
*/
getDataTest(value: string): Chainable<JQuery<HTMLElement>>;
/**
* Custom command to logout through UI.
* @example cy.logout()
*/
logout(): Chainable<JQuery<HTMLElement>>;
/**
* Custom command to login user into the app.
* @example cy.login('admin', 'password)

View file

@ -0,0 +1,77 @@
-- add tag column
ALTER TABLE umami.website_event ADD COLUMN "tag" String AFTER "event_name";
ALTER TABLE umami.website_event_stats_hourly ADD COLUMN "tag" SimpleAggregateFunction(groupArrayArray, Array(String)) AFTER "max_time";
-- update materialized view
DROP TABLE umami.website_event_stats_hourly_mv;
CREATE MATERIALIZED VIEW umami.website_event_stats_hourly_mv
TO umami.website_event_stats_hourly
AS
SELECT
website_id,
session_id,
visit_id,
hostname,
browser,
os,
device,
screen,
language,
country,
subdivision1,
city,
entry_url,
exit_url,
url_paths as url_path,
url_query,
referrer_domain,
page_title,
event_type,
event_name,
views,
min_time,
max_time,
tag,
timestamp as created_at
FROM (SELECT
website_id,
session_id,
visit_id,
hostname,
browser,
os,
device,
screen,
language,
country,
subdivision1,
city,
argMinState(url_path, created_at) entry_url,
argMaxState(url_path, created_at) exit_url,
arrayFilter(x -> x != '', groupArray(url_path)) as url_paths,
arrayFilter(x -> x != '', groupArray(url_query)) url_query,
arrayFilter(x -> x != '', groupArray(referrer_domain)) referrer_domain,
arrayFilter(x -> x != '', groupArray(page_title)) page_title,
event_type,
if(event_type = 2, groupArray(event_name), []) event_name,
sumIf(1, event_type = 1) views,
min(created_at) min_time,
max(created_at) max_time,
arrayFilter(x -> x != '', groupArray(tag)) tag,
toStartOfHour(created_at) timestamp
FROM umami.website_event
GROUP BY website_id,
session_id,
visit_id,
hostname,
browser,
os,
device,
screen,
language,
country,
subdivision1,
city,
event_type,
timestamp);

View file

@ -26,6 +26,7 @@ CREATE TABLE umami.website_event
--events
event_type UInt32,
event_name String,
tag String,
created_at DateTime('UTC'),
job_id Nullable(UUID)
)
@ -96,6 +97,7 @@ CREATE TABLE umami.website_event_stats_hourly
views SimpleAggregateFunction(sum, UInt64),
min_time SimpleAggregateFunction(min, DateTime('UTC')),
max_time SimpleAggregateFunction(max, DateTime('UTC')),
tag SimpleAggregateFunction(groupArrayArray, Array(String)),
created_at Datetime('UTC')
)
ENGINE = AggregatingMergeTree
@ -136,6 +138,7 @@ SELECT
views,
min_time,
max_time,
tag,
timestamp as created_at
FROM (SELECT
website_id,
@ -161,6 +164,7 @@ FROM (SELECT
sumIf(1, event_type = 1) views,
min(created_at) min_time,
max(created_at) max_time,
arrayFilter(x -> x != '', groupArray(tag)) tag,
toStartOfHour(created_at) timestamp
FROM umami.website_event
GROUP BY website_id,

View file

@ -11,7 +11,7 @@ CREATE TABLE `user` (
UNIQUE INDEX `user_user_id_key`(`user_id`),
UNIQUE INDEX `user_username_key`(`username`),
PRIMARY KEY (`user_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `session` (
@ -33,7 +33,7 @@ CREATE TABLE `session` (
INDEX `session_created_at_idx`(`created_at`),
INDEX `session_website_id_idx`(`website_id`),
PRIMARY KEY (`session_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `website` (
@ -53,7 +53,7 @@ CREATE TABLE `website` (
INDEX `website_created_at_idx`(`created_at`),
INDEX `website_share_id_idx`(`share_id`),
PRIMARY KEY (`website_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `website_event` (
@ -76,7 +76,7 @@ CREATE TABLE `website_event` (
INDEX `website_event_website_id_created_at_idx`(`website_id`, `created_at`),
INDEX `website_event_website_id_session_id_created_at_idx`(`website_id`, `session_id`, `created_at`),
PRIMARY KEY (`event_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `event_data` (
@ -95,7 +95,7 @@ CREATE TABLE `event_data` (
INDEX `event_data_website_event_id_idx`(`website_event_id`),
INDEX `event_data_website_id_website_event_id_created_at_idx`(`website_id`, `website_event_id`, `created_at`),
PRIMARY KEY (`event_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `team` (
@ -109,7 +109,7 @@ CREATE TABLE `team` (
UNIQUE INDEX `team_access_code_key`(`access_code`),
INDEX `team_access_code_idx`(`access_code`),
PRIMARY KEY (`team_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `team_user` (
@ -124,7 +124,7 @@ CREATE TABLE `team_user` (
INDEX `team_user_team_id_idx`(`team_id`),
INDEX `team_user_user_id_idx`(`user_id`),
PRIMARY KEY (`team_user_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `team_website` (
@ -137,7 +137,7 @@ CREATE TABLE `team_website` (
INDEX `team_website_team_id_idx`(`team_id`),
INDEX `team_website_website_id_idx`(`website_id`),
PRIMARY KEY (`team_website_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddSystemUser
INSERT INTO user (user_id, username, role, password) VALUES ('41e2b680-648e-4b09-bcd7-3e2b10c06264' , 'admin', 'admin', '$2b$10$BUli0c.muyCW1ErNJc3jL.vFRFtFJWrT8/GcR4A.sUdCznaXiqFXa');

View file

@ -21,7 +21,7 @@ CREATE TABLE `session_data` (
INDEX `session_data_website_id_idx`(`website_id`),
INDEX `session_data_session_id_idx`(`session_id`),
PRIMARY KEY (`session_data_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `report` (
@ -41,7 +41,7 @@ CREATE TABLE `report` (
INDEX `report_type_idx`(`type`),
INDEX `report_name_idx`(`name`),
PRIMARY KEY (`report_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- EventData migration
UPDATE event_data

View file

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE `website_event` ADD COLUMN `tag` VARCHAR(50) NULL;
-- CreateIndex
CREATE INDEX `website_event_website_id_created_at_tag_idx` ON `website_event`(`website_id`, `created_at`, `tag`);

View file

@ -1,5 +1,6 @@
generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
binaryTargets = ["native"]
}
datasource db {
@ -19,10 +20,10 @@ model User {
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0)
deletedAt DateTime? @map("deleted_at") @db.Timestamp(0)
websiteUser Website[] @relation("user")
websiteCreateUser Website[] @relation("createUser")
teamUser TeamUser[]
report Report[]
websiteUser Website[] @relation("user")
websiteCreateUser Website[] @relation("createUser")
teamUser TeamUser[]
report Report[]
@@map("user")
}
@ -102,6 +103,7 @@ model WebsiteEvent {
pageTitle String? @map("page_title") @db.VarChar(500)
eventType Int @default(1) @map("event_type") @db.UnsignedInt
eventName String? @map("event_name") @db.VarChar(50)
tag String? @db.VarChar(50)
eventData EventData[]
session Session @relation(fields: [sessionId], references: [id])
@ -116,6 +118,7 @@ model WebsiteEvent {
@@index([websiteId, createdAt, referrerDomain])
@@index([websiteId, createdAt, pageTitle])
@@index([websiteId, createdAt, eventName])
@@index([websiteId, createdAt, tag])
@@index([websiteId, sessionId, createdAt])
@@index([websiteId, visitId, createdAt])
@@map("website_event")
@ -174,8 +177,8 @@ model Team {
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0)
deletedAt DateTime? @map("deleted_at") @db.Timestamp(0)
website Website[]
teamUser TeamUser[]
website Website[]
teamUser TeamUser[]
@@index([accessCode])
@@map("team")

View file

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "website_event" ADD COLUMN "tag" VARCHAR(50);
-- CreateIndex
CREATE INDEX "website_event_website_id_created_at_tag_idx" ON "website_event"("website_id", "created_at", "tag");

View file

@ -1,5 +1,6 @@
generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
binaryTargets = ["native"]
}
datasource db {
@ -19,8 +20,8 @@ model User {
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
websiteUser Website[] @relation("user")
websiteCreateUser Website[] @relation("createUser")
websiteUser Website[] @relation("user")
websiteCreateUser Website[] @relation("createUser")
teamUser TeamUser[]
report Report[]
@ -102,6 +103,7 @@ model WebsiteEvent {
pageTitle String? @map("page_title") @db.VarChar(500)
eventType Int @default(1) @map("event_type") @db.Integer
eventName String? @map("event_name") @db.VarChar(50)
tag String? @db.VarChar(50)
eventData EventData[]
session Session @relation(fields: [sessionId], references: [id])
@ -116,6 +118,7 @@ model WebsiteEvent {
@@index([websiteId, createdAt, referrerDomain])
@@index([websiteId, createdAt, pageTitle])
@@index([websiteId, createdAt, eventName])
@@index([websiteId, createdAt, tag])
@@index([websiteId, sessionId, createdAt])
@@index([websiteId, visitId, createdAt])
@@map("website_event")
@ -147,7 +150,7 @@ model SessionData {
id String @id() @map("session_data_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid
sessionId String @map("session_id") @db.Uuid
dataKey String @map("data_key") @db.VarChar(500)
dataKey String @map("data_key") @db.VarChar(500)
stringValue String? @map("string_value") @db.VarChar(500)
numberValue Decimal? @map("number_value") @db.Decimal(19, 4)
dateValue DateTime? @map("date_value") @db.Timestamptz(6)

View file

@ -1,5 +1,4 @@
---
version: '3'
services:
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
@ -12,6 +11,7 @@ services:
depends_on:
db:
condition: service_healthy
init: true
restart: always
healthcheck:
test: ["CMD-SHELL", "curl http://localhost:3000/api/heartbeat"]

3
next-env.d.ts vendored
View file

@ -1,6 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

View file

@ -1,12 +1,14 @@
/* eslint-disable @typescript-eslint/no-var-requires */
require('dotenv').config();
const path = require('path');
const pkg = require('./package.json');
const TRACKER_SCRIPT = '/script.js';
const basePath = process.env.BASE_PATH;
const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT;
const cloudMode = process.env.CLOUD_MODE;
const cloudUrl = process.env.CLOUD_URL;
const corsMaxAge = process.env.CORS_MAX_AGE;
const defaultLocale = process.env.DEFAULT_LOCALE;
const disableLogin = process.env.DISABLE_LOGIN;
const disableUI = process.env.DISABLE_UI;
@ -14,6 +16,7 @@ const forceSSL = process.env.FORCE_SSL;
const frameAncestors = process.env.ALLOWED_FRAME_URLS;
const privateMode = process.env.PRIVATE_MODE;
const trackerScriptName = process.env.TRACKER_SCRIPT_NAME;
const trackerScriptURL = process.env.TRACKER_SCRIPT_URL;
const contentSecurityPolicy = [
`default-src 'self'`,
@ -24,7 +27,7 @@ const contentSecurityPolicy = [
`frame-ancestors 'self' ${frameAncestors}`,
];
const headers = [
const defaultHeaders = [
{
key: 'X-DNS-Prefetch-Control',
value: 'on',
@ -39,34 +42,78 @@ const headers = [
];
if (forceSSL) {
headers.push({
defaultHeaders.push({
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
});
}
const trackerHeaders = [
{
key: 'Access-Control-Allow-Origin',
value: '*',
},
{
key: 'Cache-Control',
value: 'public, max-age=86400, must-revalidate',
},
];
const apiHeaders = [
{
key: 'Access-Control-Allow-Origin',
value: '*'
},
{
key: 'Access-Control-Allow-Headers',
value: '*'
},
{
key: 'Access-Control-Allow-Methods',
value: 'GET, DELETE, POST, PUT'
},
{
key: 'Access-Control-Max-Age',
value: corsMaxAge || '86400'
},
];
const headers = [
{
source: '/api/:path*',
headers: apiHeaders
},
{
source: '/:path*',
headers: defaultHeaders,
},
{
source: TRACKER_SCRIPT,
headers: trackerHeaders,
},
];
const rewrites = [];
if (trackerScriptURL) {
rewrites.push({
source: TRACKER_SCRIPT,
destination: trackerScriptURL,
});
}
if (collectApiEndpoint) {
headers.push({
source: collectApiEndpoint,
headers: apiHeaders,
});
rewrites.push({
source: collectApiEndpoint,
destination: '/api/send',
});
}
if (trackerScriptName) {
const names = trackerScriptName?.split(',').map(name => name.trim());
if (names) {
names.forEach(name => {
rewrites.push({
source: `/${name.replace(/^\/+/, '')}`,
destination: '/script.js',
});
});
}
}
const redirects = [
{
source: '/settings',
@ -85,6 +132,27 @@ const redirects = [
},
];
// Adding rewrites + headers for all alternative tracker script names.
if (trackerScriptName) {
const names = trackerScriptName?.split(',').map(name => name.trim());
if (names) {
names.forEach(name => {
const normalizedSource = `/${name.replace(/^\/+/, '')}`;
rewrites.push({
source: normalizedSource,
destination: TRACKER_SCRIPT,
});
headers.push({
source: normalizedSource,
headers: trackerHeaders,
});
});
}
}
if (cloudMode && cloudUrl) {
redirects.push({
source: '/settings/:path*',
@ -129,36 +197,26 @@ const config = {
typescript: {
ignoreBuildErrors: true,
},
experimental: {
turbo: {
rules: {
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
},
},
webpack(config) {
const fileLoaderRule = config.module.rules.find(rule => rule.test?.test?.('.svg'));
config.module.rules.push(
{
...fileLoaderRule,
test: /\.svg$/i,
resourceQuery: /url/,
},
{
test: /\.svg$/i,
issuer: fileLoaderRule.issuer,
resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] },
use: ['@svgr/webpack'],
},
);
fileLoaderRule.exclude = /\.svg$/i;
config.resolve.alias['public'] = path.resolve('./public');
config.module.rules.push({
test: /\.svg$/,
issuer: /\.(js|ts)x?$/,
use: ['@svgr/webpack'],
});
return config;
},
async headers() {
return [
{
source: '/:path*',
headers,
},
];
return headers;
},
async rewrites() {
return [

View file

@ -1,6 +1,6 @@
{
"name": "umami",
"version": "2.13.2",
"version": "2.17.0",
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
"author": "Umami Software, Inc. <hello@umami.is>",
"license": "MIT",
@ -10,7 +10,7 @@
"url": "https://github.com/umami-software/umami.git"
},
"scripts": {
"dev": "next dev -p 3000",
"dev": "next dev -p 3000 --turbo",
"build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app",
"start": "next start",
"build-docker": "npm-run-all build-db build-tracker build-geo build-app",
@ -64,17 +64,19 @@
".next/cache"
],
"dependencies": {
"@clickhouse/client": "^1.4.1",
"@clickhouse/client": "^1.10.1",
"@date-fns/utc": "^1.2.0",
"@dicebear/collection": "^9.2.1",
"@dicebear/core": "^9.2.1",
"@fontsource/inter": "^4.5.15",
"@prisma/client": "5.17",
"@prisma/extension-read-replicas": "^0.3.0",
"@hello-pangea/dnd": "^17.0.0",
"@prisma/client": "6.1.0",
"@prisma/extension-read-replicas": "^0.4.0",
"@react-spring/web": "^9.7.3",
"@tanstack/react-query": "^5.28.6",
"@umami/prisma-client": "^0.14.0",
"@umami/redis-client": "^0.21.0",
"@umami/redis-client": "^0.26.0",
"bcryptjs": "^2.4.3",
"chalk": "^4.1.1",
"chart.js": "^4.4.2",
"chartjs-adapter-date-fns": "^3.0.0",
@ -96,19 +98,18 @@
"is-docker": "^3.0.0",
"is-localhost-ip": "^1.4.0",
"isbot": "^5.1.16",
"jsonwebtoken": "^9.0.2",
"kafkajs": "^2.1.0",
"maxmind": "^4.3.6",
"maxmind": "^4.3.24",
"md5": "^2.3.0",
"moment-timezone": "^0.5.35",
"next": "14.2.5",
"next-basics": "^0.39.0",
"next": "15.0.4",
"node-fetch": "^3.2.8",
"npm-run-all": "^4.1.5",
"prisma": "5.17",
"react": "^18.2.0",
"react-basics": "^0.125.0",
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^18.2.0",
"prisma": "6.1.0",
"pure-rand": "^6.1.0",
"react": "^19.0.0",
"react-basics": "^0.126.0",
"react-dom": "^19.0.0",
"react-error-boundary": "^4.0.4",
"react-intl": "^6.5.5",
"react-simple-maps": "^2.3.0",
@ -116,14 +117,15 @@
"react-window": "^1.8.6",
"request-ip": "^3.3.0",
"semver": "^7.5.4",
"serialize-error": "^12.0.0",
"thenby": "^1.3.4",
"uuid": "^9.0.0",
"yup": "^0.32.11",
"zod": "^3.24.1",
"zustand": "^4.5.5"
},
"devDependencies": {
"@formatjs/cli": "^4.2.29",
"@netlify/plugin-nextjs": "^5.1.0",
"@netlify/plugin-nextjs": "^5.8.1",
"@rollup/plugin-alias": "^5.0.0",
"@rollup/plugin-commonjs": "^25.0.4",
"@rollup/plugin-json": "^6.0.0",
@ -132,16 +134,17 @@
"@svgr/rollup": "^8.1.0",
"@svgr/webpack": "^8.1.0",
"@types/cypress": "^1.1.3",
"@types/jest": "^29.5.12",
"@types/node": "^20.9.0",
"@types/react": "^18.2.41",
"@types/react-dom": "^18.2.17",
"@types/jest": "^29.5.14",
"@types/node": "^22.13.4",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.2",
"@types/react-intl": "^3.0.0",
"@types/react-window": "^1.8.8",
"@typescript-eslint/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.3",
"cross-env": "^7.0.3",
"cypress": "^13.6.6",
"esbuild": "^0.17.17",
"esbuild": "^0.25.0",
"eslint": "^8.33.0",
"eslint-config-next": "^14.0.4",
"eslint-config-prettier": "^8.5.0",

File diff suppressed because one or more lines are too long

View file

@ -2,7 +2,7 @@
"label.access-code": [
{
"type": 0,
"value": "Access code"
"value": "Přístupový kód"
}
],
"label.actions": [
@ -14,31 +14,31 @@
"label.activity": [
{
"type": 0,
"value": "Activity log"
"value": "Log aktivity"
}
],
"label.add": [
{
"type": 0,
"value": "Add"
"value": "Přidat"
}
],
"label.add-description": [
{
"type": 0,
"value": "Add description"
"value": "Přidat popis"
}
],
"label.add-member": [
{
"type": 0,
"value": "Add member"
"value": "Přidat člena"
}
],
"label.add-step": [
{
"type": 0,
"value": "Add step"
"value": "Přidat krok"
}
],
"label.add-website": [
@ -56,7 +56,7 @@
"label.after": [
{
"type": 0,
"value": "After"
"value": "Po"
}
],
"label.all": [
@ -68,7 +68,7 @@
"label.all-time": [
{
"type": 0,
"value": "All time"
"value": "Celá doba"
}
],
"label.analytics": [
@ -80,7 +80,7 @@
"label.average": [
{
"type": 0,
"value": "Average"
"value": "Průměr"
}
],
"label.back": [
@ -92,7 +92,7 @@
"label.before": [
{
"type": 0,
"value": "Before"
"value": "Před"
}
],
"label.bounce-rate": [
@ -110,13 +110,13 @@
"label.browser": [
{
"type": 0,
"value": "Browser"
"value": "Prohlížeč"
}
],
"label.browsers": [
{
"type": 0,
"value": "Prohlížeč"
"value": "Prohlížeče"
}
],
"label.cancel": [
@ -134,31 +134,31 @@
"label.cities": [
{
"type": 0,
"value": "Cities"
"value": "Města"
}
],
"label.city": [
{
"type": 0,
"value": "City"
"value": "Město"
}
],
"label.clear-all": [
{
"type": 0,
"value": "Clear all"
"value": "Vyčistit vše"
}
],
"label.compare": [
{
"type": 0,
"value": "Compare"
"value": "Porovnat"
}
],
"label.confirm": [
{
"type": 0,
"value": "Confirm"
"value": "Potvrdit"
}
],
"label.confirm-password": [
@ -170,61 +170,61 @@
"label.contains": [
{
"type": 0,
"value": "Contains"
"value": "Obsahuje"
}
],
"label.continue": [
{
"type": 0,
"value": "Continue"
"value": "Pokračovat"
}
],
"label.count": [
{
"type": 0,
"value": "Count"
"value": "Počet"
}
],
"label.countries": [
{
"type": 0,
"value": "Země"
"value": "Státy"
}
],
"label.country": [
{
"type": 0,
"value": "Country"
"value": "Stát"
}
],
"label.create": [
{
"type": 0,
"value": "Create"
"value": "Vytvořit"
}
],
"label.create-report": [
{
"type": 0,
"value": "Create report"
"value": "Vytvořit hlášení"
}
],
"label.create-team": [
{
"type": 0,
"value": "Create team"
"value": "Vytvořit tým"
}
],
"label.create-user": [
{
"type": 0,
"value": "Create user"
"value": "Vytvořit uživatele"
}
],
"label.created": [
{
"type": 0,
"value": "Created"
"value": "Vytvořeno"
}
],
"label.created-by": [
@ -236,7 +236,7 @@
"label.current": [
{
"type": 0,
"value": "Current"
"value": "Aktuální"
}
],
"label.current-password": [
@ -266,7 +266,7 @@
"label.date": [
{
"type": 0,
"value": "Date"
"value": "Datum"
}
],
"label.date-range": [
@ -278,7 +278,7 @@
"label.day": [
{
"type": 0,
"value": "Day"
"value": "Den"
}
],
"label.default-date-range": [
@ -296,19 +296,19 @@
"label.delete-report": [
{
"type": 0,
"value": "Delete report"
"value": "Smazat hlášení"
}
],
"label.delete-team": [
{
"type": 0,
"value": "Delete team"
"value": "Smazat tým"
}
],
"label.delete-user": [
{
"type": 0,
"value": "Delete user"
"value": "Smazat uživatele"
}
],
"label.delete-website": [
@ -320,7 +320,7 @@
"label.description": [
{
"type": 0,
"value": "Description"
"value": "Popis"
}
],
"label.desktop": [
@ -332,13 +332,13 @@
"label.details": [
{
"type": 0,
"value": "Details"
"value": "Detaily"
}
],
"label.device": [
{
"type": 0,
"value": "Device"
"value": "Zařízení"
}
],
"label.devices": [
@ -356,7 +356,7 @@
"label.does-not-contain": [
{
"type": 0,
"value": "Does not contain"
"value": "Neobsahuje"
}
],
"label.domain": [
@ -380,13 +380,13 @@
"label.edit-dashboard": [
{
"type": 0,
"value": "Edit dashboard"
"value": "Upravit dashboard"
}
],
"label.edit-member": [
{
"type": 0,
"value": "Edit member"
"value": "Upravit člena"
}
],
"label.enable-share-url": [
@ -404,13 +404,13 @@
"label.entry": [
{
"type": 0,
"value": "Entry URL"
"value": "Vstupní URL"
}
],
"label.event": [
{
"type": 0,
"value": "Event"
"value": "Událost"
}
],
"label.event-data": [
@ -440,7 +440,7 @@
"label.field": [
{
"type": 0,
"value": "Field"
"value": "Pole"
}
],
"label.fields": [
@ -452,7 +452,7 @@
"label.filter": [
{
"type": 0,
"value": "Filter"
"value": "Filtr"
}
],
"label.filter-combined": [
@ -470,7 +470,7 @@
"label.filters": [
{
"type": 0,
"value": "Filters"
"value": "Filtry"
}
],
"label.first-seen": [
@ -494,13 +494,13 @@
"label.goal": [
{
"type": 0,
"value": "Goal"
"value": "l"
}
],
"label.goals": [
{
"type": 0,
"value": "Goals"
"value": "Cíle"
}
],
"label.goals-description": [
@ -596,13 +596,13 @@
"label.language": [
{
"type": 0,
"value": "Language"
"value": "Jazyk"
}
],
"label.languages": [
{
"type": 0,
"value": "Languages"
"value": "Jazyky"
}
],
"label.laptop": [
@ -642,7 +642,7 @@
"label.last-months": [
{
"type": 0,
"value": "Last "
"value": "Posledních "
},
{
"type": 1,
@ -650,7 +650,7 @@
},
{
"type": 0,
"value": " months"
"value": " měsíců"
}
],
"label.last-seen": [
@ -662,13 +662,13 @@
"label.leave": [
{
"type": 0,
"value": "Leave"
"value": "Opustit"
}
],
"label.leave-team": [
{
"type": 0,
"value": "Leave team"
"value": "Opustit tým"
}
],
"label.less-than": [
@ -698,13 +698,13 @@
"label.manage": [
{
"type": 0,
"value": "Manage"
"value": "Spravovat"
}
],
"label.manager": [
{
"type": 0,
"value": "Manager"
"value": "Správce"
}
],
"label.max": [
@ -716,13 +716,13 @@
"label.member": [
{
"type": 0,
"value": "Member"
"value": "Člen"
}
],
"label.members": [
{
"type": 0,
"value": "Members"
"value": "Členové"
}
],
"label.min": [
@ -746,13 +746,13 @@
"label.my-account": [
{
"type": 0,
"value": "My account"
"value": "Můj účet"
}
],
"label.my-websites": [
{
"type": 0,
"value": "My websites"
"value": "Mé weby"
}
],
"label.name": [
@ -822,13 +822,13 @@
"label.overview": [
{
"type": 0,
"value": "Overview"
"value": "Přehled"
}
],
"label.owner": [
{
"type": 0,
"value": "Owner"
"value": "Vlastník"
}
],
"label.page-of": [
@ -858,7 +858,7 @@
"label.pageTitle": [
{
"type": 0,
"value": "Page title"
"value": "Název stránky"
}
],
"label.pages": [
@ -876,13 +876,13 @@
"label.path": [
{
"type": 0,
"value": "Path"
"value": "Cesta"
}
],
"label.paths": [
{
"type": 0,
"value": "Paths"
"value": "Cesty"
}
],
"label.powered-by": [
@ -1444,13 +1444,13 @@
"label.visitors": [
{
"type": 0,
"value": "Návštěvy"
"value": "Návštěvníci"
}
],
"label.visits": [
{
"type": 0,
"value": "Visits"
"value": "Návštěvy"
}
],
"label.website": [
@ -1474,13 +1474,13 @@
"label.window": [
{
"type": 0,
"value": "Window"
"value": "Okno"
}
],
"label.yesterday": [
{
"type": 0,
"value": "Yesterday"
"value": "Včera"
}
],
"message.action-confirmation": [

File diff suppressed because it is too large Load diff

View file

@ -410,13 +410,13 @@
"label.event": [
{
"type": 0,
"value": "Ereigniss"
"value": "Ereignis"
}
],
"label.event-data": [
{
"type": 0,
"value": "Ereignissdaten"
"value": "Ereignisdaten"
}
],
"label.events": [
@ -1798,7 +1798,7 @@
"message.triggered-event": [
{
"type": 0,
"value": "Ausgelöstes Ereigniss"
"value": "Ereignis ausgelöst"
}
],
"message.user-deleted": [

File diff suppressed because it is too large Load diff

View file

@ -404,7 +404,7 @@
"label.entry": [
{
"type": 0,
"value": "Entry URL"
"value": "URL d'entrée"
}
],
"label.event": [
@ -476,7 +476,7 @@
"label.first-seen": [
{
"type": 0,
"value": "First seen"
"value": "Vu pour la première fois"
}
],
"label.funnel": [
@ -506,7 +506,7 @@
"label.goals-description": [
{
"type": 0,
"value": "Track your goals for pageviews and events."
"value": "Suivez vos objectifs en matière de pages vues et d'événements."
}
],
"label.greater-than": [
@ -590,7 +590,7 @@
"label.journey-description": [
{
"type": 0,
"value": "Understand how users navigate through your website."
"value": "Comprendre comment les utilisateurs naviguent sur votre site web."
}
],
"label.language": [
@ -886,19 +886,19 @@
"label.previous": [
{
"type": 0,
"value": "Previous"
"value": "Précédent"
}
],
"label.previous-period": [
{
"type": 0,
"value": "Previous period"
"value": "Période précédente"
}
],
"label.previous-year": [
{
"type": 0,
"value": "Previous year"
"value": "Année précédente"
}
],
"label.profile": [
@ -910,13 +910,13 @@
"label.properties": [
{
"type": 0,
"value": "Properties"
"value": "Propriétés"
}
],
"label.property": [
{
"type": 0,
"value": "Property"
"value": "Propriété"
}
],
"label.queries": [
@ -1036,13 +1036,13 @@
"label.revenue-description": [
{
"type": 0,
"value": "Look into your revenue across time."
"value": "Examinez vos revenus au fil du temps."
}
],
"label.revenue-property": [
{
"type": 0,
"value": "Revenue Property"
"value": "Propriétés des revenues"
}
],
"label.role": [
@ -1078,7 +1078,7 @@
"label.select": [
{
"type": 0,
"value": "Select"
"value": "Selectionner"
}
],
"label.select-date": [
@ -1132,7 +1132,7 @@
"label.start-step": [
{
"type": 0,
"value": "Start Step"
"value": "Etape de démarrage"
}
],
"label.steps": [
@ -1168,7 +1168,7 @@
"label.team-manager": [
{
"type": 0,
"value": "Team manager"
"value": "Manager de l'équipe"
}
],
"label.team-member": [
@ -1192,7 +1192,7 @@
"label.team-view-only": [
{
"type": 0,
"value": "Team view only"
"value": "Vue d'équipe uniquement"
}
],
"label.team-websites": [
@ -1318,7 +1318,7 @@
"label.uniqueCustomers": [
{
"type": 0,
"value": "Unique Customers"
"value": "Clients uniques"
}
],
"label.unknown": [
@ -1360,7 +1360,7 @@
"label.user-property": [
{
"type": 0,
"value": "User Property"
"value": "Propriétés d'utilisateurs"
}
],
"label.username": [

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
"label.access-code": [
{
"type": 0,
"value": "Access code"
"value": "पहुंच कोड"
}
],
"label.actions": [
@ -14,19 +14,19 @@
"label.activity": [
{
"type": 0,
"value": "Activity log"
"value": "गतिविधि लॉग"
}
],
"label.add": [
{
"type": 0,
"value": "Add"
"value": "जोडो"
}
],
"label.add-description": [
{
"type": 0,
"value": "Add description"
"value": "विवरण लिखें"
}
],
"label.add-member": [
@ -1006,7 +1006,7 @@
"label.reports": [
{
"type": 0,
"value": "Reports"
"value": "प्रतिवेदन"
}
],
"label.required": [

File diff suppressed because it is too large Load diff

View file

@ -1450,7 +1450,7 @@
"label.visits": [
{
"type": 0,
"value": "訪問数"
"value": "訪問数"
}
],
"label.website": [

View file

@ -1012,7 +1012,7 @@
"label.retention-description": [
{
"type": 0,
"value": "사용자가 얼마나 자주 돌아오는지를 추적하여 웹사이트의 리텐션을 측정하십시오."
"value": "사용자가 얼마나 자주 돌아오는지를 추적하여 웹사이트의 리텐션을 측정하세요."
}
],
"label.revenue": [
@ -1372,7 +1372,7 @@
"label.utm-description": [
{
"type": 0,
"value": "UTM 매개변수를 통해 캠페인을 추적합니다."
"value": "UTM 매개변수를 통해 캠페인을 추적하세요."
}
],
"label.value": [
@ -1414,7 +1414,7 @@
"label.visit-duration": [
{
"type": 0,
"value": "평균 방문 시간"
"value": "방문 시간"
}
],
"label.visitors": [
@ -1548,7 +1548,7 @@
"message.error": [
{
"type": 0,
"value": "오류가 발생했습니다."
"value": "문제가 발생했습니다."
}
],
"message.event-log": [

View file

@ -32,13 +32,13 @@
"label.add-member": [
{
"type": 0,
"value": "Add member"
"value": "Гишүүн нэмэх"
}
],
"label.add-step": [
{
"type": 0,
"value": "Add step"
"value": "Алхам нэмэх"
}
],
"label.add-website": [
@ -74,7 +74,7 @@
"label.analytics": [
{
"type": 0,
"value": "Analytics"
"value": "Аналитик"
}
],
"label.average": [
@ -152,7 +152,7 @@
"label.compare": [
{
"type": 0,
"value": "Compare"
"value": "Харьцуулах"
}
],
"label.confirm": [
@ -182,7 +182,7 @@
"label.count": [
{
"type": 0,
"value": "Count"
"value": "Тоо"
}
],
"label.countries": [
@ -230,13 +230,13 @@
"label.created-by": [
{
"type": 0,
"value": "Created By"
"value": "Үүсгэсэн"
}
],
"label.current": [
{
"type": 0,
"value": "Current"
"value": "Одоогийн"
}
],
"label.current-password": [
@ -296,7 +296,7 @@
"label.delete-report": [
{
"type": 0,
"value": "Delete report"
"value": "Тайлан устгах"
}
],
"label.delete-team": [
@ -386,7 +386,7 @@
"label.edit-member": [
{
"type": 0,
"value": "Edit member"
"value": "Гишүүн засах"
}
],
"label.enable-share-url": [
@ -398,13 +398,13 @@
"label.end-step": [
{
"type": 0,
"value": "End Step"
"value": "Төгсгөлийн алхам"
}
],
"label.entry": [
{
"type": 0,
"value": "Entry URL"
"value": "Орох зам"
}
],
"label.event": [
@ -428,7 +428,7 @@
"label.exit": [
{
"type": 0,
"value": "Exit URL"
"value": "Гарах зам"
}
],
"label.false": [
@ -476,7 +476,7 @@
"label.first-seen": [
{
"type": 0,
"value": "First seen"
"value": "Анх харсан"
}
],
"label.funnel": [
@ -494,19 +494,19 @@
"label.goal": [
{
"type": 0,
"value": "Goal"
"value": "Зорилго"
}
],
"label.goals": [
{
"type": 0,
"value": "Goals"
"value": "Зорилго"
}
],
"label.goals-description": [
{
"type": 0,
"value": "Track your goals for pageviews and events."
"value": "Хуудас үзсэн болон үйлдлийн зорилгыг мөрдөх."
}
],
"label.greater-than": [
@ -524,13 +524,13 @@
"label.host": [
{
"type": 0,
"value": "Host"
"value": "Хост"
}
],
"label.hosts": [
{
"type": 0,
"value": "Hosts"
"value": "Хост"
}
],
"label.insights": [
@ -584,13 +584,13 @@
"label.journey": [
{
"type": 0,
"value": "Journey"
"value": "Аялал"
}
],
"label.journey-description": [
{
"type": 0,
"value": "Understand how users navigate through your website."
"value": "Хэрэглэгчид таны цахим хуудсаар хэрхэн шилжиж явсныг шинжлэх."
}
],
"label.language": [
@ -642,7 +642,7 @@
"label.last-months": [
{
"type": 0,
"value": "Last "
"value": "Сүүлийн "
},
{
"type": 1,
@ -650,13 +650,13 @@
},
{
"type": 0,
"value": " months"
"value": " сар"
}
],
"label.last-seen": [
{
"type": 0,
"value": "Last seen"
"value": "Сүүлд харагдсан"
}
],
"label.leave": [
@ -698,13 +698,13 @@
"label.manage": [
{
"type": 0,
"value": "Manage"
"value": "Удирдах"
}
],
"label.manager": [
{
"type": 0,
"value": "Manager"
"value": "Удирдагч"
}
],
"label.max": [
@ -716,7 +716,7 @@
"label.member": [
{
"type": 0,
"value": "Member"
"value": "Гишүүн"
}
],
"label.members": [
@ -746,7 +746,7 @@
"label.my-account": [
{
"type": 0,
"value": "My account"
"value": "Миний бүртгэл"
}
],
"label.my-websites": [
@ -789,7 +789,7 @@
"value": [
{
"type": 0,
"value": "record"
"value": "бичлэг"
}
]
},
@ -797,7 +797,7 @@
"value": [
{
"type": 0,
"value": "records"
"value": "бичлэг"
}
]
}
@ -810,7 +810,7 @@
"label.ok": [
{
"type": 0,
"value": "OK"
"value": "ЗА"
}
],
"label.os": [
@ -876,13 +876,13 @@
"label.path": [
{
"type": 0,
"value": "Path"
"value": "Зам"
}
],
"label.paths": [
{
"type": 0,
"value": "Paths"
"value": "Зам"
}
],
"label.powered-by": [
@ -898,19 +898,19 @@
"label.previous": [
{
"type": 0,
"value": "Previous"
"value": "Өмнөх"
}
],
"label.previous-period": [
{
"type": 0,
"value": "Previous period"
"value": "Өмнөх үе"
}
],
"label.previous-year": [
{
"type": 0,
"value": "Previous year"
"value": "Өмнөх жил"
}
],
"label.profile": [
@ -922,13 +922,13 @@
"label.properties": [
{
"type": 0,
"value": "Properties"
"value": "Шинж чанар"
}
],
"label.property": [
{
"type": 0,
"value": "Property"
"value": "Шинж чанар"
}
],
"label.queries": [
@ -1000,7 +1000,7 @@
"label.remove-member": [
{
"type": 0,
"value": "Remove member"
"value": "Гишүүн хасах"
}
],
"label.reports": [
@ -1042,19 +1042,19 @@
"label.revenue": [
{
"type": 0,
"value": "Revenue"
"value": "Орлого"
}
],
"label.revenue-description": [
{
"type": 0,
"value": "Look into your revenue across time."
"value": "Цаг хугацааны туршид орлогын өөрчлөлтийг харах."
}
],
"label.revenue-property": [
{
"type": 0,
"value": "Revenue Property"
"value": "Орлогын шинж чанар"
}
],
"label.role": [
@ -1090,7 +1090,7 @@
"label.select": [
{
"type": 0,
"value": "Select"
"value": "Сонгох"
}
],
"label.select-date": [
@ -1144,13 +1144,13 @@
"label.start-step": [
{
"type": 0,
"value": "Start Step"
"value": "Эхлэх алхам"
}
],
"label.steps": [
{
"type": 0,
"value": "Steps"
"value": "Алхам"
}
],
"label.sum": [
@ -1180,7 +1180,7 @@
"label.team-manager": [
{
"type": 0,
"value": "Team manager"
"value": "Багийн удирдагч"
}
],
"label.team-member": [
@ -1294,13 +1294,13 @@
"label.transfer": [
{
"type": 0,
"value": "Transfer"
"value": "Шилжүүлэх"
}
],
"label.transfer-website": [
{
"type": 0,
"value": "Transfer website"
"value": "Вебийг шилжүүлэх"
}
],
"label.true": [
@ -1330,7 +1330,7 @@
"label.uniqueCustomers": [
{
"type": 0,
"value": "Unique Customers"
"value": "Давтагдаагүй зочин"
}
],
"label.unknown": [
@ -1348,7 +1348,7 @@
"label.update": [
{
"type": 0,
"value": "Update"
"value": "Шинэчлэх"
}
],
"label.url": [
@ -1360,7 +1360,7 @@
"label.urls": [
{
"type": 0,
"value": "URLs"
"value": "URL-ууд"
}
],
"label.user": [
@ -1372,7 +1372,7 @@
"label.user-property": [
{
"type": 0,
"value": "User Property"
"value": "Хэрэглэгчийн шинж"
}
],
"label.username": [
@ -1396,7 +1396,7 @@
"label.utm-description": [
{
"type": 0,
"value": "Track your campaigns through UTM parameters."
"value": "UTM параметраар кампанит ажлаа мөрдөх."
}
],
"label.value": [
@ -1432,7 +1432,7 @@
"label.views-per-visit": [
{
"type": 0,
"value": "Views per visit"
"value": "Зочдын хуудас үзсэн тоо"
}
],
"label.visit-duration": [
@ -1450,7 +1450,7 @@
"label.visits": [
{
"type": 0,
"value": "Visits"
"value": "Зочилсон"
}
],
"label.website": [
@ -1486,7 +1486,7 @@
"message.action-confirmation": [
{
"type": 0,
"value": "Type "
"value": "Доорх хэсэгт "
},
{
"type": 1,
@ -1494,7 +1494,7 @@
},
{
"type": 0,
"value": " in the box below to confirm."
"value": " гэж бичин баталгаажуулна уу."
}
],
"message.active-users": [
@ -1542,7 +1542,7 @@
"message.collected-data": [
{
"type": 0,
"value": "Collected data"
"value": "Цуглуулсан өгөгдөл"
}
],
"message.confirm-delete": [
@ -1576,7 +1576,7 @@
"message.confirm-remove": [
{
"type": 0,
"value": "Are you sure you want to remove "
"value": "Та "
},
{
"type": 1,
@ -1584,7 +1584,7 @@
},
{
"type": 0,
"value": "?"
"value": "-г устгахдаа итгэлтэй байна уу?"
}
],
"message.confirm-reset": [
@ -1604,7 +1604,7 @@
"message.delete-team-warning": [
{
"type": 0,
"value": "Deleting a team will also delete all team websites."
"value": "Баг устгах нь мөн түүнд харъяалагдах вебүүдийг устгах болно."
}
],
"message.delete-website-warning": [
@ -1806,25 +1806,25 @@
"message.transfer-team-website-to-user": [
{
"type": 0,
"value": "Transfer this website to your account?"
"value": "Энэ вебийг өөрийн бүртгэл рүү шилжүүлэх үү?"
}
],
"message.transfer-user-website-to-team": [
{
"type": 0,
"value": "Select the team to transfer this website to."
"value": "Энэ вебийг шилжүүлж авах багийг сонгоно уу."
}
],
"message.transfer-website": [
{
"type": 0,
"value": "Transfer website ownership to your account or another team."
"value": "Энэ вебийг өөрийн бүртгэл рүү эсвэл багт шилжүүлж авах."
}
],
"message.triggered-event": [
{
"type": 0,
"value": "Triggered event"
"value": "Өдөөсөн үйлдэл"
}
],
"message.user-deleted": [
@ -1836,7 +1836,7 @@
"message.viewed-page": [
{
"type": 0,
"value": "Viewed page"
"value": "Үзсэн хуудас"
}
],
"message.visitor-log": [
@ -1876,7 +1876,7 @@
"message.visitors-dropped-off": [
{
"type": 0,
"value": "Visitors dropped off"
"value": "Зочдын уналт"
}
]
}

File diff suppressed because it is too large Load diff

View file

@ -38,13 +38,13 @@
"label.add-step": [
{
"type": 0,
"value": "Add step"
"value": "Adaugă pas"
}
],
"label.add-website": [
{
"type": 0,
"value": "Adăugare site web"
"value": "Adaugă site web"
}
],
"label.admin": [
@ -152,7 +152,7 @@
"label.compare": [
{
"type": 0,
"value": "Compare"
"value": "Compară"
}
],
"label.confirm": [
@ -182,7 +182,7 @@
"label.count": [
{
"type": 0,
"value": "Count"
"value": "Număr"
}
],
"label.countries": [
@ -230,13 +230,13 @@
"label.created-by": [
{
"type": 0,
"value": "Created By"
"value": "Creat de"
}
],
"label.current": [
{
"type": 0,
"value": "Current"
"value": "Curent"
}
],
"label.current-password": [
@ -266,13 +266,13 @@
"label.date": [
{
"type": 0,
"value": "Data"
"value": "Dată"
}
],
"label.date-range": [
{
"type": 0,
"value": "Interval de date"
"value": "Interval"
}
],
"label.day": [
@ -284,7 +284,7 @@
"label.default-date-range": [
{
"type": 0,
"value": "Interval de date implicit"
"value": "Interval implicit"
}
],
"label.delete": [
@ -314,7 +314,7 @@
"label.delete-website": [
{
"type": 0,
"value": "Ștergere site web"
"value": "Șterge site web"
}
],
"label.description": [
@ -398,13 +398,13 @@
"label.end-step": [
{
"type": 0,
"value": "End Step"
"value": "Pas final"
}
],
"label.entry": [
{
"type": 0,
"value": "Entry URL"
"value": "URL de intrare"
}
],
"label.event": [
@ -428,7 +428,7 @@
"label.exit": [
{
"type": 0,
"value": "Exit URL"
"value": "URL de ieșire"
}
],
"label.false": [
@ -476,7 +476,7 @@
"label.first-seen": [
{
"type": 0,
"value": "First seen"
"value": "Văzut pentru prima dată"
}
],
"label.funnel": [
@ -494,19 +494,19 @@
"label.goal": [
{
"type": 0,
"value": "Goal"
"value": "Obiectiv"
}
],
"label.goals": [
{
"type": 0,
"value": "Goals"
"value": "Obiective"
}
],
"label.goals-description": [
{
"type": 0,
"value": "Track your goals for pageviews and events."
"value": "Urmărește obiectivele de vizualizări și evenimente."
}
],
"label.greater-than": [
@ -584,13 +584,13 @@
"label.journey": [
{
"type": 0,
"value": "Journey"
"value": "Traseu"
}
],
"label.journey-description": [
{
"type": 0,
"value": "Understand how users navigate through your website."
"value": "Înțelege cum navighează vizitatorii prin website."
}
],
"label.language": [
@ -642,7 +642,7 @@
"label.last-months": [
{
"type": 0,
"value": "Last "
"value": "Ultimele "
},
{
"type": 1,
@ -650,13 +650,13 @@
},
{
"type": 0,
"value": " months"
"value": " luni"
}
],
"label.last-seen": [
{
"type": 0,
"value": "Last seen"
"value": "Văzut ultima dată"
}
],
"label.leave": [
@ -876,13 +876,13 @@
"label.path": [
{
"type": 0,
"value": "Path"
"value": "Rută"
}
],
"label.paths": [
{
"type": 0,
"value": "Paths"
"value": "Rute"
}
],
"label.powered-by": [
@ -898,19 +898,19 @@
"label.previous": [
{
"type": 0,
"value": "Previous"
"value": "Anterior"
}
],
"label.previous-period": [
{
"type": 0,
"value": "Previous period"
"value": "Perioda anterioară"
}
],
"label.previous-year": [
{
"type": 0,
"value": "Previous year"
"value": "Anul anterior"
}
],
"label.profile": [
@ -922,13 +922,13 @@
"label.properties": [
{
"type": 0,
"value": "Properties"
"value": "Proprietăți"
}
],
"label.property": [
{
"type": 0,
"value": "Property"
"value": "Proprietate"
}
],
"label.queries": [
@ -1042,13 +1042,13 @@
"label.revenue": [
{
"type": 0,
"value": "Revenue"
"value": "Venit"
}
],
"label.revenue-description": [
{
"type": 0,
"value": "Look into your revenue across time."
"value": "Urmărește venitul în timp."
}
],
"label.revenue-property": [
@ -1114,7 +1114,7 @@
"label.session": [
{
"type": 0,
"value": "Session"
"value": "Sesiune"
}
],
"label.sessions": [
@ -1144,13 +1144,13 @@
"label.start-step": [
{
"type": 0,
"value": "Start Step"
"value": "Pas de început"
}
],
"label.steps": [
{
"type": 0,
"value": "Steps"
"value": "Pași"
}
],
"label.sum": [
@ -1174,13 +1174,13 @@
"label.team-id": [
{
"type": 0,
"value": "ID Echipa"
"value": "ID Echipă"
}
],
"label.team-manager": [
{
"type": 0,
"value": "Team manager"
"value": "Manager echipă"
}
],
"label.team-member": [
@ -1288,7 +1288,7 @@
"label.transactions": [
{
"type": 0,
"value": "Transactions"
"value": "Tranzacții"
}
],
"label.transfer": [
@ -1330,7 +1330,7 @@
"label.uniqueCustomers": [
{
"type": 0,
"value": "Unique Customers"
"value": "Clienți unici"
}
],
"label.unknown": [
@ -1372,7 +1372,7 @@
"label.user-property": [
{
"type": 0,
"value": "User Property"
"value": "Proprietatea utilizatorului"
}
],
"label.username": [
@ -1396,7 +1396,7 @@
"label.utm-description": [
{
"type": 0,
"value": "Track your campaigns through UTM parameters."
"value": "Urmărește campaniile tale cu parametri UTM."
}
],
"label.value": [
@ -1432,7 +1432,7 @@
"label.views-per-visit": [
{
"type": 0,
"value": "Views per visit"
"value": "Vizualizări per vizită"
}
],
"label.visit-duration": [
@ -1450,7 +1450,7 @@
"label.visits": [
{
"type": 0,
"value": "Visits"
"value": "Vizite"
}
],
"label.website": [
@ -1534,7 +1534,7 @@
"message.collected-data": [
{
"type": 0,
"value": "Collected data"
"value": "Date colectate"
}
],
"message.confirm-delete": [

File diff suppressed because it is too large Load diff

View file

@ -476,7 +476,7 @@
"label.first-seen": [
{
"type": 0,
"value": "First seen"
"value": "首次出现"
}
],
"label.funnel": [
@ -656,7 +656,7 @@
"label.last-seen": [
{
"type": 0,
"value": "Last seen"
"value": "最后出现"
}
],
"label.leave": [
@ -1050,19 +1050,19 @@
"label.revenue": [
{
"type": 0,
"value": "Revenue"
"value": "收入"
}
],
"label.revenue-description": [
{
"type": 0,
"value": "Look into your revenue across time."
"value": "查看您的收入随时间的变化。"
}
],
"label.revenue-property": [
{
"type": 0,
"value": "Revenue Property"
"value": "收入值"
}
],
"label.role": [
@ -1296,7 +1296,7 @@
"label.transactions": [
{
"type": 0,
"value": "Transactions"
"value": "交易"
}
],
"label.transfer": [
@ -1338,7 +1338,7 @@
"label.uniqueCustomers": [
{
"type": 0,
"value": "Unique Customers"
"value": "独特客户"
}
],
"label.unknown": [
@ -1380,7 +1380,7 @@
"label.user-property": [
{
"type": 0,
"value": "User Property"
"value": "用户属性"
}
],
"label.username": [

View file

@ -8,13 +8,13 @@
"label.actions": [
{
"type": 0,
"value": "行"
"value": "行"
}
],
"label.activity": [
{
"type": 0,
"value": "活動日誌"
"value": "活動紀錄"
}
],
"label.add": [
@ -32,13 +32,13 @@
"label.add-member": [
{
"type": 0,
"value": "Add member"
"value": "新增成員"
}
],
"label.add-step": [
{
"type": 0,
"value": "Add step"
"value": "新增步驟"
}
],
"label.add-website": [
@ -104,7 +104,7 @@
"label.breakdown": [
{
"type": 0,
"value": "分解"
"value": "細項分析"
}
],
"label.browser": [
@ -152,7 +152,7 @@
"label.compare": [
{
"type": 0,
"value": "Compare"
"value": "比較"
}
],
"label.confirm": [
@ -182,7 +182,7 @@
"label.count": [
{
"type": 0,
"value": "Count"
"value": "數量"
}
],
"label.countries": [
@ -206,7 +206,7 @@
"label.create-report": [
{
"type": 0,
"value": "建立報"
"value": "建立報"
}
],
"label.create-team": [
@ -230,13 +230,13 @@
"label.created-by": [
{
"type": 0,
"value": "Created By"
"value": "建立者"
}
],
"label.current": [
{
"type": 0,
"value": "Current"
"value": "目前"
}
],
"label.current-password": [
@ -296,7 +296,7 @@
"label.delete-report": [
{
"type": 0,
"value": "Delete report"
"value": "刪除報表"
}
],
"label.delete-team": [
@ -368,7 +368,7 @@
"label.dropoff": [
{
"type": 0,
"value": "退出"
"value": "離開"
}
],
"label.edit": [
@ -386,25 +386,25 @@
"label.edit-member": [
{
"type": 0,
"value": "Edit member"
"value": "編輯成員"
}
],
"label.enable-share-url": [
{
"type": 0,
"value": "啟用分享網址"
"value": "啟用分享連結"
}
],
"label.end-step": [
{
"type": 0,
"value": "End Step"
"value": "結束步驟"
}
],
"label.entry": [
{
"type": 0,
"value": "Entry URL"
"value": "進入網址"
}
],
"label.event": [
@ -428,7 +428,7 @@
"label.exit": [
{
"type": 0,
"value": "Exit URL"
"value": "離開網址"
}
],
"label.false": [
@ -470,43 +470,43 @@
"label.filters": [
{
"type": 0,
"value": "篩選"
"value": "篩選條件"
}
],
"label.first-seen": [
{
"type": 0,
"value": "First seen"
"value": "首次造訪"
}
],
"label.funnel": [
{
"type": 0,
"value": "漏斗"
"value": "漏斗分析"
}
],
"label.funnel-description": [
{
"type": 0,
"value": "瞭解使用者的轉換率和退出率"
"value": "瞭解使用者的轉換率與流失率。"
}
],
"label.goal": [
{
"type": 0,
"value": "Goal"
"value": "目標"
}
],
"label.goals": [
{
"type": 0,
"value": "Goals"
"value": "目標"
}
],
"label.goals-description": [
{
"type": 0,
"value": "Track your goals for pageviews and events."
"value": "追蹤網頁瀏覽和事件的目標。"
}
],
"label.greater-than": [
@ -524,13 +524,13 @@
"label.host": [
{
"type": 0,
"value": "Host"
"value": "主機名稱"
}
],
"label.hosts": [
{
"type": 0,
"value": "Hosts"
"value": "主機名稱"
}
],
"label.insights": [
@ -542,7 +542,7 @@
"label.insights-description": [
{
"type": 0,
"value": "透過使用區段和篩選器來深入探索你的數據"
"value": "使用區段和篩選器來深入分析您的資料。"
}
],
"label.is": [
@ -584,13 +584,13 @@
"label.journey": [
{
"type": 0,
"value": "Journey"
"value": "使用者旅程"
}
],
"label.journey-description": [
{
"type": 0,
"value": "Understand how users navigate through your website."
"value": "瞭解使用者如何瀏覽您的網站。"
}
],
"label.language": [
@ -642,7 +642,7 @@
"label.last-months": [
{
"type": 0,
"value": "Last "
"value": "最近 "
},
{
"type": 1,
@ -650,13 +650,13 @@
},
{
"type": 0,
"value": " months"
"value": " 個月"
}
],
"label.last-seen": [
{
"type": 0,
"value": "Last seen"
"value": "最後造訪"
}
],
"label.leave": [
@ -698,25 +698,25 @@
"label.manage": [
{
"type": 0,
"value": "Manage"
"value": "管理"
}
],
"label.manager": [
{
"type": 0,
"value": "Manager"
"value": "管理者"
}
],
"label.max": [
{
"type": 0,
"value": "最大"
"value": "最大"
}
],
"label.member": [
{
"type": 0,
"value": "Member"
"value": "成員"
}
],
"label.members": [
@ -728,7 +728,7 @@
"label.min": [
{
"type": 0,
"value": "最小"
"value": "最小"
}
],
"label.mobile": [
@ -746,7 +746,7 @@
"label.my-account": [
{
"type": 0,
"value": "My account"
"value": "我的帳號"
}
],
"label.my-websites": [
@ -780,31 +780,7 @@
},
{
"type": 0,
"value": " "
},
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "record"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "records"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "x"
"value": " 筆紀錄"
}
],
"label.ok": [
@ -822,7 +798,7 @@
"label.overview": [
{
"type": 0,
"value": "覽"
"value": "覽"
}
],
"label.owner": [
@ -834,7 +810,7 @@
"label.page-of": [
{
"type": 0,
"value": "頁面 "
"value": " "
},
{
"type": 1,
@ -842,29 +818,33 @@
},
{
"type": 0,
"value": " / "
"value": " 頁,共 "
},
{
"type": 1,
"value": "total"
},
{
"type": 0,
"value": " 頁"
}
],
"label.page-views": [
{
"type": 0,
"value": "頁面瀏覽"
"value": "網頁瀏覽次數"
}
],
"label.pageTitle": [
{
"type": 0,
"value": "標題"
"value": "頁標題"
}
],
"label.pages": [
{
"type": 0,
"value": ""
"value": "頁"
}
],
"label.password": [
@ -876,13 +856,13 @@
"label.path": [
{
"type": 0,
"value": "Path"
"value": "路徑"
}
],
"label.paths": [
{
"type": 0,
"value": "Paths"
"value": "路徑"
}
],
"label.powered-by": [
@ -896,43 +876,43 @@
},
{
"type": 0,
"value": " 提供"
"value": " 提供技術支援"
}
],
"label.previous": [
{
"type": 0,
"value": "Previous"
"value": "上一個"
}
],
"label.previous-period": [
{
"type": 0,
"value": "Previous period"
"value": "上一期間"
}
],
"label.previous-year": [
{
"type": 0,
"value": "Previous year"
"value": "去年"
}
],
"label.profile": [
{
"type": 0,
"value": "個人資料"
"value": "個人檔案"
}
],
"label.properties": [
{
"type": 0,
"value": "Properties"
"value": "屬性"
}
],
"label.property": [
{
"type": 0,
"value": "Property"
"value": "屬性"
}
],
"label.queries": [
@ -986,13 +966,13 @@
"label.region": [
{
"type": 0,
"value": ""
"value": "區"
}
],
"label.regions": [
{
"type": 0,
"value": ""
"value": "區"
}
],
"label.remove": [
@ -1004,13 +984,13 @@
"label.remove-member": [
{
"type": 0,
"value": "Remove member"
"value": "移除成員"
}
],
"label.reports": [
{
"type": 0,
"value": "報"
"value": "報"
}
],
"label.required": [
@ -1028,13 +1008,13 @@
"label.reset-website": [
{
"type": 0,
"value": "重設網站"
"value": "重設網站統計資料"
}
],
"label.retention": [
{
"type": 0,
"value": "留"
"value": "存率"
}
],
"label.retention-description": [
@ -1046,19 +1026,19 @@
"label.revenue": [
{
"type": 0,
"value": "Revenue"
"value": "營收"
}
],
"label.revenue-description": [
{
"type": 0,
"value": "Look into your revenue across time."
"value": "查看您的營收趨勢。"
}
],
"label.revenue-property": [
{
"type": 0,
"value": "Revenue Property"
"value": "營收屬性"
}
],
"label.role": [
@ -1088,37 +1068,37 @@
"label.search": [
{
"type": 0,
"value": "Search"
"value": "搜尋"
}
],
"label.select": [
{
"type": 0,
"value": "Select"
"value": "選取"
}
],
"label.select-date": [
{
"type": 0,
"value": "選日期"
"value": "選日期"
}
],
"label.select-role": [
{
"type": 0,
"value": "Select role"
"value": "選取角色"
}
],
"label.select-website": [
{
"type": 0,
"value": "選網站"
"value": "選網站"
}
],
"label.session": [
{
"type": 0,
"value": "Session"
"value": "工作階段"
}
],
"label.sessions": [
@ -1136,7 +1116,7 @@
"label.share-url": [
{
"type": 0,
"value": "分享網址"
"value": "分享連結"
}
],
"label.single-day": [
@ -1148,13 +1128,13 @@
"label.start-step": [
{
"type": 0,
"value": "Start Step"
"value": "起始步驟"
}
],
"label.steps": [
{
"type": 0,
"value": "Steps"
"value": "步驟"
}
],
"label.sum": [
@ -1184,7 +1164,7 @@
"label.team-manager": [
{
"type": 0,
"value": "Team manager"
"value": "團隊管理者"
}
],
"label.team-member": [
@ -1208,7 +1188,7 @@
"label.team-view-only": [
{
"type": 0,
"value": "Team view only"
"value": "團隊僅供檢視"
}
],
"label.team-websites": [
@ -1280,7 +1260,7 @@
"label.total-records": [
{
"type": 0,
"value": "總記錄"
"value": "紀錄總數"
}
],
"label.tracking-code": [
@ -1292,19 +1272,19 @@
"label.transactions": [
{
"type": 0,
"value": "Transactions"
"value": "交易"
}
],
"label.transfer": [
{
"type": 0,
"value": "Transfer"
"value": "轉移"
}
],
"label.transfer-website": [
{
"type": 0,
"value": "Transfer website"
"value": "轉移網站"
}
],
"label.true": [
@ -1322,19 +1302,19 @@
"label.unique": [
{
"type": 0,
"value": "獨立"
"value": "不重複"
}
],
"label.unique-visitors": [
{
"type": 0,
"value": "獨立訪客"
"value": "不重複訪客"
}
],
"label.uniqueCustomers": [
{
"type": 0,
"value": "Unique Customers"
"value": "不重複客戶"
}
],
"label.unknown": [
@ -1346,13 +1326,13 @@
"label.untitled": [
{
"type": 0,
"value": "無標題"
"value": "未命名"
}
],
"label.update": [
{
"type": 0,
"value": "Update"
"value": "更新"
}
],
"label.url": [
@ -1376,7 +1356,7 @@
"label.user-property": [
{
"type": 0,
"value": "User Property"
"value": "使用者屬性"
}
],
"label.username": [
@ -1400,7 +1380,7 @@
"label.utm-description": [
{
"type": 0,
"value": "Track your campaigns through UTM parameters."
"value": "透過 UTM 參數追蹤您的行銷活動。"
}
],
"label.value": [
@ -1430,19 +1410,19 @@
"label.views": [
{
"type": 0,
"value": "檢視"
"value": "瀏覽次數"
}
],
"label.views-per-visit": [
{
"type": 0,
"value": "Views per visit"
"value": "每次造訪的瀏覽次數"
}
],
"label.visit-duration": [
{
"type": 0,
"value": "平均造訪時間"
"value": "造訪時間"
}
],
"label.visitors": [
@ -1454,7 +1434,7 @@
"label.visits": [
{
"type": 0,
"value": "Visits"
"value": "造訪次數"
}
],
"label.website": [
@ -1490,7 +1470,7 @@
"message.action-confirmation": [
{
"type": 0,
"value": "Type "
"value": "請在下方欄位輸入 "
},
{
"type": 1,
@ -1498,7 +1478,7 @@
},
{
"type": 0,
"value": " in the box below to confirm."
"value": " 以確認。"
}
],
"message.active-users": [
@ -1512,13 +1492,13 @@
},
{
"type": 0,
"value": " 個活躍的訪客"
"value": " 訪客"
}
],
"message.collected-data": [
{
"type": 0,
"value": "Collected data"
"value": "已蒐集的資料"
}
],
"message.confirm-delete": [
@ -1552,7 +1532,7 @@
"message.confirm-remove": [
{
"type": 0,
"value": "Are you sure you want to remove "
"value": "您確定要移除 "
},
{
"type": 1,
@ -1560,7 +1540,7 @@
},
{
"type": 0,
"value": "?"
"value": " 嗎?"
}
],
"message.confirm-reset": [
@ -1574,19 +1554,19 @@
},
{
"type": 0,
"value": " 嗎?"
"value": " 的統計資料嗎?"
}
],
"message.delete-team-warning": [
{
"type": 0,
"value": "Deleting a team will also delete all team websites."
"value": "刪除團隊的同時也會刪除所有團隊的網站。"
}
],
"message.delete-website-warning": [
{
"type": 0,
"value": "所有網站資料將被刪除。"
"value": "所有網站資料將被刪除。"
}
],
"message.error": [
@ -1596,17 +1576,21 @@
}
],
"message.event-log": [
{
"type": 1,
"value": "event"
},
{
"type": 0,
"value": " 在 "
"value": "在 "
},
{
"type": 1,
"value": "url"
},
{
"type": 0,
"value": " 上的 "
},
{
"type": 1,
"value": "event"
}
],
"message.go-to-settings": [
@ -1618,19 +1602,19 @@
"message.incorrect-username-password": [
{
"type": 0,
"value": "使用者名稱和/或密碼不正確。"
"value": "使用者名稱或密碼不正確。"
}
],
"message.invalid-domain": [
{
"type": 0,
"value": "無效的網域。請不要包含 http/https。"
"value": "無效的網域。請包含 http/https。"
}
],
"message.min-password-length": [
{
"type": 0,
"value": "最少需要 "
"value": "密碼長度至少需 "
},
{
"type": 1,
@ -1652,7 +1636,7 @@
},
{
"type": 0,
"value": " 的新版本已經可以使用"
"value": " 的新版本已推出"
}
],
"message.no-data-available": [
@ -1694,7 +1678,7 @@
"message.no-users": [
{
"type": 0,
"value": "沒有使用者。"
"value": "沒有任何使用者。"
}
],
"message.no-websites-configured": [
@ -1706,13 +1690,13 @@
"message.page-not-found": [
{
"type": 0,
"value": "找不到"
"value": "找不到頁"
}
],
"message.reset-website": [
{
"type": 0,
"value": "要重設此網站,請在下方的方框中輸入 "
"value": "要重設此網站的統計資料,請在下方欄位輸入 "
},
{
"type": 1,
@ -1726,7 +1710,7 @@
"message.reset-website-warning": [
{
"type": 0,
"value": "此網站的所有統計將被刪除,但您的設定將保持不變。"
"value": "此網站的所有統計資料都將被刪除,但您的設定將保持不變。"
}
],
"message.saved": [
@ -1738,13 +1722,13 @@
"message.share-url": [
{
"type": 0,
"value": "您的網站統計資料可在以下網址公開檢視:"
"value": "您的網站統計資料可在以下網址公開檢視:"
}
],
"message.team-already-member": [
{
"type": 0,
"value": "您已是團隊的成員。"
"value": "您已團隊的成員。"
}
],
"message.team-not-found": [
@ -1756,13 +1740,13 @@
"message.team-websites-info": [
{
"type": 0,
"value": "團隊的任何成員都可以檢視網站。"
"value": "團隊中的所有成員都可以檢視網站。"
}
],
"message.tracking-code": [
{
"type": 0,
"value": "要追蹤此網站的統計,請將以下代碼放在您的 HTML 的 "
"value": "要追蹤此網站的統計資料,請將以下程式碼放在您 HTML 的 "
},
{
"children": [
@ -1782,25 +1766,25 @@
"message.transfer-team-website-to-user": [
{
"type": 0,
"value": "Transfer this website to your account?"
"value": "要將此網站轉移至您的帳號嗎?"
}
],
"message.transfer-user-website-to-team": [
{
"type": 0,
"value": "Select the team to transfer this website to."
"value": "請選擇要轉移此網站的團隊。"
}
],
"message.transfer-website": [
{
"type": 0,
"value": "Transfer website ownership to your account or another team."
"value": "將網站所有權轉移至您的帳號或其他團隊。"
}
],
"message.triggered-event": [
{
"type": 0,
"value": "Triggered event"
"value": "已觸發的事件"
}
],
"message.user-deleted": [
@ -1812,7 +1796,7 @@
"message.viewed-page": [
{
"type": 0,
"value": "Viewed page"
"value": "已瀏覽的網頁"
}
],
"message.visitor-log": [
@ -1856,7 +1840,7 @@
"message.visitors-dropped-off": [
{
"type": 0,
"value": "Visitors dropped off"
"value": "訪客已離開"
}
]
}

View file

@ -19,13 +19,8 @@ const customResolver = resolve({
const aliasConfig = {
entries: [
{ find: /^app/, replacement: path.resolve('./src/app') },
{ find: /^components/, replacement: path.resolve('./src/components') },
{ find: /^hooks/, replacement: path.resolve('./src/hooks') },
{ find: /^lib/, replacement: path.resolve('./src/lib') },
{ find: /^store/, replacement: path.resolve('./src/store') },
{ find: /^@/, replacement: path.resolve('./src/') },
{ find: /^public/, replacement: path.resolve('./public') },
{ find: /^assets/, replacement: path.resolve('./src/assets') },
],
customResolver,
};

View file

@ -1,94 +0,0 @@
/* eslint-disable no-console */
require('dotenv').config();
const { hashPassword } = require('next-basics');
const chalk = require('chalk');
const prompts = require('prompts');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
const runQuery = async query => {
return query.catch(e => {
throw e;
});
};
const updateUserByUsername = (username, data) => {
return runQuery(
prisma.user.update({
where: {
username,
},
data,
}),
);
};
const changePassword = async (username, newPassword) => {
const password = hashPassword(newPassword);
return updateUserByUsername(username, { password });
};
const getUsernameAndPassword = async () => {
let [username, password] = process.argv.slice(2);
if (username && password) {
return { username, password };
}
const questions = [];
if (!username) {
questions.push({
type: 'text',
name: 'username',
message: 'Enter user to change password',
});
}
if (!password) {
questions.push(
{
type: 'password',
name: 'password',
message: 'Enter new password',
},
{
type: 'password',
name: 'confirmation',
message: 'Confirm new password',
},
);
}
const answers = await prompts(questions);
if (answers.password !== answers.confirmation) {
throw new Error(`Passwords don't match`);
}
return {
username: username || answers.username,
password: answers.password,
};
};
(async () => {
let username, password;
try {
({ username, password } = await getUsernameAndPassword());
} catch (error) {
console.log(chalk.redBright(error.message));
return;
}
try {
await changePassword(username, password);
console.log('Password changed for user', chalk.greenBright(username));
} catch (error) {
if (error.meta.cause.includes('Record to update not found')) {
console.log('User not found:', chalk.redBright(username));
} else {
throw error;
}
}
prisma.$disconnect();
})();

View file

@ -82,9 +82,11 @@ async function checkV1Tables() {
}
async function applyMigration() {
console.log(execSync('prisma migrate deploy').toString());
if (!process.env.SKIP_DB_MIGRATION) {
console.log(execSync('prisma migrate deploy').toString());
success('Database is up to date.');
success('Database is up to date.');
}
}
(async () => {

View file

@ -23,5 +23,5 @@ if (!process.env.SKIP_DB_CHECK && !process.env.DATABASE_TYPE) {
}
if (process.env.CLOUD_MODE) {
checkMissing(['CLOUD_URL', 'KAFKA_BROKER', 'KAFKA_URL', 'REDIS_URL']);
checkMissing(['CLOUD_URL', 'KAFKA_BROKER', 'KAFKA_URL', 'REDIS_URL', 'KAFKA_SASL_MECHANISM']);
}

View file

@ -15,9 +15,9 @@ async function sendTelemetry(type) {
node: process.version,
platform: os.platform(),
arch: os.arch(),
os: `${os.type()} (${os.version()})`,
isDocker: isDocker(),
isCi: isCI,
os: `${os.type()} ${os.version()}`,
is_docker: isDocker(),
is_ci: isCI,
},
};

View file

@ -2,7 +2,7 @@
import { Loading } from 'react-basics';
import Script from 'next/script';
import { usePathname } from 'next/navigation';
import { useLogin, useConfig } from 'components/hooks';
import { useLogin, useConfig } from '@/components/hooks';
import UpdateNotice from './UpdateNotice';
export function App({ children }) {
@ -22,6 +22,10 @@ export function App({ children }) {
return null;
}
if (config.uiDisabled) {
return null;
}
return (
<>
{children}

View file

@ -1,14 +1,16 @@
'use client';
import { useEffect } from 'react';
import { Icon, Text } from 'react-basics';
import Link from 'next/link';
import classNames from 'classnames';
import HamburgerButton from 'components/common/HamburgerButton';
import ThemeButton from 'components/input/ThemeButton';
import LanguageButton from 'components/input/LanguageButton';
import ProfileButton from 'components/input/ProfileButton';
import TeamsButton from 'components/input/TeamsButton';
import Icons from 'components/icons';
import { useMessages, useNavigation, useTeamUrl } from 'components/hooks';
import HamburgerButton from '@/components/common/HamburgerButton';
import ThemeButton from '@/components/input/ThemeButton';
import LanguageButton from '@/components/input/LanguageButton';
import ProfileButton from '@/components/input/ProfileButton';
import TeamsButton from '@/components/input/TeamsButton';
import Icons from '@/components/icons';
import { useMessages, useNavigation, useTeamUrl } from '@/components/hooks';
import { getItem, setItem } from '@/lib/storage';
import styles from './NavBar.module.css';
export function NavBar() {
@ -74,10 +76,24 @@ export function NavBar() {
const handleTeamChange = (teamId: string) => {
const url = teamId ? `/teams/${teamId}` : '/';
if (!cloudMode) {
setItem('umami.team', { id: teamId });
}
router.push(cloudMode ? `${process.env.cloudUrl}${url}` : url);
};
useEffect(() => {
if (!cloudMode) {
const teamIdLocal = getItem('umami.team')?.id;
if (teamIdLocal && teamIdLocal !== teamId) {
router.push(
pathname !== '/' && pathname !== '/dashboard' ? '/' : `/teams/${teamIdLocal}/dashboard`,
);
}
}
}, [cloudMode]);
return (
<div className={styles.navbar}>
<div className={styles.logo}>

View file

@ -1,10 +1,10 @@
import { useEffect, useCallback, useState } from 'react';
import { createPortal } from 'react-dom';
import { Button } from 'react-basics';
import { setItem } from 'next-basics';
import useStore, { checkVersion } from 'store/version';
import { REPO_URL, VERSION_CHECK } from 'lib/constants';
import { useMessages } from 'components/hooks';
import { setItem } from '@/lib/storage';
import useStore, { checkVersion } from '@/store/version';
import { REPO_URL, VERSION_CHECK } from '@/lib/constants';
import { useMessages } from '@/components/hooks';
import { usePathname } from 'next/navigation';
import styles from './UpdateNotice.module.css';
@ -14,6 +14,7 @@ export function UpdateNotice({ user, config }) {
const pathname = usePathname();
const [dismissed, setDismissed] = useState(checked);
const allowUpdate =
process.env.NODE_ENV === 'production' &&
user?.isAdmin &&
!config?.updatesDisabled &&
!pathname.includes('/share/') &&

View file

@ -1,12 +1,12 @@
import { Button } from 'react-basics';
import Link from 'next/link';
import Script from 'next/script';
import WebsiteSelect from 'components/input/WebsiteSelect';
import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
import EventsChart from 'components/metrics/EventsChart';
import WebsiteSelect from '@/components/input/WebsiteSelect';
import Page from '@/components/layout/Page';
import PageHeader from '@/components/layout/PageHeader';
import EventsChart from '@/components/metrics/EventsChart';
import WebsiteChart from '../websites/[websiteId]/WebsiteChart';
import { useApi, useNavigation } from 'components/hooks';
import { useApi, useNavigation } from '@/components/hooks';
import styles from './TestConsole.module.css';
export function TestConsole({ websiteId }: { websiteId: string }) {
@ -48,6 +48,46 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
});
}
function handleRunRevenue() {
window['umami'].track(props => ({
...props,
url: '/checkout-cart',
referrer: 'https://www.google.com',
}));
window['umami'].track('checkout-cart', {
revenue: parseFloat((Math.random() * 1000).toFixed(2)),
currency: 'USD',
});
window['umami'].track('affiliate-link', {
revenue: parseFloat((Math.random() * 1000).toFixed(2)),
currency: 'USD',
});
window['umami'].track('promotion-link', {
revenue: parseFloat((Math.random() * 1000).toFixed(2)),
currency: 'USD',
});
window['umami'].track('checkout-cart', {
revenue: parseFloat((Math.random() * 1000).toFixed(2)),
currency: 'EUR',
});
window['umami'].track('promotion-link', {
revenue: parseFloat((Math.random() * 1000).toFixed(2)),
currency: 'EUR',
});
window['umami'].track('affiliate-link', {
item1: {
productIdentity: 'ABC424',
revenue: parseFloat((Math.random() * 10000).toFixed(2)),
currency: 'JPY',
},
item2: {
productIdentity: 'ZYW684',
revenue: parseFloat((Math.random() * 10000).toFixed(2)),
currency: 'JPY',
},
});
}
function handleRunIdentify() {
window['umami'].identify({
userId: 123,
@ -127,10 +167,19 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
>
Send event with data
</Button>
<Button
id="generate-revenue-button"
data-umami-event="checkout-cart"
data-umami-event-revenue={(Math.random() * 10000).toFixed(2).toString()}
data-umami-event-currency="USD"
variant="primary"
>
Generate revenue data
</Button>
<Button
id="button-with-div-button"
data-umami-event="button-click"
data-umami-event-name="bob"
data-umami-event-name={'bob'}
data-umami-event-id="123"
variant="primary"
>
@ -155,6 +204,9 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
<Button id="manual-button" variant="primary" onClick={handleRunIdentify}>
Run identify
</Button>
<Button id="manual-button" variant="primary" onClick={handleRunRevenue}>
Revenue script
</Button>
</div>
</div>
<WebsiteChart websiteId={website.id} />

View file

@ -5,7 +5,9 @@ async function getEnabled() {
return !!process.env.ENABLE_TEST_CONSOLE;
}
export default async function ({ params: { websiteId } }) {
export default async function ({ params }: { params: { websiteId: string } }) {
const { websiteId } = await params;
const enabled = await getEnabled();
if (!enabled) {

View file

@ -1,34 +1,34 @@
.buttons {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
margin-bottom: 20px;
}
.item {
padding: 5px 0;
}
.item h1 {
font-weight: 600;
font-size: 16px;
}
.item h2 {
font-size: 14px;
color: var(--base700);
}
.text {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 20px;
border-radius: 5px;
border: 1px solid var(--base400);
background: var(--base50);
margin-bottom: 10px;
}
.active .text {
border-color: var(--base600);
box-shadow: 4px 4px 4px var(--base100);
.text {
position: relative;
}
.name {
font-weight: 600;
font-size: 16px;
}
.domain {
font-size: 14px;
color: var(--base700);
}
.dragActive {
@ -38,3 +38,20 @@
.dragActive:active {
cursor: grabbing;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
gap: 20px;
}
.search {
max-width: 360px;
}
.active {
border-color: var(--base600);
box-shadow: 4px 4px 4px var(--base100);
}

View file

@ -1,30 +1,49 @@
import { useState, useMemo } from 'react';
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
import { useState, useMemo, useEffect } from 'react';
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
import classNames from 'classnames';
import { Button, Loading } from 'react-basics';
import { Button, Loading, Toggle, SearchField } from 'react-basics';
import { firstBy } from 'thenby';
import useDashboard, { saveDashboard } from 'store/dashboard';
import { useMessages, useWebsites } from 'components/hooks';
import useDashboard, { saveDashboard } from '@/store/dashboard';
import { useMessages, useWebsites } from '@/components/hooks';
import styles from './DashboardEdit.module.css';
const DRAG_ID = 'dashboard-website-ordering';
export function DashboardEdit({ teamId }: { teamId: string }) {
const settings = useDashboard();
const { websiteOrder } = settings;
const { websiteOrder, websiteActive, isEdited } = settings;
const { formatMessage, labels } = useMessages();
const [order, setOrder] = useState(websiteOrder || []);
const [active, setActive] = useState(websiteActive || []);
const [edited, setEdited] = useState(isEdited);
const [websites, setWebsites] = useState([]);
const [search, setSearch] = useState('');
const {
result,
query: { isLoading },
setParams,
} = useWebsites({ teamId });
const websites = result?.data;
useEffect(() => {
if (result?.data) {
setWebsites(prevWebsites => {
const newWebsites = [...prevWebsites, ...result.data];
if (newWebsites.length < result.count) {
setParams(prevParams => ({ ...prevParams, page: prevParams.page + 1 }));
}
return newWebsites;
});
}
}, [result]);
const ordered = useMemo(() => {
if (websites) {
return websites
.map((website: { id: any }) => ({ ...website, order: order.indexOf(website.id) }))
.map((website: { id: any; name: string; domain: string }) => ({
...website,
order: order.indexOf(website.id),
}))
.sort(firstBy('order'));
}
return [];
@ -38,21 +57,33 @@ export function DashboardEdit({ teamId }: { teamId: string }) {
orderedWebsites.splice(destination.index, 0, removed);
setOrder(orderedWebsites.map(website => website?.id || 0));
setEdited(true);
}
function handleActiveWebsites(id: string) {
setActive(prevActive =>
prevActive.includes(id) ? prevActive.filter(a => a !== id) : [...prevActive, id],
);
setEdited(true);
}
function handleSave() {
saveDashboard({
editing: false,
isEdited: edited,
websiteOrder: order,
websiteActive: active,
});
}
function handleCancel() {
saveDashboard({ editing: false, websiteOrder });
saveDashboard({ editing: false, websiteOrder, websiteActive, isEdited });
}
function handleReset() {
setOrder([]);
setActive([]);
setEdited(false);
}
if (isLoading) {
@ -61,16 +92,19 @@ export function DashboardEdit({ teamId }: { teamId: string }) {
return (
<>
<div className={styles.buttons}>
<Button onClick={handleSave} variant="primary" size="sm">
{formatMessage(labels.save)}
</Button>
<Button onClick={handleCancel} size="sm">
{formatMessage(labels.cancel)}
</Button>
<Button onClick={handleReset} size="sm">
{formatMessage(labels.reset)}
</Button>
<div className={styles.header}>
<SearchField className={styles.search} value={search} onSearch={setSearch} />
<div className={styles.buttons}>
<Button onClick={handleSave} variant="primary" size="sm">
{formatMessage(labels.save)}
</Button>
<Button onClick={handleCancel} size="sm">
{formatMessage(labels.cancel)}
</Button>
<Button onClick={handleReset} size="sm">
{formatMessage(labels.reset)}
</Button>
</div>
</div>
<div className={styles.dragActive}>
<DragDropContext onDragEnd={handleWebsiteDrag}>
@ -81,25 +115,38 @@ export function DashboardEdit({ teamId }: { teamId: string }) {
ref={provided.innerRef}
style={{ marginBottom: snapshot.isDraggingOver ? 260 : null }}
>
{ordered.map(({ id, name, domain }, index) => (
<Draggable key={id} draggableId={`${DRAG_ID}-${id}`} index={index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
className={classNames(styles.item, {
[styles.active]: snapshot.isDragging,
})}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<div className={styles.text}>
<h1>{name}</h1>
<h2>{domain}</h2>
{ordered.map(({ id, name, domain }, index) => {
if (
search &&
!`${name.toLowerCase()}${domain.toLowerCase()}`.includes(search.toLowerCase())
) {
return null;
}
return (
<Draggable key={id} draggableId={`${DRAG_ID}-${id}`} index={index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
className={classNames(styles.item, {
[styles.active]: snapshot.isDragging,
})}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<div className={styles.text}>
<div className={styles.name}>{name}</div>
<div className={styles.domain}>{domain}</div>
</div>
<Toggle
checked={active.includes(id)}
onChange={() => handleActiveWebsites(id)}
/>
</div>
</div>
)}
</Draggable>
))}
)}
</Draggable>
);
})}
{provided.placeholder}
</div>
)}

View file

@ -1,21 +1,21 @@
'use client';
import { Icon, Icons, Loading, Text } from 'react-basics';
import PageHeader from 'components/layout/PageHeader';
import Pager from 'components/common/Pager';
import PageHeader from '@/components/layout/PageHeader';
import Pager from '@/components/common/Pager';
import WebsiteChartList from '../websites/[websiteId]/WebsiteChartList';
import DashboardSettingsButton from 'app/(main)/dashboard/DashboardSettingsButton';
import DashboardEdit from 'app/(main)/dashboard/DashboardEdit';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import { useMessages, useLocale, useTeamUrl, useWebsites } from 'components/hooks';
import useDashboard from 'store/dashboard';
import LinkButton from 'components/common/LinkButton';
import DashboardSettingsButton from '@/app/(main)/dashboard/DashboardSettingsButton';
import DashboardEdit from '@/app/(main)/dashboard/DashboardEdit';
import EmptyPlaceholder from '@/components/common/EmptyPlaceholder';
import { useMessages, useLocale, useTeamUrl, useWebsites } from '@/components/hooks';
import useDashboard from '@/store/dashboard';
import LinkButton from '@/components/common/LinkButton';
export function DashboardPage() {
const { formatMessage, labels, messages } = useMessages();
const { teamId, renderTeamUrl } = useTeamUrl();
const { showCharts, editing } = useDashboard();
const { showCharts, editing, isEdited } = useDashboard();
const { dir } = useLocale();
const pageSize = 10;
const pageSize = isEdited ? 200 : 10;
const { result, query, params, setParams } = useWebsites({ teamId }, { pageSize });
const { page } = params;

View file

@ -1,7 +1,7 @@
import { TooltipPopup, Icon, Text, Flexbox, Button } from 'react-basics';
import Icons from 'components/icons';
import { saveDashboard } from 'store/dashboard';
import { useMessages } from 'components/hooks';
import Icons from '@/components/icons';
import { saveDashboard } from '@/store/dashboard';
import { useMessages } from '@/components/hooks';
export function DashboardSettingsButton() {
const { formatMessage, labels } = useMessages();

View file

@ -1,14 +1,10 @@
import { Metadata } from 'next';
import App from './App';
import NavBar from './NavBar';
import Page from 'components/layout/Page';
import Page from '@/components/layout/Page';
import styles from './layout.module.css';
export default function ({ children }) {
if (process.env.DISABLE_UI) {
return null;
}
export default async function ({ children }) {
return (
<App>
<main className={styles.layout}>

View file

@ -1,8 +1,8 @@
import DateFilter from 'components/input/DateFilter';
import DateFilter from '@/components/input/DateFilter';
import { Button, Flexbox } from 'react-basics';
import { useDateRange, useMessages } from 'components/hooks';
import { DEFAULT_DATE_RANGE } from 'lib/constants';
import { DateRange } from 'lib/types';
import { useDateRange, useMessages } from '@/components/hooks';
import { DEFAULT_DATE_RANGE } from '@/lib/constants';
import { DateRange } from '@/lib/types';
import styles from './DateRangeSetting.module.css';
export function DateRangeSetting() {

View file

@ -1,8 +1,8 @@
import { useState } from 'react';
import { Button, Dropdown, Item, Flexbox } from 'react-basics';
import { useLocale, useMessages } from 'components/hooks';
import { DEFAULT_LOCALE } from 'lib/constants';
import { languages } from 'lib/lang';
import { useLocale, useMessages } from '@/components/hooks';
import { DEFAULT_LOCALE } from '@/lib/constants';
import { languages } from '@/lib/lang';
import styles from './LanguageSetting.module.css';
export function LanguageSetting() {

View file

@ -1,7 +1,7 @@
import { Button, Icon, Text, useToasts, ModalTrigger, Modal } from 'react-basics';
import PasswordEditForm from 'app/(main)/profile/PasswordEditForm';
import Icons from 'components/icons';
import { useMessages } from 'components/hooks';
import PasswordEditForm from '@/app/(main)/profile/PasswordEditForm';
import Icons from '@/components/icons';
import { useMessages } from '@/components/hooks';
export function PasswordChangeButton() {
const { formatMessage, labels, messages } = useMessages();

View file

@ -1,6 +1,6 @@
import { useRef } from 'react';
import { Form, FormRow, FormInput, FormButtons, PasswordField, Button } from 'react-basics';
import { useApi, useMessages } from 'components/hooks';
import { useApi, useMessages } from '@/components/hooks';
export function PasswordEditForm({ onSave, onClose }) {
const { formatMessage, labels, messages } = useMessages();

View file

@ -1,5 +1,5 @@
import PageHeader from 'components/layout/PageHeader';
import { useMessages } from 'components/hooks';
import PageHeader from '@/components/layout/PageHeader';
import { useMessages } from '@/components/hooks';
export function ProfileHeader() {
const { formatMessage, labels } = useMessages();

View file

@ -1,11 +1,11 @@
import { Form, FormRow } from 'react-basics';
import TimezoneSetting from 'app/(main)/profile/TimezoneSetting';
import DateRangeSetting from 'app/(main)/profile/DateRangeSetting';
import LanguageSetting from 'app/(main)/profile/LanguageSetting';
import ThemeSetting from 'app/(main)/profile/ThemeSetting';
import TimezoneSetting from '@/app/(main)/profile/TimezoneSetting';
import DateRangeSetting from '@/app/(main)/profile/DateRangeSetting';
import LanguageSetting from '@/app/(main)/profile/LanguageSetting';
import ThemeSetting from '@/app/(main)/profile/ThemeSetting';
import PasswordChangeButton from './PasswordChangeButton';
import { useLogin, useMessages } from 'components/hooks';
import { ROLES } from 'lib/constants';
import { useLogin, useMessages } from '@/components/hooks';
import { ROLES } from '@/lib/constants';
export function ProfileSettings() {
const { user } = useLogin();

View file

@ -1,8 +1,8 @@
import classNames from 'classnames';
import { Button, Icon } from 'react-basics';
import { useTheme } from 'components/hooks';
import Sun from 'assets/sun.svg';
import Moon from 'assets/moon.svg';
import { useTheme } from '@/components/hooks';
import Sun from '@/assets/sun.svg';
import Moon from '@/assets/moon.svg';
import styles from './ThemeSetting.module.css';
export function ThemeSetting() {

View file

@ -1,11 +1,10 @@
import { useState } from 'react';
import { Dropdown, Item, Button, Flexbox } from 'react-basics';
import moment from 'moment-timezone';
import { useTimezone, useMessages } from 'components/hooks';
import { getTimezone } from 'lib/date';
import { useTimezone, useMessages } from '@/components/hooks';
import { getTimezone } from '@/lib/date';
import styles from './TimezoneSetting.module.css';
const timezones = moment.tz.names();
const timezones = Intl.supportedValuesOf('timeZone');
export function TimezoneSetting() {
const [search, setSearch] = useState('');

View file

@ -1,6 +1,6 @@
import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
import { useApi, useMessages, useModified } from 'components/hooks';
import ConfirmationForm from 'components/common/ConfirmationForm';
import { useApi, useMessages, useModified } from '@/components/hooks';
import ConfirmationForm from '@/components/common/ConfirmationForm';
export function ReportDeleteButton({
reportId,
@ -11,7 +11,7 @@ export function ReportDeleteButton({
reportName: string;
onDelete?: () => void;
}) {
const { formatMessage, labels, messages, FormattedMessage } = useMessages();
const { formatMessage, labels, messages } = useMessages();
const { del, useMutation } = useApi();
const { mutate, isPending, error } = useMutation({
mutationFn: reportId => del(`/reports/${reportId}`),
@ -39,12 +39,9 @@ export function ReportDeleteButton({
<Modal title={formatMessage(labels.deleteReport)}>
{(close: () => void) => (
<ConfirmationForm
message={
<FormattedMessage
{...messages.confirmDelete}
values={{ target: <b>{reportName}</b> }}
/>
}
message={formatMessage(messages.confirmDelete, {
target: <b key={messages.confirmDelete.id}>{reportName}</b>,
})}
isLoading={isPending}
error={error}
onConfirm={handleConfirm.bind(null, close)}

View file

@ -1,6 +1,6 @@
import { useReports } from 'components/hooks';
import { useReports } from '@/components/hooks';
import ReportsTable from './ReportsTable';
import DataTable from 'components/common/DataTable';
import DataTable from '@/components/common/DataTable';
import { ReactNode } from 'react';
export default function ReportsDataTable({

View file

@ -1,8 +1,8 @@
import PageHeader from 'components/layout/PageHeader';
import PageHeader from '@/components/layout/PageHeader';
import { Icon, Icons, Text } from 'react-basics';
import { useLogin, useMessages, useTeamUrl } from 'components/hooks';
import LinkButton from 'components/common/LinkButton';
import { ROLES } from 'lib/constants';
import { useLogin, useMessages, useTeamUrl } from '@/components/hooks';
import LinkButton from '@/components/common/LinkButton';
import { ROLES } from '@/lib/constants';
export function ReportsHeader() {
const { formatMessage, labels } = useMessages();

View file

@ -2,7 +2,7 @@
import { Metadata } from 'next';
import ReportsHeader from './ReportsHeader';
import ReportsDataTable from './ReportsDataTable';
import { useTeamUrl } from 'components/hooks';
import { useTeamUrl } from '@/components/hooks';
export default function ReportsPage() {
const { teamId } = useTeamUrl();

View file

@ -1,7 +1,7 @@
import { GridColumn, GridTable, Icon, Icons, Text } from 'react-basics';
import LinkButton from 'components/common/LinkButton';
import { useMessages, useLogin, useTeamUrl } from 'components/hooks';
import { REPORT_TYPES } from 'lib/constants';
import LinkButton from '@/components/common/LinkButton';
import { useMessages, useLogin, useTeamUrl } from '@/components/hooks';
import { REPORT_TYPES } from '@/lib/constants';
import ReportDeleteButton from './ReportDeleteButton';
export function ReportsTable({ data = [], showDomain }: { data: any[]; showDomain?: boolean }) {

View file

@ -1,9 +1,9 @@
import { useContext } from 'react';
import { FormRow } from 'react-basics';
import { parseDateRange } from 'lib/date';
import DateFilter from 'components/input/DateFilter';
import WebsiteSelect from 'components/input/WebsiteSelect';
import { useMessages, useTeamUrl, useWebsite } from 'components/hooks';
import { parseDateRange } from '@/lib/date';
import DateFilter from '@/components/input/DateFilter';
import WebsiteSelect from '@/components/input/WebsiteSelect';
import { useMessages, useTeamUrl, useWebsite } from '@/components/hooks';
import { ReportContext } from './Report';
import styles from './BaseParameters.module.css';

View file

@ -1,6 +1,6 @@
import { useState } from 'react';
import { createPortal } from 'react-dom';
import { REPORT_PARAMETERS } from 'lib/constants';
import { REPORT_PARAMETERS } from '@/lib/constants';
import PopupForm from './PopupForm';
import FieldSelectForm from './FieldSelectForm';

View file

@ -1,5 +1,5 @@
import { Form, FormRow, Menu, Item } from 'react-basics';
import { useMessages } from 'components/hooks';
import { useMessages } from '@/components/hooks';
export default function FieldAggregateForm({
name,

View file

@ -1,7 +1,7 @@
import { useFilters, useFormat, useLocale, useMessages, useWebsiteValues } from 'components/hooks';
import { OPERATORS } from 'lib/constants';
import { isEqualsOperator } from 'lib/params';
import { useMemo, useState } from 'react';
import { useFilters, useFormat, useMessages, useWebsiteValues } from '@/components/hooks';
import { OPERATORS } from '@/lib/constants';
import { isEqualsOperator } from '@/lib/params';
import {
Button,
Dropdown,
@ -55,7 +55,6 @@ export default function FieldFilterEditForm({
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 = [],
@ -80,29 +79,17 @@ export default function FieldFilterEditForm({
};
const formattedValues = useMemo(() => {
if (!values) {
return {};
}
const formatted = {};
const format = (val: string) => {
formatted[val] = formatValue(val, name);
return formatted[val];
};
return values.reduce((obj: { [x: string]: string }, { value }: { value: string }) => {
obj[value] = formatValue(value, name);
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]);
return obj;
}, {});
}, [formatValue, name, values]);
const filteredValues = useMemo(() => {
return value
? values.filter((n: string | number) =>
formattedValues[n].toLowerCase().includes(value.toLowerCase()),
formattedValues[n]?.toLowerCase()?.includes(value.toLowerCase()),
)
: values;
}, [value, formattedValues]);
@ -226,7 +213,7 @@ const ResultsMenu = ({ values, type, isLoading, onSelect }) => {
return (
<Menu className={styles.menu} variant="popup" onSelect={onSelect}>
{values?.map((value: any) => {
{values?.map(({ value }) => {
return <Item key={value}>{formatValue(value, type)}</Item>;
})}
</Menu>

View file

@ -1,5 +1,5 @@
import { useFields, useMessages } from 'components/hooks';
import Icons from 'components/icons';
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';

View file

@ -1,5 +1,5 @@
import { Menu, Item, Form, FormRow } from 'react-basics';
import { useMessages } from 'components/hooks';
import { useMessages } from '@/components/hooks';
import styles from './FieldSelectForm.module.css';
import { Key } from 'react';

View file

@ -1,13 +1,13 @@
import { useContext } from 'react';
import { useMessages, useFormat, useFilters, useFields } from 'components/hooks';
import Icons from 'components/icons';
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 { isSearchOperator } from '@/lib/params';
import styles from './FilterParameters.module.css';
export function FilterParameters() {

View file

@ -1,8 +1,8 @@
import { ReactNode } from 'react';
import { Icon } from 'react-basics';
import Icons from 'components/icons';
import Empty from 'components/common/Empty';
import { useMessages } from 'components/hooks';
import Icons from '@/components/icons';
import Empty from '@/components/common/Empty';
import { useMessages } from '@/components/hooks';
import styles from './ParameterList.module.css';
import classNames from 'classnames';

View file

@ -1,7 +1,7 @@
import { createContext, ReactNode } from 'react';
import { Loading } from 'react-basics';
import classNames from 'classnames';
import { useReport } from 'components/hooks';
import { useReport } from '@/components/hooks';
import styles from './Report.module.css';
export const ReportContext = createContext(null);

View file

@ -1,10 +1,10 @@
import { useContext } from 'react';
import { Icon, LoadingButton, InlineEditField, useToasts } from 'react-basics';
import { useMessages, useApi, useNavigation, useTeamUrl } from 'components/hooks';
import { useMessages, useApi, useNavigation, useTeamUrl } from '@/components/hooks';
import { ReportContext } from './Report';
import styles from './ReportHeader.module.css';
import { REPORT_TYPES } from 'lib/constants';
import Breadcrumb from 'components/common/Breadcrumb';
import { REPORT_TYPES } from '@/lib/constants';
import Breadcrumb from '@/components/common/Breadcrumb';
export function ReportHeader({ icon }) {
const { report, updateReport } = useContext(ReportContext);

View file

@ -1,5 +1,5 @@
'use client';
import { useReport } from 'components/hooks';
import { useReport } from '@/components/hooks';
import EventDataReport from '../event-data/EventDataReport';
import FunnelReport from '../funnel/FunnelReport';
import GoalReport from '../goals/GoalsReport';

View file

@ -1,7 +1,9 @@
import { Metadata } from 'next';
import ReportPage from './ReportPage';
export default function ({ params: { reportId } }) {
export default async function ({ params }: { params: { reportId: string } }) {
const { reportId } = await params;
return <ReportPage reportId={reportId} />;
}

View file

@ -1,11 +1,12 @@
import Funnel from 'assets/funnel.svg';
import Lightbulb from 'assets/lightbulb.svg';
import Magnet from 'assets/magnet.svg';
import Path from 'assets/path.svg';
import Tag from 'assets/tag.svg';
import Target from 'assets/target.svg';
import { useMessages, useTeamUrl } from 'components/hooks';
import PageHeader from 'components/layout/PageHeader';
import Funnel from '@/assets/funnel.svg';
import Money from '@/assets/money.svg';
import Lightbulb from '@/assets/lightbulb.svg';
import Magnet from '@/assets/magnet.svg';
import Path from '@/assets/path.svg';
import Tag from '@/assets/tag.svg';
import Target from '@/assets/target.svg';
import { useMessages, useTeamUrl } from '@/components/hooks';
import PageHeader from '@/components/layout/PageHeader';
import Link from 'next/link';
import { Button, Icon, Icons, Text } from 'react-basics';
import styles from './ReportTemplates.module.css';
@ -51,12 +52,12 @@ export function ReportTemplates({ showHeader = true }: { showHeader?: boolean })
url: renderTeamUrl('/reports/journey'),
icon: <Path />,
},
// {
// title: formatMessage(labels.revenue),
// description: formatMessage(labels.revenueDescription),
// url: renderTeamUrl('/reports/revenue'),
// icon: <Money />,
// },
{
title: formatMessage(labels.revenue),
description: formatMessage(labels.revenueDescription),
url: renderTeamUrl('/reports/revenue'),
icon: <Money />,
},
];
return (

View file

@ -1,9 +1,9 @@
import { useContext } from 'react';
import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics';
import Empty from 'components/common/Empty';
import Icons from 'components/icons';
import { useApi, useMessages } from 'components/hooks';
import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants';
import Empty from '@/components/common/Empty';
import Icons from '@/components/icons';
import { useApi, useMessages } from '@/components/hooks';
import { DATA_TYPES, REPORT_PARAMETERS } from '@/lib/constants';
import { ReportContext } from '../[reportId]/Report';
import FieldAddForm from '../[reportId]/FieldAddForm';
import ParameterList from '../[reportId]/ParameterList';

View file

@ -4,7 +4,7 @@ import ReportMenu from '../[reportId]/ReportMenu';
import ReportBody from '../[reportId]/ReportBody';
import EventDataParameters from './EventDataParameters';
import EventDataTable from './EventDataTable';
import Nodes from 'assets/nodes.svg';
import Nodes from '@/assets/nodes.svg';
const defaultParameters = {
type: 'event-data',

View file

@ -1,6 +1,6 @@
import { useContext } from 'react';
import { GridTable, GridColumn } from 'react-basics';
import { useMessages } from 'components/hooks';
import { useMessages } from '@/components/hooks';
import { ReportContext } from '../[reportId]/Report';
export function EventDataTable() {

View file

@ -1,8 +1,8 @@
import { useContext } from 'react';
import classNames from 'classnames';
import { useMessages } from 'components/hooks';
import { useMessages } from '@/components/hooks';
import { ReportContext } from '../[reportId]/Report';
import { formatLongNumber } from 'lib/format';
import { formatLongNumber } from '@/lib/format';
import styles from './FunnelChart.module.css';
export interface FunnelChartProps {

View file

@ -1,5 +1,5 @@
import { useContext } from 'react';
import { useMessages } from 'components/hooks';
import { useMessages } from '@/components/hooks';
import {
Icon,
Form,
@ -12,7 +12,7 @@ import {
TextField,
Button,
} from 'react-basics';
import Icons from 'components/icons';
import Icons from '@/components/icons';
import FunnelStepAddForm from './FunnelStepAddForm';
import { ReportContext } from '../[reportId]/Report';
import BaseParameters from '../[reportId]/BaseParameters';

View file

@ -4,8 +4,8 @@ import Report from '../[reportId]/Report';
import ReportHeader from '../[reportId]/ReportHeader';
import ReportMenu from '../[reportId]/ReportMenu';
import ReportBody from '../[reportId]/ReportBody';
import Funnel from 'assets/funnel.svg';
import { REPORT_TYPES } from 'lib/constants';
import Funnel from '@/assets/funnel.svg';
import { REPORT_TYPES } from '@/lib/constants';
const defaultParameters = {
type: REPORT_TYPES.funnel,

View file

@ -1,5 +1,5 @@
import { useState } from 'react';
import { useMessages } from 'components/hooks';
import { useMessages } from '@/components/hooks';
import { Button, FormRow, TextField, Flexbox, Dropdown, Item } from 'react-basics';
import styles from './FunnelStepAddForm.module.css';

View file

@ -1,4 +1,4 @@
import { useMessages } from 'components/hooks';
import { useMessages } from '@/components/hooks';
import { useState } from 'react';
import { Button, Dropdown, Flexbox, FormRow, Item, TextField } from 'react-basics';
import styles from './GoalsAddForm.module.css';

View file

@ -1,8 +1,8 @@
import { useContext } from 'react';
import classNames from 'classnames';
import { useMessages } from 'components/hooks';
import { useMessages } from '@/components/hooks';
import { ReportContext } from '../[reportId]/Report';
import { formatLongNumber } from 'lib/format';
import { formatLongNumber } from '@/lib/format';
import styles from './GoalsChart.module.css';
export function GoalsChart({ className }: { className?: string; isLoading?: boolean }) {

View file

@ -1,6 +1,6 @@
import { useMessages } from 'components/hooks';
import Icons from 'components/icons';
import { formatNumber } from 'lib/format';
import { useMessages } from '@/components/hooks';
import Icons from '@/components/icons';
import { formatNumber } from '@/lib/format';
import { useContext } from 'react';
import {
Button,

View file

@ -4,8 +4,8 @@ import Report from '../[reportId]/Report';
import ReportHeader from '../[reportId]/ReportHeader';
import ReportMenu from '../[reportId]/ReportMenu';
import ReportBody from '../[reportId]/ReportBody';
import Target from 'assets/target.svg';
import { REPORT_TYPES } from 'lib/constants';
import Target from '@/assets/target.svg';
import { REPORT_TYPES } from '@/lib/constants';
const defaultParameters = {
type: REPORT_TYPES.goals,

View file

@ -1,4 +1,4 @@
import { useMessages } from 'components/hooks';
import { useMessages } from '@/components/hooks';
import { useContext } from 'react';
import { Form, FormButtons, SubmitButton } from 'react-basics';
import BaseParameters from '../[reportId]/BaseParameters';

View file

@ -4,8 +4,8 @@ import ReportMenu from '../[reportId]/ReportMenu';
import ReportBody from '../[reportId]/ReportBody';
import InsightsParameters from './InsightsParameters';
import InsightsTable from './InsightsTable';
import Lightbulb from 'assets/lightbulb.svg';
import { REPORT_TYPES } from 'lib/constants';
import Lightbulb from '@/assets/lightbulb.svg';
import { REPORT_TYPES } from '@/lib/constants';
const defaultParameters = {
type: REPORT_TYPES.insights,

View file

@ -1,9 +1,9 @@
import { useContext, useEffect, useState } from 'react';
import { GridTable, GridColumn } from 'react-basics';
import { useFormat, useMessages } from 'components/hooks';
import { useFormat, useMessages } from '@/components/hooks';
import { ReportContext } from '../[reportId]/Report';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import { formatShortTime } from 'lib/format';
import EmptyPlaceholder from '@/components/common/EmptyPlaceholder';
import { formatShortTime } from '@/lib/format';
export function InsightsTable() {
const [fields, setFields] = useState([]);

View file

@ -1,5 +1,5 @@
import { useContext } from 'react';
import { useMessages } from 'components/hooks';
import { useMessages } from '@/components/hooks';
import {
Dropdown,
Form,

View file

@ -5,8 +5,8 @@ import ReportMenu from '../[reportId]/ReportMenu';
import ReportBody from '../[reportId]/ReportBody';
import JourneyParameters from './JourneyParameters';
import JourneyView from './JourneyView';
import Path from 'assets/path.svg';
import { REPORT_TYPES } from 'lib/constants';
import Path from '@/assets/path.svg';
import { REPORT_TYPES } from '@/lib/constants';
const defaultParameters = {
type: REPORT_TYPES.journey,

Some files were not shown because too many files have changed in this diff Show more