From 9b349b53c44923e464a87c9b081d7a2ebb54b8e1 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Tue, 25 Apr 2023 22:52:53 -0700 Subject: [PATCH 01/88] limit realtime record set to 15 --- components/pages/realtime/RealtimeUrls.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/components/pages/realtime/RealtimeUrls.js b/components/pages/realtime/RealtimeUrls.js index dfbf1fda..18d8f2f6 100644 --- a/components/pages/realtime/RealtimeUrls.js +++ b/components/pages/realtime/RealtimeUrls.js @@ -10,6 +10,7 @@ export function RealtimeUrls({ websiteDomain, data = {} }) { const { formatMessage, labels } = useMessages(); const { pageviews } = data; const [filter, setFilter] = useState(FILTER_REFERRERS); + const limit = 15; const buttons = [ { @@ -47,7 +48,8 @@ export function RealtimeUrls({ websiteDomain, data = {} }) { } return arr; }, []) - .sort(firstBy('y', -1)), + .sort(firstBy('y', -1)) + .slice(0, limit), ); const pages = percentFilter( @@ -62,7 +64,8 @@ export function RealtimeUrls({ websiteDomain, data = {} }) { } return arr; }, []) - .sort(firstBy('y', -1)), + .sort(firstBy('y', -1)) + .slice(0, limit), ); return [referrers, pages]; From dcb9f69c43cca6b2656d9b864db7066f6776f71f Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Tue, 25 Apr 2023 23:25:51 -0700 Subject: [PATCH 02/88] Fix Title filters --- components/metrics/MetricsBar.js | 5 +++-- components/metrics/MetricsTable.js | 18 ++++++++++++++++-- pages/api/websites/[id]/pageviews.ts | 8 ++++---- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/components/metrics/MetricsBar.js b/components/metrics/MetricsBar.js index 25b93115..ccaf627c 100644 --- a/components/metrics/MetricsBar.js +++ b/components/metrics/MetricsBar.js @@ -16,13 +16,13 @@ export function MetricsBar({ websiteId }) { const { startDate, endDate, modified } = dateRange; const [format, setFormat] = useState(true); const { - query: { url, referrer, os, browser, device, country, region, city }, + query: { url, referrer, title, os, browser, device, country, region, city }, } = usePageQuery(); const { data, error, isLoading, isFetched } = useQuery( [ 'websites:stats', - { websiteId, modified, url, referrer, os, browser, device, country, region, city }, + { websiteId, modified, url, referrer, title, os, browser, device, country, region, city }, ], () => get(`/websites/${websiteId}/stats`, { @@ -30,6 +30,7 @@ export function MetricsBar({ websiteId }) { endAt: +endDate, url, referrer, + title, os, browser, device, diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js index 97deb39d..3ad7f434 100644 --- a/components/metrics/MetricsTable.js +++ b/components/metrics/MetricsTable.js @@ -30,7 +30,7 @@ export function MetricsTable({ const { resolveUrl, router, - query: { url, referrer, os, browser, device, country, region, city }, + query: { url, referrer, title, os, browser, device, country, region, city }, } = usePageQuery(); const { formatMessage, labels } = useMessages(); const { get, useQuery } = useApi(); @@ -38,7 +38,20 @@ export function MetricsTable({ const { data, isLoading, isFetched, error } = useQuery( [ 'websites:metrics', - { websiteId, type, modified, url, referrer, os, browser, device, country, region, city }, + { + websiteId, + type, + modified, + url, + referrer, + os, + title, + browser, + device, + country, + region, + city, + }, ], () => get(`/websites/${websiteId}/metrics`, { @@ -46,6 +59,7 @@ export function MetricsTable({ startAt: +startDate, endAt: +endDate, url, + title, referrer, os, browser, diff --git a/pages/api/websites/[id]/pageviews.ts b/pages/api/websites/[id]/pageviews.ts index 775a4d82..9dfd2264 100644 --- a/pages/api/websites/[id]/pageviews.ts +++ b/pages/api/websites/[id]/pageviews.ts @@ -16,7 +16,7 @@ export interface WebsitePageviewRequestQuery { timezone: string; url?: string; referrer?: string; - pageTitle?: string; + title?: string; os?: string; browser?: string; device?: string; @@ -40,7 +40,7 @@ export default async ( timezone, url, referrer, - pageTitle, + title, os, browser, device, @@ -71,7 +71,7 @@ export default async ( filters: { url, referrer, - pageTitle, + title, os, browser, device, @@ -88,7 +88,7 @@ export default async ( count: 'distinct website_event.', filters: { url, - pageTitle, + title, os, browser, device, From c954bb2ce74628097f1b30e1ab36a8bd0925be17 Mon Sep 17 00:00:00 2001 From: Bilguun Ochirbat Date: Wed, 26 Apr 2023 17:07:19 +0800 Subject: [PATCH 03/88] Update mn-MN.json --- lang/mn-MN.json | 108 ++++++++++++++++++++++++------------------------ 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/lang/mn-MN.json b/lang/mn-MN.json index 9e3b4499..b15076ad 100644 --- a/lang/mn-MN.json +++ b/lang/mn-MN.json @@ -1,7 +1,7 @@ { - "label.access-code": "Access code", + "label.access-code": "Хандалтын код", "label.actions": "Үйлдлүүд", - "label.activity-log": "Activity log", + "label.activity-log": "Үйл ажиллагааны бүртгэл", "label.add-website": "Веб нэмэх", "label.admin": "Админ", "label.all": "Бүх", @@ -13,27 +13,27 @@ "label.browsers": "Хөтөч", "label.cancel": "Цуцлах", "label.change-password": "Нууц үг солих", - "label.cities": "Cities", - "label.clear-all": "Clear all", - "label.confirm": "Confirm", + "label.cities": "Хотууд", + "label.clear-all": "Бүгдийг арилгах", + "label.confirm": "Батлах", "label.confirm-password": "Шинэ нууц үгээ давтах", - "label.continue": "Continue", + "label.continue": "Үргэлжлүүлэх", "label.countries": "Улс", - "label.create-team": "Create team", - "label.create-user": "Create user", - "label.created": "Created", + "label.create-team": "Баг үүсгэх", + "label.create-user": "Хэрэглэгч үүсгэх", + "label.created": "Үүсгэсэн", "label.current-password": "Ашиглаж буй нууц үг", "label.custom-range": "Дурын хугацаа", "label.dashboard": "Хянах самбар", - "label.data": "Data", - "label.date-range": "Хугацааны мужид", + "label.data": "Өгөгдөл", + "label.date-range": "Хугацааны муж", "label.default-date-range": "Өгөгдмөл хугацааны муж", "label.delete": "Устгах", - "label.delete-team": "Delete team", - "label.delete-user": "Delete user", + "label.delete-team": "Баг устгах", + "label.delete-user": "Хэрэглэгч устгах", "label.delete-website": "Веб устгах", "label.desktop": "Суурин компьютер", - "label.details": "Details", + "label.details": "Мэдээлэл", "label.devices": "Төхөөрөмж", "label.dismiss": "Үл хэргэсэх", "label.domain": "Домэйн", @@ -43,18 +43,18 @@ "label.events": "Үйлдэл", "label.filter-combined": "Нэгтгэсэн", "label.filter-raw": "Түүхий", - "label.join": "Join", - "label.join-team": "Join team", + "label.join": "Нэгдэх", + "label.join-team": "Багт нэгдэх", "label.language": "Хэл", "label.languages": "Хэл", "label.laptop": "Зөөврийн компьютер", "label.last-days": "Сүүлийн {x} хоног", "label.last-hours": "Сүүлийн {x} цаг", - "label.leave": "Leave", - "label.leave-team": "Leave team", + "label.leave": "Гарах", + "label.leave-team": "Багаас гарах", "label.login": "Нэвтрэх", "label.logout": "Гарах", - "label.members": "Members", + "label.members": "Гишүүд", "label.mobile": "Утас", "label.more": "Цааш", "label.name": "Нэр", @@ -67,80 +67,80 @@ "label.password": "Нууц үг", "label.powered-by": "{name} дээр суурилсан", "label.profile": "Бүртгэл", - "label.queries": "Queries", + "label.queries": "Query-нүүд", "label.query-parameters": "Query параметр", "label.realtime": "Яг одоо", "label.referrers": "Чиглүүлэгч", "label.refresh": "Сэргээх", - "label.regenerate": "Regenerate", - "label.regions": "Regions", - "label.remove": "Remove", + "label.regenerate": "Дахин үүсгэх", + "label.regions": "Бүсүүд", + "label.remove": "Устгах", "label.required": "Шаардлагатай", - "label.reset": "Хуучин хэвд нь оруулах", + "label.reset": "Дахин эхлүүлэх", "label.reset-website": "Тоон үзүүлэлтийг дахин эхлүүлэх", - "label.role": "Role", + "label.role": "Эрх", "label.save": "Хадгалах", "label.screens": "Дэлгэц", - "label.select-website": "Select website", + "label.select-website": "Веб сонгох", "label.sessions": "Sessions", "label.settings": "Тохиргоо", "label.share-url": "Хуваалцах холбоос", "label.single-day": "Нэг өдөр", "label.tablet": "Таблет", - "label.team": "Team", - "label.team-guest": "Team guest", - "label.team-id": "Team ID", - "label.team-member": "Team member", - "label.team-owner": "Team owner", - "label.teams": "Teams", + "label.team": "Баг", + "label.team-guest": "Багийн зочин", + "label.team-id": "Багийн ID", + "label.team-member": "Багийн гишүүн", + "label.team-owner": "Багийн эзэмшигч", + "label.teams": "Багууд", "label.theme": "Загвар", "label.this-month": "Энэ сар", "label.this-week": "Энэ долоо хоног", "label.this-year": "Энэ жил", "label.timezone": "Цагийн бүс", - "label.title": "Title", + "label.title": "Гарчиг", "label.today": "Өнөөдөр", "label.toggle-charts": "Графикийг харуулах/нуух", "label.tracking-code": "Мөрдөх код", "label.unique-visitors": "Зочин", "label.unknown": "Тодорхойгүй", - "label.user": "User", + "label.user": "Хэрэглэгч", "label.username": "Хэрэглэгчийн нэр", - "label.users": "Users", - "label.view": "View", + "label.users": "Хэрэглэгчид", + "label.view": "Харах", "label.view-details": "Дэлгэрүүлж харах", "label.views": "Үзсэн", "label.visitors": "Зочин", - "label.website-id": "Website ID", + "label.website-id": "Вебийн ID", "label.websites": "Вебүүд", "label.yesterday": "Өчигдөр", "message.active-users": "одоо {x} {x, plural, one {зочин} other {зочин}} байна", "message.confirm-delete": "Та {target}-г устгахдаа итгэлтэй байна уу?", - "message.confirm-leave": "Are you sure you want to leave {target}?", + "message.confirm-leave": "Та {target}-с гарахдаа итгэлтэй байна уу?", "message.confirm-reset": "Та {target}-н тоон үзүүлэлтүүдийг устгахдаа итгэлтэй байна уу?", - "message.delete-website": "Веб устгах", - "message.delete-website-warning": "Үүнтэй холбоотой бүх өгөгдөл устах болно.", + "message.delete-website": "Веб устгахын тулд доорх хэсэгт {confirmation} гэж бичиж, баталгаажуулна уу.", + "message.delete-website-warning": "Энэ вебтэй холбоотой бүх өгөгдөл устах болно.", "message.error": "Ямар нэг зүйл буруу боллоо.", - "message.event-log": "{event} on {url}", + "message.event-log": "{url}-д {event}", "message.go-to-settings": "Тохиргоо руу очих", "message.incorrect-username-password": "Буруу хэрэглэгчийн нэр/нууц үг.", "message.invalid-domain": "Буруу домэйн", - "message.min-password-length": "Minimum length of {n} characters", + "message.min-password-length": "Хамгийн багадаа {n} тэмдэгт", "message.no-data-available": "Өгөгдөл алга.", - "message.no-match-password": "Нууц үг тохирохгүй байна", - "message.no-teams": "You have not created any teams.", - "message.no-users": "There are no users.", + "message.no-match-password": "Нууц үг тохирохгүй байна.", + "message.no-teams": "Та ямар ч баг үүсгээгүй байна.", + "message.no-users": "Хэрэглэгч байхгүй байна.", "message.page-not-found": "Хуудас олдсонгүй.", - "message.reset-website": "Тоон үзүүлэлтийг дахин эхлүүлэх", + "message.reset-website": "Тоон үзүүлэлийг дахин эхлүүлэхийн тулд доорх хэсэгт {confirmation} гэж бичиж, баталгаажуулна уу.", "message.reset-website-warning": "Энэ вебийн бүх тоон үзүүлэлтүүдийг устгах болно. Гэхдээ мөрдөх код хэвэндээ үлдэнэ.", - "message.saved": "Амжилттай хадгаллаа.", - "message.share-url": "{target}-г нийтэд хуваалцах холбоос.", - "message.team-already-member": "You are already a member of the team.", - "message.team-not-found": "Team not found.", - "message.tracking-code": "Мөрдөх код", - "message.user-deleted": "User deleted.", + "message.saved": "Хадгалсан.", + "message.share-url": "Таны вебийн тоон үзүүлэлтүүд доорх URL дээр нийтэд харагдах болно:", + "message.team-already-member": "Та аль хэдийн энэ багийн гишүүн болсон байна.", + "message.team-not-found": "Баг олдсонгүй.", + "message.tracking-code": "Энэ вебийн хандалтуудыг мөрдөхийн тулд доорх кодыг HTML-нхээ ... хэсэгт байрлуулна уу.", + "message.user-deleted": "Хэрэглэгч устсан.", "message.visitor-log": "{country} улсаас {os} {device} дээр {browser} хөтөч ашиглан орсон", - "messages.no-team-websites": "This team does not have any websites.", + "messages.no-team-websites": "Энэ багт ямар ч веб алга.", "messages.no-websites-configured": "Та ямар нэгэн веб тохируулаагүй байна.", - "messages.team-websites-info": "Websites can be viewed by anyone on the team." + "messages.team-websites-info": "Вебийг багийн бүх гишүүд үзэж болно." } From d38dc1b5ab510e00b88dbc5dfe96a04df1b9b2e7 Mon Sep 17 00:00:00 2001 From: Felipe Valtl de Mello <3065339+valtlfelipe@users.noreply.github.com> Date: Wed, 26 Apr 2023 10:53:25 -0300 Subject: [PATCH 04/88] Update pt-BR.json translations --- lang/pt-BR.json | 96 ++++++++++++++++++++++++------------------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/lang/pt-BR.json b/lang/pt-BR.json index f02b7fb9..fa40b4d2 100644 --- a/lang/pt-BR.json +++ b/lang/pt-BR.json @@ -1,27 +1,27 @@ { - "label.access-code": "Access code", + "label.access-code": "Código de acesso", "label.actions": "Ações", - "label.activity-log": "Activity log", + "label.activity-log": "Log de atividade", "label.add-website": "Adicionar site", "label.admin": "Administrador", "label.all": "Todos", "label.all-time": "Todo o período", - "label.analytics": "Analytics", + "label.analytics": "Estatísticas", "label.average-visit-time": "Tempo médio da visita", "label.back": "Voltar", "label.bounce-rate": "Taxa de rejeição", "label.browsers": "Navegadores", "label.cancel": "Cancelar", "label.change-password": "Alterar a senha", - "label.cities": "Cities", - "label.clear-all": "Clear all", - "label.confirm": "Confirm", + "label.cities": "Cidades", + "label.clear-all": "Limpar tudo", + "label.confirm": "Confirmar", "label.confirm-password": "Confirme a nova senha", - "label.continue": "Continue", + "label.continue": "Continuar", "label.countries": "Países", - "label.create-team": "Create team", - "label.create-user": "Create user", - "label.created": "Created", + "label.create-team": "Criar time", + "label.create-user": "Criar usuário", + "label.created": "Criado", "label.current-password": "Senha atual", "label.custom-range": "Intervalo personalizado", "label.dashboard": "Painel", @@ -29,37 +29,37 @@ "label.date-range": "Intervalo de datas", "label.default-date-range": "Intervalo de datas predefinido", "label.delete": "Remover", - "label.delete-team": "Delete team", - "label.delete-user": "Delete user", + "label.delete-team": "Remover time", + "label.delete-user": "Remover usuário", "label.delete-website": "Remover site", "label.desktop": "Computador", - "label.details": "Details", + "label.details": "Detalhes", "label.devices": "Dispositivos", "label.dismiss": "Dispensar", "label.domain": "Domínio", "label.edit": "Editar", - "label.edit-dashboard": "Edit dashboard", + "label.edit-dashboard": "Editar painel", "label.enable-share-url": "Ativar link de compartilhamento", "label.events": "Eventos", "label.filter-combined": "Combinado", "label.filter-raw": "Dados brutos", - "label.join": "Join", - "label.join-team": "Join team", + "label.join": "Entrar", + "label.join-team": "Entrar no time", "label.language": "Idioma", "label.languages": "Idiomas", "label.laptop": "Notebook", "label.last-days": "Últimos {x} dias", "label.last-hours": "Últimas {x} horas", - "label.leave": "Leave", - "label.leave-team": "Leave team", + "label.leave": "Sair", + "label.leave-team": "Sair do time", "label.login": "Iniciar sessão", "label.logout": "Sair", - "label.members": "Members", + "label.members": "Membros", "label.mobile": "Celular", "label.more": "Mais", "label.name": "Nome", "label.new-password": "Nova senha", - "label.none": "None", + "label.none": "nenhum", "label.operating-systems": "Sistemas operacionais", "label.owner": "Proprietário", "label.page-views": "Visualizações de página", @@ -67,80 +67,80 @@ "label.password": "Senha", "label.powered-by": "Distribuído por {name}", "label.profile": "Perfil", - "label.queries": "Queries", + "label.queries": "Parâmetros", "label.query-parameters": "Parâmetros de Consulta", "label.realtime": "Tempo real", "label.referrers": "Referências", "label.refresh": "Atualizar", - "label.regenerate": "Regenerate", - "label.regions": "Regions", - "label.remove": "Remove", + "label.regenerate": "Regerar", + "label.regions": "Regiões", + "label.remove": "Remover", "label.required": "Obrigatório", "label.reset": "Redefinir", "label.reset-website": "Redefinir estatísticas", - "label.role": "Role", + "label.role": "Papel", "label.save": "Salvar", "label.screens": "Telas", - "label.select-website": "Select website", - "label.sessions": "Sessions", + "label.select-website": "Selecionar site", + "label.sessions": "Sessões", "label.settings": "Configurações", "label.share-url": "Link de compartilhamento", "label.single-day": "Dia específico", "label.tablet": "Tablet", - "label.team": "Team", - "label.team-guest": "Team guest", - "label.team-id": "Team ID", - "label.team-member": "Team member", - "label.team-owner": "Team owner", - "label.teams": "Teams", + "label.team": "Time", + "label.team-guest": "Convidado", + "label.team-id": "ID do Time", + "label.team-member": "Membro", + "label.team-owner": "Proprietário", + "label.teams": "Times", "label.theme": "Tema", "label.this-month": "Este mês", "label.this-week": "Esta semana", "label.this-year": "Este ano", "label.timezone": "Fuso horário", - "label.title": "Title", + "label.title": "Título", "label.today": "Hoje", "label.toggle-charts": "Mostrar/Esconder gráficos", "label.tracking-code": "Código de rastreamento", "label.unique-visitors": "Visitantes únicos", "label.unknown": "Desconhecido", - "label.user": "User", + "label.user": "Usuário", "label.username": "Nome de usuário", - "label.users": "Users", - "label.view": "View", + "label.users": "Usuários", + "label.view": "Ver", "label.view-details": "Ver detalhes", "label.views": "Visualizações", "label.visitors": "Visitantes", - "label.website-id": "Website ID", + "label.website-id": "ID do Site", "label.websites": "Sites", "label.yesterday": "Ontem", "message.active-users": "{x} {x, plural, one {visitante} other {visitantes}} neste momento", "message.confirm-delete": "Deseja realmente remover {target}?", - "message.confirm-leave": "Are you sure you want to leave {target}?", + "message.confirm-leave": "Você tem certeza que deseja sair de {target}?", "message.confirm-reset": "Você tem certeza que deseja redefinir as estatísticas de {target}?", "message.delete-website": "Remover site", "message.delete-website-warning": "Todos os dados associados também serão eliminados.", "message.error": "Ocorreu um erro.", - "message.event-log": "{event} on {url}", + "message.event-log": "{event} em {url}", "message.go-to-settings": "Ir para as configurações", "message.incorrect-username-password": "O nome de usuário e/ou senha está incorreto.", "message.invalid-domain": "Domínio inválido", - "message.min-password-length": "Minimum length of {n} characters", + "message.min-password-length": "Quantidade mínima de {n} caracteres", "message.no-data-available": "Sem dados disponíveis.", "message.no-match-password": "As senhas não correspondem", - "message.no-teams": "You have not created any teams.", - "message.no-users": "There are no users.", + "message.no-teams": "Você não criou nenhum time.", + "message.no-users": "Não há usuários.", "message.page-not-found": "Página não encontrada.", "message.reset-website": "Redefinir estatísticas", "message.reset-website-warning": "Todas as estatísticas deste site serão removidas, mas seu código de rastreamento permanecerá intacto.", "message.saved": "Salvo com sucesso.", "message.share-url": "Este é o link público de compartilhamento para {target}.", - "message.team-already-member": "You are already a member of the team.", - "message.team-not-found": "Team not found.", + "message.team-already-member": "Você já um membro do time.", + "message.team-not-found": "Time não encontrado.", "message.tracking-code": "Código de rastreamento", - "message.user-deleted": "User deleted.", + "message.user-deleted": "Usuário removido.", "message.visitor-log": "Visitante de {country} usando {browser} no {device} {os}", - "messages.no-team-websites": "This team does not have any websites.", + "messages.no-team-websites": "Este time não possui nenhum site.", "messages.no-websites-configured": "Nenhum site foi configurado ainda.", - "messages.team-websites-info": "Websites can be viewed by anyone on the team." + "messages.team-websites-info": "Os sites podem ser visualizados por qualquer membro da equipe." } From f5cb18b9b50f83b169e98d1680695c89f123ea16 Mon Sep 17 00:00:00 2001 From: Felipe Valtl de Mello <3065339+valtlfelipe@users.noreply.github.com> Date: Wed, 26 Apr 2023 10:55:12 -0300 Subject: [PATCH 05/88] Update pt-BR.json --- lang/pt-BR.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lang/pt-BR.json b/lang/pt-BR.json index fa40b4d2..590cb33c 100644 --- a/lang/pt-BR.json +++ b/lang/pt-BR.json @@ -59,7 +59,7 @@ "label.more": "Mais", "label.name": "Nome", "label.new-password": "Nova senha", - "label.none": "nenhum", + "label.none": "Nenhum", "label.operating-systems": "Sistemas operacionais", "label.owner": "Proprietário", "label.page-views": "Visualizações de página", @@ -129,7 +129,7 @@ "message.no-data-available": "Sem dados disponíveis.", "message.no-match-password": "As senhas não correspondem", "message.no-teams": "Você não criou nenhum time.", - "message.no-users": "Não há usuários.", + "message.no-users": "Não há nenhum usuário.", "message.page-not-found": "Página não encontrada.", "message.reset-website": "Redefinir estatísticas", "message.reset-website-warning": "Todas as estatísticas deste site serão removidas, mas seu código de rastreamento permanecerá intacto.", From ac0efb29aa5a8ba235c7f36429fcc0caa2ba8e85 Mon Sep 17 00:00:00 2001 From: Maxime-J Date: Thu, 27 Apr 2023 13:38:07 +0000 Subject: [PATCH 06/88] Cleanup check-db --- scripts/check-db.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/check-db.js b/scripts/check-db.js index 3fd3a908..a84a775c 100644 --- a/scripts/check-db.js +++ b/scripts/check-db.js @@ -11,7 +11,7 @@ if (process.env.SKIP_DB_CHECK) { } function getDatabaseType(url = process.env.DATABASE_URL) { - const type = process.env.DATABASE_TYPE || (url && url.split(':')[0]); + const type = url && url.split(':')[0]; if (type === 'postgres') { return 'postgresql'; @@ -20,7 +20,6 @@ function getDatabaseType(url = process.env.DATABASE_URL) { return type; } -const databaseType = getDatabaseType(); const prisma = new PrismaClient(); function success(msg) { @@ -49,10 +48,11 @@ async function checkConnection() { } } -async function checkDatabaseVersion(databaseType) { +async function checkDatabaseVersion() { const query = await prisma.$queryRaw`select version() as version`; const version = semver.valid(semver.coerce(query[0].version)); + const databaseType = getDatabaseType(); const minVersion = databaseType === 'postgresql' ? '9.4.0' : '5.7.0'; if (semver.lt(version, minVersion)) { @@ -87,7 +87,7 @@ async function applyMigration() { let err = false; for (let fn of [checkEnv, checkConnection, checkDatabaseVersion, checkV1Tables, applyMigration]) { try { - fn.name === 'checkDatabaseVersion' ? await fn(databaseType) : await fn(); + await fn(); } catch (e) { error(e.message); err = true; From 0a1ed145af1d69212352e8900e70349423f724cb Mon Sep 17 00:00:00 2001 From: Florian Roher Date: Fri, 28 Apr 2023 21:51:07 +0200 Subject: [PATCH 07/88] swiss german translations --- lang-ignore.json | 42 +++++++++------ lang/de-CH.json | 88 +++++++++++++++---------------- lang/de-DE.json | 32 ++++++------ public/intl/messages/de-CH.json | 92 ++++++++++++++++----------------- public/intl/messages/de-DE.json | 32 ++++++------ 5 files changed, 149 insertions(+), 137 deletions(-) diff --git a/lang-ignore.json b/lang-ignore.json index d4707cda..0efa7258 100644 --- a/lang-ignore.json +++ b/lang-ignore.json @@ -1,16 +1,32 @@ { "cs-CZ": ["label.reset", "metrics.device.tablet"], - "de-DE": [ - "label.administrator", - "label.name", + "de-CH": [ + "label.admin", + "label.analytics", + "label.desktop", + "label.details", "label.domain", - "label.theme", - "metrics.device.desktop", - "metrics.device.laptop", - "metrics.device.tablet", - "metrics.referrers", - "metrics.utm", - "metrics.utm_medium" + "label.laptop", + "label.tablet", + "label.name", + "label.sessions", + "label.team", + "label.team-id", + "label.teams" + ], + "de-DE": [ + "label.admin", + "label.analytics", + "label.desktop", + "label.details", + "label.domain", + "label.laptop", + "label.tablet", + "label.name", + "label.sessions", + "label.team", + "label.team-id", + "label.teams" ], "en-GB": "*", "fr-FR": ["metrics.actions", "metrics.pages"], @@ -37,9 +53,5 @@ "metrics.device.tablet", "metrics.filter.raw" ], - "pt-PT": [ - "label.websites", - "metrics.device.desktop", - "metrics.device.tablet" - ] + "pt-PT": ["label.websites", "metrics.device.desktop", "metrics.device.tablet"] } diff --git a/lang/de-CH.json b/lang/de-CH.json index 03980e5c..981f1590 100644 --- a/lang/de-CH.json +++ b/lang/de-CH.json @@ -1,7 +1,7 @@ { - "label.access-code": "Access code", + "label.access-code": "Zuegangscode", "label.actions": "Aktione", - "label.activity-log": "Activity log", + "label.activity-log": "Aktivitätsverlauf", "label.add-website": "Websiite hinzuefüege", "label.admin": "Administrator", "label.all": "Alli", @@ -13,24 +13,24 @@ "label.browsers": "Browser", "label.cancel": "Abbreche", "label.change-password": "Passwort ändere", - "label.cities": "Cities", - "label.clear-all": "Clear all", - "label.confirm": "Confirm", + "label.cities": "Städt", + "label.clear-all": "Alles lösche", + "label.confirm": "Bestätige", "label.confirm-password": "Passwort widerhole", - "label.continue": "Continue", + "label.continue": "Wiiter", "label.countries": "Länder", - "label.create-team": "Create team", - "label.create-user": "Create user", - "label.created": "Created", + "label.create-team": "Team erstelle", + "label.create-user": "Benutzer erstelle", + "label.created": "Erstellt", "label.current-password": "Jetzigs Passwort", "label.custom-range": "Benutzerdefinierte Bereich", "label.dashboard": "Übersicht", - "label.data": "Data", + "label.data": "Datä", "label.date-range": "Datumsbereich", "label.default-date-range": "Vorigstellte Datumsbereich", "label.delete": "Lösche", - "label.delete-team": "Delete team", - "label.delete-user": "Delete user", + "label.delete-team": "Team lösche", + "label.delete-user": "Benutzer lösche", "label.delete-website": "Websiite lösche", "label.desktop": "Desktop", "label.details": "Details", @@ -43,104 +43,104 @@ "label.events": "Ereigniss", "label.filter-combined": "Kombiniert", "label.filter-raw": "Rohdate", - "label.join": "Join", - "label.join-team": "Join team", + "label.join": "Biträte", + "label.join-team": "Team biträte", "label.language": "Sprach", "label.languages": "Sprache", "label.laptop": "Laptop", "label.last-days": "Letzti {x} Täg", "label.last-hours": "Letzti {x} Stunde", - "label.leave": "Leave", - "label.leave-team": "Leave team", - "label.login": "Login", + "label.leave": "Verlah", + "label.leave-team": "Team verlah", + "label.login": "Aamelde", "label.logout": "Abmelde", - "label.members": "Members", + "label.members": "Mitglieder", "label.mobile": "Handy", "label.more": "Meh", "label.name": "Name", "label.new-password": "Neus Passwort", "label.none": "Keis", - "label.operating-systems": "Betriebssystem", + "label.operating-systems": "Betriibssystem", "label.owner": "Bsitzer", "label.page-views": "Siitenufrüef", "label.pages": "Siite", "label.password": "Passwort", "label.powered-by": "Betribe dur {name}", "label.profile": "Profil", - "label.queries": "Queries", + "label.queries": "Abfrage", "label.query-parameters": "Abfragparameter", "label.realtime": "Echtzit", "label.referrers": "Referrer", "label.refresh": "Aktualisiere", - "label.regenerate": "Regenerate", - "label.regions": "Regions", - "label.remove": "Remove", + "label.regenerate": "Erneuere", + "label.regions": "Regionä", + "label.remove": "Entferne", "label.required": "Erforderlich", "label.reset": "Zruggsetze", "label.reset-website": "Statistik zruggsetze", - "label.role": "Role", + "label.role": "Rollä", "label.save": "Speichere", "label.screens": "Bildschirmuflösige", - "label.select-website": "Select website", + "label.select-website": "Websiite uuswähle", "label.sessions": "Sessions", "label.settings": "Istellige", "label.share-url": "Freigab-URL", "label.single-day": "Ein Tag", "label.tablet": "Tablet", "label.team": "Team", - "label.team-guest": "Team guest", + "label.team-guest": "Team Gast", "label.team-id": "Team ID", - "label.team-member": "Team member", - "label.team-owner": "Team owner", + "label.team-member": "Team Mitglied", + "label.team-owner": "Team Bsitzer", "label.teams": "Teams", "label.theme": "Thema", "label.this-month": "De Monet", "label.this-week": "Die Wuche", "label.this-year": "Das Jahr", - "label.timezone": "Zitzone", - "label.title": "Title", + "label.timezone": "Ziitzone", + "label.title": "Titel", "label.today": "Hüt", "label.toggle-charts": "Schaubilder umschalte", "label.tracking-code": "Tracking Code", "label.unique-visitors": "Eidütigi Bsuecher", "label.unknown": "Unbekannt", - "label.user": "User", + "label.user": "Benutzer", "label.username": "Benutzername", - "label.users": "Users", - "label.view": "View", + "label.users": "Benutzer", + "label.view": "Azeige", "label.view-details": "Details azeige", "label.views": "Ufrüef", "label.visitors": "Bsuecher", - "label.website-id": "Website ID", + "label.website-id": "Websiite ID", "label.websites": "Websiite", "label.yesterday": "Gester", "message.active-users": "{x} {x, plural, one {aktive Bsuecher} other {aktivi Bsuecher}}", "message.confirm-delete": "Sind Sie sich sicher, {target} zlösche?", - "message.confirm-leave": "Are you sure you want to leave {target}?", + "message.confirm-leave": "Sind Sie sich sicher, {target} zverlah?", "message.confirm-reset": "Sind Sie sicher, dass Sie dStatistike vo {target} zruggsetze wend?", "message.delete-website": "Websiite lösche", "message.delete-website-warning": "Alli dezueghörige Date werdet ebefalls glöscht.", "message.error": "Es isch en Fehler uftrete.", - "message.event-log": "{event} on {url}", + "message.event-log": "{event} uf {url}", "message.go-to-settings": "Zu de Istellige", "message.incorrect-username-password": "Falschs Passwort oder Benutzername.", "message.invalid-domain": "Ungültigi Domain", - "message.min-password-length": "Minimum length of {n} characters", + "message.min-password-length": "Miminamli längi vo {n} Zeiche", "message.no-data-available": "Kei Date vorhande.", "message.no-match-password": "Passwörter stimmed ned überi", - "message.no-teams": "You have not created any teams.", - "message.no-users": "There are no users.", + "message.no-teams": "Bisher sind no kei Teams erstellt worde.", + "message.no-users": "Da gits kei Benutzer", "message.page-not-found": "Siite ned gfunde.", "message.reset-website": "Statistik zruggsetze", "message.reset-website-warning": "Alli Date für die Websiite werdet glöscht, nur de Tracking Code blibt bestah.", "message.saved": "Erfolgrich gspeichert.", "message.share-url": "Das isch die öffentlichi URL zum Teile für {target}.", - "message.team-already-member": "You are already a member of the team.", - "message.team-not-found": "Team not found.", + "message.team-already-member": "Sie sind bereits es Mitglied vo dem Team.", + "message.team-not-found": "Team nöd gfunde.", "message.tracking-code": "Tracking Code", - "message.user-deleted": "User deleted.", + "message.user-deleted": "Benutzer glöscht.", "message.visitor-log": "Bsuecher us {country} benutzt {browser} uf {os} {device}", - "messages.no-team-websites": "This team does not have any websites.", + "messages.no-team-websites": "Dem Team sind kei Websiite zuegordnet.", "messages.no-websites-configured": "Es isch kei Websiite vorhande.", - "messages.team-websites-info": "Websites can be viewed by anyone on the team." + "messages.team-websites-info": "Websiite chönd vo jedem im Team agluegt werde" } diff --git a/lang/de-DE.json b/lang/de-DE.json index 0190d13c..0a69423d 100644 --- a/lang/de-DE.json +++ b/lang/de-DE.json @@ -19,8 +19,8 @@ "label.confirm-password": "Passwort wiederholen", "label.continue": "Weiter", "label.countries": "Länder", - "label.create-team": "Erstelle Team", - "label.create-user": "Erstelle Nutzer", + "label.create-team": "Team erstellen", + "label.create-user": "Benutzer erstellen", "label.created": "Erstellt", "label.current-password": "Derzeitiges Passwort", "label.custom-range": "Benutzerdefinierter Bereich", @@ -29,8 +29,8 @@ "label.date-range": "Datumsbereich", "label.default-date-range": "Voreingestellter Datumsbereich", "label.delete": "Löschen", - "label.delete-team": "Lösche Team", - "label.delete-user": "Lösche Nutzer", + "label.delete-team": "Team löschen", + "label.delete-user": "Benutzer löschen", "label.delete-website": "Webseite löschen", "label.desktop": "Desktop", "label.details": "Details", @@ -48,8 +48,8 @@ "label.language": "Sprache", "label.languages": "Sprachen", "label.laptop": "Laptop", - "label.last-days": "Letzten {x} Tage", - "label.last-hours": "Letzten {x} Stunden", + "label.last-days": "Letzte {x} Tage", + "label.last-hours": "Letzte {x} Stunden", "label.leave": "Verlassen", "label.leave-team": "Team verlassen", "label.login": "Anmelden", @@ -73,7 +73,7 @@ "label.referrers": "Referrer", "label.refresh": "Aktualisieren", "label.regenerate": "Erneuern", - "label.regions": "Regions", + "label.regions": "Regionen", "label.remove": "Entfernen", "label.required": "Erforderlich", "label.reset": "Zurücksetzen", @@ -101,17 +101,17 @@ "label.title": "Titel", "label.today": "Heute", "label.toggle-charts": "Schaubilder umschalten", - "label.tracking-code": "Tracking Kennung", + "label.tracking-code": "Tracking Code", "label.unique-visitors": "Eindeutige Besucher", "label.unknown": "Unbekannt", - "label.user": "User", + "label.user": "Benutzer", "label.username": "Benutzername", - "label.users": "Users", - "label.view": "View", + "label.users": "Benutzer", + "label.view": "Anzeigen", "label.view-details": "Details anzeigen", "label.views": "Aufrufe", "label.visitors": "Besucher", - "label.website-id": "Website ID", + "label.website-id": "Webseite ID", "label.websites": "Webseiten", "label.yesterday": "Gestern", "message.active-users": "{x} {x, plural, one {aktiver Besucher} other {aktive Besucher}}", @@ -121,7 +121,7 @@ "message.delete-website": "Webseite löschen", "message.delete-website-warning": "Alle zugehörigen Daten werden ebenfalls gelöscht.", "message.error": "Es ist ein Fehler aufgetreten.", - "message.event-log": "{event} on {url}", + "message.event-log": "{event} auf {url}", "message.go-to-settings": "Zu den Einstellungen", "message.incorrect-username-password": "Falsches Passwort oder Benutzername.", "message.invalid-domain": "Ungültige Domain", @@ -129,7 +129,7 @@ "message.no-data-available": "Keine Daten vorhanden.", "message.no-match-password": "Passwörter stimmen nicht überein", "message.no-teams": "Bisher wurden keine Teams erstellt.", - "message.no-users": "Hier gibt es keine Nutzer.", + "message.no-users": "Hier gibt es keine Benutzer.", "message.page-not-found": "Seite nicht gefunden.", "message.reset-website": "Statistik zurücksetzen", "message.reset-website-warning": "Alle Daten für diese Webseite werden gelöscht, jedoch bleibt der Tracking Code bestehen.", @@ -137,8 +137,8 @@ "message.share-url": "Dies ist die öffentliche URL zum Teilen für {target}.", "message.team-already-member": "Sie sind bereits Mitglied des Teams.", "message.team-not-found": "Team nicht gefunden.", - "message.tracking-code": "Tracking Kennung", - "message.user-deleted": "Nutzer gelöscht.", + "message.tracking-code": "Tracking Code", + "message.user-deleted": "Benutzer gelöscht.", "message.visitor-log": "Besucher aus {country} benutzt {browser} auf {os} {device}", "messages.no-team-websites": "Diesem Team sind keine Websites zugeordnet.", "messages.no-websites-configured": "Es ist keine Webseite vorhanden.", diff --git a/public/intl/messages/de-CH.json b/public/intl/messages/de-CH.json index 8e2630ad..f3cfaf6b 100644 --- a/public/intl/messages/de-CH.json +++ b/public/intl/messages/de-CH.json @@ -2,7 +2,7 @@ "label.access-code": [ { "type": 0, - "value": "Access code" + "value": "Zuegangscode" } ], "label.actions": [ @@ -14,7 +14,7 @@ "label.activity-log": [ { "type": 0, - "value": "Activity log" + "value": "Aktivitätsverlauf" } ], "label.add-website": [ @@ -86,19 +86,19 @@ "label.cities": [ { "type": 0, - "value": "Cities" + "value": "Städt" } ], "label.clear-all": [ { "type": 0, - "value": "Clear all" + "value": "Alles lösche" } ], "label.confirm": [ { "type": 0, - "value": "Confirm" + "value": "Bestätige" } ], "label.confirm-password": [ @@ -110,7 +110,7 @@ "label.continue": [ { "type": 0, - "value": "Continue" + "value": "Wiiter" } ], "label.countries": [ @@ -122,19 +122,19 @@ "label.create-team": [ { "type": 0, - "value": "Create team" + "value": "Team erstelle" } ], "label.create-user": [ { "type": 0, - "value": "Create user" + "value": "Benutzer erstelle" } ], "label.created": [ { "type": 0, - "value": "Created" + "value": "Erstellt" } ], "label.current-password": [ @@ -158,7 +158,7 @@ "label.data": [ { "type": 0, - "value": "Data" + "value": "Datä" } ], "label.date-range": [ @@ -182,13 +182,13 @@ "label.delete-team": [ { "type": 0, - "value": "Delete team" + "value": "Team lösche" } ], "label.delete-user": [ { "type": 0, - "value": "Delete user" + "value": "Benutzer lösche" } ], "label.delete-website": [ @@ -266,13 +266,13 @@ "label.join": [ { "type": 0, - "value": "Join" + "value": "Biträte" } ], "label.join-team": [ { "type": 0, - "value": "Join team" + "value": "Team biträte" } ], "label.language": [ @@ -324,19 +324,19 @@ "label.leave": [ { "type": 0, - "value": "Leave" + "value": "Verlah" } ], "label.leave-team": [ { "type": 0, - "value": "Leave team" + "value": "Team verlah" } ], "label.login": [ { "type": 0, - "value": "Login" + "value": "Aamelde" } ], "label.logout": [ @@ -348,7 +348,7 @@ "label.members": [ { "type": 0, - "value": "Members" + "value": "Mitglieder" } ], "label.mobile": [ @@ -384,7 +384,7 @@ "label.operating-systems": [ { "type": 0, - "value": "Betriebssystem" + "value": "Betriibssystem" } ], "label.owner": [ @@ -430,7 +430,7 @@ "label.queries": [ { "type": 0, - "value": "Queries" + "value": "Abfrage" } ], "label.query-parameters": [ @@ -460,19 +460,19 @@ "label.regenerate": [ { "type": 0, - "value": "Regenerate" + "value": "Erneuere" } ], "label.regions": [ { "type": 0, - "value": "Regions" + "value": "Regionä" } ], "label.remove": [ { "type": 0, - "value": "Remove" + "value": "Entferne" } ], "label.required": [ @@ -496,7 +496,7 @@ "label.role": [ { "type": 0, - "value": "Role" + "value": "Rollä" } ], "label.save": [ @@ -514,7 +514,7 @@ "label.select-website": [ { "type": 0, - "value": "Select website" + "value": "Websiite uuswähle" } ], "label.sessions": [ @@ -556,7 +556,7 @@ "label.team-guest": [ { "type": 0, - "value": "Team guest" + "value": "Team Gast" } ], "label.team-id": [ @@ -568,13 +568,13 @@ "label.team-member": [ { "type": 0, - "value": "Team member" + "value": "Team Mitglied" } ], "label.team-owner": [ { "type": 0, - "value": "Team owner" + "value": "Team Bsitzer" } ], "label.teams": [ @@ -610,13 +610,13 @@ "label.timezone": [ { "type": 0, - "value": "Zitzone" + "value": "Ziitzone" } ], "label.title": [ { "type": 0, - "value": "Title" + "value": "Titel" } ], "label.today": [ @@ -652,7 +652,7 @@ "label.user": [ { "type": 0, - "value": "User" + "value": "Benutzer" } ], "label.username": [ @@ -664,13 +664,13 @@ "label.users": [ { "type": 0, - "value": "Users" + "value": "Benutzer" } ], "label.view": [ { "type": 0, - "value": "View" + "value": "Azeige" } ], "label.view-details": [ @@ -694,7 +694,7 @@ "label.website-id": [ { "type": 0, - "value": "Website ID" + "value": "Websiite ID" } ], "label.websites": [ @@ -760,7 +760,7 @@ "message.confirm-leave": [ { "type": 0, - "value": "Are you sure you want to leave " + "value": "Sind Sie sich sicher, " }, { "type": 1, @@ -768,7 +768,7 @@ }, { "type": 0, - "value": "?" + "value": " zverlah?" } ], "message.confirm-reset": [ @@ -810,7 +810,7 @@ }, { "type": 0, - "value": " on " + "value": " uf " }, { "type": 1, @@ -838,7 +838,7 @@ "message.min-password-length": [ { "type": 0, - "value": "Minimum length of " + "value": "Miminamli längi vo " }, { "type": 1, @@ -846,7 +846,7 @@ }, { "type": 0, - "value": " characters" + "value": " Zeiche" } ], "message.no-data-available": [ @@ -864,13 +864,13 @@ "message.no-teams": [ { "type": 0, - "value": "You have not created any teams." + "value": "Bisher sind no kei Teams erstellt worde." } ], "message.no-users": [ { "type": 0, - "value": "There are no users." + "value": "Da gits kei Benutzer" } ], "message.page-not-found": [ @@ -914,13 +914,13 @@ "message.team-already-member": [ { "type": 0, - "value": "You are already a member of the team." + "value": "Sie sind bereits es Mitglied vo dem Team." } ], "message.team-not-found": [ { "type": 0, - "value": "Team not found." + "value": "Team nöd gfunde." } ], "message.tracking-code": [ @@ -932,7 +932,7 @@ "message.user-deleted": [ { "type": 0, - "value": "User deleted." + "value": "Benutzer glöscht." } ], "message.visitor-log": [ @@ -972,7 +972,7 @@ "messages.no-team-websites": [ { "type": 0, - "value": "This team does not have any websites." + "value": "Dem Team sind kei Websiite zuegordnet." } ], "messages.no-websites-configured": [ @@ -984,7 +984,7 @@ "messages.team-websites-info": [ { "type": 0, - "value": "Websites can be viewed by anyone on the team." + "value": "Websiite chönd vo jedem im Team agluegt werde" } ] } diff --git a/public/intl/messages/de-DE.json b/public/intl/messages/de-DE.json index 84e252f4..17df2e6d 100644 --- a/public/intl/messages/de-DE.json +++ b/public/intl/messages/de-DE.json @@ -122,13 +122,13 @@ "label.create-team": [ { "type": 0, - "value": "Erstelle Team" + "value": "Team erstellen" } ], "label.create-user": [ { "type": 0, - "value": "Erstelle Nutzer" + "value": "Benutzer erstellen" } ], "label.created": [ @@ -182,13 +182,13 @@ "label.delete-team": [ { "type": 0, - "value": "Lösche Team" + "value": "Team löschen" } ], "label.delete-user": [ { "type": 0, - "value": "Lösche Nutzer" + "value": "Benutzer löschen" } ], "label.delete-website": [ @@ -296,7 +296,7 @@ "label.last-days": [ { "type": 0, - "value": "Letzten " + "value": "Letzte " }, { "type": 1, @@ -310,7 +310,7 @@ "label.last-hours": [ { "type": 0, - "value": "Letzten " + "value": "Letzte " }, { "type": 1, @@ -466,7 +466,7 @@ "label.regions": [ { "type": 0, - "value": "Regions" + "value": "Regionen" } ], "label.remove": [ @@ -634,7 +634,7 @@ "label.tracking-code": [ { "type": 0, - "value": "Tracking Kennung" + "value": "Tracking Code" } ], "label.unique-visitors": [ @@ -652,7 +652,7 @@ "label.user": [ { "type": 0, - "value": "User" + "value": "Benutzer" } ], "label.username": [ @@ -664,13 +664,13 @@ "label.users": [ { "type": 0, - "value": "Users" + "value": "Benutzer" } ], "label.view": [ { "type": 0, - "value": "View" + "value": "Anzeigen" } ], "label.view-details": [ @@ -694,7 +694,7 @@ "label.website-id": [ { "type": 0, - "value": "Website ID" + "value": "Webseite ID" } ], "label.websites": [ @@ -810,7 +810,7 @@ }, { "type": 0, - "value": " on " + "value": " auf " }, { "type": 1, @@ -870,7 +870,7 @@ "message.no-users": [ { "type": 0, - "value": "Hier gibt es keine Nutzer." + "value": "Hier gibt es keine Benutzer." } ], "message.page-not-found": [ @@ -926,13 +926,13 @@ "message.tracking-code": [ { "type": 0, - "value": "Tracking Kennung" + "value": "Tracking Code" } ], "message.user-deleted": [ { "type": 0, - "value": "Nutzer gelöscht." + "value": "Benutzer gelöscht." } ], "message.visitor-log": [ From 004e5a954a1843ee5a2d3533e7b10398c247e136 Mon Sep 17 00:00:00 2001 From: Florian Roher Date: Fri, 28 Apr 2023 23:15:40 +0200 Subject: [PATCH 08/88] fix message.share-url --- lang/de-CH.json | 2 +- lang/de-DE.json | 2 +- public/intl/messages/de-CH.json | 10 +--------- public/intl/messages/de-DE.json | 10 +--------- 4 files changed, 4 insertions(+), 20 deletions(-) diff --git a/lang/de-CH.json b/lang/de-CH.json index 981f1590..f8602bc3 100644 --- a/lang/de-CH.json +++ b/lang/de-CH.json @@ -134,7 +134,7 @@ "message.reset-website": "Statistik zruggsetze", "message.reset-website-warning": "Alli Date für die Websiite werdet glöscht, nur de Tracking Code blibt bestah.", "message.saved": "Erfolgrich gspeichert.", - "message.share-url": "Das isch die öffentlichi URL zum Teile für {target}.", + "message.share-url": "Ihri Websiitestatistik isch under de folgende URL öffentlich zuegänglich:", "message.team-already-member": "Sie sind bereits es Mitglied vo dem Team.", "message.team-not-found": "Team nöd gfunde.", "message.tracking-code": "Tracking Code", diff --git a/lang/de-DE.json b/lang/de-DE.json index 0a69423d..b3611d5b 100644 --- a/lang/de-DE.json +++ b/lang/de-DE.json @@ -134,7 +134,7 @@ "message.reset-website": "Statistik zurücksetzen", "message.reset-website-warning": "Alle Daten für diese Webseite werden gelöscht, jedoch bleibt der Tracking Code bestehen.", "message.saved": "Erfolgreich gespeichert.", - "message.share-url": "Dies ist die öffentliche URL zum Teilen für {target}.", + "message.share-url": "Ihre Webseitenstatistik ist unter der folgenden URL öffentlich zugänglich:", "message.team-already-member": "Sie sind bereits Mitglied des Teams.", "message.team-not-found": "Team nicht gefunden.", "message.tracking-code": "Tracking Code", diff --git a/public/intl/messages/de-CH.json b/public/intl/messages/de-CH.json index f3cfaf6b..88a93208 100644 --- a/public/intl/messages/de-CH.json +++ b/public/intl/messages/de-CH.json @@ -900,15 +900,7 @@ "message.share-url": [ { "type": 0, - "value": "Das isch die öffentlichi URL zum Teile für " - }, - { - "type": 1, - "value": "target" - }, - { - "type": 0, - "value": "." + "value": "Ihri Websiitestatistik isch under de folgende URL öffentlich zuegänglich:" } ], "message.team-already-member": [ diff --git a/public/intl/messages/de-DE.json b/public/intl/messages/de-DE.json index 17df2e6d..d8a64915 100644 --- a/public/intl/messages/de-DE.json +++ b/public/intl/messages/de-DE.json @@ -900,15 +900,7 @@ "message.share-url": [ { "type": 0, - "value": "Dies ist die öffentliche URL zum Teilen für " - }, - { - "type": 1, - "value": "target" - }, - { - "type": 0, - "value": "." + "value": "Ihre Webseitenstatistik ist unter der folgenden URL öffentlich zugänglich:" } ], "message.team-already-member": [ From 65ebb26a700746d17a61d9cedb994b1fc651cdc1 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Sun, 30 Apr 2023 22:55:54 -0700 Subject: [PATCH 09/88] Fix boolean event data. --- components/pages/console/TestConsole.js | 2 ++ lib/eventData.ts | 1 + queries/analytics/event/getEvents.ts | 5 ++--- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/components/pages/console/TestConsole.js b/components/pages/console/TestConsole.js index 745bf94c..6c14c2c1 100644 --- a/components/pages/console/TestConsole.js +++ b/components/pages/console/TestConsole.js @@ -30,6 +30,8 @@ export function TestConsole() { window.umami.track('track-event-with-data', { data: { test: 'test-data', + boolean: true, + booleanError: 'true', time: new Date(), number: 1, time2: new Date().toISOString(), diff --git a/lib/eventData.ts b/lib/eventData.ts index 4588d081..aee1f9b4 100644 --- a/lib/eventData.ts +++ b/lib/eventData.ts @@ -49,6 +49,7 @@ function createKey(key, value, acc: { keyValues: any[]; parentKey: string }) { break; case 'boolean': eventDataType = EVENT_DATA_TYPE.boolean; + value = value ? 'true' : 'false'; break; case 'date': eventDataType = EVENT_DATA_TYPE.date; diff --git a/queries/analytics/event/getEvents.ts b/queries/analytics/event/getEvents.ts index b3853f2d..8197019d 100644 --- a/queries/analytics/event/getEvents.ts +++ b/queries/analytics/event/getEvents.ts @@ -1,7 +1,6 @@ -import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; -import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; -import { EVENT_TYPE } from 'lib/constants'; +import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; +import prisma from 'lib/prisma'; export function getEvents(...args: [websiteId: string, startAt: Date, eventType: number]) { return runQuery({ From 714a331cd2cea7683184f6c1d6fbda55866f3c19 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Mon, 1 May 2023 11:33:08 -0700 Subject: [PATCH 10/88] remove navbar logo pointer --- components/layout/NavBar.module.css | 1 - 1 file changed, 1 deletion(-) diff --git a/components/layout/NavBar.module.css b/components/layout/NavBar.module.css index 05dce2af..dd5085a0 100644 --- a/components/layout/NavBar.module.css +++ b/components/layout/NavBar.module.css @@ -27,7 +27,6 @@ gap: 10px; font-size: 16px; font-weight: 700; - cursor: pointer; min-width: 0; } From f01e3ad502f46d167c92c4006ce1ad5c93358704 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 30 Apr 2023 08:21:17 -0700 Subject: [PATCH 11/88] Update Next.js 13.2.4. --- next.config.js | 2 - package.json | 2 +- yarn.lock | 145 +++++++++++++++++++++---------------------------- 3 files changed, 62 insertions(+), 87 deletions(-) diff --git a/next.config.js b/next.config.js index 8efb45bc..7ba1a7ea 100644 --- a/next.config.js +++ b/next.config.js @@ -2,8 +2,6 @@ require('dotenv').config(); const pkg = require('./package.json'); -const CLOUD_URL = 'https://cloud.umami.is'; - const contentSecurityPolicy = ` default-src 'self'; img-src *; diff --git a/package.json b/package.json index c1981e7f..736ec659 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "kafkajs": "^2.1.0", "maxmind": "^4.3.6", "moment-timezone": "^0.5.35", - "next": "13.2.4", + "next": "13.3.1", "next-basics": "^0.27.0", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", diff --git a/yarn.lock b/yarn.lock index 47d67290..cc31006a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2301,10 +2301,10 @@ slash "^3.0.0" tiny-glob "^0.2.9" -"@next/env@13.2.4": - version "13.2.4" - resolved "https://registry.yarnpkg.com/@next/env/-/env-13.2.4.tgz#8b763700262b2445140a44a8c8d088cef676dbae" - integrity sha512-+Mq3TtpkeeKFZanPturjcXt+KHfKYnLlX6jMLyCrmpq6OOs4i1GqBOAauSkii9QeKCMTYzGppar21JU57b/GEA== +"@next/env@13.3.1": + version "13.3.1" + resolved "https://registry.yarnpkg.com/@next/env/-/env-13.3.1.tgz#589707043065f6b71d411ed9b8f1ffd057c0fd4a" + integrity sha512-EDtCoedIZC7JlUQ3uaQpSc4aVmyhbLHmQVALg7pFfQgOTjgSnn7mKtA0DiCMkYvvsx6aFb5octGMtWrOtGXW9A== "@next/eslint-plugin-next@12.3.4": version "12.3.4" @@ -2313,70 +2313,50 @@ dependencies: glob "7.1.7" -"@next/swc-android-arm-eabi@13.2.4": - version "13.2.4" - resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.2.4.tgz#758d0403771e549f9cee71cbabc0cb16a6c947c0" - integrity sha512-DWlalTSkLjDU11MY11jg17O1gGQzpRccM9Oes2yTqj2DpHndajrXHGxj9HGtJ+idq2k7ImUdJVWS2h2l/EDJOw== +"@next/swc-darwin-arm64@13.3.1": + version "13.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.3.1.tgz#2c9719dd10a9cdf63bf50a7576b05dcf78999fe8" + integrity sha512-UXPtriEc/pBP8luSLSCZBcbzPeVv+SSjs9cH/KygTbhmACye8/OOXRZO13Z2Wq1G0gLmEAIHQAOuF+vafPd2lw== -"@next/swc-android-arm64@13.2.4": - version "13.2.4" - resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-13.2.4.tgz#834d586523045110d5602e0c8aae9028835ac427" - integrity sha512-sRavmUImUCf332Gy+PjIfLkMhiRX1Ez4SI+3vFDRs1N5eXp+uNzjFUK/oLMMOzk6KFSkbiK/3Wt8+dHQR/flNg== +"@next/swc-darwin-x64@13.3.1": + version "13.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.3.1.tgz#0be90342c89e53a390ccd9bece15f7f5cd480049" + integrity sha512-lT36yYxosCfLtplFzJWgo0hrPu6/do8+msgM7oQkPeohDNdhjtjFUgOOwdSnPublLR6Mo2Ym4P/wl5OANuD2bw== -"@next/swc-darwin-arm64@13.2.4": - version "13.2.4" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.2.4.tgz#5006fca179a36ef3a24d293abadec7438dbb48c6" - integrity sha512-S6vBl+OrInP47TM3LlYx65betocKUUlTZDDKzTiRDbsRESeyIkBtZ6Qi5uT2zQs4imqllJznVjFd1bXLx3Aa6A== +"@next/swc-linux-arm64-gnu@13.3.1": + version "13.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.3.1.tgz#a7353265839f8b8569a346a444dc3ab3770d297e" + integrity sha512-wRb76nLWJhonH8s3kxC/1tFguEkeOPayIwe9mkaz1G/yeS3OrjeyKMJsb4+Kdg0zbTo53bNCOl59NNtDM7yyyw== -"@next/swc-darwin-x64@13.2.4": - version "13.2.4" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.2.4.tgz#6549c7c04322766acc3264ccdb3e1b43fcaf7946" - integrity sha512-a6LBuoYGcFOPGd4o8TPo7wmv5FnMr+Prz+vYHopEDuhDoMSHOnC+v+Ab4D7F0NMZkvQjEJQdJS3rqgFhlZmKlw== +"@next/swc-linux-arm64-musl@13.3.1": + version "13.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.3.1.tgz#24552e6102c350e372f83f505a1d93c880551a50" + integrity sha512-qz3BzjJRZ16Iq/jrp+pjiYOc0jTjHlfmxQmZk9x/+5uhRP6/eWQSTAPVJ33BMo6oK5O5N4644OgTAbzXzorecg== -"@next/swc-freebsd-x64@13.2.4": - version "13.2.4" - resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.2.4.tgz#0bbe28979e3e868debc2cc06e45e186ce195b7f4" - integrity sha512-kkbzKVZGPaXRBPisoAQkh3xh22r+TD+5HwoC5bOkALraJ0dsOQgSMAvzMXKsN3tMzJUPS0tjtRf1cTzrQ0I5vQ== +"@next/swc-linux-x64-gnu@13.3.1": + version "13.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.3.1.tgz#5f335a683b6eafa52307b12af97782993b6c45ff" + integrity sha512-6mgkLmwlyWlomQmpl21I3hxgqE5INoW4owTlcLpNsd1V4wP+J46BlI/5zV5KWWbzjfncIqzXoeGs5Eg+1GHODA== -"@next/swc-linux-arm-gnueabihf@13.2.4": - version "13.2.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.2.4.tgz#1d28d2203f5a7427d6e7119d7bcb5fc40959fb3e" - integrity sha512-7qA1++UY0fjprqtjBZaOA6cas/7GekpjVsZn/0uHvquuITFCdKGFCsKNBx3S0Rpxmx6WYo0GcmhNRM9ru08BGg== +"@next/swc-linux-x64-musl@13.3.1": + version "13.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.3.1.tgz#58e5aad6f97203a0788783f66324456c8f9cdb50" + integrity sha512-uqm5sielhQmKJM+qayIhgZv1KlS5pqTdQ99b+Z7hMWryXS96qE0DftTmMZowBcUL6x7s2vSXyH5wPtO1ON7LBg== -"@next/swc-linux-arm64-gnu@13.2.4": - version "13.2.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.2.4.tgz#eb26448190948cdf4c44b8f34110a3ecea32f1d0" - integrity sha512-xzYZdAeq883MwXgcwc72hqo/F/dwUxCukpDOkx/j1HTq/J0wJthMGjinN9wH5bPR98Mfeh1MZJ91WWPnZOedOg== +"@next/swc-win32-arm64-msvc@13.3.1": + version "13.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.3.1.tgz#f8ed1badab57ed4503969758754e6fb0cf326753" + integrity sha512-WomIiTj/v3LevltlibNQKmvrOymNRYL+a0dp5R73IwPWN5FvXWwSELN/kiNALig/+T3luc4qHNTyvMCp9L6U5Q== -"@next/swc-linux-arm64-musl@13.2.4": - version "13.2.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.2.4.tgz#c4227c0acd94a420bb14924820710e6284d234d3" - integrity sha512-8rXr3WfmqSiYkb71qzuDP6I6R2T2tpkmf83elDN8z783N9nvTJf2E7eLx86wu2OJCi4T05nuxCsh4IOU3LQ5xw== +"@next/swc-win32-ia32-msvc@13.3.1": + version "13.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.3.1.tgz#7f599c8975b09ee5527cc49b9e5a4d13be50635a" + integrity sha512-M+PoH+0+q658wRUbs285RIaSTYnGBSTdweH/0CdzDgA6Q4rBM0sQs4DHmO3BPP0ltCO/vViIoyG7ks66XmCA5g== -"@next/swc-linux-x64-gnu@13.2.4": - version "13.2.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.2.4.tgz#6bcb540944ee9b0209b33bfc23b240c2044dfc3e" - integrity sha512-Ngxh51zGSlYJ4EfpKG4LI6WfquulNdtmHg1yuOYlaAr33KyPJp4HeN/tivBnAHcZkoNy0hh/SbwDyCnz5PFJQQ== - -"@next/swc-linux-x64-musl@13.2.4": - version "13.2.4" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.2.4.tgz#ce21e43251eaf09a09df39372b2c3e38028c30ff" - integrity sha512-gOvwIYoSxd+j14LOcvJr+ekd9fwYT1RyMAHOp7znA10+l40wkFiMONPLWiZuHxfRk+Dy7YdNdDh3ImumvL6VwA== - -"@next/swc-win32-arm64-msvc@13.2.4": - version "13.2.4" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.2.4.tgz#68220063d8e5e082f5465498675640dedb670ff1" - integrity sha512-q3NJzcfClgBm4HvdcnoEncmztxrA5GXqKeiZ/hADvC56pwNALt3ngDC6t6qr1YW9V/EPDxCYeaX4zYxHciW4Dw== - -"@next/swc-win32-ia32-msvc@13.2.4": - version "13.2.4" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.2.4.tgz#7c120ab54a081be9566df310bed834f168252990" - integrity sha512-/eZ5ncmHUYtD2fc6EUmAIZlAJnVT2YmxDsKs1Ourx0ttTtvtma/WKlMV5NoUsyOez0f9ExLyOpeCoz5aj+MPXw== - -"@next/swc-win32-x64-msvc@13.2.4": - version "13.2.4" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.2.4.tgz#5abda92fe12b9829bf7951c4a221282c56041144" - integrity sha512-0MffFmyv7tBLlji01qc0IaPP/LVExzvj7/R5x1Jph1bTAIj4Vu81yFQWHHQAP6r4ff9Ukj1mBK6MDNVXm7Tcvw== +"@next/swc-win32-x64-msvc@13.3.1": + version "13.3.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.3.1.tgz#192d43ab44ebb98bd4f5865d0e1d7ce62703182f" + integrity sha512-Sl1F4Vp5Z1rNXWZYqJwMuWRRol4bqOB6+/d7KqkgQ4AcafKPN1PZmpkCoxv4UFHtFNIB7EotnuIhtXu3zScicQ== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -2912,10 +2892,10 @@ "@svgr/plugin-jsx" "^6.5.1" "@svgr/plugin-svgo" "^6.5.1" -"@swc/helpers@0.4.14": - version "0.4.14" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.14.tgz#1352ac6d95e3617ccb7c1498ff019654f1e12a74" - integrity sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw== +"@swc/helpers@0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.0.tgz#bf1d807b60f7290d0ec763feea7ccdeda06e85f1" + integrity sha512-SjY/p4MmECVVEWspzSRpQEM3sjR17sP8PbGxELWrT+YZMBfiUyt1MRUNjMV23zohwlG2HYtCQOsCwsTHguXkyg== dependencies: tslib "^2.4.0" @@ -3847,7 +3827,7 @@ builtin-modules@^3.3.0: resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== -busboy@^1.6.0: +busboy@1.6.0, busboy@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== @@ -7034,30 +7014,27 @@ next-basics@^0.27.0: bcryptjs "^2.4.3" jsonwebtoken "^9.0.0" -next@13.2.4: - version "13.2.4" - resolved "https://registry.yarnpkg.com/next/-/next-13.2.4.tgz#2363330392b0f7da02ab41301f60857ffa7f67d6" - integrity sha512-g1I30317cThkEpvzfXujf0O4wtaQHtDCLhlivwlTJ885Ld+eOgcz7r3TGQzeU+cSRoNHtD8tsJgzxVdYojFssw== +next@13.3.1: + version "13.3.1" + resolved "https://registry.yarnpkg.com/next/-/next-13.3.1.tgz#17625f7423db2e059d71b41bd9031756cf2b33bc" + integrity sha512-eByWRxPzKHs2oQz1yE41LX35umhz86ZSZ+mYyXBqn2IBi2hyUqxBA88avywdr4uyH+hCJczegGsDGWbzQA5Rqw== dependencies: - "@next/env" "13.2.4" - "@swc/helpers" "0.4.14" + "@next/env" "13.3.1" + "@swc/helpers" "0.5.0" + busboy "1.6.0" caniuse-lite "^1.0.30001406" postcss "8.4.14" styled-jsx "5.1.1" optionalDependencies: - "@next/swc-android-arm-eabi" "13.2.4" - "@next/swc-android-arm64" "13.2.4" - "@next/swc-darwin-arm64" "13.2.4" - "@next/swc-darwin-x64" "13.2.4" - "@next/swc-freebsd-x64" "13.2.4" - "@next/swc-linux-arm-gnueabihf" "13.2.4" - "@next/swc-linux-arm64-gnu" "13.2.4" - "@next/swc-linux-arm64-musl" "13.2.4" - "@next/swc-linux-x64-gnu" "13.2.4" - "@next/swc-linux-x64-musl" "13.2.4" - "@next/swc-win32-arm64-msvc" "13.2.4" - "@next/swc-win32-ia32-msvc" "13.2.4" - "@next/swc-win32-x64-msvc" "13.2.4" + "@next/swc-darwin-arm64" "13.3.1" + "@next/swc-darwin-x64" "13.3.1" + "@next/swc-linux-arm64-gnu" "13.3.1" + "@next/swc-linux-arm64-musl" "13.3.1" + "@next/swc-linux-x64-gnu" "13.3.1" + "@next/swc-linux-x64-musl" "13.3.1" + "@next/swc-win32-arm64-msvc" "13.3.1" + "@next/swc-win32-ia32-msvc" "13.3.1" + "@next/swc-win32-x64-msvc" "13.3.1" nice-try@^1.0.4: version "1.0.5" From c41fb5ee1b2748416b9527bb8da0d26e3f06597c Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 1 May 2023 08:31:41 -0700 Subject: [PATCH 12/88] Updated packages. --- yarn.lock | 114 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 64 insertions(+), 50 deletions(-) diff --git a/yarn.lock b/yarn.lock index cc31006a..d98ada11 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1518,6 +1518,13 @@ dependencies: regenerator-runtime "^0.13.10" +"@babel/runtime@^7.21.0": + version "7.21.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200" + integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q== + dependencies: + regenerator-runtime "^0.13.11" + "@babel/runtime@^7.8.4": version "7.20.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.0.tgz#824a9ef325ffde6f78056059db3168c08785e24a" @@ -1862,9 +1869,9 @@ eslint-visitor-keys "^3.3.0" "@eslint-community/regexpp@^4.4.0": - version "4.5.0" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.0.tgz#f6f729b02feee2c749f57e334b7a1b5f40a81724" - integrity sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ== + version "4.5.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884" + integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ== "@eslint/eslintrc@^2.0.2": version "2.0.2" @@ -3131,14 +3138,14 @@ integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== "@typescript-eslint/eslint-plugin@^5.50.0": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz#9b09ee1541bff1d2cebdcb87e7ce4a4003acde08" - integrity sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg== + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.2.tgz#684a2ce7182f3b4dac342eef7caa1c2bae476abd" + integrity sha512-yVrXupeHjRxLDcPKL10sGQ/QlVrA8J5IYOEWVqk0lJaSZP7X5DfnP7Ns3cc74/blmbipQ1htFNVGsHX6wsYm0A== dependencies: "@eslint-community/regexpp" "^4.4.0" - "@typescript-eslint/scope-manager" "5.59.1" - "@typescript-eslint/type-utils" "5.59.1" - "@typescript-eslint/utils" "5.59.1" + "@typescript-eslint/scope-manager" "5.59.2" + "@typescript-eslint/type-utils" "5.59.2" + "@typescript-eslint/utils" "5.59.2" debug "^4.3.4" grapheme-splitter "^1.0.4" ignore "^5.2.0" @@ -3157,13 +3164,13 @@ debug "^4.3.4" "@typescript-eslint/parser@^5.50.0": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.1.tgz#73c2c12127c5c1182d2e5b71a8fa2a85d215cbb4" - integrity sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g== + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.2.tgz#c2c443247901d95865b9f77332d9eee7c55655e8" + integrity sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ== dependencies: - "@typescript-eslint/scope-manager" "5.59.1" - "@typescript-eslint/types" "5.59.1" - "@typescript-eslint/typescript-estree" "5.59.1" + "@typescript-eslint/scope-manager" "5.59.2" + "@typescript-eslint/types" "5.59.2" + "@typescript-eslint/typescript-estree" "5.59.2" debug "^4.3.4" "@typescript-eslint/scope-manager@5.45.0": @@ -3174,21 +3181,21 @@ "@typescript-eslint/types" "5.45.0" "@typescript-eslint/visitor-keys" "5.45.0" -"@typescript-eslint/scope-manager@5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz#8a20222719cebc5198618a5d44113705b51fd7fe" - integrity sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA== +"@typescript-eslint/scope-manager@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz#f699fe936ee4e2c996d14f0fdd3a7da5ba7b9a4c" + integrity sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA== dependencies: - "@typescript-eslint/types" "5.59.1" - "@typescript-eslint/visitor-keys" "5.59.1" + "@typescript-eslint/types" "5.59.2" + "@typescript-eslint/visitor-keys" "5.59.2" -"@typescript-eslint/type-utils@5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz#63981d61684fd24eda2f9f08c0a47ecb000a2111" - integrity sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw== +"@typescript-eslint/type-utils@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.2.tgz#0729c237503604cd9a7084b5af04c496c9a4cdcf" + integrity sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ== dependencies: - "@typescript-eslint/typescript-estree" "5.59.1" - "@typescript-eslint/utils" "5.59.1" + "@typescript-eslint/typescript-estree" "5.59.2" + "@typescript-eslint/utils" "5.59.2" debug "^4.3.4" tsutils "^3.21.0" @@ -3197,10 +3204,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.45.0.tgz#794760b9037ee4154c09549ef5a96599621109c5" integrity sha512-QQij+u/vgskA66azc9dCmx+rev79PzX8uDHpsqSjEFtfF2gBUTRCpvYMh2gw2ghkJabNkPlSUCimsyBEQZd1DA== -"@typescript-eslint/types@5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.1.tgz#03f3fedd1c044cb336ebc34cc7855f121991f41d" - integrity sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg== +"@typescript-eslint/types@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.2.tgz#b511d2b9847fe277c5cb002a2318bd329ef4f655" + integrity sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w== "@typescript-eslint/typescript-estree@5.45.0": version "5.45.0" @@ -3215,30 +3222,30 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz#4aa546d27fd0d477c618f0ca00b483f0ec84c43c" - integrity sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA== +"@typescript-eslint/typescript-estree@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz#6e2fabd3ba01db5d69df44e0b654c0b051fe9936" + integrity sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q== dependencies: - "@typescript-eslint/types" "5.59.1" - "@typescript-eslint/visitor-keys" "5.59.1" + "@typescript-eslint/types" "5.59.2" + "@typescript-eslint/visitor-keys" "5.59.2" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.1.tgz#d89fc758ad23d2157cfae53f0b429bdf15db9473" - integrity sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA== +"@typescript-eslint/utils@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.2.tgz#0c45178124d10cc986115885688db6abc37939f4" + integrity sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.59.1" - "@typescript-eslint/types" "5.59.1" - "@typescript-eslint/typescript-estree" "5.59.1" + "@typescript-eslint/scope-manager" "5.59.2" + "@typescript-eslint/types" "5.59.2" + "@typescript-eslint/typescript-estree" "5.59.2" eslint-scope "^5.1.1" semver "^7.3.7" @@ -3250,12 +3257,12 @@ "@typescript-eslint/types" "5.45.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz#0d96c36efb6560d7fb8eb85de10442c10d8f6058" - integrity sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA== +"@typescript-eslint/visitor-keys@5.59.2": + version "5.59.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz#37a419dc2723a3eacbf722512b86d6caf7d3b750" + integrity sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig== dependencies: - "@typescript-eslint/types" "5.59.1" + "@typescript-eslint/types" "5.59.2" eslint-visitor-keys "^3.3.0" "@umami/prisma-client@^0.2.0": @@ -4485,7 +4492,14 @@ date-fns-tz@^1.1.4: resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-1.3.8.tgz#083e3a4e1f19b7857fa0c18deea6c2bc46ded7b9" integrity sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ== -date-fns@^2.23.0, date-fns@^2.29.3: +date-fns@^2.23.0: + version "2.30.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" + integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== + dependencies: + "@babel/runtime" "^7.21.0" + +date-fns@^2.29.3: version "2.29.3" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== From 5692c0e07df333eb56207860c05997ab23c7658b Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Tue, 2 May 2023 23:44:42 -0700 Subject: [PATCH 13/88] add formatted message to reset website --- components/messages.js | 4 ++++ components/pages/settings/websites/WebsiteResetForm.js | 9 +++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/components/messages.js b/components/messages.js index aa268225..245e8591 100644 --- a/components/messages.js +++ b/components/messages.js @@ -158,6 +158,10 @@ export const messages = defineMessages({ id: 'message.team-already-member', defaultMessage: 'You are already a member of the team.', }, + deleteAccount: { + id: 'message.delete-account', + defaultMessage: 'To delete this account, type {confirmation} in the box below to confirm.', + }, deleteWebsite: { id: 'message.delete-website', defaultMessage: 'To delete this website, type {confirmation} in the box below to confirm.', diff --git a/components/pages/settings/websites/WebsiteResetForm.js b/components/pages/settings/websites/WebsiteResetForm.js index 4ac24169..5fc0acf7 100644 --- a/components/pages/settings/websites/WebsiteResetForm.js +++ b/components/pages/settings/websites/WebsiteResetForm.js @@ -13,7 +13,7 @@ import useMessages from 'hooks/useMessages'; const CONFIRM_VALUE = 'RESET'; export function WebsiteResetForm({ websiteId, onSave, onClose }) { - const { formatMessage, labels, messages } = useMessages(); + const { formatMessage, labels, messages, FormattedMessage } = useMessages(); const { post, useMutation } = useApi(); const { mutate, error } = useMutation(data => post(`/websites/${websiteId}/reset`, data)); @@ -28,7 +28,12 @@ export function WebsiteResetForm({ websiteId, onSave, onClose }) { return (
-

{formatMessage(messages.resetWebsite, { confirmation: CONFIRM_VALUE })}

+

+ {CONFIRM_VALUE} }} + /> +

value === CONFIRM_VALUE }}> From d00d4f7343f06873f850a6faa91a17d734fc81bb Mon Sep 17 00:00:00 2001 From: C-A de Salaberry Date: Wed, 3 May 2023 19:52:59 +0200 Subject: [PATCH 14/88] chore(tracker): :label: add types for tracker --- tracker/index.d.ts | 138 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 tracker/index.d.ts diff --git a/tracker/index.d.ts b/tracker/index.d.ts new file mode 100644 index 00000000..67cebc08 --- /dev/null +++ b/tracker/index.d.ts @@ -0,0 +1,138 @@ +type TrackedProperties = { + /** + * Hostname of server + * + * @description extracted from `window.location.hostname` + * @example 'analytics.umami.is' + */ + hostname: string; + + /** + * Browser language + * + * @description extracted from `window.navigator.language` + * @example 'en-US', 'fr-FR' + */ + language: string; + + /** + * Page referrer + * + * @description extracted from `window.navigator.language` + * @example 'https://analytics.umami.is/docs/getting-started' + */ + referrer: string; + + /** + * Screen dimensions + * + * @description extracted from `window.screen.width` and `window.screen.height` + * @example '1920x1080', '2560x1440' + */ + screen: string; + + /** + * Page title + * + * @description extracted from `document.querySelector('head > title')` + * @example 'umami' + */ + title: string; + + /** + * Page url + * + * @description built from `${window.location.pathname}${window.location.search}` + * @example 'docs/getting-started' + */ + url: string; + + /** + * Website ID (required) + * + * @example 'b59e9c65-ae32-47f1-8400-119fcf4861c4' + */ + website: string; +}; + +type WithRequired = T & { [P in K]-?: T[P] } + +/** + * + * Event Data can work with any JSON data. There are a few rules in place to maintain performance. + * - Numbers have a max precision of 4. + * - Strings have a max length of 500. + * - Arrays are converted to a String, with the same max length of 500. + * - Objects have a max of 50 properties. Arrays are considered 1 property. + */ +type EventData = Record; +type EventProperties = { + name: string; + data?: EventData; +} & WithRequired +| WithRequired; + +interface Window { + umami: { + track: { + /** + * Track a page view + * + * @example ``` + * umami.track(); + * ``` + */ + (): Promise; + + /** + * Track an event with a given name + * + * @example ``` + * umami.track('signup-button'); + * ``` + */ + (eventName: string): Promise; + + /** + * Tracks an event with dynamic data. + * + * When tracking events, the default properties are included in the payload. This is equivalent to running: + * + * ```js + * umami.track(props => ({ + * ...props, + * name: 'signup-button', + * data: { + * name: 'newsletter', + * id: 123 + * } + * })); + * ``` + * + * @example ``` + * umami.track('signup-button', { name: 'newsletter', id: 123 }); + * ``` + */ + (eventName: string, obj: EventData): Promise; + + /** + * Tracks a page view with custom properties + * + * @example ``` + * umami.track({ website: 'e676c9b4-11e4-4ef1-a4d7-87001773e9f2', url: '/home', title: 'Home page' }); + * ``` + */ + (properties: WithRequired, 'website'>): Promise; + + /** + * Tracks an event with fully customizable dynamic data + * Ilf you don't specify any `name` and/or `data`, it will be treated as a page view + * + * @example ``` + * umami.track((props) => ({ ...props, url: path })); + * ``` + */ + (eventFunction: (prop: TrackedProperties) => EventProperties): Promise; + }; + }; +} From d5b413eb90e444edc0aa114fb0e5c57804adbd18 Mon Sep 17 00:00:00 2001 From: Marc Hagen Date: Thu, 4 May 2023 01:43:22 +0200 Subject: [PATCH 15/88] Update NL language --- lang-ignore.json | 21 +++++----- lang/nl-NL.json | 100 +++++++++++++++++++++++------------------------ 2 files changed, 60 insertions(+), 61 deletions(-) diff --git a/lang-ignore.json b/lang-ignore.json index d4707cda..de185407 100644 --- a/lang-ignore.json +++ b/lang-ignore.json @@ -22,12 +22,15 @@ ], "nb-NO": ["label.administrator", "label.dashboard"], "nl-NL": [ - "label.administrator", - "label.websites", - "metrics.browsers", - "metrics.device.desktop", - "metrics.device.laptop", - "metrics.device.tablet" + "label.analytics", + "label.browsers", + "label.laptop", + "label.tablet", + "label.team", + "label.team-id", + "label.teams", + "label.website-id", + "label.websites" ], "it-IT": [ "label.password", @@ -37,9 +40,5 @@ "metrics.device.tablet", "metrics.filter.raw" ], - "pt-PT": [ - "label.websites", - "metrics.device.desktop", - "metrics.device.tablet" - ] + "pt-PT": ["label.websites", "metrics.device.desktop", "metrics.device.tablet"] } diff --git a/lang/nl-NL.json b/lang/nl-NL.json index bac675f9..b86ff423 100644 --- a/lang/nl-NL.json +++ b/lang/nl-NL.json @@ -1,9 +1,9 @@ { - "label.access-code": "Access code", + "label.access-code": "Toegangscode", "label.actions": "Acties", - "label.activity-log": "Activity log", - "label.add-website": "Website toevoegen", - "label.admin": "Administrator", + "label.activity-log": "Activiteiten logboek", + "label.add-website": "Website koppelen", + "label.admin": "Beheerder", "label.all": "Alles", "label.all-time": "Onbeperkt", "label.analytics": "Analytics", @@ -13,48 +13,48 @@ "label.browsers": "Browsers", "label.cancel": "Annuleren", "label.change-password": "Wachtwoord wijzigen", - "label.cities": "Cities", - "label.clear-all": "Clear all", - "label.confirm": "Confirm", + "label.cities": "Steden", + "label.clear-all": "Filters wissen", + "label.confirm": "Bevestigen", "label.confirm-password": "Wachtwoord bevestigen", - "label.continue": "Continue", + "label.continue": "Doorgaan", "label.countries": "Landen", - "label.create-team": "Create team", - "label.create-user": "Create user", - "label.created": "Created", + "label.create-team": "Team aanmaken", + "label.create-user": "Gebruiker maken", + "label.created": "Gemaakt", "label.current-password": "Huidig wachtwoord", "label.custom-range": "Aangepast bereik", "label.dashboard": "Overzicht", - "label.data": "Data", + "label.data": "Gegevens", "label.date-range": "Datumbereik", "label.default-date-range": "Standaard bereik", "label.delete": "Verwijderen", - "label.delete-team": "Delete team", - "label.delete-user": "Delete user", + "label.delete-team": "Team verwijderen", + "label.delete-user": "Verwijder gebruiker", "label.delete-website": "Website verwijderen", - "label.desktop": "Desktop", - "label.details": "Details", + "label.desktop": "Computer", + "label.details": "Informatie", "label.devices": "Apparaten", "label.dismiss": "Negeren", "label.domain": "Domein", "label.edit": "Bewerken", - "label.edit-dashboard": "Edit dashboard", + "label.edit-dashboard": "Dashboard aanpassen", "label.enable-share-url": "Sta delen via openbare URL toe", "label.events": "Gebeurtenissen", "label.filter-combined": "Gecombineerd", "label.filter-raw": "Ruw", - "label.join": "Join", - "label.join-team": "Join team", + "label.join": "Lid worden", + "label.join-team": "Word lid van een team", "label.language": "Taal", - "label.languages": "Languages", + "label.languages": "Talen", "label.laptop": "Laptop", "label.last-days": "Laatste {x} dagen", "label.last-hours": "Laatste {x} uur", - "label.leave": "Leave", - "label.leave-team": "Leave team", + "label.leave": "Verlaten", + "label.leave-team": "Verlaat team", "label.login": "Inloggen", "label.logout": "Uitloggen", - "label.members": "Members", + "label.members": "Gebruikers", "label.mobile": "Mobiel", "label.more": "Toon meer", "label.name": "Naam", @@ -67,80 +67,80 @@ "label.password": "Wachtwoord", "label.powered-by": "mogelijk gemaakt door {name}", "label.profile": "Profiel", - "label.queries": "Queries", - "label.query-parameters": "Query parameters", + "label.queries": "Parameters", + "label.query-parameters": "URL-parameters", "label.realtime": "Actueel", "label.referrers": "Verwijzers", "label.refresh": "Vernieuwen", - "label.regenerate": "Regenerate", - "label.regions": "Regions", - "label.remove": "Remove", + "label.regenerate": "Opnieuw genereren", + "label.regions": "Regio's", + "label.remove": "Verwijderen", "label.required": "Verplicht", - "label.reset": "Resetten", + "label.reset": "Opnieuw instellen", "label.reset-website": "Statistieken opnieuw instellen", - "label.role": "Role", + "label.role": "Gebruikersrol", "label.save": "Opslaan", "label.screens": "Schermen", - "label.select-website": "Select website", - "label.sessions": "Sessions", + "label.select-website": "Website selecteren", + "label.sessions": "Sessies", "label.settings": "Instellingen", "label.share-url": "URL delen", "label.single-day": "Enkele dag", "label.tablet": "Tablet", "label.team": "Team", - "label.team-guest": "Team guest", + "label.team-guest": "Team gast", "label.team-id": "Team ID", - "label.team-member": "Team member", - "label.team-owner": "Team owner", + "label.team-member": "Teamlid", + "label.team-owner": "Teameigenaar", "label.teams": "Teams", "label.theme": "Thema", "label.this-month": "Deze maand", "label.this-week": "Deze week", "label.this-year": "Dit jaar", "label.timezone": "Tijdzone", - "label.title": "Title", + "label.title": "Titel", "label.today": "Vandaag", "label.toggle-charts": "Grafieken tonen/verbergen", "label.tracking-code": "Volgcode", "label.unique-visitors": "Unieke bezoekers", "label.unknown": "Onbekend", - "label.user": "User", + "label.user": "Gebruiker", "label.username": "Gebruikersnaam", - "label.users": "Users", - "label.view": "View", + "label.users": "Gebruikers", + "label.view": "Weergave", "label.view-details": "Meer details", "label.views": "Weergaven", "label.visitors": "Bezoekers", "label.website-id": "Website ID", "label.websites": "Websites", - "label.yesterday": "Yesterday", + "label.yesterday": "Gisteren", "message.active-users": "{x} actieve {x, plural, one {bezoeker} other {bezoekers}}", "message.confirm-delete": "Weet je zeker dat je {target} wilt verwijderen?", - "message.confirm-leave": "Are you sure you want to leave {target}?", + "message.confirm-leave": "Weet je zeker dat je {target} wilt verlaten?", "message.confirm-reset": "Weet je zeker dat je de statistieken van {target} opnieuw wilt instellen?", "message.delete-website": "Website verwijderen", "message.delete-website-warning": "Alle verwante gegezens zullen ook verwijderd worden.", "message.error": "Er is iets misgegaan.", - "message.event-log": "{event} on {url}", + "message.event-log": "{event} op {url}", "message.go-to-settings": "Naar instellingen", "message.incorrect-username-password": "Incorrecte gebruikersnaam/wachtwoord.", "message.invalid-domain": "Ongeldig domein", - "message.min-password-length": "Minimum length of {n} characters", + "message.min-password-length": "Minimale lengte van {n} tekens", "message.no-data-available": "Geen gegevens beschikbaar.", "message.no-match-password": "Wachtwoorden komen niet overeen", - "message.no-teams": "You have not created any teams.", - "message.no-users": "There are no users.", + "message.no-teams": "Er zijn nog geen teams aangemaakt.", + "message.no-users": "Er zijn geen gebruikers.", "message.page-not-found": "Pagina niet gevonden.", "message.reset-website": "Statistieken opnieuw instellen", "message.reset-website-warning": "Alle bijhorende statistieken van deze website worden verwijderd, maar jouw volgcode blijft gelden.", "message.saved": "Opslaan succesvol.", "message.share-url": "Met deze URL kan {target} openbaar gedeeld worden.", - "message.team-already-member": "You are already a member of the team.", - "message.team-not-found": "Team not found.", + "message.team-already-member": "Je bent al lid van het team.", + "message.team-not-found": "Team niet gevonden.", "message.tracking-code": "Volgcode", - "message.user-deleted": "User deleted.", + "message.user-deleted": "Gebruiker verwijderd.", "message.visitor-log": "Bezoeker uit {country} met {browser} op een {os} {device}", - "messages.no-team-websites": "This team does not have any websites.", + "messages.no-team-websites": "Er zijn geen websites gekoppeld aan dit team.", "messages.no-websites-configured": "Je hebt geen websites ingesteld.", - "messages.team-websites-info": "Websites can be viewed by anyone on the team." + "messages.team-websites-info": "Websites kunnen door iedereen in het team worden bekeken." } From 9e2a47800153b301bb685b54d3b5dbdb67b05fae Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 3 May 2023 17:17:57 -0700 Subject: [PATCH 16/88] Add user usage. --- pages/api/users/[id]/usage.ts | 74 +++++++++++++++++++ pages/api/users/[id]/websites.ts | 4 +- queries/analytics/event/getEventUsage.ts | 32 ++++++++ .../analytics/eventData/getEventDataUsage.ts | 32 ++++++++ queries/index.js | 2 + 5 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 pages/api/users/[id]/usage.ts create mode 100644 queries/analytics/event/getEventUsage.ts create mode 100644 queries/analytics/eventData/getEventDataUsage.ts diff --git a/pages/api/users/[id]/usage.ts b/pages/api/users/[id]/usage.ts new file mode 100644 index 00000000..0118df92 --- /dev/null +++ b/pages/api/users/[id]/usage.ts @@ -0,0 +1,74 @@ +import { useAuth, useCors } from 'lib/middleware'; +import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getEventDataUsage, getEventUsage, getUserWebsites } from 'queries'; + +export interface UserUsageRequestQuery { + id: string; + startAt: string; + endAt: string; +} + +export interface UserUsageRequestResponse { + websiteEventUsage: number; + eventDataUsage: number; + websites: { + websiteEventUsage: number; + eventDataUsage: number; + websiteId: string; + websiteName: string; + }[]; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useCors(req, res); + await useAuth(req, res); + + const { user } = req.auth; + + if (req.method === 'GET') { + if (!user.isAdmin) { + return unauthorized(res); + } + + const { id: userId, startAt, endAt } = req.query; + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const websites = await getUserWebsites(userId); + + const websiteIds = websites.map(a => a.id); + + const websiteEventUsage = await getEventUsage(websiteIds, startDate, endDate); + const eventDataUsage = await getEventDataUsage(websiteIds, startDate, endDate); + + const websiteUsage = websites.map(a => ({ + websiteId: a.id, + websiteName: a.name, + websiteEventUsage: websiteEventUsage.find(b => a.id === b.websiteId)?.count || 0, + eventDataUsage: eventDataUsage.find(b => a.id === b.websiteId)?.count || 0, + })); + + const usage = websiteUsage.reduce( + (acc, cv) => { + acc.websiteEventUsage += cv.websiteEventUsage; + acc.eventDataUsage += cv.eventDataUsage; + + return acc; + }, + { websiteEventUsage: 0, eventDataUsage: 0 }, + ); + + return ok(res, { + ...usage, + websites: websiteUsage, + }); + } + + return methodNotAllowed(res); +}; diff --git a/pages/api/users/[id]/websites.ts b/pages/api/users/[id]/websites.ts index c8b874bb..de4a3a3a 100644 --- a/pages/api/users/[id]/websites.ts +++ b/pages/api/users/[id]/websites.ts @@ -4,14 +4,14 @@ import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getUserWebsites } from 'queries'; -export interface WebsitesRequestBody { +export interface UserWebsitesRequestBody { name: string; domain: string; shareId: string; } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); diff --git a/queries/analytics/event/getEventUsage.ts b/queries/analytics/event/getEventUsage.ts new file mode 100644 index 00000000..1465264c --- /dev/null +++ b/queries/analytics/event/getEventUsage.ts @@ -0,0 +1,32 @@ +import clickhouse from 'lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; + +export function getEventUsage(...args: [websiteIds: string[], startDate: Date, endDate: Date]) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +function relationalQuery(websiteIds: string[], startDate: Date, endDate: Date) { + throw new Error('Not Implemented'); +} + +function clickhouseQuery(websiteIds: string[], startDate: Date, endDate: Date) { + const { rawQuery } = clickhouse; + + return rawQuery( + `select + website_id as websiteId, + count(*) as count + from website_event + where created_at between {startDate:DateTime64} and {endDate:DateTime64} + and website_id in {websiteIds:Array(UUID)} + group by website_id`, + { + websiteIds, + startDate, + endDate, + }, + ); +} diff --git a/queries/analytics/eventData/getEventDataUsage.ts b/queries/analytics/eventData/getEventDataUsage.ts new file mode 100644 index 00000000..5d470c3c --- /dev/null +++ b/queries/analytics/eventData/getEventDataUsage.ts @@ -0,0 +1,32 @@ +import clickhouse from 'lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; + +export function getEventDataUsage(...args: [websiteIds: string[], startDate: Date, endDate: Date]) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +function relationalQuery(websiteIds: string[], startDate: Date, endDate: Date) { + throw new Error('Not Implemented'); +} + +function clickhouseQuery(websiteIds: string[], startDate: Date, endDate: Date) { + const { rawQuery } = clickhouse; + + return rawQuery( + `select + website_id as websiteId, + count(*) as count + from event_data + where created_at between {startDate:DateTime64} and {endDate:DateTime64} + and website_id in {websiteIds:Array(UUID)} + group by website_id`, + { + websiteIds, + startDate, + endDate, + }, + ); +} diff --git a/queries/index.js b/queries/index.js index 1275e173..d87d5dd5 100644 --- a/queries/index.js +++ b/queries/index.js @@ -3,8 +3,10 @@ export * from './admin/teamUser'; export * from './admin/user'; export * from './admin/website'; export * from './analytics/event/getEventMetrics'; +export * from './analytics/event/getEventUsage'; export * from './analytics/event/getEvents'; export * from './analytics/eventData/getEventData'; +export * from './analytics/eventData/getEventDataUsage'; export * from './analytics/event/saveEvent'; export * from './analytics/pageview/getPageviewMetrics'; export * from './analytics/pageview/getPageviewStats'; From 37b94e5b96e4864714ef3062e522a7cd070f49b2 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 4 May 2023 14:54:05 -0700 Subject: [PATCH 17/88] Updated message bundles. --- public/intl/messages/mn-MN.json | 150 +++++++++++++++++++------------- public/intl/messages/nl-NL.json | 104 +++++++++++----------- public/intl/messages/pt-BR.json | 98 ++++++++++----------- 3 files changed, 189 insertions(+), 163 deletions(-) diff --git a/public/intl/messages/mn-MN.json b/public/intl/messages/mn-MN.json index 92befd98..beaab2d7 100644 --- a/public/intl/messages/mn-MN.json +++ b/public/intl/messages/mn-MN.json @@ -2,7 +2,7 @@ "label.access-code": [ { "type": 0, - "value": "Access code" + "value": "Хандалтын код" } ], "label.actions": [ @@ -14,7 +14,7 @@ "label.activity-log": [ { "type": 0, - "value": "Activity log" + "value": "Үйл ажиллагааны бүртгэл" } ], "label.add-website": [ @@ -86,19 +86,19 @@ "label.cities": [ { "type": 0, - "value": "Cities" + "value": "Хотууд" } ], "label.clear-all": [ { "type": 0, - "value": "Clear all" + "value": "Бүгдийг арилгах" } ], "label.confirm": [ { "type": 0, - "value": "Confirm" + "value": "Батлах" } ], "label.confirm-password": [ @@ -110,7 +110,7 @@ "label.continue": [ { "type": 0, - "value": "Continue" + "value": "Үргэлжлүүлэх" } ], "label.countries": [ @@ -122,19 +122,19 @@ "label.create-team": [ { "type": 0, - "value": "Create team" + "value": "Баг үүсгэх" } ], "label.create-user": [ { "type": 0, - "value": "Create user" + "value": "Хэрэглэгч үүсгэх" } ], "label.created": [ { "type": 0, - "value": "Created" + "value": "Үүсгэсэн" } ], "label.current-password": [ @@ -158,13 +158,13 @@ "label.data": [ { "type": 0, - "value": "Data" + "value": "Өгөгдөл" } ], "label.date-range": [ { "type": 0, - "value": "Хугацааны мужид" + "value": "Хугацааны муж" } ], "label.default-date-range": [ @@ -182,13 +182,13 @@ "label.delete-team": [ { "type": 0, - "value": "Delete team" + "value": "Баг устгах" } ], "label.delete-user": [ { "type": 0, - "value": "Delete user" + "value": "Хэрэглэгч устгах" } ], "label.delete-website": [ @@ -206,7 +206,7 @@ "label.details": [ { "type": 0, - "value": "Details" + "value": "Мэдээлэл" } ], "label.devices": [ @@ -266,13 +266,13 @@ "label.join": [ { "type": 0, - "value": "Join" + "value": "Нэгдэх" } ], "label.join-team": [ { "type": 0, - "value": "Join team" + "value": "Багт нэгдэх" } ], "label.language": [ @@ -324,13 +324,13 @@ "label.leave": [ { "type": 0, - "value": "Leave" + "value": "Гарах" } ], "label.leave-team": [ { "type": 0, - "value": "Leave team" + "value": "Багаас гарах" } ], "label.login": [ @@ -348,7 +348,7 @@ "label.members": [ { "type": 0, - "value": "Members" + "value": "Гишүүд" } ], "label.mobile": [ @@ -430,7 +430,7 @@ "label.queries": [ { "type": 0, - "value": "Queries" + "value": "Query-нүүд" } ], "label.query-parameters": [ @@ -460,19 +460,19 @@ "label.regenerate": [ { "type": 0, - "value": "Regenerate" + "value": "Дахин үүсгэх" } ], "label.regions": [ { "type": 0, - "value": "Regions" + "value": "Бүсүүд" } ], "label.remove": [ { "type": 0, - "value": "Remove" + "value": "Устгах" } ], "label.required": [ @@ -484,7 +484,7 @@ "label.reset": [ { "type": 0, - "value": "Хуучин хэвд нь оруулах" + "value": "Дахин эхлүүлэх" } ], "label.reset-website": [ @@ -496,7 +496,7 @@ "label.role": [ { "type": 0, - "value": "Role" + "value": "Эрх" } ], "label.save": [ @@ -514,7 +514,7 @@ "label.select-website": [ { "type": 0, - "value": "Select website" + "value": "Веб сонгох" } ], "label.sessions": [ @@ -550,37 +550,37 @@ "label.team": [ { "type": 0, - "value": "Team" + "value": "Баг" } ], "label.team-guest": [ { "type": 0, - "value": "Team guest" + "value": "Багийн зочин" } ], "label.team-id": [ { "type": 0, - "value": "Team ID" + "value": "Багийн ID" } ], "label.team-member": [ { "type": 0, - "value": "Team member" + "value": "Багийн гишүүн" } ], "label.team-owner": [ { "type": 0, - "value": "Team owner" + "value": "Багийн эзэмшигч" } ], "label.teams": [ { "type": 0, - "value": "Teams" + "value": "Багууд" } ], "label.theme": [ @@ -616,7 +616,7 @@ "label.title": [ { "type": 0, - "value": "Title" + "value": "Гарчиг" } ], "label.today": [ @@ -652,7 +652,7 @@ "label.user": [ { "type": 0, - "value": "User" + "value": "Хэрэглэгч" } ], "label.username": [ @@ -664,13 +664,13 @@ "label.users": [ { "type": 0, - "value": "Users" + "value": "Хэрэглэгчид" } ], "label.view": [ { "type": 0, - "value": "View" + "value": "Харах" } ], "label.view-details": [ @@ -694,7 +694,7 @@ "label.website-id": [ { "type": 0, - "value": "Website ID" + "value": "Вебийн ID" } ], "label.websites": [ @@ -768,7 +768,7 @@ "message.confirm-leave": [ { "type": 0, - "value": "Are you sure you want to leave " + "value": "Та " }, { "type": 1, @@ -776,7 +776,7 @@ }, { "type": 0, - "value": "?" + "value": "-с гарахдаа итгэлтэй байна уу?" } ], "message.confirm-reset": [ @@ -796,13 +796,21 @@ "message.delete-website": [ { "type": 0, - "value": "Веб устгах" + "value": "Веб устгахын тулд доорх хэсэгт " + }, + { + "type": 1, + "value": "confirmation" + }, + { + "type": 0, + "value": " гэж бичиж, баталгаажуулна уу." } ], "message.delete-website-warning": [ { "type": 0, - "value": "Үүнтэй холбоотой бүх өгөгдөл устах болно." + "value": "Энэ вебтэй холбоотой бүх өгөгдөл устах болно." } ], "message.error": [ @@ -814,15 +822,15 @@ "message.event-log": [ { "type": 1, - "value": "event" + "value": "url" }, { "type": 0, - "value": " on " + "value": "-д " }, { "type": 1, - "value": "url" + "value": "event" } ], "message.go-to-settings": [ @@ -846,7 +854,7 @@ "message.min-password-length": [ { "type": 0, - "value": "Minimum length of " + "value": "Хамгийн багадаа " }, { "type": 1, @@ -854,7 +862,7 @@ }, { "type": 0, - "value": " characters" + "value": " тэмдэгт" } ], "message.no-data-available": [ @@ -866,19 +874,19 @@ "message.no-match-password": [ { "type": 0, - "value": "Нууц үг тохирохгүй байна" + "value": "Нууц үг тохирохгүй байна." } ], "message.no-teams": [ { "type": 0, - "value": "You have not created any teams." + "value": "Та ямар ч баг үүсгээгүй байна." } ], "message.no-users": [ { "type": 0, - "value": "There are no users." + "value": "Хэрэглэгч байхгүй байна." } ], "message.page-not-found": [ @@ -890,7 +898,15 @@ "message.reset-website": [ { "type": 0, - "value": "Тоон үзүүлэлтийг дахин эхлүүлэх" + "value": "Тоон үзүүлэлийг дахин эхлүүлэхийн тулд доорх хэсэгт " + }, + { + "type": 1, + "value": "confirmation" + }, + { + "type": 0, + "value": " гэж бичиж, баталгаажуулна уу." } ], "message.reset-website-warning": [ @@ -902,41 +918,51 @@ "message.saved": [ { "type": 0, - "value": "Амжилттай хадгаллаа." + "value": "Хадгалсан." } ], "message.share-url": [ - { - "type": 1, - "value": "target" - }, { "type": 0, - "value": "-г нийтэд хуваалцах холбоос." + "value": "Таны вебийн тоон үзүүлэлтүүд доорх URL дээр нийтэд харагдах болно:" } ], "message.team-already-member": [ { "type": 0, - "value": "You are already a member of the team." + "value": "Та аль хэдийн энэ багийн гишүүн болсон байна." } ], "message.team-not-found": [ { "type": 0, - "value": "Team not found." + "value": "Баг олдсонгүй." } ], "message.tracking-code": [ { "type": 0, - "value": "Мөрдөх код" + "value": "Энэ вебийн хандалтуудыг мөрдөхийн тулд доорх кодыг HTML-нхээ " + }, + { + "children": [ + { + "type": 0, + "value": "..." + } + ], + "type": 8, + "value": "head" + }, + { + "type": 0, + "value": " хэсэгт байрлуулна уу." } ], "message.user-deleted": [ { "type": 0, - "value": "User deleted." + "value": "Хэрэглэгч устсан." } ], "message.visitor-log": [ @@ -976,7 +1002,7 @@ "messages.no-team-websites": [ { "type": 0, - "value": "This team does not have any websites." + "value": "Энэ багт ямар ч веб алга." } ], "messages.no-websites-configured": [ @@ -988,7 +1014,7 @@ "messages.team-websites-info": [ { "type": 0, - "value": "Websites can be viewed by anyone on the team." + "value": "Вебийг багийн бүх гишүүд үзэж болно." } ] } diff --git a/public/intl/messages/nl-NL.json b/public/intl/messages/nl-NL.json index bdaa16cb..714c31a0 100644 --- a/public/intl/messages/nl-NL.json +++ b/public/intl/messages/nl-NL.json @@ -2,7 +2,7 @@ "label.access-code": [ { "type": 0, - "value": "Access code" + "value": "Toegangscode" } ], "label.actions": [ @@ -14,19 +14,19 @@ "label.activity-log": [ { "type": 0, - "value": "Activity log" + "value": "Activiteiten logboek" } ], "label.add-website": [ { "type": 0, - "value": "Website toevoegen" + "value": "Website koppelen" } ], "label.admin": [ { "type": 0, - "value": "Administrator" + "value": "Beheerder" } ], "label.all": [ @@ -86,19 +86,19 @@ "label.cities": [ { "type": 0, - "value": "Cities" + "value": "Steden" } ], "label.clear-all": [ { "type": 0, - "value": "Clear all" + "value": "Filters wissen" } ], "label.confirm": [ { "type": 0, - "value": "Confirm" + "value": "Bevestigen" } ], "label.confirm-password": [ @@ -110,7 +110,7 @@ "label.continue": [ { "type": 0, - "value": "Continue" + "value": "Doorgaan" } ], "label.countries": [ @@ -122,19 +122,19 @@ "label.create-team": [ { "type": 0, - "value": "Create team" + "value": "Team aanmaken" } ], "label.create-user": [ { "type": 0, - "value": "Create user" + "value": "Gebruiker maken" } ], "label.created": [ { "type": 0, - "value": "Created" + "value": "Gemaakt" } ], "label.current-password": [ @@ -158,7 +158,7 @@ "label.data": [ { "type": 0, - "value": "Data" + "value": "Gegevens" } ], "label.date-range": [ @@ -182,13 +182,13 @@ "label.delete-team": [ { "type": 0, - "value": "Delete team" + "value": "Team verwijderen" } ], "label.delete-user": [ { "type": 0, - "value": "Delete user" + "value": "Verwijder gebruiker" } ], "label.delete-website": [ @@ -200,13 +200,13 @@ "label.desktop": [ { "type": 0, - "value": "Desktop" + "value": "Computer" } ], "label.details": [ { "type": 0, - "value": "Details" + "value": "Informatie" } ], "label.devices": [ @@ -236,7 +236,7 @@ "label.edit-dashboard": [ { "type": 0, - "value": "Edit dashboard" + "value": "Dashboard aanpassen" } ], "label.enable-share-url": [ @@ -266,13 +266,13 @@ "label.join": [ { "type": 0, - "value": "Join" + "value": "Lid worden" } ], "label.join-team": [ { "type": 0, - "value": "Join team" + "value": "Word lid van een team" } ], "label.language": [ @@ -284,7 +284,7 @@ "label.languages": [ { "type": 0, - "value": "Languages" + "value": "Talen" } ], "label.laptop": [ @@ -324,13 +324,13 @@ "label.leave": [ { "type": 0, - "value": "Leave" + "value": "Verlaten" } ], "label.leave-team": [ { "type": 0, - "value": "Leave team" + "value": "Verlaat team" } ], "label.login": [ @@ -348,7 +348,7 @@ "label.members": [ { "type": 0, - "value": "Members" + "value": "Gebruikers" } ], "label.mobile": [ @@ -430,13 +430,13 @@ "label.queries": [ { "type": 0, - "value": "Queries" + "value": "Parameters" } ], "label.query-parameters": [ { "type": 0, - "value": "Query parameters" + "value": "URL-parameters" } ], "label.realtime": [ @@ -460,19 +460,19 @@ "label.regenerate": [ { "type": 0, - "value": "Regenerate" + "value": "Opnieuw genereren" } ], "label.regions": [ { "type": 0, - "value": "Regions" + "value": "Regio's" } ], "label.remove": [ { "type": 0, - "value": "Remove" + "value": "Verwijderen" } ], "label.required": [ @@ -484,7 +484,7 @@ "label.reset": [ { "type": 0, - "value": "Resetten" + "value": "Opnieuw instellen" } ], "label.reset-website": [ @@ -496,7 +496,7 @@ "label.role": [ { "type": 0, - "value": "Role" + "value": "Gebruikersrol" } ], "label.save": [ @@ -514,13 +514,13 @@ "label.select-website": [ { "type": 0, - "value": "Select website" + "value": "Website selecteren" } ], "label.sessions": [ { "type": 0, - "value": "Sessions" + "value": "Sessies" } ], "label.settings": [ @@ -556,7 +556,7 @@ "label.team-guest": [ { "type": 0, - "value": "Team guest" + "value": "Team gast" } ], "label.team-id": [ @@ -568,13 +568,13 @@ "label.team-member": [ { "type": 0, - "value": "Team member" + "value": "Teamlid" } ], "label.team-owner": [ { "type": 0, - "value": "Team owner" + "value": "Teameigenaar" } ], "label.teams": [ @@ -616,7 +616,7 @@ "label.title": [ { "type": 0, - "value": "Title" + "value": "Titel" } ], "label.today": [ @@ -652,7 +652,7 @@ "label.user": [ { "type": 0, - "value": "User" + "value": "Gebruiker" } ], "label.username": [ @@ -664,13 +664,13 @@ "label.users": [ { "type": 0, - "value": "Users" + "value": "Gebruikers" } ], "label.view": [ { "type": 0, - "value": "View" + "value": "Weergave" } ], "label.view-details": [ @@ -706,7 +706,7 @@ "label.yesterday": [ { "type": 0, - "value": "Yesterday" + "value": "Gisteren" } ], "message.active-users": [ @@ -760,7 +760,7 @@ "message.confirm-leave": [ { "type": 0, - "value": "Are you sure you want to leave " + "value": "Weet je zeker dat je " }, { "type": 1, @@ -768,7 +768,7 @@ }, { "type": 0, - "value": "?" + "value": " wilt verlaten?" } ], "message.confirm-reset": [ @@ -810,7 +810,7 @@ }, { "type": 0, - "value": " on " + "value": " op " }, { "type": 1, @@ -838,7 +838,7 @@ "message.min-password-length": [ { "type": 0, - "value": "Minimum length of " + "value": "Minimale lengte van " }, { "type": 1, @@ -846,7 +846,7 @@ }, { "type": 0, - "value": " characters" + "value": " tekens" } ], "message.no-data-available": [ @@ -864,13 +864,13 @@ "message.no-teams": [ { "type": 0, - "value": "You have not created any teams." + "value": "Er zijn nog geen teams aangemaakt." } ], "message.no-users": [ { "type": 0, - "value": "There are no users." + "value": "Er zijn geen gebruikers." } ], "message.page-not-found": [ @@ -914,13 +914,13 @@ "message.team-already-member": [ { "type": 0, - "value": "You are already a member of the team." + "value": "Je bent al lid van het team." } ], "message.team-not-found": [ { "type": 0, - "value": "Team not found." + "value": "Team niet gevonden." } ], "message.tracking-code": [ @@ -932,7 +932,7 @@ "message.user-deleted": [ { "type": 0, - "value": "User deleted." + "value": "Gebruiker verwijderd." } ], "message.visitor-log": [ @@ -972,7 +972,7 @@ "messages.no-team-websites": [ { "type": 0, - "value": "This team does not have any websites." + "value": "Er zijn geen websites gekoppeld aan dit team." } ], "messages.no-websites-configured": [ @@ -984,7 +984,7 @@ "messages.team-websites-info": [ { "type": 0, - "value": "Websites can be viewed by anyone on the team." + "value": "Websites kunnen door iedereen in het team worden bekeken." } ] } diff --git a/public/intl/messages/pt-BR.json b/public/intl/messages/pt-BR.json index ffd77c16..63e9a49e 100644 --- a/public/intl/messages/pt-BR.json +++ b/public/intl/messages/pt-BR.json @@ -2,7 +2,7 @@ "label.access-code": [ { "type": 0, - "value": "Access code" + "value": "Código de acesso" } ], "label.actions": [ @@ -14,7 +14,7 @@ "label.activity-log": [ { "type": 0, - "value": "Activity log" + "value": "Log de atividade" } ], "label.add-website": [ @@ -44,7 +44,7 @@ "label.analytics": [ { "type": 0, - "value": "Analytics" + "value": "Estatísticas" } ], "label.average-visit-time": [ @@ -86,19 +86,19 @@ "label.cities": [ { "type": 0, - "value": "Cities" + "value": "Cidades" } ], "label.clear-all": [ { "type": 0, - "value": "Clear all" + "value": "Limpar tudo" } ], "label.confirm": [ { "type": 0, - "value": "Confirm" + "value": "Confirmar" } ], "label.confirm-password": [ @@ -110,7 +110,7 @@ "label.continue": [ { "type": 0, - "value": "Continue" + "value": "Continuar" } ], "label.countries": [ @@ -122,19 +122,19 @@ "label.create-team": [ { "type": 0, - "value": "Create team" + "value": "Criar time" } ], "label.create-user": [ { "type": 0, - "value": "Create user" + "value": "Criar usuário" } ], "label.created": [ { "type": 0, - "value": "Created" + "value": "Criado" } ], "label.current-password": [ @@ -182,13 +182,13 @@ "label.delete-team": [ { "type": 0, - "value": "Delete team" + "value": "Remover time" } ], "label.delete-user": [ { "type": 0, - "value": "Delete user" + "value": "Remover usuário" } ], "label.delete-website": [ @@ -206,7 +206,7 @@ "label.details": [ { "type": 0, - "value": "Details" + "value": "Detalhes" } ], "label.devices": [ @@ -236,7 +236,7 @@ "label.edit-dashboard": [ { "type": 0, - "value": "Edit dashboard" + "value": "Editar painel" } ], "label.enable-share-url": [ @@ -266,13 +266,13 @@ "label.join": [ { "type": 0, - "value": "Join" + "value": "Entrar" } ], "label.join-team": [ { "type": 0, - "value": "Join team" + "value": "Entrar no time" } ], "label.language": [ @@ -324,13 +324,13 @@ "label.leave": [ { "type": 0, - "value": "Leave" + "value": "Sair" } ], "label.leave-team": [ { "type": 0, - "value": "Leave team" + "value": "Sair do time" } ], "label.login": [ @@ -348,7 +348,7 @@ "label.members": [ { "type": 0, - "value": "Members" + "value": "Membros" } ], "label.mobile": [ @@ -378,7 +378,7 @@ "label.none": [ { "type": 0, - "value": "None" + "value": "Nenhum" } ], "label.operating-systems": [ @@ -430,7 +430,7 @@ "label.queries": [ { "type": 0, - "value": "Queries" + "value": "Parâmetros" } ], "label.query-parameters": [ @@ -460,19 +460,19 @@ "label.regenerate": [ { "type": 0, - "value": "Regenerate" + "value": "Regerar" } ], "label.regions": [ { "type": 0, - "value": "Regions" + "value": "Regiões" } ], "label.remove": [ { "type": 0, - "value": "Remove" + "value": "Remover" } ], "label.required": [ @@ -496,7 +496,7 @@ "label.role": [ { "type": 0, - "value": "Role" + "value": "Papel" } ], "label.save": [ @@ -514,13 +514,13 @@ "label.select-website": [ { "type": 0, - "value": "Select website" + "value": "Selecionar site" } ], "label.sessions": [ { "type": 0, - "value": "Sessions" + "value": "Sessões" } ], "label.settings": [ @@ -550,37 +550,37 @@ "label.team": [ { "type": 0, - "value": "Team" + "value": "Time" } ], "label.team-guest": [ { "type": 0, - "value": "Team guest" + "value": "Convidado" } ], "label.team-id": [ { "type": 0, - "value": "Team ID" + "value": "ID do Time" } ], "label.team-member": [ { "type": 0, - "value": "Team member" + "value": "Membro" } ], "label.team-owner": [ { "type": 0, - "value": "Team owner" + "value": "Proprietário" } ], "label.teams": [ { "type": 0, - "value": "Teams" + "value": "Times" } ], "label.theme": [ @@ -616,7 +616,7 @@ "label.title": [ { "type": 0, - "value": "Title" + "value": "Título" } ], "label.today": [ @@ -652,7 +652,7 @@ "label.user": [ { "type": 0, - "value": "User" + "value": "Usuário" } ], "label.username": [ @@ -664,13 +664,13 @@ "label.users": [ { "type": 0, - "value": "Users" + "value": "Usuários" } ], "label.view": [ { "type": 0, - "value": "View" + "value": "Ver" } ], "label.view-details": [ @@ -694,7 +694,7 @@ "label.website-id": [ { "type": 0, - "value": "Website ID" + "value": "ID do Site" } ], "label.websites": [ @@ -764,7 +764,7 @@ "message.confirm-leave": [ { "type": 0, - "value": "Are you sure you want to leave " + "value": "Você tem certeza que deseja sair de " }, { "type": 1, @@ -814,7 +814,7 @@ }, { "type": 0, - "value": " on " + "value": " em " }, { "type": 1, @@ -842,7 +842,7 @@ "message.min-password-length": [ { "type": 0, - "value": "Minimum length of " + "value": "Quantidade mínima de " }, { "type": 1, @@ -850,7 +850,7 @@ }, { "type": 0, - "value": " characters" + "value": " caracteres" } ], "message.no-data-available": [ @@ -868,13 +868,13 @@ "message.no-teams": [ { "type": 0, - "value": "You have not created any teams." + "value": "Você não criou nenhum time." } ], "message.no-users": [ { "type": 0, - "value": "There are no users." + "value": "Não há nenhum usuário." } ], "message.page-not-found": [ @@ -918,13 +918,13 @@ "message.team-already-member": [ { "type": 0, - "value": "You are already a member of the team." + "value": "Você já um membro do time." } ], "message.team-not-found": [ { "type": 0, - "value": "Team not found." + "value": "Time não encontrado." } ], "message.tracking-code": [ @@ -936,7 +936,7 @@ "message.user-deleted": [ { "type": 0, - "value": "User deleted." + "value": "Usuário removido." } ], "message.visitor-log": [ @@ -976,7 +976,7 @@ "messages.no-team-websites": [ { "type": 0, - "value": "This team does not have any websites." + "value": "Este time não possui nenhum site." } ], "messages.no-websites-configured": [ @@ -988,7 +988,7 @@ "messages.team-websites-info": [ { "type": 0, - "value": "Websites can be viewed by anyone on the team." + "value": "Os sites podem ser visualizados por qualquer membro da equipe." } ] } From 98e58581e28ade0f305332494bc1ece0c91dd2f6 Mon Sep 17 00:00:00 2001 From: theshamuel Date: Fri, 5 May 2023 23:53:16 +0100 Subject: [PATCH 18/88] add health-check for db container I'd like to propose a minor changes that can bring more stability in launching the umami server with docker-compose by adding a certain healthcheck for db container and a certain condition for depends_on directive for umami-app container --- docker-compose.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index bd63c68b..9b63c580 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,8 @@ services: DATABASE_TYPE: postgresql APP_SECRET: replace-me-with-a-random-string depends_on: - - db + db: + condition: service_healthy restart: always db: image: postgres:15-alpine @@ -22,5 +23,10 @@ services: - ./sql/schema.postgresql.sql:/docker-entrypoint-initdb.d/schema.postgresql.sql:ro - umami-db-data:/var/lib/postgresql/data restart: always + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 5 volumes: umami-db-data: From e773451679dc63f4361324d220c8aa10a3721f43 Mon Sep 17 00:00:00 2001 From: theshamuel Date: Fri, 5 May 2023 23:57:58 +0100 Subject: [PATCH 19/88] delete a redundant volume mapping I'd like to propose to delete from docker-compose a redundant volume mapping. As I see that `sql` directory had been deleted and now umami-app is responsible for migration. --- docker-compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index bd63c68b..33ae3708 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,6 @@ services: POSTGRES_USER: umami POSTGRES_PASSWORD: umami volumes: - - ./sql/schema.postgresql.sql:/docker-entrypoint-initdb.d/schema.postgresql.sql:ro - umami-db-data:/var/lib/postgresql/data restart: always volumes: From e92d958b24112ff746b6eababc37a4b90ae9b09b Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 5 May 2023 21:16:13 -0700 Subject: [PATCH 20/88] Updated redis client. Soft deletes reset username. --- lib/cache.ts | 30 +----------------------------- package.json | 2 +- pages/api/users/[id]/index.ts | 2 +- pages/api/websites/[id]/index.ts | 4 ++-- queries/admin/user.ts | 2 ++ yarn.lock | 8 ++++---- 6 files changed, 11 insertions(+), 37 deletions(-) diff --git a/lib/cache.ts b/lib/cache.ts index 2aad7ed8..528dd012 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -2,35 +2,7 @@ import { User, Website } from '@prisma/client'; import redis from '@umami/redis-client'; import { getSession, getUser, getWebsite } from '../queries'; -const DELETED = 'DELETED'; - -async function fetchObject(key, query) { - const obj = await redis.get(key); - - if (obj === DELETED) { - return null; - } - - if (!obj) { - return query().then(async data => { - if (data) { - await redis.set(key, data); - } - - return data; - }); - } - - return obj; -} - -async function storeObject(key, data) { - return redis.set(key, data); -} - -async function deleteObject(key, soft = false) { - return soft ? redis.set(key, DELETED) : redis.del(key); -} +const { fetchObject, storeObject, deleteObject } = redis; async function fetchWebsite(id): Promise { return fetchObject(`website:${id}`, () => getWebsite({ id })); diff --git a/package.json b/package.json index 736ec659..0db07f49 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@prisma/client": "4.13.0", "@tanstack/react-query": "^4.16.1", "@umami/prisma-client": "^0.2.0", - "@umami/redis-client": "^0.2.0", + "@umami/redis-client": "^0.5.0", "chalk": "^4.1.1", "chart.js": "^4.2.1", "chartjs-adapter-date-fns": "^3.0.0", diff --git a/pages/api/users/[id]/index.ts b/pages/api/users/[id]/index.ts index 8219c4a7..de4642cb 100644 --- a/pages/api/users/[id]/index.ts +++ b/pages/api/users/[id]/index.ts @@ -51,11 +51,11 @@ export default async ( data.password = hashPassword(password); } + // Only admin can change these fields if (role && isAdmin) { data.role = role; } - // Only admin can change these fields if (username && isAdmin) { data.username = username; } diff --git a/pages/api/websites/[id]/index.ts b/pages/api/websites/[id]/index.ts index c1907fce..3f660a91 100644 --- a/pages/api/websites/[id]/index.ts +++ b/pages/api/websites/[id]/index.ts @@ -1,8 +1,8 @@ +import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok, serverError, unauthorized } from 'next-basics'; import { Website, NextApiRequestQueryBody } from 'lib/types'; import { canViewWebsite, canUpdateWebsite, canDeleteWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, serverError, unauthorized } from 'next-basics'; import { deleteWebsite, getWebsite, updateWebsite } from 'queries'; export interface WebsiteRequestQuery { diff --git a/queries/admin/user.ts b/queries/admin/user.ts index 412c7785..a81a76ef 100644 --- a/queries/admin/user.ts +++ b/queries/admin/user.ts @@ -1,4 +1,5 @@ import { Prisma, Team, TeamUser } from '@prisma/client'; +import { getRandomChars } from 'next-basics'; import cache from 'lib/cache'; import { ROLES } from 'lib/constants'; import prisma from 'lib/prisma'; @@ -222,6 +223,7 @@ export async function deleteUser( cloudMode ? client.user.update({ data: { + username: getRandomChars(32), deletedAt: new Date(), }, where: { diff --git a/yarn.lock b/yarn.lock index d98ada11..41cca434 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3272,10 +3272,10 @@ dependencies: debug "^4.3.4" -"@umami/redis-client@^0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@umami/redis-client/-/redis-client-0.2.0.tgz#bdb1cd8b5c99afc5230621f19296c6d3559d68af" - integrity sha512-TONWhkuC//K2hRo3Psk7FHsuvu3XkQIYMY62/CERPtlIJz4Ac7DqsmYw4jO9/RkljA9XLl/5u+OggD4ARhMV8A== +"@umami/redis-client@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@umami/redis-client/-/redis-client-0.5.0.tgz#09b15458001bc172fc856d65316efbe5ff749461" + integrity sha512-x7wx/pMjyg3AAYzgjGOw031bNhyZ81h6tRMAl60RQQI9xlJaJEA1r0TEUrWfFi21gHAvdBLJGYCsvHzpix4LKQ== dependencies: debug "^4.3.4" redis "^4.5.1" From d827bf1417dd989c314f0253b1bafe55c0ccf6d6 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Sun, 7 May 2023 23:13:40 -0700 Subject: [PATCH 21/88] update CH kafka engine tables for error handling --- db/clickhouse/schema.sql | 42 +++++++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/db/clickhouse/schema.sql b/db/clickhouse/schema.sql index 77176413..8f48b434 100644 --- a/db/clickhouse/schema.sql +++ b/db/clickhouse/schema.sql @@ -58,7 +58,10 @@ CREATE TABLE umami.website_event_queue ( --event event_type UInt32, event_name String, - created_at DateTime('UTC') + created_at DateTime('UTC'), + --virtual columns + _error String, + _raw_message String ) ENGINE = Kafka SETTINGS kafka_broker_list = 'domain:9092,domain:9093,domain:9094', -- input broker list @@ -66,7 +69,7 @@ SETTINGS kafka_broker_list = 'domain:9092,domain:9093,domain:9094', -- input bro kafka_group_name = 'event_consumer_group', kafka_format = 'JSONEachRow', kafka_max_block_size = 1048576, - kafka_skip_broken_messages = 100; + kafka_handle_error_mode = 'stream' CREATE MATERIALIZED VIEW umami.website_event_queue_mv TO umami.website_event AS SELECT website_id, @@ -93,6 +96,19 @@ SELECT website_id, created_at FROM umami.website_event_queue; +CREATE MATERIALIZED VIEW umami.website_event_errors_mv +( + error String, + raw String +) +ENGINE = MergeTree +ORDER BY (error, raw) +SETTINGS index_granularity = 8192 AS +SELECT _error AS error, + _raw_message AS raw +FROM umami.website_event_queue +WHERE length(_error) > 0 + CREATE TABLE umami.event_data ( website_id UUID, @@ -122,7 +138,10 @@ CREATE TABLE umami.event_data_queue ( event_numeric_value Nullable(Decimal64(4)), --922337203685477.5625 event_date_value Nullable(DateTime('UTC')), event_data_type UInt32, - created_at DateTime('UTC') + created_at DateTime('UTC'), + --virtual columns + _error String, + _raw_message String ) ENGINE = Kafka SETTINGS kafka_broker_list = 'domain:9092,domain:9093,domain:9094', -- input broker list @@ -130,7 +149,7 @@ SETTINGS kafka_broker_list = 'domain:9092,domain:9093,domain:9094', -- input bro kafka_group_name = 'event_data_consumer_group', kafka_format = 'JSONEachRow', kafka_max_block_size = 1048576, - kafka_skip_broken_messages = 100; + kafka_handle_error_mode = 'stream' CREATE MATERIALIZED VIEW umami.event_data_queue_mv TO umami.event_data AS SELECT website_id, @@ -144,4 +163,17 @@ SELECT website_id, event_date_value, event_data_type, created_at -FROM umami.event_data_queue; \ No newline at end of file +FROM umami.event_data_queue; + +CREATE MATERIALIZED VIEW umami.event_data_errors_mv +( + error String, + raw String +) +ENGINE = MergeTree +ORDER BY (error, raw) +SETTINGS index_granularity = 8192 AS +SELECT _error AS error, + _raw_message AS raw +FROM umami.event_data_queue +WHERE length(_error) > 0 \ No newline at end of file From d01aa5cd52dfa00e89a2b49b9477cac456da0159 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Mon, 8 May 2023 23:46:58 -0700 Subject: [PATCH 22/88] Add funnel queries --- .../migrations/02_report_schema/migration.sql | 18 ++++ db/postgresql/schema.prisma | 28 ++++-- lib/clickhouse.ts | 24 +++++ lib/prisma.ts | 56 +++++++++++ pages/api/reports/funnel.ts | 50 ++++++++++ .../analytics/pageview/getPageviewFunnel.ts | 97 +++++++++++++++++++ queries/index.js | 1 + 7 files changed, 268 insertions(+), 6 deletions(-) create mode 100644 db/postgresql/migrations/02_report_schema/migration.sql create mode 100644 pages/api/reports/funnel.ts create mode 100644 queries/analytics/pageview/getPageviewFunnel.ts diff --git a/db/postgresql/migrations/02_report_schema/migration.sql b/db/postgresql/migrations/02_report_schema/migration.sql new file mode 100644 index 00000000..61f1164d --- /dev/null +++ b/db/postgresql/migrations/02_report_schema/migration.sql @@ -0,0 +1,18 @@ +-- CreateTable +CREATE TABLE "report" ( + "report_id" UUID NOT NULL, + "user_id" UUID NOT NULL, + "report_name" VARCHAR(200) NOT NULL, + "template_name" VARCHAR(200) NOT NULL, + "parameters" VARCHAR(6000) NOT NULL, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "report_pkey" PRIMARY KEY ("report_id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "report_report_id_key" ON "report"("report_id"); + +-- CreateIndex +CREATE INDEX "report_user_id_idx" ON "report"("user_id"); diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index b336bce4..7523ce21 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -14,11 +14,12 @@ model User { password String @db.VarChar(60) role String @map("role") @db.VarChar(50) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamptz(6) + updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) - website Website[] - teamUser TeamUser[] + website Website[] + teamUser TeamUser[] + ReportTemplate ReportTemplate[] @@map("user") } @@ -53,7 +54,7 @@ model Website { resetAt DateTime? @map("reset_at") @db.Timestamptz(6) userId String? @map("user_id") @db.Uuid createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamptz(6) + updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) user User? @relation(fields: [userId], references: [id]) @@ -116,7 +117,7 @@ model Team { name String @db.VarChar(50) accessCode String? @unique @map("access_code") @db.VarChar(50) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamptz(6) + updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) teamUser TeamUser[] teamWebsite TeamWebsite[] @@ -131,7 +132,7 @@ model TeamUser { userId String @map("user_id") @db.Uuid role String @map("role") @db.VarChar(50) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamptz(6) + updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) team Team @relation(fields: [teamId], references: [id]) user User @relation(fields: [userId], references: [id]) @@ -154,3 +155,18 @@ model TeamWebsite { @@index([websiteId]) @@map("team_website") } + +model ReportTemplate { + id String @id() @unique() @map("report_id") @db.Uuid + userId String @map("user_id") @db.Uuid + reportName String @map("report_name") @db.VarChar(200) + templateName String @map("template_name") @db.VarChar(200) + parameters String @map("parameters") @db.VarChar(6000) + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) + + user User @relation(fields: [userId], references: [id]) + + @@index([userId]) + @@map("report") +} diff --git a/lib/clickhouse.ts b/lib/clickhouse.ts index 90cf6088..88922c0f 100644 --- a/lib/clickhouse.ts +++ b/lib/clickhouse.ts @@ -121,6 +121,29 @@ function getFilterQuery(filters = {}, params = {}) { return query.join('\n'); } +function getFunnelQuery(urls: string[]): { + columnsQuery: string; + conditionQuery: string; + urlParams: { [key: string]: string }; +} { + return urls.reduce( + (pv, cv, i) => { + pv.columnsQuery += `\n,url_path = {url${i}:String}${ + i > 0 && urls[i - 1] ? ` AND request_url = {url${i - 1}:String}` : '' + },'`; + pv.conditionQuery += `${i > 0 ? ',' : ''} {url${i}:String}`; + pv.urlParams[`url${i}`] = cv; + + return pv; + }, + { + columnsQuery: '', + conditionQuery: '', + urlParams: {}, + }, + ); +} + function parseFilters(filters: WebsiteMetricFilter = {}, params: any = {}) { return { filterQuery: getFilterQuery(filters, params), @@ -168,6 +191,7 @@ export default { getDateFormat, getBetweenDates, getFilterQuery, + getFunnelQuery, getEventDataFilterQuery, parseFilters, findUnique, diff --git a/lib/prisma.ts b/lib/prisma.ts index 0a10d981..ce2238b6 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -32,6 +32,18 @@ function toUuid(): string { } } +function getAddMinutesQuery(field: string, minutes: number) { + const db = getDatabaseType(process.env.DATABASE_URL); + + if (db === POSTGRESQL) { + return `${field} + interval '${minutes} minute'`; + } + + if (db === MYSQL) { + return `DATE_ADD(${field}, interval ${minutes} minute)`; + } +} + function getDateQuery(field: string, unit: string, timezone?: string): string { const db = getDatabaseType(process.env.DATABASE_URL); @@ -122,6 +134,48 @@ function getFilterQuery(filters = {}, params = []): string { return query.join('\n'); } +function getFunnelQuery( + urls: string[], + windowMinutes: number, + initParamLength = 3, +): { + levelQuery: string; + sumQuery: string; + urlFilterQuery: string; +} { + return urls.reduce( + (pv, cv, i) => { + const levelNumber = i + 1; + const start = i > 0 ? ',' : ''; + + pv.levelQuery += `\n + , level${levelNumber} AS ( + select cl.*, + l0.created_at level_${levelNumber}_created_at, + l0.url_path as level_${levelNumber}_url + from level${i} cl + left join level0 l0 + on cl.session_id = l0.session_id + and l0.created_at between cl.level_${levelNumber}_created_at + and ${getAddMinutesQuery(`cl.level_${levelNumber}_created_at`, windowMinutes)} + and l0.referrer_path = $${i + initParamLength} + and l0.url_path = $${i + initParamLength} + )`; + + pv.sumQuery += `\n${start}SUM(CASE WHEN l1_url is not null THEN 1 ELSE 0 END) AS level1`; + + pv.urlFilterQuery += `\n${start}$${levelNumber + initParamLength} `; + + return pv; + }, + { + levelQuery: '', + sumQuery: '', + urlFilterQuery: '', + }, + ); +} + function parseFilters( filters: { [key: string]: any } = {}, params = [], @@ -152,9 +206,11 @@ async function rawQuery(query: string, params: never[] = []): Promise { export default { ...prisma, + getAddMinutesQuery, getDateQuery, getTimestampInterval, getFilterQuery, + getFunnelQuery, getEventDataFilterQuery, toUuid, parseFilters, diff --git a/pages/api/reports/funnel.ts b/pages/api/reports/funnel.ts new file mode 100644 index 00000000..080130de --- /dev/null +++ b/pages/api/reports/funnel.ts @@ -0,0 +1,50 @@ +import { canViewWebsite } from 'lib/auth'; +import { useCors, useAuth } from 'lib/middleware'; +import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiResponse } from 'next'; +import { ok, methodNotAllowed, unauthorized } from 'next-basics'; +import { getPageviewFunnel } from 'queries'; + +export interface FunnelRequestBody { + websiteId: string; + urls: string[]; + window: number; + startAt: number; + endAt: number; +} + +export interface FunnelResponse { + urls: string[]; + window: number; + startAt: number; + endAt: number; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useCors(req, res); + await useAuth(req, res); + + if (req.method === 'POST') { + const { websiteId, urls, window, startAt, endAt } = req.body; + if (!(await canViewWebsite(req.auth, websiteId))) { + return unauthorized(res); + } + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + + const data = getPageviewFunnel(websiteId, { + startDate, + endDate, + urls, + windowMinutes: window, + }); + + return ok(res); + } + + return methodNotAllowed(res); +}; diff --git a/queries/analytics/pageview/getPageviewFunnel.ts b/queries/analytics/pageview/getPageviewFunnel.ts new file mode 100644 index 00000000..f7751630 --- /dev/null +++ b/queries/analytics/pageview/getPageviewFunnel.ts @@ -0,0 +1,97 @@ +import clickhouse from 'lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; +import prisma from 'lib/prisma'; + +export async function getPageviewFunnel( + ...args: [ + websiteId: string, + criteria: { + windowMinutes: number; + startDate: Date; + endDate: Date; + urls: string[]; + }, + ] +) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery( + websiteId: string, + criteria: { + windowMinutes: number; + startDate: Date; + endDate: Date; + urls: string[]; + }, +) { + const { windowMinutes, startDate, endDate, urls } = criteria; + const { rawQuery, getFunnelQuery, toUuid } = prisma; + const { levelQuery, sumQuery, urlFilterQuery } = getFunnelQuery(urls, windowMinutes); + + const params: any = [websiteId, startDate, endDate, ...urls]; + + return rawQuery( + `WITH level0 AS ( + select session_id, url_path, created_at + from website_event + where url_path in (${urlFilterQuery}) + website_event.website_id = $1${toUuid()} + and created_at between $2 and $3 + ),level1 AS ( + select session_id, url_path as level1_url, created_at as level1_created_at + from level0 + )${levelQuery} + + SELECT ${sumQuery} + from level3; + `, + params, + ); +} + +async function clickhouseQuery( + websiteId: string, + criteria: { + windowMinutes: number; + startDate: Date; + endDate: Date; + urls: string[]; + }, +) { + const { windowMinutes, startDate, endDate, urls } = criteria; + const { rawQuery, getBetweenDates, getFunnelQuery } = clickhouse; + const { columnsQuery, conditionQuery, urlParams } = getFunnelQuery(urls); + + const params = { + websiteId, + window: windowMinutes * 60, + ...urlParams, + }; + + return rawQuery( + ` + SELECT level, + count(*) AS count + FROM ( + SELECT session_id, + windowFunnel({window:UInt32}, 'strict_order') + ( + created_at, + ${columnsQuery} + ) AS level + FROM website_event + WHERE website_id = {websiteId:UUID} + and ${getBetweenDates('created_at', startDate, endDate)} + AND (url_path in [${conditionQuery}]) + GROUP BY 1 + ) + GROUP BY level + ORDER BY level ASC; + `, + params, + ); +} diff --git a/queries/index.js b/queries/index.js index d87d5dd5..5c295fff 100644 --- a/queries/index.js +++ b/queries/index.js @@ -8,6 +8,7 @@ export * from './analytics/event/getEvents'; export * from './analytics/eventData/getEventData'; export * from './analytics/eventData/getEventDataUsage'; export * from './analytics/event/saveEvent'; +export * from './analytics/pageview/getPageviewFunnel'; export * from './analytics/pageview/getPageviewMetrics'; export * from './analytics/pageview/getPageviewStats'; export * from './analytics/session/createSession'; From 97f4a3bccc490d4856cb1dd782355fe71a879d2e Mon Sep 17 00:00:00 2001 From: Ammar <61558359+ammar-madni@users.noreply.github.com> Date: Thu, 11 May 2023 17:27:37 +0100 Subject: [PATCH 23/88] Update index.js --- pages/realtime/[id]/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/realtime/[id]/index.js b/pages/realtime/[id]/index.js index 43475fa5..ba13ded3 100644 --- a/pages/realtime/[id]/index.js +++ b/pages/realtime/[id]/index.js @@ -10,7 +10,7 @@ export default function RealtimeDetailsPage() { const { formatMessage, labels } = useMessages(); const { get, useQuery } = useApi(); const { data: website } = useQuery(['websites', websiteId], () => - get(`/websites/${websiteId}`, { enabled: !!websiteId }), + websiteId ? get(`/websites/${websiteId}`, { enabled: !!websiteId }) : null, ); const title = `${formatMessage(labels.realtime)}${website?.name ? ` - ${website.name}` : ''}`; From 1130bca1951c136bc9d7926deeb7163619bb8109 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 11 May 2023 16:42:58 -0700 Subject: [PATCH 24/88] Funnel form. --- components/input/DateFilter.js | 24 ++- components/metrics/WebsiteChart.js | 7 +- components/pages/reports/FunnelChart.js | 43 +++++ components/pages/reports/FunnelDetails.js | 0 components/pages/reports/FunnelForm.js | 104 ++++++++++++ .../pages/reports/FunnelForm.module.css | 19 +++ components/pages/reports/FunnelPage.js | 26 +++ .../settings/profile/DateRangeSetting.js | 8 +- .../migrations/02_report_schema/migration.sql | 9 +- db/postgresql/schema.prisma | 7 +- lib/clickhouse.ts | 8 +- lib/prisma.ts | 12 +- package.json | 1 + pages/_app.js | 1 + pages/api/reports/funnel.ts | 4 +- pages/reports/funnel.js | 22 +++ .../analytics/pageview/getPageviewFunnel.ts | 57 ++++--- styles/funnelChart.css | 148 ++++++++++++++++++ yarn.lock | 5 + 19 files changed, 460 insertions(+), 45 deletions(-) create mode 100644 components/pages/reports/FunnelChart.js create mode 100644 components/pages/reports/FunnelDetails.js create mode 100644 components/pages/reports/FunnelForm.js create mode 100644 components/pages/reports/FunnelForm.module.css create mode 100644 components/pages/reports/FunnelPage.js create mode 100644 pages/reports/funnel.js create mode 100644 styles/funnelChart.css diff --git a/components/input/DateFilter.js b/components/input/DateFilter.js index b6c1ee72..ecdf9039 100644 --- a/components/input/DateFilter.js +++ b/components/input/DateFilter.js @@ -9,7 +9,7 @@ import useApi from 'hooks/useApi'; import useDateRange from 'hooks/useDateRange'; import useMessages from 'hooks/useMessages'; -export function DateFilter({ websiteId, value, className }) { +export function DateFilter({ websiteId, value, className, onChange, isForm, alignment }) { const { formatMessage, labels } = useMessages(); const { get } = useApi(); const [dateRange, setDateRange] = useDateRange(websiteId); @@ -21,10 +21,26 @@ export function DateFilter({ websiteId, value, className }) { const data = await get(`/websites/${websiteId}`); if (data) { - setDateRange({ value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) }); + const websiteRange = { value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) }; + + if (!isForm) { + setDateRange(websiteRange); + } + + if (onChange) { + onChange(websiteRange); + } } } else if (value !== 'all') { - setDateRange(value); + if (!isForm) { + setDateRange(value); + } + + if (onChange) { + onChange(value); + } + + console.log(value); } } @@ -103,7 +119,7 @@ export function DateFilter({ websiteId, value, className }) { items={options} renderValue={renderValue} value={value} - alignment="end" + alignment={alignment || 'end'} onChange={handleChange} > {({ label, value, divider }) => ( diff --git a/components/metrics/WebsiteChart.js b/components/metrics/WebsiteChart.js index 6614d40f..cab0821e 100644 --- a/components/metrics/WebsiteChart.js +++ b/components/metrics/WebsiteChart.js @@ -107,7 +107,12 @@ export function WebsiteChart({
- +
diff --git a/components/pages/reports/FunnelChart.js b/components/pages/reports/FunnelChart.js new file mode 100644 index 00000000..44c99092 --- /dev/null +++ b/components/pages/reports/FunnelChart.js @@ -0,0 +1,43 @@ +import FunnelGraph from 'funnel-graph-js/dist/js/funnel-graph'; +import { useEffect, useRef } from 'react'; + +export default function FunnelChart() { + const funnel = useRef(null); + + useEffect(() => { + funnel.current.innerHTML = ''; + + const data = { + labels: ['Cv Sent', '1st Interview', '2nd Interview', '3rd Interview', 'Offer'], + subLabels: ['Cv Sent', '1st Interview', '2nd Interview', '3rd Interview', 'Offer'], + colors: [ + ['#FFB178', '#FF78B1', '#FF3C8E'], + ['#FFB178', '#FF78B1', '#FF3C8E'], + ['#A0BBFF', '#EC77FF'], + ['#A0F9FF', '#7795FF'], + ['#FFB178', '#FF78B1', '#FF3C8E'], + ], + values: [[3500], [3300], [2000], [600], [330]], + }; + + const graph = new FunnelGraph({ + container: '.funnel', + gradientDirection: 'horizontal', + data: data, + displayPercent: true, + direction: 'Vertical', + width: 1000, + height: 350, + subLabelValue: 'values', + }); + + graph.draw(); + }, []); + + return ( +
+ FunnelChart +
+
+ ); +} diff --git a/components/pages/reports/FunnelDetails.js b/components/pages/reports/FunnelDetails.js new file mode 100644 index 00000000..e69de29b diff --git a/components/pages/reports/FunnelForm.js b/components/pages/reports/FunnelForm.js new file mode 100644 index 00000000..081105f1 --- /dev/null +++ b/components/pages/reports/FunnelForm.js @@ -0,0 +1,104 @@ +import { useMutation } from '@tanstack/react-query'; +import DateFilter from 'components/input/DateFilter'; +import WebsiteSelect from 'components/input/WebsiteSelect'; +import useApi from 'hooks/useApi'; +import useMessages from 'hooks/useMessages'; +import useUser from 'hooks/useUser'; +import { parseDateRange } from 'lib/date'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import { + Button, + Form, + FormButtons, + FormInput, + FormRow, + SubmitButton, + TextField, +} from 'react-basics'; +import styles from './FunnelForm.module.css'; +import { getNextInternalQuery } from 'next/dist/server/request-meta'; + +export function FunnelForm({ onSearch }) { + const { formatMessage, labels, getMessage } = useMessages(); + const [dateRange, setDateRange] = useState(null); + const [startDate, setStartDate] = useState(null); + const [endDate, setEndDate] = useState(null); + const [urls, setUrls] = useState(['']); + const [websiteId, setWebsiteId] = useState(''); + + const handleSubmit = async data => { + onSearch(data); + }; + + const handleDateChange = value => { + const { startDate, endDate } = parseDateRange(value); + + setDateRange(value); + setStartDate(startDate); + setEndDate(endDate); + }; + + const handleAddUrl = () => setUrls([...urls, 'meow']); + + const handleRemoveUrl = i => setUrls(urls.splice(i, 1)); + + const handleUrlChange = (value, i) => { + const nextUrls = [...urls]; + + nextUrls[i] = value.target.value; + setUrls(nextUrls); + }; + + return ( + <> + + + setWebsiteId(value)} /> + + + + + + + + + + + + + + + {urls.map((a, i) => ( + + + handleUrlChange(value, i)} /> + + ))} + + + Search + + + + ); +} + +export default FunnelForm; diff --git a/components/pages/reports/FunnelForm.module.css b/components/pages/reports/FunnelForm.module.css new file mode 100644 index 00000000..9a8d924b --- /dev/null +++ b/components/pages/reports/FunnelForm.module.css @@ -0,0 +1,19 @@ +.filter { + min-width: 200px; +} + +.hiddenInput { + max-height: 100px; +} + +.hiddenInput { + visibility: hidden; + min-height: 0px; + max-height: 0px; +} + +.hidden { + visibility: hidden; + min-height: 0px; + max-height: 0px; +} diff --git a/components/pages/reports/FunnelPage.js b/components/pages/reports/FunnelPage.js new file mode 100644 index 00000000..3cfa63a8 --- /dev/null +++ b/components/pages/reports/FunnelPage.js @@ -0,0 +1,26 @@ +import Page from 'components/layout/Page'; +import FunnelChart from './FunnelChart'; +import FunnelForm from './FunnelForm'; + +export default function FunnelPage() { + function handleOnSearch() { + // do API CALL to api/reports/funnel to get funnelData + // Get DATA + } + + return ( + + funnelPage + {/* */} + website / start/endDate urls: [] + + {/* {!chartLoaded && } + {chartLoaded && ( + <> + {!view && } + {view && } + + )} */} + + ); +} diff --git a/components/pages/settings/profile/DateRangeSetting.js b/components/pages/settings/profile/DateRangeSetting.js index 152aba1d..202bbbe1 100644 --- a/components/pages/settings/profile/DateRangeSetting.js +++ b/components/pages/settings/profile/DateRangeSetting.js @@ -7,13 +7,15 @@ import useMessages from 'hooks/useMessages'; export function DateRangeSetting() { const { formatMessage, labels } = useMessages(); const [dateRange, setDateRange] = useDateRange(); - const { startDate, endDate, value } = dateRange; + const { value } = dateRange; - const handleReset = () => setDateRange(DEFAULT_DATE_RANGE); + const handleReset = () => { + setDateRange(DEFAULT_DATE_RANGE); + }; return ( - + ); diff --git a/db/postgresql/migrations/02_report_schema/migration.sql b/db/postgresql/migrations/02_report_schema/migration.sql index 61f1164d..8b2bf0f5 100644 --- a/db/postgresql/migrations/02_report_schema/migration.sql +++ b/db/postgresql/migrations/02_report_schema/migration.sql @@ -1,18 +1,19 @@ -- CreateTable -CREATE TABLE "report" ( +CREATE TABLE "user_report" ( "report_id" UUID NOT NULL, "user_id" UUID NOT NULL, + "website_id" UUID NOT NULL, "report_name" VARCHAR(200) NOT NULL, "template_name" VARCHAR(200) NOT NULL, "parameters" VARCHAR(6000) NOT NULL, "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMPTZ(6), - CONSTRAINT "report_pkey" PRIMARY KEY ("report_id") + CONSTRAINT "user_report_pkey" PRIMARY KEY ("report_id") ); -- CreateIndex -CREATE UNIQUE INDEX "report_report_id_key" ON "report"("report_id"); +CREATE UNIQUE INDEX "user_report_report_id_key" ON "user_report"("report_id"); -- CreateIndex -CREATE INDEX "report_user_id_idx" ON "report"("user_id"); +CREATE INDEX "user_report_user_id_idx" ON "user_report"("user_id"); diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index 7523ce21..ee5ff4b4 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -19,7 +19,7 @@ model User { website Website[] teamUser TeamUser[] - ReportTemplate ReportTemplate[] + ReportTemplate UserReport[] @@map("user") } @@ -156,9 +156,10 @@ model TeamWebsite { @@map("team_website") } -model ReportTemplate { +model UserReport { id String @id() @unique() @map("report_id") @db.Uuid userId String @map("user_id") @db.Uuid + websiteId String @map("website_id") @db.Uuid reportName String @map("report_name") @db.VarChar(200) templateName String @map("template_name") @db.VarChar(200) parameters String @map("parameters") @db.VarChar(6000) @@ -168,5 +169,5 @@ model ReportTemplate { user User @relation(fields: [userId], references: [id]) @@index([userId]) - @@map("report") + @@map("user_report") } diff --git a/lib/clickhouse.ts b/lib/clickhouse.ts index 88922c0f..e97be806 100644 --- a/lib/clickhouse.ts +++ b/lib/clickhouse.ts @@ -129,8 +129,8 @@ function getFunnelQuery(urls: string[]): { return urls.reduce( (pv, cv, i) => { pv.columnsQuery += `\n,url_path = {url${i}:String}${ - i > 0 && urls[i - 1] ? ` AND request_url = {url${i - 1}:String}` : '' - },'`; + i > 0 && urls[i - 1] ? ` AND referrer_path = {url${i - 1}:String}` : '' + }`; pv.conditionQuery += `${i > 0 ? ',' : ''} {url${i}:String}`; pv.urlParams[`url${i}`] = cv; @@ -150,7 +150,7 @@ function parseFilters(filters: WebsiteMetricFilter = {}, params: any = {}) { }; } -async function rawQuery(query, params = {}) { +async function rawQuery(query, params = {}): Promise { if (process.env.LOG_QUERY) { log('QUERY:\n', query); log('PARAMETERS:\n', params); @@ -158,7 +158,7 @@ async function rawQuery(query, params = {}) { await connect(); - return clickhouse.query(query, { params }).toPromise(); + return clickhouse.query(query, { params }).toPromise() as Promise; } async function findUnique(data) { diff --git a/lib/prisma.ts b/lib/prisma.ts index ce2238b6..fdd8a58d 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -148,7 +148,8 @@ function getFunnelQuery( const levelNumber = i + 1; const start = i > 0 ? ',' : ''; - pv.levelQuery += `\n + if (levelNumber >= 2) { + pv.levelQuery += `\n , level${levelNumber} AS ( select cl.*, l0.created_at level_${levelNumber}_created_at, @@ -156,13 +157,14 @@ function getFunnelQuery( from level${i} cl left join level0 l0 on cl.session_id = l0.session_id - and l0.created_at between cl.level_${levelNumber}_created_at - and ${getAddMinutesQuery(`cl.level_${levelNumber}_created_at`, windowMinutes)} + and l0.created_at between cl.level_${i}_created_at + and ${getAddMinutesQuery(`cl.level_${i}_created_at`, windowMinutes)} and l0.referrer_path = $${i + initParamLength} - and l0.url_path = $${i + initParamLength} + and l0.url_path = $${levelNumber + initParamLength} )`; + } - pv.sumQuery += `\n${start}SUM(CASE WHEN l1_url is not null THEN 1 ELSE 0 END) AS level1`; + pv.sumQuery += `\n${start}SUM(CASE WHEN level_${levelNumber}_url is not null THEN 1 ELSE 0 END) AS level${levelNumber}`; pv.urlFilterQuery += `\n${start}$${levelNumber + initParamLength} `; diff --git a/package.json b/package.json index 0db07f49..7f365db1 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "dotenv": "^10.0.0", "formik": "^2.2.9", "fs-extra": "^10.0.1", + "funnel-graph-js": "^1.3.7", "immer": "^9.0.12", "ipaddr.js": "^2.0.1", "is-ci": "^3.0.1", diff --git a/pages/_app.js b/pages/_app.js index 22458215..bc55355b 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -9,6 +9,7 @@ import useConfig from 'hooks/useConfig'; import '@fontsource/inter/400.css'; import '@fontsource/inter/700.css'; import 'react-basics/dist/styles.css'; +import 'styles/funnelChart.css'; import 'styles/variables.css'; import 'styles/locale.css'; import 'styles/index.css'; diff --git a/pages/api/reports/funnel.ts b/pages/api/reports/funnel.ts index 080130de..ee450eb6 100644 --- a/pages/api/reports/funnel.ts +++ b/pages/api/reports/funnel.ts @@ -36,14 +36,14 @@ export default async ( const startDate = new Date(+startAt); const endDate = new Date(+endAt); - const data = getPageviewFunnel(websiteId, { + const data = await getPageviewFunnel(websiteId, { startDate, endDate, urls, windowMinutes: window, }); - return ok(res); + return ok(res, data); } return methodNotAllowed(res); diff --git a/pages/reports/funnel.js b/pages/reports/funnel.js new file mode 100644 index 00000000..d4bf7dd2 --- /dev/null +++ b/pages/reports/funnel.js @@ -0,0 +1,22 @@ +import { useRouter } from 'next/router'; +import AppLayout from 'components/layout/AppLayout'; +import FunnelPage from 'components/pages/reports/FunnelPage'; +import useMessages from 'hooks/useMessages'; + +export default function DetailsPage() { + // const { formatMessage, labels } = useMessages(); + // const router = useRouter(); + // const { id } = router.query; + + // if (!id) { + // return null; + // } + + // return {/* */}; + + return ( +
+ +
+ ); +} diff --git a/queries/analytics/pageview/getPageviewFunnel.ts b/queries/analytics/pageview/getPageviewFunnel.ts index f7751630..d80a681c 100644 --- a/queries/analytics/pageview/getPageviewFunnel.ts +++ b/queries/analytics/pageview/getPageviewFunnel.ts @@ -27,7 +27,13 @@ async function relationalQuery( endDate: Date; urls: string[]; }, -) { +): Promise< + { + level: number; + url: string; + count: any; + }[] +> { const { windowMinutes, startDate, endDate, urls } = criteria; const { rawQuery, getFunnelQuery, toUuid } = prisma; const { levelQuery, sumQuery, urlFilterQuery } = getFunnelQuery(urls, windowMinutes); @@ -36,21 +42,24 @@ async function relationalQuery( return rawQuery( `WITH level0 AS ( - select session_id, url_path, created_at - from website_event - where url_path in (${urlFilterQuery}) - website_event.website_id = $1${toUuid()} - and created_at between $2 and $3 - ),level1 AS ( - select session_id, url_path as level1_url, created_at as level1_created_at - from level0 - )${levelQuery} - - SELECT ${sumQuery} - from level3; - `, + select session_id, url_path, referrer_path, created_at + from website_event + where url_path in (${urlFilterQuery}) + and website_id = $1${toUuid()} + and created_at between $2 and $3 + ),level1 AS ( + select session_id, url_path as level_1_url, created_at as level_1_created_at + from level0 + where url_path = $4 + )${levelQuery} + + SELECT ${sumQuery} + from level3; + `, params, - ); + ).then((a: { [key: string]: number }) => { + return urls.map((b, i) => ({ level: i + 1, url: b, count: a[`level${i + 1}`] || 0 })); + }); } async function clickhouseQuery( @@ -61,7 +70,13 @@ async function clickhouseQuery( endDate: Date; urls: string[]; }, -) { +): Promise< + { + level: number; + url: string; + count: any; + }[] +> { const { windowMinutes, startDate, endDate, urls } = criteria; const { rawQuery, getBetweenDates, getFunnelQuery } = clickhouse; const { columnsQuery, conditionQuery, urlParams } = getFunnelQuery(urls); @@ -72,7 +87,7 @@ async function clickhouseQuery( ...urlParams, }; - return rawQuery( + return rawQuery<{ level: number; count: number }[]>( ` SELECT level, count(*) AS count @@ -80,7 +95,7 @@ async function clickhouseQuery( SELECT session_id, windowFunnel({window:UInt32}, 'strict_order') ( - created_at, + created_at ${columnsQuery} ) AS level FROM website_event @@ -93,5 +108,9 @@ async function clickhouseQuery( ORDER BY level ASC; `, params, - ); + ).then(a => { + return a + .filter(b => b.level !== 0) + .map((c, i) => ({ level: c.level, url: urls[i], count: c.count })); + }); } diff --git a/styles/funnelChart.css b/styles/funnelChart.css new file mode 100644 index 00000000..c72d42e7 --- /dev/null +++ b/styles/funnelChart.css @@ -0,0 +1,148 @@ +.svg-funnel-js { + display: inline-block; + position: relative; +} +.svg-funnel-js svg { + display: block; +} +.svg-funnel-js .svg-funnel-js__labels { + position: absolute; + display: flex; + width: 100%; + height: 100%; + top: 0; + left: 0; +} +.svg-funnel-js.svg-funnel-js--vertical .svg-funnel-js__labels { + flex-direction: column; +} + +.svg-funnel-js body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.svg-funnel-js { + font-family: 'Open Sans', sans-serif; +} +.svg-funnel-js .svg-funnel-js__container { + width: 100%; + height: 100%; +} +.svg-funnel-js .svg-funnel-js__labels { + width: 100%; + box-sizing: border-box; +} +.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label { + flex: 1 1 0; + position: relative; +} +.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label .label__value { + font-size: 24px; + color: #fff; + line-height: 18px; + margin-bottom: 6px; +} +.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label .label__title { + font-size: 12px; + font-weight: bold; + color: #21ffa2; +} +.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label .label__percentage { + font-size: 16px; + font-weight: bold; + color: #9896dc; +} +.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label .label__segment-percentages { + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 100%; + left: 0; + padding: 8px 24px; + box-sizing: border-box; + background-color: rgba(8, 7, 48, 0.8); + margin-top: 24px; + opacity: 0; + transition: opacity 0.1s ease; + cursor: default; +} +.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label .label__segment-percentages ul { + margin: 0; + padding: 0; + list-style-type: none; +} +.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label .label__segment-percentages ul li { + font-size: 13px; + line-height: 16px; + color: #fff; + margin: 18px 0; +} +.svg-funnel-js + .svg-funnel-js__labels + .svg-funnel-js__label + .label__segment-percentages + ul + li + .percentage__list-label { + font-weight: bold; + color: #05df9d; +} +.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label:hover .label__segment-percentages { + opacity: 1; +} +.svg-funnel-js:not(.svg-funnel-js--vertical) { + padding-top: 64px; + padding-bottom: 16px; +} +.svg-funnel-js:not(.svg-funnel-js--vertical) .svg-funnel-js__label { + padding-left: 24px; +} +.svg-funnel-js:not(.svg-funnel-js--vertical) .svg-funnel-js__label:not(:first-child) { + border-left: 1px solid #9896dc; +} +.svg-funnel-js.svg-funnel-js--vertical { + padding-left: 120px; + padding-right: 16px; +} +.svg-funnel-js.svg-funnel-js--vertical .svg-funnel-js__label { + padding-top: 24px; +} +.svg-funnel-js.svg-funnel-js--vertical .svg-funnel-js__label:not(:first-child) { + border-top: 1px solid #9896dc; +} +.svg-funnel-js.svg-funnel-js--vertical .svg-funnel-js__label .label__segment-percentages { + margin-top: 0; + margin-left: 106px; + width: calc(100% - 106px); +} +.svg-funnel-js.svg-funnel-js--vertical + .svg-funnel-js__label + .label__segment-percentages + .segment-percentage__list { + display: flex; + justify-content: space-around; +} +.svg-funnel-js .svg-funnel-js__subLabels { + display: flex; + justify-content: center; + margin-top: 24px; + position: absolute; + width: 100%; + left: 0; +} +.svg-funnel-js .svg-funnel-js__subLabels .svg-funnel-js__subLabel { + display: flex; + font-size: 12px; + color: #fff; + line-height: 16px; +} +.svg-funnel-js .svg-funnel-js__subLabels .svg-funnel-js__subLabel:not(:first-child) { + margin-left: 16px; +} +.svg-funnel-js .svg-funnel-js__subLabels .svg-funnel-js__subLabel .svg-funnel-js__subLabel--color { + width: 12px; + height: 12px; + border-radius: 50%; + margin: 2px 8px 2px 0; +} diff --git a/yarn.lock b/yarn.lock index 41cca434..1f9ea77a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5500,6 +5500,11 @@ functions-have-names@^1.2.2: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +funnel-graph-js@^1.3.7: + version "1.4.2" + resolved "https://registry.yarnpkg.com/funnel-graph-js/-/funnel-graph-js-1.4.2.tgz#b82150189e8afa59104d881d5dcf55a28d715342" + integrity sha512-9bnmcBve7RDH9dTF9BLuUpuisKkDka3yrfhs+Z/106ZgJvqIse1RfKQWjW+QdAlTrZqC9oafen7t/KuJKv9ohA== + generic-names@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-4.0.0.tgz#0bd8a2fd23fe8ea16cbd0a279acd69c06933d9a3" From c8ebb7695cb69da04380681216cf67757f364d0d Mon Sep 17 00:00:00 2001 From: AkashRajpurohit Date: Fri, 12 May 2023 22:38:12 +0530 Subject: [PATCH 25/88] fix: :adhesive_bandage: null value check for metric data --- components/metrics/MetricsTable.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js index 97deb39d..df2b8a69 100644 --- a/components/metrics/MetricsTable.js +++ b/components/metrics/MetricsTable.js @@ -59,7 +59,10 @@ export function MetricsTable({ const filteredData = useMemo(() => { if (data) { - let items = percentFilter(dataFilter ? dataFilter(data, filterOptions) : data); + const dataWithoutNullValues = data.filter(val => val.x !== null); + let items = percentFilter( + dataFilter ? dataFilter(dataWithoutNullValues, filterOptions) : dataWithoutNullValues, + ); if (limit) { items = items.filter((e, i) => i < limit); } From 07cb9f621d930caa6f262e9d1462a72cc49afae5 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Sun, 14 May 2023 21:38:03 -0700 Subject: [PATCH 26/88] Funnel Table/Chart hooked up. --- components/input/DateFilter.js | 8 +-- components/layout/SettingsLayout.js | 1 + components/messages.js | 6 ++ components/pages/reports/FunnelChart.js | 57 +++++++++---------- components/pages/reports/FunnelForm.js | 56 ++++++++++-------- .../pages/reports/FunnelForm.module.css | 9 +++ components/pages/reports/FunnelPage.js | 34 +++++++---- .../pages/reports/FunnelPage.module.css | 10 ++++ components/pages/reports/FunnelTable.js | 17 ++++++ db/postgresql/schema.prisma | 32 ++++++----- pages/api/reports/funnel.ts | 2 +- pages/settings/reports/funnel.js | 16 ++++++ .../funnel.js => settings/reports/index.js} | 9 ++- 13 files changed, 170 insertions(+), 87 deletions(-) create mode 100644 components/pages/reports/FunnelPage.module.css create mode 100644 components/pages/reports/FunnelTable.js create mode 100644 pages/settings/reports/funnel.js rename pages/{reports/funnel.js => settings/reports/index.js} (76%) diff --git a/components/input/DateFilter.js b/components/input/DateFilter.js index ecdf9039..4d60627d 100644 --- a/components/input/DateFilter.js +++ b/components/input/DateFilter.js @@ -9,7 +9,7 @@ import useApi from 'hooks/useApi'; import useDateRange from 'hooks/useDateRange'; import useMessages from 'hooks/useMessages'; -export function DateFilter({ websiteId, value, className, onChange, isForm, alignment }) { +export function DateFilter({ websiteId, value, className, onChange, alignment }) { const { formatMessage, labels } = useMessages(); const { get } = useApi(); const [dateRange, setDateRange] = useDateRange(websiteId); @@ -23,7 +23,7 @@ export function DateFilter({ websiteId, value, className, onChange, isForm, alig if (data) { const websiteRange = { value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) }; - if (!isForm) { + if (!onChange) { setDateRange(websiteRange); } @@ -32,15 +32,13 @@ export function DateFilter({ websiteId, value, className, onChange, isForm, alig } } } else if (value !== 'all') { - if (!isForm) { + if (!onChange) { setDateRange(value); } if (onChange) { onChange(value); } - - console.log(value); } } diff --git a/components/layout/SettingsLayout.js b/components/layout/SettingsLayout.js index c79f0909..d58154ca 100644 --- a/components/layout/SettingsLayout.js +++ b/components/layout/SettingsLayout.js @@ -15,6 +15,7 @@ export function SettingsLayout({ children }) { const items = [ { key: 'websites', label: formatMessage(labels.websites), url: '/settings/websites' }, { key: 'teams', label: formatMessage(labels.teams), url: '/settings/teams' }, + { key: 'reports', label: 'Reports', url: '/settings/reports/funnel' }, user.isAdmin && { key: 'users', label: formatMessage(labels.users), url: '/settings/users' }, { key: 'profile', label: formatMessage(labels.profile), url: '/settings/profile' }, ].filter(n => n); diff --git a/components/messages.js b/components/messages.js index 245e8591..fe4b833a 100644 --- a/components/messages.js +++ b/components/messages.js @@ -18,7 +18,9 @@ export const labels = defineMessages({ admin: { id: 'label.admin', defaultMessage: 'Administrator' }, confirm: { id: 'label.confirm', defaultMessage: 'Confirm' }, details: { id: 'label.details', defaultMessage: 'Details' }, + website: { id: 'label.website', defaultMessage: 'Website' }, websites: { id: 'label.websites', defaultMessage: 'Websites' }, + reports: { id: 'label.reports', defaultMessage: 'Reports' }, created: { id: 'label.created', defaultMessage: 'Created' }, edit: { id: 'label.edit', defaultMessage: 'Edit' }, name: { id: 'label.name', defaultMessage: 'Name' }, @@ -183,6 +185,10 @@ export const messages = defineMessages({ id: 'message.delete-website-warning', defaultMessage: 'All website data will be deleted.', }, + noResultsFound: { + id: 'messages.no-results-found', + defaultMessage: 'No results were found.', + }, noWebsitesConfigured: { id: 'messages.no-websites-configured', defaultMessage: 'You do not have any websites configured.', diff --git a/components/pages/reports/FunnelChart.js b/components/pages/reports/FunnelChart.js index 44c99092..8739e1da 100644 --- a/components/pages/reports/FunnelChart.js +++ b/components/pages/reports/FunnelChart.js @@ -1,43 +1,40 @@ import FunnelGraph from 'funnel-graph-js/dist/js/funnel-graph'; import { useEffect, useRef } from 'react'; +import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; +import useMessages from 'hooks/useMessages'; -export default function FunnelChart() { +export default function FunnelChart({ data }) { + const { formatMessage, labels, messages } = useMessages(); const funnel = useRef(null); useEffect(() => { - funnel.current.innerHTML = ''; + if (data && data.length > 0) { + funnel.current.innerHTML = ''; - const data = { - labels: ['Cv Sent', '1st Interview', '2nd Interview', '3rd Interview', 'Offer'], - subLabels: ['Cv Sent', '1st Interview', '2nd Interview', '3rd Interview', 'Offer'], - colors: [ - ['#FFB178', '#FF78B1', '#FF3C8E'], - ['#FFB178', '#FF78B1', '#FF3C8E'], - ['#A0BBFF', '#EC77FF'], - ['#A0F9FF', '#7795FF'], - ['#FFB178', '#FF78B1', '#FF3C8E'], - ], - values: [[3500], [3300], [2000], [600], [330]], - }; + const chartData = { + labels: data.map(a => a.url), + colors: ['#147af3', '#e0f2ff'], + values: data.map(a => a.count), + }; - const graph = new FunnelGraph({ - container: '.funnel', - gradientDirection: 'horizontal', - data: data, - displayPercent: true, - direction: 'Vertical', - width: 1000, - height: 350, - subLabelValue: 'values', - }); + const graph = new FunnelGraph({ + container: '.funnel', + gradientDirection: 'horizontal', + data: chartData, + displayPercent: true, + direction: 'Vertical', + width: 1000, + height: 350, + }); - graph.draw(); - }, []); + graph.draw(); + } + }, [data]); return ( -
- FunnelChart -
-
+ <> + {data?.length > 0 &&
} + {data?.length === 0 && } + ); } diff --git a/components/pages/reports/FunnelForm.js b/components/pages/reports/FunnelForm.js index 081105f1..56be6732 100644 --- a/components/pages/reports/FunnelForm.js +++ b/components/pages/reports/FunnelForm.js @@ -1,12 +1,8 @@ -import { useMutation } from '@tanstack/react-query'; import DateFilter from 'components/input/DateFilter'; import WebsiteSelect from 'components/input/WebsiteSelect'; -import useApi from 'hooks/useApi'; import useMessages from 'hooks/useMessages'; -import useUser from 'hooks/useUser'; import { parseDateRange } from 'lib/date'; -import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { Button, Form, @@ -17,15 +13,15 @@ import { TextField, } from 'react-basics'; import styles from './FunnelForm.module.css'; -import { getNextInternalQuery } from 'next/dist/server/request-meta'; export function FunnelForm({ onSearch }) { - const { formatMessage, labels, getMessage } = useMessages(); - const [dateRange, setDateRange] = useState(null); - const [startDate, setStartDate] = useState(null); - const [endDate, setEndDate] = useState(null); + const { formatMessage, labels } = useMessages(); + const [dateRange, setDateRange] = useState(''); + const [startAt, setStartAt] = useState(); + const [endAt, setEndAt] = useState(); const [urls, setUrls] = useState(['']); const [websiteId, setWebsiteId] = useState(''); + const [window, setWindow] = useState(60); const handleSubmit = async data => { onSearch(data); @@ -35,14 +31,16 @@ export function FunnelForm({ onSearch }) { const { startDate, endDate } = parseDateRange(value); setDateRange(value); - setStartDate(startDate); - setEndDate(endDate); + setStartAt(startDate.getTime()); + setEndAt(endDate.getTime()); }; - const handleAddUrl = () => setUrls([...urls, 'meow']); + const handleAddUrl = () => setUrls([...urls, '']); const handleRemoveUrl = i => setUrls(urls.splice(i, 1)); + const handleWindowChange = value => setWindow(value.target.value); + const handleUrlChange = (value, i) => { const nextUrls = [...urls]; @@ -55,13 +53,14 @@ export function FunnelForm({ onSearch }) {
- + setWebsiteId(value)} /> @@ -73,28 +72,39 @@ export function FunnelForm({ onSearch }) { value={dateRange} alignment="start" onChange={handleDateChange} + isF /> - + - - + + + + + + + {urls.map((a, i) => ( - - + handleUrlChange(value, i)} /> + ))} - Search + + Search + diff --git a/components/pages/reports/FunnelForm.module.css b/components/pages/reports/FunnelForm.module.css index 9a8d924b..2706a99a 100644 --- a/components/pages/reports/FunnelForm.module.css +++ b/components/pages/reports/FunnelForm.module.css @@ -17,3 +17,12 @@ min-height: 0px; max-height: 0px; } + +.urlFormRow { + flex-direction: row; + gap: 0em; +} + +.urlFormRow label { + min-width: 80px; +} diff --git a/components/pages/reports/FunnelPage.js b/components/pages/reports/FunnelPage.js index 3cfa63a8..c715d857 100644 --- a/components/pages/reports/FunnelPage.js +++ b/components/pages/reports/FunnelPage.js @@ -1,26 +1,38 @@ +import { useMutation } from '@tanstack/react-query'; import Page from 'components/layout/Page'; +import PageHeader from 'components/layout/PageHeader'; +import useApi from 'hooks/useApi'; +import { useState } from 'react'; import FunnelChart from './FunnelChart'; +import FunnelTable from './FunnelTable'; import FunnelForm from './FunnelForm'; +import styles from './FunnelPage.module.css'; export default function FunnelPage() { - function handleOnSearch() { + const { post } = useApi(); + const { mutate, error, isLoading } = useMutation(data => post('/reports/funnel', data)); + const [data, setData] = useState(); + + function handleOnSearch(data) { // do API CALL to api/reports/funnel to get funnelData // Get DATA + mutate(data, { + onSuccess: async data => { + setData(data); + }, + }); } return ( - funnelPage + + + {/* */} - website / start/endDate urls: [] - - {/* {!chartLoaded && } - {chartLoaded && ( - <> - {!view && } - {view && } - - )} */} +
+

Filters

+ +
); } diff --git a/components/pages/reports/FunnelPage.module.css b/components/pages/reports/FunnelPage.module.css new file mode 100644 index 00000000..aed66b74 --- /dev/null +++ b/components/pages/reports/FunnelPage.module.css @@ -0,0 +1,10 @@ +.filters { + display: flex; + flex-direction: column; + justify-content: space-between; + border: 1px solid var(--base400); + border-radius: var(--border-radius); + line-height: 32px; + padding: 10px; + overflow: hidden; +} diff --git a/components/pages/reports/FunnelTable.js b/components/pages/reports/FunnelTable.js new file mode 100644 index 00000000..fa40fd13 --- /dev/null +++ b/components/pages/reports/FunnelTable.js @@ -0,0 +1,17 @@ +import DataTable from 'components/metrics/DataTable'; +import useMessages from 'hooks/useMessages'; +import { useState } from 'react'; + +export function DevicesTable({ ...props }) { + const { formatMessage, labels } = useMessages(); + const { data } = props; + + const tableData = + data?.map(a => ({ x: a.url, y: a.count, z: (a.count / data[0].count) * 100 })) || []; + + console.log(tableData); + + return ; +} + +export default DevicesTable; diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index ee5ff4b4..318d455d 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -17,9 +17,9 @@ model User { updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) - website Website[] - teamUser TeamUser[] - ReportTemplate UserReport[] + website Website[] + teamUser TeamUser[] + Report Report[] @@map("user") } @@ -60,6 +60,7 @@ model Website { user User? @relation(fields: [userId], references: [id]) teamWebsite TeamWebsite[] eventData EventData[] + Report Report[] @@index([userId]) @@index([createdAt]) @@ -156,18 +157,21 @@ model TeamWebsite { @@map("team_website") } -model UserReport { - id String @id() @unique() @map("report_id") @db.Uuid - userId String @map("user_id") @db.Uuid - websiteId String @map("website_id") @db.Uuid - reportName String @map("report_name") @db.VarChar(200) - templateName String @map("template_name") @db.VarChar(200) - parameters String @map("parameters") @db.VarChar(6000) - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) +model Report { + id String @id() @unique() @map("report_id") @db.Uuid + userId String @map("user_id") @db.Uuid + websiteId String @map("website_id") @db.Uuid + type String @map("type") @db.VarChar(200) + name String @map("name") @db.VarChar(200) + description String @map("description") @db.VarChar(500) + parameters String @map("parameters") @db.VarChar(6000) + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id]) + website Website @relation(fields: [websiteId], references: [id]) @@index([userId]) - @@map("user_report") + @@index([websiteId]) + @@map("report") } diff --git a/pages/api/reports/funnel.ts b/pages/api/reports/funnel.ts index ee450eb6..6e3eb602 100644 --- a/pages/api/reports/funnel.ts +++ b/pages/api/reports/funnel.ts @@ -40,7 +40,7 @@ export default async ( startDate, endDate, urls, - windowMinutes: window, + windowMinutes: +window, }); return ok(res, data); diff --git a/pages/settings/reports/funnel.js b/pages/settings/reports/funnel.js new file mode 100644 index 00000000..d8d7a5b8 --- /dev/null +++ b/pages/settings/reports/funnel.js @@ -0,0 +1,16 @@ +import AppLayout from 'components/layout/AppLayout'; +import SettingsLayout from 'components/layout/SettingsLayout'; +import FunnelPage from 'components/pages/reports/FunnelPage'; +import useMessages from 'hooks/useMessages'; + +export default function DetailsPage() { + const { formatMessage, labels } = useMessages(); + + return ( + + + + + + ); +} diff --git a/pages/reports/funnel.js b/pages/settings/reports/index.js similarity index 76% rename from pages/reports/funnel.js rename to pages/settings/reports/index.js index d4bf7dd2..ce0a3726 100644 --- a/pages/reports/funnel.js +++ b/pages/settings/reports/index.js @@ -2,6 +2,7 @@ import { useRouter } from 'next/router'; import AppLayout from 'components/layout/AppLayout'; import FunnelPage from 'components/pages/reports/FunnelPage'; import useMessages from 'hooks/useMessages'; +import SettingsLayout from 'components/layout/SettingsLayout'; export default function DetailsPage() { // const { formatMessage, labels } = useMessages(); @@ -15,8 +16,10 @@ export default function DetailsPage() { // return {/* */}; return ( -
- -
+ + + + + ); } From b5f84159d2925bb05585f62200c7593aa91f8194 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Mon, 15 May 2023 14:03:42 -0700 Subject: [PATCH 27/88] funnel checkpoint --- components/pages/reports/FunnelChart.js | 4 +++- components/pages/reports/FunnelChart.module.css | 7 +++++++ components/pages/reports/FunnelForm.js | 2 +- components/pages/reports/FunnelPage.js | 6 +++--- components/pages/reports/FunnelTable.js | 2 -- components/pages/reports/ReportDropdown.js | 0 components/pages/reports/ReportForm.js | 0 queries/analytics/pageview/getPageviewFunnel.ts | 12 +++++++----- 8 files changed, 21 insertions(+), 12 deletions(-) create mode 100644 components/pages/reports/FunnelChart.module.css create mode 100644 components/pages/reports/ReportDropdown.js create mode 100644 components/pages/reports/ReportForm.js diff --git a/components/pages/reports/FunnelChart.js b/components/pages/reports/FunnelChart.js index 8739e1da..ec03acab 100644 --- a/components/pages/reports/FunnelChart.js +++ b/components/pages/reports/FunnelChart.js @@ -2,6 +2,8 @@ import FunnelGraph from 'funnel-graph-js/dist/js/funnel-graph'; import { useEffect, useRef } from 'react'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import useMessages from 'hooks/useMessages'; +import styles from './FunnelChart.module.css'; +import classNames from 'classnames'; export default function FunnelChart({ data }) { const { formatMessage, labels, messages } = useMessages(); @@ -33,7 +35,7 @@ export default function FunnelChart({ data }) { return ( <> - {data?.length > 0 &&
} + {data?.length > 0 &&
} {data?.length === 0 && } ); diff --git a/components/pages/reports/FunnelChart.module.css b/components/pages/reports/FunnelChart.module.css new file mode 100644 index 00000000..1d7eb37e --- /dev/null +++ b/components/pages/reports/FunnelChart.module.css @@ -0,0 +1,7 @@ +.funnel div { + color: var(--font-color100) !important; +} + +.funnel svg { + max-width: 100%; +} diff --git a/components/pages/reports/FunnelForm.js b/components/pages/reports/FunnelForm.js index 56be6732..5f9758f0 100644 --- a/components/pages/reports/FunnelForm.js +++ b/components/pages/reports/FunnelForm.js @@ -19,7 +19,7 @@ export function FunnelForm({ onSearch }) { const [dateRange, setDateRange] = useState(''); const [startAt, setStartAt] = useState(); const [endAt, setEndAt] = useState(); - const [urls, setUrls] = useState(['']); + const [urls, setUrls] = useState(['/', '/docs/getting-started', '/docs/intall']); const [websiteId, setWebsiteId] = useState(''); const [window, setWindow] = useState(60); diff --git a/components/pages/reports/FunnelPage.js b/components/pages/reports/FunnelPage.js index c715d857..b69319b1 100644 --- a/components/pages/reports/FunnelPage.js +++ b/components/pages/reports/FunnelPage.js @@ -12,10 +12,11 @@ export default function FunnelPage() { const { post } = useApi(); const { mutate, error, isLoading } = useMutation(data => post('/reports/funnel', data)); const [data, setData] = useState(); + const [formData, setFormData] = useState(); function handleOnSearch(data) { - // do API CALL to api/reports/funnel to get funnelData - // Get DATA + setFormData(data); + mutate(data, { onSuccess: async data => { setData(data); @@ -28,7 +29,6 @@ export default function FunnelPage() { - {/* */}

Filters

diff --git a/components/pages/reports/FunnelTable.js b/components/pages/reports/FunnelTable.js index fa40fd13..036e20c3 100644 --- a/components/pages/reports/FunnelTable.js +++ b/components/pages/reports/FunnelTable.js @@ -9,8 +9,6 @@ export function DevicesTable({ ...props }) { const tableData = data?.map(a => ({ x: a.url, y: a.count, z: (a.count / data[0].count) * 100 })) || []; - console.log(tableData); - return ; } diff --git a/components/pages/reports/ReportDropdown.js b/components/pages/reports/ReportDropdown.js new file mode 100644 index 00000000..e69de29b diff --git a/components/pages/reports/ReportForm.js b/components/pages/reports/ReportForm.js new file mode 100644 index 00000000..e69de29b diff --git a/queries/analytics/pageview/getPageviewFunnel.ts b/queries/analytics/pageview/getPageviewFunnel.ts index d80a681c..591310cf 100644 --- a/queries/analytics/pageview/getPageviewFunnel.ts +++ b/queries/analytics/pageview/getPageviewFunnel.ts @@ -74,7 +74,7 @@ async function clickhouseQuery( { level: number; url: string; - count: any; + count: number; }[] > { const { windowMinutes, startDate, endDate, urls } = criteria; @@ -108,9 +108,11 @@ async function clickhouseQuery( ORDER BY level ASC; `, params, - ).then(a => { - return a - .filter(b => b.level !== 0) - .map((c, i) => ({ level: c.level, url: urls[i], count: c.count })); + ).then(results => { + return urls.map((a, i) => ({ + level: i + 1, + url: a, + count: results[i + 1]?.count || 0, + })); }); } From 2ce62c10232d3b6e3fb454fd7ef83a630025c528 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Mon, 15 May 2023 20:41:12 -0700 Subject: [PATCH 28/88] Add usage. --- lib/cache.ts | 6 ++++++ lib/middleware.ts | 11 ++++++++++- lib/session.ts | 13 ++++++++++++- pages/api/send.ts | 1 + 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/cache.ts b/lib/cache.ts index 528dd012..e63a53bb 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -49,6 +49,11 @@ async function deleteSession(id) { return deleteObject(`session:${id}`); } +async function fetchUserBlock(userId: string) { + const key = `user:block:${userId}`; + return redis.get(key); +} + export default { fetchWebsite, storeWebsite, @@ -59,5 +64,6 @@ export default { fetchSession, storeSession, deleteSession, + fetchUserBlock, enabled: redis.enabled, }; diff --git a/lib/middleware.ts b/lib/middleware.ts index 79c48404..1fd13b09 100644 --- a/lib/middleware.ts +++ b/lib/middleware.ts @@ -1,4 +1,10 @@ -import { createMiddleware, unauthorized, badRequest, parseSecureToken } from 'next-basics'; +import { + createMiddleware, + unauthorized, + badRequest, + parseSecureToken, + tooManyRequest, +} from 'next-basics'; import debug from 'debug'; import cors from 'cors'; import { validate } from 'uuid'; @@ -30,6 +36,9 @@ export const useSession = createMiddleware(async (req, res, next) => { (req as any).session = session; } catch (e: any) { + if (e.message === 'Usage Limit.') { + return tooManyRequest(res, e.message); + } return badRequest(res, e.message); } diff --git a/lib/session.ts b/lib/session.ts index 937bfef2..32f3bdc8 100644 --- a/lib/session.ts +++ b/lib/session.ts @@ -6,6 +6,7 @@ import { CollectRequestBody, NextApiRequestCollect } from 'pages/api/send'; import { createSession } from 'queries'; import { validate } from 'uuid'; import { loadSession, loadWebsite } from './query'; +import cache from './cache'; export async function findSession(req: NextApiRequestCollect) { const { payload } = getJsonBody(req); @@ -21,6 +22,8 @@ export async function findSession(req: NextApiRequestCollect) { const result = await parseToken(cacheToken, secret()); if (result) { + await checkUserBlock(result?.ownerId); + return result; } } @@ -39,6 +42,8 @@ export async function findSession(req: NextApiRequestCollect) { throw new Error(`Website not found: ${websiteId}.`); } + await checkUserBlock(website.userId); + const { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device } = await getClientInfo(req, payload); const sessionId = uuid(websiteId, hostname, ip, userAgent); @@ -88,5 +93,11 @@ export async function findSession(req: NextApiRequestCollect) { } } - return session; + return { ...session, ownerId: website.userId }; +} + +async function checkUserBlock(userId: string) { + if (process.env.ENABLE_BLOCKER && (await cache.fetchUserBlock(userId))) { + throw new Error('Usage Limit.'); + } } diff --git a/pages/api/send.ts b/pages/api/send.ts index 51ddb980..df7ceb6e 100644 --- a/pages/api/send.ts +++ b/pages/api/send.ts @@ -28,6 +28,7 @@ export interface NextApiRequestCollect extends NextApiRequest { session: { id: string; websiteId: string; + ownerId: string; hostname: string; browser: string; os: string; From f528fac7be6e47bd20a306ef408d77b0dc75bb8d Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Tue, 16 May 2023 11:06:42 -0700 Subject: [PATCH 29/88] fix teams api route and messaging --- components/pages/settings/teams/TeamLeaveForm.js | 4 ++-- components/pages/settings/teams/TeamMemberRemoveButton.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/pages/settings/teams/TeamLeaveForm.js b/components/pages/settings/teams/TeamLeaveForm.js index 9b61d4d9..954006ab 100644 --- a/components/pages/settings/teams/TeamLeaveForm.js +++ b/components/pages/settings/teams/TeamLeaveForm.js @@ -5,7 +5,7 @@ import useMessages from 'hooks/useMessages'; export function TeamLeaveForm({ teamId, userId, teamName, onSave, onClose }) { const { formatMessage, labels, messages, FormattedMessage } = useMessages(); const { del, useMutation } = useApi(); - const { mutate, error, isLoading } = useMutation(() => del(`/team/${teamId}/users/${userId}`)); + const { mutate, error, isLoading } = useMutation(() => del(`/teams/${teamId}/users/${userId}`)); const handleSubmit = async () => { mutate( @@ -22,7 +22,7 @@ export function TeamLeaveForm({ teamId, userId, teamName, onSave, onClose }) { return (

- {teamName} }} /> + {teamName} }} />

diff --git a/components/pages/settings/teams/TeamMemberRemoveButton.js b/components/pages/settings/teams/TeamMemberRemoveButton.js index 9dde180b..9f326476 100644 --- a/components/pages/settings/teams/TeamMemberRemoveButton.js +++ b/components/pages/settings/teams/TeamMemberRemoveButton.js @@ -5,7 +5,7 @@ import { Icon, Icons, LoadingButton, Text } from 'react-basics'; export function TeamMemberRemoveButton({ teamId, userId, disabled, onSave }) { const { formatMessage, labels } = useMessages(); const { del, useMutation } = useApi(); - const { mutate, isLoading } = useMutation(() => del(`/team/${teamId}/users/${userId}`)); + const { mutate, isLoading } = useMutation(() => del(`/teams/${teamId}/users/${userId}`)); const handleRemoveTeamMember = () => { mutate( From 2b5429aa1f0d8c25225e1d596854f16b5cfb0294 Mon Sep 17 00:00:00 2001 From: Aitor Alonso Date: Tue, 16 May 2023 21:02:56 +0200 Subject: [PATCH 30/88] fix: show year instead of epoch --- components/metrics/BarChart.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/metrics/BarChart.js b/components/metrics/BarChart.js index cd7070e8..8e1784df 100644 --- a/components/metrics/BarChart.js +++ b/components/metrics/BarChart.js @@ -52,6 +52,8 @@ export function BarChart({ return dateFormat(d, 'MMM d', locale); case 'month': return dateFormat(d, 'MMM', locale); + case 'year': + return dateFormat(d, 'YYY', locale); default: return label; } From f01668f4c8c4db0f993e63345b79028df12e3c5f Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Tue, 16 May 2023 12:17:51 -0700 Subject: [PATCH 31/88] add user role to login response --- pages/api/auth/login.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/auth/login.ts b/pages/api/auth/login.ts index 10c92647..64c2c26d 100644 --- a/pages/api/auth/login.ts +++ b/pages/api/auth/login.ts @@ -50,7 +50,7 @@ export default async ( return ok(res, { token, - user: { id: user.id, username: user.username, createdAt: user.createdAt }, + user: { id: user.id, username: user.username, role: user.role, createdAt: user.createdAt }, }); } From b48efef38b80d75c1da7e7ad202ccfa20d638752 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 16 May 2023 19:17:12 -0700 Subject: [PATCH 32/88] Allow multiple filters. --- components/metrics/MetricsTable.js | 19 +++++++++++++++---- components/metrics/QueryParametersTable.js | 2 +- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js index 3bb313cd..a918c0fb 100644 --- a/components/metrics/MetricsTable.js +++ b/components/metrics/MetricsTable.js @@ -73,16 +73,27 @@ export function MetricsTable({ const filteredData = useMemo(() => { if (data) { - const dataWithoutNullValues = data.filter(val => val.x !== null); - let items = percentFilter( - dataFilter ? dataFilter(dataWithoutNullValues, filterOptions) : dataWithoutNullValues, - ); + let items; + + if (dataFilter) { + if (Array.isArray(dataFilter)) { + items = dataFilter.reduce((arr, filter) => { + return filter(arr); + }, data); + } else { + items = dataFilter(data); + } + } + + items = percentFilter(items); + if (limit) { items = items.filter((e, i) => i < limit); } if (filterOptions?.sort === false) { return items; } + return items.sort(firstBy('y', -1).thenBy('x')); } return []; diff --git a/components/metrics/QueryParametersTable.js b/components/metrics/QueryParametersTable.js index c5f573e3..23193c2e 100644 --- a/components/metrics/QueryParametersTable.js +++ b/components/metrics/QueryParametersTable.js @@ -9,7 +9,7 @@ import styles from './QueryParametersTable.module.css'; const filters = { [FILTER_RAW]: emptyFilter, - [FILTER_COMBINED]: paramFilter, + [FILTER_COMBINED]: [emptyFilter, paramFilter], }; export function QueryParametersTable({ websiteId, showFilters, ...props }) { From b663146ad5ef654937b861bfb096037f885ad692 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 16 May 2023 19:31:40 -0700 Subject: [PATCH 33/88] Fixed data being undefined. --- components/metrics/MetricsTable.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js index a918c0fb..50262798 100644 --- a/components/metrics/MetricsTable.js +++ b/components/metrics/MetricsTable.js @@ -73,13 +73,13 @@ export function MetricsTable({ const filteredData = useMemo(() => { if (data) { - let items; + let items = data; if (dataFilter) { if (Array.isArray(dataFilter)) { items = dataFilter.reduce((arr, filter) => { return filter(arr); - }, data); + }, items); } else { items = dataFilter(data); } From c567eddff99c518cc128a3668d5762354898c096 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 16 May 2023 19:40:02 -0700 Subject: [PATCH 34/88] Fixed website undefined issue. --- pages/realtime/[id]/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pages/realtime/[id]/index.js b/pages/realtime/[id]/index.js index ba13ded3..c9e82622 100644 --- a/pages/realtime/[id]/index.js +++ b/pages/realtime/[id]/index.js @@ -9,9 +9,9 @@ export default function RealtimeDetailsPage() { const { id: websiteId } = router.query; const { formatMessage, labels } = useMessages(); const { get, useQuery } = useApi(); - const { data: website } = useQuery(['websites', websiteId], () => - websiteId ? get(`/websites/${websiteId}`, { enabled: !!websiteId }) : null, - ); + const { data: website } = useQuery(['websites', websiteId], () => get(`/websites/${websiteId}`), { + enabled: !!websiteId, + }); const title = `${formatMessage(labels.realtime)}${website?.name ? ` - ${website.name}` : ''}`; if (!websiteId) { From 19ac0fce3632c7ebc9226462a14d5b292a7c9ec1 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 16 May 2023 20:41:20 -0700 Subject: [PATCH 35/88] Added check-env script. --- package.json | 3 ++- scripts/check-env.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 scripts/check-env.js diff --git a/package.json b/package.json index 0db07f49..02820f22 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "scripts": { "dev": "next dev -p 3000", - "build": "npm-run-all build-db check-db build-tracker build-geo build-app", + "build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app", "start": "next start", "build-docker": "npm-run-all build-db build-tracker build-geo build-app", "start-docker": "npm-run-all check-db update-tracker start-server", @@ -27,6 +27,7 @@ "update-tracker": "node scripts/update-tracker.js", "update-db": "prisma migrate deploy", "check-db": "node scripts/check-db.js", + "check-env": "node scripts/check-env.js", "copy-db-files": "node scripts/copy-db-files.js", "extract-messages": "formatjs extract \"{pages,components}/**/*.js\" --out-file build/messages.json", "merge-messages": "node scripts/merge-messages.js", diff --git a/scripts/check-env.js b/scripts/check-env.js new file mode 100644 index 00000000..612e658c --- /dev/null +++ b/scripts/check-env.js @@ -0,0 +1,32 @@ +/* eslint-disable no-console */ +require('dotenv').config(); + +function checkMissing(vars) { + const missing = vars.reduce((arr, key) => { + if (!process.env[key]) { + arr.push(key); + } + }, []); + + if (missing.length) { + console.log(`The following environment variables are not defined:`); + for (const item of missing) { + console.log(' - ', item); + } + process.exit(1); + } +} + +checkMissing(['DATABASE_URL']); + +if (process.env.CLICKHOUSE_URL) { + checkMissing(['CA_CERT', 'CLIENT_CERT', 'CLIENT_KEY', 'KAFKA_BROKER', 'KAFKA_URL', 'REDIS_URL']); +} + +if (process.env.CLOUD_MODE) { + checkMissing(['CLOUD_URL']); +} + +if (process.env.ENABLE_BLOCKER) { + checkMissing(['REDIS_URL']); +} From 2259d4fae562168da7868ed1be8fb37c6b6d0c9a Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 16 May 2023 20:46:15 -0700 Subject: [PATCH 36/88] Fixed script. --- scripts/check-env.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/check-env.js b/scripts/check-env.js index 612e658c..10685517 100644 --- a/scripts/check-env.js +++ b/scripts/check-env.js @@ -6,6 +6,7 @@ function checkMissing(vars) { if (!process.env[key]) { arr.push(key); } + return arr; }, []); if (missing.length) { From 9d4808f5ef92d7cda75446780eff8f8594acb186 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 16 May 2023 21:13:17 -0700 Subject: [PATCH 37/88] Update check-env for CI build. --- scripts/check-env.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/check-env.js b/scripts/check-env.js index 10685517..a68fd822 100644 --- a/scripts/check-env.js +++ b/scripts/check-env.js @@ -18,7 +18,9 @@ function checkMissing(vars) { } } -checkMissing(['DATABASE_URL']); +if (!process.env.SKIP_DB_CHECK && !process.env.DATABASE_TYPE) { + checkMissing(['DATABASE_URL']); +} if (process.env.CLICKHOUSE_URL) { checkMissing(['CA_CERT', 'CLIENT_CERT', 'CLIENT_KEY', 'KAFKA_BROKER', 'KAFKA_URL', 'REDIS_URL']); From ad918c5bba1b618b7ddc286080d521e923b3fcbe Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 16 May 2023 21:19:39 -0700 Subject: [PATCH 38/88] Update CI workflow. --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c24c2e6d..c140f626 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,9 +26,9 @@ jobs: db-type: mysql steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} cache: 'npm' From 416fee0f537861b884c87fbe1b01d9e78a7a84e7 Mon Sep 17 00:00:00 2001 From: Abhilaksh Bansal Date: Wed, 17 May 2023 15:31:41 +0530 Subject: [PATCH 39/88] Fixed user delete error 500 --- queries/admin/user.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/queries/admin/user.ts b/queries/admin/user.ts index 412c7785..449a3e9b 100644 --- a/queries/admin/user.ts +++ b/queries/admin/user.ts @@ -197,9 +197,16 @@ export async function deleteUser( }), client.teamUser.deleteMany({ where: { - teamId: { - in: teamIds, - }, + OR: [ + { + teamId: { + in: teamIds, + }, + }, + { + userId, + }, + ], }, }), client.team.deleteMany({ From a5700d4a257f7e7cf6e0820cceebee664c7f452a Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 17 May 2023 23:20:06 -0700 Subject: [PATCH 40/88] Added reports section. --- assets/funnel.svg | 1 + assets/lightbulb.svg | 80 +++++++++++++++++++ assets/nodes.svg | 1 + components/input/DateFilter.js | 38 ++++----- components/input/WebsiteDateFilter.js | 26 ++++++ components/layout/NavBar.js | 1 + components/messages.js | 3 + components/metrics/WebsiteChart.js | 4 +- components/pages/reports/EventDataReport.js | 33 ++++++++ components/pages/reports/Report.js | 5 ++ components/pages/reports/ReportHeader.js | 42 ++++++++++ components/pages/reports/ReportsList.js | 66 +++++++++++++++ .../pages/reports/ReportsList.module.css | 32 ++++++++ components/pages/reports/reports.module.css | 14 ++++ .../settings/profile/DateRangeSetting.js | 3 +- hooks/index.js | 17 ++++ hooks/useApi.ts | 4 +- hooks/useConfig.js | 4 +- hooks/useCountryNames.js | 4 +- hooks/useDateRange.js | 4 +- hooks/useDocumentClick.js | 4 +- hooks/useEscapeKey.js | 4 +- hooks/useForceUpdate.js | 4 +- hooks/useLanguageNames.js | 4 +- hooks/useLocale.js | 4 +- hooks/useMessages.js | 4 +- hooks/usePageQuery.js | 4 +- hooks/useRequireLogin.js | 4 +- hooks/useShareToken.js | 4 +- hooks/useSticky.js | 4 +- hooks/useTheme.js | 4 +- hooks/useTimezone.js | 4 +- hooks/useUser.js | 4 +- pages/api/websites/[id]/data.ts | 13 +++ pages/reports/event-data/index.js | 5 ++ pages/reports/index.js | 13 +++ 36 files changed, 422 insertions(+), 43 deletions(-) create mode 100644 assets/funnel.svg create mode 100644 assets/lightbulb.svg create mode 100644 assets/nodes.svg create mode 100644 components/input/WebsiteDateFilter.js create mode 100644 components/pages/reports/EventDataReport.js create mode 100644 components/pages/reports/Report.js create mode 100644 components/pages/reports/ReportHeader.js create mode 100644 components/pages/reports/ReportsList.js create mode 100644 components/pages/reports/ReportsList.module.css create mode 100644 components/pages/reports/reports.module.css create mode 100644 hooks/index.js create mode 100644 pages/api/websites/[id]/data.ts create mode 100644 pages/reports/event-data/index.js create mode 100644 pages/reports/index.js diff --git a/assets/funnel.svg b/assets/funnel.svg new file mode 100644 index 00000000..46f4623f --- /dev/null +++ b/assets/funnel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/lightbulb.svg b/assets/lightbulb.svg new file mode 100644 index 00000000..73b699a3 --- /dev/null +++ b/assets/lightbulb.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/nodes.svg b/assets/nodes.svg new file mode 100644 index 00000000..0bbd37c4 --- /dev/null +++ b/assets/nodes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/input/DateFilter.js b/components/input/DateFilter.js index b6c1ee72..b7521e27 100644 --- a/components/input/DateFilter.js +++ b/components/input/DateFilter.js @@ -3,31 +3,22 @@ import { Icon, Modal, Dropdown, Item, Text, Flexbox } from 'react-basics'; import { endOfYear, isSameDay } from 'date-fns'; import DatePickerForm from 'components/metrics/DatePickerForm'; import useLocale from 'hooks/useLocale'; -import { dateFormat, getDateRangeValues } from 'lib/date'; +import { dateFormat } from 'lib/date'; import Icons from 'components/icons'; -import useApi from 'hooks/useApi'; -import useDateRange from 'hooks/useDateRange'; import useMessages from 'hooks/useMessages'; -export function DateFilter({ websiteId, value, className }) { +export function DateFilter({ + value, + startDate, + endDate, + className, + onChange, + showAllTime = false, + alignment = 'end', +}) { const { formatMessage, labels } = useMessages(); - const { get } = useApi(); - const [dateRange, setDateRange] = useDateRange(websiteId); - const { startDate, endDate } = dateRange; const [showPicker, setShowPicker] = useState(false); - async function handleDateChange(value) { - if (value === 'all' && websiteId) { - const data = await get(`/websites/${websiteId}`); - - if (data) { - setDateRange({ value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) }); - } - } else if (value !== 'all') { - setDateRange(value); - } - } - const options = [ { label: formatMessage(labels.today), value: '1day' }, { @@ -61,7 +52,7 @@ export function DateFilter({ websiteId, value, className }) { value: '90day', }, { label: formatMessage(labels.thisYear), value: '1year' }, - websiteId && { + showAllTime && { label: formatMessage(labels.allTime), value: 'all', divider: true, @@ -86,12 +77,12 @@ export function DateFilter({ websiteId, value, className }) { setShowPicker(true); return; } - handleDateChange(value); + onChange(value); }; const handlePickerChange = value => { setShowPicker(false); - handleDateChange(value); + onChange(value); }; const handleClose = () => setShowPicker(false); @@ -103,7 +94,8 @@ export function DateFilter({ websiteId, value, className }) { items={options} renderValue={renderValue} value={value} - alignment="end" + alignment={alignment} + placeholder={formatMessage(labels.selectDate)} onChange={handleChange} > {({ label, value, divider }) => ( diff --git a/components/input/WebsiteDateFilter.js b/components/input/WebsiteDateFilter.js new file mode 100644 index 00000000..0d5d7569 --- /dev/null +++ b/components/input/WebsiteDateFilter.js @@ -0,0 +1,26 @@ +import { getDateRangeValues } from 'lib/date'; +import useApi from 'hooks/useApi'; +import useDateRange from 'hooks/useDateRange'; +import DateFilter from './DateFilter'; + +export default function WebsiteDateFilter({ websiteId, value }) { + const { get } = useApi(); + const [dateRange, setDateRange] = useDateRange(websiteId); + const { startDate, endDate } = dateRange; + + const handleChange = async value => { + if (value === 'all' && websiteId) { + const data = await get(`/websites/${websiteId}`); + + if (data) { + setDateRange({ value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) }); + } + } else if (value !== 'all') { + setDateRange(value); + } + }; + + return ( + + ); +} diff --git a/components/layout/NavBar.js b/components/layout/NavBar.js index 5a6c877e..b5532ba0 100644 --- a/components/layout/NavBar.js +++ b/components/layout/NavBar.js @@ -18,6 +18,7 @@ export function NavBar() { const links = [ { label: formatMessage(labels.dashboard), url: '/dashboard' }, + { label: formatMessage(labels.reports), url: '/reports' }, { label: formatMessage(labels.realtime), url: '/realtime' }, !cloudMode && { label: formatMessage(labels.settings), url: '/settings' }, ].filter(n => n); diff --git a/components/messages.js b/components/messages.js index 245e8591..bdc8770d 100644 --- a/components/messages.js +++ b/components/messages.js @@ -97,6 +97,7 @@ export const labels = defineMessages({ allTime: { id: 'label.all-time', defaultMessage: 'All time' }, customRange: { id: 'label.custom-range', defaultMessage: 'Custom range' }, selectWebsite: { id: 'label.select-website', defaultMessage: 'Select website' }, + selectDate: { id: 'label.select-date', defaultMessage: 'Select date' }, all: { id: 'label.all', defaultMessage: 'All' }, sessions: { id: 'label.sessions', defaultMessage: 'Sessions' }, pageNotFound: { id: 'message.page-not-found', defaultMessage: 'Page not found' }, @@ -117,6 +118,8 @@ export const labels = defineMessages({ view: { id: 'label.view', defaultMessage: 'View' }, cities: { id: 'label.cities', defaultMessage: 'Cities' }, regions: { id: 'label.regions', defaultMessage: 'Regions' }, + reports: { id: 'label.reports', defaultMessage: 'Reports' }, + eventData: { id: 'label.event-data', defaultMessage: 'Event data' }, }); export const messages = defineMessages({ diff --git a/components/metrics/WebsiteChart.js b/components/metrics/WebsiteChart.js index 6614d40f..7b902df1 100644 --- a/components/metrics/WebsiteChart.js +++ b/components/metrics/WebsiteChart.js @@ -5,7 +5,7 @@ import classNames from 'classnames'; import PageviewsChart from './PageviewsChart'; import MetricsBar from './MetricsBar'; import WebsiteHeader from './WebsiteHeader'; -import DateFilter from 'components/input/DateFilter'; +import WebsiteDateFilter from 'components/input/WebsiteDateFilter'; import ErrorMessage from 'components/common/ErrorMessage'; import FilterTags from 'components/metrics/FilterTags'; import RefreshButton from 'components/input/RefreshButton'; @@ -107,7 +107,7 @@ export function WebsiteChart({
- +
diff --git a/components/pages/reports/EventDataReport.js b/components/pages/reports/EventDataReport.js new file mode 100644 index 00000000..8c01e25f --- /dev/null +++ b/components/pages/reports/EventDataReport.js @@ -0,0 +1,33 @@ +import { useState } from 'react'; +import { Form, FormRow, FormInput, TextField } from 'react-basics'; +import AppLayout from 'components/layout/AppLayout'; +import Report from './Report'; +import ReportHeader from './ReportHeader'; +import useMessages from 'hooks/useMessages'; +import Nodes from 'assets/nodes.svg'; +import styles from './reports.module.css'; + +export default function EventDataReport({ websiteId, data }) { + const [values, setValues] = useState({ query: '' }); + const { formatMessage, labels } = useMessages(); + + return ( + + + } /> +
+
+ + + + + + + +
+
+
+
+
+ ); +} diff --git a/components/pages/reports/Report.js b/components/pages/reports/Report.js new file mode 100644 index 00000000..1754e89f --- /dev/null +++ b/components/pages/reports/Report.js @@ -0,0 +1,5 @@ +import Page from 'components/layout/Page'; + +export default function Report({ children, ...props }) { + return {children}; +} diff --git a/components/pages/reports/ReportHeader.js b/components/pages/reports/ReportHeader.js new file mode 100644 index 00000000..44502149 --- /dev/null +++ b/components/pages/reports/ReportHeader.js @@ -0,0 +1,42 @@ +import { useState } from 'react'; +import { Flexbox, Icon, Text } from 'react-basics'; +import WebsiteSelect from 'components/input/WebsiteSelect'; +import PageHeader from 'components/layout/PageHeader'; +import DateFilter from 'components/input/DateFilter'; +import { parseDateRange } from 'lib/date'; + +export default function ReportHeader({ title, icon }) { + const [websiteId, setWebsiteId] = useState(); + const [dateRange, setDateRange] = useState({}); + const { value, startDate, endDate } = dateRange; + + const handleSelect = id => { + setWebsiteId(id); + }; + + const handleDateChange = value => setDateRange(parseDateRange(value)); + + const Title = () => { + return ( + <> + {icon} + {title} + + ); + }; + + return ( + }> + + + + + + ); +} diff --git a/components/pages/reports/ReportsList.js b/components/pages/reports/ReportsList.js new file mode 100644 index 00000000..44dc556d --- /dev/null +++ b/components/pages/reports/ReportsList.js @@ -0,0 +1,66 @@ +import Link from 'next/link'; +import { Button, Icons, Text, Icon } from 'react-basics'; +import Page from 'components/layout/Page'; +import PageHeader from 'components/layout/PageHeader'; +import Funnel from 'assets/funnel.svg'; +import Nodes from 'assets/nodes.svg'; +import Lightbulb from 'assets/lightbulb.svg'; +import styles from './ReportsList.module.css'; + +const reports = [ + { + title: 'Event data', + description: 'Query your event data.', + url: '/reports/event-data', + icon: , + }, + { + title: 'Funnel', + description: 'Understand the conversion and drop-off rate of users.', + url: '/reports/funnel', + icon: , + }, + { + title: 'Insights', + description: 'Explore your data by applying segments and filters.', + url: '/reports/insights', + icon: , + }, +]; + +function Report({ title, description, url, icon }) { + return ( +
+
+ {icon} + {title} +
+
{description}
+
+ + + +
+
+ ); +} + +export default function ReportsList() { + return ( + + +
+ {reports.map(({ title, description, url, icon }) => { + return ( + + ); + })} +
+
+ ); +} diff --git a/components/pages/reports/ReportsList.module.css b/components/pages/reports/ReportsList.module.css new file mode 100644 index 00000000..0cdcb835 --- /dev/null +++ b/components/pages/reports/ReportsList.module.css @@ -0,0 +1,32 @@ +.reports { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); + gap: 20px; +} + +.report { + display: flex; + flex-direction: column; + gap: 20px; + padding: 20px; + border: 1px solid var(--base500); + border-radius: var(--border-radius); +} + +.title { + display: flex; + gap: 10px; + align-items: center; + font-size: var(--font-size-lg); + font-weight: 700; +} + +.description { + flex: 1; +} + +.buttons { + display: flex; + align-items: center; + justify-content: center; +} diff --git a/components/pages/reports/reports.module.css b/components/pages/reports/reports.module.css new file mode 100644 index 00000000..e18a857a --- /dev/null +++ b/components/pages/reports/reports.module.css @@ -0,0 +1,14 @@ +.container { + display: grid; + grid-template-rows: 1fr; + grid-template-columns: max-content 1fr; +} + +.menu { + width: 300px; + grid-column: 1 / 2; +} + +.content { + grid-column: 2 / 3; +} diff --git a/components/pages/settings/profile/DateRangeSetting.js b/components/pages/settings/profile/DateRangeSetting.js index 152aba1d..44c3dc42 100644 --- a/components/pages/settings/profile/DateRangeSetting.js +++ b/components/pages/settings/profile/DateRangeSetting.js @@ -9,11 +9,12 @@ export function DateRangeSetting() { const [dateRange, setDateRange] = useDateRange(); const { startDate, endDate, value } = dateRange; + const handleChange = value => setDateRange(value); const handleReset = () => setDateRange(DEFAULT_DATE_RANGE); return ( - + ); diff --git a/hooks/index.js b/hooks/index.js new file mode 100644 index 00000000..15898eb5 --- /dev/null +++ b/hooks/index.js @@ -0,0 +1,17 @@ +export * from './useApi'; +export * from './useConfig'; +export * from './useCountryNames'; +export * from './useDateRange'; +export * from './useDocumentClick'; +export * from './useEscapeKey'; +export * from './useForceUpdate'; +export * from './useLanguageNames'; +export * from './useLocale'; +export * from './useMessages'; +export * from './usePageQuery'; +export * from './useRequireLogin'; +export * from './useShareToken'; +export * from './useSticky'; +export * from './useTheme'; +export * from './useTimezone'; +export * from './useUser'; diff --git a/hooks/useApi.ts b/hooks/useApi.ts index 9e1e7e20..f41547a9 100644 --- a/hooks/useApi.ts +++ b/hooks/useApi.ts @@ -7,7 +7,7 @@ import useStore from 'store/app'; const selector = state => state.shareToken; -export default function useApi() { +export function useApi() { const { basePath } = useRouter(); const shareToken = useStore(selector); @@ -18,3 +18,5 @@ export default function useApi() { return { get, post, put, del, ...reactQuery }; } + +export default useApi; diff --git a/hooks/useConfig.js b/hooks/useConfig.js index b395829c..6dda7b74 100644 --- a/hooks/useConfig.js +++ b/hooks/useConfig.js @@ -4,7 +4,7 @@ import useApi from 'hooks/useApi'; let loading = false; -export default function useConfig() { +export function useConfig() { const { config } = useStore(); const { get } = useApi(); @@ -23,3 +23,5 @@ export default function useConfig() { return config || {}; } + +export default useConfig; diff --git a/hooks/useCountryNames.js b/hooks/useCountryNames.js index 0834202b..60c992a5 100644 --- a/hooks/useCountryNames.js +++ b/hooks/useCountryNames.js @@ -7,7 +7,7 @@ const countryNames = { 'en-US': enUS, }; -export default function useCountryNames(locale) { +export function useCountryNames(locale) { const [list, setList] = useState(countryNames[locale] || enUS); const { basePath } = useRouter(); @@ -32,3 +32,5 @@ export default function useCountryNames(locale) { return list; } + +export default useCountryNames; diff --git a/hooks/useDateRange.js b/hooks/useDateRange.js index a9896065..e8a542f4 100644 --- a/hooks/useDateRange.js +++ b/hooks/useDateRange.js @@ -5,7 +5,7 @@ import useLocale from './useLocale'; import websiteStore, { setWebsiteDateRange } from 'store/websites'; import appStore, { setDateRange } from 'store/app'; -export default function useDateRange(websiteId) { +export function useDateRange(websiteId) { const { locale } = useLocale(); const websiteConfig = websiteStore(state => state[websiteId]?.dateRange); const defaultConfig = DEFAULT_DATE_RANGE; @@ -23,3 +23,5 @@ export default function useDateRange(websiteId) { return [dateRange, saveDateRange]; } + +export default useDateRange; diff --git a/hooks/useDocumentClick.js b/hooks/useDocumentClick.js index e1baae7e..be3d09be 100644 --- a/hooks/useDocumentClick.js +++ b/hooks/useDocumentClick.js @@ -1,6 +1,6 @@ import { useEffect } from 'react'; -export default function useDocumentClick(handler) { +export function useDocumentClick(handler) { useEffect(() => { document.addEventListener('click', handler); @@ -11,3 +11,5 @@ export default function useDocumentClick(handler) { return null; } + +export default useDocumentClick; diff --git a/hooks/useEscapeKey.js b/hooks/useEscapeKey.js index b8020c31..1a17f18f 100644 --- a/hooks/useEscapeKey.js +++ b/hooks/useEscapeKey.js @@ -1,6 +1,6 @@ import { useEffect, useCallback } from 'react'; -export default function useEscapeKey(handler) { +export function useEscapeKey(handler) { const escFunction = useCallback(event => { if (event.keyCode === 27) { handler(event); @@ -17,3 +17,5 @@ export default function useEscapeKey(handler) { return null; } + +export default useEscapeKey; diff --git a/hooks/useForceUpdate.js b/hooks/useForceUpdate.js index 2b8d6101..35f7fe16 100644 --- a/hooks/useForceUpdate.js +++ b/hooks/useForceUpdate.js @@ -1,9 +1,11 @@ import { useCallback, useState } from 'react'; -export default function useForceUpdate() { +export function useForceUpdate() { const [, update] = useState(Object.create(null)); return useCallback(() => { update(Object.create(null)); }, [update]); } + +export default useForceUpdate; diff --git a/hooks/useLanguageNames.js b/hooks/useLanguageNames.js index 3b153f28..aa5cbe39 100644 --- a/hooks/useLanguageNames.js +++ b/hooks/useLanguageNames.js @@ -7,7 +7,7 @@ const languageNames = { 'en-US': enUS, }; -export default function useLanguageNames(locale) { +export function useLanguageNames(locale) { const [list, setList] = useState(languageNames[locale] || enUS); const { basePath } = useRouter(); @@ -32,3 +32,5 @@ export default function useLanguageNames(locale) { return list; } + +export default useLanguageNames; diff --git a/hooks/useLocale.js b/hooks/useLocale.js index 5cece347..1279cea9 100644 --- a/hooks/useLocale.js +++ b/hooks/useLocale.js @@ -13,7 +13,7 @@ const messages = { const selector = state => state.locale; -export default function useLocale() { +export function useLocale() { const locale = useStore(selector); const { basePath } = useRouter(); const forceUpdate = useForceUpdate(); @@ -61,3 +61,5 @@ export default function useLocale() { return { locale, saveLocale, messages, dir, dateLocale }; } + +export default useLocale; diff --git a/hooks/useMessages.js b/hooks/useMessages.js index 1bb65778..0719afd8 100644 --- a/hooks/useMessages.js +++ b/hooks/useMessages.js @@ -1,7 +1,7 @@ import { useIntl, FormattedMessage } from 'react-intl'; import { messages, labels } from 'components/messages'; -export default function useMessages() { +export function useMessages() { const { formatMessage } = useIntl(); function getMessage(id) { @@ -12,3 +12,5 @@ export default function useMessages() { return { formatMessage, FormattedMessage, messages, labels, getMessage }; } + +export default useMessages; diff --git a/hooks/usePageQuery.js b/hooks/usePageQuery.js index b2f0acf1..b275d580 100644 --- a/hooks/usePageQuery.js +++ b/hooks/usePageQuery.js @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { useRouter } from 'next/router'; import { buildUrl } from 'next-basics'; -export default function usePageQuery() { +export function usePageQuery() { const router = useRouter(); const { pathname, search } = location; const { asPath } = router; @@ -29,3 +29,5 @@ export default function usePageQuery() { return { pathname, query, resolveUrl, router }; } + +export default usePageQuery; diff --git a/hooks/useRequireLogin.js b/hooks/useRequireLogin.js index 24cfdf0b..3a95c988 100644 --- a/hooks/useRequireLogin.js +++ b/hooks/useRequireLogin.js @@ -3,7 +3,7 @@ import { useRouter } from 'next/router'; import useApi from 'hooks/useApi'; import useUser from 'hooks/useUser'; -export default function useRequireLogin() { +export function useRequireLogin() { const router = useRouter(); const { get } = useApi(); const { user, setUser } = useUser(); @@ -26,3 +26,5 @@ export default function useRequireLogin() { return { user }; } + +export default useRequireLogin; diff --git a/hooks/useShareToken.js b/hooks/useShareToken.js index bac7ec97..3d6b9698 100644 --- a/hooks/useShareToken.js +++ b/hooks/useShareToken.js @@ -4,7 +4,7 @@ import useApi from './useApi'; const selector = state => state.shareToken; -export default function useShareToken(shareId) { +export function useShareToken(shareId) { const shareToken = useStore(selector); const { get } = useApi(); @@ -24,3 +24,5 @@ export default function useShareToken(shareId) { return shareToken; } + +export default useShareToken; diff --git a/hooks/useSticky.js b/hooks/useSticky.js index ae4dce72..be33f6ed 100644 --- a/hooks/useSticky.js +++ b/hooks/useSticky.js @@ -1,6 +1,6 @@ import { useState, useEffect, useRef } from 'react'; -export default function useSticky({ enabled = true, threshold = 1 }) { +export function useSticky({ enabled = true, threshold = 1 }) { const [isSticky, setIsSticky] = useState(false); const ref = useRef(null); @@ -21,3 +21,5 @@ export default function useSticky({ enabled = true, threshold = 1 }) { return { ref, isSticky }; } + +export default useSticky; diff --git a/hooks/useTheme.js b/hooks/useTheme.js index c50f442f..a10894e5 100644 --- a/hooks/useTheme.js +++ b/hooks/useTheme.js @@ -5,7 +5,7 @@ import { THEME_CONFIG } from 'lib/constants'; const selector = state => state.theme; -export default function useTheme() { +export function useTheme() { const defaultTheme = typeof window !== 'undefined' ? window?.matchMedia('(prefers-color-scheme: dark)')?.matches @@ -34,3 +34,5 @@ export default function useTheme() { return [theme, saveTheme]; } + +export default useTheme; diff --git a/hooks/useTimezone.js b/hooks/useTimezone.js index 8eb5d5f8..fb347c4d 100644 --- a/hooks/useTimezone.js +++ b/hooks/useTimezone.js @@ -3,7 +3,7 @@ import { getTimezone } from 'lib/date'; import { getItem, setItem } from 'next-basics'; import { TIMEZONE_CONFIG } from 'lib/constants'; -export default function useTimezone() { +export function useTimezone() { const [timezone, setTimezone] = useState(getItem(TIMEZONE_CONFIG) || getTimezone()); const saveTimezone = useCallback( @@ -16,3 +16,5 @@ export default function useTimezone() { return [timezone, saveTimezone]; } + +export default useTimezone; diff --git a/hooks/useUser.js b/hooks/useUser.js index 6b73c113..c5f1a826 100644 --- a/hooks/useUser.js +++ b/hooks/useUser.js @@ -2,8 +2,10 @@ import useStore, { setUser } from 'store/app'; const selector = state => state.user; -export default function useUser() { +export function useUser() { const user = useStore(selector); return { user, setUser }; } + +export default useUser; diff --git a/pages/api/websites/[id]/data.ts b/pages/api/websites/[id]/data.ts new file mode 100644 index 00000000..93e97067 --- /dev/null +++ b/pages/api/websites/[id]/data.ts @@ -0,0 +1,13 @@ +import { NextApiResponse } from 'next'; +import { useAuth } from 'lib/middleware'; +import { NextApiRequestQueryBody, User } from 'lib/types'; +import { ok } from 'next-basics'; + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useAuth(req, res); + + return ok(res, req.auth.user); +}; diff --git a/pages/reports/event-data/index.js b/pages/reports/event-data/index.js new file mode 100644 index 00000000..154c79be --- /dev/null +++ b/pages/reports/event-data/index.js @@ -0,0 +1,5 @@ +import EventDataReport from 'components/pages/reports/EventDataReport'; + +export default function Report() { + return ; +} diff --git a/pages/reports/index.js b/pages/reports/index.js new file mode 100644 index 00000000..b26023cc --- /dev/null +++ b/pages/reports/index.js @@ -0,0 +1,13 @@ +import AppLayout from 'components/layout/AppLayout'; +import ReportsList from 'components/pages/reports/ReportsList'; +import useMessages from 'hooks/useMessages'; + +export default function ReportsPage() { + const { formatMessage, labels } = useMessages(); + + return ( + + + + ); +} From 55e91d56596e008548710755932b30f1675e4ff4 Mon Sep 17 00:00:00 2001 From: Joseph Lee Date: Thu, 18 May 2023 17:26:52 +0900 Subject: [PATCH 41/88] Fix to add basePath to trackingCode generation --- components/pages/settings/websites/TrackingCode.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/components/pages/settings/websites/TrackingCode.js b/components/pages/settings/websites/TrackingCode.js index 425dda06..c847ed0d 100644 --- a/components/pages/settings/websites/TrackingCode.js +++ b/components/pages/settings/websites/TrackingCode.js @@ -4,10 +4,12 @@ import useConfig from 'hooks/useConfig'; export function TrackingCode({ websiteId }) { const { formatMessage, messages } = useMessages(); - const { trackerScriptName } = useConfig(); + const { basePath, trackerScriptName } = useConfig(); const url = trackerScriptName?.startsWith('http') ? trackerScriptName - : `${location.origin}/${trackerScriptName?.split(',')?.map(n => n.trim())?.[0] || 'script.js'}`; + : `${location.origin}${basePath}/${ + trackerScriptName?.split(',')?.map(n => n.trim())?.[0] || 'script.js' + }`; const code = ``; From de509e7cccfbe988a627f498ea3a400a893e563e Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 18 May 2023 11:17:35 -0700 Subject: [PATCH 42/88] checkpoint --- components/layout/NavBar.js | 1 + components/layout/ReportsLayout.js | 23 +++ components/layout/ReportsLayout.module.css | 23 +++ components/layout/SettingsLayout.js | 1 - components/pages/reports/FunnelChart.js | 42 ---- .../pages/reports/FunnelChart.module.css | 7 - components/pages/reports/FunnelDetails.js | 0 components/pages/reports/ReportDropdown.js | 0 components/pages/reports/ReportForm.js | 28 +++ .../pages/reports/ReportForm.module.css | 23 +++ .../pages/reports/funnel/FunnelChart.js | 186 ++++++++++++++++++ .../reports/funnel/FunnelChart.module.css | 23 +++ .../pages/reports/{ => funnel}/FunnelForm.js | 8 +- .../{ => funnel}/FunnelForm.module.css | 5 - .../pages/reports/{ => funnel}/FunnelPage.js | 20 +- .../{ => funnel}/FunnelPage.module.css | 0 .../pages/reports/{ => funnel}/FunnelTable.js | 3 +- package.json | 1 - pages/_app.js | 1 - pages/{settings => }/reports/funnel.js | 9 +- pages/{settings => }/reports/index.js | 2 +- .../analytics/pageview/getPageviewFunnel.ts | 17 +- styles/funnelChart.css | 148 -------------- 23 files changed, 335 insertions(+), 236 deletions(-) create mode 100644 components/layout/ReportsLayout.js create mode 100644 components/layout/ReportsLayout.module.css delete mode 100644 components/pages/reports/FunnelChart.js delete mode 100644 components/pages/reports/FunnelChart.module.css delete mode 100644 components/pages/reports/FunnelDetails.js delete mode 100644 components/pages/reports/ReportDropdown.js create mode 100644 components/pages/reports/ReportForm.module.css create mode 100644 components/pages/reports/funnel/FunnelChart.js create mode 100644 components/pages/reports/funnel/FunnelChart.module.css rename components/pages/reports/{ => funnel}/FunnelForm.js (96%) rename components/pages/reports/{ => funnel}/FunnelForm.module.css (83%) rename components/pages/reports/{ => funnel}/FunnelPage.js (67%) rename components/pages/reports/{ => funnel}/FunnelPage.module.css (100%) rename components/pages/reports/{ => funnel}/FunnelTable.js (73%) rename pages/{settings => }/reports/funnel.js (54%) rename pages/{settings => }/reports/index.js (90%) delete mode 100644 styles/funnelChart.css diff --git a/components/layout/NavBar.js b/components/layout/NavBar.js index 5a6c877e..ba06d172 100644 --- a/components/layout/NavBar.js +++ b/components/layout/NavBar.js @@ -19,6 +19,7 @@ export function NavBar() { const links = [ { label: formatMessage(labels.dashboard), url: '/dashboard' }, { label: formatMessage(labels.realtime), url: '/realtime' }, + { label: formatMessage(labels.reports), url: '/reports/funnel' }, !cloudMode && { label: formatMessage(labels.settings), url: '/settings' }, ].filter(n => n); diff --git a/components/layout/ReportsLayout.js b/components/layout/ReportsLayout.js new file mode 100644 index 00000000..fd63a67e --- /dev/null +++ b/components/layout/ReportsLayout.js @@ -0,0 +1,23 @@ +import { Column, Row } from 'react-basics'; +import styles from './ReportsLayout.module.css'; + +export function SettingsLayout({ children, filter, header }) { + return ( + <> + {header} + + {filter && ( + +

Filters

+ {filter} +
+ )} + + {children} + +
+ + ); +} + +export default SettingsLayout; diff --git a/components/layout/ReportsLayout.module.css b/components/layout/ReportsLayout.module.css new file mode 100644 index 00000000..6922665f --- /dev/null +++ b/components/layout/ReportsLayout.module.css @@ -0,0 +1,23 @@ +.filter { + margin-top: 30px; + min-width: 200px; + max-width: 100vw; + padding: 10px; + background: var(--base50); + border-radius: 5px; + border: 1px solid var(--border-color); +} + +.filter h2 { + padding-bottom: 20px; +} + +.content { + min-height: 50vh; +} + +@media only screen and (max-width: 768px) { + .menu { + display: none; + } +} diff --git a/components/layout/SettingsLayout.js b/components/layout/SettingsLayout.js index d58154ca..c79f0909 100644 --- a/components/layout/SettingsLayout.js +++ b/components/layout/SettingsLayout.js @@ -15,7 +15,6 @@ export function SettingsLayout({ children }) { const items = [ { key: 'websites', label: formatMessage(labels.websites), url: '/settings/websites' }, { key: 'teams', label: formatMessage(labels.teams), url: '/settings/teams' }, - { key: 'reports', label: 'Reports', url: '/settings/reports/funnel' }, user.isAdmin && { key: 'users', label: formatMessage(labels.users), url: '/settings/users' }, { key: 'profile', label: formatMessage(labels.profile), url: '/settings/profile' }, ].filter(n => n); diff --git a/components/pages/reports/FunnelChart.js b/components/pages/reports/FunnelChart.js deleted file mode 100644 index ec03acab..00000000 --- a/components/pages/reports/FunnelChart.js +++ /dev/null @@ -1,42 +0,0 @@ -import FunnelGraph from 'funnel-graph-js/dist/js/funnel-graph'; -import { useEffect, useRef } from 'react'; -import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; -import useMessages from 'hooks/useMessages'; -import styles from './FunnelChart.module.css'; -import classNames from 'classnames'; - -export default function FunnelChart({ data }) { - const { formatMessage, labels, messages } = useMessages(); - const funnel = useRef(null); - - useEffect(() => { - if (data && data.length > 0) { - funnel.current.innerHTML = ''; - - const chartData = { - labels: data.map(a => a.url), - colors: ['#147af3', '#e0f2ff'], - values: data.map(a => a.count), - }; - - const graph = new FunnelGraph({ - container: '.funnel', - gradientDirection: 'horizontal', - data: chartData, - displayPercent: true, - direction: 'Vertical', - width: 1000, - height: 350, - }); - - graph.draw(); - } - }, [data]); - - return ( - <> - {data?.length > 0 &&
} - {data?.length === 0 && } - - ); -} diff --git a/components/pages/reports/FunnelChart.module.css b/components/pages/reports/FunnelChart.module.css deleted file mode 100644 index 1d7eb37e..00000000 --- a/components/pages/reports/FunnelChart.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.funnel div { - color: var(--font-color100) !important; -} - -.funnel svg { - max-width: 100%; -} diff --git a/components/pages/reports/FunnelDetails.js b/components/pages/reports/FunnelDetails.js deleted file mode 100644 index e69de29b..00000000 diff --git a/components/pages/reports/ReportDropdown.js b/components/pages/reports/ReportDropdown.js deleted file mode 100644 index e69de29b..00000000 diff --git a/components/pages/reports/ReportForm.js b/components/pages/reports/ReportForm.js index e69de29b..b32a8531 100644 --- a/components/pages/reports/ReportForm.js +++ b/components/pages/reports/ReportForm.js @@ -0,0 +1,28 @@ +import useMessages from 'hooks/useMessages'; +import { Form, FormButtons, FormInput, FormRow, SubmitButton, TextField } from 'react-basics'; + +export function FunnelForm({ onSearch }) { + const { formatMessage, labels } = useMessages(); + + const handleSubmit = () => {}; + + return ( + <> +
+ + + + + + + + + Save + + +
+ + ); +} + +export default FunnelForm; diff --git a/components/pages/reports/ReportForm.module.css b/components/pages/reports/ReportForm.module.css new file mode 100644 index 00000000..2a07d552 --- /dev/null +++ b/components/pages/reports/ReportForm.module.css @@ -0,0 +1,23 @@ +.filter { + min-width: 200px; +} + +.hiddenInput { + max-height: 100px; +} + +.hiddenInput { + visibility: hidden; + min-height: 0px; + max-height: 0px; +} + +.hidden { + visibility: hidden; + min-height: 0px; + max-height: 0px; +} + +.urlFormRow label { + min-width: 80px; +} diff --git a/components/pages/reports/funnel/FunnelChart.js b/components/pages/reports/funnel/FunnelChart.js new file mode 100644 index 00000000..5a0b5699 --- /dev/null +++ b/components/pages/reports/funnel/FunnelChart.js @@ -0,0 +1,186 @@ +import Chart from 'chart.js/auto'; +import classNames from 'classnames'; +import { colord } from 'colord'; +import HoverTooltip from 'components/common/HoverTooltip'; +import Legend from 'components/metrics/Legend'; +import useLocale from 'hooks/useLocale'; +import useMessages from 'hooks/useMessages'; +import useTheme from 'hooks/useTheme'; +import { DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants'; +import { dateFormat } from 'lib/date'; +import { formatLongNumber } from 'lib/format'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Loading, StatusLight } from 'react-basics'; +import styles from './FunnelChart.module.css'; + +export function FunnelChart({ + data, + animationDuration = DEFAULT_ANIMATION_DURATION, + stacked = false, + loading = false, + onCreate = () => {}, + onUpdate = () => {}, + className, +}) { + const { formatMessage, labels } = useMessages(); + const canvas = useRef(); + const chart = useRef(null); + const [tooltip, setTooltip] = useState(null); + const { locale } = useLocale(); + const [theme] = useTheme(); + + const datasets = useMemo(() => { + const primaryColor = colord(THEME_COLORS[theme].primary); + return [ + { + label: formatMessage(labels.uniqueVisitors), + data: data, + borderWidth: 1, + hoverBackgroundColor: primaryColor.alpha(0.9).toRgbString(), + backgroundColor: primaryColor.alpha(0.6).toRgbString(), + borderColor: primaryColor.alpha(0.9).toRgbString(), + hoverBorderColor: primaryColor.toRgbString(), + }, + ]; + }, [data]); + + const colors = useMemo( + () => ({ + text: THEME_COLORS[theme].gray700, + line: THEME_COLORS[theme].gray200, + }), + [theme], + ); + + const renderYLabel = label => { + return +label > 1000 ? formatLongNumber(label) : label; + }; + + const renderTooltip = useCallback(model => { + const { opacity, labelColors, dataPoints } = model.tooltip; + + if (!dataPoints?.length || !opacity) { + setTooltip(null); + return; + } + + setTooltip( +
+
+ +
+
{dataPoints[0].raw.x}
+
{formatLongNumber(dataPoints[0].raw.y)}
+
+
+
+
, + ); + }, []); + + 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: renderTooltip, + }, + }, + scales: { + x: { + grid: { + display: false, + }, + border: { + color: colors.line, + }, + ticks: { + color: colors.text, + autoSkip: false, + maxRotation: 0, + }, + }, + y: { + type: 'linear', + min: 0, + beginAtZero: true, + stacked, + grid: { + color: colors.line, + }, + border: { + color: colors.line, + }, + ticks: { + color: colors.text, + callback: renderYLabel, + }, + }, + }, + }; + }, [animationDuration, renderTooltip, stacked, colors, locale]); + + const createChart = () => { + Chart.defaults.font.family = 'Inter'; + + const options = getOptions(); + + chart.current = new Chart(canvas.current, { + type: 'bar', + data: { datasets }, + options, + }); + + onCreate(chart.current); + }; + + const updateChart = () => { + setTooltip(null); + + chart.current.data.datasets[0].data = datasets[0].data; + chart.current.data.datasets[0].label = datasets[0].label; + + chart.current.options = getOptions(); + + onUpdate(chart.current); + + chart.current.update(); + }; + + useEffect(() => { + if (datasets) { + if (!chart.current) { + createChart(); + } else { + updateChart(); + } + } + }, [datasets, theme, animationDuration, locale]); + + return ( + <> +
+ {loading && } + +
+ + {tooltip && } + + ); +} + +export default FunnelChart; diff --git a/components/pages/reports/funnel/FunnelChart.module.css b/components/pages/reports/funnel/FunnelChart.module.css new file mode 100644 index 00000000..f071a29e --- /dev/null +++ b/components/pages/reports/funnel/FunnelChart.module.css @@ -0,0 +1,23 @@ +.chart { + position: relative; + height: 400px; + overflow: hidden; +} + +.tooltip { + display: flex; + flex-direction: column; + gap: 10px; +} + +.tooltip .value { + display: flex; + flex-direction: column; + text-transform: lowercase; +} + +@media only screen and (max-width: 992px) { + .chart { + /*height: 200px;*/ + } +} diff --git a/components/pages/reports/FunnelForm.js b/components/pages/reports/funnel/FunnelForm.js similarity index 96% rename from components/pages/reports/FunnelForm.js rename to components/pages/reports/funnel/FunnelForm.js index 5f9758f0..30edcc56 100644 --- a/components/pages/reports/FunnelForm.js +++ b/components/pages/reports/funnel/FunnelForm.js @@ -37,7 +37,11 @@ export function FunnelForm({ onSearch }) { const handleAddUrl = () => setUrls([...urls, '']); - const handleRemoveUrl = i => setUrls(urls.splice(i, 1)); + const handleRemoveUrl = i => { + const nextUrls = [...urls]; + nextUrls.splice(i, 1); + setUrls(nextUrls); + }; const handleWindowChange = value => setWindow(value.target.value); @@ -103,7 +107,7 @@ export function FunnelForm({ onSearch }) { - Search + Query diff --git a/components/pages/reports/FunnelForm.module.css b/components/pages/reports/funnel/FunnelForm.module.css similarity index 83% rename from components/pages/reports/FunnelForm.module.css rename to components/pages/reports/funnel/FunnelForm.module.css index 2706a99a..2a07d552 100644 --- a/components/pages/reports/FunnelForm.module.css +++ b/components/pages/reports/funnel/FunnelForm.module.css @@ -18,11 +18,6 @@ max-height: 0px; } -.urlFormRow { - flex-direction: row; - gap: 0em; -} - .urlFormRow label { min-width: 80px; } diff --git a/components/pages/reports/FunnelPage.js b/components/pages/reports/funnel/FunnelPage.js similarity index 67% rename from components/pages/reports/FunnelPage.js rename to components/pages/reports/funnel/FunnelPage.js index b69319b1..3bfbb9e3 100644 --- a/components/pages/reports/FunnelPage.js +++ b/components/pages/reports/funnel/FunnelPage.js @@ -1,17 +1,19 @@ import { useMutation } from '@tanstack/react-query'; import Page from 'components/layout/Page'; import PageHeader from 'components/layout/PageHeader'; +import ReportsLayout from 'components/layout/ReportsLayout'; import useApi from 'hooks/useApi'; import { useState } from 'react'; import FunnelChart from './FunnelChart'; import FunnelTable from './FunnelTable'; import FunnelForm from './FunnelForm'; + import styles from './FunnelPage.module.css'; export default function FunnelPage() { const { post } = useApi(); const { mutate, error, isLoading } = useMutation(data => post('/reports/funnel', data)); - const [data, setData] = useState(); + const [data, setData] = useState([{}]); const [formData, setFormData] = useState(); function handleOnSearch(data) { @@ -25,14 +27,12 @@ export default function FunnelPage() { } return ( - - - - -
-

Filters

- -
-
+ } header={'test'}> + + + + + + ); } diff --git a/components/pages/reports/FunnelPage.module.css b/components/pages/reports/funnel/FunnelPage.module.css similarity index 100% rename from components/pages/reports/FunnelPage.module.css rename to components/pages/reports/funnel/FunnelPage.module.css diff --git a/components/pages/reports/FunnelTable.js b/components/pages/reports/funnel/FunnelTable.js similarity index 73% rename from components/pages/reports/FunnelTable.js rename to components/pages/reports/funnel/FunnelTable.js index 036e20c3..2bbabc81 100644 --- a/components/pages/reports/FunnelTable.js +++ b/components/pages/reports/funnel/FunnelTable.js @@ -1,13 +1,12 @@ import DataTable from 'components/metrics/DataTable'; import useMessages from 'hooks/useMessages'; -import { useState } from 'react'; export function DevicesTable({ ...props }) { const { formatMessage, labels } = useMessages(); const { data } = props; const tableData = - data?.map(a => ({ x: a.url, y: a.count, z: (a.count / data[0].count) * 100 })) || []; + data?.map(a => ({ x: a.x, y: a.y, z: Math.floor(a.y / data[0].y) * 100 })) || []; return ; } diff --git a/package.json b/package.json index 7f365db1..0db07f49 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,6 @@ "dotenv": "^10.0.0", "formik": "^2.2.9", "fs-extra": "^10.0.1", - "funnel-graph-js": "^1.3.7", "immer": "^9.0.12", "ipaddr.js": "^2.0.1", "is-ci": "^3.0.1", diff --git a/pages/_app.js b/pages/_app.js index bc55355b..22458215 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -9,7 +9,6 @@ import useConfig from 'hooks/useConfig'; import '@fontsource/inter/400.css'; import '@fontsource/inter/700.css'; import 'react-basics/dist/styles.css'; -import 'styles/funnelChart.css'; import 'styles/variables.css'; import 'styles/locale.css'; import 'styles/index.css'; diff --git a/pages/settings/reports/funnel.js b/pages/reports/funnel.js similarity index 54% rename from pages/settings/reports/funnel.js rename to pages/reports/funnel.js index d8d7a5b8..3ba11306 100644 --- a/pages/settings/reports/funnel.js +++ b/pages/reports/funnel.js @@ -1,16 +1,13 @@ import AppLayout from 'components/layout/AppLayout'; -import SettingsLayout from 'components/layout/SettingsLayout'; -import FunnelPage from 'components/pages/reports/FunnelPage'; +import FunnelPage from 'components/pages/reports/funnel/FunnelPage'; import useMessages from 'hooks/useMessages'; -export default function DetailsPage() { +export default function Funnel() { const { formatMessage, labels } = useMessages(); return ( - - - + ); } diff --git a/pages/settings/reports/index.js b/pages/reports/index.js similarity index 90% rename from pages/settings/reports/index.js rename to pages/reports/index.js index ce0a3726..cdd6ae38 100644 --- a/pages/settings/reports/index.js +++ b/pages/reports/index.js @@ -1,6 +1,6 @@ import { useRouter } from 'next/router'; import AppLayout from 'components/layout/AppLayout'; -import FunnelPage from 'components/pages/reports/FunnelPage'; +import FunnelPage from 'components/pages/reports/funnel/FunnelPage'; import useMessages from 'hooks/useMessages'; import SettingsLayout from 'components/layout/SettingsLayout'; diff --git a/queries/analytics/pageview/getPageviewFunnel.ts b/queries/analytics/pageview/getPageviewFunnel.ts index 591310cf..ef62e526 100644 --- a/queries/analytics/pageview/getPageviewFunnel.ts +++ b/queries/analytics/pageview/getPageviewFunnel.ts @@ -29,9 +29,8 @@ async function relationalQuery( }, ): Promise< { - level: number; - url: string; - count: any; + x: string; + y: number; }[] > { const { windowMinutes, startDate, endDate, urls } = criteria; @@ -58,7 +57,7 @@ async function relationalQuery( `, params, ).then((a: { [key: string]: number }) => { - return urls.map((b, i) => ({ level: i + 1, url: b, count: a[`level${i + 1}`] || 0 })); + return urls.map((b, i) => ({ x: b, y: a[`level${i + 1}`] || 0 })); }); } @@ -72,9 +71,8 @@ async function clickhouseQuery( }, ): Promise< { - level: number; - url: string; - count: number; + x: string; + y: number; }[] > { const { windowMinutes, startDate, endDate, urls } = criteria; @@ -110,9 +108,8 @@ async function clickhouseQuery( params, ).then(results => { return urls.map((a, i) => ({ - level: i + 1, - url: a, - count: results[i + 1]?.count || 0, + x: a, + y: results[i + 1]?.count || 0, })); }); } diff --git a/styles/funnelChart.css b/styles/funnelChart.css deleted file mode 100644 index c72d42e7..00000000 --- a/styles/funnelChart.css +++ /dev/null @@ -1,148 +0,0 @@ -.svg-funnel-js { - display: inline-block; - position: relative; -} -.svg-funnel-js svg { - display: block; -} -.svg-funnel-js .svg-funnel-js__labels { - position: absolute; - display: flex; - width: 100%; - height: 100%; - top: 0; - left: 0; -} -.svg-funnel-js.svg-funnel-js--vertical .svg-funnel-js__labels { - flex-direction: column; -} - -.svg-funnel-js body { - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.svg-funnel-js { - font-family: 'Open Sans', sans-serif; -} -.svg-funnel-js .svg-funnel-js__container { - width: 100%; - height: 100%; -} -.svg-funnel-js .svg-funnel-js__labels { - width: 100%; - box-sizing: border-box; -} -.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label { - flex: 1 1 0; - position: relative; -} -.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label .label__value { - font-size: 24px; - color: #fff; - line-height: 18px; - margin-bottom: 6px; -} -.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label .label__title { - font-size: 12px; - font-weight: bold; - color: #21ffa2; -} -.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label .label__percentage { - font-size: 16px; - font-weight: bold; - color: #9896dc; -} -.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label .label__segment-percentages { - position: absolute; - top: 50%; - transform: translateY(-50%); - width: 100%; - left: 0; - padding: 8px 24px; - box-sizing: border-box; - background-color: rgba(8, 7, 48, 0.8); - margin-top: 24px; - opacity: 0; - transition: opacity 0.1s ease; - cursor: default; -} -.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label .label__segment-percentages ul { - margin: 0; - padding: 0; - list-style-type: none; -} -.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label .label__segment-percentages ul li { - font-size: 13px; - line-height: 16px; - color: #fff; - margin: 18px 0; -} -.svg-funnel-js - .svg-funnel-js__labels - .svg-funnel-js__label - .label__segment-percentages - ul - li - .percentage__list-label { - font-weight: bold; - color: #05df9d; -} -.svg-funnel-js .svg-funnel-js__labels .svg-funnel-js__label:hover .label__segment-percentages { - opacity: 1; -} -.svg-funnel-js:not(.svg-funnel-js--vertical) { - padding-top: 64px; - padding-bottom: 16px; -} -.svg-funnel-js:not(.svg-funnel-js--vertical) .svg-funnel-js__label { - padding-left: 24px; -} -.svg-funnel-js:not(.svg-funnel-js--vertical) .svg-funnel-js__label:not(:first-child) { - border-left: 1px solid #9896dc; -} -.svg-funnel-js.svg-funnel-js--vertical { - padding-left: 120px; - padding-right: 16px; -} -.svg-funnel-js.svg-funnel-js--vertical .svg-funnel-js__label { - padding-top: 24px; -} -.svg-funnel-js.svg-funnel-js--vertical .svg-funnel-js__label:not(:first-child) { - border-top: 1px solid #9896dc; -} -.svg-funnel-js.svg-funnel-js--vertical .svg-funnel-js__label .label__segment-percentages { - margin-top: 0; - margin-left: 106px; - width: calc(100% - 106px); -} -.svg-funnel-js.svg-funnel-js--vertical - .svg-funnel-js__label - .label__segment-percentages - .segment-percentage__list { - display: flex; - justify-content: space-around; -} -.svg-funnel-js .svg-funnel-js__subLabels { - display: flex; - justify-content: center; - margin-top: 24px; - position: absolute; - width: 100%; - left: 0; -} -.svg-funnel-js .svg-funnel-js__subLabels .svg-funnel-js__subLabel { - display: flex; - font-size: 12px; - color: #fff; - line-height: 16px; -} -.svg-funnel-js .svg-funnel-js__subLabels .svg-funnel-js__subLabel:not(:first-child) { - margin-left: 16px; -} -.svg-funnel-js .svg-funnel-js__subLabels .svg-funnel-js__subLabel .svg-funnel-js__subLabel--color { - width: 12px; - height: 12px; - border-radius: 50%; - margin: 2px 8px 2px 0; -} From 4df7d6a2a12d0bcc43d0d1e6fc8ddb2fee647b22 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 18 May 2023 13:13:18 -0700 Subject: [PATCH 43/88] Add userReport api --- lib/auth.ts | 31 +++++++++++++++++-- pages/api/reports/[id].ts | 60 +++++++++++++++++++++++++++++++++++++ pages/api/reports/index.ts | 43 ++++++++++++++++++++++++++ queries/admin/user.ts | 14 +++++++++ queries/admin/userReport.ts | 37 +++++++++++++++++++++++ queries/admin/website.ts | 5 ++++ queries/index.js | 1 + 7 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 pages/api/reports/[id].ts create mode 100644 pages/api/reports/index.ts create mode 100644 queries/admin/userReport.ts diff --git a/lib/auth.ts b/lib/auth.ts index 2195ad8f..4d11a289 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -15,6 +15,7 @@ import { getTeamWebsite, getTeamWebsiteByTeamMemberId } from 'queries/admin/team import { validate } from 'uuid'; import { Auth } from './types'; import { loadWebsite } from './query'; +import { UserReport } from '@prisma/client'; const log = debug('umami:auth'); @@ -135,7 +136,34 @@ export async function canDeleteWebsite({ user }: Auth, websiteId: string) { return false; } -// To-do: Implement when payments are setup. +export async function canViewUserReport(auth: Auth, userReport: UserReport) { + if (auth.user.isAdmin) { + return true; + } + + if ((auth.user.id = userReport.userId)) { + return true; + } + + if (await canViewWebsite(auth, userReport.websiteId)) { + return true; + } + + return false; +} + +export async function canUpdateUserReport(auth: Auth, userReport: UserReport) { + if (auth.user.isAdmin) { + return true; + } + + if ((auth.user.id = userReport.userId)) { + return true; + } + + return false; +} + export async function canCreateTeam({ user }: Auth) { if (user.isAdmin) { return true; @@ -144,7 +172,6 @@ export async function canCreateTeam({ user }: Auth) { return !!user; } -// To-do: Implement when payments are setup. export async function canViewTeam({ user }: Auth, teamId: string) { if (user.isAdmin) { return true; diff --git a/pages/api/reports/[id].ts b/pages/api/reports/[id].ts new file mode 100644 index 00000000..42002d18 --- /dev/null +++ b/pages/api/reports/[id].ts @@ -0,0 +1,60 @@ +import { canUpdateUserReport, canViewUserReport } from 'lib/auth'; +import { useAuth, useCors } from 'lib/middleware'; +import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getUserReportById, updateUserReport } from 'queries'; + +export interface UserReportRequestQuery { + id: string; +} + +export interface UserReportRequestBody { + websiteId: string; + reportName: string; + templateName: string; + parameters: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useCors(req, res); + await useAuth(req, res); + + if (req.method === 'GET') { + const { id: userReportId } = req.query; + + const data = await getUserReportById(userReportId); + + if (!(await canViewUserReport(req.auth, data))) { + return unauthorized(res); + } + + return ok(res, data); + } + + if (req.method === 'POST') { + const { id: userReportId } = req.query; + + const data = await getUserReportById(userReportId); + + if (!(await canUpdateUserReport(req.auth, data))) { + return unauthorized(res); + } + + const updated = await updateUserReport( + { + ...req.body, + }, + { + id: userReportId, + }, + ); + + return ok(res, updated); + } + + return methodNotAllowed(res); +}; diff --git a/pages/api/reports/index.ts b/pages/api/reports/index.ts new file mode 100644 index 00000000..e5855355 --- /dev/null +++ b/pages/api/reports/index.ts @@ -0,0 +1,43 @@ +import { uuid } from 'lib/crypto'; +import { useAuth, useCors } from 'lib/middleware'; +import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok } from 'next-basics'; +import { createUserReport, getUserReports } from 'queries'; + +export interface UserReportRequestBody { + websiteId: string; + reportName: string; + templateName: string; + parameters: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useCors(req, res); + await useAuth(req, res); + + const { + user: { id: userId }, + } = req.auth; + + if (req.method === 'GET') { + const data = await getUserReports(userId); + + return ok(res, data); + } + + if (req.method === 'POST') { + const data = await createUserReport({ + id: uuid(), + userId, + ...req.body, + }); + + return ok(res, data); + } + + return methodNotAllowed(res); +}; diff --git a/queries/admin/user.ts b/queries/admin/user.ts index a81a76ef..b7f452c7 100644 --- a/queries/admin/user.ts +++ b/queries/admin/user.ts @@ -210,6 +210,20 @@ export async function deleteUser( }, }, }), + client.userReport.deleteMany({ + where: { + OR: [ + { + websiteId: { + in: websiteIds, + }, + }, + { + userId, + }, + ], + }, + }), cloudMode ? client.website.updateMany({ data: { diff --git a/queries/admin/userReport.ts b/queries/admin/userReport.ts new file mode 100644 index 00000000..d31b512e --- /dev/null +++ b/queries/admin/userReport.ts @@ -0,0 +1,37 @@ +import { Prisma, UserReport } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function createUserReport( + data: Prisma.UserReportUncheckedCreateInput, +): Promise { + return prisma.client.userReport.create({ data }); +} + +export async function getUserReportById(userReportId: string): Promise { + return prisma.client.userReport.findUnique({ + where: { + id: userReportId, + }, + }); +} + +export async function getUserReports(userId: string): Promise { + return prisma.client.userReport.findMany({ + where: { + userId, + }, + }); +} + +export async function updateUserReport( + data: Prisma.UserReportUpdateInput, + where: Prisma.UserReportWhereUniqueInput, +): Promise { + return prisma.client.userReport.update({ data, where }); +} + +export async function deleteUserReport( + where: Prisma.UserReportWhereUniqueInput, +): Promise { + return prisma.client.userReport.delete({ where }); +} diff --git a/queries/admin/website.ts b/queries/admin/website.ts index f5ce5739..e6d53fce 100644 --- a/queries/admin/website.ts +++ b/queries/admin/website.ts @@ -92,6 +92,11 @@ export async function deleteWebsite( websiteId, }, }), + client.userReport.deleteMany({ + where: { + websiteId, + }, + }), cloudMode ? prisma.client.website.update({ data: { diff --git a/queries/index.js b/queries/index.js index 5c295fff..e565df25 100644 --- a/queries/index.js +++ b/queries/index.js @@ -1,6 +1,7 @@ export * from './admin/team'; export * from './admin/teamUser'; export * from './admin/user'; +export * from './admin/userReport'; export * from './admin/website'; export * from './analytics/event/getEventMetrics'; export * from './analytics/event/getEventUsage'; From 0fb93ff00b545fabdbd80aa79eec48c902cc5d97 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 20 May 2023 09:02:08 -0700 Subject: [PATCH 44/88] Report updates. --- assets/add-user.svg | 2 +- assets/clock.svg | 2 +- assets/dashboard.svg | 2 +- assets/funnel.svg | 2 +- assets/lightbulb.svg | 81 +--------- assets/lock.svg | 2 +- assets/logo.svg | 2 +- assets/nodes.svg | 2 +- assets/profile.svg | 2 +- assets/user.svg | 2 +- assets/users.svg | 2 +- assets/website.svg | 2 +- components/input/LanguageButton.js | 3 +- components/layout/NavBar.js | 3 +- components/layout/PageHeader.js | 5 +- components/messages.js | 5 +- components/pages/reports/EventDataReport.js | 33 ---- components/pages/reports/Report.js | 11 +- components/pages/reports/ReportBody.js | 7 + components/pages/reports/ReportForm.js | 28 ---- .../pages/reports/ReportForm.module.css | 19 --- components/pages/reports/ReportHeader.js | 53 ++++-- .../reports/{ReportsList.js => ReportList.js} | 10 +- ...sList.module.css => ReportList.module.css} | 0 components/pages/reports/ReportMenu.js | 7 + .../reports/event-data/EventDataReport.js | 30 ++++ components/pages/reports/funnel/FunnelForm.js | 118 -------------- .../reports/funnel/FunnelForm.module.css | 13 -- components/pages/reports/funnel/FunnelPage.js | 36 ----- .../pages/reports/funnel/FunnelParameters.js | 67 ++++++++ .../funnel/FunnelParameters.module.css | 7 + .../pages/reports/funnel/FunnelReport.js | 28 ++++ ...age.module.css => FunnelReport.module.css} | 0 components/pages/reports/reports.module.css | 14 +- hooks/index.js | 1 + hooks/useDateRange.js | 4 +- hooks/useReport.js | 22 +++ lang/am-ET.json | 151 +++++++++++------- lang/ar-SA.json | 9 ++ lang/be-BY.json | 9 ++ lang/bn-BD.json | 9 ++ lang/ca-ES.json | 9 ++ lang/cs-CZ.json | 9 ++ lang/da-DK.json | 9 ++ lang/de-CH.json | 9 ++ lang/de-DE.json | 9 ++ lang/el-GR.json | 9 ++ lang/en-GB.json | 9 ++ lang/en-US.json | 11 +- lang/es-MX.json | 9 ++ lang/fa-IR.json | 9 ++ lang/fi-FI.json | 9 ++ lang/fo-FO.json | 9 ++ lang/fr-FR.json | 9 ++ lang/ga-ES.json | 9 ++ lang/he-IL.json | 9 ++ lang/hi-IN.json | 9 ++ lang/hr-HR.json | 151 +++++++++++------- lang/hu-HU.json | 9 ++ lang/id-ID.json | 9 ++ lang/it-IT.json | 9 ++ lang/ja-JP.json | 9 ++ lang/km-KH.json | 9 ++ lang/ko-KR.json | 9 ++ lang/lt-LT.json | 9 ++ lang/mn-MN.json | 9 ++ lang/ms-MY.json | 9 ++ lang/nb-NO.json | 9 ++ lang/nl-NL.json | 9 ++ lang/pl-PL.json | 9 ++ lang/pt-BR.json | 9 ++ lang/pt-PT.json | 9 ++ lang/ro-RO.json | 9 ++ lang/ru-RU.json | 9 ++ lang/si-LK.json | 151 +++++++++++------- lang/sk-SK.json | 9 ++ lang/sl-SI.json | 9 ++ lang/sv-SE.json | 9 ++ lang/ta-IN.json | 9 ++ lang/th-TH.json | 9 ++ lang/tr-TR.json | 9 ++ lang/uk-UA.json | 9 ++ lang/ur-PK.json | 9 ++ lang/vi-VN.json | 9 ++ lang/zh-CN.json | 9 ++ lang/zh-TW.json | 9 ++ package.json | 2 +- pages/api/websites/[id]/data.ts | 13 -- pages/api/websites/[id]/eventData.ts | 2 +- pages/reports/event-data.js | 13 ++ pages/reports/event-data/index.js | 5 - pages/reports/funnel.js | 6 +- pages/reports/index.js | 4 +- public/intl/messages/ar-SA.json | 2 +- public/intl/messages/be-BY.json | 2 +- public/intl/messages/bn-BD.json | 2 +- public/intl/messages/ca-ES.json | 2 +- public/intl/messages/cs-CZ.json | 2 +- public/intl/messages/da-DK.json | 2 +- public/intl/messages/de-CH.json | 2 +- public/intl/messages/de-DE.json | 2 +- public/intl/messages/el-GR.json | 2 +- public/intl/messages/en-GB.json | 2 +- public/intl/messages/en-US.json | 2 +- public/intl/messages/es-MX.json | 2 +- public/intl/messages/fa-IR.json | 2 +- public/intl/messages/fi-FI.json | 2 +- public/intl/messages/fo-FO.json | 2 +- public/intl/messages/fr-FR.json | 2 +- public/intl/messages/ga-ES.json | 2 +- public/intl/messages/he-IL.json | 2 +- public/intl/messages/hi-IN.json | 2 +- public/intl/messages/hu-HU.json | 2 +- public/intl/messages/id-ID.json | 2 +- public/intl/messages/it-IT.json | 2 +- public/intl/messages/ja-JP.json | 2 +- public/intl/messages/km-KH.json | 2 +- public/intl/messages/ko-KR.json | 2 +- public/intl/messages/lt-LT.json | 2 +- public/intl/messages/mn-MN.json | 2 +- public/intl/messages/ms-MY.json | 2 +- public/intl/messages/nb-NO.json | 2 +- public/intl/messages/nl-NL.json | 2 +- public/intl/messages/pl-PL.json | 2 +- public/intl/messages/pt-BR.json | 2 +- public/intl/messages/pt-PT.json | 2 +- public/intl/messages/ro-RO.json | 2 +- public/intl/messages/ru-RU.json | 2 +- public/intl/messages/sk-SK.json | 2 +- public/intl/messages/sl-SI.json | 2 +- public/intl/messages/sv-SE.json | 2 +- public/intl/messages/ta-IN.json | 2 +- public/intl/messages/th-TH.json | 2 +- public/intl/messages/tr-TR.json | 2 +- public/intl/messages/uk-UA.json | 2 +- public/intl/messages/ur-PK.json | 2 +- public/intl/messages/vi-VN.json | 2 +- public/intl/messages/zh-CN.json | 2 +- public/intl/messages/zh-TW.json | 2 +- rollup.tracker.config.js | 2 +- store/app.js | 2 +- store/dashboard.js | 2 +- store/queries.js | 2 +- store/reports.js | 61 +++++++ store/version.js | 2 +- store/websites.js | 2 +- yarn.lock | 17 +- 147 files changed, 1095 insertions(+), 628 deletions(-) delete mode 100644 components/pages/reports/EventDataReport.js create mode 100644 components/pages/reports/ReportBody.js delete mode 100644 components/pages/reports/ReportForm.js delete mode 100644 components/pages/reports/ReportForm.module.css rename components/pages/reports/{ReportsList.js => ReportList.js} (85%) rename components/pages/reports/{ReportsList.module.css => ReportList.module.css} (100%) create mode 100644 components/pages/reports/ReportMenu.js create mode 100644 components/pages/reports/event-data/EventDataReport.js delete mode 100644 components/pages/reports/funnel/FunnelForm.js delete mode 100644 components/pages/reports/funnel/FunnelForm.module.css delete mode 100644 components/pages/reports/funnel/FunnelPage.js create mode 100644 components/pages/reports/funnel/FunnelParameters.js create mode 100644 components/pages/reports/funnel/FunnelParameters.module.css create mode 100644 components/pages/reports/funnel/FunnelReport.js rename components/pages/reports/funnel/{FunnelPage.module.css => FunnelReport.module.css} (100%) create mode 100644 hooks/useReport.js delete mode 100644 pages/api/websites/[id]/data.ts create mode 100644 pages/reports/event-data.js delete mode 100644 pages/reports/event-data/index.js create mode 100644 store/reports.js diff --git a/assets/add-user.svg b/assets/add-user.svg index 9d0544c6..c6b4f484 100644 --- a/assets/add-user.svg +++ b/assets/add-user.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/clock.svg b/assets/clock.svg index 9c2a9a41..ab4c1dec 100644 --- a/assets/clock.svg +++ b/assets/clock.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/dashboard.svg b/assets/dashboard.svg index 11859d28..2090e5dc 100644 --- a/assets/dashboard.svg +++ b/assets/dashboard.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/funnel.svg b/assets/funnel.svg index 46f4623f..63fb7158 100644 --- a/assets/funnel.svg +++ b/assets/funnel.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/lightbulb.svg b/assets/lightbulb.svg index 73b699a3..c7895a7d 100644 --- a/assets/lightbulb.svg +++ b/assets/lightbulb.svg @@ -1,80 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/lock.svg b/assets/lock.svg index c13fb7c7..27fcc5e1 100644 --- a/assets/lock.svg +++ b/assets/lock.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/logo.svg b/assets/logo.svg index d2c71326..b1395313 100644 --- a/assets/logo.svg +++ b/assets/logo.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/nodes.svg b/assets/nodes.svg index 0bbd37c4..b3e22a75 100644 --- a/assets/nodes.svg +++ b/assets/nodes.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/profile.svg b/assets/profile.svg index 133b1bc1..6a1af5a0 100644 --- a/assets/profile.svg +++ b/assets/profile.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/user.svg b/assets/user.svg index a75cbb8d..245a67f6 100644 --- a/assets/user.svg +++ b/assets/user.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/users.svg b/assets/users.svg index f775ea91..7036a22c 100644 --- a/assets/users.svg +++ b/assets/users.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/website.svg b/assets/website.svg index cfa9e565..6096a650 100644 --- a/assets/website.svg +++ b/assets/website.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/components/input/LanguageButton.js b/components/input/LanguageButton.js index 1297d6c2..e2a51223 100644 --- a/components/input/LanguageButton.js +++ b/components/input/LanguageButton.js @@ -10,7 +10,8 @@ export function LanguageButton() { const items = Object.keys(languages).map(key => ({ ...languages[key], value: key })); function handleSelect(value) { - saveLocale(value); + //saveLocale(value); + console.log('WTFFFF'); } return ( diff --git a/components/layout/NavBar.js b/components/layout/NavBar.js index 95d27306..a5ac35ef 100644 --- a/components/layout/NavBar.js +++ b/components/layout/NavBar.js @@ -18,9 +18,8 @@ export function NavBar() { const links = [ { label: formatMessage(labels.dashboard), url: '/dashboard' }, - { label: formatMessage(labels.reports), url: '/reports' }, { label: formatMessage(labels.realtime), url: '/realtime' }, - { label: formatMessage(labels.reports), url: '/reports/funnel' }, + { label: formatMessage(labels.reports), url: '/reports' }, !cloudMode && { label: formatMessage(labels.settings), url: '/settings' }, ].filter(n => n); diff --git a/components/layout/PageHeader.js b/components/layout/PageHeader.js index bf243c21..c01192d2 100644 --- a/components/layout/PageHeader.js +++ b/components/layout/PageHeader.js @@ -1,9 +1,10 @@ +import classNames from 'classnames'; import React from 'react'; import styles from './PageHeader.module.css'; -export function PageHeader({ title, children }) { +export function PageHeader({ title, children, className }) { return ( -
+
{title}
{children}
diff --git a/components/messages.js b/components/messages.js index a89a800b..fb0dee3c 100644 --- a/components/messages.js +++ b/components/messages.js @@ -80,7 +80,8 @@ export const labels = defineMessages({ countries: { id: 'label.countries', defaultMessage: 'Countries' }, languages: { id: 'label.languages', defaultMessage: 'Languages' }, events: { id: 'label.events', defaultMessage: 'Events' }, - query: { id: 'label.query-parameters', defaultMessage: 'Query parameters' }, + query: { id: 'label.query', defaultMessage: 'Query' }, + queryParameters: { id: 'label.query-parameters', defaultMessage: 'Query parameters' }, back: { id: 'label.back', defaultMessage: 'Back' }, visitors: { id: 'label.visitors', defaultMessage: 'Visitors' }, filterCombined: { id: 'label.filter-combined', defaultMessage: 'Combined' }, @@ -121,6 +122,8 @@ export const labels = defineMessages({ regions: { id: 'label.regions', defaultMessage: 'Regions' }, reports: { id: 'label.reports', defaultMessage: 'Reports' }, eventData: { id: 'label.event-data', defaultMessage: 'Event data' }, + funnel: { id: 'label.funnel', defaultMessage: 'Funnel' }, + urls: { id: 'label.urls', defaultMessage: 'URLs' }, }); export const messages = defineMessages({ diff --git a/components/pages/reports/EventDataReport.js b/components/pages/reports/EventDataReport.js deleted file mode 100644 index 8c01e25f..00000000 --- a/components/pages/reports/EventDataReport.js +++ /dev/null @@ -1,33 +0,0 @@ -import { useState } from 'react'; -import { Form, FormRow, FormInput, TextField } from 'react-basics'; -import AppLayout from 'components/layout/AppLayout'; -import Report from './Report'; -import ReportHeader from './ReportHeader'; -import useMessages from 'hooks/useMessages'; -import Nodes from 'assets/nodes.svg'; -import styles from './reports.module.css'; - -export default function EventDataReport({ websiteId, data }) { - const [values, setValues] = useState({ query: '' }); - const { formatMessage, labels } = useMessages(); - - return ( - - - } /> -
-
-
- - - - - -
-
-
-
-
-
- ); -} diff --git a/components/pages/reports/Report.js b/components/pages/reports/Report.js index 1754e89f..4e3fcd04 100644 --- a/components/pages/reports/Report.js +++ b/components/pages/reports/Report.js @@ -1,5 +1,12 @@ import Page from 'components/layout/Page'; +import styles from './reports.module.css'; -export default function Report({ children, ...props }) { - return {children}; +export function Report({ children, ...props }) { + return ( + + {children} + + ); } + +export default Report; diff --git a/components/pages/reports/ReportBody.js b/components/pages/reports/ReportBody.js new file mode 100644 index 00000000..2310c8af --- /dev/null +++ b/components/pages/reports/ReportBody.js @@ -0,0 +1,7 @@ +import styles from './reports.module.css'; + +export function ReportBody({ children }) { + return
{children}
; +} + +export default ReportBody; diff --git a/components/pages/reports/ReportForm.js b/components/pages/reports/ReportForm.js deleted file mode 100644 index cdf47eab..00000000 --- a/components/pages/reports/ReportForm.js +++ /dev/null @@ -1,28 +0,0 @@ -import useMessages from 'hooks/useMessages'; -import { Form, FormButtons, FormInput, FormRow, SubmitButton, TextField } from 'react-basics'; - -export function FunnelForm() { - const { formatMessage, labels } = useMessages(); - - const handleSubmit = () => {}; - - return ( - <> -
- - - - - - - - - Save - - -
- - ); -} - -export default FunnelForm; diff --git a/components/pages/reports/ReportForm.module.css b/components/pages/reports/ReportForm.module.css deleted file mode 100644 index 4b12238f..00000000 --- a/components/pages/reports/ReportForm.module.css +++ /dev/null @@ -1,19 +0,0 @@ -.filter { - min-width: 200px; -} - -.hiddenInput { - visibility: hidden; - min-height: 0px; - max-height: 0px; -} - -.hidden { - visibility: hidden; - min-height: 0px; - max-height: 0px; -} - -.urlFormRow label { - min-width: 80px; -} diff --git a/components/pages/reports/ReportHeader.js b/components/pages/reports/ReportHeader.js index 44502149..90d58ca8 100644 --- a/components/pages/reports/ReportHeader.js +++ b/components/pages/reports/ReportHeader.js @@ -1,32 +1,50 @@ -import { useState } from 'react'; -import { Flexbox, Icon, Text } from 'react-basics'; +import { Flexbox, Icon, LoadingButton, Text, useToast } from 'react-basics'; import WebsiteSelect from 'components/input/WebsiteSelect'; import PageHeader from 'components/layout/PageHeader'; import DateFilter from 'components/input/DateFilter'; import { parseDateRange } from 'lib/date'; +import { updateReport } from 'store/reports'; +import { useMessages, useApi } from 'hooks'; +import styles from './reports.module.css'; -export default function ReportHeader({ title, icon }) { - const [websiteId, setWebsiteId] = useState(); - const [dateRange, setDateRange] = useState({}); - const { value, startDate, endDate } = dateRange; +export function ReportHeader({ report, icon }) { + const { formatMessage, labels, messages } = useMessages(); + const { toast, showToast } = useToast(); + const { post, useMutation } = useApi(); + const { mutate, isLoading } = useMutation(data => post(`/reports`, data)); - const handleSelect = id => { - setWebsiteId(id); + const { id, websiteId, name, parameters } = report || {}; + const { value, startDate, endDate } = parameters?.dateRange || {}; + + console.log('REPORT HEADER', report); + + const handleSelect = websiteId => { + updateReport(id, { websiteId }); }; - const handleDateChange = value => setDateRange(parseDateRange(value)); + const handleDateChange = value => { + updateReport(id, { parameters: { dateRange: { ...parseDateRange(value) } } }); + }; + + const handleSave = async () => { + mutate(report, { + onSuccess: async () => { + showToast({ message: formatMessage(messages.saved), variant: 'success' }); + }, + }); + }; const Title = () => { return ( <> - {icon} - {title} + {icon} + {name} ); }; return ( - }> + } className={styles.header}> + + {formatMessage(labels.save)} + + {toast} ); } + +export default ReportHeader; diff --git a/components/pages/reports/ReportsList.js b/components/pages/reports/ReportList.js similarity index 85% rename from components/pages/reports/ReportsList.js rename to components/pages/reports/ReportList.js index 44dc556d..da0a622c 100644 --- a/components/pages/reports/ReportsList.js +++ b/components/pages/reports/ReportList.js @@ -5,7 +5,7 @@ import PageHeader from 'components/layout/PageHeader'; import Funnel from 'assets/funnel.svg'; import Nodes from 'assets/nodes.svg'; import Lightbulb from 'assets/lightbulb.svg'; -import styles from './ReportsList.module.css'; +import styles from './ReportList.module.css'; const reports = [ { @@ -28,7 +28,7 @@ const reports = [ }, ]; -function Report({ title, description, url, icon }) { +function ReportItem({ title, description, url, icon }) { return (
@@ -50,17 +50,19 @@ function Report({ title, description, url, icon }) { ); } -export default function ReportsList() { +export function ReportList() { return (
{reports.map(({ title, description, url, icon }) => { return ( - + ); })}
); } + +export default ReportList; diff --git a/components/pages/reports/ReportsList.module.css b/components/pages/reports/ReportList.module.css similarity index 100% rename from components/pages/reports/ReportsList.module.css rename to components/pages/reports/ReportList.module.css diff --git a/components/pages/reports/ReportMenu.js b/components/pages/reports/ReportMenu.js new file mode 100644 index 00000000..abfea6fe --- /dev/null +++ b/components/pages/reports/ReportMenu.js @@ -0,0 +1,7 @@ +import styles from './reports.module.css'; + +export function ReportMenu({ children }) { + return
{children}
; +} + +export default ReportMenu; diff --git a/components/pages/reports/event-data/EventDataReport.js b/components/pages/reports/event-data/EventDataReport.js new file mode 100644 index 00000000..a32cd6e2 --- /dev/null +++ b/components/pages/reports/event-data/EventDataReport.js @@ -0,0 +1,30 @@ +import { useState } from 'react'; +import { Form, FormRow, FormInput, TextField } from 'react-basics'; +import Report from '../Report'; +import ReportHeader from '../ReportHeader'; +import useMessages from 'hooks/useMessages'; +import Nodes from 'assets/nodes.svg'; +import styles from '../reports.module.css'; + +export default function EventDataReport({ websiteId, data }) { + const [values, setValues] = useState({ query: '' }); + const { formatMessage, labels } = useMessages(); + + return ( + + } /> +
+
+
+ + + + + +
+
+
+
+
+ ); +} diff --git a/components/pages/reports/funnel/FunnelForm.js b/components/pages/reports/funnel/FunnelForm.js deleted file mode 100644 index 30edcc56..00000000 --- a/components/pages/reports/funnel/FunnelForm.js +++ /dev/null @@ -1,118 +0,0 @@ -import DateFilter from 'components/input/DateFilter'; -import WebsiteSelect from 'components/input/WebsiteSelect'; -import useMessages from 'hooks/useMessages'; -import { parseDateRange } from 'lib/date'; -import { useState } from 'react'; -import { - Button, - Form, - FormButtons, - FormInput, - FormRow, - SubmitButton, - TextField, -} from 'react-basics'; -import styles from './FunnelForm.module.css'; - -export function FunnelForm({ onSearch }) { - const { formatMessage, labels } = useMessages(); - const [dateRange, setDateRange] = useState(''); - const [startAt, setStartAt] = useState(); - const [endAt, setEndAt] = useState(); - const [urls, setUrls] = useState(['/', '/docs/getting-started', '/docs/intall']); - const [websiteId, setWebsiteId] = useState(''); - const [window, setWindow] = useState(60); - - const handleSubmit = async data => { - onSearch(data); - }; - - const handleDateChange = value => { - const { startDate, endDate } = parseDateRange(value); - - setDateRange(value); - setStartAt(startDate.getTime()); - setEndAt(endDate.getTime()); - }; - - const handleAddUrl = () => setUrls([...urls, '']); - - const handleRemoveUrl = i => { - const nextUrls = [...urls]; - nextUrls.splice(i, 1); - setUrls(nextUrls); - }; - - const handleWindowChange = value => setWindow(value.target.value); - - const handleUrlChange = (value, i) => { - const nextUrls = [...urls]; - - nextUrls[i] = value.target.value; - setUrls(nextUrls); - }; - - return ( - <> -
- - setWebsiteId(value)} /> - - - - - - - - - - - - - - - - - - - - {urls.map((a, i) => ( - - handleUrlChange(value, i)} /> - - - ))} - - - - Query - - -
- - ); -} - -export default FunnelForm; diff --git a/components/pages/reports/funnel/FunnelForm.module.css b/components/pages/reports/funnel/FunnelForm.module.css deleted file mode 100644 index f251b63f..00000000 --- a/components/pages/reports/funnel/FunnelForm.module.css +++ /dev/null @@ -1,13 +0,0 @@ -.filter { - min-width: 200px; -} - -.hiddenInput { - visibility: hidden; - min-height: 0px; - max-height: 0px; -} - -.urlFormRow label { - min-width: 80px; -} diff --git a/components/pages/reports/funnel/FunnelPage.js b/components/pages/reports/funnel/FunnelPage.js deleted file mode 100644 index 9f4f9bdf..00000000 --- a/components/pages/reports/funnel/FunnelPage.js +++ /dev/null @@ -1,36 +0,0 @@ -import { useMutation } from '@tanstack/react-query'; -import Page from 'components/layout/Page'; -import PageHeader from 'components/layout/PageHeader'; -import ReportsLayout from 'components/layout/ReportsLayout'; -import useApi from 'hooks/useApi'; -import { useState } from 'react'; -import FunnelChart from './FunnelChart'; -import FunnelTable from './FunnelTable'; -import FunnelForm from './FunnelForm'; - -export default function FunnelPage() { - const { post } = useApi(); - const { mutate } = useMutation(data => post('/reports/funnel', data)); - const [data, setData] = useState([{}]); - const [setFormData] = useState(); - - function handleOnSearch(data) { - setFormData(data); - - mutate(data, { - onSuccess: async data => { - setData(data); - }, - }); - } - - return ( - } header={'test'}> - - - - - - - ); -} diff --git a/components/pages/reports/funnel/FunnelParameters.js b/components/pages/reports/funnel/FunnelParameters.js new file mode 100644 index 00000000..d387686e --- /dev/null +++ b/components/pages/reports/funnel/FunnelParameters.js @@ -0,0 +1,67 @@ +import { useMessages } from 'hooks'; +import { + Icon, + Form, + FormButtons, + FormInput, + FormRow, + PopupTrigger, + Popup, + SubmitButton, + TextField, +} from 'react-basics'; +import Icons from 'components/icons'; +import { updateReport } from 'store/reports'; +import { useRef } from 'react'; +import styles from './FunnelParameters.module.css'; + +export function FunnelParameters({ report }) { + const { formatMessage, labels } = useMessages(); + const ref = useRef(null); + const { id, websiteId, parameters, isLoading } = report || {}; + + const handleSubmit = values => { + console.log({ values }); + updateReport(id, { parameters: values, isLoading: false }); + }; + + console.log('PARAMETERS', parameters); + + return ( + <> +
+ + + + + + }> + hi + + + + {formatMessage(labels.query)} + + +
+ + ); +} + +function AddURLButton() { + return ( + + + + + + HALLO + + + ); +} + +export default FunnelParameters; diff --git a/components/pages/reports/funnel/FunnelParameters.module.css b/components/pages/reports/funnel/FunnelParameters.module.css new file mode 100644 index 00000000..0e68e095 --- /dev/null +++ b/components/pages/reports/funnel/FunnelParameters.module.css @@ -0,0 +1,7 @@ +.popup { + background: var(--base50); + padding: 20px; + margin-left: 10px; + border: 1px solid var(--base400); + border-radius: var(--border-radius); +} \ No newline at end of file diff --git a/components/pages/reports/funnel/FunnelReport.js b/components/pages/reports/funnel/FunnelReport.js new file mode 100644 index 00000000..3c466050 --- /dev/null +++ b/components/pages/reports/funnel/FunnelReport.js @@ -0,0 +1,28 @@ +import FunnelChart from './FunnelChart'; +import FunnelTable from './FunnelTable'; +import FunnelParameters from './FunnelParameters'; +import Report from '../Report'; +import ReportHeader from '../ReportHeader'; +import ReportMenu from '../ReportMenu'; +import ReportBody from '../ReportBody'; +import Funnel from 'assets/funnel.svg'; +import { useReport } from 'hooks'; + +export default function FunnelReport({ reportId }) { + const report = useReport(reportId); + + console.log('REPORT', { report }); + + return ( + + } report={report} /> + + + + + + + + + ); +} diff --git a/components/pages/reports/funnel/FunnelPage.module.css b/components/pages/reports/funnel/FunnelReport.module.css similarity index 100% rename from components/pages/reports/funnel/FunnelPage.module.css rename to components/pages/reports/funnel/FunnelReport.module.css diff --git a/components/pages/reports/reports.module.css b/components/pages/reports/reports.module.css index e18a857a..b75aee64 100644 --- a/components/pages/reports/reports.module.css +++ b/components/pages/reports/reports.module.css @@ -1,14 +1,24 @@ .container { display: grid; - grid-template-rows: 1fr; + grid-template-rows: max-content 1fr; grid-template-columns: max-content 1fr; } +.header { + grid-row: 1 / 2; + grid-column: 1 / 3; +} + .menu { width: 300px; + padding-right: 20px; + border-right: 1px solid var(--base300); + grid-row: 2/3; grid-column: 1 / 2; } -.content { +.body { + padding-left: 20px; + grid-row: 2/3; grid-column: 2 / 3; } diff --git a/hooks/index.js b/hooks/index.js index 15898eb5..3f2ba4d7 100644 --- a/hooks/index.js +++ b/hooks/index.js @@ -9,6 +9,7 @@ export * from './useLanguageNames'; export * from './useLocale'; export * from './useMessages'; export * from './usePageQuery'; +export * from './useReport'; export * from './useRequireLogin'; export * from './useShareToken'; export * from './useSticky'; diff --git a/hooks/useDateRange.js b/hooks/useDateRange.js index e8a542f4..17552805 100644 --- a/hooks/useDateRange.js +++ b/hooks/useDateRange.js @@ -12,14 +12,14 @@ export function useDateRange(websiteId) { const globalConfig = appStore(state => state.dateRange); const dateRange = parseDateRange(websiteConfig || globalConfig || defaultConfig, locale); - function saveDateRange(value) { + const saveDateRange = value => { if (websiteId) { setWebsiteDateRange(websiteId, value); } else { setItem(DATE_RANGE_CONFIG, value); setDateRange(value); } - } + }; return [dateRange, saveDateRange]; } diff --git a/hooks/useReport.js b/hooks/useReport.js new file mode 100644 index 00000000..1b1c6f0e --- /dev/null +++ b/hooks/useReport.js @@ -0,0 +1,22 @@ +import useStore, { createReport } from 'store/reports'; +import { useCallback, useEffect, useState } from 'react'; +import { useForceUpdate } from 'hooks'; + +export function useReport(reportId) { + const [id, setId] = useState(reportId); + + const selector = useCallback(state => state[id], [id]); + const report = useStore(selector); + + useEffect(() => { + if (!report) { + setId(createReport().id); + } + }, []); + + console.log('USE REPORT', report); + + return report; +} + +export default useReport; diff --git a/lang/am-ET.json b/lang/am-ET.json index f58a25cd..14bcc9ae 100644 --- a/lang/am-ET.json +++ b/lang/am-ET.json @@ -1,120 +1,155 @@ { - "label.accounts": "Accounts", - "label.add-account": "Add account", - "label.add-column": "Add column", - "label.add-filter": "Add filter", + "label.access-code": "Access code", + "label.actions": "Actions", + "label.activity-log": "Activity log", "label.add-website": "Add website", - "label.administrator": "Administrator", + "label.admin": "Administrator", "label.all": "All", "label.all-time": "All time", - "label.all-websites": "All websites", + "label.analytics": "Analytics", + "label.average-visit-time": "Average visit time", "label.back": "Back", + "label.bounce-rate": "Bounce rate", + "label.browsers": "Browsers", "label.cancel": "Cancel", "label.change-password": "Change password", + "label.cities": "Cities", + "label.clear-all": "Clear all", + "label.confirm": "Confirm", "label.confirm-password": "Confirm password", - "label.copy-to-clipboard": "Copy to clipboard", + "label.continue": "Continue", + "label.countries": "Countries", + "label.create-team": "Create team", + "label.create-user": "Create user", + "label.created": "Created", "label.current-password": "Current password", "label.custom-range": "Custom range", "label.dashboard": "Dashboard", + "label.data": "Data", "label.date-range": "Date range", "label.default-date-range": "Default date range", "label.delete": "Delete", - "label.delete-account": "Delete account", + "label.delete-team": "Delete team", + "label.delete-user": "Delete user", "label.delete-website": "Delete website", + "label.desktop": "Desktop", + "label.details": "Details", + "label.devices": "Devices", "label.dismiss": "Dismiss", "label.domain": "Domain", "label.edit": "Edit", - "label.edit-account": "Edit account", - "label.edit-website": "Edit website", + "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Enable share URL", "label.event-data": "Event Data", - "label.field-name": "Field Name", - "label.invalid": "Invalid", - "label.invalid-domain": "Invalid domain", + "label.events": "Events", + "label.filter-combined": "Combined", + "label.filter-raw": "Raw", + "label.funnel": "Funnel", + "label.join": "Join", + "label.join-team": "Join team", "label.language": "Language", + "label.languages": "Languages", + "label.laptop": "Laptop", "label.last-days": "Last {x} days", "label.last-hours": "Last {x} hours", - "label.logged-in-as": "Logged in as {username}", + "label.leave": "Leave", + "label.leave-team": "Leave team", "label.login": "Login", "label.logout": "Logout", + "label.members": "Members", + "label.mobile": "Mobile", "label.more": "More", "label.name": "Name", "label.new-password": "New password", "label.none": "None", + "label.operating-systems": "Operating systems", "label.owner": "Owner", + "label.page-views": "Page views", + "label.pages": "Pages", "label.password": "Password", - "label.passwords-dont-match": "Passwords don't match", + "label.powered-by": "Powered by {name}", "label.profile": "Profile", + "label.queries": "Queries", + "label.query": "Query", + "label.query-parameters": "Query parameters", "label.realtime": "Realtime", - "label.realtime-logs": "Realtime logs", + "label.referrers": "Referrers", "label.refresh": "Refresh", + "label.regenerate": "Regenerate", + "label.regions": "Regions", + "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Required", "label.reset": "Reset", "label.reset-website": "Reset statistics", + "label.role": "Role", "label.save": "Save", - "label.search": "Search", + "label.screens": "Screens", + "label.select-date": "Select date", + "label.select-website": "Select website", + "label.sessions": "Sessions", "label.settings": "Settings", "label.share-url": "Share URL", "label.single-day": "Single day", + "label.tablet": "Tablet", + "label.team": "Team", + "label.team-guest": "Team guest", + "label.team-id": "Team ID", + "label.team-member": "Team member", + "label.team-owner": "Team owner", + "label.teams": "Teams", "label.theme": "Theme", "label.this-month": "This month", "label.this-week": "This week", "label.this-year": "This year", "label.timezone": "Timezone", + "label.title": "Title", "label.today": "Today", + "label.toggle-charts": "Toggle charts", "label.tracking-code": "Tracking code", - "label.type": "Type", + "label.unique-visitors": "Unique visitors", "label.unknown": "Unknown", + "label.urls": "URLs", + "label.user": "User", "label.username": "Username", - "label.value": "Value", + "label.users": "Users", + "label.view": "View", "label.view-details": "View details", + "label.views": "Views", + "label.visitors": "Visitors", + "label.website": "Website", + "label.website-id": "Website ID", "label.websites": "Websites", "label.yesterday": "Yesterday", "message.active-users": "{x} current {x, plural, one {visitor} other {visitors}}", "message.confirm-delete": "Are you sure you want to delete {target}?", + "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Are you sure you want to reset {target}'s statistics?", - "message.copied": "Copied!", - "message.delete-warning": "All associated data will be deleted as well.", - "message.edit-dashboard": "Edit dashboard", - "message.failure": "Something went wrong.", - "message.get-share-url": "Get share URL", - "message.get-tracking-code": "Get tracking code", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", + "message.delete-website": "To delete this website, type {confirmation} in the box below to confirm.", + "message.delete-website-warning": "All website data will be deleted.", + "message.error": "Something went wrong.", + "message.event-log": "{event} on {url}", "message.go-to-settings": "Go to settings", "message.incorrect-username-password": "Incorrect username/password.", - "message.log.visitor": "Visitor from {country} using {browser} on {os} {device}", - "message.new-version-available": "A new version of umami {version} is available!", + "message.invalid-domain": "Invalid domain. Do not include http/https.", + "message.min-password-length": "Minimum length of {n} characters", "message.no-data-available": "No data available.", - "message.no-websites-configured": "You don't have any websites configured.", + "message.no-match-password": "Passwords do not match.", + "message.no-teams": "You have not created any teams.", + "message.no-users": "There are no users.", "message.page-not-found": "Page not found.", - "message.powered-by": "Powered by {name}", - "message.reset-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.", - "message.save-success": "Saved successfully.", + "message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.", + "message.reset-website-warning": "All statistics for this website will be deleted, but your settings will remain intact.", + "message.saved": "Saved.", "message.share-url": "This is the publicly shared URL for {target}.", - "message.toggle-charts": "Toggle charts", - "message.track-stats": "To track stats for {target}, place the following code in the {head} section of your website.", - "message.type-delete": "Type {delete} in the box below to confirm.", - "message.type-reset": "Type {reset} in the box below to confirm.", - "metrics.actions": "Actions", - "metrics.average-visit-time": "Average visit time", - "metrics.bounce-rate": "Bounce rate", - "metrics.browsers": "Browsers", - "metrics.countries": "Countries", - "metrics.device.desktop": "Desktop", - "metrics.device.laptop": "Laptop", - "metrics.device.mobile": "Mobile", - "metrics.device.tablet": "Tablet", - "metrics.devices": "Devices", - "metrics.events": "Events", - "metrics.filter.combined": "Combined", - "metrics.filter.raw": "Raw", - "metrics.languages": "Languages", - "metrics.operating-systems": "Operating systems", - "metrics.page-views": "Page views", - "metrics.pages": "Pages", - "metrics.query-parameters": "Query parameters", - "metrics.referrers": "Referrers", - "metrics.screens": "Screens", - "metrics.unique-visitors": "Unique visitors", - "metrics.views": "Views", - "metrics.visitors": "Visitors" + "message.team-already-member": "You are already a member of the team.", + "message.team-not-found": "Team not found.", + "message.tracking-code": "To track stats for this website, place the following code in the ... section of your HTML.", + "message.user-deleted": "User deleted.", + "message.visitor-log": "Visitor from {country} using {browser} on {os} {device}", + "messages.no-results-found": "No results were found.", + "messages.no-team-websites": "This team does not have any websites.", + "messages.no-websites-configured": "You do not have any websites configured.", + "messages.team-websites-info": "Websites can be viewed by anyone on the team." } diff --git a/lang/ar-SA.json b/lang/ar-SA.json index 6bb39439..3ed0d3a5 100644 --- a/lang/ar-SA.json +++ b/lang/ar-SA.json @@ -40,9 +40,11 @@ "label.edit": "تعديل", "label.edit-dashboard": "تعديل لوحة التحكم", "label.enable-share-url": "تفعيل مشاركة الرابط", + "label.event-data": "Event data", "label.events": "الأحداث", "label.filter-combined": "مجمعة", "label.filter-raw": "مفصلة", + "label.funnel": "Funnel", "label.join": "انضمام", "label.join-team": "الانضمام للمجموعة", "label.language": "اللغة", @@ -68,6 +70,7 @@ "label.powered-by": "مشغل بواسطة {name}", "label.profile": "الملف الشخصي", "label.queries": "استعلامات", + "label.query": "Query", "label.query-parameters": "متغيرات الرابط", "label.realtime": "الوقت الفعلي", "label.referrers": "التحويلات", @@ -75,12 +78,14 @@ "label.regenerate": "اعادة انشاء", "label.regions": "المناطق", "label.remove": "إزالة", + "label.reports": "Reports", "label.required": "اجباري", "label.reset": "اعادة تعيين", "label.reset-website": "اعادة تعيين الإحصائيات", "label.role": "الصلاحية", "label.save": "حفظ", "label.screens": "الشاشات", + "label.select-date": "Select date", "label.select-website": "اختيار موقع", "label.sessions": "الزيارات", "label.settings": "اعدادات", @@ -104,6 +109,7 @@ "label.tracking-code": "كود التتبع", "label.unique-visitors": "زائرون فريدون", "label.unknown": "غير معروف", + "label.urls": "URLs", "label.user": "مستخدم", "label.username": "اسم المستخدم", "label.users": "المستخدمين", @@ -111,6 +117,7 @@ "label.view-details": "عرض التفاصيل", "label.views": "المشاهدات", "label.visitors": "الزوار", + "label.website": "Website", "label.website-id": "معرف الموقع", "label.websites": "المواقع", "label.yesterday": "الأمس", @@ -118,6 +125,7 @@ "message.confirm-delete": "هل أنت متأكد من حذف {target}?", "message.confirm-leave": "هل أنت متأكد من مغادرة {target}?", "message.confirm-reset": "هل أنت متأكد من اعادة تعيين الإحصائيات لـ {target}؟", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "حذف الموقع", "message.delete-website-warning": "كافة البيانات المرتبطة سيم حذفها ايضا.", "message.error": "حدث خطأ ما.", @@ -140,6 +148,7 @@ "message.tracking-code": "كود التتبع", "message.user-deleted": "تم حذف المستخدم.", "message.visitor-log": "زائر من {country} يستخدم {browser} على {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "هذه المجموعة ليس لديه اي موقع.", "messages.no-websites-configured": "لم تقم بإعداد اي موقع.", "messages.team-websites-info": "يمكن مشاهدة الموقع من اي عضو في المجموعة." diff --git a/lang/be-BY.json b/lang/be-BY.json index ed8b9be8..76844060 100644 --- a/lang/be-BY.json +++ b/lang/be-BY.json @@ -40,9 +40,11 @@ "label.edit": "Змяніць", "label.edit-dashboard": "Змяніць інфармацыйную панэль", "label.enable-share-url": "Дазволіць дзяліцца спасылкай", + "label.event-data": "Event data", "label.events": "Падзеі", "label.filter-combined": "Камбініаваны", "label.filter-raw": "Сырыя", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Мова", @@ -68,6 +70,7 @@ "label.powered-by": "Зроблена {name}", "label.profile": "Профіль", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "У рэяльным часе", "label.referrers": "Referrers", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Абавязкова", "label.reset": "Скінуць", "label.reset-website": "Скінуць статыстыку", "label.role": "Role", "label.save": "Захаваць", "label.screens": "Экраны", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Налады", @@ -104,6 +109,7 @@ "label.tracking-code": "Код адсочвання", "label.unique-visitors": "Унікальныя наведвальнікі", "label.unknown": "Невядома", + "label.urls": "URLs", "label.user": "User", "label.username": "Імя карыстальніка", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "Пабачыць дэталі", "label.views": "Прагляды", "label.visitors": "Наведвальнікі", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Сайты", "label.yesterday": "Учора", @@ -118,6 +125,7 @@ "message.confirm-delete": "Вы дакладна хочаце выдаліць {target}?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Вы дакладна хочаце скінуць {target} статыстыку?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Выдаліць сайт", "message.delete-website-warning": "Усе асацыяваныя дадзеныя будуць таксама выдалены.", "message.error": "Нешта пайшло не так.", @@ -140,6 +148,7 @@ "message.tracking-code": "Код адсочвання", "message.user-deleted": "User deleted.", "message.visitor-log": "Наведвальнік з {country} праз {browser} на {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "Вы не наладзілі ніводнага сайту.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/bn-BD.json b/lang/bn-BD.json index eca1363b..01af693c 100644 --- a/lang/bn-BD.json +++ b/lang/bn-BD.json @@ -40,9 +40,11 @@ "label.edit": "সম্পাদনা করুন", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "শেয়ার ইউআরএল শেয়ার করুন", + "label.event-data": "Event data", "label.events": "ঘটনা", "label.filter-combined": "সম্মিলিত", "label.filter-raw": "অপরিশোধিত", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "ভাষা", @@ -68,6 +70,7 @@ "label.powered-by": "{name} দ্বারা চালিত", "label.profile": "প্রোফাইল", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "সরাসরি", "label.referrers": "রেফারার্স", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "প্রয়োজনীয়", "label.reset": "রিসেট", "label.reset-website": "ওয়েবসাইট রিসেট করুন", "label.role": "Role", "label.save": "সংরক্ষণ", "label.screens": "স্ক্রিনগুলি", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "সেটিংস", @@ -104,6 +109,7 @@ "label.tracking-code": "ট্র্যাকিং কোড", "label.unique-visitors": "অনন্য ভিজিটর", "label.unknown": "অজানা", + "label.urls": "URLs", "label.user": "User", "label.username": "ব্যবহারকারীর নাম", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "বিস্তারিত দেখুন", "label.views": "ভিউস", "label.visitors": "পরিদর্শনার্থী", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "সবগুলো ওয়েবসাইট", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "আপনি কি নিশ্চিত যে আপনি {target} মুছতে চান?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "আপনি কি নিশ্চিত যে আপনি {target} এর পরিসংখ্যান পুনরায় সেট করতে চান?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "ওয়েবসাইট মুছুন", "message.delete-website-warning": "সমস্ত সম্পর্কিত ডেটা পাশাপাশি মুছে ফেলা হবে।", "message.error": "কিছু ভুল হয়েছে।", @@ -140,6 +148,7 @@ "message.tracking-code": "ট্র্যাকিং কোড", "message.user-deleted": "User deleted.", "message.visitor-log": "{country} থেকে একজন ভিসিটর {ব্রাউজার}, ব্যবহার করছেন {os} {device} এর মধ্যে।", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "কোনও ওয়েবসাইট কনফিগার করা নেই।", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/ca-ES.json b/lang/ca-ES.json index 3f1d7910..31c6c3e5 100644 --- a/lang/ca-ES.json +++ b/lang/ca-ES.json @@ -40,9 +40,11 @@ "label.edit": "Edita", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Activa l'enllaç per compartir", + "label.event-data": "Event data", "label.events": "Esdeveniments", "label.filter-combined": "Combinat", "label.filter-raw": "En cru", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Language", @@ -68,6 +70,7 @@ "label.powered-by": "Funciona amb {name}", "label.profile": "Perfil", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Temps real", "label.referrers": "Referents", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Obligatori", "label.reset": "Restableix", "label.reset-website": "Restableix estadístiques", "label.role": "Role", "label.save": "Desa", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Configuració", @@ -104,6 +109,7 @@ "label.tracking-code": "Codi de seguiment", "label.unique-visitors": "Visitants únics", "label.unknown": "Desconegut", + "label.urls": "URLs", "label.user": "User", "label.username": "Nom d'usuari", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "Veure els detalls", "label.views": "Vistes", "label.visitors": "Visitants", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Llocs web", "label.yesterday": "Ahir", @@ -118,6 +125,7 @@ "message.confirm-delete": "Segur que vols esborrar {target}?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Segur que vols restablir les estadístiques de {target}?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Esborra el lloc web", "message.delete-website-warning": "També s'esborraran totes les dades relacionades.", "message.error": "S'ha produït un error.", @@ -140,6 +148,7 @@ "message.tracking-code": "Codi de seguiment", "message.user-deleted": "User deleted.", "message.visitor-log": "Visitant de {country} usant {browser} a {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "No hi ha cap lloc web configurat.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/cs-CZ.json b/lang/cs-CZ.json index baf8d5e1..a2bc4577 100644 --- a/lang/cs-CZ.json +++ b/lang/cs-CZ.json @@ -40,9 +40,11 @@ "label.edit": "Upravit", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Povolit sdílení URL", + "label.event-data": "Event data", "label.events": "Události", "label.filter-combined": "Kombinace", "label.filter-raw": "Nezpracované", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Language", @@ -68,6 +70,7 @@ "label.powered-by": "Běží na {name}", "label.profile": "Profil", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Aktuálně", "label.referrers": "Odkazy", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Vyžadováno", "label.reset": "Reset", "label.reset-website": "Reset statistics", "label.role": "Role", "label.save": "Uložit", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Nastavení", @@ -104,6 +109,7 @@ "label.tracking-code": "Sledovací kód", "label.unique-visitors": "Jedinečné návštěvy", "label.unknown": "Neznámý", + "label.urls": "URLs", "label.user": "User", "label.username": "Uživatelské jméno", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "Zobrazit detaily", "label.views": "Zobrazení", "label.visitors": "Návštěvy", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Weby", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "Opravdu smazat {target}?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Smazat web", "message.delete-website-warning": "Všechna související data budou také smazána.", "message.error": "Něco se pokazilo.", @@ -140,6 +148,7 @@ "message.tracking-code": "Sledovací kód", "message.user-deleted": "User deleted.", "message.visitor-log": "Návštěvník z {country} s prohlížečem {browser} na {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "Nemáte nastavený žádný web.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/da-DK.json b/lang/da-DK.json index d8c94f84..2b437fbd 100644 --- a/lang/da-DK.json +++ b/lang/da-DK.json @@ -40,9 +40,11 @@ "label.edit": "Rediger", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Aktivér delings-URL", + "label.event-data": "Event data", "label.events": "Hændelser", "label.filter-combined": "Kombineret", "label.filter-raw": "Rå", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Sprog", @@ -68,6 +70,7 @@ "label.powered-by": "Drevet af {name}", "label.profile": "Profil", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Realtid", "label.referrers": "Henvisninger", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Påkrævet", "label.reset": "Nulstil", "label.reset-website": "Nulstil statistikker", "label.role": "Role", "label.save": "Gem", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Indstillinger", @@ -104,6 +109,7 @@ "label.tracking-code": "Sporingskode", "label.unique-visitors": "Unikke besøgende", "label.unknown": "Ukendt", + "label.urls": "URLs", "label.user": "User", "label.username": "Brugernavn", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "Vis detajler", "label.views": "Visninger", "label.visitors": "Besøgende", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Hjemmesider", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "Er du sikker på at du vil slette {target}?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Er du sikker på at du ville nulstille {target}'s statistikker?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Slet hjemmeside", "message.delete-website-warning": "Alle tilknyttede data slettes også.", "message.error": "Noget gik galt.", @@ -140,6 +148,7 @@ "message.tracking-code": "Sporingskode", "message.user-deleted": "User deleted.", "message.visitor-log": "Besøgende fra {country} bruger {browser} på {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "Du har ikke konfigureret nogen hjemmesider.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/de-CH.json b/lang/de-CH.json index f8602bc3..1c2f8fde 100644 --- a/lang/de-CH.json +++ b/lang/de-CH.json @@ -40,9 +40,11 @@ "label.edit": "Bearbeite", "label.edit-dashboard": "Dashboard bearbeite", "label.enable-share-url": "Freigab-URL aktiviere", + "label.event-data": "Event data", "label.events": "Ereigniss", "label.filter-combined": "Kombiniert", "label.filter-raw": "Rohdate", + "label.funnel": "Funnel", "label.join": "Biträte", "label.join-team": "Team biträte", "label.language": "Sprach", @@ -68,6 +70,7 @@ "label.powered-by": "Betribe dur {name}", "label.profile": "Profil", "label.queries": "Abfrage", + "label.query": "Query", "label.query-parameters": "Abfragparameter", "label.realtime": "Echtzit", "label.referrers": "Referrer", @@ -75,12 +78,14 @@ "label.regenerate": "Erneuere", "label.regions": "Regionä", "label.remove": "Entferne", + "label.reports": "Reports", "label.required": "Erforderlich", "label.reset": "Zruggsetze", "label.reset-website": "Statistik zruggsetze", "label.role": "Rollä", "label.save": "Speichere", "label.screens": "Bildschirmuflösige", + "label.select-date": "Select date", "label.select-website": "Websiite uuswähle", "label.sessions": "Sessions", "label.settings": "Istellige", @@ -104,6 +109,7 @@ "label.tracking-code": "Tracking Code", "label.unique-visitors": "Eidütigi Bsuecher", "label.unknown": "Unbekannt", + "label.urls": "URLs", "label.user": "Benutzer", "label.username": "Benutzername", "label.users": "Benutzer", @@ -111,6 +117,7 @@ "label.view-details": "Details azeige", "label.views": "Ufrüef", "label.visitors": "Bsuecher", + "label.website": "Website", "label.website-id": "Websiite ID", "label.websites": "Websiite", "label.yesterday": "Gester", @@ -118,6 +125,7 @@ "message.confirm-delete": "Sind Sie sich sicher, {target} zlösche?", "message.confirm-leave": "Sind Sie sich sicher, {target} zverlah?", "message.confirm-reset": "Sind Sie sicher, dass Sie dStatistike vo {target} zruggsetze wend?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Websiite lösche", "message.delete-website-warning": "Alli dezueghörige Date werdet ebefalls glöscht.", "message.error": "Es isch en Fehler uftrete.", @@ -140,6 +148,7 @@ "message.tracking-code": "Tracking Code", "message.user-deleted": "Benutzer glöscht.", "message.visitor-log": "Bsuecher us {country} benutzt {browser} uf {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "Dem Team sind kei Websiite zuegordnet.", "messages.no-websites-configured": "Es isch kei Websiite vorhande.", "messages.team-websites-info": "Websiite chönd vo jedem im Team agluegt werde" diff --git a/lang/de-DE.json b/lang/de-DE.json index b3611d5b..aa84f725 100644 --- a/lang/de-DE.json +++ b/lang/de-DE.json @@ -40,9 +40,11 @@ "label.edit": "Bearbeiten", "label.edit-dashboard": "Dashboard bearbeiten", "label.enable-share-url": "Freigabe-URL aktivieren", + "label.event-data": "Event data", "label.events": "Ereignisse", "label.filter-combined": "Kombiniert", "label.filter-raw": "Rohdaten", + "label.funnel": "Funnel", "label.join": "Beitreten", "label.join-team": "Team beitreten", "label.language": "Sprache", @@ -68,6 +70,7 @@ "label.powered-by": "Betrieben durch {name}", "label.profile": "Profil", "label.queries": "Abfragen", + "label.query": "Query", "label.query-parameters": "Abfrageparameter", "label.realtime": "Echtzeit", "label.referrers": "Referrer", @@ -75,12 +78,14 @@ "label.regenerate": "Erneuern", "label.regions": "Regionen", "label.remove": "Entfernen", + "label.reports": "Reports", "label.required": "Erforderlich", "label.reset": "Zurücksetzen", "label.reset-website": "Statistik zurücksetzen", "label.role": "Rolle", "label.save": "Speichern", "label.screens": "Bildschirmauflösungen", + "label.select-date": "Select date", "label.select-website": "Website auswählen", "label.sessions": "Sessions", "label.settings": "Einstellungen", @@ -104,6 +109,7 @@ "label.tracking-code": "Tracking Code", "label.unique-visitors": "Eindeutige Besucher", "label.unknown": "Unbekannt", + "label.urls": "URLs", "label.user": "Benutzer", "label.username": "Benutzername", "label.users": "Benutzer", @@ -111,6 +117,7 @@ "label.view-details": "Details anzeigen", "label.views": "Aufrufe", "label.visitors": "Besucher", + "label.website": "Website", "label.website-id": "Webseite ID", "label.websites": "Webseiten", "label.yesterday": "Gestern", @@ -118,6 +125,7 @@ "message.confirm-delete": "Sind Sie sich sicher, {target} zu löschen?", "message.confirm-leave": "Sind Sie sicher, dass die {target} verlassen möchten?", "message.confirm-reset": "Sind Sie sicher, dass Sie die Statistiken von {target} zurücksetzen wollen?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Webseite löschen", "message.delete-website-warning": "Alle zugehörigen Daten werden ebenfalls gelöscht.", "message.error": "Es ist ein Fehler aufgetreten.", @@ -140,6 +148,7 @@ "message.tracking-code": "Tracking Code", "message.user-deleted": "Benutzer gelöscht.", "message.visitor-log": "Besucher aus {country} benutzt {browser} auf {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "Diesem Team sind keine Websites zugeordnet.", "messages.no-websites-configured": "Es ist keine Webseite vorhanden.", "messages.team-websites-info": "Webseiten können von jedem im Team eingesehen werden." diff --git a/lang/el-GR.json b/lang/el-GR.json index f32cd93a..06f82484 100644 --- a/lang/el-GR.json +++ b/lang/el-GR.json @@ -40,9 +40,11 @@ "label.edit": "Επεξεργασία", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Ενεργοποίηση κοινής χρήσης URL", + "label.event-data": "Event data", "label.events": "Γεγονότα", "label.filter-combined": "Σε συνδυασμό", "label.filter-raw": "Ακατέργαστο", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Language", @@ -68,6 +70,7 @@ "label.powered-by": "Με την υποστήριξη του {name}", "label.profile": "Προφίλ", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Realtime", "label.referrers": "Παραπομπές", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Απαιτείται", "label.reset": "Επαναφορά", "label.reset-website": "Reset statistics", "label.role": "Role", "label.save": "Αποθήκευση", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Ρυθμίσεις", @@ -104,6 +109,7 @@ "label.tracking-code": "Κωδικός παρακολούθησης", "label.unique-visitors": "Μοναδικοί επισκέπτες", "label.unknown": "Άγνωστο", + "label.urls": "URLs", "label.user": "User", "label.username": "Όνομα χρήστη", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "Λεπτομέρειες", "label.views": "Προβολές", "label.visitors": "Επισκέπτες", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Ιστότοποι", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "Είστε βέβαιοι ότι θέλετε να διαγράψετε το {target};", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Διαγραφή ιστότοπου", "message.delete-website-warning": "Όλα τα σχετικά δεδομένα θα διαγραφούν επίσης.", "message.error": "Κάτι πήγε στραβά.", @@ -140,6 +148,7 @@ "message.tracking-code": "Κωδικός παρακολούθησης", "message.user-deleted": "User deleted.", "message.visitor-log": "Visitor from {country} using {browser} on {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "Δεν έχετε ρυθμίσει κανένα ιστότοπο.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/en-GB.json b/lang/en-GB.json index 296106f2..95fe2439 100644 --- a/lang/en-GB.json +++ b/lang/en-GB.json @@ -40,9 +40,11 @@ "label.edit": "Edit", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Enable share URL", + "label.event-data": "Event data", "label.events": "Events", "label.filter-combined": "Combined", "label.filter-raw": "Raw", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Language", @@ -68,6 +70,7 @@ "label.powered-by": "Powered by {name}", "label.profile": "Profile", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Realtime", "label.referrers": "Referrers", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Required", "label.reset": "Reset", "label.reset-website": "Reset statistics", "label.role": "Role", "label.save": "Save", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Settings", @@ -104,6 +109,7 @@ "label.tracking-code": "Tracking code", "label.unique-visitors": "Unique visitors", "label.unknown": "Unknown", + "label.urls": "URLs", "label.user": "User", "label.username": "Username", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "View details", "label.views": "Views", "label.visitors": "Visitors", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Websites", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "Are you sure you want to delete {target}?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Are you sure you want to reset {target}'s statistics?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Delete website", "message.delete-website-warning": "All associated data will be deleted as well.", "message.error": "Something went wrong.", @@ -140,6 +148,7 @@ "message.tracking-code": "Tracking code", "message.user-deleted": "User deleted.", "message.visitor-log": "Visitor from {country} using {browser} on {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "You don't have any websites configured.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/en-US.json b/lang/en-US.json index e80398c6..f963688f 100644 --- a/lang/en-US.json +++ b/lang/en-US.json @@ -23,7 +23,7 @@ "label.create-user": "Create user", "label.created": "Created", "label.current-password": "Current password", - "label.custom-range": "Custom-range", + "label.custom-range": "Custom range", "label.dashboard": "Dashboard", "label.data": "Data", "label.date-range": "Date range", @@ -40,9 +40,11 @@ "label.edit": "Edit", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Enable share URL", + "label.event-data": "Event data", "label.events": "Events", "label.filter-combined": "Combined", "label.filter-raw": "Raw", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Language", @@ -68,6 +70,7 @@ "label.powered-by": "Powered by {name}", "label.profile": "Profile", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Realtime", "label.referrers": "Referrers", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Required", "label.reset": "Reset", "label.reset-website": "Reset website", "label.role": "Role", "label.save": "Save", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Settings", @@ -104,6 +109,7 @@ "label.tracking-code": "Tracking code", "label.unique-visitors": "Unique visitors", "label.unknown": "Unknown", + "label.urls": "URLs", "label.user": "User", "label.username": "Username", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "View details", "label.views": "Views", "label.visitors": "Visitors", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Websites", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "Are you sure you want to delete {target}?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Are you sure you want to reset {target}?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "To delete this website, type {confirmation} in the box below to confirm.", "message.delete-website-warning": "All website data will be deleted.", "message.error": "Something went wrong.", @@ -140,6 +148,7 @@ "message.tracking-code": "To track stats for this website, place the following code in the ... section of your HTML.", "message.user-deleted": "User deleted.", "message.visitor-log": "Visitor from {country} using {browser} on {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "You do not have any websites configured.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/es-MX.json b/lang/es-MX.json index 999c1d8f..6435758d 100644 --- a/lang/es-MX.json +++ b/lang/es-MX.json @@ -40,9 +40,11 @@ "label.edit": "Editar", "label.edit-dashboard": "Editar panel", "label.enable-share-url": "Habilitar compartir URL", + "label.event-data": "Event data", "label.events": "Eventos", "label.filter-combined": "Combinado", "label.filter-raw": "Personalizado", + "label.funnel": "Funnel", "label.join": "Unir", "label.join-team": "Unir a equipo", "label.language": "Idioma", @@ -68,6 +70,7 @@ "label.powered-by": "Analíticas de {name}", "label.profile": "Perfil", "label.queries": "Consultas", + "label.query": "Query", "label.query-parameters": "Parámetros de petición", "label.realtime": "Tiempo real", "label.referrers": "Referido desde", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerar", "label.regions": "Regiones", "label.remove": "Quitar", + "label.reports": "Reports", "label.required": "Obligatorio", "label.reset": "Reiniciar", "label.reset-website": "Reiniciar estadísticas", "label.role": "Rol", "label.save": "Guardar", "label.screens": "Pantallas", + "label.select-date": "Select date", "label.select-website": "Seleccionar sitio web", "label.sessions": "Sesiones", "label.settings": "Configuraciones", @@ -104,6 +109,7 @@ "label.tracking-code": "Código de rastreo", "label.unique-visitors": "Visitantes únicos", "label.unknown": "Desconocida", + "label.urls": "URLs", "label.user": "Usuario", "label.username": "Nombre de usuario", "label.users": "Usuarios", @@ -111,6 +117,7 @@ "label.view-details": "Ver detalles", "label.views": "Vistas", "label.visitors": "Visitantes", + "label.website": "Website", "label.website-id": "ID del sitio web", "label.websites": "Sitios", "label.yesterday": "Ayer", @@ -118,6 +125,7 @@ "message.confirm-delete": "¿Seguro que quieres eliminar {target}?", "message.confirm-leave": "¿Seguro que quieres abandonar {target}?", "message.confirm-reset": "¿Seguro que quieres BORRAR las analíticas de {target}?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Eliminar sitio web", "message.delete-website-warning": "Toda la información relacionada será eliminada.", "message.error": "Algo falló.", @@ -140,6 +148,7 @@ "message.tracking-code": "Código de rastreo", "message.user-deleted": "Usuario eliminado.", "message.visitor-log": "Visitante desde {country} usando {browser} en {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "Este equipo no tiene ningún sitio web configurado.", "messages.no-websites-configured": "No tienes ningún sitio configurado.", "messages.team-websites-info": "Las analíticas de tus sitios pueden verse por cualquier miembro del equipo." diff --git a/lang/fa-IR.json b/lang/fa-IR.json index 94330ce3..477481c2 100644 --- a/lang/fa-IR.json +++ b/lang/fa-IR.json @@ -40,9 +40,11 @@ "label.edit": "ویرایش", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "فعال کردن اشتراک گذاری URL", + "label.event-data": "Event data", "label.events": "رویدادها", "label.filter-combined": "ترکیب شده", "label.filter-raw": "خام", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "زبان", @@ -68,6 +70,7 @@ "label.powered-by": "قدرت گرفته توسط {name}", "label.profile": "پروفایل", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "آمار زنده", "label.referrers": "ارجاع دهندگان", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "ضروری", "label.reset": "بازنشانی", "label.reset-website": "بازنشانی آمار", "label.role": "Role", "label.save": "ذخیره", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "تنظیمات", @@ -104,6 +109,7 @@ "label.tracking-code": "کد رهگیری", "label.unique-visitors": "بازدیدکننده‌های یکتا", "label.unknown": "ناشناخته", + "label.urls": "URLs", "label.user": "User", "label.username": "نام کاربری", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "مشاهده‌ی جزئیات", "label.views": "بازدید", "label.visitors": "بازدیدکننده", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "وب‌سایت‌ها", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "آیا مطمئن هستید می‌خواهید {target} را حذف کنید?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "آیا از بازنشانی آمار {target} مطمئن هستید?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "حذف وب‌سایت", "message.delete-website-warning": "همه‌ی داده‌های مرتبط هم حذف خواهد شد.", "message.error": "مشکلی پیش آمده است.", @@ -140,6 +148,7 @@ "message.tracking-code": "کد رهگیری", "message.user-deleted": "User deleted.", "message.visitor-log": "بازدیدکننده از کشور {country} با مروگر {browser} در {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "شما هیچ وب‌سایتی را پیکربندی نکرده‌اید.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/fi-FI.json b/lang/fi-FI.json index 28ef3e72..4031f115 100644 --- a/lang/fi-FI.json +++ b/lang/fi-FI.json @@ -40,9 +40,11 @@ "label.edit": "Muokkaa", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Ota jakamisen URL-osoite käyttöön", + "label.event-data": "Event data", "label.events": "Tapahtumat", "label.filter-combined": "Yhdistetty", "label.filter-raw": "Käsittelemätön", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Kieli", @@ -68,6 +70,7 @@ "label.powered-by": "Voimanlähteenä {name}", "label.profile": "Profiili", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Juuri nyt", "label.referrers": "Viittaajat", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Vaaditaan", "label.reset": "Nollaa", "label.reset-website": "Nollaa tilastot", "label.role": "Role", "label.save": "Tallenna", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Asetukset", @@ -104,6 +109,7 @@ "label.tracking-code": "Seurantakoodi", "label.unique-visitors": "Yksittäiset kävijät", "label.unknown": "Tuntematon", + "label.urls": "URLs", "label.user": "User", "label.username": "Käyttäjänimi", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "Katso tiedot", "label.views": "Näyttökerrat", "label.visitors": "Vierailijat", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Verkkosivut", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "Haluatko varmasti poistaa sivuston {target}?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Haluatko varmasti poistaa sivuston {target} tilastot?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Poista verkkosivu", "message.delete-website-warning": "Kaikki siihen liittyvät tiedot poistetaan.", "message.error": "Jotain meni pieleen.", @@ -140,6 +148,7 @@ "message.tracking-code": "Seurantakoodi", "message.user-deleted": "User deleted.", "message.visitor-log": "Vierailija maasta {country} selaimella {browser} laitteella {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "Sinulla ei ole määritettyjä verkkosivustoja.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/fo-FO.json b/lang/fo-FO.json index 63e7742f..ed8e7208 100644 --- a/lang/fo-FO.json +++ b/lang/fo-FO.json @@ -40,9 +40,11 @@ "label.edit": "Ger broyting", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Virkja deili leinki", + "label.event-data": "Event data", "label.events": "Hendingar/tiltøk", "label.filter-combined": "Samansett", "label.filter-raw": "Óviðgjørt", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Language", @@ -68,6 +70,7 @@ "label.powered-by": "Powered by {name}", "label.profile": "Vangi", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Beinleiðis", "label.referrers": "Framsendingar", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Kravt", "label.reset": "Nulstilla", "label.reset-website": "Reset statistics", "label.role": "Role", "label.save": "Goym", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Stillingar", @@ -104,6 +109,7 @@ "label.tracking-code": "Spori kota", "label.unique-visitors": "Einsýna vitjanir", "label.unknown": "Ókent", + "label.urls": "URLs", "label.user": "User", "label.username": "Brúkaranavn", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "Vís frágreiðing", "label.views": "Sýningar", "label.visitors": "Vitjandi", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Heimasíður", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "Ert tú sikkur at tú ynskir at strika {target}?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Sletta heimasíðu", "message.delete-website-warning": "Øll data ið er knýtt at verður eisini strika.", "message.error": "Okkurt bleiv gali.", @@ -140,6 +148,7 @@ "message.tracking-code": "Spori kota", "message.user-deleted": "User deleted.", "message.visitor-log": "Vitjandi frá {country} brúkar {browser} á {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "Tú hevur ongar heimasíður stillaða til.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/fr-FR.json b/lang/fr-FR.json index 162a6f8f..a7f54219 100644 --- a/lang/fr-FR.json +++ b/lang/fr-FR.json @@ -40,9 +40,11 @@ "label.edit": "Modifier", "label.edit-dashboard": "Modifier le tableau de bord", "label.enable-share-url": "Activer l'URL de partage", + "label.event-data": "Event data", "label.events": "Événements", "label.filter-combined": "Combiné", "label.filter-raw": "Brut", + "label.funnel": "Funnel", "label.join": "Rejoindre", "label.join-team": "Rejoindre une équipe", "label.language": "Langue", @@ -68,6 +70,7 @@ "label.powered-by": "Propulsé par {name}", "label.profile": "Profil", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Paramètres d'URL", "label.realtime": "Temps réel", "label.referrers": "Sources", @@ -75,12 +78,14 @@ "label.regenerate": "Régénérer", "label.regions": "Régions", "label.remove": "Retirer", + "label.reports": "Reports", "label.required": "Requis", "label.reset": "Réinitialiser", "label.reset-website": "Réinitialiser les statistiques", "label.role": "Rôle", "label.save": "Enregistrer", "label.screens": "Résolutions d'écran", + "label.select-date": "Select date", "label.select-website": "Choisir un site", "label.sessions": "Sessions", "label.settings": "Paramètres", @@ -104,6 +109,7 @@ "label.tracking-code": "Code de suivi", "label.unique-visitors": "Visiteurs uniques", "label.unknown": "Inconnu", + "label.urls": "URLs", "label.user": "Utilisateur", "label.username": "Nom d'utilisateur", "label.users": "Utilisateurs", @@ -111,6 +117,7 @@ "label.view-details": "Voir les détails", "label.views": "Vues", "label.visitors": "Visiteurs", + "label.website": "Website", "label.website-id": "ID de site", "label.websites": "Sites", "label.yesterday": "Hier", @@ -118,6 +125,7 @@ "message.confirm-delete": "Êtes-vous sûr de vouloir supprimer {target} ?", "message.confirm-leave": "Êtes-vous sûr de vouloir quitter {target} ?", "message.confirm-reset": "Êtes-vous sûr de vouloir réinitialiser les statistiques de {target} ?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Pour supprimer ce site, taper {confirmation} ci-dessous pour confirmer.", "message.delete-website-warning": "Toutes les données associées seront supprimées.", "message.error": "Un problème est survenu.", @@ -140,6 +148,7 @@ "message.tracking-code": "Code de suivi", "message.user-deleted": "Utilisateur supprimé.", "message.visitor-log": "Visiteur de {country} utilisant {browser} sur {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "Cette équipe n'a aucun site.", "messages.no-websites-configured": "Vous n'avez configuré aucun site.", "messages.team-websites-info": "Les sites peuvent être vus par tout utilisateur dans l'équipe." diff --git a/lang/ga-ES.json b/lang/ga-ES.json index a8aa0383..35f312c7 100644 --- a/lang/ga-ES.json +++ b/lang/ga-ES.json @@ -40,9 +40,11 @@ "label.edit": "Editar", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Activar URL de compartición", + "label.event-data": "Event data", "label.events": "Eventos", "label.filter-combined": "Combinado", "label.filter-raw": "Raw", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Idioma", @@ -68,6 +70,7 @@ "label.powered-by": "Funciona grazas a {name}", "label.profile": "Perfil", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Agora mesmo", "label.referrers": "Orixes", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Requerido", "label.reset": "Restablecer", "label.reset-website": "Restablecer estatísticas", "label.role": "Role", "label.save": "Gardar", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Axustes", @@ -104,6 +109,7 @@ "label.tracking-code": "Código de seguimento", "label.unique-visitors": "Visitas únicas", "label.unknown": "Descoñecido", + "label.urls": "URLs", "label.user": "User", "label.username": "Identificador", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "Ver detalles", "label.views": "Visualizacións", "label.visitors": "Visitantes", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Sitios web", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "Tes a certeza de querer eliminar {target}?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Tes a certeza de querer restablecer as estatísticas de {target}?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Eliminar sitio web", "message.delete-website-warning": "Tamén serán borrados tódolos datos asociados.", "message.error": "Houbo un fallo.", @@ -140,6 +148,7 @@ "message.tracking-code": "Código de seguimento", "message.user-deleted": "User deleted.", "message.visitor-log": "Visitante desde {country} usando {browser} en {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "Non tes sitios web configurados.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/he-IL.json b/lang/he-IL.json index f9284e17..df9a5cd6 100644 --- a/lang/he-IL.json +++ b/lang/he-IL.json @@ -40,9 +40,11 @@ "label.edit": "עריכה", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "הפעלת URL שיתוף", + "label.event-data": "Event data", "label.events": "אירועים", "label.filter-combined": "משותף", "label.filter-raw": "גולמי", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Language", @@ -68,6 +70,7 @@ "label.powered-by": "Powered by {name}", "label.profile": "פרופיל", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "זמן אמת", "label.referrers": "מפנים", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "נדרש", "label.reset": "איפוס", "label.reset-website": "Reset statistics", "label.role": "Role", "label.save": "שמירה", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "הגדרות", @@ -104,6 +109,7 @@ "label.tracking-code": "קוד מעקב", "label.unique-visitors": "מבקרים ייחודיים", "label.unknown": "לא ידוע", + "label.urls": "URLs", "label.user": "User", "label.username": "שם משתמש", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "פרטים נוספים", "label.views": "צפיות", "label.visitors": "מבקרים", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "אתרים", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "האם באמת למחוק את {target}?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "הסרת אתר", "message.delete-website-warning": "כל המידע המקושר יימחק", "message.error": "משהו השתבש", @@ -140,6 +148,7 @@ "message.tracking-code": "קוד מעקב", "message.user-deleted": "User deleted.", "message.visitor-log": "מבקר ממדינת {country} משתמבש בדפדפן {browser} ב-{os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "לא מוגדרים אתרים", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/hi-IN.json b/lang/hi-IN.json index 794e0cd5..bddae4c8 100644 --- a/lang/hi-IN.json +++ b/lang/hi-IN.json @@ -40,9 +40,11 @@ "label.edit": "संपादित करें", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "शेयर URL सक्षम करें", + "label.event-data": "Event data", "label.events": "स्पर्धाएँ", "label.filter-combined": "संयुक्त", "label.filter-raw": "रॉ", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Language", @@ -68,6 +70,7 @@ "label.powered-by": "{name} द्वारा संचालित", "label.profile": "प्रोफ़ाइल", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "वास्तव काल", "label.referrers": "सन्दर्भदाता", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "अपेक्षित", "label.reset": "रीसेट", "label.reset-website": "Reset statistics", "label.role": "Role", "label.save": "सहेजें", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "समायोजन", @@ -104,6 +109,7 @@ "label.tracking-code": "ट्रैकिंग कोड", "label.unique-visitors": "अद्वितीय आगंतुकों", "label.unknown": "अज्ञात", + "label.urls": "URLs", "label.user": "User", "label.username": "उपयोगकर्ता नाम", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "विवरण देखें", "label.views": "दृश्य", "label.visitors": "आगंतुकों", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "वेबसाइटों", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "क्या आप वाकई में {target} हटाना चाहते हैं?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "वेबसाइट हटाएं", "message.delete-website-warning": "सभी संबद्ध डेटा को भी हटा दिया जाएगा।", "message.error": "कुछ गलत हो गया।", @@ -140,6 +148,7 @@ "message.tracking-code": "ट्रैकिंग कोड", "message.user-deleted": "User deleted.", "message.visitor-log": "{country} का आगंतुक, जो {browser} का उपयोग करता है, {os} यन्त्र पर", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "आपके पास कोई वेबसाइट कॉन्फ़िगर नहीं है।", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/hr-HR.json b/lang/hr-HR.json index e9072a89..263852fe 100644 --- a/lang/hr-HR.json +++ b/lang/hr-HR.json @@ -1,120 +1,155 @@ { - "label.accounts": "Računi", - "label.add-account": "Dodaj račun", - "label.add-column": "Dodaj stupac", - "label.add-filter": "Dodaj filter", + "label.access-code": "Access code", + "label.actions": "Actions", + "label.activity-log": "Activity log", "label.add-website": "Dodaj web stranicu", - "label.administrator": "Administrator", + "label.admin": "Administrator", "label.all": "Sve", "label.all-time": "Svo vrijeme", - "label.all-websites": "Sve web stranice", + "label.analytics": "Analytics", + "label.average-visit-time": "Average visit time", "label.back": "Natrag ", + "label.bounce-rate": "Bounce rate", + "label.browsers": "Browsers", "label.cancel": "Odustani", "label.change-password": "Promijeni lozinku", + "label.cities": "Cities", + "label.clear-all": "Clear all", + "label.confirm": "Confirm", "label.confirm-password": "Potvrdi lozinku", - "label.copy-to-clipboard": "Kopiraj u međuspremnik", + "label.continue": "Continue", + "label.countries": "Countries", + "label.create-team": "Create team", + "label.create-user": "Create user", + "label.created": "Created", "label.current-password": "Trenutna lozinka", "label.custom-range": "Prilagođeni raspon", "label.dashboard": "Nadzorna ploča", + "label.data": "Data", "label.date-range": "Raspon datuma", "label.default-date-range": "Zadani datumski raspon", "label.delete": "Obriši", - "label.delete-account": "Obriši račun", + "label.delete-team": "Delete team", + "label.delete-user": "Delete user", "label.delete-website": "Obriši web stranicu", + "label.desktop": "Desktop", + "label.details": "Details", + "label.devices": "Devices", "label.dismiss": "Odbaci", "label.domain": "Domena", "label.edit": "Uredi", - "label.edit-account": "Uredi račun", - "label.edit-website": "Uredi web stranicu", + "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Omogući dijeljenje poveznice", "label.event-data": "Podaci događaja", - "label.field-name": "Naziv polja", - "label.invalid": "Neispravno", - "label.invalid-domain": "Neispravna domena", + "label.events": "Events", + "label.filter-combined": "Combined", + "label.filter-raw": "Raw", + "label.funnel": "Funnel", + "label.join": "Join", + "label.join-team": "Join team", "label.language": "Jezik", + "label.languages": "Languages", + "label.laptop": "Laptop", "label.last-days": "Zadnjih {x} dana", "label.last-hours": "Zadnjih {x} sati", - "label.logged-in-as": "Prijavljen kao {username}", + "label.leave": "Leave", + "label.leave-team": "Leave team", "label.login": "Prijava", "label.logout": "Odjava", + "label.members": "Members", + "label.mobile": "Mobile", "label.more": "Više", "label.name": "Ime", "label.new-password": "Nova lozinka", "label.none": "Ništa", + "label.operating-systems": "Operating systems", "label.owner": "Vlasnik", + "label.page-views": "Page views", + "label.pages": "Pages", "label.password": "Lozinka", - "label.passwords-dont-match": "Lozinke se ne podudaraju", + "label.powered-by": "Powered by {name}", "label.profile": "Profil", + "label.queries": "Queries", + "label.query": "Query", + "label.query-parameters": "Query parameters", "label.realtime": "Stvarno vrijeme", - "label.realtime-logs": "Trenutni zapisi", + "label.referrers": "Referrers", "label.refresh": "Osvježi", + "label.regenerate": "Regenerate", + "label.regions": "Regions", + "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Potrebna", "label.reset": "Resetirati", "label.reset-website": "Resetirati web stranicu", + "label.role": "Role", "label.save": "Spremi", - "label.search": "Pretraži", + "label.screens": "Screens", + "label.select-date": "Select date", + "label.select-website": "Select website", + "label.sessions": "Sessions", "label.settings": "Postavke", "label.share-url": "Podijeli poveznicu", "label.single-day": "Jedan dan", + "label.tablet": "Tablet", + "label.team": "Team", + "label.team-guest": "Team guest", + "label.team-id": "Team ID", + "label.team-member": "Team member", + "label.team-owner": "Team owner", + "label.teams": "Teams", "label.theme": "Tema", "label.this-month": "Ovaj mjesec", "label.this-week": "Ovaj tjedan", "label.this-year": "Ova godina", "label.timezone": "Vremenska zona", + "label.title": "Title", "label.today": "Danas", + "label.toggle-charts": "Toggle charts", "label.tracking-code": "Kod za praćenje", - "label.type": "Tip", + "label.unique-visitors": "Unique visitors", "label.unknown": "Nepoznato", + "label.urls": "URLs", + "label.user": "User", "label.username": "Korisničko ime", - "label.value": "Vrijednost", + "label.users": "Users", + "label.view": "View", "label.view-details": "Pogledaj detalje", + "label.views": "Views", + "label.visitors": "Visitors", + "label.website": "Website", + "label.website-id": "Website ID", "label.websites": "Web stranice", "label.yesterday": "Jučer", "message.active-users": "{x} Trenutno {x, plural, one {posjetitelj} other {posjetitelja}}", "message.confirm-delete": "Jeste li sigurni da želite obrisati {target}?", + "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Jeste li sigurni da želite resetirati {target}'s statistiku?", - "message.copied": "Kopirano!", - "message.delete-warning": "Izbrisat će se svi povezani podaci.", - "message.edit-dashboard": "Uredi nadzornu ploču", - "message.failure": "Nešto je pošlo po zlu.", - "message.get-share-url": "Dohvati poveznicu za dijeljenje", - "message.get-tracking-code": "Dohvati kod za praćenje", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", + "message.delete-website": "To delete this website, type {confirmation} in the box below to confirm.", + "message.delete-website-warning": "All website data will be deleted.", + "message.error": "Something went wrong.", + "message.event-log": "{event} on {url}", "message.go-to-settings": "Idi u postavke", "message.incorrect-username-password": "Neispravno korisničke ime/lozinka.", - "message.log.visitor": "Posjetitelj iz {country} koristi {browser} na {os} {device}", - "message.new-version-available": "Nova verzija umami {version} je dostupna!", + "message.invalid-domain": "Invalid domain. Do not include http/https.", + "message.min-password-length": "Minimum length of {n} characters", "message.no-data-available": "Nema dostupnih podataka.", - "message.no-websites-configured": "Nemate konfiguriranu nijednu web stranicu.", + "message.no-match-password": "Passwords do not match.", + "message.no-teams": "You have not created any teams.", + "message.no-users": "There are no users.", "message.page-not-found": "Stranica nije pronađena.", - "message.powered-by": "Pokreće {name}", - "message.reset-warning": "Sve statistike za ovu web stranicu bit će izbrisane, ali će vaš kod za praćenje ostati netaknut.", - "message.save-success": "Uspješno spremljeno.", + "message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.", + "message.reset-website-warning": "All statistics for this website will be deleted, but your settings will remain intact.", + "message.saved": "Saved.", "message.share-url": "Ovo je javno dijeljena poveznica za {target}.", - "message.toggle-charts": "Uključi/isključi grafikone", - "message.track-stats": "Da biste pratili statistiku za {target}, postavite sljedeći kod u odjeljak {head} svoje web stranice.", - "message.type-delete": "Upišite {delete} u donji okvir za potvrdu.", - "message.type-reset": " Upišite {reset} u donji okvir za potvrdu. ", - "metrics.actions": "Akcije", - "metrics.average-visit-time": "Prosječno vrijeme posjeta", - "metrics.bounce-rate": "Stopa napuštanja stranice", - "metrics.browsers": "Web preglednici", - "metrics.countries": "Zemlje", - "metrics.device.desktop": "Pc", - "metrics.device.laptop": "Laptop", - "metrics.device.mobile": "Mobitel", - "metrics.device.tablet": "Tablet", - "metrics.devices": "Uređaji", - "metrics.events": "Događaji", - "metrics.filter.combined": "Kombinirano", - "metrics.filter.raw": "Neobrađeni podaci", - "metrics.languages": "Jezici", - "metrics.operating-systems": "Operativni sustavi", - "metrics.page-views": "Pregledi stranice", - "metrics.pages": "Stranice", - "metrics.query-parameters": "Parametri upita", - "metrics.referrers": "Upučivaći", - "metrics.screens": "Zasloni", - "metrics.unique-visitors": "Jedinstveni posjetitelji", - "metrics.views": "Pregledi", - "metrics.visitors": "Posjetitelji" + "message.team-already-member": "You are already a member of the team.", + "message.team-not-found": "Team not found.", + "message.tracking-code": "To track stats for this website, place the following code in the ... section of your HTML.", + "message.user-deleted": "User deleted.", + "message.visitor-log": "Visitor from {country} using {browser} on {os} {device}", + "messages.no-results-found": "No results were found.", + "messages.no-team-websites": "This team does not have any websites.", + "messages.no-websites-configured": "You do not have any websites configured.", + "messages.team-websites-info": "Websites can be viewed by anyone on the team." } diff --git a/lang/hu-HU.json b/lang/hu-HU.json index 803a2332..23ddf608 100644 --- a/lang/hu-HU.json +++ b/lang/hu-HU.json @@ -40,9 +40,11 @@ "label.edit": "Módosítás", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "URL-megosztás engedélyezése", + "label.event-data": "Event data", "label.events": "Események", "label.filter-combined": "Összevont", "label.filter-raw": "Nyers", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Language", @@ -68,6 +70,7 @@ "label.powered-by": "Működteti az {name}", "label.profile": "Profil", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Valós idejű", "label.referrers": "Hivatkozók", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Kötelező", "label.reset": "Visszaállítás", "label.reset-website": "Reset statistics", "label.role": "Role", "label.save": "Mentés", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Beállítások", @@ -104,6 +109,7 @@ "label.tracking-code": "Követési kód", "label.unique-visitors": "Egyedi látogatók", "label.unknown": "Ismeretlen", + "label.urls": "URLs", "label.user": "User", "label.username": "Felhasználónév", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "Részletek", "label.views": "Megtekintések", "label.visitors": "Látogatók", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Weboldalak", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "Biztos, hogy törölni szeretnéd {target} elemet?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Weboldal eltávolítása", "message.delete-website-warning": "Minden társított adat törlésre kerül.", "message.error": "Valami baj történt.", @@ -140,6 +148,7 @@ "message.tracking-code": "Követési kód", "message.user-deleted": "User deleted.", "message.visitor-log": "Látógató {country} területéről, {os} {device} eszközön, {browser} böngészőből.", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "Még nem állítottál be egyetlen weboldalt sem.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/id-ID.json b/lang/id-ID.json index b93172cf..b8d396c1 100644 --- a/lang/id-ID.json +++ b/lang/id-ID.json @@ -40,9 +40,11 @@ "label.edit": "Sunting", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Aktifkan URL berbagi", + "label.event-data": "Event data", "label.events": "Perihal", "label.filter-combined": "Gabungan", "label.filter-raw": "Mentah", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Bahasa", @@ -68,6 +70,7 @@ "label.powered-by": "Didukung oleh {name}", "label.profile": "Profil", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Waktu nyata", "label.referrers": "Perujuk", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Wajib", "label.reset": "Atur ulang", "label.reset-website": "Atur ulang statistik", "label.role": "Role", "label.save": "Simpan", "label.screens": "Layar", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Pengaturan", @@ -104,6 +109,7 @@ "label.tracking-code": "Kode lacak", "label.unique-visitors": "Pengunjung unik", "label.unknown": "Tidak diketahui", + "label.urls": "URLs", "label.user": "User", "label.username": "Nama pengguna", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "Lihat Detil", "label.views": "Tampilan", "label.visitors": "Pengunjung", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Situs web", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "Apakah kamu yakin ingin menghapus {target}?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Anda yakin ingin mengatur ulang statistik {target}?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Hapus situs web", "message.delete-website-warning": "Semua data terkait juga akan dihapus.", "message.error": "Ada yang salah.", @@ -140,6 +148,7 @@ "message.tracking-code": "Kode lacak", "message.user-deleted": "User deleted.", "message.visitor-log": "Pengunjung dari {country} dengan {browser} di {device} {os}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "Anda tidak memiliki situs web yang dikonfigurasi.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/it-IT.json b/lang/it-IT.json index 87c2f7c3..efdacda3 100644 --- a/lang/it-IT.json +++ b/lang/it-IT.json @@ -40,9 +40,11 @@ "label.edit": "Modifica", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Abilita URL di condivisione", + "label.event-data": "Event data", "label.events": "Eventi", "label.filter-combined": "Aggregati", "label.filter-raw": "Raw", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Lingua", @@ -68,6 +70,7 @@ "label.powered-by": "Powered by {name}", "label.profile": "Profilo", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Tempo reale", "label.referrers": "Referrers", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Obbligatorio", "label.reset": "Reset", "label.reset-website": "Resetta le statistiche", "label.role": "Role", "label.save": "Salva", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Impostazioni", @@ -104,6 +109,7 @@ "label.tracking-code": "Codice di tracking", "label.unique-visitors": "Visitatori unici", "label.unknown": "Sconosciuto", + "label.urls": "URLs", "label.user": "User", "label.username": "Nome utente", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "Vedi dettagli", "label.views": "Visualizzazioni", "label.visitors": "Visitatori", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Siti web", "label.yesterday": "Ieri", @@ -118,6 +125,7 @@ "message.confirm-delete": "Sei sicuro di voler eliminare {target}?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Sei sicuro di voler azzerare le statistiche di {target}?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Elimina sito", "message.delete-website-warning": "Saranno eliminati anche tutti i dati associati.", "message.error": "Si è verificato un errore.", @@ -140,6 +148,7 @@ "message.tracking-code": "Codice di tracking", "message.user-deleted": "User deleted.", "message.visitor-log": "Utenti da {country} tramite {browser} su {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "Non hai ancora configurato alcun sito.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/ja-JP.json b/lang/ja-JP.json index 58a326bd..14031177 100644 --- a/lang/ja-JP.json +++ b/lang/ja-JP.json @@ -40,9 +40,11 @@ "label.edit": "編集", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "共有リンクを有効にする", + "label.event-data": "Event data", "label.events": "イベント", "label.filter-combined": "パスまで", "label.filter-raw": "すべて表示", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Language", @@ -68,6 +70,7 @@ "label.powered-by": "このシステムは {name} で実行されています。", "label.profile": "プロファイル", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "リアルタイム", "label.referrers": "リファラー", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "必須", "label.reset": "リセット", "label.reset-website": "Reset statistics", "label.role": "Role", "label.save": "保存", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "設定", @@ -104,6 +109,7 @@ "label.tracking-code": "トラッキングコード", "label.unique-visitors": "ユニーク訪問者数", "label.unknown": "不明", + "label.urls": "URLs", "label.user": "User", "label.username": "ユーザー名", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "詳細を見る", "label.views": "閲覧数", "label.visitors": "訪問者数", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Webサイト", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "{target}を削除してもよろしいですか?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Webサイトの削除", "message.delete-website-warning": "関連するすべてのデータも削除されます。", "message.error": "問題が発生しました。", @@ -140,6 +148,7 @@ "message.tracking-code": "トラッキングコード", "message.user-deleted": "User deleted.", "message.visitor-log": "{os}({device})で{browser}を使用している{country}からの訪問者", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "Webサイトが設定されていません。", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/km-KH.json b/lang/km-KH.json index 4eec564f..edcb25e7 100644 --- a/lang/km-KH.json +++ b/lang/km-KH.json @@ -40,9 +40,11 @@ "label.edit": "កែប្រែ", "label.edit-dashboard": "កែផ្ទាំងគ្រប់គ្រង", "label.enable-share-url": "បើកការចែករំលែក URL", + "label.event-data": "Event data", "label.events": "ព្រឹត្តិការណ៍", "label.filter-combined": "រួមបញ្ចូលគ្នា", "label.filter-raw": "ដើម", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "ភាសា", @@ -68,6 +70,7 @@ "label.powered-by": "ដំណើរការដោយ {name}", "label.profile": "ប្រវត្តិរូប", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "ប៉ារ៉ាម៉ែត្រ Query", "label.realtime": "ឥលូវនេះ", "label.referrers": "អ្នកណែនាំ", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "ទាមទារ", "label.reset": "កំណត់ឡើងវិញ", "label.reset-website": "កំណត់ស្ថិតិឡើងវិញ", "label.role": "Role", "label.save": "រក្សាទុក", "label.screens": "ប្រភេទឧបករណ៍", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "ការកំណត់", @@ -104,6 +109,7 @@ "label.tracking-code": "លេខកូដតាមដាន", "label.unique-visitors": "អ្នកចូលមើលម្នាក់ៗ", "label.unknown": "មិនស្គាល់", + "label.urls": "URLs", "label.user": "User", "label.username": "ឈ្មោះ​អ្នកប្រើប្រាស់", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "មើលព័ត៌មានលម្អិត", "label.views": "អ្នកចូលមើល", "label.visitors": "អ្នកទស្សនា", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "គេហទំព័រ", "label.yesterday": "ម្សិលមិញ", @@ -118,6 +125,7 @@ "message.confirm-delete": "តើអ្នកប្រាកដថាចង់លុប {target} ទេ?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "តើអ្នកប្រាកដថាចង់កំណត់ស្ថិតិរបស់ {target} ឡើងវិញទេ?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "លុបគេហទំព័រ", "message.delete-website-warning": "ទិន្នន័យដែលពាក់ព័ន្ធទាំងអស់នឹងត្រូវបានលុបផងដែរ។", "message.error": "មាន​អ្វីមួយ​មិន​ប្រក្រតី។", @@ -140,6 +148,7 @@ "message.tracking-code": "លេខកូដតាមដាន", "message.user-deleted": "User deleted.", "message.visitor-log": "អ្នកមើលពីប្រទេស {country} ប្រើប្រាស់កម្មវិធី {browser} លើឧបករណ៍ {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "អ្នកមិនទាន់បានដាក់គេហទំព័រណាមួយចូលទេ។", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/ko-KR.json b/lang/ko-KR.json index 633552a7..d927dd77 100644 --- a/lang/ko-KR.json +++ b/lang/ko-KR.json @@ -40,9 +40,11 @@ "label.edit": "편집", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "URL 공유 활성화", + "label.event-data": "Event data", "label.events": "이벤트", "label.filter-combined": "합쳐서 보기", "label.filter-raw": "전체 보기", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Language", @@ -68,6 +70,7 @@ "label.powered-by": "이 시스템은 {name}에서 구동되고 있습니다.", "label.profile": "프로필", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "실시간", "label.referrers": "리퍼러", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "필수", "label.reset": "리셋", "label.reset-website": "Reset statistics", "label.role": "Role", "label.save": "저장", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "설정", @@ -104,6 +109,7 @@ "label.tracking-code": "추적 코드", "label.unique-visitors": "순방문자(UV)", "label.unknown": "알 수 없음", + "label.urls": "URLs", "label.user": "User", "label.username": "사용자명", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "상세보기", "label.views": "조회수", "label.visitors": "방문객", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "웹사이트", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "{target}을(를) 삭제하시겠습니까?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "웹사이트 삭제", "message.delete-website-warning": "관련된 모든 데이터도 삭제됩니다.", "message.error": "오류가 발생하였습니다.", @@ -140,6 +148,7 @@ "message.tracking-code": "추적 코드", "message.user-deleted": "User deleted.", "message.visitor-log": "{os} {device}에서 {browser}을(를) 사용하는 {country}의 방문자", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "구성된 웹 사이트가 없습니다.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/lt-LT.json b/lang/lt-LT.json index 768c32dd..9ccf6202 100644 --- a/lang/lt-LT.json +++ b/lang/lt-LT.json @@ -40,9 +40,11 @@ "label.edit": "Redaguoti", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Įjungti bendrinimą su nuoroda", + "label.event-data": "Event data", "label.events": "Įvykiai", "label.filter-combined": "Kombinuoti", "label.filter-raw": "Neapdoroti", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Language", @@ -68,6 +70,7 @@ "label.powered-by": "Powered by {name}", "label.profile": "Profilis", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Realiuoju laiku", "label.referrers": "Referrers", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Reikalinga", "label.reset": "Atstatyti", "label.reset-website": "Atstatyti statistikos duomenis", "label.role": "Role", "label.save": "Išsaugoti", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Nustatymai", @@ -104,6 +109,7 @@ "label.tracking-code": "Sekimo kodas", "label.unique-visitors": "Unikalūs lankytojai", "label.unknown": "Nežinoma", + "label.urls": "URLs", "label.user": "User", "label.username": "Vartotojo vardas", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "Peržiūrėti detaliau", "label.views": "Peržiūros", "label.visitors": "Lankytojai", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Svetainės", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "Ar esate tikri, jog norite ištrinti svetainę {target}?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Are esate tikri, jog norite atstatyti svetainės {target} statistikos duomenis?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Ištrinti svetainę", "message.delete-website-warning": "Visi susiję duomenys taip pat bus ištrinti.", "message.error": "Kažkas įvyko ne taip.", @@ -140,6 +148,7 @@ "message.tracking-code": "Sekimo kodas", "message.user-deleted": "User deleted.", "message.visitor-log": "Lankytojas iš {country}, naudojantis {browser} sistemoje {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "Jūs nesate susikonfiguravę jokių svetainių.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/mn-MN.json b/lang/mn-MN.json index b15076ad..e9d9fa34 100644 --- a/lang/mn-MN.json +++ b/lang/mn-MN.json @@ -40,9 +40,11 @@ "label.edit": "Засах", "label.edit-dashboard": "Хянах самбар засах", "label.enable-share-url": "Хуваалцах холбоос идэвхжүүлэх", + "label.event-data": "Event data", "label.events": "Үйлдэл", "label.filter-combined": "Нэгтгэсэн", "label.filter-raw": "Түүхий", + "label.funnel": "Funnel", "label.join": "Нэгдэх", "label.join-team": "Багт нэгдэх", "label.language": "Хэл", @@ -68,6 +70,7 @@ "label.powered-by": "{name} дээр суурилсан", "label.profile": "Бүртгэл", "label.queries": "Query-нүүд", + "label.query": "Query", "label.query-parameters": "Query параметр", "label.realtime": "Яг одоо", "label.referrers": "Чиглүүлэгч", @@ -75,12 +78,14 @@ "label.regenerate": "Дахин үүсгэх", "label.regions": "Бүсүүд", "label.remove": "Устгах", + "label.reports": "Reports", "label.required": "Шаардлагатай", "label.reset": "Дахин эхлүүлэх", "label.reset-website": "Тоон үзүүлэлтийг дахин эхлүүлэх", "label.role": "Эрх", "label.save": "Хадгалах", "label.screens": "Дэлгэц", + "label.select-date": "Select date", "label.select-website": "Веб сонгох", "label.sessions": "Sessions", "label.settings": "Тохиргоо", @@ -104,6 +109,7 @@ "label.tracking-code": "Мөрдөх код", "label.unique-visitors": "Зочин", "label.unknown": "Тодорхойгүй", + "label.urls": "URLs", "label.user": "Хэрэглэгч", "label.username": "Хэрэглэгчийн нэр", "label.users": "Хэрэглэгчид", @@ -111,6 +117,7 @@ "label.view-details": "Дэлгэрүүлж харах", "label.views": "Үзсэн", "label.visitors": "Зочин", + "label.website": "Website", "label.website-id": "Вебийн ID", "label.websites": "Вебүүд", "label.yesterday": "Өчигдөр", @@ -118,6 +125,7 @@ "message.confirm-delete": "Та {target}-г устгахдаа итгэлтэй байна уу?", "message.confirm-leave": "Та {target}-с гарахдаа итгэлтэй байна уу?", "message.confirm-reset": "Та {target}-н тоон үзүүлэлтүүдийг устгахдаа итгэлтэй байна уу?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Веб устгахын тулд доорх хэсэгт {confirmation} гэж бичиж, баталгаажуулна уу.", "message.delete-website-warning": "Энэ вебтэй холбоотой бүх өгөгдөл устах болно.", "message.error": "Ямар нэг зүйл буруу боллоо.", @@ -140,6 +148,7 @@ "message.tracking-code": "Энэ вебийн хандалтуудыг мөрдөхийн тулд доорх кодыг HTML-нхээ ... хэсэгт байрлуулна уу.", "message.user-deleted": "Хэрэглэгч устсан.", "message.visitor-log": "{country} улсаас {os} {device} дээр {browser} хөтөч ашиглан орсон", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "Энэ багт ямар ч веб алга.", "messages.no-websites-configured": "Та ямар нэгэн веб тохируулаагүй байна.", "messages.team-websites-info": "Вебийг багийн бүх гишүүд үзэж болно." diff --git a/lang/ms-MY.json b/lang/ms-MY.json index a9469af8..97c37c58 100644 --- a/lang/ms-MY.json +++ b/lang/ms-MY.json @@ -40,9 +40,11 @@ "label.edit": "Edit", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Aktifkan url berkongsi", + "label.event-data": "Event data", "label.events": "Peristiwa", "label.filter-combined": "Digabungkan", "label.filter-raw": "Mentah", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Language", @@ -68,6 +70,7 @@ "label.powered-by": "Disediakan oleh {name}", "label.profile": "Profil", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Siaran langsung", "label.referrers": "Perujuk", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Diperlukan", "label.reset": "Tetapkan semula", "label.reset-website": "Reset statistics", "label.role": "Role", "label.save": "Simpan", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Tetapan", @@ -104,6 +109,7 @@ "label.tracking-code": "Kod penjejakan", "label.unique-visitors": "Pelawat unik", "label.unknown": "Tidak diketahui", + "label.urls": "URLs", "label.user": "User", "label.username": "Nama pengguna", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "Lihat butiran", "label.views": "Lawatan", "label.visitors": "Pelawat", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Laman web", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "Pastikah anda ingin memadam {target}?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Padam laman web", "message.delete-website-warning": "Semua data yang berkaitan juga akan dihapuskan.", "message.error": "Ada yang tidak kena.", @@ -140,6 +148,7 @@ "message.tracking-code": "Kod penjejakan", "message.user-deleted": "User deleted.", "message.visitor-log": "Pelawat dari {country} mengguna {browser} pada {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "Anda tidak ada sebarang laman web yang telah dikonfigurasikan.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/nb-NO.json b/lang/nb-NO.json index 4bb8c74f..84ab9b9f 100644 --- a/lang/nb-NO.json +++ b/lang/nb-NO.json @@ -40,9 +40,11 @@ "label.edit": "Rediger", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Aktiver delings-URL", + "label.event-data": "Event data", "label.events": "Arrangementer", "label.filter-combined": "Kombinert", "label.filter-raw": "Rå", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Språk", @@ -68,6 +70,7 @@ "label.powered-by": "Drevet av {name}", "label.profile": "Profil", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Sanntid", "label.referrers": "Referanser", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Påkrevd", "label.reset": "Nullstill", "label.reset-website": "Nullstill statistikk", "label.role": "Role", "label.save": "Lagre", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Innstillinger", @@ -104,6 +109,7 @@ "label.tracking-code": "Sporingskode", "label.unique-visitors": "Unike besøkende", "label.unknown": "Ukjent", + "label.urls": "URLs", "label.user": "User", "label.username": "Brukernavn", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "Vis detaljer", "label.views": "Visninger", "label.visitors": "Besøkende", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Nettsteder", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "Er du sikker på at du vil slette {target}?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Er du sikker på at du vil nullstille {target}'s statistikk?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Slett nettstedet", "message.delete-website-warning": "Alle tilknyttede data slettes også.", "message.error": "Noe gikk galt.", @@ -140,6 +148,7 @@ "message.tracking-code": "Sporingskode", "message.user-deleted": "User deleted.", "message.visitor-log": "Besøkende fra {country} med {browser} på {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "Du har ikke satt opp noen nettsteder.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/nl-NL.json b/lang/nl-NL.json index b86ff423..9291dace 100644 --- a/lang/nl-NL.json +++ b/lang/nl-NL.json @@ -40,9 +40,11 @@ "label.edit": "Bewerken", "label.edit-dashboard": "Dashboard aanpassen", "label.enable-share-url": "Sta delen via openbare URL toe", + "label.event-data": "Event data", "label.events": "Gebeurtenissen", "label.filter-combined": "Gecombineerd", "label.filter-raw": "Ruw", + "label.funnel": "Funnel", "label.join": "Lid worden", "label.join-team": "Word lid van een team", "label.language": "Taal", @@ -68,6 +70,7 @@ "label.powered-by": "mogelijk gemaakt door {name}", "label.profile": "Profiel", "label.queries": "Parameters", + "label.query": "Query", "label.query-parameters": "URL-parameters", "label.realtime": "Actueel", "label.referrers": "Verwijzers", @@ -75,12 +78,14 @@ "label.regenerate": "Opnieuw genereren", "label.regions": "Regio's", "label.remove": "Verwijderen", + "label.reports": "Reports", "label.required": "Verplicht", "label.reset": "Opnieuw instellen", "label.reset-website": "Statistieken opnieuw instellen", "label.role": "Gebruikersrol", "label.save": "Opslaan", "label.screens": "Schermen", + "label.select-date": "Select date", "label.select-website": "Website selecteren", "label.sessions": "Sessies", "label.settings": "Instellingen", @@ -104,6 +109,7 @@ "label.tracking-code": "Volgcode", "label.unique-visitors": "Unieke bezoekers", "label.unknown": "Onbekend", + "label.urls": "URLs", "label.user": "Gebruiker", "label.username": "Gebruikersnaam", "label.users": "Gebruikers", @@ -111,6 +117,7 @@ "label.view-details": "Meer details", "label.views": "Weergaven", "label.visitors": "Bezoekers", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Websites", "label.yesterday": "Gisteren", @@ -118,6 +125,7 @@ "message.confirm-delete": "Weet je zeker dat je {target} wilt verwijderen?", "message.confirm-leave": "Weet je zeker dat je {target} wilt verlaten?", "message.confirm-reset": "Weet je zeker dat je de statistieken van {target} opnieuw wilt instellen?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Website verwijderen", "message.delete-website-warning": "Alle verwante gegezens zullen ook verwijderd worden.", "message.error": "Er is iets misgegaan.", @@ -140,6 +148,7 @@ "message.tracking-code": "Volgcode", "message.user-deleted": "Gebruiker verwijderd.", "message.visitor-log": "Bezoeker uit {country} met {browser} op een {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "Er zijn geen websites gekoppeld aan dit team.", "messages.no-websites-configured": "Je hebt geen websites ingesteld.", "messages.team-websites-info": "Websites kunnen door iedereen in het team worden bekeken." diff --git a/lang/pl-PL.json b/lang/pl-PL.json index 919fd9b3..899ad891 100644 --- a/lang/pl-PL.json +++ b/lang/pl-PL.json @@ -40,9 +40,11 @@ "label.edit": "Edytuj", "label.edit-dashboard": "Edytuj panel", "label.enable-share-url": "Włącz udostępnianie adresu URL", + "label.event-data": "Event data", "label.events": "Zdarzenia", "label.filter-combined": "Połączone", "label.filter-raw": "Surowe dane", + "label.funnel": "Funnel", "label.join": "Dołącz", "label.join-team": "Dołącz do zespołu", "label.language": "Język", @@ -68,6 +70,7 @@ "label.powered-by": "Obsługiwane przez {name}", "label.profile": "Profil", "label.queries": "Zapytania", + "label.query": "Query", "label.query-parameters": "Parametry query", "label.realtime": "Czas rzeczywisty", "label.referrers": "Źródła odsyłające", @@ -75,12 +78,14 @@ "label.regenerate": "Wygeneruj ponownie", "label.regions": "Regiony", "label.remove": "Usuń", + "label.reports": "Reports", "label.required": "Wymagany", "label.reset": "Zresetuj", "label.reset-website": "Zresetuj statystyki", "label.role": "Role", "label.save": "Zapisz", "label.screens": "Ekrany", + "label.select-date": "Select date", "label.select-website": "Wybierz witrynę", "label.sessions": "Sesje", "label.settings": "Ustawienia", @@ -104,6 +109,7 @@ "label.tracking-code": "Kod śledzenia", "label.unique-visitors": "Unikalni odwiedzający", "label.unknown": "Nieznany", + "label.urls": "URLs", "label.user": "Użytkownik", "label.username": "Nazwa użytkownika", "label.users": "Użytkownicy", @@ -111,6 +117,7 @@ "label.view-details": "Pokaż szczegóły", "label.views": "Wyświetlenia", "label.visitors": "Odwiedzający", + "label.website": "Website", "label.website-id": "ID witryny", "label.websites": "Witryny", "label.yesterday": "Wczoraj", @@ -118,6 +125,7 @@ "message.confirm-delete": "Czy na pewno chcesz usunąć {target}?", "message.confirm-leave": "Czy na pewno chcesz opuścić {target}?", "message.confirm-reset": "Czy na pewno chcesz zresetować statystyki {target}?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Usuń witrynę", "message.delete-website-warning": "Wszystkie powiązane dane również zostaną usunięte.", "message.error": "Coś poszło nie tak.", @@ -140,6 +148,7 @@ "message.tracking-code": "Kod śledzenia", "message.user-deleted": "Użytkownik usunięty.", "message.visitor-log": "Odwiedzający z {country} używa {browser} na {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "Ten zespół nie ma żadnych witryn internetowych.", "messages.no-websites-configured": "Nie masz skonfigurowanych żadnych witryn internetowych.", "messages.team-websites-info": "Strony internetowe mogą być przeglądane przez każdego członka zespołu." diff --git a/lang/pt-BR.json b/lang/pt-BR.json index 590cb33c..0835544f 100644 --- a/lang/pt-BR.json +++ b/lang/pt-BR.json @@ -40,9 +40,11 @@ "label.edit": "Editar", "label.edit-dashboard": "Editar painel", "label.enable-share-url": "Ativar link de compartilhamento", + "label.event-data": "Event data", "label.events": "Eventos", "label.filter-combined": "Combinado", "label.filter-raw": "Dados brutos", + "label.funnel": "Funnel", "label.join": "Entrar", "label.join-team": "Entrar no time", "label.language": "Idioma", @@ -68,6 +70,7 @@ "label.powered-by": "Distribuído por {name}", "label.profile": "Perfil", "label.queries": "Parâmetros", + "label.query": "Query", "label.query-parameters": "Parâmetros de Consulta", "label.realtime": "Tempo real", "label.referrers": "Referências", @@ -75,12 +78,14 @@ "label.regenerate": "Regerar", "label.regions": "Regiões", "label.remove": "Remover", + "label.reports": "Reports", "label.required": "Obrigatório", "label.reset": "Redefinir", "label.reset-website": "Redefinir estatísticas", "label.role": "Papel", "label.save": "Salvar", "label.screens": "Telas", + "label.select-date": "Select date", "label.select-website": "Selecionar site", "label.sessions": "Sessões", "label.settings": "Configurações", @@ -104,6 +109,7 @@ "label.tracking-code": "Código de rastreamento", "label.unique-visitors": "Visitantes únicos", "label.unknown": "Desconhecido", + "label.urls": "URLs", "label.user": "Usuário", "label.username": "Nome de usuário", "label.users": "Usuários", @@ -111,6 +117,7 @@ "label.view-details": "Ver detalhes", "label.views": "Visualizações", "label.visitors": "Visitantes", + "label.website": "Website", "label.website-id": "ID do Site", "label.websites": "Sites", "label.yesterday": "Ontem", @@ -118,6 +125,7 @@ "message.confirm-delete": "Deseja realmente remover {target}?", "message.confirm-leave": "Você tem certeza que deseja sair de {target}?", "message.confirm-reset": "Você tem certeza que deseja redefinir as estatísticas de {target}?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Remover site", "message.delete-website-warning": "Todos os dados associados também serão eliminados.", "message.error": "Ocorreu um erro.", @@ -140,6 +148,7 @@ "message.tracking-code": "Código de rastreamento", "message.user-deleted": "Usuário removido.", "message.visitor-log": "Visitante de {country} usando {browser} no {device} {os}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "Este time não possui nenhum site.", "messages.no-websites-configured": "Nenhum site foi configurado ainda.", "messages.team-websites-info": "Os sites podem ser visualizados por qualquer membro da equipe." diff --git a/lang/pt-PT.json b/lang/pt-PT.json index e67479e5..e39c89cc 100644 --- a/lang/pt-PT.json +++ b/lang/pt-PT.json @@ -40,9 +40,11 @@ "label.edit": "Editar", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Ativar link de partilha", + "label.event-data": "Event data", "label.events": "Eventos", "label.filter-combined": "Combinado", "label.filter-raw": "Dados brutos", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Língua", @@ -68,6 +70,7 @@ "label.powered-by": "Distribuído por {name}", "label.profile": "Perfil", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Tempo real", "label.referrers": "Referenciadores", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Obrigatório", "label.reset": "Repor", "label.reset-website": "Repor estatísticas", "label.role": "Role", "label.save": "Guardar", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Definições", @@ -104,6 +109,7 @@ "label.tracking-code": "Código de rastreamento", "label.unique-visitors": "Visitantes únicos", "label.unknown": "Desconhecido", + "label.urls": "URLs", "label.user": "User", "label.username": "Nome de utilizador", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "Ver detalhes", "label.views": "Visualizações", "label.visitors": "Visitantes", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Websites", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "Tem a certeza que pretende eliminar {target}?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Tem a certeza que pretende restaurar as estatísticas de {target}?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Eliminar website", "message.delete-website-warning": "Todos os dados associados também serão eliminados.", "message.error": "Ocorreu um erro.", @@ -140,6 +148,7 @@ "message.tracking-code": "Código de rastreamento", "message.user-deleted": "User deleted.", "message.visitor-log": "Visitante de {country} a usar {browser} no {device} {os}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "Não tens nenhum website configurado.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/ro-RO.json b/lang/ro-RO.json index deefd899..22d93713 100644 --- a/lang/ro-RO.json +++ b/lang/ro-RO.json @@ -40,9 +40,11 @@ "label.edit": "Editare", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Activare adresă URL de distribuire", + "label.event-data": "Event data", "label.events": "Evenimente", "label.filter-combined": "Combinat", "label.filter-raw": "Brut", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Language", @@ -68,6 +70,7 @@ "label.powered-by": "Cu sprijinul {name}", "label.profile": "Profil", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Realtime", "label.referrers": "Site-uri de proveniență", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Obligatoriu", "label.reset": "Resetează", "label.reset-website": "Resetează statisticile pentru site", "label.role": "Role", "label.save": "Salvează", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Setări", @@ -104,6 +109,7 @@ "label.tracking-code": "Cod de urmărire", "label.unique-visitors": "Vizitatori unici", "label.unknown": "Necunoscut", + "label.urls": "URLs", "label.user": "User", "label.username": "Nume utilizator", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "Vizualizare detalii", "label.views": "Vizualizări", "label.visitors": "Vizitatori", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Site-uri web", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "Sunteți sigur că doriți să ștergeți {target}?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Sunteți sigur că doriți să resetați statisticile pentru {target}?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Ștergere site web", "message.delete-website-warning": "Toate datele asociate vor fi șterse, de asemenea.", "message.error": "Ceva n-a mers bine.", @@ -140,6 +148,7 @@ "message.tracking-code": "Cod de urmărire", "message.user-deleted": "User deleted.", "message.visitor-log": "Vizitator din {country} folosind {browser} pe {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "Nu aveți niciun site web configurat.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/ru-RU.json b/lang/ru-RU.json index c5093c8c..475d9598 100644 --- a/lang/ru-RU.json +++ b/lang/ru-RU.json @@ -40,9 +40,11 @@ "label.edit": "Редактировать", "label.edit-dashboard": "Редактировать дашборд", "label.enable-share-url": "Разрешить делиться ссылкой", + "label.event-data": "Event data", "label.events": "События", "label.filter-combined": "Объединенные", "label.filter-raw": "Сырые данные", + "label.funnel": "Funnel", "label.join": "Присоединиться", "label.join-team": "Присоединиться к команде", "label.language": "Язык", @@ -68,6 +70,7 @@ "label.powered-by": "На движке {name}", "label.profile": "Профиль", "label.queries": "Запросы", + "label.query": "Query", "label.query-parameters": "Параметры запроса", "label.realtime": "Реальное время", "label.referrers": "Источники", @@ -75,12 +78,14 @@ "label.regenerate": "Обновить", "label.regions": "Регионы", "label.remove": "Удалить", + "label.reports": "Reports", "label.required": "Обязательное", "label.reset": "Сбросить", "label.reset-website": "Сбросить статистику", "label.role": "Роль", "label.save": "Сохранить", "label.screens": "Экраны", + "label.select-date": "Select date", "label.select-website": "Выбрать сайт", "label.sessions": "Сессии", "label.settings": "Настройки", @@ -104,6 +109,7 @@ "label.tracking-code": "Код отслеживания", "label.unique-visitors": "Уникальные посетители", "label.unknown": "Неизвестно", + "label.urls": "URLs", "label.user": "Пользователь", "label.username": "Имя пользователя", "label.users": "Пользователи", @@ -111,6 +117,7 @@ "label.view-details": "Посмотреть детали", "label.views": "Просмотры", "label.visitors": "Посетители", + "label.website": "Website", "label.website-id": "ID сайта", "label.websites": "Сайты", "label.yesterday": "Вчера", @@ -118,6 +125,7 @@ "message.confirm-delete": "Вы уверены, что хотите удалить {target}?", "message.confirm-leave": "Вы уверены, что хотите уйти {target}?", "message.confirm-reset": "Вы уверены, что хотите сбросить статистику {target}?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Удалить сайт", "message.delete-website-warning": "Все связанные данные будут также удалены.", "message.error": "Что-то пошло не так.", @@ -140,6 +148,7 @@ "message.tracking-code": "Код отслеживания", "message.user-deleted": "Пользователь удален.", "message.visitor-log": "Посетитель из {country} используя {browser} на {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "У этой команды нет ни одного сайта.", "messages.no-websites-configured": "У вас нет настроенных сайтов.", "messages.team-websites-info": "Сайты могут просматривать все члены команды." diff --git a/lang/si-LK.json b/lang/si-LK.json index 33346087..c592790d 100644 --- a/lang/si-LK.json +++ b/lang/si-LK.json @@ -1,120 +1,155 @@ { - "label.accounts": "ගිණුම්", - "label.add-account": "ගිණුම එකතු කරන්න", - "label.add-column": "තීරුව එක් කරන්න", - "label.add-filter": "පෙරහන එකතු කරන්න", + "label.access-code": "Access code", + "label.actions": "Actions", + "label.activity-log": "Activity log", "label.add-website": "වෙබ් අඩවිය එක් කරන්න", - "label.administrator": "පරිපාලක", + "label.admin": "Administrator", "label.all": "සියල්ල", "label.all-time": "හැම වෙලාවෙම", - "label.all-websites": "සියලුම වෙබ් අඩවි", + "label.analytics": "Analytics", + "label.average-visit-time": "Average visit time", "label.back": "ආපසු", + "label.bounce-rate": "Bounce rate", + "label.browsers": "Browsers", "label.cancel": "අවලංගු කරන්න", "label.change-password": "මුරපදය වෙනස් කරන්න", + "label.cities": "Cities", + "label.clear-all": "Clear all", + "label.confirm": "Confirm", "label.confirm-password": "මුරපදය සත්‍යාපනය කරන්න", - "label.copy-to-clipboard": "පසුරු පුවරුවට පිටපත් කරන්න", + "label.continue": "Continue", + "label.countries": "Countries", + "label.create-team": "Create team", + "label.create-user": "Create user", + "label.created": "Created", "label.current-password": "වත්මන් මුරපදය", "label.custom-range": "අභිරුචි පරාසය", "label.dashboard": "උපකරණ පුවරුව", + "label.data": "Data", "label.date-range": "දින පරාසය", "label.default-date-range": "පෙරනිමි දින පරාසය", "label.delete": "මකන්න", - "label.delete-account": "ගිණුම මකන්න", + "label.delete-team": "Delete team", + "label.delete-user": "Delete user", "label.delete-website": "වෙබ් අඩවිය මකන්න", + "label.desktop": "Desktop", + "label.details": "Details", + "label.devices": "Devices", "label.dismiss": "මගහරින්න", "label.domain": "වසම", "label.edit": "සංස්කරණය කරන්න", - "label.edit-account": "ගිණුම සංස්කරණය කරන්න", - "label.edit-website": "වෙබ් අඩවිය සංස්කරණය කරන්න", + "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "බෙදාගැනීමේ URL සබල කරන්න", "label.event-data": "සිදුවීම් දත්ත", - "label.field-name": "ක්ෂේත්‍ර නාම", - "label.invalid": "වලංගු නැත", - "label.invalid-domain": "වලංගු නොවන වසමක්", + "label.events": "Events", + "label.filter-combined": "Combined", + "label.filter-raw": "Raw", + "label.funnel": "Funnel", + "label.join": "Join", + "label.join-team": "Join team", "label.language": "භාෂාව", + "label.languages": "Languages", + "label.laptop": "Laptop", "label.last-days": "අන්තිම {x} දින", "label.last-hours": "අන්තිම {x} පැය", - "label.logged-in-as": "ලොග් වී ඇත්තේ {username}", + "label.leave": "Leave", + "label.leave-team": "Leave team", "label.login": "ලොග් වෙන්න", "label.logout": "පිටවීම", + "label.members": "Members", + "label.mobile": "Mobile", "label.more": "තවත්", "label.name": "නම", "label.new-password": "අලුත් මුරපදය", "label.none": "කිසිවක් නැත", + "label.operating-systems": "Operating systems", "label.owner": "හිමිකරු", + "label.page-views": "Page views", + "label.pages": "Pages", "label.password": "මුරපදය", - "label.passwords-dont-match": "මුරපද නොගැලපේ", + "label.powered-by": "Powered by {name}", "label.profile": "පැතිකඩ", + "label.queries": "Queries", + "label.query": "Query", + "label.query-parameters": "Query parameters", "label.realtime": "තත්ය කාල", - "label.realtime-logs": "තත්‍ය කාලීන ලොග්", + "label.referrers": "Referrers", "label.refresh": "නැවුම් කරන්න", + "label.regenerate": "Regenerate", + "label.regions": "Regions", + "label.remove": "Remove", + "label.reports": "Reports", "label.required": "අවශ්‍යයි", "label.reset": "යළි පිහිටුවන්න", "label.reset-website": "සංඛ්යා ලේඛන නැවත සකසන්න", + "label.role": "Role", "label.save": "සුරකින්න", - "label.search": "සෙවීම", + "label.screens": "Screens", + "label.select-date": "Select date", + "label.select-website": "Select website", + "label.sessions": "Sessions", "label.settings": "සැකසුම්", "label.share-url": "බෙදාගැනීමේ URL", "label.single-day": "තනි දවස", + "label.tablet": "Tablet", + "label.team": "Team", + "label.team-guest": "Team guest", + "label.team-id": "Team ID", + "label.team-member": "Team member", + "label.team-owner": "Team owner", + "label.teams": "Teams", "label.theme": "තේමාව", "label.this-month": "මෙ මාසය", "label.this-week": "මේ සතිය", "label.this-year": "මේ අවුරුද්ද", "label.timezone": "වේලා කලාපය", + "label.title": "Title", "label.today": "අද", + "label.toggle-charts": "Toggle charts", "label.tracking-code": "ලුහුබැඳීමේ කේතය", - "label.type": "වර්ගය", + "label.unique-visitors": "Unique visitors", "label.unknown": "නොදනී", + "label.urls": "URLs", + "label.user": "User", "label.username": "පරිශීලක නාමය", - "label.value": "වටිනාකම", + "label.users": "Users", + "label.view": "View", "label.view-details": "තොරතුරු පෙන්වන්න", + "label.views": "Views", + "label.visitors": "Visitors", + "label.website": "Website", + "label.website-id": "Website ID", "label.websites": "වෙබ් අඩවි", "label.yesterday": "ඊයේ", "message.active-users": "{x} දැන් {x, plural, one {අමුත්තා} other {අමුත්තන්}}", "message.confirm-delete": "{target} මකා දැමීම ගැන විශ්වාසද?", + "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "{target} ට අදාල සංඛ්‍යාලේඛන නැවත පිහිටුවීමට අවශ්‍යද?", - "message.copied": "පිටපත් කරගත්තා!", - "message.delete-warning": "සියලුම ආශ්‍රිත දත්ත ද මකා දැමෙනු ඇත.", - "message.edit-dashboard": "උපකරණ පුවරුව සංස්කරණය කරන්න", - "message.failure": "යම් ගැටලුවක් මතු වී ඇත.", - "message.get-share-url": "බෙදාගැනීමේ URL ලබා ගන්න", - "message.get-tracking-code": "ලුහුබැඳීමේ කේතය ලබා ගන්න", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", + "message.delete-website": "To delete this website, type {confirmation} in the box below to confirm.", + "message.delete-website-warning": "All website data will be deleted.", + "message.error": "Something went wrong.", + "message.event-log": "{event} on {url}", "message.go-to-settings": "සැකසීම් වෙත යන්න", "message.incorrect-username-password": "වැරදි පරිශීලක නාමය/මුරපදය.", - "message.log.visitor": "{country} වලින් පැමිණි අමුත්තකු {device} එකේ, මේ {os} එකේ, මේ {browser} එකෙන් ඉන්නවා", - "message.new-version-available": "umami අලුත්ම {version} වන අනුවාදය නිකුත් උනා!", + "message.invalid-domain": "Invalid domain. Do not include http/https.", + "message.min-password-length": "Minimum length of {n} characters", "message.no-data-available": "පෙන්වීමට දත්ත නොමැත.", - "message.no-websites-configured": "ඔබට වින්‍යාස කර ඇති වෙබ් අඩවි කිසිවක් නොමැත.", + "message.no-match-password": "Passwords do not match.", + "message.no-teams": "You have not created any teams.", + "message.no-users": "There are no users.", "message.page-not-found": "පිටුව හමු නොවීය.", - "message.powered-by": "බල ගැන්වුයේ {name}", - "message.reset-warning": "සියලුම සංඛ්‍යාලේඛන මකා දමනු ඇත. නමුත් ඔබගේ නිරීක්ෂණ කේතය නොවෙනස්ව පවතිනු ඇත.", - "message.save-success": "සාර්තකව සුරැකිණි.", + "message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.", + "message.reset-website-warning": "All statistics for this website will be deleted, but your settings will remain intact.", + "message.saved": "Saved.", "message.share-url": "මේ {target} සඳහා ප්‍රසිද්ධියේ බෙදාගත් URL එකයි.", - "message.toggle-charts": "ප්‍රස්ථාර ටොගල් කරන්න", - "message.track-stats": "{target} හි සංඛ්යාලේඛන බැලීම සදහා, පහත කේතය {head} කොටසට ඇතුලත් කරන්න.", - "message.type-delete": "සත්‍යාපනය සදහා {delete} ලෙස පහල කොටුවේ ටයිප් කරන්න", - "message.type-reset": "සත්‍යාපනය සදහා {reset} ලෙස පහල කොටුවේ ටයිප් කරන්න", - "metrics.actions": "ක්රියාවන්", - "metrics.average-visit-time": "සාමාන්‍ය සංචාර කාලය", - "metrics.bounce-rate": "හැරී යන ප්‍රමාණය", - "metrics.browsers": "බ්‍රව්සර්", - "metrics.countries": "රටවල්", - "metrics.device.desktop": "ඩෙස්ක්ටොප්", - "metrics.device.laptop": "ලැප්ටොප්", - "metrics.device.mobile": "ජංගම", - "metrics.device.tablet": "ටැබ්ලට්", - "metrics.devices": "උපකරණ", - "metrics.events": "සිද්ධීන්", - "metrics.filter.combined": "ඒකාබද්ධ", - "metrics.filter.raw": "අමු", - "metrics.languages": "භාෂා", - "metrics.operating-systems": "මෙහෙයුම් පද්ධති", - "metrics.page-views": "පිටු බැලීම්", - "metrics.pages": "පිටු", - "metrics.query-parameters": "විමසුම් පරාමිතීන්", - "metrics.referrers": "යොමු කරන්නන්", - "metrics.screens": "තිර", - "metrics.unique-visitors": "අලුත්ම අමුත්තන්", - "metrics.views": "බැලූ ගණන", - "metrics.visitors": "අමුත්තන්" + "message.team-already-member": "You are already a member of the team.", + "message.team-not-found": "Team not found.", + "message.tracking-code": "To track stats for this website, place the following code in the ... section of your HTML.", + "message.user-deleted": "User deleted.", + "message.visitor-log": "Visitor from {country} using {browser} on {os} {device}", + "messages.no-results-found": "No results were found.", + "messages.no-team-websites": "This team does not have any websites.", + "messages.no-websites-configured": "You do not have any websites configured.", + "messages.team-websites-info": "Websites can be viewed by anyone on the team." } diff --git a/lang/sk-SK.json b/lang/sk-SK.json index 2d05cd2e..57948263 100644 --- a/lang/sk-SK.json +++ b/lang/sk-SK.json @@ -40,9 +40,11 @@ "label.edit": "Upraviť", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Povoliť zdielanie URL", + "label.event-data": "Event data", "label.events": "Udalosti", "label.filter-combined": "Kombinácie", "label.filter-raw": "Nezpracované", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Language", @@ -68,6 +70,7 @@ "label.powered-by": "Powered by {name}", "label.profile": "Profil", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Aktuálne", "label.referrers": "Odkazy", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Povinné", "label.reset": "Reset", "label.reset-website": "Reset statistics", "label.role": "Role", "label.save": "Uložiť", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Nastavenia", @@ -104,6 +109,7 @@ "label.tracking-code": "Sledovací kód", "label.unique-visitors": "Jedinečné návštevy", "label.unknown": "Neznámý", + "label.urls": "URLs", "label.user": "User", "label.username": "Užívateľské meno", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "Zobraziť detaily", "label.views": "Zobrazení", "label.visitors": "Návštevy", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Weby", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "Naozaj zmazať {target}?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Zmazať web", "message.delete-website-warning": "Všetky príbuzné data budu tiež zmazané.", "message.error": "Niečo sa pokazilo.", @@ -140,6 +148,7 @@ "message.tracking-code": "Sledovací kód", "message.user-deleted": "User deleted.", "message.visitor-log": "Návštevník z {country} s prehliadačom {browser} na {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "Nemáte nastavený žiadny web.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/sl-SI.json b/lang/sl-SI.json index 459e5e7f..b27b2065 100644 --- a/lang/sl-SI.json +++ b/lang/sl-SI.json @@ -40,9 +40,11 @@ "label.edit": "Uredi", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Omogoči URL za skupno rabo", + "label.event-data": "Event data", "label.events": "Dogodki", "label.filter-combined": "Skupno", "label.filter-raw": "Neobdelane meritve", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Language", @@ -68,6 +70,7 @@ "label.powered-by": "Zagotavlja {name}", "label.profile": "Profil", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "V realnem času", "label.referrers": "Viri", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Zahtevano", "label.reset": "Ponastavi", "label.reset-website": "Reset statistics", "label.role": "Role", "label.save": "Shrani", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Nastavitve", @@ -104,6 +109,7 @@ "label.tracking-code": "Koda za sledenje", "label.unique-visitors": "Unikatni obiskovalci", "label.unknown": "Neznano", + "label.urls": "URLs", "label.user": "User", "label.username": "Uporabniško ime", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "Prikaži podrobnosti", "label.views": "Ogledi", "label.visitors": "Obiskovalci", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Spletna mesta", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "Ste prepričani, da želite izbrisati {target}?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Izbriši spletno mesto", "message.delete-website-warning": "Izbrisani bodo tudi vsi povezani podatki.", "message.error": "Prišlo je do napake.", @@ -140,6 +148,7 @@ "message.tracking-code": "Koda za sledenje", "message.user-deleted": "User deleted.", "message.visitor-log": "Obiskovalec iz {country} uporablja {browser} na {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "Ni nastavljenih spletnih mest.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/sv-SE.json b/lang/sv-SE.json index 16a8d62a..c3155fc8 100644 --- a/lang/sv-SE.json +++ b/lang/sv-SE.json @@ -40,9 +40,11 @@ "label.edit": "Redigera", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Aktivera delnings-URL", + "label.event-data": "Event data", "label.events": "Händelser", "label.filter-combined": "Kombinerade", "label.filter-raw": "Rådata", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Språk", @@ -68,6 +70,7 @@ "label.powered-by": "Drivs av {name}", "label.profile": "Profil", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Realtid", "label.referrers": "Hänvisare", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Krävs", "label.reset": "Återställ", "label.reset-website": "Återställ statistik", "label.role": "Role", "label.save": "Spara", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Inställningar", @@ -104,6 +109,7 @@ "label.tracking-code": "Spårningskod", "label.unique-visitors": "Unika besökare", "label.unknown": "Okänd", + "label.urls": "URLs", "label.user": "User", "label.username": "Användarnamn", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "Visa detaljer", "label.views": "Visningar", "label.visitors": "Besökare", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Webbsajt", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "Är du säker på att du vill radera {target}?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Är du säker på att du vill återställa statistiken för {target}?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Radera webbsajt", "message.delete-website-warning": "All tillhörande data kommer också raderas.", "message.error": "Något gick fel.", @@ -140,6 +148,7 @@ "message.tracking-code": "Spårningskod", "message.user-deleted": "User deleted.", "message.visitor-log": "Besökare från {country} med {browser} på {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "Du har inga webbsajter.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/ta-IN.json b/lang/ta-IN.json index e4fc9f7e..2d7efa16 100644 --- a/lang/ta-IN.json +++ b/lang/ta-IN.json @@ -40,9 +40,11 @@ "label.edit": "திருத்துதல்", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "கள முகவரியை பகிரலாம்", + "label.event-data": "Event data", "label.events": "நிகழ்வுகள்", "label.filter-combined": "ஒருங்கிணைந்த", "label.filter-raw": "மூல", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Language", @@ -68,6 +70,7 @@ "label.powered-by": "{name} ஆல் இயக்கப்படுகிறது", "label.profile": "சுயவிவரம்", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "தற்போதைய", "label.referrers": "குறிப்பிடுவோர்", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "தேவையானவை", "label.reset": "மீட்டமை", "label.reset-website": "Reset statistics", "label.role": "Role", "label.save": "சேமி", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "அமைப்புகள்", @@ -104,6 +109,7 @@ "label.tracking-code": "கண்காணிப்பு குறியீடு", "label.unique-visitors": "தனிப்பட்ட பார்வையாளர்கள்", "label.unknown": "தெரியாத", + "label.urls": "URLs", "label.user": "User", "label.username": "பயனர்பெயர்", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "விபரங்களை பார்", "label.views": "பார்வைகள்", "label.visitors": "பார்வையாளர்கள்", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "வலைத்தளங்கள்", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "நீங்கள் நிச்சயமாக {target} நீக்க விரும்புகிறீர்களா?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "வலைத்தளத்தை நீக்கு", "message.delete-website-warning": "தொடர்புடைய எல்லா தரவும் நீக்கப்படும்.", "message.error": "ஏதோ தவறு நடந்துவிட்டது.", @@ -140,6 +148,7 @@ "message.tracking-code": "கண்காணிப்பு குறியீடு", "message.user-deleted": "User deleted.", "message.visitor-log": "{country}வில் இருந்து பார்வையாளர் {browser} ஐ {os} {device}லில் பயன்படுத்துகிறார்", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "உங்களிடம் எந்த வலைத்தளங்களும் கட்டமைக்கப்படவில்லை.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/th-TH.json b/lang/th-TH.json index d71f4633..4f12f420 100644 --- a/lang/th-TH.json +++ b/lang/th-TH.json @@ -40,9 +40,11 @@ "label.edit": "แก้ไข", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "เปิดใช้งานการแชร์ลิงก์", + "label.event-data": "Event data", "label.events": "เหตุการณ์", "label.filter-combined": "ข้อมูลรวม", "label.filter-raw": "ข้อมูลดิบ", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "ภาษา", @@ -68,6 +70,7 @@ "label.powered-by": "ขับเคลื่อนโดย {name}", "label.profile": "โปรไฟล์", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "เรียลไทม์", "label.referrers": "แหล่งที่มา", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "ต้องการ", "label.reset": "รีเซต", "label.reset-website": "รีเซตข้อมูลสถิติ", "label.role": "Role", "label.save": "บันทึก", "label.screens": "ขนาดหน้าจอ", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "ตั้งค่า", @@ -104,6 +109,7 @@ "label.tracking-code": "โค้ดสำหรับใช้ติดตาม", "label.unique-visitors": "ผู้เข้าชม", "label.unknown": "ไม่รู้จัก", + "label.urls": "URLs", "label.user": "User", "label.username": "ชื่อผู้ใช้", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "แสดงรายละเอียด", "label.views": "การเข้าชม", "label.visitors": "ผู้เข้าชม", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "เว็บไซต์", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "คุณแน่ใจหรือไม่ว่าต้องการลบ {target} ?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "คุณแน่ใจหรือไม่ว่าต้องการรีเซตข้อมูลสถิติของ {target} ?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "ลบเว็บไซต์", "message.delete-website-warning": "ข้อมูลที่เกี่ยวข้องทั้งหมดจะถูกลบ.", "message.error": "เกิดข้อผิดพลาด.", @@ -140,6 +148,7 @@ "message.tracking-code": "โค้ดสำหรับใช้ติดตาม", "message.user-deleted": "User deleted.", "message.visitor-log": "ผู้เข้าชมจาก {country} กำลังใช้งานผ่าน {browser} บน {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "คุณยังไม่ได้ตั้งค่าเว็บไซต์ใด ๆ ไว้.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/tr-TR.json b/lang/tr-TR.json index 97556349..47ed7ab3 100644 --- a/lang/tr-TR.json +++ b/lang/tr-TR.json @@ -40,9 +40,11 @@ "label.edit": "Düzenle", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Anonim paylaşım URL'i aktif", + "label.event-data": "Event data", "label.events": "Olaylar", "label.filter-combined": "Birleşik", "label.filter-raw": "Ham", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Language", @@ -68,6 +70,7 @@ "label.powered-by": "Sağlayıcı: {name}", "label.profile": "Profil", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Gerçek Zamanlı", "label.referrers": "Yönlendirenler", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Zorunlu alan", "label.reset": "Sıfırla", "label.reset-website": "Reset statistics", "label.role": "Role", "label.save": "Kaydet", "label.screens": "Ekranlar", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Ayarlar", @@ -104,6 +109,7 @@ "label.tracking-code": "İzleme kodu", "label.unique-visitors": "Tekil kullanıcı", "label.unknown": "Bilinmeyen", + "label.urls": "URLs", "label.user": "User", "label.username": "Kullanıcı adı", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "Detayı incele", "label.views": "Görüntüleme", "label.visitors": "Ziyaretçi", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Web siteleri", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "{target} kaydını silmek istediğinizden emin misiniz?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Web sitesini sil", "message.delete-website-warning": "İlişkili tüm veriler de silinecektir.", "message.error": "Bir şeyler ters gitti!", @@ -140,6 +148,7 @@ "message.tracking-code": "İzleme kodu", "message.user-deleted": "User deleted.", "message.visitor-log": "Yeni ziyaretçi: {country}, {os}, {device}, {browser}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "Henüz hiç web sitesi tanımlamadınız", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/uk-UA.json b/lang/uk-UA.json index 413f2dad..e475c39b 100644 --- a/lang/uk-UA.json +++ b/lang/uk-UA.json @@ -40,9 +40,11 @@ "label.edit": "Редагувати", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Дозволити ділитися посиланням", + "label.event-data": "Event data", "label.events": "Події", "label.filter-combined": "Об'єднані", "label.filter-raw": "Сирі дані", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Language", @@ -68,6 +70,7 @@ "label.powered-by": "На базі {name}", "label.profile": "Профіль", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "У реальному часі", "label.referrers": "Джерела", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Обов'язкове", "label.reset": "Скинути", "label.reset-website": "Скинути статистику сайту", "label.role": "Role", "label.save": "Зберегти", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Налаштування", @@ -104,6 +109,7 @@ "label.tracking-code": "Код для відслідковування", "label.unique-visitors": "Унікальні відвідувачі", "label.unknown": "Невідомо", + "label.urls": "URLs", "label.user": "User", "label.username": "Ім'я користувача", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "Переглянути деталі", "label.views": "Перегляди", "label.visitors": "Відвідувачі", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Сайти", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "Ви впевнені, що бажаєте видалити {target}?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Ви впевнені, що бажаєте скинути статистику для {target}?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Видалити сайт", "message.delete-website-warning": "Усі пов'язані дані будуть видалені також.", "message.error": "Щось пішло не так.", @@ -140,6 +148,7 @@ "message.tracking-code": "Код для відслідковування", "message.user-deleted": "User deleted.", "message.visitor-log": "Відвідувач з {country} використовуючи {browser} на {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "У вас немає налаштованих сайтів.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/ur-PK.json b/lang/ur-PK.json index d206de64..a3cae9e4 100644 --- a/lang/ur-PK.json +++ b/lang/ur-PK.json @@ -40,9 +40,11 @@ "label.edit": "ترمیم", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "شیئر یو آر ایل کو فعال کریں", + "label.event-data": "Event data", "label.events": "واقعات", "label.filter-combined": "مشترکہ", "label.filter-raw": "خام", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Language", @@ -68,6 +70,7 @@ "label.powered-by": "تقویت یافتہ بذریعہ {name}", "label.profile": "پروفائل", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "براہ راست", "label.referrers": "بھیجنے والے", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "درکار ہے", "label.reset": "دوبارہ ترتیب دیں", "label.reset-website": "اعدادوشمار کو دوبارہ ترتیب دیں", "label.role": "Role", "label.save": "محفوظ کریں", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "ترتیبات", @@ -104,6 +109,7 @@ "label.tracking-code": "ٹریکنگ کوڈ", "label.unique-visitors": "منفرد زائرین", "label.unknown": "نامعلوم", + "label.urls": "URLs", "label.user": "User", "label.username": "صارف نام", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "تفصیلات دیکھیں", "label.views": "مناظر", "label.visitors": "زائرین", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "ویب سائٹس", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "کیا آپ واقعی {target} کو حذف کرنا چاہتے ہیں؟", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "کیا آپ واقعی {target} کے اعدادوشمار کو دوبارہ ترتیب دینا چاہتے ہیں؟", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "ویب سائٹ مٹایں", "message.delete-website-warning": "تمام متعلقہ ڈیٹا بھی حذف کر دیا جائے گا۔", "message.error": "کچھ غلط ہو گیا.", @@ -140,6 +148,7 @@ "message.tracking-code": "ٹریکنگ کوڈ", "message.user-deleted": "User deleted.", "message.visitor-log": "{os} {device} پر {browser} کا استعمال کرتے ہوئے {country} سے آنے والا", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "آپ کے پاس کوئی ویب سائٹ کنفیگر نہیں ہے۔", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/vi-VN.json b/lang/vi-VN.json index fe27ce4d..81e507bc 100644 --- a/lang/vi-VN.json +++ b/lang/vi-VN.json @@ -40,9 +40,11 @@ "label.edit": "Chỉnh sửa", "label.edit-dashboard": "Edit dashboard", "label.enable-share-url": "Bật khả năng chia sẻ URL", + "label.event-data": "Event data", "label.events": "Sự kiện", "label.filter-combined": "Kết hợp", "label.filter-raw": "Gốc", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "Language", @@ -68,6 +70,7 @@ "label.powered-by": "Bản quyền thuộc về {name}", "label.profile": "Hồ sơ", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Thời gian thực", "label.referrers": "Liên kết giới thiệu", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "Yêu cầu", "label.reset": "Tái thiết lập", "label.reset-website": "Tái thiết lập thống kê", "label.role": "Role", "label.save": "Lưu", "label.screens": "Screens", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "Cài đặt", @@ -104,6 +109,7 @@ "label.tracking-code": "Mã theo dõi", "label.unique-visitors": "Khách truy cập một lần", "label.unknown": "Không rõ", + "label.urls": "URLs", "label.user": "User", "label.username": "Tên đăng nhập", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "Xem chi tiết", "label.views": "Xem", "label.visitors": "Khách", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "Websites", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "Bạn có chắc chắn muốn xoá {target}?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "Bạn có chắc chắn muốn tái thiết lập thống kê {target}?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "Xóa website", "message.delete-website-warning": "Tất cả các dữ liệu liên quan cũng sẽ bị xoá.", "message.error": "Đã xảy ra lỗi.", @@ -140,6 +148,7 @@ "message.tracking-code": "Mã theo dõi", "message.user-deleted": "User deleted.", "message.visitor-log": "Khách từ {country} đang dùng {browser} trên {os} {device}", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "Bạn chưa có bất cứ website nào.", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/lang/zh-CN.json b/lang/zh-CN.json index 1aba3988..d120ed1a 100644 --- a/lang/zh-CN.json +++ b/lang/zh-CN.json @@ -40,9 +40,11 @@ "label.edit": "编辑", "label.edit-dashboard": "编辑仪表板", "label.enable-share-url": "启用共享链接", + "label.event-data": "Event data", "label.events": "行为类别", "label.filter-combined": "合并", "label.filter-raw": "原始", + "label.funnel": "Funnel", "label.join": "加入", "label.join-team": "加入团队", "label.language": "语言", @@ -68,6 +70,7 @@ "label.powered-by": "由 {name} 提供支持", "label.profile": "个人资料", "label.queries": "查询", + "label.query": "Query", "label.query-parameters": "查询参数", "label.realtime": "实时", "label.referrers": "来源域名", @@ -75,12 +78,14 @@ "label.regenerate": "重新生成", "label.regions": "州/省", "label.remove": "移除", + "label.reports": "Reports", "label.required": "必填", "label.reset": "重置", "label.reset-website": "重置统计数据", "label.role": "角色", "label.save": "保存", "label.screens": "屏幕尺寸", + "label.select-date": "Select date", "label.select-website": "选择网站", "label.sessions": "会话", "label.settings": "设置", @@ -104,6 +109,7 @@ "label.tracking-code": "跟踪代码", "label.unique-visitors": "独立访客", "label.unknown": "未知", + "label.urls": "URLs", "label.user": "用户", "label.username": "用户名", "label.users": "用户", @@ -111,6 +117,7 @@ "label.view-details": "查看更多", "label.views": "浏览量", "label.visitors": "访客", + "label.website": "Website", "label.website-id": "网站 ID", "label.websites": "网站", "label.yesterday": "昨天", @@ -118,6 +125,7 @@ "message.confirm-delete": "你确定要删除 {target} 吗?", "message.confirm-leave": "你确定要离开 {target} 吗?", "message.confirm-reset": "您确定要重置 {target} 的数据吗?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "删除网站", "message.delete-website-warning": "所有相关数据将会被删除。", "message.error": "出现错误。", @@ -140,6 +148,7 @@ "message.tracking-code": "跟踪代码", "message.user-deleted": "User detected.", "message.visitor-log": "来自{country}的访客在搭载 {os} 的{device}上使用 {browser} 浏览器进行访问。", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "这个团队没有任何网站。", "messages.no-websites-configured": "你还没有设置任何网站。", "messages.team-websites-info": "团队中的任何人都可查看网站。" diff --git a/lang/zh-TW.json b/lang/zh-TW.json index 42370059..d15136a8 100644 --- a/lang/zh-TW.json +++ b/lang/zh-TW.json @@ -40,9 +40,11 @@ "label.edit": "編輯", "label.edit-dashboard": "編輯管理面板", "label.enable-share-url": "啟用分享連結", + "label.event-data": "Event data", "label.events": "行為類別", "label.filter-combined": "總和", "label.filter-raw": "原始", + "label.funnel": "Funnel", "label.join": "Join", "label.join-team": "Join team", "label.language": "語言", @@ -68,6 +70,7 @@ "label.powered-by": "運行 {name}", "label.profile": "個人資料", "label.queries": "Queries", + "label.query": "Query", "label.query-parameters": "查詢參數", "label.realtime": "實時", "label.referrers": "指入域名", @@ -75,12 +78,14 @@ "label.regenerate": "Regenerate", "label.regions": "Regions", "label.remove": "Remove", + "label.reports": "Reports", "label.required": "必填", "label.reset": "重置", "label.reset-website": "重置統計數據", "label.role": "Role", "label.save": "保存", "label.screens": "屏幕尺寸", + "label.select-date": "Select date", "label.select-website": "Select website", "label.sessions": "Sessions", "label.settings": "設置", @@ -104,6 +109,7 @@ "label.tracking-code": "追蹤代碼", "label.unique-visitors": "獨立訪客", "label.unknown": "未知", + "label.urls": "URLs", "label.user": "User", "label.username": "用户名", "label.users": "Users", @@ -111,6 +117,7 @@ "label.view-details": "查看更多", "label.views": "頁面流量", "label.visitors": "獨立訪客", + "label.website": "Website", "label.website-id": "Website ID", "label.websites": "網站", "label.yesterday": "Yesterday", @@ -118,6 +125,7 @@ "message.confirm-delete": "你確定要刪除 {target} 嗎?", "message.confirm-leave": "Are you sure you want to leave {target}?", "message.confirm-reset": "您確定要重置 {target} 的數據嗎?", + "message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.", "message.delete-website": "刪除網站", "message.delete-website-warning": "所有相關數據將會被刪除。", "message.error": "出現錯誤。", @@ -140,6 +148,7 @@ "message.tracking-code": "追蹤代碼", "message.user-deleted": "User deleted.", "message.visitor-log": "來自{country}的訪客在搭載 {os} 的{device}上使用 {browser} 進行訪問。", + "messages.no-results-found": "No results were found.", "messages.no-team-websites": "This team does not have any websites.", "messages.no-websites-configured": "目前無任何網站設定。", "messages.team-websites-info": "Websites can be viewed by anyone on the team." diff --git a/package.json b/package.json index 02820f22..c4a790dd 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "timezone-support": "^2.0.2", "uuid": "^8.3.2", "yup": "^0.32.11", - "zustand": "^3.7.2" + "zustand": "^4.3.8" }, "devDependencies": { "@formatjs/cli": "^4.2.29", diff --git a/pages/api/websites/[id]/data.ts b/pages/api/websites/[id]/data.ts deleted file mode 100644 index 93e97067..00000000 --- a/pages/api/websites/[id]/data.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NextApiResponse } from 'next'; -import { useAuth } from 'lib/middleware'; -import { NextApiRequestQueryBody, User } from 'lib/types'; -import { ok } from 'next-basics'; - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - - return ok(res, req.auth.user); -}; diff --git a/pages/api/websites/[id]/eventData.ts b/pages/api/websites/[id]/eventData.ts index 65c4d687..04a6d83b 100644 --- a/pages/api/websites/[id]/eventData.ts +++ b/pages/api/websites/[id]/eventData.ts @@ -35,7 +35,7 @@ export default async ( const { id: websiteId } = req.query; - if (req.method === 'POST') { + if (req.method === 'GET') { if (!(await canViewWebsite(req.auth, websiteId))) { return unauthorized(res); } diff --git a/pages/reports/event-data.js b/pages/reports/event-data.js new file mode 100644 index 00000000..b5749c96 --- /dev/null +++ b/pages/reports/event-data.js @@ -0,0 +1,13 @@ +import AppLayout from 'components/layout/AppLayout'; +import EventDataReport from 'components/pages/reports/event-data/EventDataReport'; +import { useMessages } from 'hooks'; + +export default function Report() { + const { formatMessage, labels } = useMessages(); + + return ( + + + + ); +} diff --git a/pages/reports/event-data/index.js b/pages/reports/event-data/index.js deleted file mode 100644 index 154c79be..00000000 --- a/pages/reports/event-data/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import EventDataReport from 'components/pages/reports/EventDataReport'; - -export default function Report() { - return ; -} diff --git a/pages/reports/funnel.js b/pages/reports/funnel.js index 3ba11306..731ffa47 100644 --- a/pages/reports/funnel.js +++ b/pages/reports/funnel.js @@ -1,13 +1,13 @@ import AppLayout from 'components/layout/AppLayout'; -import FunnelPage from 'components/pages/reports/funnel/FunnelPage'; +import FunnelReport from 'components/pages/reports/funnel/FunnelReport'; import useMessages from 'hooks/useMessages'; export default function Funnel() { const { formatMessage, labels } = useMessages(); return ( - - + + ); } diff --git a/pages/reports/index.js b/pages/reports/index.js index b26023cc..70f684cb 100644 --- a/pages/reports/index.js +++ b/pages/reports/index.js @@ -1,5 +1,5 @@ import AppLayout from 'components/layout/AppLayout'; -import ReportsList from 'components/pages/reports/ReportsList'; +import ReportList from 'components/pages/reports/ReportList'; import useMessages from 'hooks/useMessages'; export default function ReportsPage() { @@ -7,7 +7,7 @@ export default function ReportsPage() { return ( - + ); } diff --git a/public/intl/messages/ar-SA.json b/public/intl/messages/ar-SA.json index fc38707d..c7723665 100644 --- a/public/intl/messages/ar-SA.json +++ b/public/intl/messages/ar-SA.json @@ -987,4 +987,4 @@ "value": "يمكن مشاهدة الموقع من اي عضو في المجموعة." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/be-BY.json b/public/intl/messages/be-BY.json index 56604985..37718636 100644 --- a/public/intl/messages/be-BY.json +++ b/public/intl/messages/be-BY.json @@ -987,4 +987,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/bn-BD.json b/public/intl/messages/bn-BD.json index 145815a5..7a2e5176 100644 --- a/public/intl/messages/bn-BD.json +++ b/public/intl/messages/bn-BD.json @@ -987,4 +987,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/ca-ES.json b/public/intl/messages/ca-ES.json index 5d5d6ff7..9ae29bcf 100644 --- a/public/intl/messages/ca-ES.json +++ b/public/intl/messages/ca-ES.json @@ -987,4 +987,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/cs-CZ.json b/public/intl/messages/cs-CZ.json index fc617b4e..1ea0c864 100644 --- a/public/intl/messages/cs-CZ.json +++ b/public/intl/messages/cs-CZ.json @@ -987,4 +987,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/da-DK.json b/public/intl/messages/da-DK.json index 4401b5d8..bd68b67f 100644 --- a/public/intl/messages/da-DK.json +++ b/public/intl/messages/da-DK.json @@ -987,4 +987,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/de-CH.json b/public/intl/messages/de-CH.json index 88a93208..6861f5a4 100644 --- a/public/intl/messages/de-CH.json +++ b/public/intl/messages/de-CH.json @@ -979,4 +979,4 @@ "value": "Websiite chönd vo jedem im Team agluegt werde" } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/de-DE.json b/public/intl/messages/de-DE.json index d8a64915..85239abc 100644 --- a/public/intl/messages/de-DE.json +++ b/public/intl/messages/de-DE.json @@ -979,4 +979,4 @@ "value": "Webseiten können von jedem im Team eingesehen werden." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/el-GR.json b/public/intl/messages/el-GR.json index ecc6c306..688b73c0 100644 --- a/public/intl/messages/el-GR.json +++ b/public/intl/messages/el-GR.json @@ -987,4 +987,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/en-GB.json b/public/intl/messages/en-GB.json index a0dffae5..ffd74588 100644 --- a/public/intl/messages/en-GB.json +++ b/public/intl/messages/en-GB.json @@ -987,4 +987,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/en-US.json b/public/intl/messages/en-US.json index 0c7bb7dd..21fb9de8 100644 --- a/public/intl/messages/en-US.json +++ b/public/intl/messages/en-US.json @@ -1009,4 +1009,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/es-MX.json b/public/intl/messages/es-MX.json index fdc2483d..9a07a296 100644 --- a/public/intl/messages/es-MX.json +++ b/public/intl/messages/es-MX.json @@ -987,4 +987,4 @@ "value": "Las analíticas de tus sitios pueden verse por cualquier miembro del equipo." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/fa-IR.json b/public/intl/messages/fa-IR.json index 8f9cdc68..34c8544a 100644 --- a/public/intl/messages/fa-IR.json +++ b/public/intl/messages/fa-IR.json @@ -987,4 +987,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/fi-FI.json b/public/intl/messages/fi-FI.json index 9a23d285..03b0aef0 100644 --- a/public/intl/messages/fi-FI.json +++ b/public/intl/messages/fi-FI.json @@ -987,4 +987,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/fo-FO.json b/public/intl/messages/fo-FO.json index 52e2bc33..e1e45a1d 100644 --- a/public/intl/messages/fo-FO.json +++ b/public/intl/messages/fo-FO.json @@ -987,4 +987,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/fr-FR.json b/public/intl/messages/fr-FR.json index 7361eca2..11754452 100644 --- a/public/intl/messages/fr-FR.json +++ b/public/intl/messages/fr-FR.json @@ -983,4 +983,4 @@ "value": "Les sites peuvent être vus par tout utilisateur dans l'équipe." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/ga-ES.json b/public/intl/messages/ga-ES.json index 114a55eb..35d98325 100644 --- a/public/intl/messages/ga-ES.json +++ b/public/intl/messages/ga-ES.json @@ -987,4 +987,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/he-IL.json b/public/intl/messages/he-IL.json index 57704b11..3e0b844b 100644 --- a/public/intl/messages/he-IL.json +++ b/public/intl/messages/he-IL.json @@ -975,4 +975,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/hi-IN.json b/public/intl/messages/hi-IN.json index aa550dfb..d51f71fe 100644 --- a/public/intl/messages/hi-IN.json +++ b/public/intl/messages/hi-IN.json @@ -979,4 +979,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/hu-HU.json b/public/intl/messages/hu-HU.json index 8bb833bf..a41352a0 100644 --- a/public/intl/messages/hu-HU.json +++ b/public/intl/messages/hu-HU.json @@ -991,4 +991,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/id-ID.json b/public/intl/messages/id-ID.json index 00a84f18..02e50783 100644 --- a/public/intl/messages/id-ID.json +++ b/public/intl/messages/id-ID.json @@ -955,4 +955,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/it-IT.json b/public/intl/messages/it-IT.json index 3cc27fd4..ad026521 100644 --- a/public/intl/messages/it-IT.json +++ b/public/intl/messages/it-IT.json @@ -991,4 +991,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/ja-JP.json b/public/intl/messages/ja-JP.json index 55941ebf..384d8014 100644 --- a/public/intl/messages/ja-JP.json +++ b/public/intl/messages/ja-JP.json @@ -963,4 +963,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/km-KH.json b/public/intl/messages/km-KH.json index cfa45bbf..b6e93a33 100644 --- a/public/intl/messages/km-KH.json +++ b/public/intl/messages/km-KH.json @@ -959,4 +959,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/ko-KR.json b/public/intl/messages/ko-KR.json index 7b368a5f..9665bc3d 100644 --- a/public/intl/messages/ko-KR.json +++ b/public/intl/messages/ko-KR.json @@ -963,4 +963,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/lt-LT.json b/public/intl/messages/lt-LT.json index 3bc4d21b..113a1394 100644 --- a/public/intl/messages/lt-LT.json +++ b/public/intl/messages/lt-LT.json @@ -1112,4 +1112,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/mn-MN.json b/public/intl/messages/mn-MN.json index beaab2d7..6ad96b19 100644 --- a/public/intl/messages/mn-MN.json +++ b/public/intl/messages/mn-MN.json @@ -1017,4 +1017,4 @@ "value": "Вебийг багийн бүх гишүүд үзэж болно." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/ms-MY.json b/public/intl/messages/ms-MY.json index 31eee0d2..771672a7 100644 --- a/public/intl/messages/ms-MY.json +++ b/public/intl/messages/ms-MY.json @@ -979,4 +979,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/nb-NO.json b/public/intl/messages/nb-NO.json index bf9ffcd9..50a9ec18 100644 --- a/public/intl/messages/nb-NO.json +++ b/public/intl/messages/nb-NO.json @@ -991,4 +991,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/nl-NL.json b/public/intl/messages/nl-NL.json index 714c31a0..e775f2d5 100644 --- a/public/intl/messages/nl-NL.json +++ b/public/intl/messages/nl-NL.json @@ -987,4 +987,4 @@ "value": "Websites kunnen door iedereen in het team worden bekeken." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/pl-PL.json b/public/intl/messages/pl-PL.json index ba133523..0f5667ff 100644 --- a/public/intl/messages/pl-PL.json +++ b/public/intl/messages/pl-PL.json @@ -987,4 +987,4 @@ "value": "Strony internetowe mogą być przeglądane przez każdego członka zespołu." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/pt-BR.json b/public/intl/messages/pt-BR.json index 63e9a49e..6eade1e3 100644 --- a/public/intl/messages/pt-BR.json +++ b/public/intl/messages/pt-BR.json @@ -991,4 +991,4 @@ "value": "Os sites podem ser visualizados por qualquer membro da equipe." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/pt-PT.json b/public/intl/messages/pt-PT.json index 079af91b..a858ff8c 100644 --- a/public/intl/messages/pt-PT.json +++ b/public/intl/messages/pt-PT.json @@ -991,4 +991,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/ro-RO.json b/public/intl/messages/ro-RO.json index 5ea1dece..e7305a38 100644 --- a/public/intl/messages/ro-RO.json +++ b/public/intl/messages/ro-RO.json @@ -987,4 +987,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/ru-RU.json b/public/intl/messages/ru-RU.json index 155828c5..5d164635 100644 --- a/public/intl/messages/ru-RU.json +++ b/public/intl/messages/ru-RU.json @@ -963,4 +963,4 @@ "value": "Сайты могут просматривать все члены команды." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/sk-SK.json b/public/intl/messages/sk-SK.json index ad3e9b19..3abce8ef 100644 --- a/public/intl/messages/sk-SK.json +++ b/public/intl/messages/sk-SK.json @@ -987,4 +987,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/sl-SI.json b/public/intl/messages/sl-SI.json index aa584730..93dbb9d5 100644 --- a/public/intl/messages/sl-SI.json +++ b/public/intl/messages/sl-SI.json @@ -987,4 +987,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/sv-SE.json b/public/intl/messages/sv-SE.json index 93040948..2b3663a8 100644 --- a/public/intl/messages/sv-SE.json +++ b/public/intl/messages/sv-SE.json @@ -991,4 +991,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/ta-IN.json b/public/intl/messages/ta-IN.json index d46893d6..b7210292 100644 --- a/public/intl/messages/ta-IN.json +++ b/public/intl/messages/ta-IN.json @@ -983,4 +983,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/th-TH.json b/public/intl/messages/th-TH.json index 74ab2981..54641c9c 100644 --- a/public/intl/messages/th-TH.json +++ b/public/intl/messages/th-TH.json @@ -983,4 +983,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/tr-TR.json b/public/intl/messages/tr-TR.json index f8c3a275..a8b03f95 100644 --- a/public/intl/messages/tr-TR.json +++ b/public/intl/messages/tr-TR.json @@ -955,4 +955,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/uk-UA.json b/public/intl/messages/uk-UA.json index 0cf3d7c1..a198c74e 100644 --- a/public/intl/messages/uk-UA.json +++ b/public/intl/messages/uk-UA.json @@ -963,4 +963,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/ur-PK.json b/public/intl/messages/ur-PK.json index 303ca457..fffff35e 100644 --- a/public/intl/messages/ur-PK.json +++ b/public/intl/messages/ur-PK.json @@ -987,4 +987,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/vi-VN.json b/public/intl/messages/vi-VN.json index e9b65724..400fd678 100644 --- a/public/intl/messages/vi-VN.json +++ b/public/intl/messages/vi-VN.json @@ -979,4 +979,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/zh-CN.json b/public/intl/messages/zh-CN.json index 53421291..d17a7d16 100644 --- a/public/intl/messages/zh-CN.json +++ b/public/intl/messages/zh-CN.json @@ -975,4 +975,4 @@ "value": "团队中的任何人都可查看网站。" } ] -} +} \ No newline at end of file diff --git a/public/intl/messages/zh-TW.json b/public/intl/messages/zh-TW.json index a306e4cc..135d3647 100644 --- a/public/intl/messages/zh-TW.json +++ b/public/intl/messages/zh-TW.json @@ -971,4 +971,4 @@ "value": "Websites can be viewed by anyone on the team." } ] -} +} \ No newline at end of file diff --git a/rollup.tracker.config.js b/rollup.tracker.config.js index f4e7223c..8003e0da 100644 --- a/rollup.tracker.config.js +++ b/rollup.tracker.config.js @@ -4,7 +4,7 @@ import replace from '@rollup/plugin-replace'; import { terser } from 'rollup-plugin-terser'; export default { - input: 'tracker/index.js', + input: 'tracker/event-data.js', output: { file: 'public/script.js', format: 'iife', diff --git a/store/app.js b/store/app.js index 594bc169..53fdbd92 100644 --- a/store/app.js +++ b/store/app.js @@ -1,4 +1,4 @@ -import create from 'zustand'; +import { create } from 'zustand'; import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE, diff --git a/store/dashboard.js b/store/dashboard.js index 7c512228..f6677542 100644 --- a/store/dashboard.js +++ b/store/dashboard.js @@ -1,4 +1,4 @@ -import create from 'zustand'; +import { create } from 'zustand'; import { DASHBOARD_CONFIG, DEFAULT_WEBSITE_LIMIT } from 'lib/constants'; import { getItem, setItem } from 'next-basics'; diff --git a/store/queries.js b/store/queries.js index 92a8f3d5..1de2f04b 100644 --- a/store/queries.js +++ b/store/queries.js @@ -1,4 +1,4 @@ -import create from 'zustand'; +import { create } from 'zustand'; const store = create(() => ({})); diff --git a/store/reports.js b/store/reports.js new file mode 100644 index 00000000..4716aa43 --- /dev/null +++ b/store/reports.js @@ -0,0 +1,61 @@ +import { create } from 'zustand'; +import produce from 'immer'; +import { getRandomChars } from 'next-basics'; + +const emptyReport = { + name: 'Untitled', + description: '', + parameters: {}, +}; + +const initialState = {}; + +const store = create(() => ({ ...initialState })); + +export function updateReport(id, data) { + const report = store.getState()[id]; + + console.log('UPDATE STORE START', id, report); + + if (report) { + store.setState( + produce(state => { + const item = state[id]; + const { parameters, ...rest } = data; + + if (parameters) { + item.parameters = { ...item.parameters, ...parameters }; + } + + for (const key in rest) { + item[key] = rest[key]; + } + + return state; + }), + ); + } +} + +export function createReport() { + const id = `new_${getRandomChars(16)}`; + const report = { ...emptyReport, id }; + + store.setState( + produce(state => { + state[id] = report; + + return state; + }), + ); + + console.log('CREATE STORE', report); + + return report; +} + +export default store; + +if (typeof window !== 'undefined') { + window.__STORE__ = store; +} diff --git a/store/version.js b/store/version.js index cb5208fa..c232c7fa 100644 --- a/store/version.js +++ b/store/version.js @@ -1,4 +1,4 @@ -import create from 'zustand'; +import { create } from 'zustand'; import produce from 'immer'; import semver from 'semver'; import { CURRENT_VERSION, VERSION_CHECK, UPDATES_URL } from 'lib/constants'; diff --git a/store/websites.js b/store/websites.js index e174cbac..34f8242d 100644 --- a/store/websites.js +++ b/store/websites.js @@ -1,4 +1,4 @@ -import create from 'zustand'; +import { create } from 'zustand'; import produce from 'immer'; import app from './app'; import { parseDateRange } from 'lib/date'; diff --git a/yarn.lock b/yarn.lock index 1f9ea77a..1d0bc7fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5500,11 +5500,6 @@ functions-have-names@^1.2.2: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== -funnel-graph-js@^1.3.7: - version "1.4.2" - resolved "https://registry.yarnpkg.com/funnel-graph-js/-/funnel-graph-js-1.4.2.tgz#b82150189e8afa59104d881d5dcf55a28d715342" - integrity sha512-9bnmcBve7RDH9dTF9BLuUpuisKkDka3yrfhs+Z/106ZgJvqIse1RfKQWjW+QdAlTrZqC9oafen7t/KuJKv9ohA== - generic-names@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-4.0.0.tgz#0bd8a2fd23fe8ea16cbd0a279acd69c06933d9a3" @@ -9749,7 +9744,7 @@ use-memo-one@^1.1.1: resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== -use-sync-external-store@^1.2.0: +use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== @@ -9984,7 +9979,9 @@ yup@^0.32.11: property-expr "^2.0.4" toposort "^2.0.2" -zustand@^3.7.2: - version "3.7.2" - resolved "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz" - integrity sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA== +zustand@^4.3.8: + version "4.3.8" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.8.tgz#37113df8e9e1421b0be1b2dca02b49b76210e7c4" + integrity sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg== + dependencies: + use-sync-external-store "1.2.0" From 1f799f17e9141ae7095c2605e992294fb66fcc1d Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Mon, 22 May 2023 15:38:03 -0700 Subject: [PATCH 45/88] Fix permissions. --- pages/api/me/websites.ts | 11 +---------- pages/api/realtime/[id].ts | 24 +++++++++++++++++++----- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/pages/api/me/websites.ts b/pages/api/me/websites.ts index 15ea2485..dc9c0d62 100644 --- a/pages/api/me/websites.ts +++ b/pages/api/me/websites.ts @@ -4,16 +4,7 @@ import { NextApiResponse } from 'next'; import { methodNotAllowed, ok } from 'next-basics'; import { getUserWebsites } from 'queries'; -export interface WebsitesRequestBody { - name: string; - domain: string; - shareId: string; -} - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { +export default async (req: NextApiRequestQueryBody, res: NextApiResponse) => { await useCors(req, res); await useAuth(req, res); diff --git a/pages/api/realtime/[id].ts b/pages/api/realtime/[id].ts index 069db54a..e78599c6 100644 --- a/pages/api/realtime/[id].ts +++ b/pages/api/realtime/[id].ts @@ -1,22 +1,36 @@ import { subMinutes } from 'date-fns'; -import { RealtimeInit, NextApiRequestAuth } from 'lib/types'; +import { canViewWebsite } from 'lib/auth'; import { useAuth } from 'lib/middleware'; +import { NextApiRequestQueryBody, RealtimeInit } from 'lib/types'; import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok } from 'next-basics'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getRealtimeData } from 'queries'; -export default async (req: NextApiRequestAuth, res: NextApiResponse) => { +export interface RealtimeRequestQuery { + id: string; + startAt: number; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useAuth(req, res); if (req.method === 'GET') { - const { id, startAt } = req.query; + const { id: websiteId, startAt } = req.query; + + if (!(await canViewWebsite(req.auth, websiteId))) { + return unauthorized(res); + } + let startTime = subMinutes(new Date(), 30); if (+startAt > startTime.getTime()) { startTime = new Date(+startAt); } - const data = await getRealtimeData(id, startTime); + const data = await getRealtimeData(websiteId, startTime); return ok(res, data); } From fb4dd75e18cebabe5a47175aee0ed3c622b5eae3 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 24 May 2023 21:40:02 -0700 Subject: [PATCH 46/88] Refactored funnel report. Made BarChart more generic. --- components/common/HoverTooltip.js | 6 +- components/common/WorldMap.js | 2 +- components/input/LanguageButton.js | 46 +++-- components/input/ThemeButton.js | 4 +- components/messages.js | 4 + components/metrics/BarChart.js | 131 ++++-------- components/metrics/DataTable.js | 2 +- components/metrics/DataTable.module.css | 2 +- components/metrics/MetricsBar.module.css | 2 +- components/metrics/PageviewsChart.js | 91 +++++--- components/metrics/WebsiteHeader.module.css | 6 + components/pages/reports/ReportHeader.js | 2 - .../pages/reports/funnel/FunnelChart.js | 194 ++++-------------- .../reports/funnel/FunnelChart.module.css | 24 +-- .../pages/reports/funnel/FunnelParameters.js | 67 +++++- .../funnel/FunnelParameters.module.css | 20 +- .../pages/reports/funnel/FunnelReport.js | 24 ++- .../pages/reports/funnel/FunnelTable.js | 17 +- .../pages/settings/profile/ThemeSetting.js | 6 +- hooks/useReport.js | 8 +- hooks/useTheme.js | 25 ++- pages/api/reports/funnel.ts | 1 + rollup.tracker.config.js | 2 +- store/reports.js | 8 +- 24 files changed, 327 insertions(+), 367 deletions(-) diff --git a/components/common/HoverTooltip.js b/components/common/HoverTooltip.js index 2a98ab84..59fd6277 100644 --- a/components/common/HoverTooltip.js +++ b/components/common/HoverTooltip.js @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { Tooltip } from 'react-basics'; import styles from './HoverTooltip.module.css'; -export function HoverTooltip({ tooltip }) { +export function HoverTooltip({ children }) { const [position, setPosition] = useState({ x: -1000, y: -1000 }); useEffect(() => { @@ -18,8 +18,8 @@ export function HoverTooltip({ tooltip }) { }, []); return ( -
- +
+
); } diff --git a/components/common/WorldMap.js b/components/common/WorldMap.js index 55a13f0b..a371e2d3 100644 --- a/components/common/WorldMap.js +++ b/components/common/WorldMap.js @@ -15,7 +15,7 @@ import styles from './WorldMap.module.css'; export function WorldMap({ data, className }) { const { basePath } = useRouter(); const [tooltip, setTooltip] = useState(); - const [theme] = useTheme(); + const { theme } = useTheme(); const colors = useMemo( () => ({ baseColor: THEME_COLORS[theme].primary, diff --git a/components/input/LanguageButton.js b/components/input/LanguageButton.js index e2a51223..049b49f3 100644 --- a/components/input/LanguageButton.js +++ b/components/input/LanguageButton.js @@ -9,9 +9,9 @@ export function LanguageButton() { const { locale, saveLocale, dir } = useLocale(); const items = Object.keys(languages).map(key => ({ ...languages[key], value: key })); - function handleSelect(value) { - //saveLocale(value); - console.log('WTFFFF'); + function handleSelect(value, close) { + saveLocale(value); + close(); } return ( @@ -22,24 +22,28 @@ export function LanguageButton() { -
- {items.map(({ value, label }) => { - return ( -
- {label} - {value === locale && ( - - - - )} -
- ); - })} -
+ {close => { + return ( +
+ {items.map(({ value, label }) => { + return ( +
+ {label} + {value === locale && ( + + + + )} +
+ ); + })} +
+ ); + }}
); diff --git a/components/input/ThemeButton.js b/components/input/ThemeButton.js index b945ab7d..8ab0cdcd 100644 --- a/components/input/ThemeButton.js +++ b/components/input/ThemeButton.js @@ -5,7 +5,7 @@ import Icons from 'components/icons'; import styles from './ThemeButton.module.css'; export function ThemeButton() { - const [theme, setTheme] = useTheme(); + const { theme, saveTheme } = useTheme(); const transitions = useTransition(theme, { initial: { opacity: 1 }, @@ -21,7 +21,7 @@ export function ThemeButton() { }); function handleClick() { - setTheme(theme === 'light' ? 'dark' : 'light'); + saveTheme(theme === 'light' ? 'dark' : 'light'); } return ( diff --git a/components/messages.js b/components/messages.js index fb0dee3c..0de3a44c 100644 --- a/components/messages.js +++ b/components/messages.js @@ -123,7 +123,11 @@ export const labels = defineMessages({ reports: { id: 'label.reports', defaultMessage: 'Reports' }, eventData: { id: 'label.event-data', defaultMessage: 'Event data' }, funnel: { id: 'label.funnel', defaultMessage: 'Funnel' }, + url: { id: 'label.url', defaultMessage: 'URL' }, urls: { id: 'label.urls', defaultMessage: 'URLs' }, + add: { id: 'label.add', defaultMessage: 'Add' }, + window: { id: 'label.window', defaultMessage: 'Window' }, + addUrl: { id: 'label.add-url', defaultMessage: 'Add URL' }, }); export const messages = defineMessages({ diff --git a/components/metrics/BarChart.js b/components/metrics/BarChart.js index 8e1784df..9206b800 100644 --- a/components/metrics/BarChart.js +++ b/components/metrics/BarChart.js @@ -1,102 +1,39 @@ -import { useState, useRef, useEffect, useMemo, useCallback } from 'react'; -import { StatusLight, Loading } from 'react-basics'; +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 { formatLongNumber } from 'lib/format'; -import { dateFormat } from 'lib/date'; import useLocale from 'hooks/useLocale'; import useTheme from 'hooks/useTheme'; -import { DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants'; +import { DEFAULT_ANIMATION_DURATION } from 'lib/constants'; import styles from './BarChart.module.css'; +function defaultRenderYLabel(label) { + return +label > 1000 ? formatLongNumber(label) : label; +} + export function BarChart({ datasets, unit, animationDuration = DEFAULT_ANIMATION_DURATION, stacked = false, loading = false, - onCreate = () => {}, - onUpdate = () => {}, + renderXLabel, + renderYLabel, + XAxisType = 'time', + YAxisType = 'linear', + renderTooltip, + onCreate, + onUpdate, className, }) { const canvas = useRef(); const chart = useRef(null); const [tooltip, setTooltip] = useState(null); const { locale } = useLocale(); - const [theme] = useTheme(); - - const colors = useMemo( - () => ({ - text: THEME_COLORS[theme].gray700, - line: THEME_COLORS[theme].gray200, - }), - [theme], - ); - - const renderYLabel = label => { - return +label > 1000 ? formatLongNumber(label) : label; - }; - - const renderXLabel = useCallback( - (label, index, values) => { - const d = new Date(values[index].value); - - switch (unit) { - case 'minute': - return dateFormat(d, 'h:mm', locale); - case 'hour': - return dateFormat(d, 'p', locale); - case 'day': - return dateFormat(d, 'MMM d', locale); - case 'month': - return dateFormat(d, 'MMM', locale); - case 'year': - return dateFormat(d, 'YYY', locale); - default: - return label; - } - }, - [locale, unit], - ); - - const renderTooltip = useCallback( - model => { - const { opacity, labelColors, dataPoints } = model.tooltip; - - if (!dataPoints?.length || !opacity) { - setTooltip(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', - }; - - setTooltip( -
-
{dateFormat(new Date(dataPoints[0].raw.x), formats[unit], locale)}
-
- -
- {formatLongNumber(dataPoints[0].raw.y)} {dataPoints[0].dataset.label} -
-
-
-
, - ); - }, - [unit], - ); + const { theme, colors } = useTheme(); const getOptions = useCallback(() => { return { @@ -117,12 +54,12 @@ export function BarChart({ }, tooltip: { enabled: false, - external: renderTooltip, + external: renderTooltip ? renderTooltip.bind(null, setTooltip) : undefined, }, }, scales: { x: { - type: 'time', + type: XAxisType, stacked: true, time: { unit, @@ -131,34 +68,44 @@ export function BarChart({ display: false, }, border: { - color: colors.line, + color: colors.chart.line, }, ticks: { - color: colors.text, + color: colors.chart.text, autoSkip: false, maxRotation: 0, callback: renderXLabel, }, }, y: { - type: 'linear', + type: YAxisType, min: 0, beginAtZero: true, stacked, grid: { - color: colors.line, + color: colors.chart.line, }, border: { - color: colors.line, + color: colors.chart.line, }, ticks: { color: colors.text, - callback: renderYLabel, + callback: renderYLabel || defaultRenderYLabel, }, }, }, }; - }, [animationDuration, renderTooltip, renderXLabel, stacked, colors, unit, locale]); + }, [ + animationDuration, + renderTooltip, + renderXLabel, + XAxisType, + YAxisType, + stacked, + colors, + unit, + locale, + ]); const createChart = () => { Chart.defaults.font.family = 'Inter'; @@ -173,7 +120,7 @@ export function BarChart({ options, }); - onCreate(chart.current); + onCreate?.(chart.current); }; const updateChart = () => { @@ -186,7 +133,7 @@ export function BarChart({ chart.current.options = getOptions(); - onUpdate(chart.current); + onUpdate?.(chart.current); chart.current.update(); }; @@ -208,7 +155,11 @@ export function BarChart({
- {tooltip && } + {tooltip && ( + +
{tooltip}
+
+ )} ); } diff --git a/components/metrics/DataTable.js b/components/metrics/DataTable.js index 086f98ae..a2c1a568 100644 --- a/components/metrics/DataTable.js +++ b/components/metrics/DataTable.js @@ -5,8 +5,8 @@ import { useSpring, animated, config } from 'react-spring'; import classNames from 'classnames'; import NoData from 'components/common/NoData'; import { formatNumber, formatLongNumber } from 'lib/format'; +import useMessages from 'hooks/useMessages'; import styles from './DataTable.module.css'; -import useMessages from '../../hooks/useMessages'; export function DataTable({ data = [], diff --git a/components/metrics/DataTable.module.css b/components/metrics/DataTable.module.css index c5b2bd7c..04e12e9b 100644 --- a/components/metrics/DataTable.module.css +++ b/components/metrics/DataTable.module.css @@ -1,9 +1,9 @@ .table { position: relative; - height: 100%; display: grid; grid-template-rows: fit-content(100%) auto; overflow: hidden; + flex: 1; } .body { diff --git a/components/metrics/MetricsBar.module.css b/components/metrics/MetricsBar.module.css index 0e305c70..eaf81c48 100644 --- a/components/metrics/MetricsBar.module.css +++ b/components/metrics/MetricsBar.module.css @@ -1,7 +1,7 @@ .bar { display: flex; cursor: pointer; - min-height: 80px; + min-height: 110px; gap: 20px; flex-wrap: wrap; } diff --git a/components/metrics/PageviewsChart.js b/components/metrics/PageviewsChart.js index 6ea16226..0dcf0ac5 100644 --- a/components/metrics/PageviewsChart.js +++ b/components/metrics/PageviewsChart.js @@ -1,33 +1,73 @@ -import { useMemo } from 'react'; -import { colord } from 'colord'; +import { useCallback, useMemo } from 'react'; +import { StatusLight } from 'react-basics'; import BarChart from './BarChart'; -import { THEME_COLORS } from 'lib/constants'; import useTheme from 'hooks/useTheme'; import useMessages from 'hooks/useMessages'; import useLocale from 'hooks/useLocale'; +import { dateFormat } from 'lib/date'; +import { formatLongNumber } from 'lib/format'; -export function PageviewsChart({ websiteId, data, unit, records, className, loading, ...props }) { +export function PageviewsChart({ websiteId, data, unit, className, loading, ...props }) { const { formatMessage, labels } = useMessages(); - const [theme] = useTheme(); + const { colors } = useTheme(); const { locale } = useLocale(); - const colors = useMemo(() => { - const primaryColor = colord(THEME_COLORS[theme].primary); - return { - views: { - hoverBackgroundColor: primaryColor.alpha(0.7).toRgbString(), - backgroundColor: primaryColor.alpha(0.4).toRgbString(), - borderColor: primaryColor.alpha(0.7).toRgbString(), - hoverBorderColor: primaryColor.toRgbString(), - }, - visitors: { - hoverBackgroundColor: primaryColor.alpha(0.9).toRgbString(), - backgroundColor: primaryColor.alpha(0.6).toRgbString(), - borderColor: primaryColor.alpha(0.9).toRgbString(), - hoverBorderColor: primaryColor.toRgbString(), - }, - }; - }, [theme]); + const renderXLabel = useCallback( + (label, index, values) => { + const d = new Date(values[index].value); + + switch (unit) { + case 'minute': + return dateFormat(d, 'h:mm', locale); + case 'hour': + return dateFormat(d, 'p', locale); + case 'day': + return dateFormat(d, 'MMM d', locale); + case 'month': + return dateFormat(d, 'MMM', locale); + case 'year': + return dateFormat(d, 'YYY', locale); + default: + return label; + } + }, + [locale, unit], + ); + + const renderTooltip = useCallback( + (setTooltip, model) => { + const { opacity, labelColors, dataPoints } = model.tooltip; + + if (!dataPoints?.length || !opacity) { + setTooltip(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', + }; + + setTooltip( + <> +
{dateFormat(new Date(dataPoints[0].raw.x), formats[unit], locale)}
+
+ + {formatLongNumber(dataPoints[0].raw.y)} {dataPoints[0].dataset.label} + +
+ , + ); + }, + [unit], + ); const datasets = useMemo(() => { if (!data) return []; @@ -37,13 +77,13 @@ export function PageviewsChart({ websiteId, data, unit, records, className, load label: formatMessage(labels.uniqueVisitors), data: data.sessions, borderWidth: 1, - ...colors.visitors, + ...colors.chart.visitors, }, { label: formatMessage(labels.pageViews), data: data.pageviews, borderWidth: 1, - ...colors.views, + ...colors.chart.views, }, ]; }, [data, locale, colors]); @@ -55,8 +95,9 @@ export function PageviewsChart({ websiteId, data, unit, records, className, load className={className} datasets={datasets} unit={unit} - records={records} loading={loading} + renderXLabel={renderXLabel} + renderTooltip={renderTooltip} /> ); } diff --git a/components/metrics/WebsiteHeader.module.css b/components/metrics/WebsiteHeader.module.css index e5ebcca7..68fd22f8 100644 --- a/components/metrics/WebsiteHeader.module.css +++ b/components/metrics/WebsiteHeader.module.css @@ -1,3 +1,9 @@ +.header { + display: flex; + flex-direction: row; + align-items: center; +} + .title { display: flex; flex-direction: row; diff --git a/components/pages/reports/ReportHeader.js b/components/pages/reports/ReportHeader.js index 90d58ca8..7da47245 100644 --- a/components/pages/reports/ReportHeader.js +++ b/components/pages/reports/ReportHeader.js @@ -16,8 +16,6 @@ export function ReportHeader({ report, icon }) { const { id, websiteId, name, parameters } = report || {}; const { value, startDate, endDate } = parameters?.dateRange || {}; - console.log('REPORT HEADER', report); - const handleSelect = websiteId => { updateReport(id, { websiteId }); }; diff --git a/components/pages/reports/funnel/FunnelChart.js b/components/pages/reports/funnel/FunnelChart.js index 307c78ee..aecd48a7 100644 --- a/components/pages/reports/funnel/FunnelChart.js +++ b/components/pages/reports/funnel/FunnelChart.js @@ -1,184 +1,60 @@ -import Chart from 'chart.js/auto'; -import classNames from 'classnames'; -import { colord } from 'colord'; -import HoverTooltip from 'components/common/HoverTooltip'; -import Legend from 'components/metrics/Legend'; -import useLocale from 'hooks/useLocale'; +import { useCallback, useMemo } from 'react'; +import { Loading } from 'react-basics'; import useMessages from 'hooks/useMessages'; import useTheme from 'hooks/useTheme'; -import { DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants'; +import BarChart from 'components/metrics/BarChart'; import { formatLongNumber } from 'lib/format'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Loading, StatusLight } from 'react-basics'; import styles from './FunnelChart.module.css'; -export function FunnelChart({ - data, - animationDuration = DEFAULT_ANIMATION_DURATION, - stacked = false, - loading = false, - onCreate = () => {}, - onUpdate = () => {}, - className, -}) { +export function FunnelChart({ report, data, loading, className }) { const { formatMessage, labels } = useMessages(); - const canvas = useRef(); - const chart = useRef(null); - const [tooltip, setTooltip] = useState(null); - const { locale } = useLocale(); - const [theme] = useTheme(); + const { colors } = useTheme(); - const datasets = useMemo(() => { - const primaryColor = colord(THEME_COLORS[theme].primary); - return [ - { - label: formatMessage(labels.uniqueVisitors), - data: data, - borderWidth: 1, - hoverBackgroundColor: primaryColor.alpha(0.9).toRgbString(), - backgroundColor: primaryColor.alpha(0.6).toRgbString(), - borderColor: primaryColor.alpha(0.9).toRgbString(), - hoverBorderColor: primaryColor.toRgbString(), - }, - ]; - }, [data]); + const { parameters } = report || {}; - const colors = useMemo( - () => ({ - text: THEME_COLORS[theme].gray700, - line: THEME_COLORS[theme].gray200, - }), - [theme], + const renderXLabel = useCallback( + (label, index) => { + return parameters.urls[index]; + }, + [parameters], ); - const renderYLabel = label => { - return +label > 1000 ? formatLongNumber(label) : label; - }; - - const renderTooltip = useCallback(model => { - const { opacity, labelColors, dataPoints } = model.tooltip; + const renderTooltip = useCallback((setTooltip, model) => { + const { opacity, dataPoints } = model.tooltip; if (!dataPoints?.length || !opacity) { setTooltip(null); return; } - setTooltip( -
-
- -
-
{dataPoints[0].raw.x}
-
{formatLongNumber(dataPoints[0].raw.y)}
-
-
-
-
, - ); + setTooltip(`${formatLongNumber(dataPoints[0].raw.y)} ${formatMessage(labels.visitors)}`); }, []); - const getOptions = useCallback(() => { - return { - responsive: true, - maintainAspectRatio: false, - animation: { - duration: animationDuration, - resize: { - duration: 0, - }, - active: { - duration: 0, - }, + const datasets = useMemo(() => { + return [ + { + label: formatMessage(labels.uniqueVisitors), + data: data, + borderWidth: 1, + ...colors.chart.visitors, }, - plugins: { - legend: { - display: false, - }, - tooltip: { - enabled: false, - external: renderTooltip, - }, - }, - scales: { - x: { - grid: { - display: false, - }, - border: { - color: colors.line, - }, - ticks: { - color: colors.text, - autoSkip: false, - maxRotation: 0, - }, - }, - y: { - type: 'linear', - min: 0, - beginAtZero: true, - stacked, - grid: { - color: colors.line, - }, - border: { - color: colors.line, - }, - ticks: { - color: colors.text, - callback: renderYLabel, - }, - }, - }, - }; - }, [animationDuration, renderTooltip, stacked, colors, locale]); + ]; + }, [data]); - const createChart = () => { - Chart.defaults.font.family = 'Inter'; - - const options = getOptions(); - - chart.current = new Chart(canvas.current, { - type: 'bar', - data: { datasets }, - options, - }); - - onCreate(chart.current); - }; - - const updateChart = () => { - setTooltip(null); - - chart.current.data.datasets[0].data = datasets[0].data; - chart.current.data.datasets[0].label = datasets[0].label; - - chart.current.options = getOptions(); - - onUpdate(chart.current); - - chart.current.update(); - }; - - useEffect(() => { - if (datasets) { - if (!chart.current) { - createChart(); - } else { - updateChart(); - } - } - }, [datasets, theme, animationDuration, locale]); + if (loading) { + return ; + } return ( - <> -
- {loading && } - -
- - {tooltip && } - + ); } diff --git a/components/pages/reports/funnel/FunnelChart.module.css b/components/pages/reports/funnel/FunnelChart.module.css index f071a29e..9e1690b3 100644 --- a/components/pages/reports/funnel/FunnelChart.module.css +++ b/components/pages/reports/funnel/FunnelChart.module.css @@ -1,23 +1,3 @@ -.chart { - position: relative; - height: 400px; - overflow: hidden; -} - -.tooltip { - display: flex; - flex-direction: column; - gap: 10px; -} - -.tooltip .value { - display: flex; - flex-direction: column; - text-transform: lowercase; -} - -@media only screen and (max-width: 992px) { - .chart { - /*height: 200px;*/ - } +.loading { + height: 300px; } diff --git a/components/pages/reports/funnel/FunnelParameters.js b/components/pages/reports/funnel/FunnelParameters.js index d387686e..f43728c7 100644 --- a/components/pages/reports/funnel/FunnelParameters.js +++ b/components/pages/reports/funnel/FunnelParameters.js @@ -1,5 +1,6 @@ import { useMessages } from 'hooks'; import { + Button, Icon, Form, FormButtons, @@ -8,29 +9,38 @@ import { PopupTrigger, Popup, SubmitButton, + Text, TextField, } from 'react-basics'; import Icons from 'components/icons'; import { updateReport } from 'store/reports'; -import { useRef } from 'react'; +import { useRef, useState } from 'react'; import styles from './FunnelParameters.module.css'; export function FunnelParameters({ report }) { const { formatMessage, labels } = useMessages(); const ref = useRef(null); const { id, websiteId, parameters, isLoading } = report || {}; + const queryDisabled = !websiteId || parameters?.urls?.length < 2; const handleSubmit = values => { - console.log({ values }); - updateReport(id, { parameters: values, isLoading: false }); + updateReport(id, { parameters: values, isLoading: false, update: Date.now() }); }; - console.log('PARAMETERS', parameters); + const handleAdd = url => { + updateReport(id, { parameters: { ...parameters, urls: parameters.urls.concat(url) } }); + }; + + const handleRemove = index => { + const urls = [...parameters.urls]; + urls.splice(index, 1); + updateReport(id, { parameters: { ...parameters, urls } }); + }; return ( <>
- + - }> - hi + }> +
+ {parameters?.urls.map((url, index) => { + return ( +
+ {url} + handleRemove(index)}> + + +
+ ); + })} +
- + {formatMessage(labels.query)} @@ -51,14 +72,40 @@ export function FunnelParameters({ report }) { ); } -function AddURLButton() { +function AddURLButton({ onAdd }) { + const [url, setUrl] = useState(''); + const { formatMessage, labels } = useMessages(); + + const handleAdd = close => { + onAdd?.(url); + setUrl(''); + close(); + }; + + const handleChange = e => { + setUrl(e.target.value); + }; + return ( - HALLO + {close => { + return ( + + + + + + + + + ); + }} ); diff --git a/components/pages/reports/funnel/FunnelParameters.module.css b/components/pages/reports/funnel/FunnelParameters.module.css index 0e68e095..d60048a7 100644 --- a/components/pages/reports/funnel/FunnelParameters.module.css +++ b/components/pages/reports/funnel/FunnelParameters.module.css @@ -4,4 +4,22 @@ margin-left: 10px; border: 1px solid var(--base400); border-radius: var(--border-radius); -} \ No newline at end of file + width: 400px; +} + +.urls { + display: flex; + flex-direction: column; + gap: 10px; +} + +.url { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 12px; + border: 1px solid var(--base400); + border-radius: var(--border-radius); + box-shadow: 1px 1px 1px var(--base400); +} diff --git a/components/pages/reports/funnel/FunnelReport.js b/components/pages/reports/funnel/FunnelReport.js index 3c466050..da422d54 100644 --- a/components/pages/reports/funnel/FunnelReport.js +++ b/components/pages/reports/funnel/FunnelReport.js @@ -7,21 +7,35 @@ import ReportMenu from '../ReportMenu'; import ReportBody from '../ReportBody'; import Funnel from 'assets/funnel.svg'; import { useReport } from 'hooks'; +import useApi from 'hooks/useApi'; export default function FunnelReport({ reportId }) { - const report = useReport(reportId); + const report = useReport(reportId, { window: 60, urls: ['/', '/docs'] }); + const { post, useQuery } = useApi(); + const { data, isLoading, error } = useQuery( + ['report:funnel', report?.update], + () => { + const { websiteId, parameters } = report || {}; - console.log('REPORT', { report }); + return post(`/reports/funnel`, { + websiteId: websiteId, + ...parameters, + startAt: +parameters.dateRange.startDate, + endAt: +parameters.dateRange.endDate, + }); + }, + { enabled: !!report?.update }, + ); return ( - + } report={report} /> - - + + ); diff --git a/components/pages/reports/funnel/FunnelTable.js b/components/pages/reports/funnel/FunnelTable.js index 27b4bbfc..7b726e48 100644 --- a/components/pages/reports/funnel/FunnelTable.js +++ b/components/pages/reports/funnel/FunnelTable.js @@ -1,12 +1,17 @@ import DataTable from 'components/metrics/DataTable'; +import { useMessages } from 'hooks'; -export function FunnelTable({ ...props }) { - const { data } = props; +export function FunnelTable({ data }) { + const { formatMessage, labels } = useMessages(); - const tableData = - data?.map(a => ({ x: a.x, y: a.y, z: Math.floor(a.y / data[0].y) * 100 })) || []; - - return ; + return ( + + ); } export default FunnelTable; diff --git a/components/pages/settings/profile/ThemeSetting.js b/components/pages/settings/profile/ThemeSetting.js index f4503268..54a8dac8 100644 --- a/components/pages/settings/profile/ThemeSetting.js +++ b/components/pages/settings/profile/ThemeSetting.js @@ -6,13 +6,13 @@ import Moon from 'assets/moon.svg'; import styles from './ThemeSetting.module.css'; export function ThemeSetting() { - const [theme, setTheme] = useTheme(); + const { theme, saveTheme } = useTheme(); return (
+ ); }} - - + + ); } diff --git a/components/pages/reports/funnel/FunnelParameters.module.css b/components/pages/reports/funnel/FunnelParameters.module.css index d60048a7..25ef0241 100644 --- a/components/pages/reports/funnel/FunnelParameters.module.css +++ b/components/pages/reports/funnel/FunnelParameters.module.css @@ -1,12 +1,3 @@ -.popup { - background: var(--base50); - padding: 20px; - margin-left: 10px; - border: 1px solid var(--base400); - border-radius: var(--border-radius); - width: 400px; -} - .urls { display: flex; flex-direction: column; From 3ccdd95f49948202ada769530f07bc71bb58fa46 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 25 May 2023 10:26:00 -0700 Subject: [PATCH 48/88] Moved render functions into lib/charts. --- components/common/WorldMap.js | 2 +- components/metrics/BarChart.js | 8 +-- components/metrics/EventsChart.js | 12 ++-- components/metrics/PageviewsChart.js | 71 ++----------------- .../pages/reports/funnel/FunnelParameters.js | 47 ++++++------ lib/charts.js | 62 ++++++++++++++++ 6 files changed, 101 insertions(+), 101 deletions(-) create mode 100644 lib/charts.js diff --git a/components/common/WorldMap.js b/components/common/WorldMap.js index a371e2d3..31c6686b 100644 --- a/components/common/WorldMap.js +++ b/components/common/WorldMap.js @@ -86,7 +86,7 @@ export function WorldMap({ data, className }) { - {tooltip && } + {tooltip && {tooltip}}
); } diff --git a/components/metrics/BarChart.js b/components/metrics/BarChart.js index 9206b800..2aba3ea5 100644 --- a/components/metrics/BarChart.js +++ b/components/metrics/BarChart.js @@ -4,16 +4,12 @@ import classNames from 'classnames'; import Chart from 'chart.js/auto'; import HoverTooltip from 'components/common/HoverTooltip'; import Legend from 'components/metrics/Legend'; -import { formatLongNumber } from 'lib/format'; import useLocale from 'hooks/useLocale'; import useTheme from 'hooks/useTheme'; import { DEFAULT_ANIMATION_DURATION } from 'lib/constants'; +import { renderNumberLabels } from 'lib/charts'; import styles from './BarChart.module.css'; -function defaultRenderYLabel(label) { - return +label > 1000 ? formatLongNumber(label) : label; -} - export function BarChart({ datasets, unit, @@ -90,7 +86,7 @@ export function BarChart({ }, ticks: { color: colors.text, - callback: renderYLabel || defaultRenderYLabel, + callback: renderYLabel || renderNumberLabels, }, }, }, diff --git a/components/metrics/EventsChart.js b/components/metrics/EventsChart.js index eb397cc9..334ee591 100644 --- a/components/metrics/EventsChart.js +++ b/components/metrics/EventsChart.js @@ -2,16 +2,15 @@ import { useMemo } from 'react'; import { Loading } from 'react-basics'; import { colord } from 'colord'; import BarChart from './BarChart'; -import { getDateArray, getDateLength } from 'lib/date'; -import useApi from 'hooks/useApi'; -import useDateRange from 'hooks/useDateRange'; -import useTimezone from 'hooks/useTimezone'; -import usePageQuery from 'hooks/usePageQuery'; +import { getDateArray } from 'lib/date'; +import { useApi, useLocale, useDateRange, useTimezone, usePageQuery } from 'hooks'; import { EVENT_COLORS } from 'lib/constants'; +import { renderDateLabels, renderStatusTooltip } from 'lib/charts'; export function EventsChart({ websiteId, className, token }) { const { get, useQuery } = useApi(); const [{ startDate, endDate, unit, modified }] = useDateRange(websiteId); + const { locale } = useLocale(); const [timezone] = useTimezone(); const { query: { url, eventName }, @@ -70,9 +69,10 @@ export function EventsChart({ websiteId, className, token }) { datasets={datasets} unit={unit} height={300} - records={getDateLength(startDate, endDate, unit)} loading={isLoading} stacked + renderXLabel={renderDateLabels(unit, locale)} + renderTooltip={renderStatusTooltip(unit, locale)} /> ); } diff --git a/components/metrics/PageviewsChart.js b/components/metrics/PageviewsChart.js index 0dcf0ac5..bf56a0bd 100644 --- a/components/metrics/PageviewsChart.js +++ b/components/metrics/PageviewsChart.js @@ -1,74 +1,13 @@ -import { useCallback, useMemo } from 'react'; -import { StatusLight } from 'react-basics'; +import { useMemo } from 'react'; import BarChart from './BarChart'; -import useTheme from 'hooks/useTheme'; -import useMessages from 'hooks/useMessages'; -import useLocale from 'hooks/useLocale'; -import { dateFormat } from 'lib/date'; -import { formatLongNumber } from 'lib/format'; +import { useLocale, useTheme, useMessages } from 'hooks'; +import { renderDateLabels, renderStatusTooltip } from 'lib/charts'; export function PageviewsChart({ websiteId, data, unit, className, loading, ...props }) { const { formatMessage, labels } = useMessages(); const { colors } = useTheme(); const { locale } = useLocale(); - const renderXLabel = useCallback( - (label, index, values) => { - const d = new Date(values[index].value); - - switch (unit) { - case 'minute': - return dateFormat(d, 'h:mm', locale); - case 'hour': - return dateFormat(d, 'p', locale); - case 'day': - return dateFormat(d, 'MMM d', locale); - case 'month': - return dateFormat(d, 'MMM', locale); - case 'year': - return dateFormat(d, 'YYY', locale); - default: - return label; - } - }, - [locale, unit], - ); - - const renderTooltip = useCallback( - (setTooltip, model) => { - const { opacity, labelColors, dataPoints } = model.tooltip; - - if (!dataPoints?.length || !opacity) { - setTooltip(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', - }; - - setTooltip( - <> -
{dateFormat(new Date(dataPoints[0].raw.x), formats[unit], locale)}
-
- - {formatLongNumber(dataPoints[0].raw.y)} {dataPoints[0].dataset.label} - -
- , - ); - }, - [unit], - ); - const datasets = useMemo(() => { if (!data) return []; @@ -96,8 +35,8 @@ export function PageviewsChart({ websiteId, data, unit, className, loading, ...p datasets={datasets} unit={unit} loading={loading} - renderXLabel={renderXLabel} - renderTooltip={renderTooltip} + renderXLabel={renderDateLabels(unit, locale)} + renderTooltip={renderStatusTooltip(unit, locale)} /> ); } diff --git a/components/pages/reports/funnel/FunnelParameters.js b/components/pages/reports/funnel/FunnelParameters.js index bced9a1a..3caf6a8e 100644 --- a/components/pages/reports/funnel/FunnelParameters.js +++ b/components/pages/reports/funnel/FunnelParameters.js @@ -11,6 +11,7 @@ import { SubmitButton, Text, TextField, + Tooltip, } from 'react-basics'; import Icons from 'components/icons'; import { updateReport } from 'store/reports'; @@ -91,28 +92,30 @@ function AddURLButton({ onAdd }) { }; return ( - - - - - - {close => { - return ( -
- - - - - - - -
- ); - }} -
-
+ + + + + + + {close => { + return ( +
+ + + + + + + +
+ ); + }} +
+
+
); } diff --git a/lib/charts.js b/lib/charts.js new file mode 100644 index 00000000..c9d54218 --- /dev/null +++ b/lib/charts.js @@ -0,0 +1,62 @@ +import { StatusLight } from 'react-basics'; +import { dateFormat } from 'lib/date'; +import { formatLongNumber } from 'lib/format'; + +export function renderNumberLabels(label) { + return +label > 1000 ? formatLongNumber(label) : label; +} + +export function renderDateLabels(unit, locale) { + return (label, index, values) => { + const d = new Date(values[index].value); + + switch (unit) { + case 'minute': + return dateFormat(d, 'h:mm', locale); + case 'hour': + return dateFormat(d, 'p', locale); + case 'day': + return dateFormat(d, 'MMM d', locale); + case 'month': + return dateFormat(d, 'MMM', locale); + case 'year': + return dateFormat(d, 'YYY', locale); + default: + return label; + } + }; +} + +export function renderStatusTooltip(unit, locale) { + return (setTooltip, model) => { + const { opacity, labelColors, dataPoints } = model.tooltip; + + if (!dataPoints?.length || !opacity) { + setTooltip(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', + }; + + setTooltip( + <> +
{dateFormat(new Date(dataPoints[0].raw.x), formats[unit], locale)}
+
+ + {formatLongNumber(dataPoints[0].raw.y)} {dataPoints[0].dataset.label} + +
+ , + ); + }; +} From 06e6cbec9aa8e81759d966ce20b64792e67aa6d4 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 25 May 2023 16:46:37 -0700 Subject: [PATCH 49/88] Add share_id validation. --- lib/constants.ts | 2 ++ pages/api/websites/[id]/index.ts | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/lib/constants.ts b/lib/constants.ts index 425d729f..7eef10b1 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -147,6 +147,8 @@ export const EVENT_COLORS = [ export const DOMAIN_REGEX = /^(localhost(:[1-9]\d{0,4})?|((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63})$/; +export const SHARE_ID_REGEX = /^[a-zA-Z0-9]{16}$/; + export const DESKTOP_SCREEN_WIDTH = 1920; export const LAPTOP_SCREEN_WIDTH = 1024; export const MOBILE_SCREEN_WIDTH = 479; diff --git a/pages/api/websites/[id]/index.ts b/pages/api/websites/[id]/index.ts index 3f660a91..1d7e4ac3 100644 --- a/pages/api/websites/[id]/index.ts +++ b/pages/api/websites/[id]/index.ts @@ -4,6 +4,7 @@ import { Website, NextApiRequestQueryBody } from 'lib/types'; import { canViewWebsite, canUpdateWebsite, canDeleteWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { deleteWebsite, getWebsite, updateWebsite } from 'queries'; +import { SHARE_ID_REGEX } from 'lib/constants'; export interface WebsiteRequestQuery { id: string; @@ -43,6 +44,10 @@ export default async ( let website; + if (shareId && !shareId.match(SHARE_ID_REGEX)) { + return serverError(res, 'Invalid share ID.'); + } + try { website = await updateWebsite(websiteId, { name, domain, shareId }); } catch (e: any) { From 9d936fe8e59634f9b545827fb47a350d10dda730 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 25 May 2023 20:07:58 -0700 Subject: [PATCH 50/88] add ownerId to non-cache. --- lib/session.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/session.ts b/lib/session.ts index 32f3bdc8..1fedb91b 100644 --- a/lib/session.ts +++ b/lib/session.ts @@ -63,6 +63,7 @@ export async function findSession(req: NextApiRequestCollect) { subdivision1, subdivision2, city, + ownerId: website.userId, }; } From 0724201f695df711c66c4fcb74d414d20aaf3242 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 27 May 2023 13:29:09 -0700 Subject: [PATCH 51/88] Added indexes. Removed old migration. --- .../migrations/02_report_schema/migration.sql | 19 ------------------- db/postgresql/schema.prisma | 2 ++ 2 files changed, 2 insertions(+), 19 deletions(-) delete mode 100644 db/postgresql/migrations/02_report_schema/migration.sql diff --git a/db/postgresql/migrations/02_report_schema/migration.sql b/db/postgresql/migrations/02_report_schema/migration.sql deleted file mode 100644 index 8b2bf0f5..00000000 --- a/db/postgresql/migrations/02_report_schema/migration.sql +++ /dev/null @@ -1,19 +0,0 @@ --- CreateTable -CREATE TABLE "user_report" ( - "report_id" UUID NOT NULL, - "user_id" UUID NOT NULL, - "website_id" UUID NOT NULL, - "report_name" VARCHAR(200) NOT NULL, - "template_name" VARCHAR(200) NOT NULL, - "parameters" VARCHAR(6000) NOT NULL, - "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMPTZ(6), - - CONSTRAINT "user_report_pkey" PRIMARY KEY ("report_id") -); - --- CreateIndex -CREATE UNIQUE INDEX "user_report_report_id_key" ON "user_report"("report_id"); - --- CreateIndex -CREATE INDEX "user_report_user_id_idx" ON "user_report"("user_id"); diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index 318d455d..7c4cb94f 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -173,5 +173,7 @@ model Report { @@index([userId]) @@index([websiteId]) + @@index([type]) + @@index([name]) @@map("report") } From bc37f5124e6edd168912809a7ec2a23ee7bd5660 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 27 May 2023 22:36:41 -0700 Subject: [PATCH 52/88] Update report migration. --- .../migrations/02_report_schema/migration.sql | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 db/postgresql/migrations/02_report_schema/migration.sql diff --git a/db/postgresql/migrations/02_report_schema/migration.sql b/db/postgresql/migrations/02_report_schema/migration.sql new file mode 100644 index 00000000..f4f3e885 --- /dev/null +++ b/db/postgresql/migrations/02_report_schema/migration.sql @@ -0,0 +1,29 @@ +-- CreateTable +CREATE TABLE "report" ( + "report_id" UUID NOT NULL, + "user_id" UUID NOT NULL, + "website_id" UUID NOT NULL, + "type" VARCHAR(200) NOT NULL, + "name" VARCHAR(200) NOT NULL, + "description" VARCHAR(500) NOT NULL, + "parameters" VARCHAR(6000) NOT NULL, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + + CONSTRAINT "report_pkey" PRIMARY KEY ("report_id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "report_report_id_key" ON "report"("report_id"); + +-- CreateIndex +CREATE INDEX "report_user_id_idx" ON "report"("user_id"); + +-- CreateIndex +CREATE INDEX "report_website_id_idx" ON "report"("website_id"); + +-- CreateIndex +CREATE INDEX "report_type_idx" ON "report"("type"); + +-- CreateIndex +CREATE INDEX "report_name_idx" ON "report"("name"); From bfb52eb6785be6442bc2c635fd0bc6ed0f580862 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 28 May 2023 21:37:34 -0700 Subject: [PATCH 53/88] Added report context. Removed report store. --- components/common/WorldMap.js | 21 ++--- components/messages.js | 1 + components/pages/reports/Report.js | 16 +++- components/pages/reports/ReportHeader.js | 50 +++++++--- components/pages/reports/funnel/AddUrlForm.js | 45 +++++++++ .../pages/reports/funnel/FunnelChart.js | 8 +- .../pages/reports/funnel/FunnelParameters.js | 94 ++++++++----------- .../funnel/FunnelParameters.module.css | 6 +- .../pages/reports/funnel/FunnelReport.js | 36 +++---- .../pages/reports/funnel/FunnelTable.js | 7 +- hooks/useReport.js | 76 +++++++++++++-- hooks/useTheme.js | 9 ++ lib/auth.ts | 12 +-- package.json | 2 +- pages/api/reports/[id].ts | 45 +++++---- pages/api/reports/index.ts | 32 ++++--- pages/realtime/{[id]/index.js => [id].js} | 0 pages/reports/[id].js | 20 ++++ pages/reports/index.js | 10 +- .../settings/teams/{[id]/index.js => [id].js} | 0 .../settings/users/{[id]/index.js => [id].js} | 0 .../websites/{[id]/index.js => [id].js} | 0 pages/websites/{[id]/index.js => [id].js} | 0 queries/admin/report.ts | 33 +++++++ queries/admin/user.ts | 2 +- queries/admin/userReport.ts | 37 -------- queries/admin/website.ts | 2 +- queries/index.js | 2 +- rollup.tracker.config.js | 2 +- store/reports.js | 57 ----------- yarn.lock | 20 ++-- 31 files changed, 372 insertions(+), 273 deletions(-) create mode 100644 components/pages/reports/funnel/AddUrlForm.js rename pages/realtime/{[id]/index.js => [id].js} (100%) create mode 100644 pages/reports/[id].js rename pages/settings/teams/{[id]/index.js => [id].js} (100%) rename pages/settings/users/{[id]/index.js => [id].js} (100%) rename pages/settings/websites/{[id]/index.js => [id].js} (100%) rename pages/websites/{[id]/index.js => [id].js} (100%) create mode 100644 queries/admin/report.ts delete mode 100644 queries/admin/userReport.ts delete mode 100644 store/reports.js diff --git a/components/common/WorldMap.js b/components/common/WorldMap.js index 31c6686b..985160d8 100644 --- a/components/common/WorldMap.js +++ b/components/common/WorldMap.js @@ -4,7 +4,7 @@ import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simp import classNames from 'classnames'; import { colord } from 'colord'; import HoverTooltip from 'components/common/HoverTooltip'; -import { ISO_COUNTRIES, THEME_COLORS, MAP_FILE } from 'lib/constants'; +import { ISO_COUNTRIES, MAP_FILE } from 'lib/constants'; import useTheme from 'hooks/useTheme'; import useCountryNames from 'hooks/useCountryNames'; import useLocale from 'hooks/useLocale'; @@ -15,16 +15,7 @@ import styles from './WorldMap.module.css'; export function WorldMap({ data, className }) { const { basePath } = useRouter(); const [tooltip, setTooltip] = useState(); - const { theme } = useTheme(); - const colors = useMemo( - () => ({ - baseColor: THEME_COLORS[theme].primary, - fillColor: THEME_COLORS[theme].gray100, - strokeColor: THEME_COLORS[theme].primary, - hoverColor: THEME_COLORS[theme].primary, - }), - [theme], - ); + const { theme, colors } = useTheme(); const { locale } = useLocale(); const countryNames = useCountryNames(locale); const metrics = useMemo(() => (data ? percentFilter(data) : []), [data]); @@ -34,10 +25,10 @@ export function WorldMap({ data, className }) { const country = metrics?.find(({ x }) => x === code); if (!country) { - return colors.fillColor; + return colors.map.fillColor; } - return colord(colors.baseColor) + return colord(colors.map.baseColor) [theme === 'light' ? 'lighten' : 'darken'](0.4 * (1.0 - country.z / 100)) .toHex(); } @@ -70,11 +61,11 @@ export function WorldMap({ data, className }) { key={geo.rsmKey} geography={geo} fill={getFillColor(code)} - stroke={colors.strokeColor} + stroke={colors.map.strokeColor} opacity={getOpacity(code)} style={{ default: { outline: 'none' }, - hover: { outline: 'none', fill: colors.hoverColor }, + hover: { outline: 'none', fill: colors.map.hoverColor }, pressed: { outline: 'none' }, }} onMouseOver={() => handleHover(code)} diff --git a/components/messages.js b/components/messages.js index 0de3a44c..adae6aa2 100644 --- a/components/messages.js +++ b/components/messages.js @@ -128,6 +128,7 @@ export const labels = defineMessages({ add: { id: 'label.add', defaultMessage: 'Add' }, window: { id: 'label.window', defaultMessage: 'Window' }, addUrl: { id: 'label.add-url', defaultMessage: 'Add URL' }, + runQuery: { id: 'label.run-query', defaultMessage: 'Run query' }, }); export const messages = defineMessages({ diff --git a/components/pages/reports/Report.js b/components/pages/reports/Report.js index 4e3fcd04..0a98ef75 100644 --- a/components/pages/reports/Report.js +++ b/components/pages/reports/Report.js @@ -1,11 +1,19 @@ +import { createContext } from 'react'; import Page from 'components/layout/Page'; import styles from './reports.module.css'; +import { useReport } from 'hooks'; + +export const ReportContext = createContext(null); + +export function Report({ reportId, defaultParameters, children, ...props }) { + const report = useReport(reportId, defaultParameters); -export function Report({ children, ...props }) { return ( - - {children} - + + + {children} + + ); } diff --git a/components/pages/reports/ReportHeader.js b/components/pages/reports/ReportHeader.js index 7da47245..bf2c7fa8 100644 --- a/components/pages/reports/ReportHeader.js +++ b/components/pages/reports/ReportHeader.js @@ -1,42 +1,62 @@ -import { Flexbox, Icon, LoadingButton, Text, useToast } from 'react-basics'; +import { useContext } from 'react'; +import { useRouter } from 'next/router'; +import { Flexbox, Icon, LoadingButton, InlineEditField, useToast } from 'react-basics'; import WebsiteSelect from 'components/input/WebsiteSelect'; import PageHeader from 'components/layout/PageHeader'; import DateFilter from 'components/input/DateFilter'; import { parseDateRange } from 'lib/date'; -import { updateReport } from 'store/reports'; import { useMessages, useApi } from 'hooks'; +import { ReportContext } from './Report'; import styles from './reports.module.css'; -export function ReportHeader({ report, icon }) { +export function ReportHeader({ icon }) { + const { report, updateReport } = useContext(ReportContext); const { formatMessage, labels, messages } = useMessages(); const { toast, showToast } = useToast(); const { post, useMutation } = useApi(); - const { mutate, isLoading } = useMutation(data => post(`/reports`, data)); + const router = useRouter(); + const { mutate: create, isLoading: isCreating } = useMutation(data => post(`/reports`, data)); + const { mutate: update, isLoading: isUpdating } = useMutation(data => + post(`/reports/${data.id}`, data), + ); - const { id, websiteId, name, parameters } = report || {}; - const { value, startDate, endDate } = parameters?.dateRange || {}; + const { websiteId, name, dateRange } = report || {}; + const { value, startDate, endDate } = dateRange || {}; const handleSelect = websiteId => { - updateReport(id, { websiteId }); + updateReport({ websiteId }); }; const handleDateChange = value => { - updateReport(id, { parameters: { dateRange: { ...parseDateRange(value) } } }); + updateReport({ dateRange: { ...parseDateRange(value) } }); }; const handleSave = async () => { - mutate(report, { - onSuccess: async () => { - showToast({ message: formatMessage(messages.saved), variant: 'success' }); - }, - }); + if (!report.id) { + create(report, { + onSuccess: async ({ id }) => { + router.push(`/reports/${id}`, null, { shallow: true }); + showToast({ message: formatMessage(messages.saved), variant: 'success' }); + }, + }); + } else { + update(report, { + onSuccess: async () => { + showToast({ message: formatMessage(messages.saved), variant: 'success' }); + }, + }); + } + }; + + const handleNameChange = name => { + updateReport({ name }); }; const Title = () => { return ( <> {icon} - {name} + ); }; @@ -54,7 +74,7 @@ export function ReportHeader({ report, icon }) { diff --git a/components/pages/reports/funnel/AddUrlForm.js b/components/pages/reports/funnel/AddUrlForm.js new file mode 100644 index 00000000..07f34867 --- /dev/null +++ b/components/pages/reports/funnel/AddUrlForm.js @@ -0,0 +1,45 @@ +import { useState } from 'react'; +import { useMessages } from 'hooks'; +import { Button, Form, FormButtons, FormRow, TextField } from 'react-basics'; + +export function AddUrlForm({ defaultValue = '', onSave, onClose }) { + const [url, setUrl] = useState(defaultValue); + const { formatMessage, labels } = useMessages(); + + const handleSave = () => { + onSave?.(url); + setUrl(''); + onClose(); + }; + + const handleChange = e => { + setUrl(e.target.value); + }; + + const handleClose = () => { + setUrl(''); + onClose(); + }; + + return ( +
+ + + + + + + +
+ ); +} + +export default AddUrlForm; diff --git a/components/pages/reports/funnel/FunnelChart.js b/components/pages/reports/funnel/FunnelChart.js index aecd48a7..3dfeb19e 100644 --- a/components/pages/reports/funnel/FunnelChart.js +++ b/components/pages/reports/funnel/FunnelChart.js @@ -1,16 +1,18 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback, useContext, useMemo } from 'react'; import { Loading } from 'react-basics'; import useMessages from 'hooks/useMessages'; import useTheme from 'hooks/useTheme'; import BarChart from 'components/metrics/BarChart'; import { formatLongNumber } from 'lib/format'; import styles from './FunnelChart.module.css'; +import { ReportContext } from '../Report'; -export function FunnelChart({ report, data, loading, className }) { +export function FunnelChart({ className, loading }) { + const { report } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); const { colors } = useTheme(); - const { parameters } = report || {}; + const { parameters, data } = report || {}; const renderXLabel = useCallback( (label, index) => { diff --git a/components/pages/reports/funnel/FunnelParameters.js b/components/pages/reports/funnel/FunnelParameters.js index 3caf6a8e..c87d52e4 100644 --- a/components/pages/reports/funnel/FunnelParameters.js +++ b/components/pages/reports/funnel/FunnelParameters.js @@ -1,12 +1,11 @@ +import { useContext, useRef, useState } from 'react'; import { useMessages } from 'hooks'; import { - Button, Icon, Form, FormButtons, FormInput, FormRow, - ModalTrigger, Modal, SubmitButton, Text, @@ -14,30 +13,36 @@ import { Tooltip, } from 'react-basics'; import Icons from 'components/icons'; -import { updateReport } from 'store/reports'; -import { useRef, useState } from 'react'; +import AddUrlForm from './AddUrlForm'; +import { ReportContext } from 'components/pages/reports/Report'; import styles from './FunnelParameters.module.css'; -export function FunnelParameters({ report }) { +export function FunnelParameters() { + const { report, runReport, updateReport, isRunning } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); + const [show, setShow] = useState(false); const ref = useRef(null); - const { id, websiteId, parameters, isLoading } = report || {}; + const { websiteId, parameters } = report || {}; const queryDisabled = !websiteId || parameters?.urls?.length < 2; const handleSubmit = values => { - updateReport(id, { parameters: values, isLoading: false, update: Date.now() }); + runReport(values); }; - const handleAdd = url => { - updateReport(id, { parameters: { ...parameters, urls: parameters.urls.concat(url) } }); + const handleAddUrl = url => { + updateReport({ parameters: { ...parameters, urls: parameters.urls.concat(url) } }); }; - const handleRemove = index => { + const handleRemoveUrl = (index, e) => { + e.stopPropagation(); const urls = [...parameters.urls]; urls.splice(index, 1); - updateReport(id, { parameters: { ...parameters, urls } }); + updateReport({ parameters: { ...parameters, urls } }); }; + const showAddForm = () => setShow(true); + const hideAddForm = () => setShow(false); + return ( <>
@@ -49,72 +54,49 @@ export function FunnelParameters({ report }) { - }> + }>
- {parameters?.urls.map((url, index) => { + {parameters?.urls?.map((url, index) => { return (
{url} - handleRemove(index)}> - - + + + + +
); })}
- - {formatMessage(labels.query)} + + {formatMessage(labels.runQuery)} + {show && ( + + + + )} ); } -function AddURLButton({ onAdd }) { - const [url, setUrl] = useState(''); +function AddUrlButton({ onClick }) { const { formatMessage, labels } = useMessages(); - const handleAdd = close => { - onAdd?.(url); - setUrl(''); - close(); - }; - - const handleChange = e => { - setUrl(e.target.value); - }; - const handleClose = close => { - setUrl(''); - close(); - }; - return ( - - - - - - {close => { - return ( -
- - - - - - - -
- ); - }} -
-
+ + +
); } diff --git a/components/pages/reports/funnel/FunnelParameters.module.css b/components/pages/reports/funnel/FunnelParameters.module.css index 25ef0241..98ae0831 100644 --- a/components/pages/reports/funnel/FunnelParameters.module.css +++ b/components/pages/reports/funnel/FunnelParameters.module.css @@ -1,7 +1,7 @@ .urls { display: flex; flex-direction: column; - gap: 10px; + gap: 16px; } .url { @@ -14,3 +14,7 @@ border-radius: var(--border-radius); box-shadow: 1px 1px 1px var(--base400); } + +.icon { + align-self: center; +} diff --git a/components/pages/reports/funnel/FunnelReport.js b/components/pages/reports/funnel/FunnelReport.js index da422d54..26a81a09 100644 --- a/components/pages/reports/funnel/FunnelReport.js +++ b/components/pages/reports/funnel/FunnelReport.js @@ -1,41 +1,29 @@ +import { useContext } from 'react'; import FunnelChart from './FunnelChart'; import FunnelTable from './FunnelTable'; import FunnelParameters from './FunnelParameters'; -import Report from '../Report'; +import Report, { ReportContext } from '../Report'; import ReportHeader from '../ReportHeader'; import ReportMenu from '../ReportMenu'; import ReportBody from '../ReportBody'; import Funnel from 'assets/funnel.svg'; import { useReport } from 'hooks'; -import useApi from 'hooks/useApi'; + +const defaultParameters = { + type: 'funnel', + parameters: { window: 60, urls: ['/', '/docs'] }, +}; export default function FunnelReport({ reportId }) { - const report = useReport(reportId, { window: 60, urls: ['/', '/docs'] }); - const { post, useQuery } = useApi(); - const { data, isLoading, error } = useQuery( - ['report:funnel', report?.update], - () => { - const { websiteId, parameters } = report || {}; - - return post(`/reports/funnel`, { - websiteId: websiteId, - ...parameters, - startAt: +parameters.dateRange.startDate, - endAt: +parameters.dateRange.endDate, - }); - }, - { enabled: !!report?.update }, - ); - return ( - - } report={report} /> + + } /> - + - - + + ); diff --git a/components/pages/reports/funnel/FunnelTable.js b/components/pages/reports/funnel/FunnelTable.js index 7b726e48..ff6bdfb5 100644 --- a/components/pages/reports/funnel/FunnelTable.js +++ b/components/pages/reports/funnel/FunnelTable.js @@ -1,12 +1,15 @@ +import { useContext } from 'react'; import DataTable from 'components/metrics/DataTable'; import { useMessages } from 'hooks'; +import { ReportContext } from '../Report'; -export function FunnelTable({ data }) { +export function FunnelTable() { + const { report } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); return ( state[id], [id]); - const report = useStore(selector); + const loadReport = async id => { + const data = await get(`/reports/${id}`); + + setReport(data); + }; + + const runReport = useCallback( + async parameters => { + const { websiteId, type, dateRange } = report; + setIsRunning(true); + + const data = await post(`/reports/${type}`, { + websiteId: websiteId, + ...parameters, + startAt: +dateRange?.startDate, + endAt: +dateRange?.endDate, + }); + + setReport( + produce(state => { + state.parameters = parameters; + state.data = data; + + return state; + }), + ); + + setIsRunning(false); + }, + [report], + ); + + const updateReport = useCallback( + async data => { + setReport( + produce(state => { + const { parameters, ...rest } = data; + + if (parameters) { + state.parameters = { ...state.parameters, ...parameters }; + } + + for (const key in rest) { + state[key] = rest[key]; + } + + return state; + }), + ); + }, + [report], + ); useEffect(() => { - if (!report) { - const newReport = createReport(defaultParameters); - setId(newReport.id); + if (!reportId) { + setReport({ ...baseParameters, ...defaultParameters }); + } else { + loadReport(reportId); } }, []); - return report; + return { report, runReport, updateReport, isRunning }; } export default useReport; diff --git a/hooks/useTheme.js b/hooks/useTheme.js index b8b51226..7e40f601 100644 --- a/hooks/useTheme.js +++ b/hooks/useTheme.js @@ -17,6 +17,9 @@ export function useTheme() { const primaryColor = colord(THEME_COLORS[theme].primary); const colors = { + theme: { + ...THEME_COLORS[theme], + }, chart: { text: THEME_COLORS[theme].gray700, line: THEME_COLORS[theme].gray200, @@ -33,6 +36,12 @@ export function useTheme() { hoverBorderColor: primaryColor.toRgbString(), }, }, + map: { + baseColor: THEME_COLORS[theme].primary, + fillColor: THEME_COLORS[theme].gray100, + strokeColor: THEME_COLORS[theme].primary, + hoverColor: THEME_COLORS[theme].primary, + }, }; function saveTheme(value) { diff --git a/lib/auth.ts b/lib/auth.ts index 37dc6acb..bf01a1ab 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -1,4 +1,4 @@ -import { UserReport } from '@prisma/client'; +import { Report } from '@prisma/client'; import redis from '@umami/redis-client'; import debug from 'debug'; import { PERMISSIONS, ROLE_PERMISSIONS, SHARE_TOKEN_HEADER } from 'lib/constants'; @@ -135,28 +135,28 @@ export async function canDeleteWebsite({ user }: Auth, websiteId: string) { return false; } -export async function canViewUserReport(auth: Auth, userReport: UserReport) { +export async function canViewReport(auth: Auth, report: Report) { if (auth.user.isAdmin) { return true; } - if ((auth.user.id = userReport.userId)) { + if ((auth.user.id = report.userId)) { return true; } - if (await canViewWebsite(auth, userReport.websiteId)) { + if (await canViewWebsite(auth, report.websiteId)) { return true; } return false; } -export async function canUpdateUserReport(auth: Auth, userReport: UserReport) { +export async function canUpdateReport(auth: Auth, report: Report) { if (auth.user.isAdmin) { return true; } - if ((auth.user.id = userReport.userId)) { + if ((auth.user.id = report.userId)) { return true; } diff --git a/package.json b/package.json index c4a790dd..bd195662 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "maxmind": "^4.3.6", "moment-timezone": "^0.5.35", "next": "13.3.1", - "next-basics": "^0.27.0", + "next-basics": "^0.30.0", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", "react": "^18.2.0", diff --git a/pages/api/reports/[id].ts b/pages/api/reports/[id].ts index 42002d18..d1c6c89a 100644 --- a/pages/api/reports/[id].ts +++ b/pages/api/reports/[id].ts @@ -1,59 +1,68 @@ -import { canUpdateUserReport, canViewUserReport } from 'lib/auth'; +import { canUpdateReport, canViewReport } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getUserReportById, updateUserReport } from 'queries'; +import { getReportById, updateReport } from 'queries'; -export interface UserReportRequestQuery { +export interface ReportRequestQuery { id: string; } -export interface UserReportRequestBody { +export interface ReportRequestBody { websiteId: string; - reportName: string; - templateName: string; + type: string; + name: string; + description: string; parameters: string; } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); await useAuth(req, res); if (req.method === 'GET') { - const { id: userReportId } = req.query; + const { id: reportId } = req.query; - const data = await getUserReportById(userReportId); + const data = await getReportById(reportId); - if (!(await canViewUserReport(req.auth, data))) { + if (!(await canViewReport(req.auth, data))) { return unauthorized(res); } + data.parameters = JSON.parse(data.parameters); + return ok(res, data); } if (req.method === 'POST') { - const { id: userReportId } = req.query; + const { id: reportId } = req.query; - const data = await getUserReportById(userReportId); + const { websiteId, type, name, description, parameters } = req.body; - if (!(await canUpdateUserReport(req.auth, data))) { + const data = await getReportById(reportId); + + if (!(await canUpdateReport(req.auth, data))) { return unauthorized(res); } - const updated = await updateUserReport( + const result = await updateReport( { - ...req.body, - }, + websiteId, + type, + name, + description, + parameters: JSON.stringify(parameters), + } as any, { - id: userReportId, + id: reportId, }, ); - return ok(res, updated); + return ok(res, result); } return methodNotAllowed(res); diff --git a/pages/api/reports/index.ts b/pages/api/reports/index.ts index e5855355..55dc4bf5 100644 --- a/pages/api/reports/index.ts +++ b/pages/api/reports/index.ts @@ -3,17 +3,21 @@ import { useAuth, useCors } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok } from 'next-basics'; -import { createUserReport, getUserReports } from 'queries'; +import { createReport, getReports } from 'queries'; -export interface UserReportRequestBody { +export interface ReportRequestBody { websiteId: string; - reportName: string; - templateName: string; - parameters: string; + name: string; + type: string; + description: string; + parameters: { + window: string; + urls: string[]; + }; } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); @@ -24,19 +28,25 @@ export default async ( } = req.auth; if (req.method === 'GET') { - const data = await getUserReports(userId); + const data = await getReports(userId); return ok(res, data); } if (req.method === 'POST') { - const data = await createUserReport({ + const { websiteId, type, name, description, parameters } = req.body; + + const result = await createReport({ id: uuid(), userId, - ...req.body, - }); + websiteId, + type, + name, + description, + parameters: JSON.stringify(parameters), + } as any); - return ok(res, data); + return ok(res, result); } return methodNotAllowed(res); diff --git a/pages/realtime/[id]/index.js b/pages/realtime/[id].js similarity index 100% rename from pages/realtime/[id]/index.js rename to pages/realtime/[id].js diff --git a/pages/reports/[id].js b/pages/reports/[id].js new file mode 100644 index 00000000..95f20a78 --- /dev/null +++ b/pages/reports/[id].js @@ -0,0 +1,20 @@ +import { useRouter } from 'next/router'; +import AppLayout from 'components/layout/AppLayout'; +import FunnelReport from 'components/pages/reports/funnel/FunnelReport'; +import useMessages from 'hooks/useMessages'; + +export default function ReportsPage() { + const { formatMessage, labels } = useMessages(); + const router = useRouter(); + const { id } = router.query; + + if (!id) { + return null; + } + + return ( + + + + ); +} diff --git a/pages/reports/index.js b/pages/reports/index.js index 70f684cb..6da2b943 100644 --- a/pages/reports/index.js +++ b/pages/reports/index.js @@ -1,13 +1,21 @@ +import { useState } from 'react'; +import { Item, Tabs } from 'react-basics'; import AppLayout from 'components/layout/AppLayout'; import ReportList from 'components/pages/reports/ReportList'; import useMessages from 'hooks/useMessages'; export default function ReportsPage() { + const [tab, setTab] = useState('create'); const { formatMessage, labels } = useMessages(); return ( - + + {formatMessage(labels.reports)} + {formatMessage(labels.save)} + + {tab === 'create' && } + {tab === 'saved' &&

My reports

}
); } diff --git a/pages/settings/teams/[id]/index.js b/pages/settings/teams/[id].js similarity index 100% rename from pages/settings/teams/[id]/index.js rename to pages/settings/teams/[id].js diff --git a/pages/settings/users/[id]/index.js b/pages/settings/users/[id].js similarity index 100% rename from pages/settings/users/[id]/index.js rename to pages/settings/users/[id].js diff --git a/pages/settings/websites/[id]/index.js b/pages/settings/websites/[id].js similarity index 100% rename from pages/settings/websites/[id]/index.js rename to pages/settings/websites/[id].js diff --git a/pages/websites/[id]/index.js b/pages/websites/[id].js similarity index 100% rename from pages/websites/[id]/index.js rename to pages/websites/[id].js diff --git a/queries/admin/report.ts b/queries/admin/report.ts new file mode 100644 index 00000000..506902f5 --- /dev/null +++ b/queries/admin/report.ts @@ -0,0 +1,33 @@ +import { Prisma, Report } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function createReport(data: Prisma.ReportUncheckedCreateInput): Promise { + return prisma.client.report.create({ data }); +} + +export async function getReportById(reportId: string): Promise { + return prisma.client.report.findUnique({ + where: { + id: reportId, + }, + }); +} + +export async function getReports(userId: string): Promise { + return prisma.client.report.findMany({ + where: { + userId, + }, + }); +} + +export async function updateReport( + data: Prisma.ReportUpdateInput, + where: Prisma.ReportWhereUniqueInput, +): Promise { + return prisma.client.report.update({ data, where }); +} + +export async function deleteReport(where: Prisma.ReportWhereUniqueInput): Promise { + return prisma.client.report.delete({ where }); +} diff --git a/queries/admin/user.ts b/queries/admin/user.ts index b7f452c7..3ba2654b 100644 --- a/queries/admin/user.ts +++ b/queries/admin/user.ts @@ -210,7 +210,7 @@ export async function deleteUser( }, }, }), - client.userReport.deleteMany({ + client.report.deleteMany({ where: { OR: [ { diff --git a/queries/admin/userReport.ts b/queries/admin/userReport.ts deleted file mode 100644 index d31b512e..00000000 --- a/queries/admin/userReport.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Prisma, UserReport } from '@prisma/client'; -import prisma from 'lib/prisma'; - -export async function createUserReport( - data: Prisma.UserReportUncheckedCreateInput, -): Promise { - return prisma.client.userReport.create({ data }); -} - -export async function getUserReportById(userReportId: string): Promise { - return prisma.client.userReport.findUnique({ - where: { - id: userReportId, - }, - }); -} - -export async function getUserReports(userId: string): Promise { - return prisma.client.userReport.findMany({ - where: { - userId, - }, - }); -} - -export async function updateUserReport( - data: Prisma.UserReportUpdateInput, - where: Prisma.UserReportWhereUniqueInput, -): Promise { - return prisma.client.userReport.update({ data, where }); -} - -export async function deleteUserReport( - where: Prisma.UserReportWhereUniqueInput, -): Promise { - return prisma.client.userReport.delete({ where }); -} diff --git a/queries/admin/website.ts b/queries/admin/website.ts index e6d53fce..8c381d6d 100644 --- a/queries/admin/website.ts +++ b/queries/admin/website.ts @@ -92,7 +92,7 @@ export async function deleteWebsite( websiteId, }, }), - client.userReport.deleteMany({ + client.report.deleteMany({ where: { websiteId, }, diff --git a/queries/index.js b/queries/index.js index e565df25..302c88db 100644 --- a/queries/index.js +++ b/queries/index.js @@ -1,7 +1,7 @@ export * from './admin/team'; export * from './admin/teamUser'; export * from './admin/user'; -export * from './admin/userReport'; +export * from './admin/report'; export * from './admin/website'; export * from './analytics/event/getEventMetrics'; export * from './analytics/event/getEventUsage'; diff --git a/rollup.tracker.config.js b/rollup.tracker.config.js index f4e7223c..827e46f3 100644 --- a/rollup.tracker.config.js +++ b/rollup.tracker.config.js @@ -4,7 +4,7 @@ import replace from '@rollup/plugin-replace'; import { terser } from 'rollup-plugin-terser'; export default { - input: 'tracker/index.js', + input: 'tracker/[id].js', output: { file: 'public/script.js', format: 'iife', diff --git a/store/reports.js b/store/reports.js deleted file mode 100644 index fa658d4d..00000000 --- a/store/reports.js +++ /dev/null @@ -1,57 +0,0 @@ -import { create } from 'zustand'; -import produce from 'immer'; -import { getRandomChars } from 'next-basics'; - -const emptyReport = { - name: 'Untitled', - description: '', - parameters: {}, -}; - -const initialState = {}; - -const store = create(() => ({ ...initialState })); - -export function updateReport(id, data) { - const report = store.getState()[id]; - - if (report) { - store.setState( - produce(state => { - const item = state[id]; - const { parameters, ...rest } = data; - - if (parameters) { - item.parameters = { ...item.parameters, ...parameters }; - } - - for (const key in rest) { - item[key] = rest[key]; - } - - return state; - }), - ); - } -} - -export function createReport(parameters) { - const id = `new_${getRandomChars(16)}`; - const report = { ...emptyReport, id, parameters }; - - store.setState( - produce(state => { - state[id] = report; - - return state; - }), - ); - - return report; -} - -export default store; - -if (typeof window !== 'undefined') { - window.__STORE__ = store; -} diff --git a/yarn.lock b/yarn.lock index 1d0bc7fd..e2c1432d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3715,11 +3715,6 @@ balanced-match@^2.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9" integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA== -base-x@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a" - integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw== - base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -7019,14 +7014,14 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -next-basics@^0.27.0: - version "0.27.0" - resolved "https://registry.yarnpkg.com/next-basics/-/next-basics-0.27.0.tgz#7d46c88de4b53cadfef86230f8cbe7dba6f10dc6" - integrity sha512-ZviF4O4/14eBjGG7fK83oswuM/rur37TRxcjXCKPJN4kOTUgrzn9Sz+vpzkB8PZ2WaHv5ONQ7TkBEwnhMCEyMQ== +next-basics@^0.30.0: + version "0.30.0" + resolved "https://registry.yarnpkg.com/next-basics/-/next-basics-0.30.0.tgz#1d5f1b3bd3cd66218f00ab0390659a819ed274fa" + integrity sha512-IWNrBsxM8watkvWHWkUp5PC0WypqeHMEaj5rCufjEfwuIe++ePtDrYgUKY6Se6y6QJWbyHliozCpa1KpVyZHnQ== dependencies: - base-x "^4.0.0" bcryptjs "^2.4.3" jsonwebtoken "^9.0.0" + pure-rand "^6.0.2" next@13.3.1: version "13.3.1" @@ -8144,6 +8139,11 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== +pure-rand@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.2.tgz#a9c2ddcae9b68d736a8163036f088a2781c8b306" + integrity sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ== + qs@~6.5.2: version "6.5.3" resolved "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz" From e9b0d3f7963a400acbaeb7a1fb310635c30b264a Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 28 May 2023 22:28:11 -0700 Subject: [PATCH 54/88] Updated date range handling. --- components/input/DateFilter.js | 2 +- components/input/WebsiteDateFilter.js | 3 +-- components/metrics/DatePickerForm.js | 16 ++++++++++------ lib/date.js | 19 +++++++++++++------ 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/components/input/DateFilter.js b/components/input/DateFilter.js index b7521e27..7fc4319d 100644 --- a/components/input/DateFilter.js +++ b/components/input/DateFilter.js @@ -65,7 +65,7 @@ export function DateFilter({ ].filter(n => n); const renderValue = value => { - return value === 'custom' ? ( + return value.startsWith('range') ? ( handleChange('custom')} /> ) : ( options.find(e => e.value === value).label diff --git a/components/input/WebsiteDateFilter.js b/components/input/WebsiteDateFilter.js index 0d5d7569..71075dd7 100644 --- a/components/input/WebsiteDateFilter.js +++ b/components/input/WebsiteDateFilter.js @@ -1,4 +1,3 @@ -import { getDateRangeValues } from 'lib/date'; import useApi from 'hooks/useApi'; import useDateRange from 'hooks/useDateRange'; import DateFilter from './DateFilter'; @@ -13,7 +12,7 @@ export default function WebsiteDateFilter({ websiteId, value }) { const data = await get(`/websites/${websiteId}`); if (data) { - setDateRange({ value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) }); + setDateRange(`range:${new Date(data.createdAt)}:${Date.now()}`); } } else if (value !== 'all') { setDateRange(value); diff --git a/components/metrics/DatePickerForm.js b/components/metrics/DatePickerForm.js index 96730591..53f027bb 100644 --- a/components/metrics/DatePickerForm.js +++ b/components/metrics/DatePickerForm.js @@ -2,7 +2,6 @@ import { useState } from 'react'; import { Button, ButtonGroup, Calendar } from 'react-basics'; import { isAfter, isBefore, isSameDay } from 'date-fns'; import useLocale from 'hooks/useLocale'; -import { getDateRangeValues } from 'lib/date'; import { getDateLocale } from 'lib/lang'; import { FILTER_DAY, FILTER_RANGE } from 'lib/constants'; import useMessages from 'hooks/useMessages'; @@ -19,7 +18,7 @@ export function DatePickerForm({ const [selected, setSelected] = useState( isSameDay(defaultStartDate, defaultEndDate) ? FILTER_DAY : FILTER_RANGE, ); - const [date, setDate] = useState(defaultStartDate); + const [singleDate, setSingleDate] = useState(defaultStartDate); const [startDate, setStartDate] = useState(defaultStartDate); const [endDate, setEndDate] = useState(defaultEndDate); const { locale } = useLocale(); @@ -27,14 +26,14 @@ export function DatePickerForm({ const disabled = selected === FILTER_DAY - ? isAfter(minDate, date) && isBefore(maxDate, date) + ? isAfter(minDate, singleDate) && isBefore(maxDate, singleDate) : isAfter(startDate, endDate); const handleSave = () => { if (selected === FILTER_DAY) { - onChange({ ...getDateRangeValues(date, date), value: 'custom' }); + onChange(`range:${singleDate.getTime()}:${singleDate.getTime()}`); } else { - onChange({ ...getDateRangeValues(startDate, endDate), value: 'custom' }); + onChange(`range:${startDate.getTime()}:${endDate.getTime()}`); } }; @@ -48,7 +47,12 @@ export function DatePickerForm({
{selected === FILTER_DAY && ( - + )} {selected === FILTER_RANGE && ( <> diff --git a/lib/date.js b/lib/date.js index 1cfca75d..526354b3 100644 --- a/lib/date.js +++ b/lib/date.js @@ -40,20 +40,27 @@ export function getLocalTime(t) { export function parseDateRange(value, locale = 'en-US') { if (typeof value === 'object') { - const { startDate, endDate } = value; + return value; + } + + if (value?.startsWith?.('range')) { + const [, startAt, endAt] = value.split(':'); + + const startDate = new Date(+startAt); + const endDate = new Date(+endAt); + return { - ...value, - startDate: typeof startDate === 'string' ? parseISO(startDate) : startDate, - endDate: typeof endDate === 'string' ? parseISO(endDate) : endDate, + ...getDateRangeValues(startDate, endDate), + value, }; } const now = new Date(); const dateLocale = getDateLocale(locale); - const match = value.match(/^(?[0-9-]+)(?hour|day|week|month|year)$/); + const match = value?.match?.(/^(?[0-9-]+)(?hour|day|week|month|year)$/); - if (!match) return {}; + if (!match) return null; const { num, unit } = match.groups; From 0d567ebe603cc9e5ee2710c2455e12b01e84343a Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Mon, 29 May 2023 21:19:53 -0700 Subject: [PATCH 55/88] Delete event_data on website reset --- queries/admin/website.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/queries/admin/website.ts b/queries/admin/website.ts index 8c381d6d..8714d8c9 100644 --- a/queries/admin/website.ts +++ b/queries/admin/website.ts @@ -50,6 +50,9 @@ export async function resetWebsite( const { client, transaction } = prisma; return transaction([ + client.eventData.deleteMany({ + where: { websiteId }, + }), client.websiteEvent.deleteMany({ where: { websiteId }, }), From 43f5854f157c3e356cb6f9ad8b5722fdd536545a Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 30 May 2023 16:49:22 -0700 Subject: [PATCH 56/88] Update report parameters. --- components/pages/reports/Report.js | 2 ++ components/pages/reports/ReportHeader.js | 11 ++++++----- hooks/useReport.js | 10 +++------- pages/api/reports/[id].ts | 4 ++++ pages/api/reports/funnel.ts | 20 ++++++++++++-------- 5 files changed, 27 insertions(+), 20 deletions(-) diff --git a/components/pages/reports/Report.js b/components/pages/reports/Report.js index 0a98ef75..0239e64f 100644 --- a/components/pages/reports/Report.js +++ b/components/pages/reports/Report.js @@ -8,6 +8,8 @@ export const ReportContext = createContext(null); export function Report({ reportId, defaultParameters, children, ...props }) { const report = useReport(reportId, defaultParameters); + //console.log(report); + return ( diff --git a/components/pages/reports/ReportHeader.js b/components/pages/reports/ReportHeader.js index bf2c7fa8..42d78e75 100644 --- a/components/pages/reports/ReportHeader.js +++ b/components/pages/reports/ReportHeader.js @@ -20,15 +20,16 @@ export function ReportHeader({ icon }) { post(`/reports/${data.id}`, data), ); - const { websiteId, name, dateRange } = report || {}; + const { name, parameters } = report || {}; + const { websiteId, dateRange } = parameters || {}; const { value, startDate, endDate } = dateRange || {}; - const handleSelect = websiteId => { - updateReport({ websiteId }); + const handleWebsiteSelect = websiteId => { + updateReport({ parameters: { websiteId } }); }; const handleDateChange = value => { - updateReport({ dateRange: { ...parseDateRange(value) } }); + updateReport({ parameters: { dateRange: { ...parseDateRange(value) } } }); }; const handleSave = async () => { @@ -71,7 +72,7 @@ export function ReportHeader({ icon }) { onChange={handleDateChange} showAllTime /> - + { - const { websiteId, type, dateRange } = report; setIsRunning(true); - const data = await post(`/reports/${type}`, { - websiteId: websiteId, - ...parameters, - startAt: +dateRange?.startDate, - endAt: +dateRange?.endDate, - }); + const { type } = report; + + const data = await post(`/reports/${type}`, parameters); setReport( produce(state => { diff --git a/pages/api/reports/[id].ts b/pages/api/reports/[id].ts index d1c6c89a..bcd22b4e 100644 --- a/pages/api/reports/[id].ts +++ b/pages/api/reports/[id].ts @@ -40,6 +40,9 @@ export default async ( if (req.method === 'POST') { const { id: reportId } = req.query; + const { + user: { id: userId }, + } = req.auth; const { websiteId, type, name, description, parameters } = req.body; @@ -52,6 +55,7 @@ export default async ( const result = await updateReport( { websiteId, + userId, type, name, description, diff --git a/pages/api/reports/funnel.ts b/pages/api/reports/funnel.ts index 4308c09d..1e121326 100644 --- a/pages/api/reports/funnel.ts +++ b/pages/api/reports/funnel.ts @@ -9,8 +9,10 @@ export interface FunnelRequestBody { websiteId: string; urls: string[]; window: number; - startAt: number; - endAt: number; + dateRange: { + startDate: string; + endDate: string; + }; } export interface FunnelResponse { @@ -28,18 +30,20 @@ export default async ( await useAuth(req, res); if (req.method === 'POST') { - const { websiteId, urls, window, startAt, endAt } = req.body; + const { + websiteId, + urls, + window, + dateRange: { startDate, endDate }, + } = req.body; if (!(await canViewWebsite(req.auth, websiteId))) { return unauthorized(res); } - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - const data = await getPageviewFunnel(websiteId, { - startDate, - endDate, + startDate: new Date(startDate), + endDate: new Date(endDate), urls, windowMinutes: +window, }); From 77f758205bf3acfbfd1f313141afbbf1ee73a9ca Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 30 May 2023 19:58:28 -0700 Subject: [PATCH 57/88] Fixed tracker config. --- rollup.tracker.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rollup.tracker.config.js b/rollup.tracker.config.js index 827e46f3..f4e7223c 100644 --- a/rollup.tracker.config.js +++ b/rollup.tracker.config.js @@ -4,7 +4,7 @@ import replace from '@rollup/plugin-replace'; import { terser } from 'rollup-plugin-terser'; export default { - input: 'tracker/[id].js', + input: 'tracker/index.js', output: { file: 'public/script.js', format: 'iife', From 1038a54fe4c46aa3edc0641dfa07c0c6c9aff411 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Wed, 31 May 2023 12:01:16 -0700 Subject: [PATCH 58/88] add mysql 02 migration --- .../migrations/02_report_schema/migration.sql | 19 +++++++++++++++ db/mysql/schema.prisma | 23 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 db/mysql/migrations/02_report_schema/migration.sql diff --git a/db/mysql/migrations/02_report_schema/migration.sql b/db/mysql/migrations/02_report_schema/migration.sql new file mode 100644 index 00000000..0476e9f7 --- /dev/null +++ b/db/mysql/migrations/02_report_schema/migration.sql @@ -0,0 +1,19 @@ +-- CreateTable +CREATE TABLE `report` ( + `report_id` VARCHAR(36) NOT NULL, + `user_id` VARCHAR(36) NOT NULL, + `website_id` VARCHAR(36) NOT NULL, + `type` VARCHAR(200) NOT NULL, + `name` VARCHAR(200) NOT NULL, + `description` VARCHAR(500) NOT NULL, + `parameters` VARCHAR(6000) NOT NULL, + `created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0), + `updated_at` TIMESTAMP(0) NULL, + + UNIQUE INDEX `report_report_id_key`(`report_id`), + INDEX `report_user_id_idx`(`user_id`), + INDEX `report_website_id_idx`(`website_id`), + INDEX `report_type_idx`(`type`), + INDEX `report_name_idx`(`name`), + PRIMARY KEY (`report_id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/db/mysql/schema.prisma b/db/mysql/schema.prisma index 6455c8c0..0752f418 100644 --- a/db/mysql/schema.prisma +++ b/db/mysql/schema.prisma @@ -19,6 +19,7 @@ model User { website Website[] teamUser TeamUser[] + Report Report[] @@map("user") } @@ -59,6 +60,7 @@ model Website { user User? @relation(fields: [userId], references: [id]) teamWebsite TeamWebsite[] eventData EventData[] + Report Report[] @@index([userId]) @@index([createdAt]) @@ -155,3 +157,24 @@ model TeamWebsite { @@index([websiteId]) @@map("team_website") } + +model Report { + id String @id() @unique() @map("report_id") @db.VarChar(36) + userId String @map("user_id") @db.VarChar(36) + websiteId String @map("website_id") @db.VarChar(36) + type String @map("type") @db.VarChar(200) + name String @map("name") @db.VarChar(200) + description String @map("description") @db.VarChar(500) + parameters String @map("parameters") @db.VarChar(6000) + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) + updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0) + + user User @relation(fields: [userId], references: [id]) + website Website @relation(fields: [websiteId], references: [id]) + + @@index([userId]) + @@index([websiteId]) + @@index([type]) + @@index([name]) + @@map("report") +} \ No newline at end of file From b484286523273c3bc2bbd40cb28ab011bab356c9 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 31 May 2023 21:46:49 -0700 Subject: [PATCH 59/88] Feat/um 305 unique session ch (#2065) * Add session_data / session redis to CH. * Add mysql migration. --- components/pages/console/TestConsole.js | 45 +++++-- db/clickhouse/migrations/01_edit_keys.sql | 4 + db/clickhouse/schema.sql | 24 ++-- .../migrations/03_session_data/migration.sql | 37 ++++++ db/mysql/schema.prisma | 54 +++++--- .../migrations/03_session_data/migration.sql | 31 +++++ db/postgresql/schema.prisma | 45 +++++-- lib/clickhouse.ts | 4 +- lib/constants.ts | 7 +- lib/{eventData.ts => dynamicData.ts} | 28 ++-- lib/prisma.ts | 4 +- lib/session.ts | 23 +--- lib/types.ts | 16 ++- pages/api/send.ts | 124 +++++++++++------- pages/api/users/[id]/index.ts | 4 +- pages/api/users/index.ts | 4 +- queries/admin/user.ts | 4 +- queries/analytics/event/saveEvent.ts | 4 +- queries/analytics/eventData/saveEventData.ts | 42 +++--- queries/analytics/session/createSession.ts | 112 ++++------------ queries/analytics/session/getSession.ts | 39 +----- queries/analytics/session/saveSessionData.ts | 43 ++++++ tracker/index.js | 7 +- 23 files changed, 405 insertions(+), 300 deletions(-) create mode 100644 db/clickhouse/migrations/01_edit_keys.sql create mode 100644 db/mysql/migrations/03_session_data/migration.sql create mode 100644 db/postgresql/migrations/03_session_data/migration.sql rename lib/{eventData.ts => dynamicData.ts} (63%) create mode 100644 queries/analytics/session/saveSessionData.ts diff --git a/components/pages/console/TestConsole.js b/components/pages/console/TestConsole.js index 6c14c2c1..eda93f0b 100644 --- a/components/pages/console/TestConsole.js +++ b/components/pages/console/TestConsole.js @@ -28,22 +28,41 @@ export function TestConsole() { window.umami.track({ url: '/page-view', referrer: 'https://www.google.com' }); window.umami.track('track-event-no-data'); window.umami.track('track-event-with-data', { - data: { + test: 'test-data', + boolean: true, + booleanError: 'true', + time: new Date(), + number: 1, + time2: new Date().toISOString(), + nested: { test: 'test-data', - boolean: true, - booleanError: 'true', - time: new Date(), number: 1, - time2: new Date().toISOString(), - nested: { + object: { test: 'test-data', - number: 1, - object: { - test: 'test-data', - }, }, - array: [1, 2, 3], }, + array: [1, 2, 3], + }); + } + + function handleIdentifyClick() { + window.umami.identify({ + userId: 123, + name: 'brian', + number: Math.random() * 100, + test: 'test-data', + boolean: true, + booleanError: 'true', + time: new Date(), + time2: new Date().toISOString(), + nested: { + test: 'test-data', + number: 1, + object: { + test: 'test-data', + }, + }, + array: [1, 2, 3], }); } @@ -116,6 +135,10 @@ export function TestConsole() { +

+ diff --git a/db/clickhouse/migrations/01_edit_keys.sql b/db/clickhouse/migrations/01_edit_keys.sql new file mode 100644 index 00000000..b948ff38 --- /dev/null +++ b/db/clickhouse/migrations/01_edit_keys.sql @@ -0,0 +1,4 @@ +ALTER TABLE "event_data" RENAME COLUMN "event_date_value" TO "date_value"; +ALTER TABLE "event_data" RENAME COLUMN "event_numeric_value" TO "numeric_value"; +ALTER TABLE "event_data" RENAME COLUMN "event_string_value" TO "string_value"; +ALTER TABLE "event_data" RENAME COLUMN "event_data_type" TO "data_type"; \ No newline at end of file diff --git a/db/clickhouse/schema.sql b/db/clickhouse/schema.sql index 8f48b434..de1082ec 100644 --- a/db/clickhouse/schema.sql +++ b/db/clickhouse/schema.sql @@ -117,10 +117,10 @@ CREATE TABLE umami.event_data url_path String, event_name String, event_key String, - event_string_value Nullable(String), - event_numeric_value Nullable(Decimal64(4)), --922337203685477.5625 - event_date_value Nullable(DateTime('UTC')), - event_data_type UInt32, + string_value Nullable(String), + numeric_value Nullable(Decimal64(4)), --922337203685477.5625 + date_value Nullable(DateTime('UTC')), + data_type UInt32, created_at DateTime('UTC') ) engine = MergeTree @@ -134,10 +134,10 @@ CREATE TABLE umami.event_data_queue ( url_path String, event_name String, event_key String, - event_string_value Nullable(String), - event_numeric_value Nullable(Decimal64(4)), --922337203685477.5625 - event_date_value Nullable(DateTime('UTC')), - event_data_type UInt32, + string_value Nullable(String), + numeric_value Nullable(Decimal64(4)), --922337203685477.5625 + date_value Nullable(DateTime('UTC')), + data_type UInt32, created_at DateTime('UTC'), --virtual columns _error String, @@ -158,10 +158,10 @@ SELECT website_id, url_path, event_name, event_key, - event_string_value, - event_numeric_value, - event_date_value, - event_data_type, + string_value, + numeric_value, + date_value, + data_type, created_at FROM umami.event_data_queue; diff --git a/db/mysql/migrations/03_session_data/migration.sql b/db/mysql/migrations/03_session_data/migration.sql new file mode 100644 index 00000000..3618080d --- /dev/null +++ b/db/mysql/migrations/03_session_data/migration.sql @@ -0,0 +1,37 @@ +/* + Warnings: + + - The primary key for the `event_data` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to drop the column `event_data_type` on the `event_data` table. All the data in the column will be lost. + - You are about to drop the column `event_date_value` on the `event_data` table. All the data in the column will be lost. + - You are about to drop the column `event_id` on the `event_data` table. All the data in the column will be lost. + - You are about to drop the column `event_numeric_value` on the `event_data` table. All the data in the column will be lost. + - You are about to drop the column `event_string_value` on the `event_data` table. All the data in the column will be lost. + - Added the required column `data_type` to the `event_data` table without a default value. This is not possible if the table is not empty. + - Added the required column `event_data_id` to the `event_data` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE `event_data` RENAME COLUMN `event_data_type` TO `data_type`; +ALTER TABLE `event_data` RENAME COLUMN `event_date_value` TO `date_value`; +ALTER TABLE `event_data` RENAME COLUMN `event_id` TO `event_data_id`; +ALTER TABLE `event_data` RENAME COLUMN `event_numeric_value` TO `numeric_value`; +ALTER TABLE `event_data` RENAME COLUMN `event_string_value` TO `string_value`; + +-- CreateTable +CREATE TABLE `session_data` ( + `session_data_id` VARCHAR(36) NOT NULL, + `website_id` VARCHAR(36) NOT NULL, + `session_id` VARCHAR(36) NOT NULL, + `event_key` VARCHAR(500) NOT NULL, + `event_string_value` VARCHAR(500) NULL, + `event_numeric_value` DECIMAL(19, 4) NULL, + `event_date_value` TIMESTAMP(0) NULL, + `event_data_type` INTEGER UNSIGNED NOT NULL, + `created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0), + + INDEX `session_data_created_at_idx`(`created_at`), + INDEX `session_data_website_id_idx`(`website_id`), + INDEX `session_data_session_id_idx`(`session_id`), + PRIMARY KEY (`session_data_id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/db/mysql/schema.prisma b/db/mysql/schema.prisma index 0752f418..88d5ef80 100644 --- a/db/mysql/schema.prisma +++ b/db/mysql/schema.prisma @@ -14,12 +14,12 @@ model User { password String @db.VarChar(60) role String @map("role") @db.VarChar(50) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) - updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamp(0) + updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0) deletedAt DateTime? @map("deleted_at") @db.Timestamp(0) website Website[] teamUser TeamUser[] - Report Report[] + report Report[] @@map("user") } @@ -40,6 +40,7 @@ model Session { createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) websiteEvent WebsiteEvent[] + sessionData SessionData[] @@index([createdAt]) @@index([websiteId]) @@ -54,13 +55,14 @@ model Website { resetAt DateTime? @map("reset_at") @db.Timestamp(0) userId String? @map("user_id") @db.VarChar(36) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) - updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamp(0) + updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0) deletedAt DateTime? @map("deleted_at") @db.Timestamp(0) user User? @relation(fields: [userId], references: [id]) teamWebsite TeamWebsite[] eventData EventData[] - Report Report[] + report Report[] + sessionData SessionData[] @@index([userId]) @@index([createdAt]) @@ -94,15 +96,15 @@ model WebsiteEvent { } model EventData { - id String @id() @map("event_id") @db.VarChar(36) - websiteEventId String @map("website_event_id") @db.VarChar(36) - websiteId String @map("website_id") @db.VarChar(36) - eventKey String @map("event_key") @db.VarChar(500) - eventStringValue String? @map("event_string_value") @db.VarChar(500) - eventNumericValue Decimal? @map("event_numeric_value") @db.Decimal(19, 4) - eventDateValue DateTime? @map("event_date_value") @db.Timestamp(0) - eventDataType Int @map("event_data_type") @db.UnsignedInt - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) + id String @id() @map("event_data_id") @db.VarChar(36) + websiteEventId String @map("website_event_id") @db.VarChar(36) + websiteId String @map("website_id") @db.VarChar(36) + eventKey String @map("event_key") @db.VarChar(500) + stringValue String? @map("string_value") @db.VarChar(500) + numericValue Decimal? @map("numeric_value") @db.Decimal(19, 4) + dateValue DateTime? @map("date_value") @db.Timestamp(0) + dataType Int @map("data_type") @db.UnsignedInt + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) website Website @relation(fields: [websiteId], references: [id]) websiteEvent WebsiteEvent @relation(fields: [websiteEventId], references: [id]) @@ -114,12 +116,32 @@ model EventData { @@map("event_data") } +model SessionData { + id String @id() @map("session_data_id") @db.VarChar(36) + websiteId String @map("website_id") @db.VarChar(36) + sessionId String @map("session_id") @db.VarChar(36) + eventKey String @map("event_key") @db.VarChar(500) + eventStringValue String? @map("event_string_value") @db.VarChar(500) + eventNumericValue Decimal? @map("event_numeric_value") @db.Decimal(19, 4) + eventDateValue DateTime? @map("event_date_value") @db.Timestamp(0) + eventDataType Int @map("event_data_type") @db.UnsignedInt + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) + + website Website @relation(fields: [websiteId], references: [id]) + session Session @relation(fields: [sessionId], references: [id]) + + @@index([createdAt]) + @@index([websiteId]) + @@index([sessionId]) + @@map("session_data") +} + model Team { id String @id() @unique() @map("team_id") @db.VarChar(36) name String @db.VarChar(50) accessCode String? @unique @map("access_code") @db.VarChar(50) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) - updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamp(0) + updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0) teamUser TeamUser[] teamWebsite TeamWebsite[] @@ -134,7 +156,7 @@ model TeamUser { userId String @map("user_id") @db.VarChar(36) role String @map("role") @db.VarChar(50) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) - updatedAt DateTime? @map("updated_at") @updatedAt @db.Timestamp(0) + updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0) team Team @relation(fields: [teamId], references: [id]) user User @relation(fields: [userId], references: [id]) @@ -177,4 +199,4 @@ model Report { @@index([type]) @@index([name]) @@map("report") -} \ No newline at end of file +} diff --git a/db/postgresql/migrations/03_session_data/migration.sql b/db/postgresql/migrations/03_session_data/migration.sql new file mode 100644 index 00000000..233c3793 --- /dev/null +++ b/db/postgresql/migrations/03_session_data/migration.sql @@ -0,0 +1,31 @@ +-- AlterTable +ALTER TABLE "event_data" RENAME COLUMN "event_data_type" TO "data_type"; +ALTER TABLE "event_data" RENAME COLUMN "event_date_value" TO "date_value"; +ALTER TABLE "event_data" RENAME COLUMN "event_id" TO "event_data_id"; +ALTER TABLE "event_data" RENAME COLUMN "event_numeric_value" TO "numeric_value"; +ALTER TABLE "event_data" RENAME COLUMN "event_string_value" TO "string_value"; + +-- CreateTable +CREATE TABLE "session_data" ( + "session_data_id" UUID NOT NULL, + "website_id" UUID NOT NULL, + "session_id" UUID NOT NULL, + "session_key" VARCHAR(500) NOT NULL, + "string_value" VARCHAR(500), + "numeric_value" DECIMAL(19,4), + "date_value" TIMESTAMPTZ(6), + "data_type" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "deleted_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "session_data_pkey" PRIMARY KEY ("session_data_id") +); + +-- CreateIndex +CREATE INDEX "session_data_created_at_idx" ON "session_data"("created_at"); + +-- CreateIndex +CREATE INDEX "session_data_website_id_idx" ON "session_data"("website_id"); + +-- CreateIndex +CREATE INDEX "session_data_session_id_idx" ON "session_data"("session_id"); diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index 7c4cb94f..bfd71fd1 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -19,7 +19,7 @@ model User { website Website[] teamUser TeamUser[] - Report Report[] + report Report[] @@map("user") } @@ -40,6 +40,7 @@ model Session { createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) websiteEvent WebsiteEvent[] + sessionData SessionData[] @@index([createdAt]) @@index([websiteId]) @@ -60,7 +61,8 @@ model Website { user User? @relation(fields: [userId], references: [id]) teamWebsite TeamWebsite[] eventData EventData[] - Report Report[] + report Report[] + sessionData SessionData[] @@index([userId]) @@index([createdAt]) @@ -94,15 +96,15 @@ model WebsiteEvent { } model EventData { - id String @id() @map("event_id") @db.Uuid - websiteId String @map("website_id") @db.Uuid - websiteEventId String @map("website_event_id") @db.Uuid - eventKey String @map("event_key") @db.VarChar(500) - eventStringValue String? @map("event_string_value") @db.VarChar(500) - eventNumericValue Decimal? @map("event_numeric_value") @db.Decimal(19, 4) - eventDateValue DateTime? @map("event_date_value") @db.Timestamptz(6) - eventDataType Int @map("event_data_type") @db.Integer - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + id String @id() @map("event_data_id") @db.Uuid + websiteId String @map("website_id") @db.Uuid + websiteEventId String @map("website_event_id") @db.Uuid + eventKey String @map("event_key") @db.VarChar(500) + stringValue String? @map("string_value") @db.VarChar(500) + numericValue Decimal? @map("numeric_value") @db.Decimal(19, 4) + dateValue DateTime? @map("date_value") @db.Timestamptz(6) + dataType Int @map("data_type") @db.Integer + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) website Website @relation(fields: [websiteId], references: [id]) websiteEvent WebsiteEvent @relation(fields: [websiteEventId], references: [id]) @@ -113,6 +115,27 @@ model EventData { @@map("event_data") } +model SessionData { + id String @id() @map("session_data_id") @db.Uuid + websiteId String @map("website_id") @db.Uuid + sessionId String @map("session_id") @db.Uuid + sessionKey String @map("session_key") @db.VarChar(500) + stringValue String? @map("string_value") @db.VarChar(500) + numericValue Decimal? @map("numeric_value") @db.Decimal(19, 4) + dateValue DateTime? @map("date_value") @db.Timestamptz(6) + dataType Int @map("data_type") @db.Integer + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + deletedAt DateTime? @default(now()) @map("deleted_at") @db.Timestamptz(6) + + website Website @relation(fields: [websiteId], references: [id]) + session Session @relation(fields: [sessionId], references: [id]) + + @@index([createdAt]) + @@index([websiteId]) + @@index([sessionId]) + @@map("session_data") +} + model Team { id String @id() @unique() @map("team_id") @db.Uuid name String @db.VarChar(50) diff --git a/lib/clickhouse.ts b/lib/clickhouse.ts index e97be806..c91cf3da 100644 --- a/lib/clickhouse.ts +++ b/lib/clickhouse.ts @@ -2,7 +2,7 @@ import { ClickHouse } from 'clickhouse'; import dateFormat from 'dateformat'; import debug from 'debug'; import { CLICKHOUSE } from 'lib/db'; -import { getEventDataType } from './eventData'; +import { getDynamicDataType } from './dynamicData'; import { WebsiteMetricFilter } from './types'; import { FILTER_COLUMNS } from './constants'; @@ -74,7 +74,7 @@ function getEventDataFilterQuery( params: any, ) { const query = filters.reduce((ac, cv, i) => { - const type = getEventDataType(cv.eventValue); + const type = getDynamicDataType(cv.eventValue); let value = cv.eventValue; diff --git a/lib/constants.ts b/lib/constants.ts index 7eef10b1..9edbe100 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -42,6 +42,11 @@ export const SESSION_COLUMNS = [ 'city', ]; +export const COLLECTION_TYPE = { + event: 'event', + identify: 'identify', +}; + export const FILTER_COLUMNS = { url: 'url_path', referrer: 'referrer_domain', @@ -56,7 +61,7 @@ export const EVENT_TYPE = { customEvent: 2, } as const; -export const EVENT_DATA_TYPE = { +export const DYNAMIC_DATA_TYPE = { string: 1, number: 2, boolean: 3, diff --git a/lib/eventData.ts b/lib/dynamicData.ts similarity index 63% rename from lib/eventData.ts rename to lib/dynamicData.ts index aee1f9b4..da8eb8b2 100644 --- a/lib/eventData.ts +++ b/lib/dynamicData.ts @@ -1,12 +1,12 @@ import { isValid, parseISO } from 'date-fns'; -import { EVENT_DATA_TYPE } from './constants'; -import { EventDataTypes } from './types'; +import { DYNAMIC_DATA_TYPE } from './constants'; +import { DynamicDataType } from './types'; export function flattenJSON( eventData: { [key: string]: any }, - keyValues: { key: string; value: any; eventDataType: EventDataTypes }[] = [], + keyValues: { key: string; value: any; dynamicDataType: DynamicDataType }[] = [], parentKey = '', -): { key: string; value: any; eventDataType: EventDataTypes }[] { +): { key: string; value: any; dynamicDataType: DynamicDataType }[] { return Object.keys(eventData).reduce( (acc, key) => { const value = eventData[key]; @@ -25,7 +25,7 @@ export function flattenJSON( ).keyValues; } -export function getEventDataType(value: any): string { +export function getDynamicDataType(value: any): string { let type: string = typeof value; if ((type === 'string' && isValid(value)) || isValid(parseISO(value))) { @@ -36,34 +36,34 @@ export function getEventDataType(value: any): string { } function createKey(key, value, acc: { keyValues: any[]; parentKey: string }) { - const type = getEventDataType(value); + const type = getDynamicDataType(value); - let eventDataType = null; + let dynamicDataType = null; switch (type) { case 'number': - eventDataType = EVENT_DATA_TYPE.number; + dynamicDataType = DYNAMIC_DATA_TYPE.number; break; case 'string': - eventDataType = EVENT_DATA_TYPE.string; + dynamicDataType = DYNAMIC_DATA_TYPE.string; break; case 'boolean': - eventDataType = EVENT_DATA_TYPE.boolean; + dynamicDataType = DYNAMIC_DATA_TYPE.boolean; value = value ? 'true' : 'false'; break; case 'date': - eventDataType = EVENT_DATA_TYPE.date; + dynamicDataType = DYNAMIC_DATA_TYPE.date; break; case 'object': - eventDataType = EVENT_DATA_TYPE.array; + dynamicDataType = DYNAMIC_DATA_TYPE.array; value = JSON.stringify(value); break; default: - eventDataType = EVENT_DATA_TYPE.string; + dynamicDataType = DYNAMIC_DATA_TYPE.string; break; } - acc.keyValues.push({ key, value, eventDataType }); + acc.keyValues.push({ key, value, dynamicDataType }); } function getKeyName(key, parentKey) { diff --git a/lib/prisma.ts b/lib/prisma.ts index fdd8a58d..51707049 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -1,7 +1,7 @@ import prisma from '@umami/prisma-client'; import moment from 'moment-timezone'; import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db'; -import { getEventDataType } from './eventData'; +import { getDynamicDataType } from './dynamicData'; import { FILTER_COLUMNS } from './constants'; const MYSQL_DATE_FORMATS = { @@ -85,7 +85,7 @@ function getEventDataFilterQuery( params: any[], ) { const query = filters.reduce((ac, cv) => { - const type = getEventDataType(cv.eventValue); + const type = getDynamicDataType(cv.eventValue); let value = cv.eventValue; diff --git a/lib/session.ts b/lib/session.ts index 1fedb91b..04cbc9b0 100644 --- a/lib/session.ts +++ b/lib/session.ts @@ -1,12 +1,11 @@ -import clickhouse from 'lib/clickhouse'; import { secret, uuid } from 'lib/crypto'; import { getClientInfo, getJsonBody } from 'lib/detect'; import { parseToken } from 'next-basics'; import { CollectRequestBody, NextApiRequestCollect } from 'pages/api/send'; import { createSession } from 'queries'; import { validate } from 'uuid'; -import { loadSession, loadWebsite } from './query'; import cache from './cache'; +import { loadSession, loadWebsite } from './query'; export async function findSession(req: NextApiRequestCollect) { const { payload } = getJsonBody(req); @@ -46,26 +45,8 @@ export async function findSession(req: NextApiRequestCollect) { const { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device } = await getClientInfo(req, payload); - const sessionId = uuid(websiteId, hostname, ip, userAgent); - // Clickhouse does not require session lookup - if (clickhouse.enabled) { - return { - id: sessionId, - websiteId, - hostname, - browser, - os, - device, - screen, - language, - country, - subdivision1, - subdivision2, - city, - ownerId: website.userId, - }; - } + const sessionId = uuid(websiteId, hostname, ip, userAgent); // Find session let session = await loadSession(sessionId); diff --git a/lib/types.ts b/lib/types.ts index 37c1ffdc..05c09120 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,18 +1,20 @@ import { NextApiRequest } from 'next'; -import { EVENT_DATA_TYPE, EVENT_TYPE, KAFKA_TOPIC, ROLES } from './constants'; +import { COLLECTION_TYPE, DYNAMIC_DATA_TYPE, EVENT_TYPE, KAFKA_TOPIC, ROLES } from './constants'; type ObjectValues = T[keyof T]; -export type Roles = ObjectValues; +export type CollectionType = ObjectValues; -export type EventTypes = ObjectValues; +export type Role = ObjectValues; -export type EventDataTypes = ObjectValues; +export type EventType = ObjectValues; -export type KafkaTopics = ObjectValues; +export type DynamicDataType = ObjectValues; -export interface EventData { - [key: string]: number | string | EventData | number[] | string[] | EventData[]; +export type KafkaTopic = ObjectValues; + +export interface DynamicData { + [key: string]: number | string | DynamicData | number[] | string[] | DynamicData[]; } export interface Auth { diff --git a/pages/api/send.ts b/pages/api/send.ts index df7ceb6e..24264fa3 100644 --- a/pages/api/send.ts +++ b/pages/api/send.ts @@ -7,6 +7,9 @@ import { getJsonBody, getIpAddress } from 'lib/detect'; import { secret } from 'lib/crypto'; import { NextApiRequest, NextApiResponse } from 'next'; import { Resolver } from 'dns/promises'; +import { CollectionType } from 'lib/types'; +import { COLLECTION_TYPE } from 'lib/constants'; +import { saveSessionData } from 'queries/analytics/session/saveSessionData'; export interface CollectRequestBody { payload: { @@ -20,7 +23,7 @@ export interface CollectRequestBody { website: string; name: string; }; - type: string; + type: CollectionType; } export interface NextApiRequestCollect extends NextApiRequest { @@ -52,17 +55,81 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => { const { type, payload } = getJsonBody(req); - if (type !== 'event') { + validateBody(res, { type, payload }); + + if (await hasBlockedIp(req)) { + return forbidden(res); + } + + const { url, referrer, name: eventName, data: dynamicData, title: pageTitle } = payload; + + await useSession(req, res); + + const session = req.session; + + if (type === COLLECTION_TYPE.event) { + // eslint-disable-next-line prefer-const + let [urlPath, urlQuery] = url?.split('?') || []; + let [referrerPath, referrerQuery] = referrer?.split('?') || []; + let referrerDomain; + + if (!urlPath) { + urlPath = '/'; + } + + if (referrerPath?.startsWith('http')) { + const refUrl = new URL(referrer); + referrerPath = refUrl.pathname; + referrerQuery = refUrl.search.substring(1); + referrerDomain = refUrl.hostname.replace(/www\./, ''); + } + + if (process.env.REMOVE_TRAILING_SLASH) { + urlPath = urlPath.replace(/.+\/$/, ''); + } + + await saveEvent({ + urlPath, + urlQuery, + referrerPath, + referrerQuery, + referrerDomain, + pageTitle, + eventName, + eventData: dynamicData, + ...session, + sessionId: session.id, + }); + } + + if (type === COLLECTION_TYPE.identify) { + if (!dynamicData) { + return badRequest(res, 'Data required.'); + } + + await saveSessionData({ ...session, sessionData: dynamicData, sessionId: session.id }); + } + + const token = createToken(session, secret()); + + return send(res, token); +}; + +function validateBody(res: NextApiResponse, { type, payload }: CollectRequestBody) { + const { data } = payload; + + // Validate type + if (type !== COLLECTION_TYPE.event && type !== COLLECTION_TYPE.identify) { return badRequest(res, 'Wrong payload type.'); } - const { url, referrer, name: eventName, data: eventData, title: pageTitle } = payload; - // Validate eventData is JSON - if (eventData && !(typeof eventData === 'object' && !Array.isArray(eventData))) { + if (data && !(typeof data === 'object' && !Array.isArray(data))) { return badRequest(res, 'Invalid event data.'); } +} +async function hasBlockedIp(req: NextApiRequestCollect) { const ignoreIps = process.env.IGNORE_IP; const ignoreHostnames = process.env.IGNORE_HOSTNAME; @@ -100,49 +167,6 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => { return false; }); - if (blocked) { - return forbidden(res); - } + return blocked; } - - await useSession(req, res); - - const session = req.session; - - // eslint-disable-next-line prefer-const - let [urlPath, urlQuery] = url?.split('?') || []; - let [referrerPath, referrerQuery] = referrer?.split('?') || []; - let referrerDomain; - - if (!urlPath) { - urlPath = '/'; - } - - if (referrerPath?.startsWith('http')) { - const refUrl = new URL(referrer); - referrerPath = refUrl.pathname; - referrerQuery = refUrl.search.substring(1); - referrerDomain = refUrl.hostname.replace(/www\./, ''); - } - - if (process.env.REMOVE_TRAILING_SLASH) { - urlPath = urlPath.replace(/.+\/$/, ''); - } - - await saveEvent({ - urlPath, - urlQuery, - referrerPath, - referrerQuery, - referrerDomain, - pageTitle, - eventName, - eventData, - ...session, - sessionId: session.id, - }); - - const token = createToken(session, secret()); - - return send(res, token); -}; +} diff --git a/pages/api/users/[id]/index.ts b/pages/api/users/[id]/index.ts index de4642cb..a4ab05ff 100644 --- a/pages/api/users/[id]/index.ts +++ b/pages/api/users/[id]/index.ts @@ -1,4 +1,4 @@ -import { NextApiRequestQueryBody, Roles, User } from 'lib/types'; +import { NextApiRequestQueryBody, Role, User } from 'lib/types'; import { canDeleteUser, canUpdateUser, canViewUser } from 'lib/auth'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; @@ -12,7 +12,7 @@ export interface UserRequestQuery { export interface UserRequestBody { username: string; password: string; - role: Roles; + role: Role; } export default async ( diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index 4d35d856..c6103c35 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -2,7 +2,7 @@ import { canCreateUser, canViewUsers } from 'lib/auth'; import { ROLES } from 'lib/constants'; import { uuid } from 'lib/crypto'; import { useAuth } from 'lib/middleware'; -import { NextApiRequestQueryBody, Roles, User } from 'lib/types'; +import { NextApiRequestQueryBody, Role, User } from 'lib/types'; import { NextApiResponse } from 'next'; import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createUser, getUser, getUsers } from 'queries'; @@ -11,7 +11,7 @@ export interface UsersRequestBody { username: string; password: string; id: string; - role?: Roles; + role?: Role; } export default async ( diff --git a/queries/admin/user.ts b/queries/admin/user.ts index 3ba2654b..ec23559f 100644 --- a/queries/admin/user.ts +++ b/queries/admin/user.ts @@ -3,7 +3,7 @@ import { getRandomChars } from 'next-basics'; import cache from 'lib/cache'; import { ROLES } from 'lib/constants'; import prisma from 'lib/prisma'; -import { Website, User, Roles } from 'lib/types'; +import { Website, User, Role } from 'lib/types'; export async function getUser( where: Prisma.UserWhereInput | Prisma.UserWhereUniqueInput, @@ -91,7 +91,7 @@ export async function createUser(data: { id: string; username: string; password: string; - role: Roles; + role: Role; }): Promise<{ id: string; username: string; diff --git a/queries/analytics/event/saveEvent.ts b/queries/analytics/event/saveEvent.ts index 9a7db00d..51087a59 100644 --- a/queries/analytics/event/saveEvent.ts +++ b/queries/analytics/event/saveEvent.ts @@ -133,9 +133,10 @@ async function clickhouseQuery(data: { const createdAt = getDateFormat(new Date()); const message = { + ...args, website_id: websiteId, session_id: sessionId, - event_id: eventId, + event_id: uuid(), country: country ? country : null, subdivision1: country && subdivision1 ? `${country}-${subdivision1}` : null, subdivision2: subdivision2 ? subdivision2 : null, @@ -149,7 +150,6 @@ async function clickhouseQuery(data: { event_type: eventName ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, event_name: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null, created_at: createdAt, - ...args, }; await sendMessage(message, 'event'); diff --git a/queries/analytics/eventData/saveEventData.ts b/queries/analytics/eventData/saveEventData.ts index 90e63565..96ea8831 100644 --- a/queries/analytics/eventData/saveEventData.ts +++ b/queries/analytics/eventData/saveEventData.ts @@ -1,11 +1,11 @@ import { Prisma } from '@prisma/client'; -import { EVENT_DATA_TYPE } from 'lib/constants'; +import { DYNAMIC_DATA_TYPE } from 'lib/constants'; import { uuid } from 'lib/crypto'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import { flattenJSON } from 'lib/eventData'; +import { flattenJSON } from 'lib/dynamicData'; import kafka from 'lib/kafka'; import prisma from 'lib/prisma'; -import { EventData } from 'lib/types'; +import { DynamicData } from 'lib/types'; export async function saveEventData(args: { websiteId: string; @@ -13,7 +13,7 @@ export async function saveEventData(args: { sessionId?: string; urlPath?: string; eventName?: string; - eventData: EventData; + eventData: DynamicData; createdAt?: string; }) { return runQuery({ @@ -25,7 +25,7 @@ export async function saveEventData(args: { async function relationalQuery(data: { websiteId: string; eventId: string; - eventData: EventData; + eventData: DynamicData; }): Promise { const { websiteId, eventId, eventData } = data; @@ -36,16 +36,16 @@ async function relationalQuery(data: { id: uuid(), websiteEventId: eventId, websiteId, - eventKey: a.key, - eventStringValue: - a.eventDataType === EVENT_DATA_TYPE.string || - a.eventDataType === EVENT_DATA_TYPE.boolean || - a.eventDataType === EVENT_DATA_TYPE.array + key: a.key, + stringValue: + a.dynamicDataType === DYNAMIC_DATA_TYPE.string || + a.dynamicDataType === DYNAMIC_DATA_TYPE.boolean || + a.dynamicDataType === DYNAMIC_DATA_TYPE.array ? a.value : null, - eventNumericValue: a.eventDataType === EVENT_DATA_TYPE.number ? a.value : null, - eventDateValue: a.eventDataType === EVENT_DATA_TYPE.date ? new Date(a.value) : null, - eventDataType: a.eventDataType, + numericValue: a.dynamicDataType === DYNAMIC_DATA_TYPE.number ? a.value : null, + dateValue: a.dynamicDataType === DYNAMIC_DATA_TYPE.date ? new Date(a.value) : null, + dataType: a.dynamicDataType, })); return prisma.client.eventData.createMany({ @@ -59,7 +59,7 @@ async function clickhouseQuery(data: { sessionId?: string; urlPath?: string; eventName?: string; - eventData: EventData; + eventData: DynamicData; createdAt?: string; }) { const { websiteId, sessionId, eventId, urlPath, eventName, eventData, createdAt } = data; @@ -75,15 +75,15 @@ async function clickhouseQuery(data: { url_path: urlPath, event_name: eventName, event_key: a.key, - event_string_value: - a.eventDataType === EVENT_DATA_TYPE.string || - a.eventDataType === EVENT_DATA_TYPE.boolean || - a.eventDataType === EVENT_DATA_TYPE.array + string_value: + a.dynamicDataType === DYNAMIC_DATA_TYPE.string || + a.dynamicDataType === DYNAMIC_DATA_TYPE.boolean || + a.dynamicDataType === DYNAMIC_DATA_TYPE.array ? a.value : null, - event_numeric_value: a.eventDataType === EVENT_DATA_TYPE.number ? a.value : null, - event_date_value: a.eventDataType === EVENT_DATA_TYPE.date ? getDateFormat(a.value) : null, - event_data_type: a.eventDataType, + numeric_value: a.dynamicDataType === DYNAMIC_DATA_TYPE.number ? a.value : null, + date_value: a.dynamicDataType === DYNAMIC_DATA_TYPE.date ? getDateFormat(a.value) : null, + data_type: a.dynamicDataType, created_at: createdAt, })); diff --git a/queries/analytics/session/createSession.ts b/queries/analytics/session/createSession.ts index 22f7892f..4fd36d2e 100644 --- a/queries/analytics/session/createSession.ts +++ b/queries/analytics/session/createSession.ts @@ -1,23 +1,8 @@ -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import kafka from 'lib/kafka'; -import prisma from 'lib/prisma'; -import cache from 'lib/cache'; import { Prisma } from '@prisma/client'; +import cache from 'lib/cache'; +import prisma from 'lib/prisma'; -export async function createSession(args: Prisma.SessionCreateInput) { - return runQuery({ - [PRISMA]: () => relationalQuery(args), - [CLICKHOUSE]: () => clickhouseQuery(args), - }).then(async data => { - if (cache.enabled) { - await cache.storeSession(data); - } - - return data; - }); -} - -async function relationalQuery(data: Prisma.SessionCreateInput) { +export async function createSession(data: Prisma.SessionCreateInput) { const { id, websiteId, @@ -33,71 +18,28 @@ async function relationalQuery(data: Prisma.SessionCreateInput) { city, } = data; - return prisma.client.session.create({ - data: { - id, - websiteId, - hostname, - browser, - os, - device, - screen, - language, - country, - subdivision1: country && subdivision1 ? `${country}-${subdivision1}` : null, - subdivision2, - city, - }, - }); -} - -async function clickhouseQuery(data: { - id: string; - websiteId: string; - hostname?: string; - browser?: string; - os?: string; - device?: string; - screen?: string; - language?: string; - country?: string; - subdivision1?: string; - subdivision2?: string; - city?: string; -}) { - const { - id, - websiteId, - hostname, - browser, - os, - device, - screen, - language, - country, - subdivision1, - subdivision2, - city, - } = data; - const { getDateFormat, sendMessage } = kafka; - - const msg = { - session_id: id, - website_id: websiteId, - hostname, - browser, - os, - device, - screen, - language, - country, - subdivision1, - subdivision2, - city, - created_at: getDateFormat(new Date()), - }; - - await sendMessage(msg, 'event'); - - return data; + return prisma.client.session + .create({ + data: { + id, + websiteId, + hostname, + browser, + os, + device, + screen, + language, + country, + subdivision1: country && subdivision1 ? `${country}-${subdivision1}` : null, + subdivision2, + city, + }, + }) + .then(async data => { + if (cache.enabled) { + await cache.storeSession(data); + } + + return data; + }); } diff --git a/queries/analytics/session/getSession.ts b/queries/analytics/session/getSession.ts index d226e832..2fd8d18f 100644 --- a/queries/analytics/session/getSession.ts +++ b/queries/analytics/session/getSession.ts @@ -1,43 +1,8 @@ -import clickhouse from 'lib/clickhouse'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import prisma from 'lib/prisma'; import { Prisma } from '@prisma/client'; +import prisma from 'lib/prisma'; -export async function getSession(args: { id: string }) { - return runQuery({ - [PRISMA]: () => relationalQuery(args), - [CLICKHOUSE]: () => clickhouseQuery(args), - }); -} - -async function relationalQuery(where: Prisma.SessionWhereUniqueInput) { +export async function getSession(where: Prisma.SessionWhereUniqueInput) { return prisma.client.session.findUnique({ where, }); } - -async function clickhouseQuery({ id: sessionId }: { id: string }) { - const { rawQuery, findFirst } = clickhouse; - const params = { sessionId }; - - return rawQuery( - `select - session_id, - website_id, - created_at, - hostname, - browser, - os, - device, - screen, - language, - country, - subdivision1, - subdivision2, - city - from website_event - where session_id = {sessionId:UUID} - limit 1`, - params, - ).then(result => findFirst(result)); -} diff --git a/queries/analytics/session/saveSessionData.ts b/queries/analytics/session/saveSessionData.ts new file mode 100644 index 00000000..76842b4f --- /dev/null +++ b/queries/analytics/session/saveSessionData.ts @@ -0,0 +1,43 @@ +import { DYNAMIC_DATA_TYPE } from 'lib/constants'; +import { uuid } from 'lib/crypto'; +import { flattenJSON } from 'lib/dynamicData'; +import prisma from 'lib/prisma'; +import { DynamicData } from 'lib/types'; + +export async function saveSessionData(data: { + websiteId: string; + sessionId: string; + sessionData: DynamicData; +}) { + const { client, transaction } = prisma; + const { websiteId, sessionId, sessionData } = data; + + const jsonKeys = flattenJSON(sessionData); + + const flattendData = jsonKeys.map(a => ({ + id: uuid(), + websiteId, + sessionId, + key: a.key, + stringValue: + a.dynamicDataType === DYNAMIC_DATA_TYPE.string || + a.dynamicDataType === DYNAMIC_DATA_TYPE.boolean || + a.dynamicDataType === DYNAMIC_DATA_TYPE.array + ? a.value + : null, + numericValue: a.dynamicDataType === DYNAMIC_DATA_TYPE.number ? a.value : null, + dateValue: a.dynamicDataType === DYNAMIC_DATA_TYPE.date ? new Date(a.value) : null, + dataType: a.dynamicDataType, + })); + + return transaction([ + client.sessionData.deleteMany({ + where: { + sessionId, + }, + }), + client.sessionData.createMany({ + data: flattendData, + }), + ]); +} diff --git a/tracker/index.js b/tracker/index.js index 1c40036e..6127ed19 100644 --- a/tracker/index.js +++ b/tracker/index.js @@ -173,7 +173,7 @@ } }; - const send = payload => { + const send = (payload, type = 'event') => { if (trackingDisabled()) return; const headers = { 'Content-Type': 'application/json', @@ -183,7 +183,7 @@ } return fetch(endpoint, { method: 'POST', - body: JSON.stringify({ type: 'event', payload }), + body: JSON.stringify({ type, payload }), headers, }) .then(res => res.text()) @@ -205,11 +205,14 @@ return send(getPayload()); }; + const identify = data => send({ ...getPayload(), data }, 'identify'); + /* Start */ if (!window.umami) { window.umami = { track, + identify, }; } From dc6da1e41fe5546e5217379ff54d4cd6eadeaa98 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 1 Jun 2023 10:34:39 -0700 Subject: [PATCH 60/88] Update yarn. --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index bd195662..1eb7aa5a 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", "react": "^18.2.0", - "react-basics": "^0.77.0", + "react-basics": "^0.82.0", "react-beautiful-dnd": "^13.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.4", diff --git a/yarn.lock b/yarn.lock index e2c1432d..e7d7e8e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8191,10 +8191,10 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-basics@^0.77.0: - version "0.77.0" - resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.77.0.tgz#31d35b4db8119c699eeab24a7cab10a613b549f4" - integrity sha512-L14dZqlg7P9m700OvND1fsdZA/vvLH3W0Ntw5Oyk2RxE47K6oMESSuPhDi1yC2QjDYwFdKzGSsgJGfWlc++Kww== +react-basics@^0.82.0: + version "0.82.0" + resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.82.0.tgz#1df241f4ef97a8d0c7c81d4954d065efc2c415a5" + integrity sha512-DrHuRJqncx7cWYFz4rAahEsCsF3s5W1CJyJY276EUphfn0NDKZcvnrSnr6G+KTUAEkvksvlAm1t3nDv85coUxg== dependencies: classnames "^2.3.1" date-fns "^2.29.3" From a3639d59a348f63e42a09ac17f6d828037baf559 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 1 Jun 2023 11:54:24 -0700 Subject: [PATCH 61/88] Add increment to usage. --- lib/cache.ts | 6 ++++++ lib/session.ts | 2 ++ 2 files changed, 8 insertions(+) diff --git a/lib/cache.ts b/lib/cache.ts index e63a53bb..7ee7bf28 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -54,6 +54,11 @@ async function fetchUserBlock(userId: string) { return redis.get(key); } +async function incrementUserBlock(userId: string) { + const key = `user:block:${userId}`; + return redis.incr(key); +} + export default { fetchWebsite, storeWebsite, @@ -65,5 +70,6 @@ export default { storeSession, deleteSession, fetchUserBlock, + incrementUserBlock, enabled: redis.enabled, }; diff --git a/lib/session.ts b/lib/session.ts index 04cbc9b0..29ff694f 100644 --- a/lib/session.ts +++ b/lib/session.ts @@ -80,6 +80,8 @@ export async function findSession(req: NextApiRequestCollect) { async function checkUserBlock(userId: string) { if (process.env.ENABLE_BLOCKER && (await cache.fetchUserBlock(userId))) { + await cache.incrementUserBlock(userId); + throw new Error('Usage Limit.'); } } From 1869a809cfdccb3b4443dff187b9f34fdb6466bb Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 1 Jun 2023 19:30:07 -0700 Subject: [PATCH 62/88] Fixed incorrect data field. --- tracker/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracker/index.js b/tracker/index.js index 6127ed19..1686df42 100644 --- a/tracker/index.js +++ b/tracker/index.js @@ -106,7 +106,7 @@ } }); - return track(eventName, { data: eventData }); + return track(eventName, eventData); } return Promise.resolve(); }; From d575b9c377c580dd30918b2fdb734c9aac6854eb Mon Sep 17 00:00:00 2001 From: C-A de Salaberry Date: Fri, 2 Jun 2023 15:06:19 +0200 Subject: [PATCH 63/88] chore(tracker): :label: could not use `string` in `EventData` - EventData was incorrectly typed `Record` which did not allow for the most common `string` attribute - UmamiTracker is now exported to be used elsewhere if needed - A note has been added on event names being truncated to 50 characters --- tracker/index.d.ts | 151 +++++++++++++++++++++++++-------------------- 1 file changed, 83 insertions(+), 68 deletions(-) diff --git a/tracker/index.d.ts b/tracker/index.d.ts index 67cebc08..f9bd4b24 100644 --- a/tracker/index.d.ts +++ b/tracker/index.d.ts @@ -1,4 +1,4 @@ -type TrackedProperties = { +export type TrackedProperties = { /** * Hostname of server * @@ -55,7 +55,7 @@ type TrackedProperties = { website: string; }; -type WithRequired = T & { [P in K]-?: T[P] } +export type WithRequired = T & { [P in K]-?: T[P] }; /** * @@ -65,74 +65,89 @@ type WithRequired = T & { [P in K]-?: T[P] } * - Arrays are converted to a String, with the same max length of 500. * - Objects have a max of 50 properties. Arrays are considered 1 property. */ -type EventData = Record; -type EventProperties = { +export interface EventData { + [key: string]: number | string | EventData | number[] | string[] | EventData[]; +} + +export type EventProperties = { + /** + * NOTE: event names will be truncated past 50 characters + */ name: string; data?: EventData; -} & WithRequired -| WithRequired; +} & WithRequired; +export type PageViewProperties = WithRequired; +export type CustomEventFunction = ( + props: PageViewProperties, +) => EventProperties | PageViewProperties; + +export type UmamiTracker = { + track: { + /** + * Track a page view + * + * @example ``` + * umami.track(); + * ``` + */ + (): Promise; + + /** + * Track an event with a given name + * + * NOTE: event names will be truncated past 50 characters + * + * @example ``` + * umami.track('signup-button'); + * ``` + */ + (eventName: string): Promise; + + /** + * Tracks an event with dynamic data. + * + * NOTE: event names will be truncated past 50 characters + * + * When tracking events, the default properties are included in the payload. This is equivalent to running: + * + * ```js + * umami.track(props => ({ + * ...props, + * name: 'signup-button', + * data: { + * name: 'newsletter', + * id: 123 + * } + * })); + * ``` + * + * @example ``` + * umami.track('signup-button', { name: 'newsletter', id: 123 }); + * ``` + */ + (eventName: string, obj: EventData): Promise; + + /** + * Tracks a page view with custom properties + * + * @example ``` + * umami.track({ website: 'e676c9b4-11e4-4ef1-a4d7-87001773e9f2', url: '/home', title: 'Home page' }); + * ``` + */ + (properties: PageViewProperties): Promise; + + /** + * Tracks an event with fully customizable dynamic data + * Ilf you don't specify any `name` and/or `data`, it will be treated as a page view + * + * @example ``` + * umami.track((props) => ({ ...props, url: path })); + * ``` + */ + (eventFunction: CustomEventFunction): Promise; + }; +}; interface Window { - umami: { - track: { - /** - * Track a page view - * - * @example ``` - * umami.track(); - * ``` - */ - (): Promise; - - /** - * Track an event with a given name - * - * @example ``` - * umami.track('signup-button'); - * ``` - */ - (eventName: string): Promise; - - /** - * Tracks an event with dynamic data. - * - * When tracking events, the default properties are included in the payload. This is equivalent to running: - * - * ```js - * umami.track(props => ({ - * ...props, - * name: 'signup-button', - * data: { - * name: 'newsletter', - * id: 123 - * } - * })); - * ``` - * - * @example ``` - * umami.track('signup-button', { name: 'newsletter', id: 123 }); - * ``` - */ - (eventName: string, obj: EventData): Promise; - - /** - * Tracks a page view with custom properties - * - * @example ``` - * umami.track({ website: 'e676c9b4-11e4-4ef1-a4d7-87001773e9f2', url: '/home', title: 'Home page' }); - * ``` - */ - (properties: WithRequired, 'website'>): Promise; - - /** - * Tracks an event with fully customizable dynamic data - * Ilf you don't specify any `name` and/or `data`, it will be treated as a page view - * - * @example ``` - * umami.track((props) => ({ ...props, url: path })); - * ``` - */ - (eventFunction: (prop: TrackedProperties) => EventProperties): Promise; - }; - }; + umami: UmamiTracker; } From 22d477b98b9882a963863cf3127fb5e49f8be548 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 2 Jun 2023 23:10:59 -0700 Subject: [PATCH 64/88] Updates to reports. --- components/input/LanguageButton.module.css | 3 +- components/messages.js | 2 + components/pages/reports/BaseParameters.js | 43 +++++++ components/pages/reports/Report.js | 2 - components/pages/reports/ReportHeader.js | 26 ++-- components/pages/reports/ReportList.js | 2 +- .../reports/event-data/EventDataParameters.js | 66 ++++++++++ .../event-data/EventDataParameters.module.css | 27 ++++ .../reports/event-data/EventDataReport.js | 35 +++--- .../pages/reports/event-data/FieldAddForm.js | 28 +++++ .../pages/reports/funnel/FunnelParameters.js | 117 +++++++++--------- .../pages/reports/funnel/FunnelReport.js | 6 +- .../funnel/{AddUrlForm.js => UrlAddForm.js} | 7 +- .../reports/funnel/UrlAddForm.module.css | 8 ++ hooks/useLocale.js | 4 +- lib/constants.ts | 8 ++ package.json | 2 +- pages/api/reports/event-data.ts | 60 +++++++++ .../analytics/eventData/getEventDataFields.ts | 48 +++++++ queries/analytics/session/saveSessionData.ts | 2 +- queries/index.js | 1 + yarn.lock | 8 +- 22 files changed, 388 insertions(+), 117 deletions(-) create mode 100644 components/pages/reports/BaseParameters.js create mode 100644 components/pages/reports/event-data/EventDataParameters.js create mode 100644 components/pages/reports/event-data/EventDataParameters.module.css create mode 100644 components/pages/reports/event-data/FieldAddForm.js rename components/pages/reports/funnel/{AddUrlForm.js => UrlAddForm.js} (85%) create mode 100644 components/pages/reports/funnel/UrlAddForm.module.css create mode 100644 pages/api/reports/event-data.ts create mode 100644 queries/analytics/eventData/getEventDataFields.ts diff --git a/components/input/LanguageButton.module.css b/components/input/LanguageButton.module.css index 16d61978..e46729c0 100644 --- a/components/input/LanguageButton.module.css +++ b/components/input/LanguageButton.module.css @@ -1,8 +1,7 @@ .menu { display: flex; flex-flow: row wrap; - min-width: 600px; - max-width: 100vw; + max-width: 640px; padding: 10px; background: var(--base50); z-index: var(--z-index-popup); diff --git a/components/messages.js b/components/messages.js index adae6aa2..bcb2a5c1 100644 --- a/components/messages.js +++ b/components/messages.js @@ -129,6 +129,8 @@ export const labels = defineMessages({ window: { id: 'label.window', defaultMessage: 'Window' }, addUrl: { id: 'label.add-url', defaultMessage: 'Add URL' }, runQuery: { id: 'label.run-query', defaultMessage: 'Run query' }, + fields: { id: 'label.fields', defaultMessage: 'Fields' }, + addField: { id: 'label.add-field', defaultMessage: 'Add field' }, }); export const messages = defineMessages({ diff --git a/components/pages/reports/BaseParameters.js b/components/pages/reports/BaseParameters.js new file mode 100644 index 00000000..2d04f228 --- /dev/null +++ b/components/pages/reports/BaseParameters.js @@ -0,0 +1,43 @@ +import { FormRow } from 'react-basics'; +import DateFilter from 'components/input/DateFilter'; +import WebsiteSelect from 'components/input/WebsiteSelect'; +import { parseDateRange } from 'lib/date'; +import { useContext } from 'react'; +import { ReportContext } from './Report'; +import { useMessages } from 'hooks'; + +export function BaseParameters() { + const { report, updateReport } = useContext(ReportContext); + const { formatMessage, labels } = useMessages(); + + const { parameters } = report || {}; + const { websiteId, dateRange } = parameters || {}; + const { value, startDate, endDate } = dateRange || {}; + + const handleWebsiteSelect = websiteId => { + updateReport({ parameters: { websiteId } }); + }; + + const handleDateChange = value => { + updateReport({ parameters: { dateRange: { ...parseDateRange(value) } } }); + }; + + return ( + <> + + + + + + + + ); +} + +export default BaseParameters; diff --git a/components/pages/reports/Report.js b/components/pages/reports/Report.js index 0239e64f..0a98ef75 100644 --- a/components/pages/reports/Report.js +++ b/components/pages/reports/Report.js @@ -8,8 +8,6 @@ export const ReportContext = createContext(null); export function Report({ reportId, defaultParameters, children, ...props }) { const report = useReport(reportId, defaultParameters); - //console.log(report); - return ( diff --git a/components/pages/reports/ReportHeader.js b/components/pages/reports/ReportHeader.js index 42d78e75..08465ef9 100644 --- a/components/pages/reports/ReportHeader.js +++ b/components/pages/reports/ReportHeader.js @@ -64,24 +64,14 @@ export function ReportHeader({ icon }) { return ( } className={styles.header}> - - - - - {formatMessage(labels.save)} - - + + {formatMessage(labels.save)} + {toast} ); diff --git a/components/pages/reports/ReportList.js b/components/pages/reports/ReportList.js index da0a622c..6e07c81e 100644 --- a/components/pages/reports/ReportList.js +++ b/components/pages/reports/ReportList.js @@ -10,7 +10,7 @@ import styles from './ReportList.module.css'; const reports = [ { title: 'Event data', - description: 'Query your event data.', + description: 'Query your custom event data.', url: '/reports/event-data', icon: , }, diff --git a/components/pages/reports/event-data/EventDataParameters.js b/components/pages/reports/event-data/EventDataParameters.js new file mode 100644 index 00000000..cbb14193 --- /dev/null +++ b/components/pages/reports/event-data/EventDataParameters.js @@ -0,0 +1,66 @@ +import { useContext, useRef } from 'react'; +import { useApi, useMessages } from 'hooks'; +import { Form, FormRow, FormButtons, SubmitButton, Loading } from 'react-basics'; +import { ReportContext } from 'components/pages/reports/Report'; +import NoData from 'components/common/NoData'; +import styles from './EventDataParameters.module.css'; +import { DATA_TYPES } from 'lib/constants'; + +function useFields(websiteId, startDate, endDate) { + const { get, useQuery } = useApi(); + const { data, error, isLoading } = useQuery( + ['fields', websiteId, startDate, endDate], + () => get('/reports/event-data', { websiteId, startAt: +startDate, endAt: +endDate }), + { enabled: !!(websiteId && startDate && endDate) }, + ); + + return { data, error, isLoading }; +} + +export function EventDataParameters() { + const { report, runReport, isRunning } = useContext(ReportContext); + const { formatMessage, labels } = useMessages(); + const ref = useRef(null); + const { parameters } = report || {}; + const { websiteId, dateRange } = parameters || {}; + const { startDate, endDate } = dateRange || {}; + const queryDisabled = !websiteId || !dateRange; + const { data, error, isLoading } = useFields(websiteId, startDate, endDate); + + const handleSubmit = values => { + runReport(values); + }; + + if (!websiteId || !dateRange) { + return null; + } + + if (isLoading) { + return ; + } + + return ( +

+ +
+ {!data?.length && } + {data?.map?.(({ eventKey, eventDataType }) => { + return ( +
+
{eventKey}
+
{DATA_TYPES[eventDataType]}
+
+ ); + })} +
+
+ + + {formatMessage(labels.runQuery)} + + +
+ ); +} + +export default EventDataParameters; diff --git a/components/pages/reports/event-data/EventDataParameters.module.css b/components/pages/reports/event-data/EventDataParameters.module.css new file mode 100644 index 00000000..66c82842 --- /dev/null +++ b/components/pages/reports/event-data/EventDataParameters.module.css @@ -0,0 +1,27 @@ +.fields { + max-height: 300px; + overflow: auto; +} + +.field { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + height: 30px; + cursor: pointer; + padding: 4px; + border-radius: var(--border-radius); +} + +.field:hover { + background: var(--base75); +} + +.key { + font-weight: 400; +} + +.type { + color: var(--font-color300); +} diff --git a/components/pages/reports/event-data/EventDataReport.js b/components/pages/reports/event-data/EventDataReport.js index a32cd6e2..16382982 100644 --- a/components/pages/reports/event-data/EventDataReport.js +++ b/components/pages/reports/event-data/EventDataReport.js @@ -1,30 +1,23 @@ -import { useState } from 'react'; -import { Form, FormRow, FormInput, TextField } from 'react-basics'; import Report from '../Report'; import ReportHeader from '../ReportHeader'; -import useMessages from 'hooks/useMessages'; +import ReportMenu from '../ReportMenu'; +import ReportBody from '../ReportBody'; +import EventDataParameters from './EventDataParameters'; import Nodes from 'assets/nodes.svg'; -import styles from '../reports.module.css'; -export default function EventDataReport({ websiteId, data }) { - const [values, setValues] = useState({ query: '' }); - const { formatMessage, labels } = useMessages(); +const defaultParameters = { + type: 'event-data', + parameters: { fields: [], filters: [] }, +}; +export default function EventDataReport({ reportId }) { return ( - - } /> -
-
-
- - - - - -
-
-
-
+ + } /> + + + + hi. ); } diff --git a/components/pages/reports/event-data/FieldAddForm.js b/components/pages/reports/event-data/FieldAddForm.js new file mode 100644 index 00000000..b0258608 --- /dev/null +++ b/components/pages/reports/event-data/FieldAddForm.js @@ -0,0 +1,28 @@ +import { useMessages } from 'hooks'; +import { Button, Form, FormButtons, FormRow } from 'react-basics'; + +export function FieldAddForm({ onClose }) { + const { formatMessage, labels } = useMessages(); + + const handleSave = () => { + onClose(); + }; + + const handleClose = () => { + onClose(); + }; + + return ( +
+ + + + + +
+ ); +} + +export default FieldAddForm; diff --git a/components/pages/reports/funnel/FunnelParameters.js b/components/pages/reports/funnel/FunnelParameters.js index c87d52e4..991bf690 100644 --- a/components/pages/reports/funnel/FunnelParameters.js +++ b/components/pages/reports/funnel/FunnelParameters.js @@ -6,98 +6,99 @@ import { FormButtons, FormInput, FormRow, - Modal, + PopupTrigger, + Popup, SubmitButton, Text, TextField, Tooltip, } from 'react-basics'; import Icons from 'components/icons'; -import AddUrlForm from './AddUrlForm'; +import UrlAddForm from './UrlAddForm'; import { ReportContext } from 'components/pages/reports/Report'; import styles from './FunnelParameters.module.css'; +import BaseParameters from '../BaseParameters'; export function FunnelParameters() { const { report, runReport, updateReport, isRunning } = useContext(ReportContext); const { formatMessage, labels } = useMessages(); - const [show, setShow] = useState(false); const ref = useRef(null); - const { websiteId, parameters } = report || {}; - const queryDisabled = !websiteId || parameters?.urls?.length < 2; + + const { parameters } = report || {}; + const { websiteId, dateRange, urls } = parameters || {}; + const queryDisabled = !websiteId || !dateRange || urls?.length < 2; const handleSubmit = values => { runReport(values); }; const handleAddUrl = url => { - updateReport({ parameters: { ...parameters, urls: parameters.urls.concat(url) } }); + updateReport({ parameters: { urls: parameters.urls.concat(url) } }); }; const handleRemoveUrl = (index, e) => { e.stopPropagation(); const urls = [...parameters.urls]; urls.splice(index, 1); - updateReport({ parameters: { ...parameters, urls } }); + updateReport({ parameters: { urls } }); }; - const showAddForm = () => setShow(true); - const hideAddForm = () => setShow(false); - return ( - <> -
- - - - - - }> -
- {parameters?.urls?.map((url, index) => { - return ( -
- {url} - - - - - -
- ); - })} -
-
- - - {formatMessage(labels.runQuery)} - - -
- {show && ( - - - - )} - +
+ + + + + + + }> +
+ {parameters?.urls?.map((url, index) => { + return ( +
+ {url} + + + + + +
+ ); + })} +
+
+ + + {formatMessage(labels.runQuery)} + + + ); } -function AddUrlButton({ onClick }) { +function AddUrlButton({ onAdd }) { const { formatMessage, labels } = useMessages(); return ( - - - - - + + + + + + + + {close => { + return ; + }} + + ); } diff --git a/components/pages/reports/funnel/FunnelReport.js b/components/pages/reports/funnel/FunnelReport.js index 26a81a09..7b4d8ece 100644 --- a/components/pages/reports/funnel/FunnelReport.js +++ b/components/pages/reports/funnel/FunnelReport.js @@ -1,17 +1,15 @@ -import { useContext } from 'react'; import FunnelChart from './FunnelChart'; import FunnelTable from './FunnelTable'; import FunnelParameters from './FunnelParameters'; -import Report, { ReportContext } from '../Report'; +import Report from '../Report'; import ReportHeader from '../ReportHeader'; import ReportMenu from '../ReportMenu'; import ReportBody from '../ReportBody'; import Funnel from 'assets/funnel.svg'; -import { useReport } from 'hooks'; const defaultParameters = { type: 'funnel', - parameters: { window: 60, urls: ['/', '/docs'] }, + parameters: { window: 60, urls: [] }, }; export default function FunnelReport({ reportId }) { diff --git a/components/pages/reports/funnel/AddUrlForm.js b/components/pages/reports/funnel/UrlAddForm.js similarity index 85% rename from components/pages/reports/funnel/AddUrlForm.js rename to components/pages/reports/funnel/UrlAddForm.js index 07f34867..b9d25f29 100644 --- a/components/pages/reports/funnel/AddUrlForm.js +++ b/components/pages/reports/funnel/UrlAddForm.js @@ -1,8 +1,9 @@ import { useState } from 'react'; import { useMessages } from 'hooks'; import { Button, Form, FormButtons, FormRow, TextField } from 'react-basics'; +import styles from './UrlAddForm.module.css'; -export function AddUrlForm({ defaultValue = '', onSave, onClose }) { +export function UrlAddForm({ defaultValue = '', onSave, onClose }) { const [url, setUrl] = useState(defaultValue); const { formatMessage, labels } = useMessages(); @@ -22,7 +23,7 @@ export function AddUrlForm({ defaultValue = '', onSave, onClose }) { }; return ( -
+ , + res: NextApiResponse, +) => { + await useCors(req, res); + await useAuth(req, res); + + if (req.method === 'GET') { + const { websiteId, startAt, endAt } = req.query; + + if (!(await canViewWebsite(req.auth, websiteId))) { + return unauthorized(res); + } + + const data = await getEventDataFields(websiteId, new Date(+startAt), new Date(+endAt)); + + return ok(res, data); + } + + if (req.method === 'POST') { + const { + websiteId, + dateRange: { startDate, endDate }, + } = req.body; + + if (!(await canViewWebsite(req.auth, websiteId))) { + return unauthorized(res); + } + + const data = {}; + + return ok(res, data); + } + + return methodNotAllowed(res); +}; diff --git a/queries/analytics/eventData/getEventDataFields.ts b/queries/analytics/eventData/getEventDataFields.ts new file mode 100644 index 00000000..5d2b9ef0 --- /dev/null +++ b/queries/analytics/eventData/getEventDataFields.ts @@ -0,0 +1,48 @@ +import clickhouse from 'lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; +import prisma from 'lib/prisma'; +import { WebsiteEventDataMetric } from 'lib/types'; +import { loadWebsite } from 'lib/query'; + +export async function getEventDataFields( + ...args: [websiteId: string, startDate: Date, endDate: Date] +): Promise { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery(websiteId: string, startDate: Date, endDate: Date) { + const { toUuid, rawQuery } = prisma; + const website = await loadWebsite(websiteId); + const resetDate = new Date(website?.resetAt || website?.createdAt); + const params: any = [websiteId, resetDate, startDate, endDate]; + + return rawQuery( + `select + distinct event_key as eventKey, data_type as eventDataType + from event_data + where website_id = $1${toUuid()} + and created_at >= $2 + and created_at between $3 and $4`, + params, + ); +} + +async function clickhouseQuery(websiteId: string, startDate: Date, endDate: Date) { + const { rawQuery, getDateFormat, getBetweenDates } = clickhouse; + const website = await loadWebsite(websiteId); + const resetDate = new Date(website?.resetAt || website?.createdAt); + const params = { websiteId }; + + return rawQuery( + `select + distinct event_key as eventKey, data_type as eventDataType + from event_data + where website_id = {websiteId:UUID} + and created_at >= ${getDateFormat(resetDate)} + and ${getBetweenDates('created_at', startDate, endDate)}`, + params, + ); +} diff --git a/queries/analytics/session/saveSessionData.ts b/queries/analytics/session/saveSessionData.ts index 76842b4f..beec27f7 100644 --- a/queries/analytics/session/saveSessionData.ts +++ b/queries/analytics/session/saveSessionData.ts @@ -37,7 +37,7 @@ export async function saveSessionData(data: { }, }), client.sessionData.createMany({ - data: flattendData, + data: flattendData as any, }), ]); } diff --git a/queries/index.js b/queries/index.js index 302c88db..8a4b42bf 100644 --- a/queries/index.js +++ b/queries/index.js @@ -7,6 +7,7 @@ export * from './analytics/event/getEventMetrics'; export * from './analytics/event/getEventUsage'; export * from './analytics/event/getEvents'; export * from './analytics/eventData/getEventData'; +export * from './analytics/eventData/getEventDataFields'; export * from './analytics/eventData/getEventDataUsage'; export * from './analytics/event/saveEvent'; export * from './analytics/pageview/getPageviewFunnel'; diff --git a/yarn.lock b/yarn.lock index e7d7e8e1..579d52af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8191,10 +8191,10 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-basics@^0.82.0: - version "0.82.0" - resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.82.0.tgz#1df241f4ef97a8d0c7c81d4954d065efc2c415a5" - integrity sha512-DrHuRJqncx7cWYFz4rAahEsCsF3s5W1CJyJY276EUphfn0NDKZcvnrSnr6G+KTUAEkvksvlAm1t3nDv85coUxg== +react-basics@^0.83.0: + version "0.83.0" + resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.83.0.tgz#bc4a962967383ecec20a0eb49b88c004c67a7f3a" + integrity sha512-3P74I1Tp8Ih8gw3xG65mB0aTzG0oTOYg72Gpfbd3igKjg/L1wgN6stWjIwzNP7mgWFc0FQ63BB13P7pL025bNQ== dependencies: classnames "^2.3.1" date-fns "^2.29.3" From 64c6d73b77c7f6b1d8081c8c7d6aed80d7d87515 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 5 Jun 2023 03:08:43 -0700 Subject: [PATCH 65/88] Update report parameters. --- components/input/WebsiteSelect.js | 1 - components/layout/PageHeader.js | 2 +- components/layout/PageHeader.module.css | 2 +- components/messages.js | 4 +- components/pages/reports/BaseParameters.js | 2 +- components/pages/reports/ReportHeader.js | 55 ++++++++++--------- .../pages/reports/ReportHeader.module.css | 3 + .../pages/reports/funnel/FunnelParameters.js | 6 +- components/pages/reports/funnel/UrlAddForm.js | 8 +-- components/pages/reports/reports.module.css | 1 + 10 files changed, 44 insertions(+), 40 deletions(-) create mode 100644 components/pages/reports/ReportHeader.module.css diff --git a/components/input/WebsiteSelect.js b/components/input/WebsiteSelect.js index a0ac38e4..b77ae57c 100644 --- a/components/input/WebsiteSelect.js +++ b/components/input/WebsiteSelect.js @@ -19,7 +19,6 @@ export function WebsiteSelect({ websiteId, onSelect }) { onChange={onSelect} alignment="end" placeholder={formatMessage(labels.selectWebsite)} - style={{ width: 200 }} > {({ id, name }) => {name}} diff --git a/components/layout/PageHeader.js b/components/layout/PageHeader.js index c01192d2..f1363140 100644 --- a/components/layout/PageHeader.js +++ b/components/layout/PageHeader.js @@ -5,7 +5,7 @@ import styles from './PageHeader.module.css'; export function PageHeader({ title, children, className }) { return (
-
{title}
+ {title &&
{title}
}
{children}
); diff --git a/components/layout/PageHeader.module.css b/components/layout/PageHeader.module.css index 5ea85b70..03a1c7c8 100644 --- a/components/layout/PageHeader.module.css +++ b/components/layout/PageHeader.module.css @@ -4,7 +4,6 @@ align-items: center; align-content: center; align-self: stretch; - margin-bottom: 40px; flex-wrap: wrap; } @@ -23,6 +22,7 @@ font-weight: 700; gap: 20px; height: 60px; + flex: 1; } .actions { diff --git a/components/messages.js b/components/messages.js index bcb2a5c1..166a121b 100644 --- a/components/messages.js +++ b/components/messages.js @@ -48,6 +48,8 @@ export const labels = defineMessages({ deleteWebsite: { id: 'label.delete-website', defaultMessage: 'Delete website' }, reset: { id: 'label.reset', defaultMessage: 'Reset' }, addWebsite: { id: 'label.add-website', defaultMessage: 'Add website' }, + addField: { id: 'label.add-field', defaultMessage: 'Add field' }, + addDescription: { id: 'label.add-description', defaultMessage: 'Add description' }, changePassword: { id: 'label.change-password', defaultMessage: 'Change password' }, currentPassword: { id: 'label.current-password', defaultMessage: 'Current password' }, newPassword: { id: 'label.new-password', defaultMessage: 'New password' }, @@ -129,8 +131,8 @@ export const labels = defineMessages({ window: { id: 'label.window', defaultMessage: 'Window' }, addUrl: { id: 'label.add-url', defaultMessage: 'Add URL' }, runQuery: { id: 'label.run-query', defaultMessage: 'Run query' }, + field: { id: 'label.field', defaultMessage: 'Field' }, fields: { id: 'label.fields', defaultMessage: 'Fields' }, - addField: { id: 'label.add-field', defaultMessage: 'Add field' }, }); export const messages = defineMessages({ diff --git a/components/pages/reports/BaseParameters.js b/components/pages/reports/BaseParameters.js index 2d04f228..5dbe0f60 100644 --- a/components/pages/reports/BaseParameters.js +++ b/components/pages/reports/BaseParameters.js @@ -15,7 +15,7 @@ export function BaseParameters() { const { value, startDate, endDate } = dateRange || {}; const handleWebsiteSelect = websiteId => { - updateReport({ parameters: { websiteId } }); + updateReport({ websiteId, parameters: { websiteId } }); }; const handleDateChange = value => { diff --git a/components/pages/reports/ReportHeader.js b/components/pages/reports/ReportHeader.js index 08465ef9..b8104203 100644 --- a/components/pages/reports/ReportHeader.js +++ b/components/pages/reports/ReportHeader.js @@ -1,13 +1,11 @@ import { useContext } from 'react'; import { useRouter } from 'next/router'; -import { Flexbox, Icon, LoadingButton, InlineEditField, useToast } from 'react-basics'; -import WebsiteSelect from 'components/input/WebsiteSelect'; +import { Icon, LoadingButton, InlineEditField, useToast } from 'react-basics'; import PageHeader from 'components/layout/PageHeader'; -import DateFilter from 'components/input/DateFilter'; -import { parseDateRange } from 'lib/date'; import { useMessages, useApi } from 'hooks'; import { ReportContext } from './Report'; -import styles from './reports.module.css'; +import styles from './ReportHeader.module.css'; +import reportStyles from './reports.module.css'; export function ReportHeader({ icon }) { const { report, updateReport } = useContext(ReportContext); @@ -20,17 +18,8 @@ export function ReportHeader({ icon }) { post(`/reports/${data.id}`, data), ); - const { name, parameters } = report || {}; + const { name, description, parameters } = report || {}; const { websiteId, dateRange } = parameters || {}; - const { value, startDate, endDate } = dateRange || {}; - - const handleWebsiteSelect = websiteId => { - updateReport({ parameters: { websiteId } }); - }; - - const handleDateChange = value => { - updateReport({ parameters: { dateRange: { ...parseDateRange(value) } } }); - }; const handleSave = async () => { if (!report.id) { @@ -53,27 +42,41 @@ export function ReportHeader({ icon }) { updateReport({ name }); }; + const handleDescriptionChange = description => { + updateReport({ description }); + }; + const Title = () => { return ( <> {icon} - + ); }; return ( - } className={styles.header}> - - {formatMessage(labels.save)} - +
+ }> + + {formatMessage(labels.save)} + + +
+ +
{toast} - +
); } diff --git a/components/pages/reports/ReportHeader.module.css b/components/pages/reports/ReportHeader.module.css new file mode 100644 index 00000000..01e483a0 --- /dev/null +++ b/components/pages/reports/ReportHeader.module.css @@ -0,0 +1,3 @@ +.description { + color: var(--font-color300); +} diff --git a/components/pages/reports/funnel/FunnelParameters.js b/components/pages/reports/funnel/FunnelParameters.js index 991bf690..cf22f19c 100644 --- a/components/pages/reports/funnel/FunnelParameters.js +++ b/components/pages/reports/funnel/FunnelParameters.js @@ -28,8 +28,10 @@ export function FunnelParameters() { const { websiteId, dateRange, urls } = parameters || {}; const queryDisabled = !websiteId || !dateRange || urls?.length < 2; - const handleSubmit = values => { - runReport(values); + const handleSubmit = data => { + if (!queryDisabled) { + runReport(data); + } }; const handleAddUrl = url => { diff --git a/components/pages/reports/funnel/UrlAddForm.js b/components/pages/reports/funnel/UrlAddForm.js index b9d25f29..17d19a91 100644 --- a/components/pages/reports/funnel/UrlAddForm.js +++ b/components/pages/reports/funnel/UrlAddForm.js @@ -17,13 +17,8 @@ export function UrlAddForm({ defaultValue = '', onSave, onClose }) { setUrl(e.target.value); }; - const handleClose = () => { - setUrl(''); - onClose(); - }; - return ( - + {formatMessage(labels.save)} - ); diff --git a/components/pages/reports/reports.module.css b/components/pages/reports/reports.module.css index b75aee64..6fa54281 100644 --- a/components/pages/reports/reports.module.css +++ b/components/pages/reports/reports.module.css @@ -7,6 +7,7 @@ .header { grid-row: 1 / 2; grid-column: 1 / 3; + margin-bottom: 40px; } .menu { From 6ee27affc4dfa93640cd7a2b8ab2f62de8a111ed Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 13 Jun 2023 11:59:12 -0700 Subject: [PATCH 66/88] Added new react basics provider. --- components/pages/reports/ReportHeader.js | 5 +- .../settings/profile/PasswordChangeButton.js | 5 +- .../pages/settings/teams/TeamMembers.js | 5 +- .../pages/settings/teams/TeamSettings.js | 5 +- .../pages/settings/teams/TeamWebsites.js | 5 +- components/pages/settings/teams/TeamsList.js | 5 +- .../pages/settings/users/UserSettings.js | 5 +- components/pages/settings/users/UsersList.js | 5 +- .../settings/websites/WebsiteSettings.js | 5 +- .../pages/settings/websites/WebsitesList.js | 5 +- package.json | 2 +- pages/_app.js | 65 ++++++++++--------- yarn.lock | 8 +-- 13 files changed, 59 insertions(+), 66 deletions(-) diff --git a/components/pages/reports/ReportHeader.js b/components/pages/reports/ReportHeader.js index b8104203..805e45b6 100644 --- a/components/pages/reports/ReportHeader.js +++ b/components/pages/reports/ReportHeader.js @@ -1,6 +1,6 @@ import { useContext } from 'react'; import { useRouter } from 'next/router'; -import { Icon, LoadingButton, InlineEditField, useToast } from 'react-basics'; +import { Icon, LoadingButton, InlineEditField, useToasts } from 'react-basics'; import PageHeader from 'components/layout/PageHeader'; import { useMessages, useApi } from 'hooks'; import { ReportContext } from './Report'; @@ -10,7 +10,7 @@ import reportStyles from './reports.module.css'; export function ReportHeader({ icon }) { const { report, updateReport } = useContext(ReportContext); const { formatMessage, labels, messages } = useMessages(); - const { toast, showToast } = useToast(); + const { showToast } = useToasts(); const { post, useMutation } = useApi(); const router = useRouter(); const { mutate: create, isLoading: isCreating } = useMutation(data => post(`/reports`, data)); @@ -75,7 +75,6 @@ export function ReportHeader({ icon }) { onCommit={handleDescriptionChange} />
- {toast}
); } diff --git a/components/pages/settings/profile/PasswordChangeButton.js b/components/pages/settings/profile/PasswordChangeButton.js index 9aa6fdca..03bf74bc 100644 --- a/components/pages/settings/profile/PasswordChangeButton.js +++ b/components/pages/settings/profile/PasswordChangeButton.js @@ -1,11 +1,11 @@ -import { Button, Icon, Text, useToast, ModalTrigger, Modal } from 'react-basics'; +import { Button, Icon, Text, useToasts, ModalTrigger, Modal } from 'react-basics'; import PasswordEditForm from 'components/pages/settings/profile/PasswordEditForm'; import Icons from 'components/icons'; import useMessages from 'hooks/useMessages'; export function PasswordChangeButton() { const { formatMessage, labels, messages } = useMessages(); - const { toast, showToast } = useToast(); + const { showToast } = useToasts(); const handleSave = () => { showToast({ message: formatMessage(messages.saved), variant: 'success' }); @@ -13,7 +13,6 @@ export function PasswordChangeButton() { return ( <> - {toast}