mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-22 01:46:58 +01:00
Merge branch 'dev' into analytics
This commit is contained in:
commit
df790cffd2
@ -4,4 +4,9 @@ export default defineConfig({
|
||||
e2e: {
|
||||
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_password=umami
|
||||
volumes:
|
||||
- ../tsconfig.json:/tsconfig.json
|
||||
- ./tsconfig.json:/tsconfig.json
|
||||
- ../cypress.config.ts:/cypress.config.ts
|
||||
- ./:/cypress
|
||||
- ../node_modules/:/node_modules
|
||||
- ../src/lib/crypto.ts:/src/lib/crypto.ts
|
||||
volumes:
|
||||
umami-db-data:
|
||||
|
@ -6,8 +6,12 @@ describe('Login tests', () => {
|
||||
},
|
||||
() => {
|
||||
cy.visit('/login');
|
||||
cy.getDataTest('input-username').find('input').type(Cypress.env('umami_user'));
|
||||
cy.getDataTest('input-password').find('input').type(Cypress.env('umami_password'));
|
||||
cy.getDataTest('input-username').find('input').click();
|
||||
cy.getDataTest('input-username').find('input').type(Cypress.env('umami_user'), { delay: 50 });
|
||||
cy.getDataTest('input-password').find('input').click();
|
||||
cy.getDataTest('input-password')
|
||||
.find('input')
|
||||
.type(Cypress.env('umami_password'), { delay: 50 });
|
||||
cy.getDataTest('button-submit').click();
|
||||
cy.url().should('eq', Cypress.config().baseUrl + '/dashboard');
|
||||
cy.getDataTest('button-profile').click();
|
||||
|
@ -10,8 +10,10 @@ describe('Website tests', () => {
|
||||
cy.visit('/settings/websites');
|
||||
cy.getDataTest('button-website-add').click();
|
||||
cy.contains(/Add website/i).should('be.visible');
|
||||
cy.getDataTest('input-name').find('input').wait(500).type('Add test', { delay: 50 });
|
||||
cy.getDataTest('input-domain').find('input').wait(500).type('addtest.com', { delay: 50 });
|
||||
cy.getDataTest('input-name').find('input').click();
|
||||
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.get('td[label="Name"]').should('contain.text', 'Add test');
|
||||
cy.get('td[label="Domain"]').should('contain.text', 'addtest.com');
|
||||
@ -26,10 +28,10 @@ describe('Website tests', () => {
|
||||
cy.deleteWebsite(websiteId);
|
||||
});
|
||||
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
|
||||
cy.addWebsite('Update test', 'updatetest.com');
|
||||
cy.visit('/settings/websites');
|
||||
@ -37,16 +39,12 @@ describe('Website tests', () => {
|
||||
// edit website
|
||||
cy.getDataTest('link-button-edit').first().click();
|
||||
cy.contains(/Details/i).should('be.visible');
|
||||
cy.getDataTest('input-name')
|
||||
.find('input')
|
||||
.wait(500)
|
||||
.clear()
|
||||
.type('Updated website', { delay: 50 });
|
||||
cy.getDataTest('input-domain')
|
||||
.find('input')
|
||||
.wait(500)
|
||||
.clear()
|
||||
.type('updatedwebsite.com', { delay: 50 });
|
||||
cy.getDataTest('input-name').find('input').click();
|
||||
cy.getDataTest('input-name').find('input').clear();
|
||||
cy.getDataTest('input-name').find('input').type('Updated website', { delay: 50 });
|
||||
cy.getDataTest('input-domain').find('input').click();
|
||||
cy.getDataTest('input-domain').find('input').clear();
|
||||
cy.getDataTest('input-domain').find('input').type('updatedwebsite.com', { delay: 50 });
|
||||
cy.getDataTest('button-submit').click({ force: true });
|
||||
cy.getDataTest('input-name').find('input').should('have.value', 'Updated website');
|
||||
cy.getDataTest('input-domain').find('input').should('have.value', 'updatedwebsite.com');
|
||||
@ -69,7 +67,7 @@ describe('Website tests', () => {
|
||||
cy.deleteWebsite(websiteId);
|
||||
});
|
||||
cy.visit('/settings/websites');
|
||||
cy.contains('Add test').should('not.exist');
|
||||
cy.contains(/Add test/i).should('not.exist');
|
||||
});
|
||||
|
||||
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.get('input[name="confirm"').type('DELETE');
|
||||
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": {
|
||||
"@clickhouse/client": "^0.2.2",
|
||||
"@fontsource/inter": "^4.5.15",
|
||||
"@prisma/client": "5.9.1",
|
||||
"@prisma/client": "5.10.2",
|
||||
"@prisma/extension-read-replicas": "^0.3.0",
|
||||
"@react-spring/web": "^9.7.3",
|
||||
"@tanstack/react-query": "^5.12.2",
|
||||
"@umami/prisma-client": "^0.14.0",
|
||||
"@umami/redis-client": "^0.18.0",
|
||||
"chalk": "^4.1.1",
|
||||
"chart.js": "^4.2.1",
|
||||
"chart.js": "^4.4.2",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"classnames": "^2.3.1",
|
||||
"colord": "^2.9.2",
|
||||
@ -98,11 +98,11 @@
|
||||
"maxmind": "^4.3.6",
|
||||
"md5": "^2.3.0",
|
||||
"moment-timezone": "^0.5.35",
|
||||
"next": "14.1.0",
|
||||
"next": "14.1.3",
|
||||
"next-basics": "^0.39.0",
|
||||
"node-fetch": "^3.2.8",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prisma": "5.9.1",
|
||||
"prisma": "5.10.2",
|
||||
"react": "^18.2.0",
|
||||
"react-basics": "^0.123.0",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Erstellt"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Erstellt"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -32,7 +32,7 @@
|
||||
"label.add-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Add member"
|
||||
"value": "Añadir miembro"
|
||||
}
|
||||
],
|
||||
"label.add-website": [
|
||||
@ -215,6 +215,12 @@
|
||||
"value": "Creado"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -272,7 +278,7 @@
|
||||
"label.delete-report": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Delete report"
|
||||
"value": "Eliminar reporte"
|
||||
}
|
||||
],
|
||||
"label.delete-team": [
|
||||
@ -464,7 +470,7 @@
|
||||
"label.insights-description": [
|
||||
{
|
||||
"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": [
|
||||
@ -482,7 +488,7 @@
|
||||
"label.is-not-set": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Is not set"
|
||||
"value": "No está establecido"
|
||||
}
|
||||
],
|
||||
"label.is-set": [
|
||||
@ -588,7 +594,7 @@
|
||||
"label.manage": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Manage"
|
||||
"value": "Administrar"
|
||||
}
|
||||
],
|
||||
"label.max": [
|
||||
@ -600,7 +606,7 @@
|
||||
"label.member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Member"
|
||||
"value": "Miembro"
|
||||
}
|
||||
],
|
||||
"label.members": [
|
||||
@ -630,7 +636,7 @@
|
||||
"label.my-account": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "My account"
|
||||
"value": "Mi cuenta"
|
||||
}
|
||||
],
|
||||
"label.my-websites": [
|
||||
@ -842,7 +848,7 @@
|
||||
"label.remove-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Remove member"
|
||||
"value": "Eliminar miembro"
|
||||
}
|
||||
],
|
||||
"label.reports": [
|
||||
@ -926,7 +932,7 @@
|
||||
"label.select-role": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Select role"
|
||||
"value": "Seleccionar rol"
|
||||
}
|
||||
],
|
||||
"label.select-website": [
|
||||
@ -1004,7 +1010,7 @@
|
||||
"label.team-view-only": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Team view only"
|
||||
"value": "Vista solo del equipo"
|
||||
}
|
||||
],
|
||||
"label.team-websites": [
|
||||
@ -1088,13 +1094,13 @@
|
||||
"label.transfer": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer"
|
||||
"value": "Transferir"
|
||||
}
|
||||
],
|
||||
"label.transfer-website": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer website"
|
||||
"value": "Transferir sitio web"
|
||||
}
|
||||
],
|
||||
"label.true": [
|
||||
@ -1232,7 +1238,7 @@
|
||||
"message.action-confirmation": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Type "
|
||||
"value": "Escriba "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1240,7 +1246,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": " in the box below to confirm."
|
||||
"value": " en el cuadro a continuación para confirmar."
|
||||
}
|
||||
],
|
||||
"message.active-users": [
|
||||
@ -1308,7 +1314,7 @@
|
||||
"message.confirm-remove": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Are you sure you want to remove "
|
||||
"value": "¿Estás seguro de que desea eliminar "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1336,7 +1342,7 @@
|
||||
"message.delete-team-warning": [
|
||||
{
|
||||
"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": [
|
||||
@ -1532,7 +1538,7 @@
|
||||
"message.transfer-team-website-to-user": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer this website to your account?"
|
||||
"value": "¿Transferir este sitio web a su cuenta?"
|
||||
}
|
||||
],
|
||||
"message.transfer-user-website-to-team": [
|
||||
@ -1544,13 +1550,13 @@
|
||||
"message.transfer-website": [
|
||||
{
|
||||
"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": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Triggered event"
|
||||
"value": "Evento lanzado"
|
||||
}
|
||||
],
|
||||
"message.user-deleted": [
|
||||
@ -1562,7 +1568,7 @@
|
||||
"message.viewed-page": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Viewed page"
|
||||
"value": "Página vista"
|
||||
}
|
||||
],
|
||||
"message.visitor-log": [
|
||||
@ -1602,7 +1608,7 @@
|
||||
"message.visitors-dropped-off": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Visitors dropped off"
|
||||
"value": "Los visitantes salieron"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -32,7 +32,7 @@
|
||||
"label.add-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Add member"
|
||||
"value": "Ajouter un membre"
|
||||
}
|
||||
],
|
||||
"label.add-website": [
|
||||
@ -215,6 +215,12 @@
|
||||
"value": "Créé"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Crée par"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -272,7 +278,7 @@
|
||||
"label.delete-report": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Delete report"
|
||||
"value": "Supprimer le rapport"
|
||||
}
|
||||
],
|
||||
"label.delete-team": [
|
||||
@ -362,7 +368,7 @@
|
||||
"label.edit-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Edit member"
|
||||
"value": "Modifier le membre"
|
||||
}
|
||||
],
|
||||
"label.enable-share-url": [
|
||||
@ -580,7 +586,7 @@
|
||||
"label.manage": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Manage"
|
||||
"value": "Gérer"
|
||||
}
|
||||
],
|
||||
"label.max": [
|
||||
@ -592,7 +598,7 @@
|
||||
"label.member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Member"
|
||||
"value": "Membre"
|
||||
}
|
||||
],
|
||||
"label.members": [
|
||||
@ -622,7 +628,7 @@
|
||||
"label.my-account": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "My account"
|
||||
"value": "Mon compte"
|
||||
}
|
||||
],
|
||||
"label.my-websites": [
|
||||
@ -646,7 +652,7 @@
|
||||
"label.none": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Aucun·e"
|
||||
"value": "Aucun"
|
||||
}
|
||||
],
|
||||
"label.number-of-records": [
|
||||
@ -834,7 +840,7 @@
|
||||
"label.remove-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Remove member"
|
||||
"value": "Retirer le membre"
|
||||
}
|
||||
],
|
||||
"label.reports": [
|
||||
@ -918,7 +924,7 @@
|
||||
"label.select-role": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Select role"
|
||||
"value": "Choisir un rôle"
|
||||
}
|
||||
],
|
||||
"label.select-website": [
|
||||
@ -1080,13 +1086,13 @@
|
||||
"label.transfer": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer"
|
||||
"value": "Transférer"
|
||||
}
|
||||
],
|
||||
"label.transfer-website": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer website"
|
||||
"value": "Transférer le site"
|
||||
}
|
||||
],
|
||||
"label.true": [
|
||||
@ -1224,7 +1230,7 @@
|
||||
"message.action-confirmation": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Type "
|
||||
"value": "Taper "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1232,7 +1238,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": " in the box below to confirm."
|
||||
"value": " ci-dessous pour confirmer."
|
||||
}
|
||||
],
|
||||
"message.active-users": [
|
||||
@ -1304,7 +1310,7 @@
|
||||
"message.confirm-remove": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Are you sure you want to remove "
|
||||
"value": "Êtes-vous sûr de vouloir retirer "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1312,7 +1318,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "?"
|
||||
"value": " ?"
|
||||
}
|
||||
],
|
||||
"message.confirm-reset": [
|
||||
@ -1332,7 +1338,7 @@
|
||||
"message.delete-team-warning": [
|
||||
{
|
||||
"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": [
|
||||
@ -1520,19 +1526,19 @@
|
||||
"message.transfer-team-website-to-user": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer this website to your account?"
|
||||
"value": "Transférer ce site sur votre compte ?"
|
||||
}
|
||||
],
|
||||
"message.transfer-user-website-to-team": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Select the team to transfer this website to."
|
||||
"value": "Choisir l'équipe à laquelle transférer ce site."
|
||||
}
|
||||
],
|
||||
"message.transfer-website": [
|
||||
{
|
||||
"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": [
|
||||
@ -1550,7 +1556,7 @@
|
||||
"message.viewed-page": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Viewed page"
|
||||
"value": "Page vue"
|
||||
}
|
||||
],
|
||||
"message.visitor-log": [
|
||||
@ -1590,7 +1596,7 @@
|
||||
"message.visitors-dropped-off": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Visitors dropped off"
|
||||
"value": "Visiteurs ont abandonné"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -32,7 +32,7 @@
|
||||
"label.add-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Add member"
|
||||
"value": "メンバーの追加"
|
||||
}
|
||||
],
|
||||
"label.add-website": [
|
||||
@ -188,7 +188,7 @@
|
||||
"label.create": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Create"
|
||||
"value": "作成"
|
||||
}
|
||||
],
|
||||
"label.create-report": [
|
||||
@ -215,6 +215,12 @@
|
||||
"value": "作成されました"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -272,7 +278,7 @@
|
||||
"label.delete-report": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Delete report"
|
||||
"value": "レポートの削除"
|
||||
}
|
||||
],
|
||||
"label.delete-team": [
|
||||
@ -362,7 +368,7 @@
|
||||
"label.edit-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Edit member"
|
||||
"value": "メンバーの編集"
|
||||
}
|
||||
],
|
||||
"label.enable-share-url": [
|
||||
@ -410,13 +416,13 @@
|
||||
"label.filter": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Filter"
|
||||
"value": "フィルター"
|
||||
}
|
||||
],
|
||||
"label.filter-combined": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "統合"
|
||||
"value": "結合"
|
||||
}
|
||||
],
|
||||
"label.filter-raw": [
|
||||
@ -434,13 +440,13 @@
|
||||
"label.funnel": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "分析"
|
||||
"value": "ファネル"
|
||||
}
|
||||
],
|
||||
"label.funnel-description": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Understand the conversion and drop-off rate of users."
|
||||
"value": "ユーザーのコンバージョン率と離脱率を分析します。"
|
||||
}
|
||||
],
|
||||
"label.greater-than": [
|
||||
@ -458,13 +464,13 @@
|
||||
"label.insights": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "見通し"
|
||||
"value": "インサイト"
|
||||
}
|
||||
],
|
||||
"label.insights-description": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Dive deeper into your data by using segments and filters."
|
||||
"value": "セグメントとフィルタを使用して、データをさらに詳しく分析します。"
|
||||
}
|
||||
],
|
||||
"label.is": [
|
||||
@ -588,7 +594,7 @@
|
||||
"label.manage": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Manage"
|
||||
"value": "管理"
|
||||
}
|
||||
],
|
||||
"label.max": [
|
||||
@ -600,7 +606,7 @@
|
||||
"label.member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Member"
|
||||
"value": "メンバー"
|
||||
}
|
||||
],
|
||||
"label.members": [
|
||||
@ -630,7 +636,7 @@
|
||||
"label.my-account": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "My account"
|
||||
"value": "マイアカウント"
|
||||
}
|
||||
],
|
||||
"label.my-websites": [
|
||||
@ -842,7 +848,7 @@
|
||||
"label.remove-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Remove member"
|
||||
"value": "メンバーの削除"
|
||||
}
|
||||
],
|
||||
"label.reports": [
|
||||
@ -872,13 +878,13 @@
|
||||
"label.retention": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "保持"
|
||||
"value": "リテンション"
|
||||
}
|
||||
],
|
||||
"label.retention-description": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Measure your website stickiness by tracking how often users return."
|
||||
"value": "ユーザーの再訪問回数を記録して、Webサイトのリテンション率を計測します。"
|
||||
}
|
||||
],
|
||||
"label.role": [
|
||||
@ -908,13 +914,13 @@
|
||||
"label.search": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Search"
|
||||
"value": "検索"
|
||||
}
|
||||
],
|
||||
"label.select": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Select"
|
||||
"value": "選択"
|
||||
}
|
||||
],
|
||||
"label.select-date": [
|
||||
@ -926,7 +932,7 @@
|
||||
"label.select-role": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Select role"
|
||||
"value": "ロールを選択"
|
||||
}
|
||||
],
|
||||
"label.select-website": [
|
||||
@ -1004,7 +1010,7 @@
|
||||
"label.team-view-only": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Team view only"
|
||||
"value": "チーム表示のみ"
|
||||
}
|
||||
],
|
||||
"label.team-websites": [
|
||||
@ -1088,13 +1094,13 @@
|
||||
"label.transfer": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer"
|
||||
"value": "移管"
|
||||
}
|
||||
],
|
||||
"label.transfer-website": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer website"
|
||||
"value": "Webサイトの移管"
|
||||
}
|
||||
],
|
||||
"label.true": [
|
||||
@ -1232,7 +1238,7 @@
|
||||
"message.action-confirmation": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Type "
|
||||
"value": "承認する場合は、下のフォームに「"
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1240,7 +1246,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": " in the box below to confirm."
|
||||
"value": "」と入力してください。"
|
||||
}
|
||||
],
|
||||
"message.active-users": [
|
||||
@ -1298,17 +1304,13 @@
|
||||
}
|
||||
],
|
||||
"message.confirm-remove": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Are you sure you want to remove "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"value": "target"
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "?"
|
||||
"value": "を削除してもよろしいですか?"
|
||||
}
|
||||
],
|
||||
"message.confirm-reset": [
|
||||
@ -1324,7 +1326,7 @@
|
||||
"message.delete-team-warning": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Deleting a team will also delete all team websites."
|
||||
"value": "チームを削除すると、そのチームが管理しているWebサイトもすべて削除されます。"
|
||||
}
|
||||
],
|
||||
"message.delete-website-warning": [
|
||||
@ -1526,25 +1528,25 @@
|
||||
"message.transfer-team-website-to-user": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer this website to your account?"
|
||||
"value": "このWebサイトをあなたのアカウントに移管しますか?"
|
||||
}
|
||||
],
|
||||
"message.transfer-user-website-to-team": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Select the team to transfer this website to."
|
||||
"value": "このWebサイトを移管するチームを選択してください。"
|
||||
}
|
||||
],
|
||||
"message.transfer-website": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer website ownership to your account or another team."
|
||||
"value": "Webサイトの所有権を自分のアカウントまたは別のチームへ移管します。"
|
||||
}
|
||||
],
|
||||
"message.triggered-event": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Triggered event"
|
||||
"value": "トリガーされたイベント"
|
||||
}
|
||||
],
|
||||
"message.user-deleted": [
|
||||
@ -1556,7 +1558,7 @@
|
||||
"message.viewed-page": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Viewed page"
|
||||
"value": "閲覧されたページ"
|
||||
}
|
||||
],
|
||||
"message.visitor-log": [
|
||||
@ -1596,7 +1598,7 @@
|
||||
"message.visitors-dropped-off": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Visitors dropped off"
|
||||
"value": "訪問者の離脱率"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Үүсгэсэн"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "ပြုလုပ်ပြီးသော"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Gemaakt"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Utworzony"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Criado"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -215,6 +215,12 @@
|
||||
"value": "Создано"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Ustvarjeno"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Skapad"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "Created"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -32,7 +32,7 @@
|
||||
"label.add-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Add member"
|
||||
"value": "添加成员"
|
||||
}
|
||||
],
|
||||
"label.add-website": [
|
||||
@ -215,6 +215,12 @@
|
||||
"value": "已创建"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "创建者"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -272,7 +278,7 @@
|
||||
"label.delete-report": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Delete report"
|
||||
"value": "删除报告"
|
||||
}
|
||||
],
|
||||
"label.delete-team": [
|
||||
@ -362,7 +368,7 @@
|
||||
"label.edit-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Edit member"
|
||||
"value": "编辑成员"
|
||||
}
|
||||
],
|
||||
"label.enable-share-url": [
|
||||
@ -588,7 +594,7 @@
|
||||
"label.manage": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Manage"
|
||||
"value": "管理"
|
||||
}
|
||||
],
|
||||
"label.max": [
|
||||
@ -600,7 +606,7 @@
|
||||
"label.member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Member"
|
||||
"value": "成员"
|
||||
}
|
||||
],
|
||||
"label.members": [
|
||||
@ -630,7 +636,7 @@
|
||||
"label.my-account": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "My account"
|
||||
"value": "我的账户"
|
||||
}
|
||||
],
|
||||
"label.my-websites": [
|
||||
@ -700,7 +706,7 @@
|
||||
"label.os": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "OS"
|
||||
"value": "操作系统"
|
||||
}
|
||||
],
|
||||
"label.overview": [
|
||||
@ -718,7 +724,7 @@
|
||||
"label.page-of": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "总"
|
||||
"value": "总 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -726,7 +732,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "中的第"
|
||||
"value": " 中的第 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -734,7 +740,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "页"
|
||||
"value": " 页"
|
||||
}
|
||||
],
|
||||
"label.page-views": [
|
||||
@ -850,7 +856,7 @@
|
||||
"label.remove-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Remove member"
|
||||
"value": "移除成员"
|
||||
}
|
||||
],
|
||||
"label.reports": [
|
||||
@ -922,7 +928,7 @@
|
||||
"label.select": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Select"
|
||||
"value": "选择"
|
||||
}
|
||||
],
|
||||
"label.select-date": [
|
||||
@ -934,7 +940,7 @@
|
||||
"label.select-role": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Select role"
|
||||
"value": "选择角色"
|
||||
}
|
||||
],
|
||||
"label.select-website": [
|
||||
@ -1012,7 +1018,7 @@
|
||||
"label.team-view-only": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Team view only"
|
||||
"value": "仅团队视图"
|
||||
}
|
||||
],
|
||||
"label.team-websites": [
|
||||
@ -1096,13 +1102,13 @@
|
||||
"label.transfer": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer"
|
||||
"value": "转移"
|
||||
}
|
||||
],
|
||||
"label.transfer-website": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer website"
|
||||
"value": "转移网站"
|
||||
}
|
||||
],
|
||||
"label.true": [
|
||||
@ -1240,7 +1246,7 @@
|
||||
"message.action-confirmation": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Type "
|
||||
"value": "在下面的框中输入 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1248,7 +1254,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": " in the box below to confirm."
|
||||
"value": " 以确认。"
|
||||
}
|
||||
],
|
||||
"message.active-users": [
|
||||
@ -1296,7 +1302,7 @@
|
||||
"message.confirm-remove": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Are you sure you want to remove "
|
||||
"value": "您确定要移除 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1304,7 +1310,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "?"
|
||||
"value": " ?"
|
||||
}
|
||||
],
|
||||
"message.confirm-reset": [
|
||||
@ -1318,13 +1324,13 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": " 的数据吗?"
|
||||
"value": " 的数据吗?"
|
||||
}
|
||||
],
|
||||
"message.delete-team-warning": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Deleting a team will also delete all team websites."
|
||||
"value": "删除团队也会删除所有团队的网站。"
|
||||
}
|
||||
],
|
||||
"message.delete-website-warning": [
|
||||
@ -1346,7 +1352,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "上的"
|
||||
"value": " 上的 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1388,7 +1394,7 @@
|
||||
"message.new-version-available": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Umami的新版本"
|
||||
"value": "Umami 的新版本 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1396,7 +1402,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "已推出!"
|
||||
"value": " 已推出!"
|
||||
}
|
||||
],
|
||||
"message.no-data-available": [
|
||||
@ -1456,7 +1462,7 @@
|
||||
"message.reset-website": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "如果确定重置该网站, 请在下面的输入框中输入 "
|
||||
"value": "如果确定重置该网站,请在下面的输入框中输入 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1520,25 +1526,25 @@
|
||||
"message.transfer-team-website-to-user": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer this website to your account?"
|
||||
"value": "将该网站转入您的账户?"
|
||||
}
|
||||
],
|
||||
"message.transfer-user-website-to-team": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Select the team to transfer this website to."
|
||||
"value": "选择要将该网站转移到哪个团队。"
|
||||
}
|
||||
],
|
||||
"message.transfer-website": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Transfer website ownership to your account or another team."
|
||||
"value": "将网站所有权转移到您的账户或其他团队。"
|
||||
}
|
||||
],
|
||||
"message.triggered-event": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Triggered event"
|
||||
"value": "触发事件"
|
||||
}
|
||||
],
|
||||
"message.user-deleted": [
|
||||
@ -1550,13 +1556,13 @@
|
||||
"message.viewed-page": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Viewed page"
|
||||
"value": "已浏览页面"
|
||||
}
|
||||
],
|
||||
"message.visitor-log": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "来自"
|
||||
"value": "来自 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1564,7 +1570,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "的访客在搭载 "
|
||||
"value": " 的访客在搭载 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1572,7 +1578,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": " 的"
|
||||
"value": " 的 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1580,7 +1586,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "上使用 "
|
||||
"value": " 上使用 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -1594,7 +1600,7 @@
|
||||
"message.visitors-dropped-off": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Visitors dropped off"
|
||||
"value": "访客减少"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -215,6 +215,12 @@
|
||||
"value": "已建立"
|
||||
}
|
||||
],
|
||||
"label.created-by": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Created By"
|
||||
}
|
||||
],
|
||||
"label.current-password": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -10,7 +10,8 @@ export default {
|
||||
},
|
||||
plugins: [
|
||||
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: ['', ''],
|
||||
preventAssignment: true,
|
||||
}),
|
||||
|
@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
import { Metadata } from 'next';
|
||||
import ReportsHeader from './ReportsHeader';
|
||||
import ReportsDataTable from './ReportsDataTable';
|
||||
|
||||
@ -10,6 +11,7 @@ export default function ReportsPage({ teamId }: { teamId: string }) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
export const metadata = {
|
||||
|
||||
export const metadata: Metadata = {
|
||||
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';
|
||||
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 }) {
|
||||
return <ReportDetails reportId={reportId} />;
|
||||
const reports = {
|
||||
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 Lightbulb from 'assets/lightbulb.svg';
|
||||
import Magnet from 'assets/magnet.svg';
|
||||
import Tag from 'assets/tag.svg';
|
||||
import styles from './ReportTemplates.module.css';
|
||||
import { useMessages, useTeamUrl } from 'components/hooks';
|
||||
|
||||
@ -30,6 +31,12 @@ export function ReportTemplates({ showHeader = true }: { showHeader?: boolean })
|
||||
url: renderTeamUrl('/reports/retention'),
|
||||
icon: <Magnet />,
|
||||
},
|
||||
{
|
||||
title: formatMessage(labels.utm),
|
||||
description: formatMessage(labels.utmDescription),
|
||||
url: renderTeamUrl('/reports/utm'),
|
||||
icon: <Tag />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
@ -53,11 +53,11 @@ export function InsightsParameters() {
|
||||
filters,
|
||||
};
|
||||
|
||||
const handleSubmit = values => {
|
||||
const handleSubmit = (values: any) => {
|
||||
runReport(values);
|
||||
};
|
||||
|
||||
const handleAdd = (id, value) => {
|
||||
const handleAdd = (id: string | number, value: { name: any }) => {
|
||||
const data = parameterData[id];
|
||||
|
||||
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]];
|
||||
data.splice(index, 1);
|
||||
updateReport({ parameters: { [id]: data } });
|
||||
|
@ -1,6 +1,11 @@
|
||||
'use client';
|
||||
import { Metadata } from 'next';
|
||||
import RetentionReport from './RetentionReport';
|
||||
|
||||
export default function RetentionReportPage() {
|
||||
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;
|
||||
}
|
||||
|
||||
.website {
|
||||
text-align: right;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.detail {
|
||||
display: flex;
|
||||
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 { FixedSizeList } from 'react-window';
|
||||
import { format } from 'date-fns';
|
||||
@ -11,6 +11,8 @@ import Icons from 'components/icons';
|
||||
import useFormat from 'components//hooks/useFormat';
|
||||
import { BROWSERS } from 'lib/constants';
|
||||
import { stringToColor } from 'lib/format';
|
||||
import { RealtimeData } from 'lib/types';
|
||||
import { WebsiteContext } from '../WebsiteProvider';
|
||||
import styles from './RealtimeLog.module.css';
|
||||
|
||||
const TYPE_ALL = 'all';
|
||||
@ -24,7 +26,8 @@ const icons = {
|
||||
[TYPE_EVENT]: <Icons.Bolt />,
|
||||
};
|
||||
|
||||
export function RealtimeLog({ data, websiteDomain }) {
|
||||
export function RealtimeLog({ data }: { data: RealtimeData }) {
|
||||
const website = useContext(WebsiteContext);
|
||||
const [search, setSearch] = useState('');
|
||||
const { formatMessage, labels, messages, FormattedMessage } = useMessages();
|
||||
const { formatValue } = useFormat();
|
||||
@ -76,7 +79,7 @@ export function RealtimeLog({ data, websiteDomain }) {
|
||||
event: <b>{eventName || formatMessage(labels.unknown)}</b>,
|
||||
url: (
|
||||
<a
|
||||
href={`//${websiteDomain}${url}`}
|
||||
href={`//${website?.domain}${url}`}
|
||||
className={styles.link}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
@ -92,7 +95,7 @@ export function RealtimeLog({ data, websiteDomain }) {
|
||||
if (__type === TYPE_PAGEVIEW) {
|
||||
return (
|
||||
<a
|
||||
href={`//${websiteDomain}${url}`}
|
||||
href={`//${website?.domain}${url}`}
|
||||
className={styles.link}
|
||||
target="_blank"
|
||||
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 thenby from 'thenby';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
@ -6,14 +6,10 @@ import ListTable from 'components/metrics/ListTable';
|
||||
import { FILTER_PAGES, FILTER_REFERRERS } from 'lib/constants';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { RealtimeData } from 'lib/types';
|
||||
import { WebsiteContext } from '../WebsiteProvider';
|
||||
|
||||
export function RealtimeUrls({
|
||||
websiteDomain,
|
||||
data,
|
||||
}: {
|
||||
websiteDomain: string;
|
||||
data: RealtimeData;
|
||||
}) {
|
||||
export function RealtimeUrls({ data }: { data: RealtimeData }) {
|
||||
const website = useContext(WebsiteContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { pageviews } = data || {};
|
||||
const [filter, setFilter] = useState<Key>(FILTER_REFERRERS);
|
||||
@ -31,7 +27,7 @@ export function RealtimeUrls({
|
||||
];
|
||||
|
||||
const renderLink = ({ x }) => {
|
||||
const domain = x.startsWith('/') ? websiteDomain : '';
|
||||
const domain = x.startsWith('/') ? website?.domain : '';
|
||||
return (
|
||||
<a href={`//${domain}${x}`} target="_blank" rel="noreferrer noopener">
|
||||
{x}
|
||||
|
@ -1,6 +1,40 @@
|
||||
'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 }) {
|
||||
return <Realtime websiteId={websiteId} />;
|
||||
export function WebsiteRealtimePage({ 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 {
|
||||
position: relative;
|
||||
height: 400px;
|
||||
@ -13,7 +9,3 @@
|
||||
flex-direction: column;
|
||||
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 {
|
||||
font-size: 11px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
color: var(--base600);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Link from 'next/link';
|
||||
import { Flexbox, Icon, Icons, Text } from 'react-basics';
|
||||
import styles from './Breadcrumb.module.css';
|
||||
import { Fragment } from 'react';
|
||||
|
||||
export interface BreadcrumbProps {
|
||||
data: {
|
||||
@ -14,7 +15,7 @@ export function Breadcrumb({ data }: BreadcrumbProps) {
|
||||
<Flexbox alignItems="center" gap={3} className={styles.bar}>
|
||||
{data.map((a, i) => {
|
||||
return (
|
||||
<>
|
||||
<Fragment key={i}>
|
||||
{a.url ? (
|
||||
<Link href={a.url} className={styles.link}>
|
||||
<Text>{a.label}</Text>
|
||||
@ -27,7 +28,7 @@ export function Breadcrumb({ data }: BreadcrumbProps) {
|
||||
<Icons.ChevronDown />
|
||||
</Icon>
|
||||
) : null}
|
||||
</>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</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 { 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';
|
||||
|
||||
export interface FilterLinkProps {
|
||||
@ -40,7 +39,7 @@ export function FilterLink({
|
||||
{!value && `(${label || formatMessage(labels.unknown)})`}
|
||||
{value && (
|
||||
<Link href={renderUrl({ [id]: value })} className={styles.label} replace>
|
||||
{safeDecodeURI(label || value)}
|
||||
{safeDecodeURIComponent(label || value)}
|
||||
</Link>
|
||||
)}
|
||||
{externalUrl && (
|
||||
|
@ -2,6 +2,7 @@ export * from './queries/useApi';
|
||||
export * from './queries/useConfig';
|
||||
export * from './queries/useFilterQuery';
|
||||
export * from './queries/useLogin';
|
||||
export * from './queries/useRealtime';
|
||||
export * from './queries/useReport';
|
||||
export * from './queries/useReports';
|
||||
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 { 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 [isRunning, setIsRunning] = useState(false);
|
||||
const { get, post } = useApi();
|
||||
|
@ -1,19 +1,13 @@
|
||||
import { useEffect } from 'react';
|
||||
import useStore, { setTheme } from 'store/app';
|
||||
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';
|
||||
|
||||
const selector = (state: { theme: string }) => state.theme;
|
||||
|
||||
export function useTheme() {
|
||||
const defaultTheme =
|
||||
typeof window !== 'undefined'
|
||||
? window?.matchMedia('(prefers-color-scheme: dark)')?.matches
|
||||
? 'dark'
|
||||
: 'light'
|
||||
: 'light';
|
||||
const theme = useStore(selector) || getItem(THEME_CONFIG) || defaultTheme;
|
||||
const theme = useStore(selector) || getItem(THEME_CONFIG) || DEFAULT_THEME;
|
||||
const primaryColor = colord(THEME_COLORS[theme].primary);
|
||||
|
||||
const colors = {
|
||||
|
@ -32,18 +32,14 @@ export function DateFilter({
|
||||
const { locale } = useLocale();
|
||||
|
||||
const options = [
|
||||
{ label: formatMessage(labels.today), value: '1day' },
|
||||
{ label: formatMessage(labels.today), value: '0day' },
|
||||
{
|
||||
label: formatMessage(labels.lastHours, { x: 24 }),
|
||||
value: '24hour',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.yesterday),
|
||||
value: '-1day',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.thisWeek),
|
||||
value: '1week',
|
||||
value: '0week',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
@ -52,7 +48,7 @@ export function DateFilter({
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.thisMonth),
|
||||
value: '1month',
|
||||
value: '0month',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
@ -63,7 +59,7 @@ export function DateFilter({
|
||||
label: formatMessage(labels.lastDays, { x: 90 }),
|
||||
value: '90day',
|
||||
},
|
||||
{ label: formatMessage(labels.thisYear), value: '1year' },
|
||||
{ label: formatMessage(labels.thisYear), value: '0year' },
|
||||
showAllTime && {
|
||||
label: formatMessage(labels.allTime),
|
||||
value: 'all',
|
||||
|
@ -15,6 +15,7 @@ export const labels = defineMessages({
|
||||
username: { id: 'label.username', defaultMessage: 'Username' },
|
||||
password: { id: 'label.password', defaultMessage: 'Password' },
|
||||
role: { id: 'label.role', defaultMessage: 'Role' },
|
||||
admin: { id: 'label.admin', defaultMessage: 'Admin' },
|
||||
user: { id: 'label.user', defaultMessage: 'User' },
|
||||
viewOnly: { id: 'label.view-only', defaultMessage: 'View only' },
|
||||
manage: { id: 'label.manage', defaultMessage: 'Manage' },
|
||||
@ -222,6 +223,11 @@ export const labels = defineMessages({
|
||||
id: 'message.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({
|
||||
|
@ -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 { Loading } from 'react-basics';
|
||||
import { colord } from 'colord';
|
||||
import BarChart from './BarChart';
|
||||
import BarChart from 'components/charts/BarChart';
|
||||
import { getDateArray } from 'lib/date';
|
||||
import {
|
||||
useLocale,
|
||||
@ -10,8 +10,8 @@ import {
|
||||
useNavigation,
|
||||
useWebsiteEvents,
|
||||
} from 'components/hooks';
|
||||
import { EVENT_COLORS } from 'lib/constants';
|
||||
import { renderDateLabels, renderStatusTooltipPopup } from 'lib/charts';
|
||||
import { CHART_COLORS } from 'lib/constants';
|
||||
import { renderDateLabels } from 'lib/charts';
|
||||
|
||||
export interface EventsChartProps {
|
||||
websiteId: string;
|
||||
@ -26,7 +26,6 @@ export function EventsChart({ websiteId, className, token }: EventsChartProps) {
|
||||
const {
|
||||
query: { url, event },
|
||||
} = useNavigation();
|
||||
|
||||
const { data, isLoading } = useWebsiteEvents(websiteId, {
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
@ -38,9 +37,8 @@ export function EventsChart({ websiteId, className, token }: EventsChartProps) {
|
||||
offset,
|
||||
});
|
||||
|
||||
const datasets = useMemo(() => {
|
||||
const chartData = useMemo(() => {
|
||||
if (!data) return [];
|
||||
if (isLoading) return data;
|
||||
|
||||
const map = (data as any[]).reduce((obj, { x, t, y }) => {
|
||||
if (!obj[x]) {
|
||||
@ -56,8 +54,9 @@ export function EventsChart({ websiteId, className, token }: EventsChartProps) {
|
||||
map[key] = getDateArray(map[key], startDate, endDate, unit);
|
||||
});
|
||||
|
||||
return Object.keys(map).map((key, index) => {
|
||||
const color = colord(EVENT_COLORS[index % EVENT_COLORS.length]);
|
||||
return {
|
||||
datasets: Object.keys(map).map((key, index) => {
|
||||
const color = colord(CHART_COLORS[index % CHART_COLORS.length]);
|
||||
return {
|
||||
label: key,
|
||||
data: map[key],
|
||||
@ -66,8 +65,9 @@ export function EventsChart({ websiteId, className, token }: EventsChartProps) {
|
||||
borderColor: color.alpha(0.7).toRgbString(),
|
||||
borderWidth: 1,
|
||||
};
|
||||
});
|
||||
}, [data, isLoading, startDate, endDate, unit]);
|
||||
}),
|
||||
};
|
||||
}, [data, startDate, endDate, unit]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading icon="dots" />;
|
||||
@ -76,11 +76,10 @@ export function EventsChart({ websiteId, className, token }: EventsChartProps) {
|
||||
return (
|
||||
<BarChart
|
||||
className={className}
|
||||
datasets={datasets as any[]}
|
||||
data={chartData}
|
||||
unit={unit}
|
||||
stacked={true}
|
||||
renderXLabel={renderDateLabels(unit, locale)}
|
||||
renderTooltipPopup={renderStatusTooltipPopup(unit, locale)}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
|
@ -1,43 +1,34 @@
|
||||
import { useEffect } from 'react';
|
||||
import { StatusLight } from 'react-basics';
|
||||
import { colord } from 'colord';
|
||||
import classNames from 'classnames';
|
||||
import { LegendItem } from 'chart.js/auto';
|
||||
import { useLocale } from 'components/hooks';
|
||||
import { useForceUpdate } from 'components/hooks';
|
||||
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 forceUpdate = useForceUpdate();
|
||||
|
||||
const handleClick = (index: string | number) => {
|
||||
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)) {
|
||||
if (!items.find(({ text }) => text)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.legend}>
|
||||
{chart.legend.legendItems.map(({ text, fillStyle, datasetIndex, hidden }) => {
|
||||
{items.map(item => {
|
||||
const { text, fillStyle, hidden } = item;
|
||||
const color = colord(fillStyle);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={text}
|
||||
className={classNames(styles.label, { [styles.hidden]: hidden })}
|
||||
onClick={() => handleClick(datasetIndex)}
|
||||
onClick={() => onClick(item)}
|
||||
>
|
||||
<StatusLight color={color.alpha(color.alpha() + 0.2).toHex()}>
|
||||
<span className={locale}>{text}</span>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import BarChart, { BarChartProps } from './BarChart';
|
||||
import BarChart, { BarChartProps } from 'components/charts/BarChart';
|
||||
import { useLocale, useTheme, useMessages } from 'components/hooks';
|
||||
import { renderDateLabels, renderStatusTooltipPopup } from 'lib/charts';
|
||||
import { renderDateLabels } from 'lib/charts';
|
||||
|
||||
export interface PageviewsChartProps extends BarChartProps {
|
||||
data: {
|
||||
@ -17,10 +17,13 @@ export function PageviewsChart({ data, unit, isLoading, ...props }: PageviewsCha
|
||||
const { colors } = useTheme();
|
||||
const { locale } = useLocale();
|
||||
|
||||
const datasets = useMemo(() => {
|
||||
if (!data) return [];
|
||||
const chartData = useMemo(() => {
|
||||
if (!data) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return [
|
||||
return {
|
||||
datasets: [
|
||||
{
|
||||
label: formatMessage(labels.visitors),
|
||||
data: data.sessions,
|
||||
@ -33,17 +36,17 @@ export function PageviewsChart({ data, unit, isLoading, ...props }: PageviewsCha
|
||||
borderWidth: 1,
|
||||
...colors.chart.views,
|
||||
},
|
||||
];
|
||||
}, [data, colors, formatMessage, labels]);
|
||||
],
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<BarChart
|
||||
{...props}
|
||||
datasets={datasets}
|
||||
data={chartData}
|
||||
unit={unit}
|
||||
isLoading={isLoading}
|
||||
renderXLabel={renderDateLabels(unit, locale)}
|
||||
renderTooltipPopup={renderStatusTooltipPopup(unit, locale)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -196,7 +196,7 @@
|
||||
"label.window": "Ventana",
|
||||
"label.yesterday": "Ayer",
|
||||
"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-leave": "¿Seguro que quieres abandonar {target}?",
|
||||
"message.confirm-remove": "¿Estás seguro de que desea eliminar {target}?",
|
||||
|
@ -4,7 +4,7 @@
|
||||
"label.activity-log": "活动日志",
|
||||
"label.add": "添加",
|
||||
"label.add-description": "添加描述",
|
||||
"label.add-member": "Add member",
|
||||
"label.add-member": "添加成员",
|
||||
"label.add-website": "添加网站",
|
||||
"label.administrator": "管理员",
|
||||
"label.after": "之后",
|
||||
@ -35,7 +35,7 @@
|
||||
"label.create-team": "创建团队",
|
||||
"label.create-user": "创建用户",
|
||||
"label.created": "已创建",
|
||||
"label.created-by": "Created By",
|
||||
"label.created-by": "创建者",
|
||||
"label.current-password": "目前密码",
|
||||
"label.custom-range": "自定义时间段",
|
||||
"label.dashboard": "仪表板",
|
||||
@ -45,7 +45,7 @@
|
||||
"label.day": "日",
|
||||
"label.default-date-range": "默认时间段",
|
||||
"label.delete": "删除",
|
||||
"label.delete-report": "Delete report",
|
||||
"label.delete-report": "删除报告",
|
||||
"label.delete-team": "删除团队",
|
||||
"label.delete-user": "删除用户",
|
||||
"label.delete-website": "删除网站",
|
||||
@ -60,7 +60,7 @@
|
||||
"label.dropoff": "丢弃",
|
||||
"label.edit": "编辑",
|
||||
"label.edit-dashboard": "编辑仪表板",
|
||||
"label.edit-member": "Edit member",
|
||||
"label.edit-member": "编辑成员",
|
||||
"label.enable-share-url": "启用共享链接",
|
||||
"label.event": "事件",
|
||||
"label.event-data": "事件数据",
|
||||
@ -95,24 +95,24 @@
|
||||
"label.less-than-equals": "少于等于",
|
||||
"label.login": "登录",
|
||||
"label.logout": "退出",
|
||||
"label.manage": "Manage",
|
||||
"label.manage": "管理",
|
||||
"label.max": "最大",
|
||||
"label.member": "Member",
|
||||
"label.member": "成员",
|
||||
"label.members": "成员",
|
||||
"label.min": "最小",
|
||||
"label.mobile": "手机",
|
||||
"label.more": "更多",
|
||||
"label.my-account": "My account",
|
||||
"label.my-account": "我的账户",
|
||||
"label.my-websites": "我的网站",
|
||||
"label.name": "名字",
|
||||
"label.new-password": "新密码",
|
||||
"label.none": "无",
|
||||
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
|
||||
"label.ok": "OK",
|
||||
"label.os": "OS",
|
||||
"label.os": "操作系统",
|
||||
"label.overview": "概览",
|
||||
"label.owner": "所有者",
|
||||
"label.page-of": "总{total}中的第{current}页",
|
||||
"label.page-of": "总 {total} 中的第 {current} 页",
|
||||
"label.page-views": "页面浏览量",
|
||||
"label.pageTitle": "标题",
|
||||
"label.pages": "网页",
|
||||
@ -130,7 +130,7 @@
|
||||
"label.region": "州/省",
|
||||
"label.regions": "州/省",
|
||||
"label.remove": "移除",
|
||||
"label.remove-member": "Remove member",
|
||||
"label.remove-member": "移除成员",
|
||||
"label.reports": "报告",
|
||||
"label.required": "必填",
|
||||
"label.reset": "重置",
|
||||
@ -142,9 +142,9 @@
|
||||
"label.save": "保存",
|
||||
"label.screens": "屏幕尺寸",
|
||||
"label.search": "搜索",
|
||||
"label.select": "Select",
|
||||
"label.select": "选择",
|
||||
"label.select-date": "选择数据",
|
||||
"label.select-role": "Select role",
|
||||
"label.select-role": "选择角色",
|
||||
"label.select-website": "选择网站",
|
||||
"label.sessions": "会话",
|
||||
"label.settings": "设置",
|
||||
@ -157,7 +157,7 @@
|
||||
"label.team-member": "团队成员",
|
||||
"label.team-name": "团队名称",
|
||||
"label.team-owner": "团队所有者",
|
||||
"label.team-view-only": "Team view only",
|
||||
"label.team-view-only": "仅团队视图",
|
||||
"label.team-websites": "团队网站",
|
||||
"label.teams": "团队",
|
||||
"label.theme": "主题",
|
||||
@ -171,8 +171,8 @@
|
||||
"label.total": "总数",
|
||||
"label.total-records": "总记录数",
|
||||
"label.tracking-code": "跟踪代码",
|
||||
"label.transfer": "Transfer",
|
||||
"label.transfer-website": "Transfer website",
|
||||
"label.transfer": "转移",
|
||||
"label.transfer-website": "转移网站",
|
||||
"label.true": "是",
|
||||
"label.type": "类型",
|
||||
"label.unique": "独立",
|
||||
@ -195,21 +195,21 @@
|
||||
"label.websites": "网站",
|
||||
"label.window": "窗口",
|
||||
"label.yesterday": "昨天",
|
||||
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
|
||||
"message.action-confirmation": "在下面的框中输入 {confirmation} 以确认。",
|
||||
"message.active-users": "当前在线 {x} 人",
|
||||
"message.confirm-delete": "你确定要删除 {target} 吗?",
|
||||
"message.confirm-leave": "你确定要离开 {target} 吗?",
|
||||
"message.confirm-remove": "Are you sure you want to remove {target}?",
|
||||
"message.confirm-reset": "您确定要重置 {target} 的数据吗?",
|
||||
"message.delete-team-warning": "Deleting a team will also delete all team websites.",
|
||||
"message.confirm-remove": "您确定要移除 {target} ?",
|
||||
"message.confirm-reset": "您确定要重置 {target} 的数据吗?",
|
||||
"message.delete-team-warning": "删除团队也会删除所有团队的网站。",
|
||||
"message.delete-website-warning": "所有相关数据将会被删除。",
|
||||
"message.error": "出现错误。",
|
||||
"message.event-log": "{url}上的{event}",
|
||||
"message.event-log": "{url} 上的 {event}",
|
||||
"message.go-to-settings": "去设置",
|
||||
"message.incorrect-username-password": "用户名或密码不正确。",
|
||||
"message.invalid-domain": "无效域名",
|
||||
"message.min-password-length": "密码最短长度为 {n} 个字符",
|
||||
"message.new-version-available": "Umami的新版本{version}已推出!",
|
||||
"message.new-version-available": "Umami 的新版本 {version} 已推出!",
|
||||
"message.no-data-available": "无可用数据。",
|
||||
"message.no-event-data": "无可用事件。",
|
||||
"message.no-match-password": "密码不一致",
|
||||
@ -219,7 +219,7 @@
|
||||
"message.no-users": "没有任何用户。",
|
||||
"message.no-websites-configured": "你还没有设置任何网站。",
|
||||
"message.page-not-found": "网页未找到。",
|
||||
"message.reset-website": "如果确定重置该网站, 请在下面的输入框中输入 {confirmation} 进行二次确认。",
|
||||
"message.reset-website": "如果确定重置该网站,请在下面的输入框中输入 {confirmation} 进行二次确认。",
|
||||
"message.reset-website-warning": "本网站的所有统计数据将被删除,但您的跟踪代码将保持不变。",
|
||||
"message.saved": "保存成功。",
|
||||
"message.share-url": "这是 {target} 的共享链接。",
|
||||
@ -227,12 +227,12 @@
|
||||
"message.team-not-found": "未找到团队。",
|
||||
"message.team-websites-info": "团队中的任何人都可查看网站。",
|
||||
"message.tracking-code": "跟踪代码",
|
||||
"message.transfer-team-website-to-user": "Transfer this website to your account?",
|
||||
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
|
||||
"message.transfer-website": "Transfer website ownership to your account or another team.",
|
||||
"message.triggered-event": "Triggered event",
|
||||
"message.transfer-team-website-to-user": "将该网站转入您的账户?",
|
||||
"message.transfer-user-website-to-team": "选择要将该网站转移到哪个团队。",
|
||||
"message.transfer-website": "将网站所有权转移到您的账户或其他团队。",
|
||||
"message.triggered-event": "触发事件",
|
||||
"message.user-deleted": "用户已删除。",
|
||||
"message.viewed-page": "Viewed page",
|
||||
"message.visitor-log": "来自{country}的访客在搭载 {os} 的{device}上使用 {browser} 浏览器进行访问。",
|
||||
"message.visitors-dropped-off": "Visitors dropped off"
|
||||
"message.viewed-page": "已浏览页面",
|
||||
"message.visitor-log": "来自 {country} 的访客在搭载 {os} 的 {device} 上使用 {browser} 浏览器进行访问。",
|
||||
"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