Merge pull request #2576 from umami-software/analytics

v2.10.2
This commit is contained in:
Mike Cao 2024-03-06 17:30:52 -08:00 committed by GitHub
commit a2245efa2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
171 changed files with 3285 additions and 631 deletions

View File

@ -2,15 +2,9 @@
"env": { "env": {
"browser": true, "browser": true,
"es2020": true, "es2020": true,
"node": true "node": true,
"jest": true
}, },
"extends": [
"eslint:recommended",
"plugin:prettier/recommended",
"plugin:import/recommended",
"plugin:@typescript-eslint/recommended",
"next"
],
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": {
"ecmaFeatures": { "ecmaFeatures": {
@ -19,7 +13,6 @@
"ecmaVersion": 11, "ecmaVersion": 11,
"sourceType": "module" "sourceType": "module"
}, },
"plugins": ["@typescript-eslint", "prettier"],
"settings": { "settings": {
"import/resolver": { "import/resolver": {
"node": { "node": {
@ -27,6 +20,18 @@
} }
} }
}, },
"extends": [
"plugin:@typescript-eslint/recommended",
"eslint:recommended",
"plugin:prettier/recommended",
"plugin:import/errors",
"plugin:import/typescript",
"plugin:css-modules/recommended",
"plugin:cypress/recommended",
"prettier",
"next"
],
"plugins": ["@typescript-eslint", "prettier", "promise", "css-modules", "cypress"],
"rules": { "rules": {
"no-console": "error", "no-console": "error",
"react/display-name": "off", "react/display-name": "off",

View File

@ -27,9 +27,10 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'npm' cache: 'yarn'
env: env:
DATABASE_TYPE: ${{ matrix.db-type }} DATABASE_TYPE: ${{ matrix.db-type }}
- run: npm install --global yarn - run: npm install --global yarn
- run: yarn install --frozen-lockfile - run: yarn install
- run: yarn test
- run: yarn build - run: yarn build

1
.gitignore vendored
View File

@ -35,6 +35,7 @@ yarn-error.log*
# local env files # local env files
.env .env
.env.* .env.*
*.env.*
*.dev.yml *.dev.yml

View File

@ -5,13 +5,7 @@
"stylelint-config-prettier" "stylelint-config-prettier"
], ],
"rules": { "rules": {
"no-descending-specificity": null, "no-descending-specificity": null
"selector-pseudo-class-no-unknown": [
true,
{
"ignorePseudoClasses": ["global", "horizontal", "vertical"]
}
]
}, },
"ignoreFiles": ["**/*.js", "**/*.md"] "ignoreFiles": ["**/*.js", "**/*.md"]
} }

7
cypress.config.ts Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
},
});

View File

@ -0,0 +1,51 @@
---
version: '3'
services:
umami:
build: ../
#image: ghcr.io/umami-software/umami:postgresql-latest
ports:
- '3000:3000'
environment:
DATABASE_URL: postgresql://umami:umami@db:5432/umami
DATABASE_TYPE: postgresql
APP_SECRET: replace-me-with-a-random-string
depends_on:
db:
condition: service_healthy
restart: always
healthcheck:
test: ['CMD-SHELL', 'curl http://localhost:3000/api/heartbeat']
interval: 5s
timeout: 5s
retries: 5
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: umami
volumes:
- umami-db-data:/var/lib/postgresql/data
restart: always
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}']
interval: 5s
timeout: 5s
retries: 5
cypress:
image: 'cypress/included:13.6.0'
depends_on:
- umami
- db
environment:
- CYPRESS_baseUrl=http://umami:3000
- CYPRESS_umami_user=admin
- CYPRESS_umami_password=umami
volumes:
- ../tsconfig.json:/tsconfig.json
- ../cypress.config.ts:/cypress.config.ts
- ./:/cypress
- ../node_modules/:/node_modules
volumes:
umami-db-data:

18
cypress/e2e/login.cy.ts Normal file
View File

@ -0,0 +1,18 @@
describe('Login tests', () => {
it(
'logs user in with correct credentials and logs user out',
{
defaultCommandTimeout: 10000,
},
() => {
cy.visit('/login');
cy.getDataTest('input-username').find('input').type(Cypress.env('umami_user'));
cy.getDataTest('input-password').find('input').type(Cypress.env('umami_password'));
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');
},
);
});

91
cypress/e2e/website.cy.ts Normal file
View File

@ -0,0 +1,91 @@
describe('Website tests', () => {
Cypress.session.clearAllSavedSessions();
beforeEach(() => {
cy.login(Cypress.env('umami_user'), Cypress.env('umami_password'));
});
it('Add a website', () => {
// add website
cy.visit('/settings/websites');
cy.getDataTest('button-website-add').click();
cy.contains(/Add website/i).should('be.visible');
cy.getDataTest('input-name').find('input').wait(500).type('Add test', { delay: 50 });
cy.getDataTest('input-domain').find('input').wait(500).type('addtest.com', { delay: 50 });
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');
// clean-up data
cy.getDataTest('link-button-edit').first().click();
cy.contains(/Details/i).should('be.visible');
cy.getDataTest('text-field-websiteId')
.find('input')
.then($input => {
const websiteId = $input[0].value;
cy.deleteWebsite(websiteId);
});
cy.visit('/settings/websites');
cy.contains('Add test').should('not.exist');
});
it.only('Edit a website', () => {
// prep data
cy.addWebsite('Update test', 'updatetest.com');
cy.visit('/settings/websites');
// edit website
cy.getDataTest('link-button-edit').first().click();
cy.contains(/Details/i).should('be.visible');
cy.getDataTest('input-name')
.find('input')
.wait(500)
.clear()
.type('Updated website', { delay: 50 });
cy.getDataTest('input-domain')
.find('input')
.wait(500)
.clear()
.type('updatedwebsite.com', { delay: 50 });
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');
// verify tracking script
cy.get('div')
.contains(/Tracking code/i)
.click();
cy.get('textarea').should('contain.text', Cypress.config().baseUrl + '/script.js');
// clean-up data
cy.get('div')
.contains(/Details/i)
.click();
cy.contains(/Details/i).should('be.visible');
cy.getDataTest('text-field-websiteId')
.find('input')
.then($input => {
const websiteId = $input[0].value;
cy.deleteWebsite(websiteId);
});
cy.visit('/settings/websites');
cy.contains('Add test').should('not.exist');
});
it('Delete a website', () => {
// prep data
cy.addWebsite('Delete test', 'deletetest.com');
cy.visit('/settings/websites');
// delete website
cy.getDataTest('link-button-edit').first().click();
cy.contains(/Data/i).should('be.visible');
cy.get('div').contains(/Data/i).click();
cy.contains(/All website data will be deleted./i).should('be.visible');
cy.getDataTest('button-delete').click();
cy.contains(/Type DELETE in the box below to confirm./i).should('be.visible');
cy.get('input[name="confirm"').type('DELETE');
cy.get('button[type="submit"]').click();
cy.contains('Delete test').should('not.exist');
});
});

57
cypress/support/e2e.ts Normal file
View File

@ -0,0 +1,57 @@
/// <reference types="cypress" />
import { uuid } from '../../src/lib/crypto';
Cypress.Commands.add('getDataTest', (value: string) => {
return cy.get(`[data-test=${value}]`);
});
Cypress.Commands.add('login', (username: string, password: string) => {
cy.session([username, password], () => {
cy.request({
method: 'POST',
url: '/api/auth/login',
body: {
username,
password,
},
})
.then(response => {
Cypress.env('authorization', `bearer ${response.body.token}`);
window.localStorage.setItem('umami.auth', JSON.stringify(response.body.token));
})
.its('status')
.should('eq', 200);
});
});
Cypress.Commands.add('addWebsite', (name: string, domain: string) => {
cy.request({
method: 'POST',
url: '/api/websites',
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
body: {
id: uuid(),
createdBy: '41e2b680-648e-4b09-bcd7-3e2b10c06264',
name: name,
domain: domain,
},
}).then(response => {
expect(response.status).to.eq(200);
});
});
Cypress.Commands.add('deleteWebsite', (websiteId: string) => {
cy.request({
method: 'DELETE',
url: `/api/websites/${websiteId}`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
}).then(response => {
expect(response.status).to.eq(200);
});
});

26
cypress/support/index.d.ts vendored Normal file
View File

@ -0,0 +1,26 @@
/// <reference types="cypress" />
declare namespace Cypress {
interface Chainable {
/**
* Custom command to select DOM element by data-test attribute.
* @example cy.getDataTest('greeting')
*/
getDataTest(value: string): Chainable<JQuery<HTMLElement>>;
/**
* Custom command to login user into the app.
* @example cy.login('admin', 'password)
*/
login(username: string, password: string): Chainable<JQuery<HTMLElement>>;
/**
* Custom command to create a website
* @example cy.addWebsite('test', 'test.com')
*/
addWebsite(name: string, domain: string): Chainable<JQuery<HTMLElement>>;
/**
* Custom command to create a website
* @example cy.deleteWebsite('02d89813-7a72-41e1-87f0-8d668f85008b')
*/
deleteWebsite(websiteId: string): Chainable<JQuery<HTMLElement>>;
}
}

8
cypress/tsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress", "node"]
},
"include": ["**/*.ts", "../cypress.config.ts"]
}

7
jest.config.ts Normal file
View File

@ -0,0 +1,7 @@
export default {
roots: ['./src'],
testMatch: ['**/__tests__/**/*.+(ts|tsx|js)', '**/?(*.)+(spec|test).+(ts|tsx|js)'],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
};

View File

@ -1,6 +1,6 @@
{ {
"name": "umami", "name": "umami",
"version": "2.10.1", "version": "2.10.2",
"description": "A simple, fast, privacy-focused alternative to Google Analytics.", "description": "A simple, fast, privacy-focused alternative to Google Analytics.",
"author": "Umami Software, Inc. <hello@umami.is>", "author": "Umami Software, Inc. <hello@umami.is>",
"license": "MIT", "license": "MIT",
@ -42,7 +42,10 @@
"change-password": "node scripts/change-password.js", "change-password": "node scripts/change-password.js",
"lint": "next lint --quiet", "lint": "next lint --quiet",
"prepare": "node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || husky install", "prepare": "node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || husky install",
"postbuild": "node scripts/postbuild.js" "postbuild": "node scripts/postbuild.js",
"test": "jest",
"cypress-open": "cypress open cypress run",
"cypress-run": "cypress run cypress run"
}, },
"lint-staged": { "lint-staged": {
"**/*.{js,jsx,ts,tsx}": [ "**/*.{js,jsx,ts,tsx}": [
@ -83,13 +86,14 @@
"del": "^6.0.0", "del": "^6.0.0",
"detect-browser": "^5.2.0", "detect-browser": "^5.2.0",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"eslint-plugin-promise": "^6.1.1",
"fs-extra": "^10.0.1", "fs-extra": "^10.0.1",
"immer": "^9.0.12", "immer": "^9.0.12",
"ipaddr.js": "^2.0.1", "ipaddr.js": "^2.0.1",
"is-ci": "^3.0.1", "is-ci": "^3.0.1",
"is-docker": "^3.0.0", "is-docker": "^3.0.0",
"is-localhost-ip": "^1.4.0", "is-localhost-ip": "^1.4.0",
"isbot": "^3.4.5", "isbot": "^5.1.1",
"kafkajs": "^2.1.0", "kafkajs": "^2.1.0",
"maxmind": "^4.3.6", "maxmind": "^4.3.6",
"md5": "^2.3.0", "md5": "^2.3.0",
@ -100,7 +104,7 @@
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prisma": "5.9.1", "prisma": "5.9.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-basics": "^0.122.0", "react-basics": "^0.123.0",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-error-boundary": "^4.0.4", "react-error-boundary": "^4.0.4",
@ -126,6 +130,8 @@
"@rollup/plugin-replace": "^5.0.2", "@rollup/plugin-replace": "^5.0.2",
"@svgr/rollup": "^8.1.0", "@svgr/rollup": "^8.1.0",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",
"@types/cypress": "^1.1.3",
"@types/jest": "^29.5.12",
"@types/node": "^20.9.0", "@types/node": "^20.9.0",
"@types/react": "^18.2.41", "@types/react": "^18.2.41",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.2.17",
@ -133,15 +139,20 @@
"@typescript-eslint/eslint-plugin": "^6.7.3", "@typescript-eslint/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.3", "@typescript-eslint/parser": "^6.7.3",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"cypress": "^13.6.6",
"esbuild": "^0.17.17", "esbuild": "^0.17.17",
"eslint": "^8.33.0", "eslint": "^8.33.0",
"eslint-config-next": "^14.0.4", "eslint-config-next": "^14.0.4",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-cypress": "^2.15.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jest": "^27.9.0",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"extract-react-intl-messages": "^4.1.1", "extract-react-intl-messages": "^4.1.1",
"husky": "^8.0.3", "husky": "^8.0.3",
"jest": "^29.7.0",
"lint-staged": "^14.0.1", "lint-staged": "^14.0.1",
"postcss": "^8.4.31", "postcss": "^8.4.31",
"postcss-flexbugs-fixes": "^5.0.2", "postcss-flexbugs-fixes": "^5.0.2",
@ -159,10 +170,11 @@
"rollup-plugin-postcss": "^4.0.2", "rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"stylelint": "^15.10.1", "stylelint": "^15.10.1",
"stylelint-config-css-modules": "^4.1.0", "stylelint-config-css-modules": "^4.4.0",
"stylelint-config-prettier": "^9.0.3", "stylelint-config-prettier": "^9.0.3",
"stylelint-config-recommended": "^9.0.0", "stylelint-config-recommended": "^14.0.0",
"tar": "^6.1.2", "tar": "^6.1.2",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.1.6" "typescript": "^5.1.6"
} }

View File

@ -1616,7 +1616,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -32,7 +32,7 @@
"label.add-member": [ "label.add-member": [
{ {
"type": 0, "type": 0,
"value": "Add member" "value": "أضِف عضو"
} }
], ],
"label.add-website": [ "label.add-website": [
@ -44,7 +44,7 @@
"label.administrator": [ "label.administrator": [
{ {
"type": 0, "type": 0,
"value": "Administrator" "value": "مدير"
} }
], ],
"label.after": [ "label.after": [
@ -215,6 +215,12 @@
"value": "أُنشئت" "value": "أُنشئت"
} }
], ],
"label.created-by": [
{
"type": 0,
"value": "أُنشئ من قبل"
}
],
"label.current-password": [ "label.current-password": [
{ {
"type": 0, "type": 0,
@ -272,7 +278,7 @@
"label.delete-report": [ "label.delete-report": [
{ {
"type": 0, "type": 0,
"value": "Delete report" "value": "احذف التقرير"
} }
], ],
"label.delete-team": [ "label.delete-team": [
@ -332,7 +338,7 @@
"label.does-not-contain": [ "label.does-not-contain": [
{ {
"type": 0, "type": 0,
"value": "Does not contain" "value": "لا يحتوي"
} }
], ],
"label.domain": [ "label.domain": [
@ -362,7 +368,7 @@
"label.edit-member": [ "label.edit-member": [
{ {
"type": 0, "type": 0,
"value": "Edit member" "value": "عدّل العضو"
} }
], ],
"label.enable-share-url": [ "label.enable-share-url": [
@ -380,7 +386,7 @@
"label.event-data": [ "label.event-data": [
{ {
"type": 0, "type": 0,
"value": "Event data" "value": "تاريخ الحدث"
} }
], ],
"label.events": [ "label.events": [
@ -588,7 +594,7 @@
"label.manage": [ "label.manage": [
{ {
"type": 0, "type": 0,
"value": "Manage" "value": "التحكم"
} }
], ],
"label.max": [ "label.max": [
@ -600,7 +606,7 @@
"label.member": [ "label.member": [
{ {
"type": 0, "type": 0,
"value": "Member" "value": "عضو"
} }
], ],
"label.members": [ "label.members": [
@ -630,7 +636,7 @@
"label.my-account": [ "label.my-account": [
{ {
"type": 0, "type": 0,
"value": "My account" "value": "حسابي"
} }
], ],
"label.my-websites": [ "label.my-websites": [
@ -694,7 +700,7 @@
"label.ok": [ "label.ok": [
{ {
"type": 0, "type": 0,
"value": "OK" "value": "نعم"
} }
], ],
"label.os": [ "label.os": [
@ -842,7 +848,7 @@
"label.remove-member": [ "label.remove-member": [
{ {
"type": 0, "type": 0,
"value": "Remove member" "value": "احذف عضو"
} }
], ],
"label.reports": [ "label.reports": [
@ -914,7 +920,7 @@
"label.select": [ "label.select": [
{ {
"type": 0, "type": 0,
"value": "Select" "value": "اختر"
} }
], ],
"label.select-date": [ "label.select-date": [
@ -926,7 +932,7 @@
"label.select-role": [ "label.select-role": [
{ {
"type": 0, "type": 0,
"value": "Select role" "value": "حدد الدور"
} }
], ],
"label.select-website": [ "label.select-website": [
@ -1094,7 +1100,7 @@
"label.transfer-website": [ "label.transfer-website": [
{ {
"type": 0, "type": 0,
"value": "Transfer website" "value": "انقل الموقع"
} }
], ],
"label.true": [ "label.true": [
@ -1232,7 +1238,7 @@
"message.action-confirmation": [ "message.action-confirmation": [
{ {
"type": 0, "type": 0,
"value": "Type " "value": "اكتب "
}, },
{ {
"type": 1, "type": 1,
@ -1240,7 +1246,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " in the box below to confirm." "value": " في المربع أدناه للتأكيد."
} }
], ],
"message.active-users": [ "message.active-users": [
@ -1308,7 +1314,7 @@
"message.confirm-remove": [ "message.confirm-remove": [
{ {
"type": 0, "type": 0,
"value": "Are you sure you want to remove " "value": "هل انت متأكد من حذف "
}, },
{ {
"type": 1, "type": 1,
@ -1336,7 +1342,7 @@
"message.delete-team-warning": [ "message.delete-team-warning": [
{ {
"type": 0, "type": 0,
"value": "Deleting a team will also delete all team websites." "value": "حذف فريق سيؤدي إلى حذف جميع مواقع الفريق"
} }
], ],
"message.delete-website-warning": [ "message.delete-website-warning": [
@ -1532,25 +1538,25 @@
"message.transfer-team-website-to-user": [ "message.transfer-team-website-to-user": [
{ {
"type": 0, "type": 0,
"value": "Transfer this website to your account?" "value": "نقل هذا الموقع إلى حسابك؟"
} }
], ],
"message.transfer-user-website-to-team": [ "message.transfer-user-website-to-team": [
{ {
"type": 0, "type": 0,
"value": "Select the team to transfer this website to." "value": "اختر الفريق الذي تريد نقل الموقع إليه."
} }
], ],
"message.transfer-website": [ "message.transfer-website": [
{ {
"type": 0, "type": 0,
"value": "Transfer website ownership to your account or another team." "value": "نقل ملكية الموقع لحسابك أو فريق أخر."
} }
], ],
"message.triggered-event": [ "message.triggered-event": [
{ {
"type": 0, "type": 0,
"value": "Triggered event" "value": "أُطلق الحدث"
} }
], ],
"message.user-deleted": [ "message.user-deleted": [
@ -1562,7 +1568,7 @@
"message.viewed-page": [ "message.viewed-page": [
{ {
"type": 0, "type": 0,
"value": "Viewed page" "value": "شوهدت الصفحة"
} }
], ],
"message.visitor-log": [ "message.visitor-log": [
@ -1602,7 +1608,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "أنخفض عدد الزوار"
} }
] ]
} }

View File

@ -1602,7 +1602,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1602,7 +1602,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1602,7 +1602,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1602,7 +1602,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1602,7 +1602,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1594,7 +1594,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1590,7 +1590,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1602,7 +1602,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1602,7 +1602,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1608,7 +1608,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1602,7 +1602,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1602,7 +1602,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1602,7 +1602,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1602,7 +1602,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1590,7 +1590,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1610,7 +1610,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1590,7 +1590,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1594,7 +1594,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1616,7 +1616,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1606,7 +1606,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1570,7 +1570,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1606,7 +1606,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1596,7 +1596,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1574,7 +1574,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1578,7 +1578,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1727,7 +1727,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1616,7 +1616,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1594,7 +1594,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1582,7 +1582,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1606,7 +1606,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1602,7 +1602,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1602,7 +1602,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1606,7 +1606,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1606,7 +1606,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1602,7 +1602,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1570,7 +1570,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1608,7 +1608,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1602,7 +1602,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1602,7 +1602,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1606,7 +1606,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1598,7 +1598,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1598,7 +1598,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1570,7 +1570,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1578,7 +1578,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1602,7 +1602,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1594,7 +1594,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1594,7 +1594,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -1596,7 +1596,7 @@
"message.visitors-dropped-off": [ "message.visitors-dropped-off": [
{ {
"type": 0, "type": 0,
"value": "Visitors droppped off" "value": "Visitors dropped off"
} }
] ]
} }

View File

@ -10,10 +10,7 @@ if (endPoint) {
const tracker = fs.readFileSync(file); const tracker = fs.readFileSync(file);
fs.writeFileSync( fs.writeFileSync(path.resolve(file), tracker.toString().replace(/\/api\/send/g, endPoint));
path.resolve(file),
tracker.toString().replace(/"\/api\/send"/g, `"${endPoint}"`),
);
console.log(`Updated tracker endpoint: ${endPoint}.`); console.log(`Updated tracker endpoint: ${endPoint}.`);
} }

View File

@ -1,9 +1,12 @@
.selected {
font-weight: bold;
}
.popup { .popup {
display: flex; display: flex;
max-width: 300px;
max-height: 400px;
overflow-x: hidden;
}
.popup > div {
overflow-y: auto;
} }
.filter { .filter {
@ -17,6 +20,5 @@
} }
.menu { .menu {
min-width: 360px; min-width: 200px;
max-height: 300px;
} }

View File

@ -27,6 +27,7 @@ export default function FieldFilterForm({
const { formatValue } = useFormat(); const { formatValue } = useFormat();
const { locale } = useLocale(); const { locale } = useLocale();
const filters = getFilters(type); const filters = getFilters(type);
const [search, setSearch] = useState('');
const formattedValues = useMemo(() => { const formattedValues = useMemo(() => {
const formatted = {}; const formatted = {};
@ -43,6 +44,10 @@ export default function FieldFilterForm({
return formatted; return formatted;
}, [formatValue, locale, name, values]); }, [formatValue, locale, name, values]);
const filteredValues = useMemo(() => {
return search ? values.filter(n => n.includes(search)) : values;
}, [search, formattedValues]);
const renderFilterValue = value => { const renderFilterValue = value => {
return filters.find(f => f.value === value)?.label; return filters.find(f => f.value === value)?.label;
}; };
@ -74,14 +79,14 @@ export default function FieldFilterForm({
)} )}
<Dropdown <Dropdown
className={styles.dropdown} className={styles.dropdown}
popupProps={{ className: styles.popup }}
menuProps={{ className: styles.menu }} menuProps={{ className: styles.menu }}
items={values} items={filteredValues}
value={value} value={value}
renderValue={renderValue} renderValue={renderValue}
onChange={(key: any) => setValue(key)} onChange={(key: any) => setValue(key)}
style={{ allowSearch={true}
minWidth: '250px', onSearch={setSearch}
}}
> >
{(value: string) => { {(value: string) => {
return <Item key={value}>{formattedValues[value]}</Item>; return <Item key={value}>{formattedValues[value]}</Item>;

View File

@ -1,5 +1,5 @@
.body { .body {
padding-left: 20px; padding-inline-start: 20px;
grid-row: 2/3; grid-row: 2/3;
grid-column: 2 / 3; grid-column: 2 / 3;
} }

View File

@ -4,6 +4,7 @@ import { useMessages, useApi, useNavigation, useTeamUrl } from 'components/hooks
import { ReportContext } from './Report'; import { ReportContext } from './Report';
import styles from './ReportHeader.module.css'; import styles from './ReportHeader.module.css';
import { REPORT_TYPES } from 'lib/constants'; import { REPORT_TYPES } from 'lib/constants';
import Breadcrumb from 'components/common/Breadcrumb';
export function ReportHeader({ icon }) { export function ReportHeader({ icon }) {
const { report, updateReport } = useContext(ReportContext); const { report, updateReport } = useContext(ReportContext);
@ -57,9 +58,16 @@ export function ReportHeader({ icon }) {
<div className={styles.header}> <div className={styles.header}>
<div> <div>
<div className={styles.type}> <div className={styles.type}>
{formatMessage( <Breadcrumb
labels[Object.keys(REPORT_TYPES).find(key => REPORT_TYPES[key] === report?.type)], data={[
)} { label: formatMessage(labels.reports), url: '/reports' },
{
label: formatMessage(
labels[Object.keys(REPORT_TYPES).find(key => REPORT_TYPES[key] === report?.type)],
),
},
]}
/>
</div> </div>
<div className={styles.title}> <div className={styles.title}>
<Icon size="lg">{icon}</Icon> <Icon size="lg">{icon}</Icon>

View File

@ -1,7 +1,7 @@
.menu { .menu {
width: 300px; width: 300px;
padding-right: 20px; padding-inline-end: 20px;
border-right: 1px solid var(--base300); border-inline-end: 1px solid var(--base300);
grid-row: 2 / 3; grid-row: 2 / 3;
grid-column: 1 / 2; grid-column: 1 / 2;
} }

View File

@ -63,7 +63,7 @@
.value { .value {
color: var(--base50); color: var(--base50);
margin-right: 20px; margin-inline-end: 20px;
} }
.track { .track {

View File

@ -13,5 +13,5 @@
.popup { .popup {
margin-top: -10px; margin-top: -10px;
margin-left: 30px; margin-inline-start: 30px;
} }

View File

@ -7,5 +7,5 @@
.tag { .tag {
text-align: center; text-align: center;
margin-bottom: 10px; margin-bottom: 10px;
margin-right: 20px; margin-inline-end: 20px;
} }

View File

@ -6,6 +6,7 @@ import PageHeader from 'components/layout/PageHeader';
import { useMessages } from 'components/hooks'; import { useMessages } from 'components/hooks';
import UserWebsites from './UserWebsites'; import UserWebsites from './UserWebsites';
import { UserContext } from './UserProvider'; import { UserContext } from './UserProvider';
import Breadcrumb from 'components/common/Breadcrumb';
export function UserSettings({ userId }: { userId: string }) { export function UserSettings({ userId }: { userId: string }) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
@ -17,9 +18,23 @@ export function UserSettings({ userId }: { userId: string }) {
showToast({ message: formatMessage(messages.saved), variant: 'success' }); showToast({ message: formatMessage(messages.saved), variant: 'success' });
}; };
const breadcrumb = (
<Breadcrumb
data={[
{
label: formatMessage(labels.users),
url: '/settings/users',
},
{
label: user.username,
},
]}
/>
);
return ( return (
<> <>
<PageHeader title={user?.username} icon={<Icons.User />} /> <PageHeader title={user?.username} icon={<Icons.User />} breadcrumb={breadcrumb} />
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30, fontSize: 14 }}> <Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30, fontSize: 14 }}>
<Item key="details">{formatMessage(labels.details)}</Item> <Item key="details">{formatMessage(labels.details)}</Item>
<Item key="websites">{formatMessage(labels.websites)}</Item> <Item key="websites">{formatMessage(labels.websites)}</Item>

View File

@ -15,7 +15,7 @@ export function WebsiteAddButton({ teamId, onSave }: { teamId: string; onSave?:
return ( return (
<ModalTrigger> <ModalTrigger>
<Button variant="primary"> <Button data-test="button-website-add" variant="primary">
<Icon> <Icon>
<Icons.Plus /> <Icons.Plus />
</Icon> </Icon>

View File

@ -38,12 +38,17 @@ export function WebsiteAddForm({
return ( return (
<Form onSubmit={handleSubmit} error={error}> <Form onSubmit={handleSubmit} error={error}>
<FormRow label={formatMessage(labels.name)}> <FormRow label={formatMessage(labels.name)}>
<FormInput name="name" rules={{ required: formatMessage(labels.required) }}> <FormInput
data-test="input-name"
name="name"
rules={{ required: formatMessage(labels.required) }}
>
<TextField autoComplete="off" /> <TextField autoComplete="off" />
</FormInput> </FormInput>
</FormRow> </FormRow>
<FormRow label={formatMessage(labels.domain)}> <FormRow label={formatMessage(labels.domain)}>
<FormInput <FormInput
data-test="input-domain"
name="domain" name="domain"
rules={{ rules={{
required: formatMessage(labels.required), required: formatMessage(labels.required),
@ -54,7 +59,7 @@ export function WebsiteAddForm({
</FormInput> </FormInput>
</FormRow> </FormRow>
<FormButtons flex> <FormButtons flex>
<SubmitButton variant="primary" disabled={false}> <SubmitButton data-test="button-submit" variant="primary" disabled={false}>
{formatMessage(labels.save)} {formatMessage(labels.save)}
</SubmitButton> </SubmitButton>
{onClose && ( {onClose && (

View File

@ -36,7 +36,7 @@ export function WebsitesTable({
<> <>
{allowEdit && ( {allowEdit && (
<LinkButton href={renderTeamUrl(`/settings/websites/${websiteId}`)}> <LinkButton href={renderTeamUrl(`/settings/websites/${websiteId}`)}>
<Icon> <Icon data-test="link-button-edit">
<Icons.Edit /> <Icons.Edit />
</Icon> </Icon>
<Text>{formatMessage(labels.edit)}</Text> <Text>{formatMessage(labels.edit)}</Text>

View File

@ -66,7 +66,9 @@ export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?:
description={formatMessage(messages.deleteWebsiteWarning)} description={formatMessage(messages.deleteWebsiteWarning)}
> >
<ModalTrigger> <ModalTrigger>
<Button variant="danger">{formatMessage(labels.delete)}</Button> <Button data-test="button-delete" variant="danger">
{formatMessage(labels.delete)}
</Button>
<Modal title={formatMessage(labels.deleteWebsite)}> <Modal title={formatMessage(labels.deleteWebsite)}>
{(close: () => void) => ( {(close: () => void) => (
<WebsiteDeleteForm websiteId={websiteId} onSave={handleSave} onClose={close} /> <WebsiteDeleteForm websiteId={websiteId} onSave={handleSave} onClose={close} />

View File

@ -27,15 +27,20 @@ export function WebsiteEditForm({ websiteId, onSave }: { websiteId: string; onSa
return ( return (
<Form ref={ref} onSubmit={handleSubmit} error={error} values={website}> <Form ref={ref} onSubmit={handleSubmit} error={error} values={website}>
<FormRow label={formatMessage(labels.websiteId)}> <FormRow label={formatMessage(labels.websiteId)}>
<TextField value={website?.id} readOnly allowCopy /> <TextField data-test="text-field-websiteId" value={website?.id} readOnly allowCopy />
</FormRow> </FormRow>
<FormRow label={formatMessage(labels.name)}> <FormRow label={formatMessage(labels.name)}>
<FormInput name="name" rules={{ required: formatMessage(labels.required) }}> <FormInput
data-test="input-name"
name="name"
rules={{ required: formatMessage(labels.required) }}
>
<TextField /> <TextField />
</FormInput> </FormInput>
</FormRow> </FormRow>
<FormRow label={formatMessage(labels.domain)}> <FormRow label={formatMessage(labels.domain)}>
<FormInput <FormInput
data-test="input-domain"
name="domain" name="domain"
rules={{ rules={{
required: formatMessage(labels.required), required: formatMessage(labels.required),
@ -49,7 +54,9 @@ export function WebsiteEditForm({ websiteId, onSave }: { websiteId: string; onSa
</FormInput> </FormInput>
</FormRow> </FormRow>
<FormButtons> <FormButtons>
<SubmitButton variant="primary">{formatMessage(labels.save)}</SubmitButton> <SubmitButton data-test="button-submit" variant="primary">
{formatMessage(labels.save)}
</SubmitButton>
</FormButtons> </FormButtons>
</Form> </Form>
); );

View File

@ -1,14 +1,15 @@
import { useState, Key, useContext } from 'react'; import { WebsiteContext } from 'app/(main)/websites/[websiteId]/WebsiteProvider';
import { Item, Tabs, Button, Text, Icon, useToasts } from 'react-basics'; import Breadcrumb from 'components/common/Breadcrumb';
import Link from 'next/link'; import { useMessages } from 'components/hooks';
import Icons from 'components/icons'; import Icons from 'components/icons';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import WebsiteEditForm from './WebsiteEditForm'; import Link from 'next/link';
import WebsiteData from './WebsiteData'; import { Key, useContext, useState } from 'react';
import TrackingCode from './TrackingCode'; import { Button, Icon, Item, Tabs, Text, useToasts } from 'react-basics';
import ShareUrl from './ShareUrl'; import ShareUrl from './ShareUrl';
import { useMessages } from 'components/hooks'; import TrackingCode from './TrackingCode';
import { WebsiteContext } from 'app/(main)/websites/[websiteId]/WebsiteProvider'; import WebsiteData from './WebsiteData';
import WebsiteEditForm from './WebsiteEditForm';
export function WebsiteSettings({ export function WebsiteSettings({
websiteId, websiteId,
@ -28,9 +29,23 @@ export function WebsiteSettings({
showToast({ message: formatMessage(messages.saved), variant: 'success' }); showToast({ message: formatMessage(messages.saved), variant: 'success' });
}; };
const breadcrumb = (
<Breadcrumb
data={[
{
label: formatMessage(labels.websites),
url: website.teamId ? `/teams/${website.teamId}/settings/websites` : '/settings/websites',
},
{
label: website.name,
},
]}
/>
);
return ( return (
<> <>
<PageHeader title={website?.name} icon={<Icons.Globe />}> <PageHeader title={website?.name} icon={<Icons.Globe />} breadcrumb={breadcrumb}>
<Link href={`/websites/${websiteId}`} target={openExternal ? '_blank' : null}> <Link href={`/websites/${websiteId}`} target={openExternal ? '_blank' : null}>
<Button variant="primary"> <Button variant="primary">
<Icon> <Icon>

View File

@ -58,6 +58,6 @@
gap: 20px; gap: 20px;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding-right: 0; padding-inline-end: 0;
} }
} }

View File

@ -12,7 +12,7 @@ import ReferrersTable from 'components/metrics/ReferrersTable';
import ScreenTable from 'components/metrics/ScreenTable'; import ScreenTable from 'components/metrics/ScreenTable';
import EventsTable from 'components/metrics/EventsTable'; import EventsTable from 'components/metrics/EventsTable';
import SideNav from 'components/layout/SideNav'; import SideNav from 'components/layout/SideNav';
import { useNavigation, useMessages } from 'components/hooks'; import { useNavigation, useMessages, useLocale } from 'components/hooks';
import LinkButton from 'components/common/LinkButton'; import LinkButton from 'components/common/LinkButton';
import styles from './WebsiteExpandedView.module.css'; import styles from './WebsiteExpandedView.module.css';
@ -39,6 +39,7 @@ export default function WebsiteExpandedView({
websiteId: string; websiteId: string;
domainName?: string; domainName?: string;
}) { }) {
const { dir } = useLocale();
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { const {
router, router,
@ -122,7 +123,7 @@ export default function WebsiteExpandedView({
<div className={styles.layout}> <div className={styles.layout}>
<div className={styles.menu}> <div className={styles.menu}>
<LinkButton href={pathname} className={styles.back} variant="quiet" scroll={false}> <LinkButton href={pathname} className={styles.back} variant="quiet" scroll={false}>
<Icon rotate={180}> <Icon rotate={dir === 'rtl' ? 0 : 180}>
<Icons.ArrowRight /> <Icons.ArrowRight />
</Icon> </Icon>
<Text>{formatMessage(labels.back)}</Text> <Text>{formatMessage(labels.back)}</Text>

View File

@ -1,12 +1,12 @@
import { ReactNode } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { Text, Button, Icon } from 'react-basics'; import Favicon from 'components/common/Favicon';
import { useMessages, useWebsite } from 'components/hooks';
import Icons from 'components/icons';
import ActiveUsers from 'components/metrics/ActiveUsers';
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import Favicon from 'components/common/Favicon'; import { ReactNode } from 'react';
import ActiveUsers from 'components/metrics/ActiveUsers'; import { Button, Icon, Text } from 'react-basics';
import Icons from 'components/icons';
import { useMessages, useWebsite } from 'components/hooks';
import styles from './WebsiteHeader.module.css'; import styles from './WebsiteHeader.module.css';
export function WebsiteHeader({ export function WebsiteHeader({

View File

@ -52,7 +52,6 @@ export function WebsiteMetricsBar({
change={uniques.change} change={uniques.change}
/> />
<MetricCard <MetricCard
className={styles.card}
label={formatMessage(labels.bounceRate)} label={formatMessage(labels.bounceRate)}
value={uniques.value ? (num / uniques.value) * 100 : 0} value={uniques.value ? (num / uniques.value) * 100 : 0}
change={ change={
@ -65,7 +64,6 @@ export function WebsiteMetricsBar({
reverseColors reverseColors
/> />
<MetricCard <MetricCard
className={styles.card}
label={formatMessage(labels.averageVisitTime)} label={formatMessage(labels.averageVisitTime)}
value={ value={
totaltime.value && pageviews.value totaltime.value && pageviews.value

View File

@ -27,7 +27,7 @@
} }
.icon { .icon {
margin-right: 10px; margin-inline-end: 10px;
} }
.time { .time {

View File

@ -5,6 +5,7 @@ import { ReactBasicsProvider } from 'react-basics';
import ErrorBoundary from 'components/common/ErrorBoundary'; import ErrorBoundary from 'components/common/ErrorBoundary';
import { useLocale } from 'components/hooks'; import { useLocale } from 'components/hooks';
import 'chartjs-adapter-date-fns'; import 'chartjs-adapter-date-fns';
import { useEffect } from 'react';
const client = new QueryClient({ const client = new QueryClient({
defaultOptions: { defaultOptions: {
@ -16,7 +17,13 @@ const client = new QueryClient({
}); });
function MessagesProvider({ children }) { function MessagesProvider({ children }) {
const { locale, messages } = useLocale(); const { locale, messages, dir } = useLocale();
useEffect(() => {
document.documentElement.setAttribute('dir', dir);
document.documentElement.setAttribute('lang', locale);
}, [locale, dir]);
return ( return (
<IntlProvider locale={locale} messages={messages[locale]} onError={() => null}> <IntlProvider locale={locale} messages={messages[locale]} onError={() => null}>
{children} {children}

View File

@ -42,17 +42,30 @@ export function LoginForm() {
<div className={styles.title}>umami</div> <div className={styles.title}>umami</div>
<Form className={styles.form} onSubmit={handleSubmit} error={getMessage(error)}> <Form className={styles.form} onSubmit={handleSubmit} error={getMessage(error)}>
<FormRow label={formatMessage(labels.username)}> <FormRow label={formatMessage(labels.username)}>
<FormInput name="username" rules={{ required: formatMessage(labels.required) }}> <FormInput
data-test="input-username"
name="username"
rules={{ required: formatMessage(labels.required) }}
>
<TextField autoComplete="off" /> <TextField autoComplete="off" />
</FormInput> </FormInput>
</FormRow> </FormRow>
<FormRow label={formatMessage(labels.password)}> <FormRow label={formatMessage(labels.password)}>
<FormInput name="password" rules={{ required: formatMessage(labels.required) }}> <FormInput
data-test="input-password"
name="password"
rules={{ required: formatMessage(labels.required) }}
>
<PasswordField /> <PasswordField />
</FormInput> </FormInput>
</FormRow> </FormRow>
<FormButtons> <FormButtons>
<SubmitButton className={styles.button} variant="primary" disabled={isPending}> <SubmitButton
data-test="button-submit"
className={styles.button}
variant="primary"
disabled={isPending}
>
{formatMessage(labels.login)} {formatMessage(labels.login)}
</SubmitButton> </SubmitButton>
</FormButtons> </FormButtons>

View File

@ -0,0 +1,10 @@
.bar {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
color: var(--base600);
}
.link span {
color: var(--base700) !important;
}

View File

@ -0,0 +1,37 @@
import Link from 'next/link';
import { Flexbox, Icon, Icons, Text } from 'react-basics';
import styles from './Breadcrumb.module.css';
export interface BreadcrumbProps {
data: {
url?: string;
label: string;
}[];
}
export function Breadcrumb({ data }: BreadcrumbProps) {
return (
<Flexbox alignItems="center" gap={3} className={styles.bar}>
{data.map((a, i) => {
return (
<>
{a.url ? (
<Link href={a.url} className={styles.link}>
<Text>{a.label}</Text>
</Link>
) : (
<Text>{a.label}</Text>
)}
{i !== data.length - 1 ? (
<Icon rotate={270}>
<Icons.ChevronDown />
</Icon>
) : null}
</>
);
})}
</Flexbox>
);
}
export default Breadcrumb;

View File

@ -11,5 +11,5 @@
} }
.icon { .icon {
margin-right: 10px; margin-inline-end: 10px;
} }

View File

@ -35,5 +35,5 @@ a.item.selected,
.submenu a.item { .submenu a.item {
color: var(--base600); color: var(--base600);
margin-left: 40px; margin-inline-start: 40px;
} }

View File

@ -6,7 +6,7 @@
z-index: var(--z-index-popup); z-index: var(--z-index-popup);
border-radius: 5px; border-radius: 5px;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
margin-left: 10px; margin-inline-start: 10px;
} }
.item { .item {

View File

@ -18,7 +18,7 @@
font-size: 11px; font-size: 11px;
color: var(--base600); color: var(--base600);
text-align: right; text-align: right;
margin-right: 10px; margin-inline-end: 10px;
} }
.name { .name {

View File

@ -25,7 +25,7 @@ export function ProfileButton() {
return ( return (
<PopupTrigger> <PopupTrigger>
<Button variant="quiet"> <Button data-test="button-profile" variant="quiet">
<Icon> <Icon>
<Icons.Profile /> <Icons.Profile />
</Icon> </Icon>
@ -41,7 +41,7 @@ export function ProfileButton() {
<Text>{formatMessage(labels.profile)}</Text> <Text>{formatMessage(labels.profile)}</Text>
</Item> </Item>
{!cloudMode && ( {!cloudMode && (
<Item key="logout" className={styles.item}> <Item data-test="item-logout" key="logout" className={styles.item}>
<Icon> <Icon>
<Icons.Logout /> <Icons.Logout />
</Icon> </Icon>

View File

@ -23,7 +23,7 @@ export function TeamsButton({
close(); close();
}; };
if (!result) { if (!result?.count) {
return null; return null;
} }

View File

@ -13,12 +13,12 @@
} }
.buttons button:first-child { .buttons button:first-child {
border-top-right-radius: 0; border-start-end-radius: 0;
border-bottom-right-radius: 0; border-end-end-radius: 0;
border-inline-end: 1px solid var(--base400);
} }
.buttons button:last-child { .buttons button:last-child {
border-top-left-radius: 0; border-start-start-radius: 0;
border-bottom-left-radius: 0; border-end-start-radius: 0;
border-left: 1px solid var(--base400) !important;
} }

View File

@ -1,4 +1,4 @@
import { useDateRange } from 'components/hooks'; import { useDateRange, useLocale } from 'components/hooks';
import { isAfter } from 'date-fns'; import { isAfter } from 'date-fns';
import { getOffsetDateRange } from 'lib/date'; import { getOffsetDateRange } from 'lib/date';
import { Button, Icon, Icons } from 'react-basics'; import { Button, Icon, Icons } from 'react-basics';
@ -7,6 +7,7 @@ import styles from './WebsiteDateFilter.module.css';
import { DateRange } from 'lib/types'; import { DateRange } from 'lib/types';
export function WebsiteDateFilter({ websiteId }: { websiteId: string }) { export function WebsiteDateFilter({ websiteId }: { websiteId: string }) {
const { dir } = useLocale();
const [dateRange, setDateRange] = useDateRange(websiteId); const [dateRange, setDateRange] = useDateRange(websiteId);
const { value, startDate, endDate, offset } = dateRange; const { value, startDate, endDate, offset } = dateRange;
const disableForward = const disableForward =
@ -22,15 +23,15 @@ export function WebsiteDateFilter({ websiteId }: { websiteId: string }) {
return ( return (
<div className={styles.container}> <div className={styles.container}>
{value !== 'all' && ( {value !== 'all' && !value.startsWith('range') && (
<div className={styles.buttons}> <div className={styles.buttons}>
<Button onClick={() => handleIncrement(-1)}> <Button onClick={() => handleIncrement(-1)}>
<Icon rotate={90}> <Icon rotate={dir === 'rtl' ? 270 : 90}>
<Icons.ChevronDown /> <Icons.ChevronDown />
</Icon> </Icon>
</Button> </Button>
<Button onClick={() => handleIncrement(1)} disabled={disableForward}> <Button onClick={() => handleIncrement(1)} disabled={disableForward}>
<Icon rotate={270}> <Icon rotate={dir === 'rtl' ? 90 : 270}>
<Icons.ChevronDown /> <Icons.ChevronDown />
</Icon> </Icon>
</Button> </Button>

View File

@ -7,7 +7,7 @@
.menu { .menu {
width: 240px; width: 240px;
padding-top: 34px; padding-top: 34px;
padding-right: 20px; padding-inline-end: 20px;
} }
.content { .content {

View File

@ -29,12 +29,12 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
border-right: 2px solid var(--base200); border-inline-end: 2px solid var(--base200);
padding: 1rem 2rem; padding: 1rem 2rem;
gap: var(--size500); gap: var(--size500);
font-weight: 600; font-weight: 600;
width: 200px; width: 200px;
margin-right: -2px; margin-inline-end: -2px;
} }
a.item { a.item {
@ -43,7 +43,7 @@ a.item {
.item.selected { .item.selected {
color: var(--base900); color: var(--base900);
border-right-color: var(--primary400); border-inline-end-color: var(--primary400);
background: var(--blue100); background: var(--blue100);
} }

View File

@ -27,9 +27,13 @@
flex: 1; flex: 1;
} }
.breadcrumb {
padding-top: 20px;
}
.icon { .icon {
color: var(--base700); color: var(--base700);
margin-right: 1rem; margin-inline-end: 1rem;
} }
.actions { .actions {

View File

@ -7,23 +7,29 @@ export function PageHeader({
title, title,
icon, icon,
className, className,
breadcrumb,
children, children,
}: { }: {
title?: ReactNode; title?: ReactNode;
icon?: ReactNode; icon?: ReactNode;
className?: string; className?: string;
breadcrumb?: ReactNode;
children?: ReactNode; children?: ReactNode;
}) { }) {
return ( return (
<div className={classNames(styles.header, className)}> <>
{icon && ( <div className={styles.breadcrumb}>{breadcrumb}</div>
<Icon size="lg" className={styles.icon}> <div className={classNames(styles.header, className)}>
{icon} {icon && (
</Icon> <Icon size="lg" className={styles.icon}>
)} {icon}
{title && <div className={styles.title}>{title}</div>} </Icon>
<div className={styles.actions}>{children}</div> )}
</div>
{title && <div className={styles.title}>{title}</div>}
<div className={styles.actions}>{children}</div>
</div>
</>
); );
} }

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