mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-22 09:57:00 +01:00
Merge branch 'dev' into analytics
This commit is contained in:
commit
df790cffd2
@ -4,4 +4,9 @@ export default defineConfig({
|
|||||||
e2e: {
|
e2e: {
|
||||||
baseUrl: 'http://localhost:3000',
|
baseUrl: 'http://localhost:3000',
|
||||||
},
|
},
|
||||||
|
// default username / password on init
|
||||||
|
env: {
|
||||||
|
umami_user: 'admin',
|
||||||
|
umami_password: 'umami',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
@ -43,9 +43,10 @@ services:
|
|||||||
- CYPRESS_umami_user=admin
|
- CYPRESS_umami_user=admin
|
||||||
- CYPRESS_umami_password=umami
|
- CYPRESS_umami_password=umami
|
||||||
volumes:
|
volumes:
|
||||||
- ../tsconfig.json:/tsconfig.json
|
- ./tsconfig.json:/tsconfig.json
|
||||||
- ../cypress.config.ts:/cypress.config.ts
|
- ../cypress.config.ts:/cypress.config.ts
|
||||||
- ./:/cypress
|
- ./:/cypress
|
||||||
- ../node_modules/:/node_modules
|
- ../node_modules/:/node_modules
|
||||||
|
- ../src/lib/crypto.ts:/src/lib/crypto.ts
|
||||||
volumes:
|
volumes:
|
||||||
umami-db-data:
|
umami-db-data:
|
||||||
|
@ -6,8 +6,12 @@ describe('Login tests', () => {
|
|||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
cy.visit('/login');
|
cy.visit('/login');
|
||||||
cy.getDataTest('input-username').find('input').type(Cypress.env('umami_user'));
|
cy.getDataTest('input-username').find('input').click();
|
||||||
cy.getDataTest('input-password').find('input').type(Cypress.env('umami_password'));
|
cy.getDataTest('input-username').find('input').type(Cypress.env('umami_user'), { delay: 50 });
|
||||||
|
cy.getDataTest('input-password').find('input').click();
|
||||||
|
cy.getDataTest('input-password')
|
||||||
|
.find('input')
|
||||||
|
.type(Cypress.env('umami_password'), { delay: 50 });
|
||||||
cy.getDataTest('button-submit').click();
|
cy.getDataTest('button-submit').click();
|
||||||
cy.url().should('eq', Cypress.config().baseUrl + '/dashboard');
|
cy.url().should('eq', Cypress.config().baseUrl + '/dashboard');
|
||||||
cy.getDataTest('button-profile').click();
|
cy.getDataTest('button-profile').click();
|
||||||
|
@ -10,8 +10,10 @@ describe('Website tests', () => {
|
|||||||
cy.visit('/settings/websites');
|
cy.visit('/settings/websites');
|
||||||
cy.getDataTest('button-website-add').click();
|
cy.getDataTest('button-website-add').click();
|
||||||
cy.contains(/Add website/i).should('be.visible');
|
cy.contains(/Add website/i).should('be.visible');
|
||||||
cy.getDataTest('input-name').find('input').wait(500).type('Add test', { delay: 50 });
|
cy.getDataTest('input-name').find('input').click();
|
||||||
cy.getDataTest('input-domain').find('input').wait(500).type('addtest.com', { delay: 50 });
|
cy.getDataTest('input-name').find('input').type('Add test', { delay: 50 });
|
||||||
|
cy.getDataTest('input-domain').find('input').click();
|
||||||
|
cy.getDataTest('input-domain').find('input').type('addtest.com', { delay: 50 });
|
||||||
cy.getDataTest('button-submit').click();
|
cy.getDataTest('button-submit').click();
|
||||||
cy.get('td[label="Name"]').should('contain.text', 'Add test');
|
cy.get('td[label="Name"]').should('contain.text', 'Add test');
|
||||||
cy.get('td[label="Domain"]').should('contain.text', 'addtest.com');
|
cy.get('td[label="Domain"]').should('contain.text', 'addtest.com');
|
||||||
@ -26,10 +28,10 @@ describe('Website tests', () => {
|
|||||||
cy.deleteWebsite(websiteId);
|
cy.deleteWebsite(websiteId);
|
||||||
});
|
});
|
||||||
cy.visit('/settings/websites');
|
cy.visit('/settings/websites');
|
||||||
cy.contains('Add test').should('not.exist');
|
cy.contains(/Add test/i).should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
it.only('Edit a website', () => {
|
it('Edit a website', () => {
|
||||||
// prep data
|
// prep data
|
||||||
cy.addWebsite('Update test', 'updatetest.com');
|
cy.addWebsite('Update test', 'updatetest.com');
|
||||||
cy.visit('/settings/websites');
|
cy.visit('/settings/websites');
|
||||||
@ -37,16 +39,12 @@ describe('Website tests', () => {
|
|||||||
// edit website
|
// edit website
|
||||||
cy.getDataTest('link-button-edit').first().click();
|
cy.getDataTest('link-button-edit').first().click();
|
||||||
cy.contains(/Details/i).should('be.visible');
|
cy.contains(/Details/i).should('be.visible');
|
||||||
cy.getDataTest('input-name')
|
cy.getDataTest('input-name').find('input').click();
|
||||||
.find('input')
|
cy.getDataTest('input-name').find('input').clear();
|
||||||
.wait(500)
|
cy.getDataTest('input-name').find('input').type('Updated website', { delay: 50 });
|
||||||
.clear()
|
cy.getDataTest('input-domain').find('input').click();
|
||||||
.type('Updated website', { delay: 50 });
|
cy.getDataTest('input-domain').find('input').clear();
|
||||||
cy.getDataTest('input-domain')
|
cy.getDataTest('input-domain').find('input').type('updatedwebsite.com', { delay: 50 });
|
||||||
.find('input')
|
|
||||||
.wait(500)
|
|
||||||
.clear()
|
|
||||||
.type('updatedwebsite.com', { delay: 50 });
|
|
||||||
cy.getDataTest('button-submit').click({ force: true });
|
cy.getDataTest('button-submit').click({ force: true });
|
||||||
cy.getDataTest('input-name').find('input').should('have.value', 'Updated website');
|
cy.getDataTest('input-name').find('input').should('have.value', 'Updated website');
|
||||||
cy.getDataTest('input-domain').find('input').should('have.value', 'updatedwebsite.com');
|
cy.getDataTest('input-domain').find('input').should('have.value', 'updatedwebsite.com');
|
||||||
@ -69,7 +67,7 @@ describe('Website tests', () => {
|
|||||||
cy.deleteWebsite(websiteId);
|
cy.deleteWebsite(websiteId);
|
||||||
});
|
});
|
||||||
cy.visit('/settings/websites');
|
cy.visit('/settings/websites');
|
||||||
cy.contains('Add test').should('not.exist');
|
cy.contains(/Add test/i).should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Delete a website', () => {
|
it('Delete a website', () => {
|
||||||
@ -86,6 +84,6 @@ describe('Website tests', () => {
|
|||||||
cy.contains(/Type DELETE in the box below to confirm./i).should('be.visible');
|
cy.contains(/Type DELETE in the box below to confirm./i).should('be.visible');
|
||||||
cy.get('input[name="confirm"').type('DELETE');
|
cy.get('input[name="confirm"').type('DELETE');
|
||||||
cy.get('button[type="submit"]').click();
|
cy.get('button[type="submit"]').click();
|
||||||
cy.contains('Delete test').should('not.exist');
|
cy.contains(/Delete test/i).should('not.exist');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -66,14 +66,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clickhouse/client": "^0.2.2",
|
"@clickhouse/client": "^0.2.2",
|
||||||
"@fontsource/inter": "^4.5.15",
|
"@fontsource/inter": "^4.5.15",
|
||||||
"@prisma/client": "5.9.1",
|
"@prisma/client": "5.10.2",
|
||||||
"@prisma/extension-read-replicas": "^0.3.0",
|
"@prisma/extension-read-replicas": "^0.3.0",
|
||||||
"@react-spring/web": "^9.7.3",
|
"@react-spring/web": "^9.7.3",
|
||||||
"@tanstack/react-query": "^5.12.2",
|
"@tanstack/react-query": "^5.12.2",
|
||||||
"@umami/prisma-client": "^0.14.0",
|
"@umami/prisma-client": "^0.14.0",
|
||||||
"@umami/redis-client": "^0.18.0",
|
"@umami/redis-client": "^0.18.0",
|
||||||
"chalk": "^4.1.1",
|
"chalk": "^4.1.1",
|
||||||
"chart.js": "^4.2.1",
|
"chart.js": "^4.4.2",
|
||||||
"chartjs-adapter-date-fns": "^3.0.0",
|
"chartjs-adapter-date-fns": "^3.0.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"colord": "^2.9.2",
|
"colord": "^2.9.2",
|
||||||
@ -98,11 +98,11 @@
|
|||||||
"maxmind": "^4.3.6",
|
"maxmind": "^4.3.6",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"moment-timezone": "^0.5.35",
|
"moment-timezone": "^0.5.35",
|
||||||
"next": "14.1.0",
|
"next": "14.1.3",
|
||||||
"next-basics": "^0.39.0",
|
"next-basics": "^0.39.0",
|
||||||
"node-fetch": "^3.2.8",
|
"node-fetch": "^3.2.8",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prisma": "5.9.1",
|
"prisma": "5.10.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-basics": "^0.123.0",
|
"react-basics": "^0.123.0",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Erstellt"
|
"value": "Erstellt"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Erstellt"
|
"value": "Erstellt"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
"label.add-member": [
|
"label.add-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Add member"
|
"value": "Añadir miembro"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.add-website": [
|
"label.add-website": [
|
||||||
@ -215,6 +215,12 @@
|
|||||||
"value": "Creado"
|
"value": "Creado"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
@ -272,7 +278,7 @@
|
|||||||
"label.delete-report": [
|
"label.delete-report": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Delete report"
|
"value": "Eliminar reporte"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.delete-team": [
|
"label.delete-team": [
|
||||||
@ -464,7 +470,7 @@
|
|||||||
"label.insights-description": [
|
"label.insights-description": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Dive deeper into your data by using segments and filters."
|
"value": "Profundice en sus datos mediante el uso de segmentos y filtros."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.is": [
|
"label.is": [
|
||||||
@ -482,7 +488,7 @@
|
|||||||
"label.is-not-set": [
|
"label.is-not-set": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Is not set"
|
"value": "No está establecido"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.is-set": [
|
"label.is-set": [
|
||||||
@ -588,7 +594,7 @@
|
|||||||
"label.manage": [
|
"label.manage": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Manage"
|
"value": "Administrar"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.max": [
|
"label.max": [
|
||||||
@ -600,7 +606,7 @@
|
|||||||
"label.member": [
|
"label.member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Member"
|
"value": "Miembro"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.members": [
|
"label.members": [
|
||||||
@ -630,7 +636,7 @@
|
|||||||
"label.my-account": [
|
"label.my-account": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "My account"
|
"value": "Mi cuenta"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.my-websites": [
|
"label.my-websites": [
|
||||||
@ -842,7 +848,7 @@
|
|||||||
"label.remove-member": [
|
"label.remove-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Remove member"
|
"value": "Eliminar miembro"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.reports": [
|
"label.reports": [
|
||||||
@ -926,7 +932,7 @@
|
|||||||
"label.select-role": [
|
"label.select-role": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Select role"
|
"value": "Seleccionar rol"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.select-website": [
|
"label.select-website": [
|
||||||
@ -1004,7 +1010,7 @@
|
|||||||
"label.team-view-only": [
|
"label.team-view-only": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Team view only"
|
"value": "Vista solo del equipo"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.team-websites": [
|
"label.team-websites": [
|
||||||
@ -1088,13 +1094,13 @@
|
|||||||
"label.transfer": [
|
"label.transfer": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer"
|
"value": "Transferir"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.transfer-website": [
|
"label.transfer-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer website"
|
"value": "Transferir sitio web"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.true": [
|
"label.true": [
|
||||||
@ -1232,7 +1238,7 @@
|
|||||||
"message.action-confirmation": [
|
"message.action-confirmation": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Type "
|
"value": "Escriba "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1240,7 +1246,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": " in the box below to confirm."
|
"value": " en el cuadro a continuación para confirmar."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.active-users": [
|
"message.active-users": [
|
||||||
@ -1308,7 +1314,7 @@
|
|||||||
"message.confirm-remove": [
|
"message.confirm-remove": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Are you sure you want to remove "
|
"value": "¿Estás seguro de que desea eliminar "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1336,7 +1342,7 @@
|
|||||||
"message.delete-team-warning": [
|
"message.delete-team-warning": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Deleting a team will also delete all team websites."
|
"value": "Al eliminar un equipo, también se eliminarán todos los sitios web del equipo."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.delete-website-warning": [
|
"message.delete-website-warning": [
|
||||||
@ -1532,7 +1538,7 @@
|
|||||||
"message.transfer-team-website-to-user": [
|
"message.transfer-team-website-to-user": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer this website to your account?"
|
"value": "¿Transferir este sitio web a su cuenta?"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.transfer-user-website-to-team": [
|
"message.transfer-user-website-to-team": [
|
||||||
@ -1544,13 +1550,13 @@
|
|||||||
"message.transfer-website": [
|
"message.transfer-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer website ownership to your account or another team."
|
"value": "Seleccione el equipo al que transferir este sitio web."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.triggered-event": [
|
"message.triggered-event": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Triggered event"
|
"value": "Evento lanzado"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.user-deleted": [
|
"message.user-deleted": [
|
||||||
@ -1562,7 +1568,7 @@
|
|||||||
"message.viewed-page": [
|
"message.viewed-page": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Viewed page"
|
"value": "Página vista"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.visitor-log": [
|
"message.visitor-log": [
|
||||||
@ -1602,7 +1608,7 @@
|
|||||||
"message.visitors-dropped-off": [
|
"message.visitors-dropped-off": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Visitors dropped off"
|
"value": "Los visitantes salieron"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
"label.add-member": [
|
"label.add-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Add member"
|
"value": "Ajouter un membre"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.add-website": [
|
"label.add-website": [
|
||||||
@ -215,6 +215,12 @@
|
|||||||
"value": "Créé"
|
"value": "Créé"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Crée par"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
@ -272,7 +278,7 @@
|
|||||||
"label.delete-report": [
|
"label.delete-report": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Delete report"
|
"value": "Supprimer le rapport"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.delete-team": [
|
"label.delete-team": [
|
||||||
@ -362,7 +368,7 @@
|
|||||||
"label.edit-member": [
|
"label.edit-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Edit member"
|
"value": "Modifier le membre"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.enable-share-url": [
|
"label.enable-share-url": [
|
||||||
@ -580,7 +586,7 @@
|
|||||||
"label.manage": [
|
"label.manage": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Manage"
|
"value": "Gérer"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.max": [
|
"label.max": [
|
||||||
@ -592,7 +598,7 @@
|
|||||||
"label.member": [
|
"label.member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Member"
|
"value": "Membre"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.members": [
|
"label.members": [
|
||||||
@ -622,7 +628,7 @@
|
|||||||
"label.my-account": [
|
"label.my-account": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "My account"
|
"value": "Mon compte"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.my-websites": [
|
"label.my-websites": [
|
||||||
@ -646,7 +652,7 @@
|
|||||||
"label.none": [
|
"label.none": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Aucun·e"
|
"value": "Aucun"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.number-of-records": [
|
"label.number-of-records": [
|
||||||
@ -834,7 +840,7 @@
|
|||||||
"label.remove-member": [
|
"label.remove-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Remove member"
|
"value": "Retirer le membre"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.reports": [
|
"label.reports": [
|
||||||
@ -918,7 +924,7 @@
|
|||||||
"label.select-role": [
|
"label.select-role": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Select role"
|
"value": "Choisir un rôle"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.select-website": [
|
"label.select-website": [
|
||||||
@ -1080,13 +1086,13 @@
|
|||||||
"label.transfer": [
|
"label.transfer": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer"
|
"value": "Transférer"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.transfer-website": [
|
"label.transfer-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer website"
|
"value": "Transférer le site"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.true": [
|
"label.true": [
|
||||||
@ -1224,7 +1230,7 @@
|
|||||||
"message.action-confirmation": [
|
"message.action-confirmation": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Type "
|
"value": "Taper "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1232,7 +1238,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": " in the box below to confirm."
|
"value": " ci-dessous pour confirmer."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.active-users": [
|
"message.active-users": [
|
||||||
@ -1304,7 +1310,7 @@
|
|||||||
"message.confirm-remove": [
|
"message.confirm-remove": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Are you sure you want to remove "
|
"value": "Êtes-vous sûr de vouloir retirer "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1312,7 +1318,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "?"
|
"value": " ?"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.confirm-reset": [
|
"message.confirm-reset": [
|
||||||
@ -1332,7 +1338,7 @@
|
|||||||
"message.delete-team-warning": [
|
"message.delete-team-warning": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Deleting a team will also delete all team websites."
|
"value": "Supprimer une équipe supprimera aussi tous les sites de cette équipe."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.delete-website-warning": [
|
"message.delete-website-warning": [
|
||||||
@ -1520,19 +1526,19 @@
|
|||||||
"message.transfer-team-website-to-user": [
|
"message.transfer-team-website-to-user": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer this website to your account?"
|
"value": "Transférer ce site sur votre compte ?"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.transfer-user-website-to-team": [
|
"message.transfer-user-website-to-team": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Select the team to transfer this website to."
|
"value": "Choisir l'équipe à laquelle transférer ce site."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.transfer-website": [
|
"message.transfer-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer website ownership to your account or another team."
|
"value": "Transférer la propriété du site sur votre compte ou à une autre équipe."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.triggered-event": [
|
"message.triggered-event": [
|
||||||
@ -1550,7 +1556,7 @@
|
|||||||
"message.viewed-page": [
|
"message.viewed-page": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Viewed page"
|
"value": "Page vue"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.visitor-log": [
|
"message.visitor-log": [
|
||||||
@ -1590,7 +1596,7 @@
|
|||||||
"message.visitors-dropped-off": [
|
"message.visitors-dropped-off": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Visitors dropped off"
|
"value": "Visiteurs ont abandonné"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
"label.add-member": [
|
"label.add-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Add member"
|
"value": "メンバーの追加"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.add-website": [
|
"label.add-website": [
|
||||||
@ -188,7 +188,7 @@
|
|||||||
"label.create": [
|
"label.create": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Create"
|
"value": "作成"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.create-report": [
|
"label.create-report": [
|
||||||
@ -215,6 +215,12 @@
|
|||||||
"value": "作成されました"
|
"value": "作成されました"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
@ -272,7 +278,7 @@
|
|||||||
"label.delete-report": [
|
"label.delete-report": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Delete report"
|
"value": "レポートの削除"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.delete-team": [
|
"label.delete-team": [
|
||||||
@ -362,7 +368,7 @@
|
|||||||
"label.edit-member": [
|
"label.edit-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Edit member"
|
"value": "メンバーの編集"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.enable-share-url": [
|
"label.enable-share-url": [
|
||||||
@ -410,13 +416,13 @@
|
|||||||
"label.filter": [
|
"label.filter": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Filter"
|
"value": "フィルター"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.filter-combined": [
|
"label.filter-combined": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "統合"
|
"value": "結合"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.filter-raw": [
|
"label.filter-raw": [
|
||||||
@ -434,13 +440,13 @@
|
|||||||
"label.funnel": [
|
"label.funnel": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "分析"
|
"value": "ファネル"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.funnel-description": [
|
"label.funnel-description": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Understand the conversion and drop-off rate of users."
|
"value": "ユーザーのコンバージョン率と離脱率を分析します。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.greater-than": [
|
"label.greater-than": [
|
||||||
@ -458,13 +464,13 @@
|
|||||||
"label.insights": [
|
"label.insights": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "見通し"
|
"value": "インサイト"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.insights-description": [
|
"label.insights-description": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Dive deeper into your data by using segments and filters."
|
"value": "セグメントとフィルタを使用して、データをさらに詳しく分析します。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.is": [
|
"label.is": [
|
||||||
@ -588,7 +594,7 @@
|
|||||||
"label.manage": [
|
"label.manage": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Manage"
|
"value": "管理"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.max": [
|
"label.max": [
|
||||||
@ -600,7 +606,7 @@
|
|||||||
"label.member": [
|
"label.member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Member"
|
"value": "メンバー"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.members": [
|
"label.members": [
|
||||||
@ -630,7 +636,7 @@
|
|||||||
"label.my-account": [
|
"label.my-account": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "My account"
|
"value": "マイアカウント"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.my-websites": [
|
"label.my-websites": [
|
||||||
@ -842,7 +848,7 @@
|
|||||||
"label.remove-member": [
|
"label.remove-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Remove member"
|
"value": "メンバーの削除"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.reports": [
|
"label.reports": [
|
||||||
@ -872,13 +878,13 @@
|
|||||||
"label.retention": [
|
"label.retention": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "保持"
|
"value": "リテンション"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.retention-description": [
|
"label.retention-description": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Measure your website stickiness by tracking how often users return."
|
"value": "ユーザーの再訪問回数を記録して、Webサイトのリテンション率を計測します。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.role": [
|
"label.role": [
|
||||||
@ -908,13 +914,13 @@
|
|||||||
"label.search": [
|
"label.search": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Search"
|
"value": "検索"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.select": [
|
"label.select": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Select"
|
"value": "選択"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.select-date": [
|
"label.select-date": [
|
||||||
@ -926,7 +932,7 @@
|
|||||||
"label.select-role": [
|
"label.select-role": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Select role"
|
"value": "ロールを選択"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.select-website": [
|
"label.select-website": [
|
||||||
@ -1004,7 +1010,7 @@
|
|||||||
"label.team-view-only": [
|
"label.team-view-only": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Team view only"
|
"value": "チーム表示のみ"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.team-websites": [
|
"label.team-websites": [
|
||||||
@ -1088,13 +1094,13 @@
|
|||||||
"label.transfer": [
|
"label.transfer": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer"
|
"value": "移管"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.transfer-website": [
|
"label.transfer-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer website"
|
"value": "Webサイトの移管"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.true": [
|
"label.true": [
|
||||||
@ -1232,7 +1238,7 @@
|
|||||||
"message.action-confirmation": [
|
"message.action-confirmation": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Type "
|
"value": "承認する場合は、下のフォームに「"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1240,7 +1246,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": " in the box below to confirm."
|
"value": "」と入力してください。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.active-users": [
|
"message.active-users": [
|
||||||
@ -1298,17 +1304,13 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.confirm-remove": [
|
"message.confirm-remove": [
|
||||||
{
|
|
||||||
"type": 0,
|
|
||||||
"value": "Are you sure you want to remove "
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
"value": "target"
|
"value": "target"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "?"
|
"value": "を削除してもよろしいですか?"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.confirm-reset": [
|
"message.confirm-reset": [
|
||||||
@ -1324,7 +1326,7 @@
|
|||||||
"message.delete-team-warning": [
|
"message.delete-team-warning": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Deleting a team will also delete all team websites."
|
"value": "チームを削除すると、そのチームが管理しているWebサイトもすべて削除されます。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.delete-website-warning": [
|
"message.delete-website-warning": [
|
||||||
@ -1526,25 +1528,25 @@
|
|||||||
"message.transfer-team-website-to-user": [
|
"message.transfer-team-website-to-user": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer this website to your account?"
|
"value": "このWebサイトをあなたのアカウントに移管しますか?"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.transfer-user-website-to-team": [
|
"message.transfer-user-website-to-team": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Select the team to transfer this website to."
|
"value": "このWebサイトを移管するチームを選択してください。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.transfer-website": [
|
"message.transfer-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer website ownership to your account or another team."
|
"value": "Webサイトの所有権を自分のアカウントまたは別のチームへ移管します。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.triggered-event": [
|
"message.triggered-event": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Triggered event"
|
"value": "トリガーされたイベント"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.user-deleted": [
|
"message.user-deleted": [
|
||||||
@ -1556,7 +1558,7 @@
|
|||||||
"message.viewed-page": [
|
"message.viewed-page": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Viewed page"
|
"value": "閲覧されたページ"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.visitor-log": [
|
"message.visitor-log": [
|
||||||
@ -1596,7 +1598,7 @@
|
|||||||
"message.visitors-dropped-off": [
|
"message.visitors-dropped-off": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Visitors dropped off"
|
"value": "訪問者の離脱率"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Үүсгэсэн"
|
"value": "Үүсгэсэн"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "ပြုလုပ်ပြီးသော"
|
"value": "ပြုလုပ်ပြီးသော"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Gemaakt"
|
"value": "Gemaakt"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Utworzony"
|
"value": "Utworzony"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Criado"
|
"value": "Criado"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -215,6 +215,12 @@
|
|||||||
"value": "Создано"
|
"value": "Создано"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Ustvarjeno"
|
"value": "Ustvarjeno"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Skapad"
|
"value": "Skapad"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "Created"
|
"value": "Created"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
"label.add-member": [
|
"label.add-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Add member"
|
"value": "添加成员"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.add-website": [
|
"label.add-website": [
|
||||||
@ -215,6 +215,12 @@
|
|||||||
"value": "已创建"
|
"value": "已创建"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "创建者"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
@ -272,7 +278,7 @@
|
|||||||
"label.delete-report": [
|
"label.delete-report": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Delete report"
|
"value": "删除报告"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.delete-team": [
|
"label.delete-team": [
|
||||||
@ -362,7 +368,7 @@
|
|||||||
"label.edit-member": [
|
"label.edit-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Edit member"
|
"value": "编辑成员"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.enable-share-url": [
|
"label.enable-share-url": [
|
||||||
@ -588,7 +594,7 @@
|
|||||||
"label.manage": [
|
"label.manage": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Manage"
|
"value": "管理"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.max": [
|
"label.max": [
|
||||||
@ -600,7 +606,7 @@
|
|||||||
"label.member": [
|
"label.member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Member"
|
"value": "成员"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.members": [
|
"label.members": [
|
||||||
@ -630,7 +636,7 @@
|
|||||||
"label.my-account": [
|
"label.my-account": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "My account"
|
"value": "我的账户"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.my-websites": [
|
"label.my-websites": [
|
||||||
@ -700,7 +706,7 @@
|
|||||||
"label.os": [
|
"label.os": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "OS"
|
"value": "操作系统"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.overview": [
|
"label.overview": [
|
||||||
@ -718,7 +724,7 @@
|
|||||||
"label.page-of": [
|
"label.page-of": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "总"
|
"value": "总 "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -726,7 +732,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "中的第"
|
"value": " 中的第 "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -734,7 +740,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "页"
|
"value": " 页"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.page-views": [
|
"label.page-views": [
|
||||||
@ -850,7 +856,7 @@
|
|||||||
"label.remove-member": [
|
"label.remove-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Remove member"
|
"value": "移除成员"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.reports": [
|
"label.reports": [
|
||||||
@ -922,7 +928,7 @@
|
|||||||
"label.select": [
|
"label.select": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Select"
|
"value": "选择"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.select-date": [
|
"label.select-date": [
|
||||||
@ -934,7 +940,7 @@
|
|||||||
"label.select-role": [
|
"label.select-role": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Select role"
|
"value": "选择角色"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.select-website": [
|
"label.select-website": [
|
||||||
@ -1012,7 +1018,7 @@
|
|||||||
"label.team-view-only": [
|
"label.team-view-only": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Team view only"
|
"value": "仅团队视图"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.team-websites": [
|
"label.team-websites": [
|
||||||
@ -1096,13 +1102,13 @@
|
|||||||
"label.transfer": [
|
"label.transfer": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer"
|
"value": "转移"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.transfer-website": [
|
"label.transfer-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer website"
|
"value": "转移网站"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.true": [
|
"label.true": [
|
||||||
@ -1240,7 +1246,7 @@
|
|||||||
"message.action-confirmation": [
|
"message.action-confirmation": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Type "
|
"value": "在下面的框中输入 "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1248,7 +1254,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": " in the box below to confirm."
|
"value": " 以确认。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.active-users": [
|
"message.active-users": [
|
||||||
@ -1296,7 +1302,7 @@
|
|||||||
"message.confirm-remove": [
|
"message.confirm-remove": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Are you sure you want to remove "
|
"value": "您确定要移除 "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1304,7 +1310,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "?"
|
"value": " ?"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.confirm-reset": [
|
"message.confirm-reset": [
|
||||||
@ -1318,13 +1324,13 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": " 的数据吗?"
|
"value": " 的数据吗?"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.delete-team-warning": [
|
"message.delete-team-warning": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Deleting a team will also delete all team websites."
|
"value": "删除团队也会删除所有团队的网站。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.delete-website-warning": [
|
"message.delete-website-warning": [
|
||||||
@ -1346,7 +1352,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "上的"
|
"value": " 上的 "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1388,7 +1394,7 @@
|
|||||||
"message.new-version-available": [
|
"message.new-version-available": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Umami的新版本"
|
"value": "Umami 的新版本 "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1396,7 +1402,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "已推出!"
|
"value": " 已推出!"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.no-data-available": [
|
"message.no-data-available": [
|
||||||
@ -1456,7 +1462,7 @@
|
|||||||
"message.reset-website": [
|
"message.reset-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "如果确定重置该网站, 请在下面的输入框中输入 "
|
"value": "如果确定重置该网站,请在下面的输入框中输入 "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1520,25 +1526,25 @@
|
|||||||
"message.transfer-team-website-to-user": [
|
"message.transfer-team-website-to-user": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer this website to your account?"
|
"value": "将该网站转入您的账户?"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.transfer-user-website-to-team": [
|
"message.transfer-user-website-to-team": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Select the team to transfer this website to."
|
"value": "选择要将该网站转移到哪个团队。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.transfer-website": [
|
"message.transfer-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Transfer website ownership to your account or another team."
|
"value": "将网站所有权转移到您的账户或其他团队。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.triggered-event": [
|
"message.triggered-event": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Triggered event"
|
"value": "触发事件"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.user-deleted": [
|
"message.user-deleted": [
|
||||||
@ -1550,13 +1556,13 @@
|
|||||||
"message.viewed-page": [
|
"message.viewed-page": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Viewed page"
|
"value": "已浏览页面"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.visitor-log": [
|
"message.visitor-log": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "来自"
|
"value": "来自 "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1564,7 +1570,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "的访客在搭载 "
|
"value": " 的访客在搭载 "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1572,7 +1578,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": " 的"
|
"value": " 的 "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1580,7 +1586,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "上使用 "
|
"value": " 上使用 "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
@ -1594,7 +1600,7 @@
|
|||||||
"message.visitors-dropped-off": [
|
"message.visitors-dropped-off": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Visitors dropped off"
|
"value": "访客减少"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -215,6 +215,12 @@
|
|||||||
"value": "已建立"
|
"value": "已建立"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.created-by": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Created By"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.current-password": [
|
"label.current-password": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -10,7 +10,8 @@ export default {
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
replace({
|
replace({
|
||||||
'/api/send': process.env.COLLECT_API_ENDPOINT || '/api/send',
|
'__COLLECT_API_HOST__': process.env.COLLECT_API_HOST || '',
|
||||||
|
'__COLLECT_API_ENDPOINT__': process.env.COLLECT_API_ENDPOINT || '/api/send',
|
||||||
delimiters: ['', ''],
|
delimiters: ['', ''],
|
||||||
preventAssignment: true,
|
preventAssignment: true,
|
||||||
}),
|
}),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
import { Metadata } from 'next';
|
||||||
import ReportsHeader from './ReportsHeader';
|
import ReportsHeader from './ReportsHeader';
|
||||||
import ReportsDataTable from './ReportsDataTable';
|
import ReportsDataTable from './ReportsDataTable';
|
||||||
|
|
||||||
@ -10,6 +11,7 @@ export default function ReportsPage({ teamId }: { teamId: string }) {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export const metadata = {
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
title: 'Reports',
|
title: 'Reports',
|
||||||
};
|
};
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
import FunnelReport from '../funnel/FunnelReport';
|
|
||||||
import EventDataReport from '../event-data/EventDataReport';
|
|
||||||
import InsightsReport from '../insights/InsightsReport';
|
|
||||||
import RetentionReport from '../retention/RetentionReport';
|
|
||||||
import { useApi } from 'components/hooks';
|
|
||||||
|
|
||||||
const reports = {
|
|
||||||
funnel: FunnelReport,
|
|
||||||
'event-data': EventDataReport,
|
|
||||||
insights: InsightsReport,
|
|
||||||
retention: RetentionReport,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ReportDetails({ reportId }: { reportId: string }) {
|
|
||||||
const { get, useQuery } = useApi();
|
|
||||||
const { data: report } = useQuery({
|
|
||||||
queryKey: ['reports', reportId],
|
|
||||||
queryFn: () => get(`/reports/${reportId}`),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!report) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReportComponent = reports[report.type];
|
|
||||||
|
|
||||||
return <ReportComponent reportId={reportId} />;
|
|
||||||
}
|
|
@ -1,6 +1,27 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import ReportDetails from './ReportDetails';
|
import FunnelReport from '../funnel/FunnelReport';
|
||||||
|
import EventDataReport from '../event-data/EventDataReport';
|
||||||
|
import InsightsReport from '../insights/InsightsReport';
|
||||||
|
import RetentionReport from '../retention/RetentionReport';
|
||||||
|
import UTMReport from '../utm/UTMReport';
|
||||||
|
import { useReport } from 'components/hooks';
|
||||||
|
|
||||||
export default function ReportPage({ reportId }) {
|
const reports = {
|
||||||
return <ReportDetails reportId={reportId} />;
|
funnel: FunnelReport,
|
||||||
|
'event-data': EventDataReport,
|
||||||
|
insights: InsightsReport,
|
||||||
|
retention: RetentionReport,
|
||||||
|
utm: UTMReport,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ReportPage({ reportId }: { reportId: string }) {
|
||||||
|
const { report } = useReport(reportId);
|
||||||
|
|
||||||
|
if (!report) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReportComponent = reports[report.type];
|
||||||
|
|
||||||
|
return <ReportComponent reportId={reportId} />;
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import PageHeader from 'components/layout/PageHeader';
|
|||||||
import Funnel from 'assets/funnel.svg';
|
import Funnel from 'assets/funnel.svg';
|
||||||
import Lightbulb from 'assets/lightbulb.svg';
|
import Lightbulb from 'assets/lightbulb.svg';
|
||||||
import Magnet from 'assets/magnet.svg';
|
import Magnet from 'assets/magnet.svg';
|
||||||
|
import Tag from 'assets/tag.svg';
|
||||||
import styles from './ReportTemplates.module.css';
|
import styles from './ReportTemplates.module.css';
|
||||||
import { useMessages, useTeamUrl } from 'components/hooks';
|
import { useMessages, useTeamUrl } from 'components/hooks';
|
||||||
|
|
||||||
@ -30,6 +31,12 @@ export function ReportTemplates({ showHeader = true }: { showHeader?: boolean })
|
|||||||
url: renderTeamUrl('/reports/retention'),
|
url: renderTeamUrl('/reports/retention'),
|
||||||
icon: <Magnet />,
|
icon: <Magnet />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: formatMessage(labels.utm),
|
||||||
|
description: formatMessage(labels.utmDescription),
|
||||||
|
url: renderTeamUrl('/reports/utm'),
|
||||||
|
icon: <Tag />,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -53,11 +53,11 @@ export function InsightsParameters() {
|
|||||||
filters,
|
filters,
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = values => {
|
const handleSubmit = (values: any) => {
|
||||||
runReport(values);
|
runReport(values);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAdd = (id, value) => {
|
const handleAdd = (id: string | number, value: { name: any }) => {
|
||||||
const data = parameterData[id];
|
const data = parameterData[id];
|
||||||
|
|
||||||
if (!data.find(({ name }) => name === value.name)) {
|
if (!data.find(({ name }) => name === value.name)) {
|
||||||
@ -65,7 +65,7 @@ export function InsightsParameters() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemove = (id, index) => {
|
const handleRemove = (id: string, index: number) => {
|
||||||
const data = [...parameterData[id]];
|
const data = [...parameterData[id]];
|
||||||
data.splice(index, 1);
|
data.splice(index, 1);
|
||||||
updateReport({ parameters: { [id]: data } });
|
updateReport({ parameters: { [id]: data } });
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
import { Metadata } from 'next';
|
||||||
import RetentionReport from './RetentionReport';
|
import RetentionReport from './RetentionReport';
|
||||||
|
|
||||||
export default function RetentionReportPage() {
|
export default function RetentionReportPage() {
|
||||||
return <RetentionReport />;
|
return <RetentionReport />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Retention Report',
|
||||||
|
};
|
||||||
|
36
src/app/(main)/reports/utm/UTMParameters.tsx
Normal file
36
src/app/(main)/reports/utm/UTMParameters.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { useMessages } from 'components/hooks';
|
||||||
|
import { Form, FormButtons, SubmitButton } from 'react-basics';
|
||||||
|
import { ReportContext } from '../[reportId]/Report';
|
||||||
|
import BaseParameters from '../[reportId]/BaseParameters';
|
||||||
|
|
||||||
|
export function UTMParameters() {
|
||||||
|
const { report, runReport, isRunning } = useContext(ReportContext);
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
|
const { id, parameters } = report || {};
|
||||||
|
const { websiteId, dateRange } = parameters || {};
|
||||||
|
const queryDisabled = !websiteId || !dateRange;
|
||||||
|
|
||||||
|
const handleSubmit = (data: any, e: any) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!queryDisabled) {
|
||||||
|
runReport(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
|
||||||
|
<BaseParameters showDateSelect={true} allowWebsiteSelect={!id} />
|
||||||
|
<FormButtons>
|
||||||
|
<SubmitButton variant="primary" disabled={queryDisabled} isLoading={isRunning}>
|
||||||
|
{formatMessage(labels.runQuery)}
|
||||||
|
</SubmitButton>
|
||||||
|
</FormButtons>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UTMParameters;
|
28
src/app/(main)/reports/utm/UTMReport.tsx
Normal file
28
src/app/(main)/reports/utm/UTMReport.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
'use client';
|
||||||
|
import Report from '../[reportId]/Report';
|
||||||
|
import ReportHeader from '../[reportId]/ReportHeader';
|
||||||
|
import ReportMenu from '../[reportId]/ReportMenu';
|
||||||
|
import ReportBody from '../[reportId]/ReportBody';
|
||||||
|
import UTMParameters from './UTMParameters';
|
||||||
|
import UTMView from './UTMView';
|
||||||
|
import Tag from 'assets/tag.svg';
|
||||||
|
import { REPORT_TYPES } from 'lib/constants';
|
||||||
|
|
||||||
|
const defaultParameters = {
|
||||||
|
type: REPORT_TYPES.utm,
|
||||||
|
parameters: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function UTMReport({ reportId }: { reportId?: string }) {
|
||||||
|
return (
|
||||||
|
<Report reportId={reportId} defaultParameters={defaultParameters}>
|
||||||
|
<ReportHeader icon={<Tag />} />
|
||||||
|
<ReportMenu>
|
||||||
|
<UTMParameters />
|
||||||
|
</ReportMenu>
|
||||||
|
<ReportBody>
|
||||||
|
<UTMView />
|
||||||
|
</ReportBody>
|
||||||
|
</Report>
|
||||||
|
);
|
||||||
|
}
|
5
src/app/(main)/reports/utm/UTMReportPage.tsx
Normal file
5
src/app/(main)/reports/utm/UTMReportPage.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import UTMReport from './UTMReport';
|
||||||
|
|
||||||
|
export default function UTMReportPage() {
|
||||||
|
return <UTMReport />;
|
||||||
|
}
|
14
src/app/(main)/reports/utm/UTMView.module.css
Normal file
14
src/app/(main)/reports/utm/UTMView.module.css
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 50% 50%;
|
||||||
|
gap: 20px;
|
||||||
|
border-bottom: 1px solid var(--base300);
|
||||||
|
padding-bottom: 30px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
65
src/app/(main)/reports/utm/UTMView.tsx
Normal file
65
src/app/(main)/reports/utm/UTMView.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { firstBy } from 'thenby';
|
||||||
|
import { ReportContext } from '../[reportId]/Report';
|
||||||
|
import { CHART_COLORS, UTM_PARAMS } from 'lib/constants';
|
||||||
|
import PieChart from 'components/charts/PieChart';
|
||||||
|
import ListTable from 'components/metrics/ListTable';
|
||||||
|
import styles from './UTMView.module.css';
|
||||||
|
import { useMessages } from 'components/hooks';
|
||||||
|
|
||||||
|
function toArray(data: { [key: string]: number }) {
|
||||||
|
return Object.keys(data)
|
||||||
|
.map(key => {
|
||||||
|
return { name: key, value: data[key] };
|
||||||
|
})
|
||||||
|
.sort(firstBy('value', -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UTMView() {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { report } = useContext(ReportContext);
|
||||||
|
const { data } = report || {};
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{UTM_PARAMS.map(key => {
|
||||||
|
const items = toArray(data[key]);
|
||||||
|
const chartData = {
|
||||||
|
labels: items.map(({ name }) => name),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: items.map(({ value }) => value),
|
||||||
|
backgroundColor: CHART_COLORS,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const total = items.reduce((sum, { value }) => {
|
||||||
|
return +sum + +value;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={key} className={styles.row}>
|
||||||
|
<div>
|
||||||
|
<div className={styles.title}>{key}</div>
|
||||||
|
<ListTable
|
||||||
|
metric={formatMessage(labels.views)}
|
||||||
|
data={items.map(({ name, value }) => ({
|
||||||
|
x: name,
|
||||||
|
y: value,
|
||||||
|
z: (value / total) * 100,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<PieChart type="doughnut" data={chartData} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
10
src/app/(main)/reports/utm/page.tsx
Normal file
10
src/app/(main)/reports/utm/page.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
|
import UTMReportPage from './UTMReportPage';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
return <UTMReportPage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'UTM Report',
|
||||||
|
};
|
@ -1,16 +0,0 @@
|
|||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sticky {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
background: var(--base50);
|
|
||||||
border-bottom: 1px solid var(--base300);
|
|
||||||
z-index: 1;
|
|
||||||
padding: 10px 0;
|
|
||||||
}
|
|
@ -1,113 +0,0 @@
|
|||||||
import { useMemo, useState, useEffect } from 'react';
|
|
||||||
import { subMinutes, startOfMinute } from 'date-fns';
|
|
||||||
import thenby from 'thenby';
|
|
||||||
import { Grid, GridRow } from 'components/layout/Grid';
|
|
||||||
import Page from 'components/layout/Page';
|
|
||||||
import RealtimeChart from 'components/metrics/RealtimeChart';
|
|
||||||
import WorldMap from 'components/metrics/WorldMap';
|
|
||||||
import { useApi, useWebsite } from 'components/hooks';
|
|
||||||
import { percentFilter } from 'lib/filters';
|
|
||||||
import { REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants';
|
|
||||||
import { RealtimeData } from 'lib/types';
|
|
||||||
import RealtimeLog from './RealtimeLog';
|
|
||||||
import RealtimeHeader from './RealtimeHeader';
|
|
||||||
import RealtimeUrls from './RealtimeUrls';
|
|
||||||
import RealtimeCountries from './RealtimeCountries';
|
|
||||||
import WebsiteHeader from '../WebsiteHeader';
|
|
||||||
import styles from './Realtime.module.css';
|
|
||||||
|
|
||||||
function mergeData(state = [], data = [], time: number) {
|
|
||||||
const ids = state.map(({ id }) => id);
|
|
||||||
return state
|
|
||||||
.concat(data.filter(({ id }) => !ids.includes(id)))
|
|
||||||
.filter(({ timestamp }) => timestamp >= time);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Realtime({ websiteId }) {
|
|
||||||
const [currentData, setCurrentData] = useState<RealtimeData>();
|
|
||||||
const { get, useQuery } = useApi();
|
|
||||||
const { data: website } = useWebsite(websiteId);
|
|
||||||
const { data, isLoading, error } = useQuery({
|
|
||||||
queryKey: ['realtime', websiteId],
|
|
||||||
queryFn: () => get(`/realtime/${websiteId}`, { startAt: currentData?.timestamp || 0 }),
|
|
||||||
enabled: !!(websiteId && website),
|
|
||||||
refetchInterval: REALTIME_INTERVAL,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
const date = subMinutes(startOfMinute(new Date()), REALTIME_RANGE);
|
|
||||||
const time = date.getTime();
|
|
||||||
const { pageviews, sessions, events, timestamp } = data;
|
|
||||||
|
|
||||||
setCurrentData(state => ({
|
|
||||||
pageviews: mergeData(state?.pageviews, pageviews, time),
|
|
||||||
sessions: mergeData(state?.sessions, sessions, time),
|
|
||||||
events: mergeData(state?.events, events, time),
|
|
||||||
timestamp,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const realtimeData: RealtimeData = useMemo(() => {
|
|
||||||
if (!currentData) {
|
|
||||||
return { pageviews: [], sessions: [], events: [], countries: [], visitors: [], timestamp: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
currentData.countries = percentFilter(
|
|
||||||
currentData.sessions
|
|
||||||
.reduce((arr, data) => {
|
|
||||||
if (!arr.find(({ id }) => id === data.id)) {
|
|
||||||
return arr.concat(data);
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}, [])
|
|
||||||
.reduce((arr: { x: any; y: number }[], { country }: any) => {
|
|
||||||
if (country) {
|
|
||||||
const row = arr.find(({ x }) => x === country);
|
|
||||||
|
|
||||||
if (!row) {
|
|
||||||
arr.push({ x: country, y: 1 });
|
|
||||||
} else {
|
|
||||||
row.y += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}, [])
|
|
||||||
.sort(thenby.firstBy('y', -1)),
|
|
||||||
);
|
|
||||||
|
|
||||||
currentData.visitors = currentData.sessions.reduce((arr, val) => {
|
|
||||||
if (!arr.find(({ id }) => id === val.id)) {
|
|
||||||
return arr.concat(val);
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return currentData;
|
|
||||||
}, [currentData]);
|
|
||||||
|
|
||||||
if (isLoading || error) {
|
|
||||||
return <Page isLoading={isLoading} error={error} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<WebsiteHeader websiteId={websiteId} />
|
|
||||||
<RealtimeHeader data={realtimeData} />
|
|
||||||
<RealtimeChart className={styles.chart} data={realtimeData} unit="minute" />
|
|
||||||
<Grid>
|
|
||||||
<GridRow columns="one-two">
|
|
||||||
<RealtimeUrls websiteDomain={website?.domain} data={realtimeData} />
|
|
||||||
<RealtimeLog websiteDomain={website?.domain} data={realtimeData} />
|
|
||||||
</GridRow>
|
|
||||||
<GridRow columns="one-two">
|
|
||||||
<RealtimeCountries data={realtimeData?.countries} />
|
|
||||||
<WorldMap data={realtimeData?.countries} />
|
|
||||||
</GridRow>
|
|
||||||
</Grid>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Realtime;
|
|
@ -35,11 +35,6 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.website {
|
|
||||||
text-align: right;
|
|
||||||
padding: 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail {
|
.detail {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useContext, useMemo, useState } from 'react';
|
||||||
import { StatusLight, Icon, Text, SearchField } from 'react-basics';
|
import { StatusLight, Icon, Text, SearchField } from 'react-basics';
|
||||||
import { FixedSizeList } from 'react-window';
|
import { FixedSizeList } from 'react-window';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
@ -11,6 +11,8 @@ import Icons from 'components/icons';
|
|||||||
import useFormat from 'components//hooks/useFormat';
|
import useFormat from 'components//hooks/useFormat';
|
||||||
import { BROWSERS } from 'lib/constants';
|
import { BROWSERS } from 'lib/constants';
|
||||||
import { stringToColor } from 'lib/format';
|
import { stringToColor } from 'lib/format';
|
||||||
|
import { RealtimeData } from 'lib/types';
|
||||||
|
import { WebsiteContext } from '../WebsiteProvider';
|
||||||
import styles from './RealtimeLog.module.css';
|
import styles from './RealtimeLog.module.css';
|
||||||
|
|
||||||
const TYPE_ALL = 'all';
|
const TYPE_ALL = 'all';
|
||||||
@ -24,7 +26,8 @@ const icons = {
|
|||||||
[TYPE_EVENT]: <Icons.Bolt />,
|
[TYPE_EVENT]: <Icons.Bolt />,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RealtimeLog({ data, websiteDomain }) {
|
export function RealtimeLog({ data }: { data: RealtimeData }) {
|
||||||
|
const website = useContext(WebsiteContext);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const { formatMessage, labels, messages, FormattedMessage } = useMessages();
|
const { formatMessage, labels, messages, FormattedMessage } = useMessages();
|
||||||
const { formatValue } = useFormat();
|
const { formatValue } = useFormat();
|
||||||
@ -76,7 +79,7 @@ export function RealtimeLog({ data, websiteDomain }) {
|
|||||||
event: <b>{eventName || formatMessage(labels.unknown)}</b>,
|
event: <b>{eventName || formatMessage(labels.unknown)}</b>,
|
||||||
url: (
|
url: (
|
||||||
<a
|
<a
|
||||||
href={`//${websiteDomain}${url}`}
|
href={`//${website?.domain}${url}`}
|
||||||
className={styles.link}
|
className={styles.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
@ -92,7 +95,7 @@ export function RealtimeLog({ data, websiteDomain }) {
|
|||||||
if (__type === TYPE_PAGEVIEW) {
|
if (__type === TYPE_PAGEVIEW) {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={`//${websiteDomain}${url}`}
|
href={`//${website?.domain}${url}`}
|
||||||
className={styles.link}
|
className={styles.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Key, useMemo, useState } from 'react';
|
import { Key, useContext, useMemo, useState } from 'react';
|
||||||
import { ButtonGroup, Button, Flexbox } from 'react-basics';
|
import { ButtonGroup, Button, Flexbox } from 'react-basics';
|
||||||
import thenby from 'thenby';
|
import thenby from 'thenby';
|
||||||
import { percentFilter } from 'lib/filters';
|
import { percentFilter } from 'lib/filters';
|
||||||
@ -6,14 +6,10 @@ import ListTable from 'components/metrics/ListTable';
|
|||||||
import { FILTER_PAGES, FILTER_REFERRERS } from 'lib/constants';
|
import { FILTER_PAGES, FILTER_REFERRERS } from 'lib/constants';
|
||||||
import { useMessages } from 'components/hooks';
|
import { useMessages } from 'components/hooks';
|
||||||
import { RealtimeData } from 'lib/types';
|
import { RealtimeData } from 'lib/types';
|
||||||
|
import { WebsiteContext } from '../WebsiteProvider';
|
||||||
|
|
||||||
export function RealtimeUrls({
|
export function RealtimeUrls({ data }: { data: RealtimeData }) {
|
||||||
websiteDomain,
|
const website = useContext(WebsiteContext);
|
||||||
data,
|
|
||||||
}: {
|
|
||||||
websiteDomain: string;
|
|
||||||
data: RealtimeData;
|
|
||||||
}) {
|
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { pageviews } = data || {};
|
const { pageviews } = data || {};
|
||||||
const [filter, setFilter] = useState<Key>(FILTER_REFERRERS);
|
const [filter, setFilter] = useState<Key>(FILTER_REFERRERS);
|
||||||
@ -31,7 +27,7 @@ export function RealtimeUrls({
|
|||||||
];
|
];
|
||||||
|
|
||||||
const renderLink = ({ x }) => {
|
const renderLink = ({ x }) => {
|
||||||
const domain = x.startsWith('/') ? websiteDomain : '';
|
const domain = x.startsWith('/') ? website?.domain : '';
|
||||||
return (
|
return (
|
||||||
<a href={`//${domain}${x}`} target="_blank" rel="noreferrer noopener">
|
<a href={`//${domain}${x}`} target="_blank" rel="noreferrer noopener">
|
||||||
{x}
|
{x}
|
||||||
|
@ -1,6 +1,40 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import Realtime from './Realtime';
|
import { Grid, GridRow } from 'components/layout/Grid';
|
||||||
|
import Page from 'components/layout/Page';
|
||||||
|
import RealtimeChart from 'components/metrics/RealtimeChart';
|
||||||
|
import WorldMap from 'components/metrics/WorldMap';
|
||||||
|
import { useRealtime } from 'components/hooks';
|
||||||
|
import RealtimeLog from './RealtimeLog';
|
||||||
|
import RealtimeHeader from './RealtimeHeader';
|
||||||
|
import RealtimeUrls from './RealtimeUrls';
|
||||||
|
import RealtimeCountries from './RealtimeCountries';
|
||||||
|
import WebsiteHeader from '../WebsiteHeader';
|
||||||
|
import WebsiteProvider from '../WebsiteProvider';
|
||||||
|
|
||||||
export default function WebsiteRealtimePage({ websiteId }) {
|
export function WebsiteRealtimePage({ websiteId }) {
|
||||||
return <Realtime websiteId={websiteId} />;
|
const { data, isLoading, error } = useRealtime(websiteId);
|
||||||
|
|
||||||
|
if (isLoading || error) {
|
||||||
|
return <Page isLoading={isLoading} error={error} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WebsiteProvider websiteId={websiteId}>
|
||||||
|
<WebsiteHeader websiteId={websiteId} />
|
||||||
|
<RealtimeHeader data={data} />
|
||||||
|
<RealtimeChart data={data} unit="minute" />
|
||||||
|
<Grid>
|
||||||
|
<GridRow columns="one-two">
|
||||||
|
<RealtimeUrls data={data} />
|
||||||
|
<RealtimeLog data={data} />
|
||||||
|
</GridRow>
|
||||||
|
<GridRow columns="one-two">
|
||||||
|
<RealtimeCountries data={data?.countries} />
|
||||||
|
<WorldMap data={data?.countries} />
|
||||||
|
</GridRow>
|
||||||
|
</Grid>
|
||||||
|
</WebsiteProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default WebsiteRealtimePage;
|
||||||
|
1
src/assets/bookmark.svg
Normal file
1
src/assets/bookmark.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg height="512" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg"><path d="M3.515 22.875a1 1 0 0 0 1.015-.027L12 18.179l7.47 4.669A1 1 0 0 0 21 22V4a3 3 0 0 0-3-3H6a3 3 0 0 0-3 3v18a1 1 0 0 0 .515.875zM5 4a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v16.2l-6.47-4.044a1 1 0 0 0-1.06 0L5 20.2z"/></svg>
|
After Width: | Height: | Size: 306 B |
1
src/assets/flag.svg
Normal file
1
src/assets/flag.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg height="512" viewBox="0 0 510 510" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m393.159 121.41 69.152-86.44c-16.753-2.022-149.599-37.363-282.234-8.913V0h-30v361.898c-25.85 6.678-45 30.195-45 58.102v1.509c-34.191 6.969-60 37.272-60 73.491v15h240v-15c0-36.22-25.809-66.522-60-73.491V420c0-27.906-19.15-51.424-45-58.102V237.165c153.335-30.989 264.132 7.082 284.847 9.834zM252.506 480H77.647c6.19-17.461 22.873-30 42.43-30h90c19.556 0 36.238 12.539 42.429 30zm-57.429-60h-60c0-16.542 13.458-30 30-30s30 13.458 30 30zm-15-213.427V56.771c66.329-15.269 141.099-15.756 227.537-1.455l-50.619 63.274 48.8 85.4c-75.047-12.702-150.759-11.841-225.718 2.583z"/></svg>
|
After Width: | Height: | Size: 670 B |
1
src/assets/speaker.svg
Normal file
1
src/assets/speaker.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M232.011 88.828c-5.664-5.664-13.217-8.784-21.269-8.784s-15.605 3.12-21.269 8.783c-9.917 9.917-11.446 25.09-4.593 36.632-23.293 86.372-34.167 96.094-78.604 135.776-15.831 14.138-35.533 31.731-61.302 57.5-5.434 5.434-8.426 12.673-8.426 20.383s2.993 14.949 8.426 20.383l70.981 70.98c5.434 5.435 12.672 8.427 20.382 8.427a28.7 28.7 0 0 0 14.046-3.637l72.768 72.768c2.574 2.574 6.09 3.962 9.896 3.961.789 0 1.59-.06 2.398-.181 3.883-.581 7.662-2.543 10.641-5.521l25.329-25.329c6.918-6.919 7.684-16.993 1.741-22.936l-39.164-39.164c11.586-20.762 9.203-46.431-6.187-64.762 29.684-32.251 46.532-43.128 122.192-63.532a30.076 30.076 0 0 0 15.361 4.203c7.703 0 15.405-2.933 21.269-8.796 11.728-11.729 11.728-30.811 0-42.539zM127.268 419.167l-70.981-70.981c-2.412-2.411-3.74-5.632-3.74-9.068s1.328-6.657 3.74-9.068c17.786-17.786 32.665-31.645 45.371-43.163l86.911 86.911c-11.519 12.706-25.378 27.585-43.164 45.371-2.412 2.411-5.632 3.74-9.068 3.74-3.437-.001-6.657-1.33-9.069-3.742zM260.1 469.653l-25.33 25.33a4.096 4.096 0 0 1-1.197.85L162.45 424.71a1243.745 1243.745 0 0 0 26.786-27.968l71.714 71.713a4.047 4.047 0 0 1-.85 1.198zm-38.055-62.731-21.982-21.981a2607.916 2607.916 0 0 0 14.157-15.763l2.712-3.035c8.895 11.831 10.752 27.329 5.113 40.779zm-19.759-48.401-3.004 3.362-85.711-85.711 3.361-3.003c44.419-39.665 57.85-51.661 80.687-133.656l138.322 138.322c-81.993 22.837-93.99 36.268-133.655 80.686zm173.027-83.854c-5.489 5.49-14.422 5.49-19.911 0L200.786 120.052c-5.489-5.489-5.489-14.421 0-19.91 2.642-2.643 6.178-4.098 9.956-4.098s7.313 1.455 9.955 4.098l154.616 154.615c5.489 5.489 5.489 14.421 0 19.91zm-22.558-151.968a8 8 0 0 1 0-11.314l43.904-43.904a8 8 0 0 1 11.313 11.314l-43.904 43.904c-1.562 1.562-3.609 2.343-5.657 2.343s-4.094-.781-5.656-2.343zm122.699 107.695a8 8 0 0 1-8 8h-62.09a8 8 0 0 1 0-16h62.09a8 8 0 0 1 8 8zM237.061 70.09V8a8 8 0 0 1 16 0v62.09a8 8 0 0 1-16 0z"/></svg>
|
After Width: | Height: | Size: 1.9 KiB |
1
src/assets/tag.svg
Normal file
1
src/assets/tag.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg height="437pt" viewBox="0 0 437.004 437" width="437pt" xmlns="http://www.w3.org/2000/svg"><path d="M229 14.645A50.173 50.173 0 0 0 192.371.015L52.293 3.586C25.672 4.25 4.246 25.673 3.582 52.298L.016 192.37a50.215 50.215 0 0 0 14.625 36.633l193.367 193.36c19.539 19.495 51.168 19.495 70.707 0l143.644-143.645c19.528-19.524 19.528-51.184 0-70.711zm179.219 249.933-143.645 143.64c-11.722 11.7-30.703 11.7-42.426 0L28.785 214.86a30.131 30.131 0 0 1-8.777-21.98l3.566-140.074c.403-15.973 13.254-28.828 29.227-29.227l140.074-3.57c.254-.004.5-.008.754-.008a30.129 30.129 0 0 1 21.223 8.79l193.367 193.362c11.695 11.723 11.695 30.703 0 42.426zm0 0"/><path d="M130.719 82.574c-26.59 0-48.145 21.555-48.149 48.145 0 26.59 21.559 48.144 48.145 48.144 26.59 0 48.144-21.554 48.144-48.144-.03-26.574-21.566-48.114-48.14-48.145zm0 76.29c-15.547 0-28.145-12.602-28.149-28.145 0-15.543 12.602-28.145 28.145-28.145s28.144 12.602 28.144 28.145c-.015 15.535-12.605 28.125-28.14 28.144zm0 0"/></svg>
|
After Width: | Height: | Size: 984 B |
@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve"><path d="M256 0c-74.439 0-135 60.561-135 135s60.561 135 135 135 135-60.561 135-135S330.439 0 256 0zM423.966 358.195C387.006 320.667 338.009 300 286 300h-60c-52.008 0-101.006 20.667-137.966 58.195C51.255 395.539 31 444.833 31 497c0 8.284 6.716 15 15 15h420c8.284 0 15-6.716 15-15 0-52.167-20.255-101.461-57.034-138.805z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve"><path d="M256 0c-74.439 0-135 60.561-135 135s60.561 135 135 135 135-60.561 135-135S330.439 0 256 0zm167.966 358.195C387.006 320.667 338.009 300 286 300h-60c-52.008 0-101.006 20.667-137.966 58.195C51.255 395.539 31 444.833 31 497c0 8.284 6.716 15 15 15h420c8.284 0 15-6.716 15-15 0-52.167-20.255-101.461-57.034-138.805z"/></svg>
|
Before Width: | Height: | Size: 452 B After Width: | Height: | Size: 452 B |
85
src/components/charts/BarChart.tsx
Normal file
85
src/components/charts/BarChart.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { useTheme } from 'components/hooks';
|
||||||
|
import Chart, { ChartProps } from 'components/charts/Chart';
|
||||||
|
import { renderNumberLabels } from 'lib/charts';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import BarChartTooltip from 'components/charts/BarChartTooltip';
|
||||||
|
|
||||||
|
export interface BarChartProps extends ChartProps {
|
||||||
|
unit: string;
|
||||||
|
stacked?: boolean;
|
||||||
|
renderXLabel?: (label: string, index: number, values: any[]) => string;
|
||||||
|
renderYLabel?: (label: string, index: number, values: any[]) => string;
|
||||||
|
XAxisType?: string;
|
||||||
|
YAxisType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BarChart(props: BarChartProps) {
|
||||||
|
const [tooltip, setTooltip] = useState(null);
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const {
|
||||||
|
renderXLabel,
|
||||||
|
renderYLabel,
|
||||||
|
unit,
|
||||||
|
XAxisType = 'time',
|
||||||
|
YAxisType = 'linear',
|
||||||
|
stacked = false,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: XAxisType,
|
||||||
|
stacked: true,
|
||||||
|
time: {
|
||||||
|
unit,
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
color: colors.chart.line,
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: colors.chart.text,
|
||||||
|
autoSkip: false,
|
||||||
|
maxRotation: 0,
|
||||||
|
callback: renderXLabel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
type: YAxisType,
|
||||||
|
min: 0,
|
||||||
|
beginAtZero: true,
|
||||||
|
stacked,
|
||||||
|
grid: {
|
||||||
|
color: colors.chart.line,
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
color: colors.chart.line,
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: colors.chart.text,
|
||||||
|
callback: renderYLabel || renderNumberLabels,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTooltip = ({ tooltip }: { tooltip: any }) => {
|
||||||
|
const { opacity } = tooltip;
|
||||||
|
|
||||||
|
setTooltip(opacity ? <BarChartTooltip tooltip={tooltip} unit={unit} /> : null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Chart
|
||||||
|
{...props}
|
||||||
|
type="bar"
|
||||||
|
chartOptions={options}
|
||||||
|
tooltip={tooltip}
|
||||||
|
onTooltip={handleTooltip}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BarChart;
|
32
src/components/charts/BarChartTooltip.tsx
Normal file
32
src/components/charts/BarChartTooltip.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { formatDate } from 'lib/date';
|
||||||
|
import { Flexbox, StatusLight } from 'react-basics';
|
||||||
|
import { formatLongNumber } from 'lib/format';
|
||||||
|
import { useLocale } from 'components/hooks';
|
||||||
|
|
||||||
|
const formats = {
|
||||||
|
millisecond: 'T',
|
||||||
|
second: 'pp',
|
||||||
|
minute: 'p',
|
||||||
|
hour: 'h:mm aaa - PP',
|
||||||
|
day: 'PPPP',
|
||||||
|
week: 'PPPP',
|
||||||
|
month: 'LLLL yyyy',
|
||||||
|
quarter: 'qqq',
|
||||||
|
year: 'yyyy',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function BarChartTooltip({ tooltip, unit }) {
|
||||||
|
const { locale } = useLocale();
|
||||||
|
const { labelColors, dataPoints } = tooltip;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flexbox direction="column" gap={10}>
|
||||||
|
<div>{formatDate(new Date(dataPoints[0].raw.x), formats[unit], locale)}</div>
|
||||||
|
<div>
|
||||||
|
<StatusLight color={labelColors?.[0]?.backgroundColor}>
|
||||||
|
{formatLongNumber(dataPoints[0].raw.y)} {dataPoints[0].dataset.label}
|
||||||
|
</StatusLight>
|
||||||
|
</div>
|
||||||
|
</Flexbox>
|
||||||
|
);
|
||||||
|
}
|
@ -1,7 +1,3 @@
|
|||||||
.container {
|
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart {
|
.chart {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 400px;
|
height: 400px;
|
||||||
@ -13,7 +9,3 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip .value {
|
|
||||||
text-transform: lowercase;
|
|
||||||
}
|
|
141
src/components/charts/Chart.tsx
Normal file
141
src/components/charts/Chart.tsx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import { useState, useRef, useEffect, ReactNode } from 'react';
|
||||||
|
import { Loading } from 'react-basics';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import ChartJS, { LegendItem } from 'chart.js/auto';
|
||||||
|
import HoverTooltip from 'components/common/HoverTooltip';
|
||||||
|
import Legend from 'components/metrics/Legend';
|
||||||
|
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
||||||
|
import styles from './Chart.module.css';
|
||||||
|
|
||||||
|
export interface ChartProps {
|
||||||
|
type?: 'bar' | 'bubble' | 'doughnut' | 'pie' | 'line' | 'polarArea' | 'radar' | 'scatter';
|
||||||
|
data?: object;
|
||||||
|
isLoading?: boolean;
|
||||||
|
animationDuration?: number;
|
||||||
|
updateMode?: string;
|
||||||
|
onCreate?: (chart: any) => void;
|
||||||
|
onUpdate?: (chart: any) => void;
|
||||||
|
onTooltip?: (model: any) => void;
|
||||||
|
className?: string;
|
||||||
|
chartOptions?: { [key: string]: any };
|
||||||
|
tooltip?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Chart({
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
isLoading = false,
|
||||||
|
animationDuration = DEFAULT_ANIMATION_DURATION,
|
||||||
|
tooltip,
|
||||||
|
updateMode,
|
||||||
|
onCreate,
|
||||||
|
onUpdate,
|
||||||
|
onTooltip,
|
||||||
|
className,
|
||||||
|
chartOptions,
|
||||||
|
}: ChartProps) {
|
||||||
|
const canvas = useRef();
|
||||||
|
const chart = useRef(null);
|
||||||
|
const [legendItems, setLegendItems] = useState([]);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
animation: {
|
||||||
|
duration: animationDuration,
|
||||||
|
resize: {
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: false,
|
||||||
|
external: onTooltip,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...chartOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
const createChart = (data: any) => {
|
||||||
|
ChartJS.defaults.font.family = 'Inter';
|
||||||
|
|
||||||
|
chart.current = new ChartJS(canvas.current, {
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
options,
|
||||||
|
});
|
||||||
|
|
||||||
|
onCreate?.(chart.current);
|
||||||
|
|
||||||
|
setLegendItems(chart.current.legend.legendItems);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateChart = (data: any) => {
|
||||||
|
chart.current.data.datasets.forEach((dataset: { data: any }, index: string | number) => {
|
||||||
|
dataset.data = data?.datasets[index]?.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
chart.current.options = options;
|
||||||
|
|
||||||
|
// Allow config changes before update
|
||||||
|
onUpdate?.(chart.current);
|
||||||
|
|
||||||
|
chart.current.update(updateMode);
|
||||||
|
|
||||||
|
setLegendItems(chart.current.legend.legendItems);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
if (!chart.current) {
|
||||||
|
createChart(data);
|
||||||
|
} else {
|
||||||
|
updateChart(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const handleLegendClick = (item: LegendItem) => {
|
||||||
|
if (type === 'bar') {
|
||||||
|
const { datasetIndex } = item;
|
||||||
|
const meta = chart.current.getDatasetMeta(datasetIndex);
|
||||||
|
|
||||||
|
meta.hidden =
|
||||||
|
meta.hidden === null ? !chart.current.data.datasets[datasetIndex]?.hidden : null;
|
||||||
|
} else {
|
||||||
|
const { index } = item;
|
||||||
|
const meta = chart.current.getDatasetMeta(0);
|
||||||
|
const hidden = !!meta.data[index].hidden;
|
||||||
|
|
||||||
|
meta.data[index].hidden = !hidden;
|
||||||
|
chart.current.legend.legendItems[index].hidden = !hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.current.update(updateMode);
|
||||||
|
|
||||||
|
setLegendItems(chart.current.legend.legendItems);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={classNames(styles.chart, className)}>
|
||||||
|
{isLoading && <Loading position="page" icon="dots" />}
|
||||||
|
<canvas ref={canvas} />
|
||||||
|
</div>
|
||||||
|
<Legend items={legendItems} onClick={handleLegendClick} />
|
||||||
|
{tooltip && (
|
||||||
|
<HoverTooltip>
|
||||||
|
<div className={styles.tooltip}>{tooltip}</div>
|
||||||
|
</HoverTooltip>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Chart;
|
27
src/components/charts/PieChart.tsx
Normal file
27
src/components/charts/PieChart.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Chart, ChartProps } from 'components/charts/Chart';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { StatusLight } from 'react-basics';
|
||||||
|
import { formatLongNumber } from 'lib/format';
|
||||||
|
|
||||||
|
export interface PieChartProps extends ChartProps {
|
||||||
|
type?: 'doughnut' | 'pie';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PieChart(props: PieChartProps) {
|
||||||
|
const [tooltip, setTooltip] = useState(null);
|
||||||
|
const { type } = props;
|
||||||
|
|
||||||
|
const handleTooltip = ({ tooltip }) => {
|
||||||
|
const { labelColors, dataPoints } = tooltip;
|
||||||
|
|
||||||
|
setTooltip(
|
||||||
|
tooltip.opacity ? (
|
||||||
|
<StatusLight color={labelColors?.[0]?.backgroundColor}>
|
||||||
|
{formatLongNumber(dataPoints?.[0]?.raw)} {dataPoints?.[0]?.label}
|
||||||
|
</StatusLight>
|
||||||
|
) : null,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Chart {...props} type={type || 'pie'} tooltip={tooltip} onTooltip={handleTooltip} />;
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
.bar {
|
.bar {
|
||||||
font-size: 11px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--base600);
|
color: var(--base600);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Flexbox, Icon, Icons, Text } from 'react-basics';
|
import { Flexbox, Icon, Icons, Text } from 'react-basics';
|
||||||
import styles from './Breadcrumb.module.css';
|
import styles from './Breadcrumb.module.css';
|
||||||
|
import { Fragment } from 'react';
|
||||||
|
|
||||||
export interface BreadcrumbProps {
|
export interface BreadcrumbProps {
|
||||||
data: {
|
data: {
|
||||||
@ -14,7 +15,7 @@ export function Breadcrumb({ data }: BreadcrumbProps) {
|
|||||||
<Flexbox alignItems="center" gap={3} className={styles.bar}>
|
<Flexbox alignItems="center" gap={3} className={styles.bar}>
|
||||||
{data.map((a, i) => {
|
{data.map((a, i) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<Fragment key={i}>
|
||||||
{a.url ? (
|
{a.url ? (
|
||||||
<Link href={a.url} className={styles.link}>
|
<Link href={a.url} className={styles.link}>
|
||||||
<Text>{a.label}</Text>
|
<Text>{a.label}</Text>
|
||||||
@ -27,7 +28,7 @@ export function Breadcrumb({ data }: BreadcrumbProps) {
|
|||||||
<Icons.ChevronDown />
|
<Icons.ChevronDown />
|
||||||
</Icon>
|
</Icon>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import { useMessages, useNavigation } from 'components/hooks';
|
||||||
|
import { safeDecodeURIComponent } from 'next-basics';
|
||||||
|
import Link from 'next/link';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Icon, Icons } from 'react-basics';
|
import { Icon, Icons } from 'react-basics';
|
||||||
import classNames from 'classnames';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { safeDecodeURI } from 'next-basics';
|
|
||||||
import { useNavigation } from 'components/hooks';
|
|
||||||
import { useMessages } from 'components/hooks';
|
|
||||||
import styles from './FilterLink.module.css';
|
import styles from './FilterLink.module.css';
|
||||||
|
|
||||||
export interface FilterLinkProps {
|
export interface FilterLinkProps {
|
||||||
@ -40,7 +39,7 @@ export function FilterLink({
|
|||||||
{!value && `(${label || formatMessage(labels.unknown)})`}
|
{!value && `(${label || formatMessage(labels.unknown)})`}
|
||||||
{value && (
|
{value && (
|
||||||
<Link href={renderUrl({ [id]: value })} className={styles.label} replace>
|
<Link href={renderUrl({ [id]: value })} className={styles.label} replace>
|
||||||
{safeDecodeURI(label || value)}
|
{safeDecodeURIComponent(label || value)}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{externalUrl && (
|
{externalUrl && (
|
||||||
|
@ -2,6 +2,7 @@ export * from './queries/useApi';
|
|||||||
export * from './queries/useConfig';
|
export * from './queries/useConfig';
|
||||||
export * from './queries/useFilterQuery';
|
export * from './queries/useFilterQuery';
|
||||||
export * from './queries/useLogin';
|
export * from './queries/useLogin';
|
||||||
|
export * from './queries/useRealtime';
|
||||||
export * from './queries/useReport';
|
export * from './queries/useReport';
|
||||||
export * from './queries/useReports';
|
export * from './queries/useReports';
|
||||||
export * from './queries/useShareToken';
|
export * from './queries/useShareToken';
|
||||||
|
87
src/components/hooks/queries/useRealtime.ts
Normal file
87
src/components/hooks/queries/useRealtime.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { useMemo, useRef } from 'react';
|
||||||
|
import { RealtimeData } from 'lib/types';
|
||||||
|
import { useApi } from 'components/hooks';
|
||||||
|
import { REALTIME_INTERVAL, REALTIME_RANGE } from 'lib/constants';
|
||||||
|
import { startOfMinute, subMinutes } from 'date-fns';
|
||||||
|
import { percentFilter } from 'lib/filters';
|
||||||
|
import thenby from 'thenby';
|
||||||
|
|
||||||
|
function mergeData(state = [], data = [], time: number) {
|
||||||
|
const ids = state.map(({ id }) => id);
|
||||||
|
return state
|
||||||
|
.concat(data.filter(({ id }) => !ids.includes(id)))
|
||||||
|
.filter(({ timestamp }) => timestamp >= time);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRealtime(websiteId: string) {
|
||||||
|
const currentData = useRef({
|
||||||
|
pageviews: [],
|
||||||
|
sessions: [],
|
||||||
|
events: [],
|
||||||
|
countries: [],
|
||||||
|
visitors: [],
|
||||||
|
timestamp: 0,
|
||||||
|
});
|
||||||
|
const { get, useQuery } = useApi();
|
||||||
|
const { data, isLoading, error } = useQuery<RealtimeData>({
|
||||||
|
queryKey: ['realtime', websiteId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const state = currentData.current;
|
||||||
|
const data = await get(`/realtime/${websiteId}`, { startAt: state?.timestamp || 0 });
|
||||||
|
const date = subMinutes(startOfMinute(new Date()), REALTIME_RANGE);
|
||||||
|
const time = date.getTime();
|
||||||
|
const { pageviews, sessions, events, timestamp } = data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageviews: mergeData(state?.pageviews, pageviews, time),
|
||||||
|
sessions: mergeData(state?.sessions, sessions, time),
|
||||||
|
events: mergeData(state?.events, events, time),
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
enabled: !!websiteId,
|
||||||
|
refetchInterval: REALTIME_INTERVAL,
|
||||||
|
});
|
||||||
|
|
||||||
|
const realtimeData: RealtimeData = useMemo(() => {
|
||||||
|
if (!data) {
|
||||||
|
return { pageviews: [], sessions: [], events: [], countries: [], visitors: [], timestamp: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
data.countries = percentFilter(
|
||||||
|
data.sessions
|
||||||
|
.reduce((arr, data) => {
|
||||||
|
if (!arr.find(({ id }) => id === data.id)) {
|
||||||
|
return arr.concat(data);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}, [])
|
||||||
|
.reduce((arr: { x: any; y: number }[], { country }: any) => {
|
||||||
|
if (country) {
|
||||||
|
const row = arr.find(({ x }) => x === country);
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
arr.push({ x: country, y: 1 });
|
||||||
|
} else {
|
||||||
|
row.y += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}, [])
|
||||||
|
.sort(thenby.firstBy('y', -1)),
|
||||||
|
);
|
||||||
|
|
||||||
|
data.visitors = data.sessions.reduce((arr, val) => {
|
||||||
|
if (!arr.find(({ id }) => id === val.id)) {
|
||||||
|
return arr.concat(val);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return { data: realtimeData, isLoading, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useRealtime;
|
@ -4,7 +4,7 @@ import { useApi } from './useApi';
|
|||||||
import { useTimezone } from '../useTimezone';
|
import { useTimezone } from '../useTimezone';
|
||||||
import { useMessages } from '../useMessages';
|
import { useMessages } from '../useMessages';
|
||||||
|
|
||||||
export function useReport(reportId: string, defaultParameters: { [key: string]: any }) {
|
export function useReport(reportId: string, defaultParameters: { [key: string]: any } = {}) {
|
||||||
const [report, setReport] = useState(null);
|
const [report, setReport] = useState(null);
|
||||||
const [isRunning, setIsRunning] = useState(false);
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
const { get, post } = useApi();
|
const { get, post } = useApi();
|
||||||
|
@ -1,19 +1,13 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import useStore, { setTheme } from 'store/app';
|
import useStore, { setTheme } from 'store/app';
|
||||||
import { getItem, setItem } from 'next-basics';
|
import { getItem, setItem } from 'next-basics';
|
||||||
import { THEME_COLORS, THEME_CONFIG } from 'lib/constants';
|
import { DEFAULT_THEME, THEME_COLORS, THEME_CONFIG } from 'lib/constants';
|
||||||
import { colord } from 'colord';
|
import { colord } from 'colord';
|
||||||
|
|
||||||
const selector = (state: { theme: string }) => state.theme;
|
const selector = (state: { theme: string }) => state.theme;
|
||||||
|
|
||||||
export function useTheme() {
|
export function useTheme() {
|
||||||
const defaultTheme =
|
const theme = useStore(selector) || getItem(THEME_CONFIG) || DEFAULT_THEME;
|
||||||
typeof window !== 'undefined'
|
|
||||||
? window?.matchMedia('(prefers-color-scheme: dark)')?.matches
|
|
||||||
? 'dark'
|
|
||||||
: 'light'
|
|
||||||
: 'light';
|
|
||||||
const theme = useStore(selector) || getItem(THEME_CONFIG) || defaultTheme;
|
|
||||||
const primaryColor = colord(THEME_COLORS[theme].primary);
|
const primaryColor = colord(THEME_COLORS[theme].primary);
|
||||||
|
|
||||||
const colors = {
|
const colors = {
|
||||||
|
@ -32,18 +32,14 @@ export function DateFilter({
|
|||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
|
|
||||||
const options = [
|
const options = [
|
||||||
{ label: formatMessage(labels.today), value: '1day' },
|
{ label: formatMessage(labels.today), value: '0day' },
|
||||||
{
|
{
|
||||||
label: formatMessage(labels.lastHours, { x: 24 }),
|
label: formatMessage(labels.lastHours, { x: 24 }),
|
||||||
value: '24hour',
|
value: '24hour',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: formatMessage(labels.yesterday),
|
|
||||||
value: '-1day',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: formatMessage(labels.thisWeek),
|
label: formatMessage(labels.thisWeek),
|
||||||
value: '1week',
|
value: '0week',
|
||||||
divider: true,
|
divider: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -52,7 +48,7 @@ export function DateFilter({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: formatMessage(labels.thisMonth),
|
label: formatMessage(labels.thisMonth),
|
||||||
value: '1month',
|
value: '0month',
|
||||||
divider: true,
|
divider: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -63,7 +59,7 @@ export function DateFilter({
|
|||||||
label: formatMessage(labels.lastDays, { x: 90 }),
|
label: formatMessage(labels.lastDays, { x: 90 }),
|
||||||
value: '90day',
|
value: '90day',
|
||||||
},
|
},
|
||||||
{ label: formatMessage(labels.thisYear), value: '1year' },
|
{ label: formatMessage(labels.thisYear), value: '0year' },
|
||||||
showAllTime && {
|
showAllTime && {
|
||||||
label: formatMessage(labels.allTime),
|
label: formatMessage(labels.allTime),
|
||||||
value: 'all',
|
value: 'all',
|
||||||
|
@ -15,6 +15,7 @@ export const labels = defineMessages({
|
|||||||
username: { id: 'label.username', defaultMessage: 'Username' },
|
username: { id: 'label.username', defaultMessage: 'Username' },
|
||||||
password: { id: 'label.password', defaultMessage: 'Password' },
|
password: { id: 'label.password', defaultMessage: 'Password' },
|
||||||
role: { id: 'label.role', defaultMessage: 'Role' },
|
role: { id: 'label.role', defaultMessage: 'Role' },
|
||||||
|
admin: { id: 'label.admin', defaultMessage: 'Admin' },
|
||||||
user: { id: 'label.user', defaultMessage: 'User' },
|
user: { id: 'label.user', defaultMessage: 'User' },
|
||||||
viewOnly: { id: 'label.view-only', defaultMessage: 'View only' },
|
viewOnly: { id: 'label.view-only', defaultMessage: 'View only' },
|
||||||
manage: { id: 'label.manage', defaultMessage: 'Manage' },
|
manage: { id: 'label.manage', defaultMessage: 'Manage' },
|
||||||
@ -222,6 +223,11 @@ export const labels = defineMessages({
|
|||||||
id: 'message.visitors-dropped-off',
|
id: 'message.visitors-dropped-off',
|
||||||
defaultMessage: 'Visitors dropped off',
|
defaultMessage: 'Visitors dropped off',
|
||||||
},
|
},
|
||||||
|
utm: { id: 'label.utm', defaultMessage: 'UTM' },
|
||||||
|
utmDescription: {
|
||||||
|
id: 'label.utm-description',
|
||||||
|
defaultMessage: 'Track your campaigns through UTM parameters.',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
|
@ -1,174 +0,0 @@
|
|||||||
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
||||||
import { Loading } from 'react-basics';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import Chart from 'chart.js/auto';
|
|
||||||
import HoverTooltip from 'components/common/HoverTooltip';
|
|
||||||
import Legend from 'components/metrics/Legend';
|
|
||||||
import { useLocale } from 'components/hooks';
|
|
||||||
import { useTheme } from 'components/hooks';
|
|
||||||
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
|
||||||
import { renderNumberLabels } from 'lib/charts';
|
|
||||||
import styles from './BarChart.module.css';
|
|
||||||
|
|
||||||
export interface BarChartProps {
|
|
||||||
datasets?: any[];
|
|
||||||
unit?: string;
|
|
||||||
animationDuration?: number;
|
|
||||||
stacked?: boolean;
|
|
||||||
isLoading?: boolean;
|
|
||||||
renderXLabel?: (label: string, index: number, values: any[]) => string;
|
|
||||||
renderYLabel?: (label: string, index: number, values: any[]) => string;
|
|
||||||
XAxisType?: string;
|
|
||||||
YAxisType?: string;
|
|
||||||
renderTooltipPopup?: (setTooltipPopup: (data: any) => void, model: any) => void;
|
|
||||||
onCreate?: (chart: any) => void;
|
|
||||||
onUpdate?: (chart: any) => void;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BarChart({
|
|
||||||
datasets = [],
|
|
||||||
unit,
|
|
||||||
animationDuration = DEFAULT_ANIMATION_DURATION,
|
|
||||||
stacked = false,
|
|
||||||
isLoading = false,
|
|
||||||
renderXLabel,
|
|
||||||
renderYLabel,
|
|
||||||
XAxisType = 'time',
|
|
||||||
YAxisType = 'linear',
|
|
||||||
renderTooltipPopup,
|
|
||||||
onCreate,
|
|
||||||
onUpdate,
|
|
||||||
className,
|
|
||||||
}: BarChartProps) {
|
|
||||||
const canvas = useRef();
|
|
||||||
const chart = useRef(null);
|
|
||||||
const [tooltip, setTooltipPopup] = useState(null);
|
|
||||||
const { locale } = useLocale();
|
|
||||||
const { theme, colors } = useTheme();
|
|
||||||
|
|
||||||
const getOptions = useCallback(() => {
|
|
||||||
return {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
animation: {
|
|
||||||
duration: animationDuration,
|
|
||||||
resize: {
|
|
||||||
duration: 0,
|
|
||||||
},
|
|
||||||
active: {
|
|
||||||
duration: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
enabled: false,
|
|
||||||
external: renderTooltipPopup ? renderTooltipPopup.bind(null, setTooltipPopup) : undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
type: XAxisType,
|
|
||||||
stacked: true,
|
|
||||||
time: {
|
|
||||||
unit,
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
border: {
|
|
||||||
color: colors.chart.line,
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
color: colors.chart.text,
|
|
||||||
autoSkip: false,
|
|
||||||
maxRotation: 0,
|
|
||||||
callback: renderXLabel,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
type: YAxisType,
|
|
||||||
min: 0,
|
|
||||||
beginAtZero: true,
|
|
||||||
stacked,
|
|
||||||
grid: {
|
|
||||||
color: colors.chart.line,
|
|
||||||
},
|
|
||||||
border: {
|
|
||||||
color: colors.chart.line,
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
color: colors.chart.text,
|
|
||||||
callback: renderYLabel || renderNumberLabels,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
animationDuration,
|
|
||||||
renderTooltipPopup,
|
|
||||||
renderXLabel,
|
|
||||||
XAxisType,
|
|
||||||
YAxisType,
|
|
||||||
stacked,
|
|
||||||
colors,
|
|
||||||
unit,
|
|
||||||
locale,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const createChart = () => {
|
|
||||||
Chart.defaults.font.family = 'Inter';
|
|
||||||
|
|
||||||
chart.current = new Chart(canvas.current, {
|
|
||||||
type: 'bar',
|
|
||||||
data: {
|
|
||||||
datasets,
|
|
||||||
},
|
|
||||||
options: getOptions() as any,
|
|
||||||
});
|
|
||||||
|
|
||||||
onCreate?.(chart.current);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateChart = () => {
|
|
||||||
setTooltipPopup(null);
|
|
||||||
|
|
||||||
chart.current.data.datasets = datasets;
|
|
||||||
|
|
||||||
chart.current.options = getOptions();
|
|
||||||
|
|
||||||
onUpdate?.(chart.current);
|
|
||||||
|
|
||||||
chart.current.update();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (datasets) {
|
|
||||||
if (!chart.current) {
|
|
||||||
createChart();
|
|
||||||
} else {
|
|
||||||
updateChart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [datasets, unit, theme, animationDuration, locale]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={classNames(styles.chart, className)}>
|
|
||||||
{isLoading && <Loading position="page" icon="dots" />}
|
|
||||||
<canvas ref={canvas} />
|
|
||||||
</div>
|
|
||||||
<Legend chart={chart.current} />
|
|
||||||
{tooltip && (
|
|
||||||
<HoverTooltip>
|
|
||||||
<div className={styles.tooltip}>{tooltip}</div>
|
|
||||||
</HoverTooltip>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BarChart;
|
|
@ -1,7 +1,7 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Loading } from 'react-basics';
|
import { Loading } from 'react-basics';
|
||||||
import { colord } from 'colord';
|
import { colord } from 'colord';
|
||||||
import BarChart from './BarChart';
|
import BarChart from 'components/charts/BarChart';
|
||||||
import { getDateArray } from 'lib/date';
|
import { getDateArray } from 'lib/date';
|
||||||
import {
|
import {
|
||||||
useLocale,
|
useLocale,
|
||||||
@ -10,8 +10,8 @@ import {
|
|||||||
useNavigation,
|
useNavigation,
|
||||||
useWebsiteEvents,
|
useWebsiteEvents,
|
||||||
} from 'components/hooks';
|
} from 'components/hooks';
|
||||||
import { EVENT_COLORS } from 'lib/constants';
|
import { CHART_COLORS } from 'lib/constants';
|
||||||
import { renderDateLabels, renderStatusTooltipPopup } from 'lib/charts';
|
import { renderDateLabels } from 'lib/charts';
|
||||||
|
|
||||||
export interface EventsChartProps {
|
export interface EventsChartProps {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
@ -26,7 +26,6 @@ export function EventsChart({ websiteId, className, token }: EventsChartProps) {
|
|||||||
const {
|
const {
|
||||||
query: { url, event },
|
query: { url, event },
|
||||||
} = useNavigation();
|
} = useNavigation();
|
||||||
|
|
||||||
const { data, isLoading } = useWebsiteEvents(websiteId, {
|
const { data, isLoading } = useWebsiteEvents(websiteId, {
|
||||||
startAt: +startDate,
|
startAt: +startDate,
|
||||||
endAt: +endDate,
|
endAt: +endDate,
|
||||||
@ -38,9 +37,8 @@ export function EventsChart({ websiteId, className, token }: EventsChartProps) {
|
|||||||
offset,
|
offset,
|
||||||
});
|
});
|
||||||
|
|
||||||
const datasets = useMemo(() => {
|
const chartData = useMemo(() => {
|
||||||
if (!data) return [];
|
if (!data) return [];
|
||||||
if (isLoading) return data;
|
|
||||||
|
|
||||||
const map = (data as any[]).reduce((obj, { x, t, y }) => {
|
const map = (data as any[]).reduce((obj, { x, t, y }) => {
|
||||||
if (!obj[x]) {
|
if (!obj[x]) {
|
||||||
@ -56,8 +54,9 @@ export function EventsChart({ websiteId, className, token }: EventsChartProps) {
|
|||||||
map[key] = getDateArray(map[key], startDate, endDate, unit);
|
map[key] = getDateArray(map[key], startDate, endDate, unit);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Object.keys(map).map((key, index) => {
|
return {
|
||||||
const color = colord(EVENT_COLORS[index % EVENT_COLORS.length]);
|
datasets: Object.keys(map).map((key, index) => {
|
||||||
|
const color = colord(CHART_COLORS[index % CHART_COLORS.length]);
|
||||||
return {
|
return {
|
||||||
label: key,
|
label: key,
|
||||||
data: map[key],
|
data: map[key],
|
||||||
@ -66,8 +65,9 @@ export function EventsChart({ websiteId, className, token }: EventsChartProps) {
|
|||||||
borderColor: color.alpha(0.7).toRgbString(),
|
borderColor: color.alpha(0.7).toRgbString(),
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
};
|
};
|
||||||
});
|
}),
|
||||||
}, [data, isLoading, startDate, endDate, unit]);
|
};
|
||||||
|
}, [data, startDate, endDate, unit]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Loading icon="dots" />;
|
return <Loading icon="dots" />;
|
||||||
@ -76,11 +76,10 @@ export function EventsChart({ websiteId, className, token }: EventsChartProps) {
|
|||||||
return (
|
return (
|
||||||
<BarChart
|
<BarChart
|
||||||
className={className}
|
className={className}
|
||||||
datasets={datasets as any[]}
|
data={chartData}
|
||||||
unit={unit}
|
unit={unit}
|
||||||
stacked={true}
|
stacked={true}
|
||||||
renderXLabel={renderDateLabels(unit, locale)}
|
renderXLabel={renderDateLabels(unit, locale)}
|
||||||
renderTooltipPopup={renderStatusTooltipPopup(unit, locale)}
|
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -1,43 +1,34 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { StatusLight } from 'react-basics';
|
import { StatusLight } from 'react-basics';
|
||||||
import { colord } from 'colord';
|
import { colord } from 'colord';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { LegendItem } from 'chart.js/auto';
|
||||||
import { useLocale } from 'components/hooks';
|
import { useLocale } from 'components/hooks';
|
||||||
import { useForceUpdate } from 'components/hooks';
|
|
||||||
import styles from './Legend.module.css';
|
import styles from './Legend.module.css';
|
||||||
|
|
||||||
export function Legend({ chart }) {
|
export function Legend({
|
||||||
|
items = [],
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
items: any[];
|
||||||
|
onClick: (index: LegendItem) => void;
|
||||||
|
}) {
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
const forceUpdate = useForceUpdate();
|
|
||||||
|
|
||||||
const handleClick = (index: string | number) => {
|
if (!items.find(({ text }) => text)) {
|
||||||
const meta = chart.getDatasetMeta(index);
|
|
||||||
|
|
||||||
meta.hidden = meta.hidden === null ? !chart.data.datasets[index].hidden : null;
|
|
||||||
|
|
||||||
chart.update();
|
|
||||||
|
|
||||||
forceUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
forceUpdate();
|
|
||||||
}, [locale, forceUpdate]);
|
|
||||||
|
|
||||||
if (!chart?.legend?.legendItems.find(({ text }) => text)) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.legend}>
|
<div className={styles.legend}>
|
||||||
{chart.legend.legendItems.map(({ text, fillStyle, datasetIndex, hidden }) => {
|
{items.map(item => {
|
||||||
|
const { text, fillStyle, hidden } = item;
|
||||||
const color = colord(fillStyle);
|
const color = colord(fillStyle);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={text}
|
key={text}
|
||||||
className={classNames(styles.label, { [styles.hidden]: hidden })}
|
className={classNames(styles.label, { [styles.hidden]: hidden })}
|
||||||
onClick={() => handleClick(datasetIndex)}
|
onClick={() => onClick(item)}
|
||||||
>
|
>
|
||||||
<StatusLight color={color.alpha(color.alpha() + 0.2).toHex()}>
|
<StatusLight color={color.alpha(color.alpha() + 0.2).toHex()}>
|
||||||
<span className={locale}>{text}</span>
|
<span className={locale}>{text}</span>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import BarChart, { BarChartProps } from './BarChart';
|
import BarChart, { BarChartProps } from 'components/charts/BarChart';
|
||||||
import { useLocale, useTheme, useMessages } from 'components/hooks';
|
import { useLocale, useTheme, useMessages } from 'components/hooks';
|
||||||
import { renderDateLabels, renderStatusTooltipPopup } from 'lib/charts';
|
import { renderDateLabels } from 'lib/charts';
|
||||||
|
|
||||||
export interface PageviewsChartProps extends BarChartProps {
|
export interface PageviewsChartProps extends BarChartProps {
|
||||||
data: {
|
data: {
|
||||||
@ -17,10 +17,13 @@ export function PageviewsChart({ data, unit, isLoading, ...props }: PageviewsCha
|
|||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
|
|
||||||
const datasets = useMemo(() => {
|
const chartData = useMemo(() => {
|
||||||
if (!data) return [];
|
if (!data) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return {
|
||||||
|
datasets: [
|
||||||
{
|
{
|
||||||
label: formatMessage(labels.visitors),
|
label: formatMessage(labels.visitors),
|
||||||
data: data.sessions,
|
data: data.sessions,
|
||||||
@ -33,17 +36,17 @@ export function PageviewsChart({ data, unit, isLoading, ...props }: PageviewsCha
|
|||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
...colors.chart.views,
|
...colors.chart.views,
|
||||||
},
|
},
|
||||||
];
|
],
|
||||||
}, [data, colors, formatMessage, labels]);
|
};
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BarChart
|
<BarChart
|
||||||
{...props}
|
{...props}
|
||||||
datasets={datasets}
|
data={chartData}
|
||||||
unit={unit}
|
unit={unit}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
renderXLabel={renderDateLabels(unit, locale)}
|
renderXLabel={renderDateLabels(unit, locale)}
|
||||||
renderTooltipPopup={renderStatusTooltipPopup(unit, locale)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -196,7 +196,7 @@
|
|||||||
"label.window": "Ventana",
|
"label.window": "Ventana",
|
||||||
"label.yesterday": "Ayer",
|
"label.yesterday": "Ayer",
|
||||||
"message.action-confirmation": "Escriba {confirmation} en el cuadro a continuación para confirmar.",
|
"message.action-confirmation": "Escriba {confirmation} en el cuadro a continuación para confirmar.",
|
||||||
"message.active-users": "{x} {x, plural, uno {activo} otros {activos}}",
|
"message.active-users": "{x} {x, plural, one {activo} other {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": "¿Estás seguro de que desea eliminar {target}?",
|
"message.confirm-remove": "¿Estás seguro de que desea eliminar {target}?",
|
||||||
|
@ -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": "添加网站",
|
"label.add-website": "添加网站",
|
||||||
"label.administrator": "管理员",
|
"label.administrator": "管理员",
|
||||||
"label.after": "之后",
|
"label.after": "之后",
|
||||||
@ -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": "删除网站",
|
||||||
@ -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": "启用共享链接",
|
"label.enable-share-url": "启用共享链接",
|
||||||
"label.event": "事件",
|
"label.event": "事件",
|
||||||
"label.event-data": "事件数据",
|
"label.event-data": "事件数据",
|
||||||
@ -95,24 +95,24 @@
|
|||||||
"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": "OK",
|
||||||
"label.os": "OS",
|
"label.os": "操作系统",
|
||||||
"label.overview": "概览",
|
"label.overview": "概览",
|
||||||
"label.owner": "所有者",
|
"label.owner": "所有者",
|
||||||
"label.page-of": "总{total}中的第{current}页",
|
"label.page-of": "总 {total} 中的第 {current} 页",
|
||||||
"label.page-views": "页面浏览量",
|
"label.page-views": "页面浏览量",
|
||||||
"label.pageTitle": "标题",
|
"label.pageTitle": "标题",
|
||||||
"label.pages": "网页",
|
"label.pages": "网页",
|
||||||
@ -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": "设置",
|
||||||
@ -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": "团队网站",
|
"label.team-websites": "团队网站",
|
||||||
"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": "转移网站",
|
||||||
"label.true": "是",
|
"label.true": "是",
|
||||||
"label.type": "类型",
|
"label.type": "类型",
|
||||||
"label.unique": "独立",
|
"label.unique": "独立",
|
||||||
@ -195,21 +195,21 @@
|
|||||||
"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} 人",
|
"message.active-users": "当前在线 {x} 人",
|
||||||
"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": "{url}上的{event}",
|
"message.event-log": "{url} 上的 {event}",
|
||||||
"message.go-to-settings": "去设置",
|
"message.go-to-settings": "去设置",
|
||||||
"message.incorrect-username-password": "用户名或密码不正确。",
|
"message.incorrect-username-password": "用户名或密码不正确。",
|
||||||
"message.invalid-domain": "无效域名",
|
"message.invalid-domain": "无效域名",
|
||||||
"message.min-password-length": "密码最短长度为 {n} 个字符",
|
"message.min-password-length": "密码最短长度为 {n} 个字符",
|
||||||
"message.new-version-available": "Umami的新版本{version}已推出!",
|
"message.new-version-available": "Umami 的新版本 {version} 已推出!",
|
||||||
"message.no-data-available": "无可用数据。",
|
"message.no-data-available": "无可用数据。",
|
||||||
"message.no-event-data": "无可用事件。",
|
"message.no-event-data": "无可用事件。",
|
||||||
"message.no-match-password": "密码不一致",
|
"message.no-match-password": "密码不一致",
|
||||||
@ -219,7 +219,7 @@
|
|||||||
"message.no-users": "没有任何用户。",
|
"message.no-users": "没有任何用户。",
|
||||||
"message.no-websites-configured": "你还没有设置任何网站。",
|
"message.no-websites-configured": "你还没有设置任何网站。",
|
||||||
"message.page-not-found": "网页未找到。",
|
"message.page-not-found": "网页未找到。",
|
||||||
"message.reset-website": "如果确定重置该网站, 请在下面的输入框中输入 {confirmation} 进行二次确认。",
|
"message.reset-website": "如果确定重置该网站,请在下面的输入框中输入 {confirmation} 进行二次确认。",
|
||||||
"message.reset-website-warning": "本网站的所有统计数据将被删除,但您的跟踪代码将保持不变。",
|
"message.reset-website-warning": "本网站的所有统计数据将被删除,但您的跟踪代码将保持不变。",
|
||||||
"message.saved": "保存成功。",
|
"message.saved": "保存成功。",
|
||||||
"message.share-url": "这是 {target} 的共享链接。",
|
"message.share-url": "这是 {target} 的共享链接。",
|
||||||
@ -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}的访客在搭载 {os} 的{device}上使用 {browser} 浏览器进行访问。",
|
"message.visitor-log": "来自 {country} 的访客在搭载 {os} 的 {device} 上使用 {browser} 浏览器进行访问。",
|
||||||
"message.visitors-dropped-off": "Visitors dropped off"
|
"message.visitors-dropped-off": "访客减少"
|
||||||
}
|
}
|
||||||
|
27
src/lib/charts.ts
Normal file
27
src/lib/charts.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { formatDate } from 'lib/date';
|
||||||
|
import { formatLongNumber } from 'lib/format';
|
||||||
|
|
||||||
|
export function renderNumberLabels(label: string) {
|
||||||
|
return +label > 1000 ? formatLongNumber(+label) : label;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderDateLabels(unit: string, locale: string) {
|
||||||
|
return (label: string, index: number, values: any[]) => {
|
||||||
|
const d = new Date(values[index].value);
|
||||||
|
|
||||||
|
switch (unit) {
|
||||||
|
case 'minute':
|
||||||
|
return formatDate(d, 'h:mm', locale);
|
||||||
|
case 'hour':
|
||||||
|
return formatDate(d, 'p', locale);
|
||||||
|
case 'day':
|
||||||
|
return formatDate(d, 'MMM d', locale);
|
||||||
|
case 'month':
|
||||||
|
return formatDate(d, 'MMM', locale);
|
||||||
|
case 'year':
|
||||||
|
return formatDate(d, 'YYY', locale);
|
||||||
|
default:
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -1,62 +0,0 @@
|
|||||||
import { StatusLight } from 'react-basics';
|
|
||||||
import { formatDate } from 'lib/date';
|
|
||||||
import { formatLongNumber } from 'lib/format';
|
|
||||||
|
|
||||||
export function renderNumberLabels(label: string) {
|
|
||||||
return +label > 1000 ? formatLongNumber(+label) : label;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderDateLabels(unit: string, locale: string) {
|
|
||||||
return (label: string, index: number, values: any[]) => {
|
|
||||||
const d = new Date(values[index].value);
|
|
||||||
|
|
||||||
switch (unit) {
|
|
||||||
case 'minute':
|
|
||||||
return formatDate(d, 'h:mm', locale);
|
|
||||||
case 'hour':
|
|
||||||
return formatDate(d, 'p', locale);
|
|
||||||
case 'day':
|
|
||||||
return formatDate(d, 'MMM d', locale);
|
|
||||||
case 'month':
|
|
||||||
return formatDate(d, 'MMM', locale);
|
|
||||||
case 'year':
|
|
||||||
return formatDate(d, 'YYY', locale);
|
|
||||||
default:
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderStatusTooltipPopup(unit: string, locale: string) {
|
|
||||||
return (setTooltipPopup: (data: any) => void, model: any) => {
|
|
||||||
const { opacity, labelColors, dataPoints } = model.tooltip;
|
|
||||||
|
|
||||||
if (!dataPoints?.length || !opacity) {
|
|
||||||
setTooltipPopup(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formats = {
|
|
||||||
millisecond: 'T',
|
|
||||||
second: 'pp',
|
|
||||||
minute: 'p',
|
|
||||||
hour: 'h:mm aaa - PP',
|
|
||||||
day: 'PPPP',
|
|
||||||
week: 'PPPP',
|
|
||||||
month: 'LLLL yyyy',
|
|
||||||
quarter: 'qqq',
|
|
||||||
year: 'yyyy',
|
|
||||||
};
|
|
||||||
|
|
||||||
setTooltipPopup(
|
|
||||||
<>
|
|
||||||
<div>{formatDate(new Date(dataPoints[0].raw.x), formats[unit], locale)}</div>
|
|
||||||
<div>
|
|
||||||
<StatusLight color={labelColors?.[0]?.backgroundColor}>
|
|
||||||
{formatLongNumber(dataPoints[0].raw.y)} {dataPoints[0].dataset.label}
|
|
||||||
</StatusLight>
|
|
||||||
</div>
|
|
||||||
</>,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user