mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-22 09:57:00 +01:00
Merge branch 'dev' into localization
This commit is contained in:
commit
f0c6960dc3
@ -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",
|
||||||
|
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@ -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
1
.gitignore
vendored
@ -35,6 +35,7 @@ yarn-error.log*
|
|||||||
# local env files
|
# local env files
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
*.env.*
|
||||||
|
|
||||||
*.dev.yml
|
*.dev.yml
|
||||||
|
|
||||||
|
@ -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
7
cypress.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'cypress';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
e2e: {
|
||||||
|
baseUrl: 'http://localhost:3000',
|
||||||
|
},
|
||||||
|
});
|
51
cypress/docker-compose.yml
Normal file
51
cypress/docker-compose.yml
Normal 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
18
cypress/e2e/login.cy.ts
Normal 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
91
cypress/e2e/website.cy.ts
Normal 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
57
cypress/support/e2e.ts
Normal 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
26
cypress/support/index.d.ts
vendored
Normal 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
8
cypress/tsconfig.json
Normal 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
7
jest.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export default {
|
||||||
|
roots: ['./src'],
|
||||||
|
testMatch: ['**/__tests__/**/*.+(ts|tsx|js)', '**/?(*.)+(spec|test).+(ts|tsx|js)'],
|
||||||
|
transform: {
|
||||||
|
'^.+\\.(ts|tsx)$': 'ts-jest',
|
||||||
|
},
|
||||||
|
};
|
26
package.json
26
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "umami",
|
"name": "umami",
|
||||||
"version": "2.10.1",
|
"version": "2.11.0",
|
||||||
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
|
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
|
||||||
"author": "Umami Software, Inc. <hello@umami.is>",
|
"author": "Umami Software, Inc. <hello@umami.is>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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 dropped off"
|
"value": "أنخفض عدد الزوار"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -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}.`);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
@ -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>;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,7 @@
|
|||||||
|
|
||||||
.value {
|
.value {
|
||||||
color: var(--base50);
|
color: var(--base50);
|
||||||
margin-right: 20px;
|
margin-inline-end: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track {
|
.track {
|
||||||
|
@ -13,5 +13,5 @@
|
|||||||
|
|
||||||
.popup {
|
.popup {
|
||||||
margin-top: -10px;
|
margin-top: -10px;
|
||||||
margin-left: 30px;
|
margin-inline-start: 30px;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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 && (
|
||||||
|
@ -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>
|
||||||
|
@ -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} />
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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({
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
margin-right: 10px;
|
margin-inline-end: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time {
|
.time {
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
10
src/components/common/Breadcrumb.module.css
Normal file
10
src/components/common/Breadcrumb.module.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.bar {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--base600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link span {
|
||||||
|
color: var(--base700) !important;
|
||||||
|
}
|
37
src/components/common/Breadcrumb.tsx
Normal file
37
src/components/common/Breadcrumb.tsx
Normal 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;
|
@ -11,5 +11,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
margin-right: 10px;
|
margin-inline-end: 10px;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
@ -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 =
|
||||||
@ -25,12 +26,12 @@ export function WebsiteDateFilter({ websiteId }: { websiteId: string }) {
|
|||||||
{value !== 'all' && (
|
{value !== 'all' && (
|
||||||
<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>
|
||||||
|
@ -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 {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-left: 20px;
|
margin-inline-start: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
@ -13,5 +13,5 @@
|
|||||||
|
|
||||||
.value {
|
.value {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-right: 4px;
|
margin-inline-end: 4px;
|
||||||
}
|
}
|
||||||
|
@ -14,9 +14,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.calendars > div + div {
|
.calendars > div + div {
|
||||||
margin-left: 20px;
|
margin-inline-start: 20px;
|
||||||
padding-left: 20px;
|
padding-inline-start: 20px;
|
||||||
border-left: 1px solid var(--base300);
|
border-inline-start: 1px solid var(--base300);
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter {
|
.filter {
|
||||||
@ -41,7 +41,7 @@
|
|||||||
|
|
||||||
.calendars > div + div {
|
.calendars > div + div {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-left: 0;
|
margin-inline-start: 0;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,11 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row:hover {
|
||||||
|
background-color: var(--base75);
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
@ -46,6 +51,7 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex: 2;
|
flex: 2;
|
||||||
|
padding-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label a {
|
.label a {
|
||||||
@ -76,7 +82,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
color: var(--base600);
|
color: var(--base600);
|
||||||
border-left: 1px solid var(--base600);
|
border-inline-start: 1px solid var(--base600);
|
||||||
padding-inline-start: 10px;
|
padding-inline-start: 10px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
@ -68,6 +68,8 @@ export function MetricsTable({
|
|||||||
country,
|
country,
|
||||||
region,
|
region,
|
||||||
city,
|
city,
|
||||||
|
limit,
|
||||||
|
search,
|
||||||
},
|
},
|
||||||
{ retryDelay: delay || DEFAULT_ANIMATION_DURATION, onDataLoad },
|
{ retryDelay: delay || DEFAULT_ANIMATION_DURATION, onDataLoad },
|
||||||
);
|
);
|
||||||
@ -86,20 +88,8 @@ export function MetricsTable({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (search) {
|
|
||||||
items = items.filter(({ x, ...data }) => {
|
|
||||||
const value = formatValue(x, type, data);
|
|
||||||
|
|
||||||
return value?.toLowerCase().includes(search.toLowerCase());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
items = percentFilter(items);
|
items = percentFilter(items);
|
||||||
|
|
||||||
if (limit) {
|
|
||||||
items = items.slice(0, limit - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
@ -114,6 +104,7 @@ export function MetricsTable({
|
|||||||
className={styles.search}
|
className={styles.search}
|
||||||
value={search}
|
value={search}
|
||||||
onSearch={setSearch}
|
onSearch={setSearch}
|
||||||
|
delay={300}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
1
src/declaration.d.ts
vendored
1
src/declaration.d.ts
vendored
@ -2,3 +2,4 @@ declare module 'cors';
|
|||||||
declare module 'debug';
|
declare module 'debug';
|
||||||
declare module 'chartjs-adapter-date-fns';
|
declare module 'chartjs-adapter-date-fns';
|
||||||
declare module 'md5';
|
declare module 'md5';
|
||||||
|
declare module 'request-ip';
|
||||||
|
@ -4,9 +4,9 @@
|
|||||||
"label.activity-log": "سجل الأحداث",
|
"label.activity-log": "سجل الأحداث",
|
||||||
"label.add": "أضِف",
|
"label.add": "أضِف",
|
||||||
"label.add-description": "أضِف وصف",
|
"label.add-description": "أضِف وصف",
|
||||||
"label.add-member": "Add member",
|
"label.add-member": "أضِف عضو",
|
||||||
"label.add-website": "إضافة موقع",
|
"label.add-website": "إضافة موقع",
|
||||||
"label.administrator": "Administrator",
|
"label.administrator": "مدير",
|
||||||
"label.after": "يعد",
|
"label.after": "يعد",
|
||||||
"label.all": "الكل",
|
"label.all": "الكل",
|
||||||
"label.all-time": "كل الوقت",
|
"label.all-time": "كل الوقت",
|
||||||
@ -35,7 +35,7 @@
|
|||||||
"label.create-team": "أنشِئ فريق",
|
"label.create-team": "أنشِئ فريق",
|
||||||
"label.create-user": "أنشِئ مستخدم",
|
"label.create-user": "أنشِئ مستخدم",
|
||||||
"label.created": "أُنشئت",
|
"label.created": "أُنشئت",
|
||||||
"label.created-by": "Created By",
|
"label.created-by": "أُنشئ من قبل",
|
||||||
"label.current-password": "كلمة المرور الحالية",
|
"label.current-password": "كلمة المرور الحالية",
|
||||||
"label.custom-range": "فترة مخصّصة",
|
"label.custom-range": "فترة مخصّصة",
|
||||||
"label.dashboard": "الشاشة الرئيسية",
|
"label.dashboard": "الشاشة الرئيسية",
|
||||||
@ -45,7 +45,7 @@
|
|||||||
"label.day": "يوم",
|
"label.day": "يوم",
|
||||||
"label.default-date-range": "الفترة المخصّصة الافتراضية",
|
"label.default-date-range": "الفترة المخصّصة الافتراضية",
|
||||||
"label.delete": "حذف",
|
"label.delete": "حذف",
|
||||||
"label.delete-report": "Delete report",
|
"label.delete-report": "احذف التقرير",
|
||||||
"label.delete-team": "حذف الفريق",
|
"label.delete-team": "حذف الفريق",
|
||||||
"label.delete-user": "جذف مستخدم",
|
"label.delete-user": "جذف مستخدم",
|
||||||
"label.delete-website": "حذف الموقع",
|
"label.delete-website": "حذف الموقع",
|
||||||
@ -55,15 +55,15 @@
|
|||||||
"label.device": "الجهاز",
|
"label.device": "الجهاز",
|
||||||
"label.devices": "الأجهزة",
|
"label.devices": "الأجهزة",
|
||||||
"label.dismiss": "تجاهل",
|
"label.dismiss": "تجاهل",
|
||||||
"label.does-not-contain": "Does not contain",
|
"label.does-not-contain": "لا يحتوي",
|
||||||
"label.domain": "النطاق",
|
"label.domain": "النطاق",
|
||||||
"label.dropoff": "إنزال",
|
"label.dropoff": "إنزال",
|
||||||
"label.edit": "عدّل",
|
"label.edit": "عدّل",
|
||||||
"label.edit-dashboard": "عدّل لوحة التحكم",
|
"label.edit-dashboard": "عدّل لوحة التحكم",
|
||||||
"label.edit-member": "Edit member",
|
"label.edit-member": "عدّل العضو",
|
||||||
"label.enable-share-url": "فعّل مشاركة الرابط",
|
"label.enable-share-url": "فعّل مشاركة الرابط",
|
||||||
"label.event": "الحدث",
|
"label.event": "الحدث",
|
||||||
"label.event-data": "Event data",
|
"label.event-data": "تاريخ الحدث",
|
||||||
"label.events": "الأحداث",
|
"label.events": "الأحداث",
|
||||||
"label.false": "خطأ",
|
"label.false": "خطأ",
|
||||||
"label.field": "الحقل",
|
"label.field": "الحقل",
|
||||||
@ -95,20 +95,20 @@
|
|||||||
"label.less-than-equals": "أقل مِن أو يساوي",
|
"label.less-than-equals": "أقل مِن أو يساوي",
|
||||||
"label.login": "تسجيل الدخول",
|
"label.login": "تسجيل الدخول",
|
||||||
"label.logout": "تسجيل الخروج",
|
"label.logout": "تسجيل الخروج",
|
||||||
"label.manage": "Manage",
|
"label.manage": "التحكم",
|
||||||
"label.max": "الحد الأقصى",
|
"label.max": "الحد الأقصى",
|
||||||
"label.member": "Member",
|
"label.member": "عضو",
|
||||||
"label.members": "الأعضاء",
|
"label.members": "الأعضاء",
|
||||||
"label.min": "الحد الأدنى",
|
"label.min": "الحد الأدنى",
|
||||||
"label.mobile": "جوال",
|
"label.mobile": "جوال",
|
||||||
"label.more": "المزيد",
|
"label.more": "المزيد",
|
||||||
"label.my-account": "My account",
|
"label.my-account": "حسابي",
|
||||||
"label.my-websites": "مواقعي",
|
"label.my-websites": "مواقعي",
|
||||||
"label.name": "الاسم",
|
"label.name": "الاسم",
|
||||||
"label.new-password": "كلمة مرور جديدة",
|
"label.new-password": "كلمة مرور جديدة",
|
||||||
"label.none": "غير معرّف",
|
"label.none": "غير معرّف",
|
||||||
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
|
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
|
||||||
"label.ok": "OK",
|
"label.ok": "نعم",
|
||||||
"label.os": "نظام التشغيل",
|
"label.os": "نظام التشغيل",
|
||||||
"label.overview": "نظرة عامة",
|
"label.overview": "نظرة عامة",
|
||||||
"label.owner": "المالك",
|
"label.owner": "المالك",
|
||||||
@ -130,7 +130,7 @@
|
|||||||
"label.region": "المنطقة",
|
"label.region": "المنطقة",
|
||||||
"label.regions": "المناطق",
|
"label.regions": "المناطق",
|
||||||
"label.remove": "أزِل",
|
"label.remove": "أزِل",
|
||||||
"label.remove-member": "Remove member",
|
"label.remove-member": "احذف عضو",
|
||||||
"label.reports": "التقارير",
|
"label.reports": "التقارير",
|
||||||
"label.required": "اجباري",
|
"label.required": "اجباري",
|
||||||
"label.reset": "اعادة تعيين",
|
"label.reset": "اعادة تعيين",
|
||||||
@ -142,9 +142,9 @@
|
|||||||
"label.save": "حفظ",
|
"label.save": "حفظ",
|
||||||
"label.screens": "الشاشات",
|
"label.screens": "الشاشات",
|
||||||
"label.search": "بحث",
|
"label.search": "بحث",
|
||||||
"label.select": "Select",
|
"label.select": "اختر",
|
||||||
"label.select-date": "حدد التاريخ",
|
"label.select-date": "حدد التاريخ",
|
||||||
"label.select-role": "Select role",
|
"label.select-role": "حدد الدور",
|
||||||
"label.select-website": "حدد موقع",
|
"label.select-website": "حدد موقع",
|
||||||
"label.sessions": "الزيارات",
|
"label.sessions": "الزيارات",
|
||||||
"label.settings": "الإعدادات",
|
"label.settings": "الإعدادات",
|
||||||
@ -172,7 +172,7 @@
|
|||||||
"label.total-records": "إجمالي السجلات",
|
"label.total-records": "إجمالي السجلات",
|
||||||
"label.tracking-code": "كود التتبع",
|
"label.tracking-code": "كود التتبع",
|
||||||
"label.transfer": "Transfer",
|
"label.transfer": "Transfer",
|
||||||
"label.transfer-website": "Transfer website",
|
"label.transfer-website": "انقل الموقع",
|
||||||
"label.true": "حقيقي",
|
"label.true": "حقيقي",
|
||||||
"label.type": "النوع",
|
"label.type": "النوع",
|
||||||
"label.unique": "فريد",
|
"label.unique": "فريد",
|
||||||
@ -195,13 +195,13 @@
|
|||||||
"label.websites": "المواقع",
|
"label.websites": "المواقع",
|
||||||
"label.window": "النافذة",
|
"label.window": "النافذة",
|
||||||
"label.yesterday": "الأمس",
|
"label.yesterday": "الأمس",
|
||||||
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
|
"message.action-confirmation": "اكتب {confirmation} في المربع أدناه للتأكيد.",
|
||||||
"message.active-users": "{x} حاليا {x, plural, one {زائر واحد} other {زوار}}",
|
"message.active-users": "{x} حاليا {x, plural, one {زائر واحد} other {زوار}}",
|
||||||
"message.confirm-delete": "هل أنت متأكد من حذف {target}?",
|
"message.confirm-delete": "هل أنت متأكد من حذف {target}?",
|
||||||
"message.confirm-leave": "هل أنت متأكد من مغادرة {target}?",
|
"message.confirm-leave": "هل أنت متأكد من مغادرة {target}?",
|
||||||
"message.confirm-remove": "Are you sure you want to remove {target}?",
|
"message.confirm-remove": "هل انت متأكد من حذف {target}?",
|
||||||
"message.confirm-reset": "هل أنت متأكد من اعادة تعيين الإحصائيات لـ {target}؟",
|
"message.confirm-reset": "هل أنت متأكد من اعادة تعيين الإحصائيات لـ {target}؟",
|
||||||
"message.delete-team-warning": "Deleting a team will also delete all team websites.",
|
"message.delete-team-warning": "حذف فريق سيؤدي إلى حذف جميع مواقع الفريق",
|
||||||
"message.delete-website-warning": "سيتم حذف كافة بيانات الموقع.",
|
"message.delete-website-warning": "سيتم حذف كافة بيانات الموقع.",
|
||||||
"message.error": "حدث خطأ ما.",
|
"message.error": "حدث خطأ ما.",
|
||||||
"message.event-log": "{event} في {url}",
|
"message.event-log": "{event} في {url}",
|
||||||
@ -227,12 +227,12 @@
|
|||||||
"message.team-not-found": "لم يتم العثور على الفريق",
|
"message.team-not-found": "لم يتم العثور على الفريق",
|
||||||
"message.team-websites-info": "يمكن مشاهدة الموقع من اي عضو في الفريق.",
|
"message.team-websites-info": "يمكن مشاهدة الموقع من اي عضو في الفريق.",
|
||||||
"message.tracking-code": "كود التتبع",
|
"message.tracking-code": "كود التتبع",
|
||||||
"message.transfer-team-website-to-user": "Transfer this website to your account?",
|
"message.transfer-team-website-to-user": "نقل هذا الموقع إلى حسابك؟",
|
||||||
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
|
"message.transfer-user-website-to-team": "اختر الفريق الذي تريد نقل الموقع إليه.",
|
||||||
"message.transfer-website": "Transfer website ownership to your account or another team.",
|
"message.transfer-website": "نقل ملكية الموقع لحسابك أو فريق أخر.",
|
||||||
"message.triggered-event": "Triggered event",
|
"message.triggered-event": "أُطلق الحدث",
|
||||||
"message.user-deleted": "تم حذف المستخدم.",
|
"message.user-deleted": "تم حذف المستخدم.",
|
||||||
"message.viewed-page": "Viewed page",
|
"message.viewed-page": "شوهدت الصفحة",
|
||||||
"message.visitor-log": "زائر من {country} يستخدم {browser} على {os} {device}",
|
"message.visitor-log": "زائر من {country} يستخدم {browser} على {os} {device}",
|
||||||
"message.visitors-dropped-off": "Visitors dropped off"
|
"message.visitors-dropped-off": "أنخفض عدد الزوار"
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"label.activity-log": "Registro de actividad",
|
"label.activity-log": "Registro de actividad",
|
||||||
"label.add": "Añadir",
|
"label.add": "Añadir",
|
||||||
"label.add-description": "Añadir descripción",
|
"label.add-description": "Añadir descripción",
|
||||||
"label.add-member": "Add member",
|
"label.add-member": "Añadir miembro",
|
||||||
"label.add-website": "Nuevo sitio web",
|
"label.add-website": "Nuevo sitio web",
|
||||||
"label.administrator": "Administrador",
|
"label.administrator": "Administrador",
|
||||||
"label.after": "Después",
|
"label.after": "Después",
|
||||||
@ -45,7 +45,7 @@
|
|||||||
"label.day": "Día",
|
"label.day": "Día",
|
||||||
"label.default-date-range": "Intervalo por defecto",
|
"label.default-date-range": "Intervalo por defecto",
|
||||||
"label.delete": "Eliminar",
|
"label.delete": "Eliminar",
|
||||||
"label.delete-report": "Delete report",
|
"label.delete-report": "Eliminar reporte",
|
||||||
"label.delete-team": "Eliminar equipo",
|
"label.delete-team": "Eliminar equipo",
|
||||||
"label.delete-user": "Eliminar usuario",
|
"label.delete-user": "Eliminar usuario",
|
||||||
"label.delete-website": "Eliminar sitio",
|
"label.delete-website": "Eliminar sitio",
|
||||||
@ -77,10 +77,10 @@
|
|||||||
"label.greater-than": "Mayor que",
|
"label.greater-than": "Mayor que",
|
||||||
"label.greater-than-equals": "Mayor que o igual a",
|
"label.greater-than-equals": "Mayor que o igual a",
|
||||||
"label.insights": "Insights",
|
"label.insights": "Insights",
|
||||||
"label.insights-description": "Dive deeper into your data by using segments and filters.",
|
"label.insights-description": "Profundice en sus datos mediante el uso de segmentos y filtros.",
|
||||||
"label.is": "Es igual a",
|
"label.is": "Es igual a",
|
||||||
"label.is-not": "No es igual a",
|
"label.is-not": "No es igual a",
|
||||||
"label.is-not-set": "Is not set",
|
"label.is-not-set": "No está establecido",
|
||||||
"label.is-set": "Está establecido",
|
"label.is-set": "Está establecido",
|
||||||
"label.join": "Unir",
|
"label.join": "Unir",
|
||||||
"label.join-team": "Unirse al equipo",
|
"label.join-team": "Unirse al equipo",
|
||||||
@ -95,14 +95,14 @@
|
|||||||
"label.less-than-equals": "Menor que o igual a",
|
"label.less-than-equals": "Menor que o igual a",
|
||||||
"label.login": "Iniciar sesión",
|
"label.login": "Iniciar sesión",
|
||||||
"label.logout": "Cerrar sesión",
|
"label.logout": "Cerrar sesión",
|
||||||
"label.manage": "Manage",
|
"label.manage": "Administrar",
|
||||||
"label.max": "Máx",
|
"label.max": "Máx",
|
||||||
"label.member": "Member",
|
"label.member": "Miembro",
|
||||||
"label.members": "Miembros",
|
"label.members": "Miembros",
|
||||||
"label.min": "Mín",
|
"label.min": "Mín",
|
||||||
"label.mobile": "Móvil",
|
"label.mobile": "Móvil",
|
||||||
"label.more": "Más",
|
"label.more": "Más",
|
||||||
"label.my-account": "My account",
|
"label.my-account": "Mi cuenta",
|
||||||
"label.my-websites": "Mis sitios web",
|
"label.my-websites": "Mis sitios web",
|
||||||
"label.name": "Nombre",
|
"label.name": "Nombre",
|
||||||
"label.new-password": "Nueva contraseña",
|
"label.new-password": "Nueva contraseña",
|
||||||
@ -130,7 +130,7 @@
|
|||||||
"label.region": "Region",
|
"label.region": "Region",
|
||||||
"label.regions": "Regiones",
|
"label.regions": "Regiones",
|
||||||
"label.remove": "Quitar",
|
"label.remove": "Quitar",
|
||||||
"label.remove-member": "Remove member",
|
"label.remove-member": "Eliminar miembro",
|
||||||
"label.reports": "Informes",
|
"label.reports": "Informes",
|
||||||
"label.required": "Obligatorio",
|
"label.required": "Obligatorio",
|
||||||
"label.reset": "Reiniciar",
|
"label.reset": "Reiniciar",
|
||||||
@ -144,7 +144,7 @@
|
|||||||
"label.search": "Buscar",
|
"label.search": "Buscar",
|
||||||
"label.select": "Select",
|
"label.select": "Select",
|
||||||
"label.select-date": "Seleccionar fecha",
|
"label.select-date": "Seleccionar fecha",
|
||||||
"label.select-role": "Select role",
|
"label.select-role": "Seleccionar rol",
|
||||||
"label.select-website": "Seleccionar sitio web",
|
"label.select-website": "Seleccionar sitio web",
|
||||||
"label.sessions": "Sesiones",
|
"label.sessions": "Sesiones",
|
||||||
"label.settings": "Ajustes",
|
"label.settings": "Ajustes",
|
||||||
@ -157,7 +157,7 @@
|
|||||||
"label.team-member": "Miembro del equipo",
|
"label.team-member": "Miembro del equipo",
|
||||||
"label.team-name": "Nombre del equipo",
|
"label.team-name": "Nombre del equipo",
|
||||||
"label.team-owner": "Admin. del equipo",
|
"label.team-owner": "Admin. del equipo",
|
||||||
"label.team-view-only": "Team view only",
|
"label.team-view-only": "Vista solo del equipo",
|
||||||
"label.team-websites": "Sitios web del equipo",
|
"label.team-websites": "Sitios web del equipo",
|
||||||
"label.teams": "Equipos",
|
"label.teams": "Equipos",
|
||||||
"label.theme": "Tema",
|
"label.theme": "Tema",
|
||||||
@ -171,8 +171,8 @@
|
|||||||
"label.total": "Total",
|
"label.total": "Total",
|
||||||
"label.total-records": "Total de registros",
|
"label.total-records": "Total de registros",
|
||||||
"label.tracking-code": "Código de rastreo",
|
"label.tracking-code": "Código de rastreo",
|
||||||
"label.transfer": "Transfer",
|
"label.transfer": "Transferir",
|
||||||
"label.transfer-website": "Transfer website",
|
"label.transfer-website": "Transferir sitio web",
|
||||||
"label.true": "Verdadero",
|
"label.true": "Verdadero",
|
||||||
"label.type": "Tipo",
|
"label.type": "Tipo",
|
||||||
"label.unique": "Único",
|
"label.unique": "Único",
|
||||||
@ -195,13 +195,13 @@
|
|||||||
"label.websites": "Sitios web",
|
"label.websites": "Sitios web",
|
||||||
"label.window": "Ventana",
|
"label.window": "Ventana",
|
||||||
"label.yesterday": "Ayer",
|
"label.yesterday": "Ayer",
|
||||||
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
|
"message.action-confirmation": "Escriba {confirmation} en el cuadro a continuación para confirmar.",
|
||||||
"message.active-users": "{x} {x, plural, one {activo} other {activos}}",
|
"message.active-users": "{x} {x, plural, uno {activo} otros {activos}}",
|
||||||
"message.confirm-delete": "¿Seguro que quieres eliminar {target}?",
|
"message.confirm-delete": "¿Seguro que quieres eliminar {target}?",
|
||||||
"message.confirm-leave": "¿Seguro que quieres abandonar {target}?",
|
"message.confirm-leave": "¿Seguro que quieres abandonar {target}?",
|
||||||
"message.confirm-remove": "Are you sure you want to remove {target}?",
|
"message.confirm-remove": "¿Estás seguro de que desea eliminar {target}?",
|
||||||
"message.confirm-reset": "¿Seguro que quieres BORRAR las analíticas de {target}?",
|
"message.confirm-reset": "¿Seguro que quieres BORRAR las analíticas de {target}?",
|
||||||
"message.delete-team-warning": "Deleting a team will also delete all team websites.",
|
"message.delete-team-warning": "Al eliminar un equipo, también se eliminarán todos los sitios web del equipo.",
|
||||||
"message.delete-website-warning": "Toda la información relacionada será eliminada.",
|
"message.delete-website-warning": "Toda la información relacionada será eliminada.",
|
||||||
"message.error": "Algo falló.",
|
"message.error": "Algo falló.",
|
||||||
"message.event-log": "{event} en {url}",
|
"message.event-log": "{event} en {url}",
|
||||||
@ -227,12 +227,12 @@
|
|||||||
"message.team-not-found": "Equipo no encontrado.",
|
"message.team-not-found": "Equipo no encontrado.",
|
||||||
"message.team-websites-info": "Las analíticas de tus sitios web pueden ser vistas por cualquier miembro del equipo.",
|
"message.team-websites-info": "Las analíticas de tus sitios web pueden ser vistas por cualquier miembro del equipo.",
|
||||||
"message.tracking-code": "Código de rastreo",
|
"message.tracking-code": "Código de rastreo",
|
||||||
"message.transfer-team-website-to-user": "Transfer this website to your account?",
|
"message.transfer-team-website-to-user": "¿Transferir este sitio web a su cuenta?",
|
||||||
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
|
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
|
||||||
"message.transfer-website": "Transfer website ownership to your account or another team.",
|
"message.transfer-website": "Seleccione el equipo al que transferir este sitio web.",
|
||||||
"message.triggered-event": "Triggered event",
|
"message.triggered-event": "Evento lanzado",
|
||||||
"message.user-deleted": "Usuario eliminado.",
|
"message.user-deleted": "Usuario eliminado.",
|
||||||
"message.viewed-page": "Viewed page",
|
"message.viewed-page": "Página vista",
|
||||||
"message.visitor-log": "Visitante desde {country} usando {browser} en {os} {device}",
|
"message.visitor-log": "Visitante desde {country} usando {browser} en {os} {device}",
|
||||||
"message.visitors-dropped-off": "Visitors dropped off"
|
"message.visitors-dropped-off": "Los visitantes salieron"
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"label.activity-log": "アクティビティログ",
|
"label.activity-log": "アクティビティログ",
|
||||||
"label.add": "追加",
|
"label.add": "追加",
|
||||||
"label.add-description": "説明を追加",
|
"label.add-description": "説明を追加",
|
||||||
"label.add-member": "Add member",
|
"label.add-member": "メンバーの追加",
|
||||||
"label.add-website": "Webサイトの追加",
|
"label.add-website": "Webサイトの追加",
|
||||||
"label.administrator": "管理者",
|
"label.administrator": "管理者",
|
||||||
"label.after": "直後",
|
"label.after": "直後",
|
||||||
@ -30,7 +30,7 @@
|
|||||||
"label.continue": "続ける",
|
"label.continue": "続ける",
|
||||||
"label.countries": "国名",
|
"label.countries": "国名",
|
||||||
"label.country": "国",
|
"label.country": "国",
|
||||||
"label.create": "Create",
|
"label.create": "作成",
|
||||||
"label.create-report": "レポートの作成",
|
"label.create-report": "レポートの作成",
|
||||||
"label.create-team": "チームの作成",
|
"label.create-team": "チームの作成",
|
||||||
"label.create-user": "ユーザーの作成",
|
"label.create-user": "ユーザーの作成",
|
||||||
@ -45,7 +45,7 @@
|
|||||||
"label.day": "日",
|
"label.day": "日",
|
||||||
"label.default-date-range": "デフォルトの期間",
|
"label.default-date-range": "デフォルトの期間",
|
||||||
"label.delete": "削除",
|
"label.delete": "削除",
|
||||||
"label.delete-report": "Delete report",
|
"label.delete-report": "レポートの削除",
|
||||||
"label.delete-team": "チームの削除",
|
"label.delete-team": "チームの削除",
|
||||||
"label.delete-user": "ユーザーの削除",
|
"label.delete-user": "ユーザーの削除",
|
||||||
"label.delete-website": "Webサイトの削除",
|
"label.delete-website": "Webサイトの削除",
|
||||||
@ -60,7 +60,7 @@
|
|||||||
"label.dropoff": "切り捨て",
|
"label.dropoff": "切り捨て",
|
||||||
"label.edit": "編集",
|
"label.edit": "編集",
|
||||||
"label.edit-dashboard": "ダッシュボードの編集",
|
"label.edit-dashboard": "ダッシュボードの編集",
|
||||||
"label.edit-member": "Edit member",
|
"label.edit-member": "メンバーの編集",
|
||||||
"label.enable-share-url": "共有URLを有効にする",
|
"label.enable-share-url": "共有URLを有効にする",
|
||||||
"label.event": "イベント",
|
"label.event": "イベント",
|
||||||
"label.event-data": "イベントデータ",
|
"label.event-data": "イベントデータ",
|
||||||
@ -68,16 +68,16 @@
|
|||||||
"label.false": "偽",
|
"label.false": "偽",
|
||||||
"label.field": "フィールド",
|
"label.field": "フィールド",
|
||||||
"label.fields": "フィールド",
|
"label.fields": "フィールド",
|
||||||
"label.filter": "Filter",
|
"label.filter": "フィルター",
|
||||||
"label.filter-combined": "統合",
|
"label.filter-combined": "結合",
|
||||||
"label.filter-raw": "RAW",
|
"label.filter-raw": "RAW",
|
||||||
"label.filters": "フィルター",
|
"label.filters": "フィルター",
|
||||||
"label.funnel": "分析",
|
"label.funnel": "ファネル",
|
||||||
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
|
"label.funnel-description": "ユーザーのコンバージョン率と離脱率を分析します。",
|
||||||
"label.greater-than": "超過",
|
"label.greater-than": "超過",
|
||||||
"label.greater-than-equals": "以上",
|
"label.greater-than-equals": "以上",
|
||||||
"label.insights": "見通し",
|
"label.insights": "インサイト",
|
||||||
"label.insights-description": "Dive deeper into your data by using segments and filters.",
|
"label.insights-description": "セグメントとフィルタを使用して、データをさらに詳しく分析します。",
|
||||||
"label.is": "に等しい",
|
"label.is": "に等しい",
|
||||||
"label.is-not": "に等しくない",
|
"label.is-not": "に等しくない",
|
||||||
"label.is-not-set": "未設定",
|
"label.is-not-set": "未設定",
|
||||||
@ -95,14 +95,14 @@
|
|||||||
"label.less-than-equals": "以下",
|
"label.less-than-equals": "以下",
|
||||||
"label.login": "ログイン",
|
"label.login": "ログイン",
|
||||||
"label.logout": "ログアウト",
|
"label.logout": "ログアウト",
|
||||||
"label.manage": "Manage",
|
"label.manage": "管理",
|
||||||
"label.max": "最大",
|
"label.max": "最大",
|
||||||
"label.member": "Member",
|
"label.member": "メンバー",
|
||||||
"label.members": "メンバー",
|
"label.members": "メンバー",
|
||||||
"label.min": "最小",
|
"label.min": "最小",
|
||||||
"label.mobile": "携帯電話",
|
"label.mobile": "携帯電話",
|
||||||
"label.more": "もっと見る",
|
"label.more": "もっと見る",
|
||||||
"label.my-account": "My account",
|
"label.my-account": "マイアカウント",
|
||||||
"label.my-websites": "マイWebサイト",
|
"label.my-websites": "マイWebサイト",
|
||||||
"label.name": "名前",
|
"label.name": "名前",
|
||||||
"label.new-password": "新しいパスワード",
|
"label.new-password": "新しいパスワード",
|
||||||
@ -130,21 +130,21 @@
|
|||||||
"label.region": "地域",
|
"label.region": "地域",
|
||||||
"label.regions": "地域",
|
"label.regions": "地域",
|
||||||
"label.remove": "削除",
|
"label.remove": "削除",
|
||||||
"label.remove-member": "Remove member",
|
"label.remove-member": "メンバーの削除",
|
||||||
"label.reports": "レポート",
|
"label.reports": "レポート",
|
||||||
"label.required": "必須",
|
"label.required": "必須",
|
||||||
"label.reset": "リセット",
|
"label.reset": "リセット",
|
||||||
"label.reset-website": "Webサイトをリセットする",
|
"label.reset-website": "Webサイトをリセットする",
|
||||||
"label.retention": "保持",
|
"label.retention": "リテンション",
|
||||||
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
|
"label.retention-description": "ユーザーの再訪問回数を記録して、Webサイトのリテンション率を計測します。",
|
||||||
"label.role": "ロール",
|
"label.role": "ロール",
|
||||||
"label.run-query": "クエリ実行",
|
"label.run-query": "クエリ実行",
|
||||||
"label.save": "保存",
|
"label.save": "保存",
|
||||||
"label.screens": "画面サイズ",
|
"label.screens": "画面サイズ",
|
||||||
"label.search": "Search",
|
"label.search": "検索",
|
||||||
"label.select": "Select",
|
"label.select": "選択",
|
||||||
"label.select-date": "日付を選択",
|
"label.select-date": "日付を選択",
|
||||||
"label.select-role": "Select role",
|
"label.select-role": "ロールを選択",
|
||||||
"label.select-website": "Webサイトを選択",
|
"label.select-website": "Webサイトを選択",
|
||||||
"label.sessions": "セッション",
|
"label.sessions": "セッション",
|
||||||
"label.settings": "設定",
|
"label.settings": "設定",
|
||||||
@ -157,7 +157,7 @@
|
|||||||
"label.team-member": "チームメンバー",
|
"label.team-member": "チームメンバー",
|
||||||
"label.team-name": "チーム名",
|
"label.team-name": "チーム名",
|
||||||
"label.team-owner": "チーム所有者",
|
"label.team-owner": "チーム所有者",
|
||||||
"label.team-view-only": "Team view only",
|
"label.team-view-only": "チーム表示のみ",
|
||||||
"label.team-websites": "チームのWebサイト",
|
"label.team-websites": "チームのWebサイト",
|
||||||
"label.teams": "チーム",
|
"label.teams": "チーム",
|
||||||
"label.theme": "テーマ",
|
"label.theme": "テーマ",
|
||||||
@ -171,8 +171,8 @@
|
|||||||
"label.total": "累計",
|
"label.total": "累計",
|
||||||
"label.total-records": "総記録数",
|
"label.total-records": "総記録数",
|
||||||
"label.tracking-code": "トラッキングコード",
|
"label.tracking-code": "トラッキングコード",
|
||||||
"label.transfer": "Transfer",
|
"label.transfer": "移管",
|
||||||
"label.transfer-website": "Transfer website",
|
"label.transfer-website": "Webサイトの移管",
|
||||||
"label.true": "真",
|
"label.true": "真",
|
||||||
"label.type": "種別",
|
"label.type": "種別",
|
||||||
"label.unique": "ユニーク",
|
"label.unique": "ユニーク",
|
||||||
@ -195,13 +195,13 @@
|
|||||||
"label.websites": "Webサイト",
|
"label.websites": "Webサイト",
|
||||||
"label.window": "ウィンドウ",
|
"label.window": "ウィンドウ",
|
||||||
"label.yesterday": "昨日",
|
"label.yesterday": "昨日",
|
||||||
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
|
"message.action-confirmation": "承認する場合は、下のフォームに「{confirmation}」と入力してください。",
|
||||||
"message.active-users": "{x} {x, plural, one {アクティブな訪問者} other {アクティブな訪問者}}",
|
"message.active-users": "{x} {x, plural, one {アクティブな訪問者} other {アクティブな訪問者}}",
|
||||||
"message.confirm-delete": "{target}を削除してもよろしいですか?",
|
"message.confirm-delete": "{target}を削除してもよろしいですか?",
|
||||||
"message.confirm-leave": "{target}から離脱してもよろしいですか?",
|
"message.confirm-leave": "{target}から離脱してもよろしいですか?",
|
||||||
"message.confirm-remove": "Are you sure you want to remove {target}?",
|
"message.confirm-remove": "{target}を削除してもよろしいですか?",
|
||||||
"message.confirm-reset": "{target}をリセットしてもよろしいですか?",
|
"message.confirm-reset": "{target}をリセットしてもよろしいですか?",
|
||||||
"message.delete-team-warning": "Deleting a team will also delete all team websites.",
|
"message.delete-team-warning": "チームを削除すると、そのチームが管理しているWebサイトもすべて削除されます。",
|
||||||
"message.delete-website-warning": "Webサイトのデータがすべて削除されます。",
|
"message.delete-website-warning": "Webサイトのデータがすべて削除されます。",
|
||||||
"message.error": "未知のエラーが発生しました。",
|
"message.error": "未知のエラーが発生しました。",
|
||||||
"message.event-log": "{url}の{event}",
|
"message.event-log": "{url}の{event}",
|
||||||
@ -227,12 +227,12 @@
|
|||||||
"message.team-not-found": "チームが見つかりません。",
|
"message.team-not-found": "チームが見つかりません。",
|
||||||
"message.team-websites-info": "Webサイトはチーム内の誰でも見ることができます。",
|
"message.team-websites-info": "Webサイトはチーム内の誰でも見ることができます。",
|
||||||
"message.tracking-code": "このWebサイトの統計情報を追跡するには、HTMLの<head>...</head>セクションに以下のコードを記述します。",
|
"message.tracking-code": "このWebサイトの統計情報を追跡するには、HTMLの<head>...</head>セクションに以下のコードを記述します。",
|
||||||
"message.transfer-team-website-to-user": "Transfer this website to your account?",
|
"message.transfer-team-website-to-user": "このWebサイトをあなたのアカウントに移管しますか?",
|
||||||
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
|
"message.transfer-user-website-to-team": "このWebサイトを移管するチームを選択してください。",
|
||||||
"message.transfer-website": "Transfer website ownership to your account or another team.",
|
"message.transfer-website": "Webサイトの所有権を自分のアカウントまたは別のチームへ移管します。",
|
||||||
"message.triggered-event": "Triggered event",
|
"message.triggered-event": "トリガーされたイベント",
|
||||||
"message.user-deleted": "ユーザーが削除されました。",
|
"message.user-deleted": "ユーザーが削除されました。",
|
||||||
"message.viewed-page": "Viewed page",
|
"message.viewed-page": "閲覧されたページ",
|
||||||
"message.visitor-log": "{os}({device})で{browser}を使用している{country}からの訪問者",
|
"message.visitor-log": "{os}({device})で{browser}を使用している{country}からの訪問者",
|
||||||
"message.visitors-dropped-off": "Visitors dropped off"
|
"message.visitors-dropped-off": "訪問者の離脱率"
|
||||||
}
|
}
|
||||||
|
@ -1,238 +1,237 @@
|
|||||||
{
|
{
|
||||||
"label.access-code": "Access code",
|
"label.access-code": "Cod de access",
|
||||||
"label.actions": "Acțiuni",
|
"label.actions": "Acțiuni",
|
||||||
"label.activity-log": "Activity log",
|
"label.activity-log": "Jurnal de activități",
|
||||||
"label.add": "Add",
|
"label.add": "Adaugă",
|
||||||
"label.add-description": "Add description",
|
"label.add-description": "Adaugă descriere",
|
||||||
"label.add-member": "Add member",
|
"label.add-member": "Adaugă membru",
|
||||||
"label.add-website": "Adăugare site web",
|
"label.add-website": "Adăugare site web",
|
||||||
"label.administrator": "Administrator",
|
"label.administrator": "Administrator",
|
||||||
"label.after": "After",
|
"label.after": "După",
|
||||||
"label.all": "Toate",
|
"label.all": "Toate",
|
||||||
"label.all-time": "All time",
|
"label.all-time": "Pentru tot timpul",
|
||||||
"label.analytics": "Analytics",
|
"label.analytics": "Analytics",
|
||||||
"label.average": "Average",
|
"label.average": "Mediu",
|
||||||
"label.average-visit-time": "Timp mediu de vizitare",
|
"label.average-visit-time": "Timp mediu de vizitare",
|
||||||
"label.back": "Înapoi",
|
"label.back": "Înapoi",
|
||||||
"label.before": "Before",
|
"label.before": "Înainte",
|
||||||
"label.bounce-rate": "Rata de respingere",
|
"label.bounce-rate": "Rata de respingere",
|
||||||
"label.breakdown": "Breakdown",
|
"label.breakdown": "Detaliat",
|
||||||
"label.browser": "Browser",
|
"label.browser": "Browser",
|
||||||
"label.browsers": "Browsere",
|
"label.browsers": "Browsere",
|
||||||
"label.cancel": "Anulează",
|
"label.cancel": "Anulează",
|
||||||
"label.change-password": "Schimbare parolă",
|
"label.change-password": "Schimbare parolă",
|
||||||
"label.cities": "Cities",
|
"label.cities": "Orașe",
|
||||||
"label.city": "City",
|
"label.city": "Oraș",
|
||||||
"label.clear-all": "Clear all",
|
"label.clear-all": "Șterge tot",
|
||||||
"label.confirm": "Confirm",
|
"label.confirm": "Confirm",
|
||||||
"label.confirm-password": "Confirmare parolă",
|
"label.confirm-password": "Confirmare parolă",
|
||||||
"label.contains": "Contains",
|
"label.contains": "Conține",
|
||||||
"label.continue": "Continue",
|
"label.continue": "Continuă",
|
||||||
"label.countries": "Țări",
|
"label.countries": "Țări",
|
||||||
"label.country": "Country",
|
"label.country": "Țară",
|
||||||
"label.create": "Create",
|
"label.create": "Crează",
|
||||||
"label.create-report": "Create report",
|
"label.create-report": "Crează report",
|
||||||
"label.create-team": "Create team",
|
"label.create-team": "Crează echipă",
|
||||||
"label.create-user": "Create user",
|
"label.create-user": "Crează utilizator",
|
||||||
"label.created": "Created",
|
"label.created": "Creat",
|
||||||
"label.created-by": "Created By",
|
|
||||||
"label.current-password": "Parola curentă",
|
"label.current-password": "Parola curentă",
|
||||||
"label.custom-range": "Interval personalizat",
|
"label.custom-range": "Interval personalizat",
|
||||||
"label.dashboard": "Tablou de bord",
|
"label.dashboard": "Tablou de bord",
|
||||||
"label.data": "Data",
|
"label.data": "Date",
|
||||||
"label.date": "Date",
|
"label.date": "Data",
|
||||||
"label.date-range": "Interval de date",
|
"label.date-range": "Interval de date",
|
||||||
"label.day": "Day",
|
"label.day": "Zi",
|
||||||
"label.default-date-range": "Interval de date implicit",
|
"label.default-date-range": "Interval de date implicit",
|
||||||
"label.delete": "Șterge",
|
"label.delete": "Șterge",
|
||||||
"label.delete-report": "Delete report",
|
"label.delete-report": "Șterge raport",
|
||||||
"label.delete-team": "Delete team",
|
"label.delete-team": "Șterge echipă",
|
||||||
"label.delete-user": "Delete user",
|
"label.delete-user": "Șterge utilizator",
|
||||||
"label.delete-website": "Ștergere site web",
|
"label.delete-website": "Ștergere site web",
|
||||||
"label.description": "Description",
|
"label.description": "Descriere",
|
||||||
"label.desktop": "Desktop",
|
"label.desktop": "Desktop",
|
||||||
"label.details": "Details",
|
"label.details": "Detalii",
|
||||||
"label.device": "Device",
|
"label.device": "Dispozitiv",
|
||||||
"label.devices": "Dispozitive",
|
"label.devices": "Dispozitive",
|
||||||
"label.dismiss": "Renunță",
|
"label.dismiss": "Renunță",
|
||||||
"label.does-not-contain": "Does not contain",
|
"label.does-not-contain": "Nu conține",
|
||||||
"label.domain": "Domeniu",
|
"label.domain": "Domeniu",
|
||||||
"label.dropoff": "Dropoff",
|
"label.dropoff": "Rată de abandon",
|
||||||
"label.edit": "Editare",
|
"label.edit": "Editare",
|
||||||
"label.edit-dashboard": "Edit dashboard",
|
"label.edit-dashboard": "Editare tablou de bord",
|
||||||
"label.edit-member": "Edit member",
|
"label.edit-member": "Editare membru",
|
||||||
"label.enable-share-url": "Activare adresă URL de distribuire",
|
"label.enable-share-url": "Activare adresă URL de distribuire",
|
||||||
"label.event": "Event",
|
"label.event": "Eveniment",
|
||||||
"label.event-data": "Event data",
|
"label.event-data": "Date despre eveniment",
|
||||||
"label.events": "Evenimente",
|
"label.events": "Evenimente",
|
||||||
"label.false": "False",
|
"label.false": "Fals",
|
||||||
"label.field": "Field",
|
"label.field": "Câmp",
|
||||||
"label.fields": "Fields",
|
"label.fields": "Câmpuri",
|
||||||
"label.filter": "Filter",
|
"label.filter": "Filtru",
|
||||||
"label.filter-combined": "Combinat",
|
"label.filter-combined": "Combinat",
|
||||||
"label.filter-raw": "Brut",
|
"label.filter-raw": "Brut",
|
||||||
"label.filters": "Filters",
|
"label.filters": "Filtre",
|
||||||
"label.funnel": "Funnel",
|
"label.funnel": "Parcursul utilizatorului",
|
||||||
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
|
"label.funnel-description": "Înțelege rata de conversie și rata de abandon a utilizatorilor.",
|
||||||
"label.greater-than": "Greater than",
|
"label.greater-than": "Mai mare decât",
|
||||||
"label.greater-than-equals": "Greater than or equals",
|
"label.greater-than-equals": "Mai mare sau egal cu",
|
||||||
"label.insights": "Insights",
|
"label.insights": "Perspective",
|
||||||
"label.insights-description": "Dive deeper into your data by using segments and filters.",
|
"label.insights-description": "Aprofundează datele utilizând segmente și filtre.",
|
||||||
"label.is": "Is",
|
"label.is": "Este",
|
||||||
"label.is-not": "Is not",
|
"label.is-not": "Nu este",
|
||||||
"label.is-not-set": "Is not set",
|
"label.is-not-set": "Nu este setat",
|
||||||
"label.is-set": "Is set",
|
"label.is-set": "Este setat",
|
||||||
"label.join": "Join",
|
"label.join": "Alătură-te",
|
||||||
"label.join-team": "Join team",
|
"label.join-team": "Alătură-te echipei",
|
||||||
"label.language": "Language",
|
"label.language": "Limbă",
|
||||||
"label.languages": "Languages",
|
"label.languages": "Limbi",
|
||||||
"label.laptop": "Laptop",
|
"label.laptop": "Laptop",
|
||||||
"label.last-days": "Ultimele {x} zile",
|
"label.last-days": "Ultimele {x} zile",
|
||||||
"label.last-hours": "Ultimele {x} ore",
|
"label.last-hours": "Ultimele {x} ore",
|
||||||
"label.leave": "Leave",
|
"label.leave": "Părăsește",
|
||||||
"label.leave-team": "Leave team",
|
"label.leave-team": "Părăsește echipa",
|
||||||
"label.less-than": "Less than",
|
"label.less-than": "Mai puțin decât",
|
||||||
"label.less-than-equals": "Less than or equals",
|
"label.less-than-equals": "Mai puțin sau egal cu",
|
||||||
"label.login": "Autentificare",
|
"label.login": "Autentificare",
|
||||||
"label.logout": "Iesire din cont",
|
"label.logout": "Ieșire din cont",
|
||||||
"label.manage": "Manage",
|
"label.manage": "Administrează",
|
||||||
"label.max": "Max",
|
"label.max": "Max",
|
||||||
"label.member": "Member",
|
"label.member": "Membru",
|
||||||
"label.members": "Members",
|
"label.members": "Membri",
|
||||||
"label.min": "Min",
|
"label.min": "Min",
|
||||||
"label.mobile": "Mobil",
|
"label.mobile": "Mobil",
|
||||||
"label.more": "Mai mult",
|
"label.more": "Mai mult",
|
||||||
"label.my-account": "My account",
|
"label.my-account": "Contul meu",
|
||||||
"label.my-websites": "My websites",
|
"label.my-websites": "Website-ul meu",
|
||||||
"label.name": "Nume",
|
"label.name": "Nume",
|
||||||
"label.new-password": "Parola nouă",
|
"label.new-password": "Parolă nouă",
|
||||||
"label.none": "None",
|
"label.none": "Niciunul",
|
||||||
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
|
"label.number-of-records": "{x} {x, plural, one {înregistrare} other {înregistrări}}",
|
||||||
"label.ok": "OK",
|
"label.ok": "OK",
|
||||||
"label.os": "OS",
|
"label.os": "OS",
|
||||||
"label.overview": "Overview",
|
"label.overview": "Vedere de ansamblu",
|
||||||
"label.owner": "Owner",
|
"label.owner": "Titular",
|
||||||
"label.page-of": "Page {current} of {total}",
|
"label.page-of": "Pagina {current} din {total}",
|
||||||
"label.page-views": "Vizualizări de pagină",
|
"label.page-views": "Vizualizări de pagină",
|
||||||
"label.pageTitle": "Page title",
|
"label.pageTitle": "Titlul paginii",
|
||||||
"label.pages": "Pagini",
|
"label.pages": "Pagini",
|
||||||
"label.password": "Parolă",
|
"label.password": "Parolă",
|
||||||
"label.powered-by": "Cu sprijinul {name}",
|
"label.powered-by": "Cu sprijinul {name}",
|
||||||
"label.profile": "Profil",
|
"label.profile": "Profil",
|
||||||
"label.queries": "Queries",
|
"label.queries": "Interogări",
|
||||||
"label.query": "Query",
|
"label.query": "Interogare",
|
||||||
"label.query-parameters": "Query parameters",
|
"label.query-parameters": "Parametri de interogare",
|
||||||
"label.realtime": "Realtime",
|
"label.realtime": "Timp real",
|
||||||
"label.referrer": "Referrer",
|
"label.referrer": "Proveniență",
|
||||||
"label.referrers": "Site-uri de proveniență",
|
"label.referrers": "Site-uri de proveniență",
|
||||||
"label.refresh": "Reîmprospătare",
|
"label.refresh": "Reîmprospătare",
|
||||||
"label.regenerate": "Regenerate",
|
"label.regenerate": "Regenerează",
|
||||||
"label.region": "Region",
|
"label.region": "Regiune",
|
||||||
"label.regions": "Regions",
|
"label.regions": "Regiuni",
|
||||||
"label.remove": "Remove",
|
"label.remove": "Îndepărtează",
|
||||||
"label.remove-member": "Remove member",
|
"label.remove-member": "Îndepărtează membru",
|
||||||
"label.reports": "Reports",
|
"label.reports": "Rapoarte",
|
||||||
"label.required": "Obligatoriu",
|
"label.required": "Obligatoriu",
|
||||||
"label.reset": "Resetează",
|
"label.reset": "Resetează",
|
||||||
"label.reset-website": "Resetează statisticile pentru site",
|
"label.reset-website": "Resetează statisticile pentru site",
|
||||||
"label.retention": "Retention",
|
"label.retention": "Retenție",
|
||||||
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
|
"label.retention-description": "Măsoară atractivitatea site-ului tău prin urmărirea frecvenței cu care utilizatorii se întorc.",
|
||||||
"label.role": "Role",
|
"label.role": "Rol",
|
||||||
"label.run-query": "Run query",
|
"label.run-query": "Execută interogarea",
|
||||||
"label.save": "Salvează",
|
"label.save": "Salvează",
|
||||||
"label.screens": "Screens",
|
"label.screens": "Ecrane",
|
||||||
"label.search": "Search",
|
"label.search": "Căutare",
|
||||||
"label.select": "Select",
|
"label.select": "Selectează",
|
||||||
"label.select-date": "Select date",
|
"label.select-date": "Selectează data",
|
||||||
"label.select-role": "Select role",
|
"label.select-role": "Selectează rolul",
|
||||||
"label.select-website": "Select website",
|
"label.select-website": "Selectează website",
|
||||||
"label.sessions": "Sessions",
|
"label.sessions": "Sesiuni",
|
||||||
"label.settings": "Setări",
|
"label.settings": "Setări",
|
||||||
"label.share-url": "Partajare URL",
|
"label.share-url": "Partajare URL",
|
||||||
"label.single-day": "O singură zi",
|
"label.single-day": "O singură zi",
|
||||||
"label.sum": "Sum",
|
"label.sum": "Sumă",
|
||||||
"label.tablet": "Tabletă",
|
"label.tablet": "Tabletă",
|
||||||
"label.team": "Team",
|
"label.team": "Echipă",
|
||||||
"label.team-id": "Team ID",
|
"label.team-id": "ID Echipa",
|
||||||
"label.team-member": "Team member",
|
"label.team-member": "Membru echipă",
|
||||||
"label.team-name": "Team name",
|
"label.team-name": "Nume echipă",
|
||||||
"label.team-owner": "Team owner",
|
"label.team-owner": "Titular echipă",
|
||||||
"label.team-view-only": "Team view only",
|
"label.team-view-only": "Doar vizualizare echipă",
|
||||||
"label.team-websites": "Team websites",
|
"label.team-websites": "Website-uri echipă",
|
||||||
"label.teams": "Teams",
|
"label.teams": "Echipă",
|
||||||
"label.theme": "Theme",
|
"label.theme": "Temă",
|
||||||
"label.this-month": "Această lună",
|
"label.this-month": "Această lună",
|
||||||
"label.this-week": "Această săptămână",
|
"label.this-week": "Această săptămână",
|
||||||
"label.this-year": "Acest an",
|
"label.this-year": "Acest an",
|
||||||
"label.timezone": "Fus orar",
|
"label.timezone": "Fus orar",
|
||||||
"label.title": "Title",
|
"label.title": "Titlu",
|
||||||
"label.today": "Astăzi",
|
"label.today": "Astăzi",
|
||||||
"label.toggle-charts": "Schimbă graficele",
|
"label.toggle-charts": "Schimbă graficele",
|
||||||
"label.total": "Total",
|
"label.total": "Total",
|
||||||
"label.total-records": "Total records",
|
"label.total-records": "Total înregistrări",
|
||||||
"label.tracking-code": "Cod de urmărire",
|
"label.tracking-code": "Cod de urmărire",
|
||||||
"label.transfer": "Transfer",
|
"label.transfer": "Transfer",
|
||||||
"label.transfer-website": "Transfer website",
|
"label.transfer-website": "Transfer website",
|
||||||
"label.true": "True",
|
"label.true": "Adevărat",
|
||||||
"label.type": "Type",
|
"label.type": "Tip",
|
||||||
"label.unique": "Unique",
|
"label.unique": "Unici",
|
||||||
"label.unique-visitors": "Vizitatori unici",
|
"label.unique-visitors": "Vizitatori unici",
|
||||||
"label.unknown": "Necunoscut",
|
"label.unknown": "Necunoscut",
|
||||||
"label.untitled": "Untitled",
|
"label.untitled": "Fără titlu",
|
||||||
"label.url": "URL",
|
"label.url": "URL",
|
||||||
"label.urls": "URLs",
|
"label.urls": "URLs",
|
||||||
"label.user": "User",
|
"label.user": "Utilizator",
|
||||||
"label.username": "Nume utilizator",
|
"label.username": "Nume utilizator",
|
||||||
"label.users": "Users",
|
"label.users": "Utilizatori",
|
||||||
"label.value": "Value",
|
"label.value": "Valoare",
|
||||||
"label.view": "View",
|
"label.view": "Vizualizare",
|
||||||
"label.view-details": "Vizualizare detalii",
|
"label.view-details": "Vizualizare detalii",
|
||||||
"label.view-only": "View only",
|
"label.view-only": "Doar vizualizare",
|
||||||
"label.views": "Vizualizări",
|
"label.views": "Vizualizări",
|
||||||
"label.visitors": "Vizitatori",
|
"label.visitors": "Vizitatori",
|
||||||
"label.website": "Website",
|
"label.website": "Website",
|
||||||
"label.website-id": "Website ID",
|
"label.website-id": "ID Website",
|
||||||
"label.websites": "Site-uri web",
|
"label.websites": "Site-uri web",
|
||||||
"label.window": "Window",
|
"label.window": "Fereastră",
|
||||||
"label.yesterday": "Yesterday",
|
"label.yesterday": "Ieri",
|
||||||
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
|
"message.action-confirmation": "Scrie {confirmation} în câmpul de mai jos pentru a confirma.",
|
||||||
"message.active-users": "{x} {x, plural, one {vizitator activ} other {vizitatori activi}}",
|
"message.active-users": "{x} {x, plural, one {vizitator activ} other {vizitatori activi}}",
|
||||||
"message.confirm-delete": "Sunteți sigur că doriți să ștergeți {target}?",
|
"message.confirm-delete": "Ești sigur că vrei să ștergi {target}?",
|
||||||
"message.confirm-leave": "Are you sure you want to leave {target}?",
|
"message.confirm-leave": "Ești sigur că vrei să părăsești {target}?",
|
||||||
"message.confirm-remove": "Are you sure you want to remove {target}?",
|
"message.confirm-remove": "Ești sigur că vrei să ștergi {target}?",
|
||||||
"message.confirm-reset": "Sunteți sigur că doriți să resetați statisticile pentru {target}?",
|
"message.confirm-reset": "Ești sigur că vrei să resetezi statisticile pentru {target}?",
|
||||||
"message.delete-team-warning": "Deleting a team will also delete all team websites.",
|
"message.delete-team-warning": "Ștergerea unei echipe va șterge și toate website-urile echipei.",
|
||||||
"message.delete-website-warning": "Toate datele asociate vor fi șterse, de asemenea.",
|
"message.delete-website-warning": "Toate datele asociate vor fi șterse, de asemenea.",
|
||||||
"message.error": "Ceva n-a mers bine.",
|
"message.error": "Ceva n-a mers bine.",
|
||||||
"message.event-log": "{event} on {url}",
|
"message.event-log": "{event} la {url}",
|
||||||
"message.go-to-settings": "Mergi la Setări",
|
"message.go-to-settings": "Mergi la Setări",
|
||||||
"message.incorrect-username-password": "Nume utilizator / parolă incorecte.",
|
"message.incorrect-username-password": "Nume utilizator / parolă incorecte.",
|
||||||
"message.invalid-domain": "Domeniu nu este valid",
|
"message.invalid-domain": "Domeniul nu este valid",
|
||||||
"message.min-password-length": "Minimum length of {n} characters",
|
"message.min-password-length": "Lungimea minimă este de {n} caractere",
|
||||||
"message.new-version-available": "A new version of Umami {version} is available!",
|
"message.new-version-available": "O nouă versiune de Umami {version} este disponibilă!",
|
||||||
"message.no-data-available": "Nici o informație disponibilă.",
|
"message.no-data-available": "Nicio informație disponibilă.",
|
||||||
"message.no-event-data": "No event data is available.",
|
"message.no-event-data": "Nu sunt disponibile date legate de eveniment.",
|
||||||
"message.no-match-password": "Parolele nu se potrivesc",
|
"message.no-match-password": "Parolele nu se potrivesc",
|
||||||
"message.no-results-found": "No results were found.",
|
"message.no-results-found": "Nu a fost găsit niciun rezultat.",
|
||||||
"message.no-team-websites": "This team does not have any websites.",
|
"message.no-team-websites": "Echipa aceasta nu are niciun website.",
|
||||||
"message.no-teams": "You have not created any teams.",
|
"message.no-teams": "Nu ai creat nicio echipă.",
|
||||||
"message.no-users": "There are no users.",
|
"message.no-users": "Nu există utilizatori.",
|
||||||
"message.no-websites-configured": "Nu aveți niciun site web configurat.",
|
"message.no-websites-configured": "Nu ai niciun site web configurat.",
|
||||||
"message.page-not-found": "Pagina nu a fost găsită.",
|
"message.page-not-found": "Pagina nu a fost găsită.",
|
||||||
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
|
"message.reset-website": "Pentru a reseta acest website, scrie {confirmation} În câmpul de mai jos pentru a confirma.",
|
||||||
"message.reset-website-warning": "Toate statisticile pentru acest site web vor fi șterse, dar codul de urmărire va rămâne intact.",
|
"message.reset-website-warning": "Toate statisticile pentru acest site web vor fi șterse, dar codul de urmărire va rămâne intact.",
|
||||||
"message.saved": "Salvat cu succes.",
|
"message.saved": "Salvat cu succes.",
|
||||||
"message.share-url": "Aceasta este adresa URL de partajare pentru {target}.",
|
"message.share-url": "Aceasta este adresa URL de partajare pentru {target}.",
|
||||||
"message.team-already-member": "You are already a member of the team.",
|
"message.team-already-member": "Deja ești membru al acestei echipe.",
|
||||||
"message.team-not-found": "Team not found.",
|
"message.team-not-found": "Echipa nu a fost găsită.",
|
||||||
"message.team-websites-info": "Websites can be viewed by anyone on the team.",
|
"message.team-websites-info": "Site-urile web pot fi vizualizate de către oricare membru al echipei.",
|
||||||
"message.tracking-code": "Cod de urmărire",
|
"message.tracking-code": "Cod de urmărire",
|
||||||
"message.transfer-team-website-to-user": "Transfer this website to your account?",
|
"message.transfer-team-website-to-user": "Vrei să transferi acest website pe contul tău?",
|
||||||
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
|
"message.transfer-user-website-to-team": "Selectează echipa căreia vrei să îi transferi site-ul.",
|
||||||
"message.transfer-website": "Transfer website ownership to your account or another team.",
|
"message.transfer-website": "Transferă titulatura site-ului către tine sau către o altă echipă.",
|
||||||
"message.triggered-event": "Triggered event",
|
"message.triggered-event": "Eveniment declanșat",
|
||||||
"message.user-deleted": "User deleted.",
|
"message.user-deleted": "Utilizator șters.",
|
||||||
"message.viewed-page": "Viewed page",
|
"message.viewed-page": "Pagină vizualizată",
|
||||||
"message.visitor-log": "Vizitator din {country} folosind {browser} pe {os} {device}",
|
"message.visitor-log": "Vizitator din {country} folosind {browser} pe {os} {device}",
|
||||||
"message.visitors-dropped-off": "Visitors dropped off"
|
"message.visitors-dropped-off": "Vizitatori care au abandonat"
|
||||||
}
|
}
|
||||||
|
21
src/lib/__tests__/detect.test.ts
Normal file
21
src/lib/__tests__/detect.test.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import * as detect from '../detect';
|
||||||
|
|
||||||
|
const IP = '127.0.0.1';
|
||||||
|
|
||||||
|
test('getIpAddress: Custom header', () => {
|
||||||
|
process.env.CLIENT_IP_HEADER = 'x-custom-ip-header';
|
||||||
|
|
||||||
|
expect(detect.getIpAddress({ headers: { 'x-custom-ip-header': IP } } as any)).toEqual(IP);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getIpAddress: CloudFlare header', () => {
|
||||||
|
expect(detect.getIpAddress({ headers: { 'cf-connecting-ip': IP } } as any)).toEqual(IP);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getIpAddress: Standard header', () => {
|
||||||
|
expect(detect.getIpAddress({ headers: { 'x-forwarded-for': IP } } as any)).toEqual(IP);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getIpAddress: No header', () => {
|
||||||
|
expect(detect.getIpAddress({ headers: {} } as any)).toEqual(null);
|
||||||
|
});
|
38
src/lib/__tests__/format.test.ts
Normal file
38
src/lib/__tests__/format.test.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import * as format from '../format';
|
||||||
|
|
||||||
|
test('parseTime', () => {
|
||||||
|
expect(format.parseTime(86400 + 3600 + 60 + 1)).toEqual({
|
||||||
|
days: 1,
|
||||||
|
hours: 1,
|
||||||
|
minutes: 1,
|
||||||
|
seconds: 1,
|
||||||
|
ms: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formatTime', () => {
|
||||||
|
expect(format.formatTime(3600 + 60 + 1)).toBe('1:01:01');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formatShortTime', () => {
|
||||||
|
expect(format.formatShortTime(3600 + 60 + 1)).toBe('1m1s');
|
||||||
|
|
||||||
|
expect(format.formatShortTime(3600 + 60 + 1, ['h', 'm', 's'])).toBe('1h1m1s');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formatNumber', () => {
|
||||||
|
expect(format.formatNumber('10.2')).toBe('10');
|
||||||
|
expect(format.formatNumber('10.5')).toBe('11');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formatLongNumber', () => {
|
||||||
|
expect(format.formatLongNumber(1200000)).toBe('1.2m');
|
||||||
|
expect(format.formatLongNumber(575000)).toBe('575k');
|
||||||
|
expect(format.formatLongNumber(10500)).toBe('10.5k');
|
||||||
|
expect(format.formatLongNumber(1200)).toBe('1.20k');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stringToColor', () => {
|
||||||
|
expect(format.stringToColor('hello')).toBe('#d218e9');
|
||||||
|
expect(format.stringToColor('goodbye')).toBe('#11e956');
|
||||||
|
});
|
@ -61,12 +61,14 @@ function getDateFormat(date: Date) {
|
|||||||
return `'${dateFormat(date, 'UTC:yyyy-mm-dd HH:MM:ss')}'`;
|
return `'${dateFormat(date, 'UTC:yyyy-mm-dd HH:MM:ss')}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapFilter(column: string, operator: string, name: string, type = 'String') {
|
function mapFilter(column: string, filter: string, name: string, type: string = 'String') {
|
||||||
switch (operator) {
|
switch (filter) {
|
||||||
case OPERATORS.equals:
|
case OPERATORS.equals:
|
||||||
return `${column} = {${name}:${type}}`;
|
return `${column} = {${name}:${type}}`;
|
||||||
case OPERATORS.notEquals:
|
case OPERATORS.notEquals:
|
||||||
return `${column} != {${name}:${type}}`;
|
return `${column} != {${name}:${type}}`;
|
||||||
|
case OPERATORS.contains:
|
||||||
|
return `positionCaseInsensitive(${column}, {${name}:${type}}) > 0`;
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@ -75,11 +77,11 @@ function mapFilter(column: string, operator: string, name: string, type = 'Strin
|
|||||||
function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}) {
|
function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}) {
|
||||||
const query = Object.keys(filters).reduce((arr, name) => {
|
const query = Object.keys(filters).reduce((arr, name) => {
|
||||||
const value = filters[name];
|
const value = filters[name];
|
||||||
const operator = value?.filter ?? OPERATORS.equals;
|
const filter = value?.filter ?? OPERATORS.equals;
|
||||||
const column = FILTER_COLUMNS[name] ?? options?.columns?.[name];
|
const column = value?.column ?? FILTER_COLUMNS[name] ?? options?.columns?.[name];
|
||||||
|
|
||||||
if (value !== undefined && column) {
|
if (value !== undefined && column !== undefined) {
|
||||||
arr.push(`and ${mapFilter(column, operator, name)}`);
|
arr.push(`and ${mapFilter(column, filter, name)}`);
|
||||||
|
|
||||||
if (name === 'referrer') {
|
if (name === 'referrer') {
|
||||||
arr.push('and referrer_domain != {websiteDomain:String}');
|
arr.push('and referrer_domain != {websiteDomain:String}');
|
||||||
|
@ -16,7 +16,7 @@ import { NextApiRequestCollect } from 'pages/api/send';
|
|||||||
|
|
||||||
let lookup;
|
let lookup;
|
||||||
|
|
||||||
export function getIpAddress(req) {
|
export function getIpAddress(req: NextApiRequestCollect) {
|
||||||
// Custom header
|
// Custom header
|
||||||
if (req.headers[process.env.CLIENT_IP_HEADER]) {
|
if (req.headers[process.env.CLIENT_IP_HEADER]) {
|
||||||
return req.headers[process.env.CLIENT_IP_HEADER];
|
return req.headers[process.env.CLIENT_IP_HEADER];
|
||||||
@ -29,35 +29,35 @@ export function getIpAddress(req) {
|
|||||||
return getClientIp(req);
|
return getClientIp(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDevice(screen, os) {
|
export function getDevice(screen: string, os: string) {
|
||||||
if (!screen) return;
|
if (!screen) return;
|
||||||
|
|
||||||
const [width] = screen.split('x');
|
const [width] = screen.split('x');
|
||||||
|
|
||||||
if (DESKTOP_OS.includes(os)) {
|
if (DESKTOP_OS.includes(os)) {
|
||||||
if (os === 'Chrome OS' || width < DESKTOP_SCREEN_WIDTH) {
|
if (os === 'Chrome OS' || +width < DESKTOP_SCREEN_WIDTH) {
|
||||||
return 'laptop';
|
return 'laptop';
|
||||||
}
|
}
|
||||||
return 'desktop';
|
return 'desktop';
|
||||||
} else if (MOBILE_OS.includes(os)) {
|
} else if (MOBILE_OS.includes(os)) {
|
||||||
if (os === 'Amazon OS' || width > MOBILE_SCREEN_WIDTH) {
|
if (os === 'Amazon OS' || +width > MOBILE_SCREEN_WIDTH) {
|
||||||
return 'tablet';
|
return 'tablet';
|
||||||
}
|
}
|
||||||
return 'mobile';
|
return 'mobile';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (width >= DESKTOP_SCREEN_WIDTH) {
|
if (+width >= DESKTOP_SCREEN_WIDTH) {
|
||||||
return 'desktop';
|
return 'desktop';
|
||||||
} else if (width >= LAPTOP_SCREEN_WIDTH) {
|
} else if (+width >= LAPTOP_SCREEN_WIDTH) {
|
||||||
return 'laptop';
|
return 'laptop';
|
||||||
} else if (width >= MOBILE_SCREEN_WIDTH) {
|
} else if (+width >= MOBILE_SCREEN_WIDTH) {
|
||||||
return 'tablet';
|
return 'tablet';
|
||||||
} else {
|
} else {
|
||||||
return 'mobile';
|
return 'mobile';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRegionCode(country, region) {
|
function getRegionCode(country: string, region: string) {
|
||||||
if (!country || !region) {
|
if (!country || !region) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -65,7 +65,7 @@ function getRegionCode(country, region) {
|
|||||||
return region.includes('-') ? region : `${country}-${region}`;
|
return region.includes('-') ? region : `${country}-${region}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLocation(ip, req) {
|
export async function getLocation(ip: string, req: NextApiRequestCollect) {
|
||||||
// Ignore local ips
|
// Ignore local ips
|
||||||
if (await isLocalhost(ip)) {
|
if (await isLocalhost(ip)) {
|
||||||
return;
|
return;
|
||||||
|
@ -92,12 +92,14 @@ function getTimestampDiffQuery(field1: string, field2: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapFilter(column: string, operator: string, name: string, type = 'varchar') {
|
function mapFilter(column: string, filter: string, name: string, type = 'varchar') {
|
||||||
switch (operator) {
|
switch (filter) {
|
||||||
case OPERATORS.equals:
|
case OPERATORS.equals:
|
||||||
return `${column} = {{${name}::${type}}}`;
|
return `${column} = {{${name}::${type}}}`;
|
||||||
case OPERATORS.notEquals:
|
case OPERATORS.notEquals:
|
||||||
return `${column} != {{${name}::${type}}}`;
|
return `${column} != {{${name}::${type}}}`;
|
||||||
|
case OPERATORS.contains:
|
||||||
|
return `${column} like {{${name}::${type}}}`;
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@ -106,11 +108,11 @@ function mapFilter(column: string, operator: string, name: string, type = 'varch
|
|||||||
function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}): string {
|
function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}): string {
|
||||||
const query = Object.keys(filters).reduce((arr, name) => {
|
const query = Object.keys(filters).reduce((arr, name) => {
|
||||||
const value = filters[name];
|
const value = filters[name];
|
||||||
const operator = value?.filter ?? OPERATORS.equals;
|
const filter = value?.filter ?? OPERATORS.equals;
|
||||||
const column = FILTER_COLUMNS[name] ?? options?.columns?.[name];
|
const column = value?.column ?? FILTER_COLUMNS[name] ?? options?.columns?.[name];
|
||||||
|
|
||||||
if (value !== undefined && column) {
|
if (value !== undefined && column !== undefined) {
|
||||||
arr.push(`and ${mapFilter(column, operator, name)}`);
|
arr.push(`and ${mapFilter(column, filter, name)}`);
|
||||||
|
|
||||||
if (name === 'referrer') {
|
if (name === 'referrer') {
|
||||||
arr.push(
|
arr.push(
|
||||||
@ -198,14 +200,14 @@ async function pagedQuery<T>(model: string, criteria: T, filters: SearchFilter)
|
|||||||
return { data, count, page: +page, pageSize: size, orderBy };
|
return { data, count, page: +page, pageSize: size, orderBy };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getQueryMode(): Prisma.QueryMode {
|
function getQueryMode(): { mode?: Prisma.QueryMode } {
|
||||||
const db = getDatabaseType();
|
const db = getDatabaseType();
|
||||||
|
|
||||||
if (db === POSTGRESQL) {
|
if (db === POSTGRESQL) {
|
||||||
return 'insensitive';
|
return { mode: 'insensitive' };
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'default';
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSearchParameters(query: string, filters: { [key: string]: any }[]) {
|
function getSearchParameters(query: string, filters: { [key: string]: any }[]) {
|
||||||
@ -220,7 +222,7 @@ function getSearchParameters(query: string, filters: { [key: string]: any }[]) {
|
|||||||
typeof value === 'string'
|
typeof value === 'string'
|
||||||
? {
|
? {
|
||||||
[value]: query,
|
[value]: query,
|
||||||
mode,
|
...mode,
|
||||||
}
|
}
|
||||||
: parseFilter(value),
|
: parseFilter(value),
|
||||||
};
|
};
|
||||||
|
@ -213,6 +213,7 @@ export interface QueryFilters {
|
|||||||
city?: string;
|
city?: string;
|
||||||
language?: string;
|
language?: string;
|
||||||
event?: string;
|
event?: string;
|
||||||
|
search?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueryOptions {
|
export interface QueryOptions {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import ipaddr from 'ipaddr.js';
|
import ipaddr from 'ipaddr.js';
|
||||||
import isbot from 'isbot';
|
import { isbot } from 'isbot';
|
||||||
import { COLLECTION_TYPE, HOSTNAME_REGEX, IP_REGEX } from 'lib/constants';
|
import { COLLECTION_TYPE, HOSTNAME_REGEX, IP_REGEX } from 'lib/constants';
|
||||||
import { secret } from 'lib/crypto';
|
import { secret } from 'lib/crypto';
|
||||||
import { getIpAddress } from 'lib/detect';
|
import { getIpAddress } from 'lib/detect';
|
||||||
|
@ -27,6 +27,9 @@ const schema = {
|
|||||||
password: yup.string(),
|
password: yup.string(),
|
||||||
role: yup.string().matches(/admin|user|view-only/i),
|
role: yup.string().matches(/admin|user|view-only/i),
|
||||||
}),
|
}),
|
||||||
|
DELETE: yup.object().shape({
|
||||||
|
userId: yup.string().uuid().required(),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async (
|
export default async (
|
||||||
|
@ -28,6 +28,9 @@ const schema = {
|
|||||||
domain: yup.string(),
|
domain: yup.string(),
|
||||||
shareId: yup.string().matches(SHARE_ID_REGEX, { excludeEmptyString: true }).nullable(),
|
shareId: yup.string().matches(SHARE_ID_REGEX, { excludeEmptyString: true }).nullable(),
|
||||||
}),
|
}),
|
||||||
|
DELETE: yup.object().shape({
|
||||||
|
websiteId: yup.string().uuid().required(),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async (
|
export default async (
|
||||||
|
@ -26,6 +26,8 @@ export interface WebsiteMetricsRequestQuery {
|
|||||||
language?: string;
|
language?: string;
|
||||||
event?: string;
|
event?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
search?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
@ -47,6 +49,8 @@ const schema = {
|
|||||||
language: yup.string(),
|
language: yup.string(),
|
||||||
event: yup.string(),
|
event: yup.string(),
|
||||||
limit: yup.number(),
|
limit: yup.number(),
|
||||||
|
offset: yup.number(),
|
||||||
|
search: yup.string(),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -74,6 +78,8 @@ export default async (
|
|||||||
language,
|
language,
|
||||||
event,
|
event,
|
||||||
limit,
|
limit,
|
||||||
|
offset,
|
||||||
|
search,
|
||||||
} = req.query;
|
} = req.query;
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
@ -98,12 +104,19 @@ export default async (
|
|||||||
city,
|
city,
|
||||||
language,
|
language,
|
||||||
event,
|
event,
|
||||||
|
search,
|
||||||
};
|
};
|
||||||
|
|
||||||
const column = FILTER_COLUMNS[type] || type;
|
const column = FILTER_COLUMNS[type] || type;
|
||||||
|
|
||||||
if (SESSION_COLUMNS.includes(type)) {
|
if (SESSION_COLUMNS.includes(type)) {
|
||||||
const data = await getSessionMetrics(websiteId, column, filters, limit);
|
const data = await getSessionMetrics(
|
||||||
|
websiteId,
|
||||||
|
column,
|
||||||
|
{ ...filters, search },
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
);
|
||||||
|
|
||||||
if (type === 'language') {
|
if (type === 'language') {
|
||||||
const combined = {};
|
const combined = {};
|
||||||
@ -125,7 +138,13 @@ export default async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (EVENT_COLUMNS.includes(type)) {
|
if (EVENT_COLUMNS.includes(type)) {
|
||||||
const data = await getPageviewMetrics(websiteId, column, filters, limit);
|
const data = await getPageviewMetrics(
|
||||||
|
websiteId,
|
||||||
|
column,
|
||||||
|
{ ...filters, search },
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
);
|
||||||
|
|
||||||
return ok(res, data);
|
return ok(res, data);
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
import clickhouse from 'lib/clickhouse';
|
import clickhouse from 'lib/clickhouse';
|
||||||
import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db';
|
import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db';
|
||||||
import { EVENT_TYPE, SESSION_COLUMNS } from 'lib/constants';
|
import { EVENT_TYPE, SESSION_COLUMNS, OPERATORS } from 'lib/constants';
|
||||||
import { QueryFilters } from 'lib/types';
|
import { QueryFilters } from 'lib/types';
|
||||||
|
|
||||||
export async function getPageviewMetrics(
|
export async function getPageviewMetrics(
|
||||||
...args: [websiteId: string, columns: string, filters: QueryFilters, limit?: number]
|
...args: [
|
||||||
|
websiteId: string,
|
||||||
|
column: string,
|
||||||
|
filters: QueryFilters,
|
||||||
|
limit?: number,
|
||||||
|
offset?: number,
|
||||||
|
]
|
||||||
) {
|
) {
|
||||||
return runQuery({
|
return runQuery({
|
||||||
[PRISMA]: () => relationalQuery(...args),
|
[PRISMA]: () => relationalQuery(...args),
|
||||||
@ -18,6 +24,7 @@ async function relationalQuery(
|
|||||||
column: string,
|
column: string,
|
||||||
filters: QueryFilters,
|
filters: QueryFilters,
|
||||||
limit: number = 500,
|
limit: number = 500,
|
||||||
|
offset: number = 0,
|
||||||
) {
|
) {
|
||||||
const { rawQuery, parseFilters } = prisma;
|
const { rawQuery, parseFilters } = prisma;
|
||||||
const { filterQuery, joinSession, params } = await parseFilters(
|
const { filterQuery, joinSession, params } = await parseFilters(
|
||||||
@ -48,6 +55,7 @@ async function relationalQuery(
|
|||||||
group by 1
|
group by 1
|
||||||
order by 2 desc
|
order by 2 desc
|
||||||
limit ${limit}
|
limit ${limit}
|
||||||
|
offset ${offset}
|
||||||
`,
|
`,
|
||||||
params,
|
params,
|
||||||
);
|
);
|
||||||
@ -58,10 +66,19 @@ async function clickhouseQuery(
|
|||||||
column: string,
|
column: string,
|
||||||
filters: QueryFilters,
|
filters: QueryFilters,
|
||||||
limit: number = 500,
|
limit: number = 500,
|
||||||
|
offset: number = 0,
|
||||||
): Promise<{ x: string; y: number }[]> {
|
): Promise<{ x: string; y: number }[]> {
|
||||||
const { rawQuery, parseFilters } = clickhouse;
|
const { rawQuery, parseFilters } = clickhouse;
|
||||||
const { filterQuery, params } = await parseFilters(websiteId, {
|
const { filterQuery, params } = await parseFilters(websiteId, {
|
||||||
...filters,
|
...filters,
|
||||||
|
...(filters.search && {
|
||||||
|
[column]: {
|
||||||
|
value: filters.search,
|
||||||
|
filter: OPERATORS.contains,
|
||||||
|
column,
|
||||||
|
name: column,
|
||||||
|
},
|
||||||
|
}),
|
||||||
eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
|
eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -82,6 +99,7 @@ async function clickhouseQuery(
|
|||||||
group by x
|
group by x
|
||||||
order by y desc
|
order by y desc
|
||||||
limit ${limit}
|
limit ${limit}
|
||||||
|
offset ${offset}
|
||||||
`,
|
`,
|
||||||
params,
|
params,
|
||||||
).then(a => {
|
).then(a => {
|
||||||
|
@ -5,7 +5,13 @@ import { EVENT_TYPE, SESSION_COLUMNS } from 'lib/constants';
|
|||||||
import { QueryFilters } from 'lib/types';
|
import { QueryFilters } from 'lib/types';
|
||||||
|
|
||||||
export async function getSessionMetrics(
|
export async function getSessionMetrics(
|
||||||
...args: [websiteId: string, column: string, filters: QueryFilters, limit?: number]
|
...args: [
|
||||||
|
websiteId: string,
|
||||||
|
column: string,
|
||||||
|
filters: QueryFilters,
|
||||||
|
limit?: number,
|
||||||
|
offset?: number,
|
||||||
|
]
|
||||||
) {
|
) {
|
||||||
return runQuery({
|
return runQuery({
|
||||||
[PRISMA]: () => relationalQuery(...args),
|
[PRISMA]: () => relationalQuery(...args),
|
||||||
@ -18,6 +24,7 @@ async function relationalQuery(
|
|||||||
column: string,
|
column: string,
|
||||||
filters: QueryFilters,
|
filters: QueryFilters,
|
||||||
limit: number = 500,
|
limit: number = 500,
|
||||||
|
offset: number = 0,
|
||||||
) {
|
) {
|
||||||
const { parseFilters, rawQuery } = prisma;
|
const { parseFilters, rawQuery } = prisma;
|
||||||
const { filterQuery, joinSession, params } = await parseFilters(
|
const { filterQuery, joinSession, params } = await parseFilters(
|
||||||
@ -47,7 +54,9 @@ async function relationalQuery(
|
|||||||
group by 1
|
group by 1
|
||||||
${includeCountry ? ', 3' : ''}
|
${includeCountry ? ', 3' : ''}
|
||||||
order by 2 desc
|
order by 2 desc
|
||||||
limit ${limit}`,
|
limit ${limit}
|
||||||
|
offset ${offset}
|
||||||
|
`,
|
||||||
params,
|
params,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -57,6 +66,7 @@ async function clickhouseQuery(
|
|||||||
column: string,
|
column: string,
|
||||||
filters: QueryFilters,
|
filters: QueryFilters,
|
||||||
limit: number = 500,
|
limit: number = 500,
|
||||||
|
offset: number = 0,
|
||||||
): Promise<{ x: string; y: number }[]> {
|
): Promise<{ x: string; y: number }[]> {
|
||||||
const { parseFilters, rawQuery } = clickhouse;
|
const { parseFilters, rawQuery } = clickhouse;
|
||||||
const { filterQuery, params } = await parseFilters(websiteId, {
|
const { filterQuery, params } = await parseFilters(websiteId, {
|
||||||
@ -80,6 +90,7 @@ async function clickhouseQuery(
|
|||||||
${includeCountry ? ', country' : ''}
|
${includeCountry ? ', country' : ''}
|
||||||
order by y desc
|
order by y desc
|
||||||
limit ${limit}
|
limit ${limit}
|
||||||
|
offset ${offset}
|
||||||
`,
|
`,
|
||||||
params,
|
params,
|
||||||
).then(a => {
|
).then(a => {
|
||||||
|
@ -39,5 +39,5 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "next-env.d.ts", ".next/types/**/*.ts"],
|
"include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "next-env.d.ts", ".next/types/**/*.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules", "./cypress.config.ts", "cypress"]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user