Rewrite endpoints + bar cards

This commit is contained in:
Emily
2024-10-03 15:07:16 +02:00
parent 314660d8a3
commit e1953f2f9f
28 changed files with 667 additions and 434 deletions

4
TODO Normal file
View File

@@ -0,0 +1,4 @@
- Slice change on Actionable Chart
- Show more on Dashboard cards

View File

@@ -127,7 +127,7 @@ function openExternalLink(link: string) {
formatNumberK(element.count) }} </div> formatNumberK(element.count) }} </div>
</div> </div>
<div v-if="props.data.length == 0" class="flex justify-center text-text-sub font-bold text-[1.1rem]"> <div v-if="props.data.length == 0" class="flex justify-center text-text-sub font-bold text-[1.1rem]">
No visits yet No data yet
</div> </div>
</div> </div>
<div v-if="!hideShowMore" class="flex justify-center mt-4 text-text-sub/90 "> <div v-if="!hideShowMore" class="flex justify-center mt-4 text-text-sub/90 ">

View File

@@ -0,0 +1,30 @@
<script lang="ts" setup>
const isShowMore = ref<boolean>(false);
const browsersData = useFetch('/api/data/browsers', {
headers: useComputedHeaders({
limit: computed(() => isShowMore.value ? '200' : '10'),
}), lazy: true
});
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
function showMore() {
isShowMore.value = true;
showDialog.value = true;
dialogBarData.value = browsersData.data.value || [];
}
</script>
<template>
<div class="flex flex-col gap-2">
<BarCardBase @showMore="showMore()" @dataReload="browsersData.refresh()"
:data="browsersData.data.value || []" desc="The browsers most used to search your website."
:dataIcons="false" :loading="browsersData.pending.value" label="Top Browsers" sub-label="Browsers">
</BarCardBase>
</div>
</template>

View File

@@ -0,0 +1,31 @@
<script lang="ts" setup>
const isShowMore = ref<boolean>(false);
const devicesData = useFetch('/api/data/devices', {
headers: useComputedHeaders({
limit: computed(() => isShowMore.value ? '200' : '10'),
}), lazy: true
});
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
function showMore() {
isShowMore.value = true;
showDialog.value = true;
dialogBarData.value = devicesData.data.value || [];
}
</script>
<template>
<div class="flex flex-col gap-2">
<BarCardBase @showMore="showMore()" @dataReload="devicesData.refresh()" :data="devicesData.data.value || []" :dataIcons="false"
desc="The devices most used to access your website." :loading="devicesData.pending.value" label="Top Devices"
sub-label="Devices"></BarCardBase>
</div>
</template>

View File

@@ -0,0 +1,35 @@
<script lang="ts" setup>
const router = useRouter();
function goToView() {
router.push('/dashboard/events');
}
const isShowMore = ref<boolean>(false);
const eventsData = useFetch('/api/data/events', {
headers: useComputedHeaders({
limit: computed(() => isShowMore.value ? '200' : '10'),
}), lazy: true
});
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
function showMore() {
isShowMore.value = true;
showDialog.value = true;
dialogBarData.value = eventsData.data.value || [];
}
</script>
<template>
<div class="flex flex-col gap-2 h-full">
<BarCardBase @showMore="showMore()" @showRawData="goToView()"
desc="Most frequent user events triggered in this project" @dataReload="eventsData.refresh()"
:data="eventsData.data.value || []" :loading="eventsData.pending.value" label="Top Events"
sub-label="Events" :rawButton="!isLiveDemo()"></BarCardBase>
</div>
</template>

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { IconProvider } from './BarsCard.vue'; import type { IconProvider } from '../BarCard/Base.vue';
function iconProvider(id: string): ReturnType<IconProvider> { function iconProvider(id: string): ReturnType<IconProvider> {
if (id === 'self') return ['icon', 'fas fa-link']; if (id === 'self') return ['icon', 'fas fa-link'];
@@ -12,23 +12,12 @@ function iconProvider(id: string): ReturnType<IconProvider> {
const customIconStyle = `width: 2rem; padding: 1px;` const customIconStyle = `width: 2rem; padding: 1px;`
const activeProject = useActiveProject();
const { safeSnapshotDates } = useSnapshot()
const isShowMore = ref<boolean>(false); const isShowMore = ref<boolean>(false);
const headers = computed(() => { const geolocationData = useFetch('/api/data/countries', {
return { headers: useComputedHeaders({
'x-from': safeSnapshotDates.value.from, limit: computed(() => isShowMore.value ? '200' : '10'),
'x-to': safeSnapshotDates.value.to, }), lazy: true
Authorization: authorizationHeaderComputed.value,
limit: isShowMore.value === true ? '200' : '10'
}
});
const geolocationData = useFetch(`/api/metrics/${activeProject.value?._id}/data/countries`, {
method: 'POST', headers, lazy: true, immediate: false
}); });
@@ -46,18 +35,14 @@ function showMore() {
} }
onMounted(async () => {
geolocationData.execute();
});
</script> </script>
<template> <template>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<DashboardBarsCard @showMore="showMore()" @dataReload="geolocationData.refresh()" :data="geolocationData.data.value || []" :dataIcons="false" <BarCardBase @showMore="showMore()" @dataReload="geolocationData.refresh()" :data="geolocationData.data.value || []" :dataIcons="false"
:loading="geolocationData.pending.value" label="Top Countries" sub-label="Countries" :iconProvider="iconProvider" :loading="geolocationData.pending.value" label="Top Countries" sub-label="Countries" :iconProvider="iconProvider"
:customIconStyle="customIconStyle" desc=" Lists the countries where users access your website."> :customIconStyle="customIconStyle" desc=" Lists the countries where users access your website.">
</DashboardBarsCard> </BarCardBase>
</div> </div>
</template> </template>

View File

@@ -0,0 +1,30 @@
<script lang="ts" setup>
const isShowMore = ref<boolean>(false);
const ossData = useFetch('/api/data/oss', {
headers: useComputedHeaders({
limit: computed(() => isShowMore.value ? '200' : '10'),
}), lazy: true
});
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
function showMore() {
isShowMore.value = true;
showDialog.value = true;
dialogBarData.value = ossData.data.value || [];
}
</script>
<template>
<div class="flex flex-col gap-2 h-full">
<BarCardBase @showMore="showMore()" @dataReload="ossData.refresh()" :data="ossData.data.value || []"
desc="The operating systems most commonly used by your website's visitors." :dataIcons="false"
:loading="ossData.pending.value" label="Top OS" sub-label="OSs"></BarCardBase>
</div>
</template>

View File

@@ -0,0 +1,44 @@
<script lang="ts" setup>
import type { IconProvider } from './Base.vue';
function iconProvider(id: string): ReturnType<IconProvider> {
if (id === 'self') return ['icon', 'fas fa-link'];
return ['img', `https://s2.googleusercontent.com/s2/favicons?domain=${id}&sz=64`]
}
function elementTextTransformer(element: string) {
if (element === 'self') return 'Direct Link';
return element;
}
const isShowMore = ref<boolean>(false);
const referrersData = useFetch('/api/data/referrers', {
headers: useComputedHeaders({
limit: computed(() => isShowMore.value ? '200' : '10'),
}), lazy: true
});
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
function showMore() {
isShowMore.value = true;
showDialog.value = true;
dialogBarData.value = referrersData.data.value?.map(e => {
return { ...e, icon: iconProvider(e._id) }
}) || [];
}
</script>
<template>
<div class="flex flex-col gap-2">
<BarCardBase @showMore="showMore()" :elementTextTransformer="elementTextTransformer"
:iconProvider="iconProvider" @dataReload="referrersData.refresh()" :showLink=true
:data="referrersData.data.value || []" :interactive="false" desc="Where users find your website."
:dataIcons="true" :loading="referrersData.pending.value" label="Top Referrers" sub-label="Referrers">
</BarCardBase>
</div>
</template>

View File

@@ -0,0 +1,59 @@
<script lang="ts" setup>
const isShowMore = ref<boolean>(false);
const currentWebsite = ref<string>("");
const websitesData = useFetch('/api/data/websites', {
headers: useComputedHeaders({
limit: computed(() => isShowMore.value ? '200' : '10'),
}), lazy: true
});
const pagesData = useFetch('/api/data/websites_pages', {
headers: useComputedHeaders({
limit: computed(() => isShowMore.value ? '200' : '10'),
custom: {
'x-website-name': currentWebsite
}
}), lazy: true
});
const isPagesView = ref<boolean>(false);
const currentData = computed(() => {
return isPagesView.value ? pagesData : websitesData
})
async function showDetails(website: string) {
currentWebsite.value = website;
isPagesView.value = true;
}
async function showGeneral() {
websitesData.execute();
isPagesView.value = false;
}
const router = useRouter();
function goToView() {
router.push('/dashboard/visits');
}
</script>
<template>
<div class="flex flex-col gap-2 h-full">
<BarCardBase :hideShowMore="true" @showGeneral="showGeneral()" @showRawData="goToView()"
@dataReload="currentData.refresh()" @showDetails="showDetails" :data="currentData.data.value || []"
:loading="currentData.pending.value" :label="isPagesView ? 'Top pages' : 'Top Websites'"
:sub-label="isPagesView ? 'Page' : 'Website'"
:desc="isPagesView ? 'Most visited pages' : 'Most visited website in this project'"
:interactive="!isPagesView" :rawButton="!isLiveDemo()" :isDetailView="isPagesView">
</BarCardBase>
</div>
</template>

View File

@@ -3,12 +3,8 @@ import { onMounted } from 'vue';
import DateService, { type Slice } from '@services/DateService'; import DateService, { type Slice } from '@services/DateService';
import type { ChartData, ChartOptions, TooltipModel } from 'chart.js'; import type { ChartData, ChartOptions, TooltipModel } from 'chart.js';
import { useLineChart, LineChart } from 'vue-chart-3'; import { useLineChart, LineChart } from 'vue-chart-3';
registerChartComponents();
const errorData = ref<{ errored: boolean, text: string }>({ const errorData = ref<{ errored: boolean, text: string }>({ errored: false, text: '' })
errored: false,
text: ''
})
const chartOptions = ref<ChartOptions<'line'>>({ const chartOptions = ref<ChartOptions<'line'>>({
responsive: true, responsive: true,
@@ -102,7 +98,6 @@ const chartData = ref<ChartData<'line' | 'bar' | 'bubble'>>({
], ],
}); });
const { lineChartProps, lineChartRef, update: updateChart } = useLineChart({ chartData: (chartData as any), options: chartOptions }); const { lineChartProps, lineChartRef, update: updateChart } = useLineChart({ chartData: (chartData as any), options: chartOptions });
const externalTooltipElement = ref<null | HTMLDivElement>(null); const externalTooltipElement = ref<null | HTMLDivElement>(null);
@@ -135,8 +130,6 @@ function externalTooltipHandler(context: { chart: any, tooltip: TooltipModel<'li
tooltipEl.style.padding = tooltip.options.padding + 'px ' + tooltip.options.padding + 'px'; tooltipEl.style.padding = tooltip.options.padding + 'px ' + tooltip.options.padding + 'px';
} }
const selectLabels: { label: string, value: Slice }[] = [ const selectLabels: { label: string, value: Slice }[] = [
{ label: 'Hour', value: 'hour' }, { label: 'Hour', value: 'hour' },
{ label: 'Day', value: 'day' }, { label: 'Day', value: 'day' },
@@ -144,14 +137,9 @@ const selectLabels: { label: string, value: Slice }[] = [
]; ];
const selectedLabelIndex = ref<number>(1); const selectedLabelIndex = ref<number>(1);
const activeProject = useActiveProject();
const { safeSnapshotDates } = useSnapshot()
const allDatesFull = ref<string[]>([]); const allDatesFull = ref<string[]>([]);
function transformResponse(input: { _id: string, count: number }[]) { function transformResponse(input: { _id: string, count: number }[]) {
const data = input.map(e => e.count); const data = input.map(e => e.count);
const labels = input.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, selectLabels[selectedLabelIndex.value].value)); const labels = input.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, selectLabels[selectedLabelIndex.value].value));
@@ -159,50 +147,31 @@ function transformResponse(input: { _id: string, count: number }[]) {
return { data, labels } return { data, labels }
} }
const body = computed(() => {
return {
from: safeSnapshotDates.value.from,
to: safeSnapshotDates.value.to,
slice: selectLabels[selectedLabelIndex.value].value
}
});
function onResponseError(e: any) { function onResponseError(e: any) {
console.log('ON RESPONSE ERROR')
errorData.value = { errored: true, text: e.response._data.message ?? 'Generic error' } errorData.value = { errored: true, text: e.response._data.message ?? 'Generic error' }
} }
function onResponse(e: any) { function onResponse(e: any) {
console.log('ON RESPONSE')
if (e.response.status != 500) errorData.value = { errored: false, text: '' } if (e.response.status != 500) errorData.value = { errored: false, text: '' }
} }
const visitsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/visits`, {
method: 'POST', ...signHeaders({ v2: 'true' }), body, transform: transformResponse, const visitsData = useFetch('/api/timeline/visits', {
lazy: true, immediate: false, headers: useComputedHeaders({ slice: selectLabels[selectedLabelIndex.value].value }), lazy: true,
onResponseError, transform: transformResponse, onResponseError, onResponse
onResponse
}); });
const eventsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/events`, { const sessionsData = useFetch('/api/timeline/sessions', {
method: 'POST', ...signHeaders({ v2: 'true' }), body, transform: transformResponse, headers: useComputedHeaders({ slice: selectLabels[selectedLabelIndex.value].value }), lazy: true,
lazy: true, immediate: false, transform: transformResponse, onResponseError, onResponse
onResponseError,
onResponse
}); });
const sessionsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/sessions`, { const eventsData = useFetch('/api/timeline/events', {
method: 'POST', ...signHeaders({ v2: 'true' }), body, transform: transformResponse, headers: useComputedHeaders({ slice: selectLabels[selectedLabelIndex.value].value }), lazy: true,
lazy: true, immediate: false, transform: transformResponse, onResponseError, onResponse
onResponseError,
onResponse
}); });
const readyToDisplay = computed(() => !visitsData.pending.value && !eventsData.pending.value && !sessionsData.pending.value);
const readyToDisplay = computed(() => {
return !visitsData.pending.value && !eventsData.pending.value && !sessionsData.pending.value;
});
watch(readyToDisplay, () => { watch(readyToDisplay, () => {
if (readyToDisplay.value === true) onDataReady(); if (readyToDisplay.value === true) onDataReady();
@@ -226,14 +195,10 @@ function createGradient(startColor: string) {
} }
function onDataReady() { function onDataReady() {
console.log('DATA READY');
if (!visitsData.data.value) return; if (!visitsData.data.value) return;
if (!eventsData.data.value) return; if (!eventsData.data.value) return;
if (!sessionsData.data.value) return; if (!sessionsData.data.value) return;
console.log('DATA READY 2');
chartData.value.labels = visitsData.data.value.labels; chartData.value.labels = visitsData.data.value.labels;
const maxChartY = Math.max(...visitsData.data.value.data, ...sessionsData.data.value.data); const maxChartY = Math.max(...visitsData.data.value.data, ...sessionsData.data.value.data);
@@ -250,9 +215,7 @@ function onDataReady() {
chartData.value.datasets[1].backgroundColor = [createGradient('#4abde8')]; chartData.value.datasets[1].backgroundColor = [createGradient('#4abde8')];
chartData.value.datasets[2].backgroundColor = [createGradient('#fbbf24')]; chartData.value.datasets[2].backgroundColor = [createGradient('#fbbf24')];
console.log('UPDATE CHART');
updateChart(); updateChart();
} }
const currentTooltipData = ref<{ visits: number, events: number, sessions: number, date: string }>({ const currentTooltipData = ref<{ visits: number, events: number, sessions: number, date: string }>({
@@ -268,19 +231,7 @@ function onLegendChange(dataset: any, index: number, checked: any) {
dataset.hidden = !checked; dataset.hidden = !checked;
} }
const legendColors = [ const legendColors = ['#5655d7', '#4abde8', '#fbbf24']
'#5655d7',
'#4abde8',
'#fbbf24'
]
onMounted(async () => {
visitsData.execute();
eventsData.execute();
sessionsData.execute();
});
const inLiveDemo = isLiveDemo(); const inLiveDemo = isLiveDemo();

View File

@@ -1,46 +0,0 @@
<script lang="ts" setup>
const activeProject = useActiveProject();
const { safeSnapshotDates } = useSnapshot()
const isShowMore = ref<boolean>(false);
const headers = computed(() => {
return {
'x-from': safeSnapshotDates.value.from,
'x-to': safeSnapshotDates.value.to,
Authorization: authorizationHeaderComputed.value,
limit: isShowMore.value === true ? '200' : '10'
}
});
const browsersData = useFetch(`/api/metrics/${activeProject.value?._id}/data/browsers`, {
method: 'POST', headers, lazy: true, immediate: false
});
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
function showMore() {
isShowMore.value = true;
showDialog.value = true;
dialogBarData.value = browsersData.data.value || [];
}
onMounted(() => {
browsersData.execute();
});
</script>
<template>
<div class="flex flex-col gap-2">
<DashboardBarsCard @showMore="showMore()" @dataReload="browsersData.refresh()"
:data="browsersData.data.value || []" desc="The browsers most used to search your website."
:dataIcons="false" :loading="browsersData.pending.value" label="Top Browsers" sub-label="Browsers">
</DashboardBarsCard>
</div>
</template>

View File

@@ -1,46 +0,0 @@
<script lang="ts" setup>
const activeProject = useActiveProject();
const { safeSnapshotDates } = useSnapshot()
const isShowMore = ref<boolean>(false);
const headers = computed(() => {
return {
'x-from': safeSnapshotDates.value.from,
'x-to': safeSnapshotDates.value.to,
Authorization: authorizationHeaderComputed.value,
limit: isShowMore.value === true ? '200' : '10'
}
});
const devicesData = useFetch(`/api/metrics/${activeProject.value?._id}/data/devices`, {
method: 'POST', headers, lazy: true, immediate: false
});
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
function showMore() {
isShowMore.value = true;
showDialog.value = true;
dialogBarData.value = devicesData.data.value || [];
}
onMounted(() => {
devicesData.execute();
});
</script>
<template>
<div class="flex flex-col gap-2">
<DashboardBarsCard @showMore="showMore()" @dataReload="devicesData.refresh()" :data="devicesData.data.value || []" :dataIcons="false"
desc="The devices most used to access your website." :loading="devicesData.pending.value" label="Top Devices"
sub-label="Devices"></DashboardBarsCard>
</div>
</template>

View File

@@ -1,51 +0,0 @@
<script lang="ts" setup>
const router = useRouter();
function goToView() {
router.push('/dashboard/events');
}
const activeProject = useActiveProject();
const { safeSnapshotDates } = useSnapshot()
const isShowMore = ref<boolean>(false);
const headers = computed(() => {
return {
'x-from': safeSnapshotDates.value.from,
'x-to': safeSnapshotDates.value.to,
Authorization: authorizationHeaderComputed.value,
limit: isShowMore.value === true ? '200' : '10'
}
});
const eventsData = useFetch(`/api/metrics/${activeProject.value?._id}/data/events`, {
method: 'POST', headers, lazy: true, immediate: false
});
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
function showMore() {
isShowMore.value = true;
showDialog.value = true;
dialogBarData.value = eventsData.data.value || [];
}
onMounted(async () => {
eventsData.execute();
});
</script>
<template>
<div class="flex flex-col gap-2 h-full">
<DashboardBarsCard @showMore="showMore()" @showRawData="goToView()"
desc="Most frequent user events triggered in this project" @dataReload="eventsData.refresh()"
:data="eventsData.data.value || []" :loading="eventsData.pending.value" label="Top Events"
sub-label="Events" :rawButton="!isLiveDemo()"></DashboardBarsCard>
</div>
</template>

View File

@@ -1,44 +0,0 @@
<script lang="ts" setup>
const activeProject = useActiveProject();
const { safeSnapshotDates } = useSnapshot()
const isShowMore = ref<boolean>(false);
const headers = computed(() => {
return {
'x-from': safeSnapshotDates.value.from,
'x-to': safeSnapshotDates.value.to,
Authorization: authorizationHeaderComputed.value,
limit: isShowMore.value === true ? '200' : '10'
}
});
const ossData = useFetch(`/api/metrics/${activeProject.value?._id}/data/oss`, {
method: 'POST', headers, lazy: true, immediate: false
});
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
function showMore() {
isShowMore.value = true;
showDialog.value = true;
dialogBarData.value = ossData.data.value || [];
}
onMounted(() => {
ossData.execute();
});
</script>
<template>
<div class="flex flex-col gap-2 h-full">
<DashboardBarsCard @showMore="showMore()" @dataReload="ossData.refresh()" :data="ossData.data.value || []"
desc="The operating systems most commonly used by your website's visitors." :dataIcons="false"
:loading="ossData.pending.value" label="Top OS" sub-label="OSs"></DashboardBarsCard>
</div>
</template>

View File

@@ -1,68 +0,0 @@
<script lang="ts" setup>
import type { IconProvider } from './BarsCard.vue';
import ReferrerBarChart from '../referrer/ReferrerBarChart.vue';
function iconProvider(id: string): ReturnType<IconProvider> {
if (id === 'self') return ['icon', 'fas fa-link'];
return ['img', `https://s2.googleusercontent.com/s2/favicons?domain=${id}&sz=64`]
}
function elementTextTransformer(element: string) {
if (element === 'self') return 'Direct Link';
return element;
}
const activeProject = useActiveProject();
const { safeSnapshotDates } = useSnapshot()
const isShowMore = ref<boolean>(false);
const headers = computed(() => {
return {
'x-from': safeSnapshotDates.value.from,
'x-to': safeSnapshotDates.value.to,
Authorization: authorizationHeaderComputed.value,
limit: isShowMore.value === true ? '200' : '10'
}
});
const referrersData = useFetch(`/api/metrics/${activeProject.value?._id}/data/referrers`, {
method: 'POST', headers, lazy: true, immediate: false
});
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
// const customDialog = useCustomDialog();
// function onShowDetails(referrer: string) {
// customDialog.openDialog(ReferrerBarChart, { slice: 'day', referrer });
// }
function showMore() {
isShowMore.value = true;
showDialog.value = true;
dialogBarData.value = referrersData.data.value?.map(e => {
return { ...e, icon: iconProvider(e._id) }
}) || [];
}
onMounted(async () => {
referrersData.execute();
});
</script>
<template>
<div class="flex flex-col gap-2">
<DashboardBarsCard @showMore="showMore()"
:elementTextTransformer="elementTextTransformer" :iconProvider="iconProvider"
@dataReload="referrersData.refresh()" :showLink=true :data="referrersData.data.value || []"
:interactive="false" desc="Where users find your website." :dataIcons="true" :loading="referrersData.pending.value"
label="Top Referrers" sub-label="Referrers"></DashboardBarsCard>
</div>
</template>

View File

@@ -1,81 +0,0 @@
<script lang="ts" setup>
import type { VisitsWebsiteAggregated } from '~/server/api/metrics/[project_id]/data/websites';
const activeProject = useActiveProject();
const { safeSnapshotDates } = useSnapshot()
const isShowMore = ref<boolean>(false);
const currentWebsite = ref<string>("");
const websitesHeaders = computed(() => {
return {
'x-from': safeSnapshotDates.value.from,
'x-to': safeSnapshotDates.value.to,
Authorization: authorizationHeaderComputed.value,
limit: isShowMore.value === true ? '200' : '10'
}
});
const pagesHeaders = computed(() => {
return {
'x-from': safeSnapshotDates.value.from,
'x-to': safeSnapshotDates.value.to,
Authorization: authorizationHeaderComputed.value,
limit: isShowMore.value === true ? '200' : '10',
'x-website-name': currentWebsite.value
}
});
const websitesData = useFetch(`/api/metrics/${activeProject.value?._id}/data/websites`, {
method: 'POST', headers: websitesHeaders, lazy: true, immediate: false
});
const pagesData = useFetch(`/api/metrics/${activeProject.value?._id}/data/pages`, {
method: 'POST', headers: pagesHeaders, lazy: true, immediate: false
});
const isPagesView = ref<boolean>(false);
const currentData = computed(() => {
return isPagesView.value ? pagesData : websitesData
})
async function showDetails(website: string) {
currentWebsite.value = website;
pagesData.execute();
isPagesView.value = true;
}
async function showGeneral() {
websitesData.execute();
isPagesView.value = false;
}
const router = useRouter();
function goToView() {
router.push('/dashboard/visits');
}
onMounted(()=>{
websitesData.execute();
})
</script>
<template>
<div class="flex flex-col gap-2 h-full">
<DashboardBarsCard :hideShowMore="true" @showGeneral="showGeneral()" @showRawData="goToView()"
@dataReload="currentData.refresh()" @showDetails="showDetails" :data="currentData.data.value || []"
:loading="currentData.pending.value" :label="isPagesView ? 'Top pages' : 'Top Websites'"
:sub-label="isPagesView ? 'Page' : 'Website'"
:desc="isPagesView ? 'Most visited pages' : 'Most visited website in this project'"
:interactive="!isPagesView" :rawButton="!isLiveDemo()" :isDetailView="isPagesView">
</DashboardBarsCard>
</div>
</template>

View File

@@ -1,25 +1,47 @@
import type { StringExpressionOperator } from "mongoose";
type RefOrPrimitive<T> = T | Ref<T> | ComputedRef<T>
export type CustomOptions = { export type CustomOptions = {
useSnapshotDates?: boolean, useSnapshotDates?: boolean,
useActivePid?: boolean, useActivePid?: boolean,
slice?: string, slice?: RefOrPrimitive<string>,
limit?: RefOrPrimitive<number | string>,
custom?: Record<string, RefOrPrimitive<string>>
} }
const { token } = useAccessToken(); const { token } = useAccessToken();
const { projectId } = useProject(); const { projectId } = useProject();
const { safeSnapshotDates } = useSnapshot() const { safeSnapshotDates } = useSnapshot()
function getValueFromRefOrPrimitive<T>(data?: T | Ref<T> | ComputedRef<T>) {
console.log('Getting value of', data);
if (!data) return;
if (isRef(data)) return data.value;
return data;
}
export function useComputedHeaders(customOptions?: CustomOptions) { export function useComputedHeaders(customOptions?: CustomOptions) {
const useSnapshotDates = customOptions?.useSnapshotDates || true; const useSnapshotDates = customOptions?.useSnapshotDates || true;
const useActivePid = customOptions?.useActivePid || true; const useActivePid = customOptions?.useActivePid || true;
const headers = computed<Record<string, string>>(() => { const headers = computed<Record<string, string>>(() => {
const parsedCustom: Record<string, string> = {}
const customKeys = Object.keys(customOptions?.custom || {});
for (const key of customKeys) {
console.log('key', key);
parsedCustom[key] = getValueFromRefOrPrimitive((customOptions?.custom || {})[key]) ?? ''
}
return { return {
'Authorization': `Bearer ${token.value}`, 'Authorization': `Bearer ${token.value}`,
'x-pid': useActivePid ? (projectId.value ?? '') : '', 'x-pid': useActivePid ? (projectId.value ?? '') : '',
'x-from': useSnapshotDates ? (safeSnapshotDates.value.from ?? '') : '', 'x-from': useSnapshotDates ? (safeSnapshotDates.value.from ?? '') : '',
'x-to': useSnapshotDates ? (safeSnapshotDates.value.to ?? '') : '', 'x-to': useSnapshotDates ? (safeSnapshotDates.value.to ?? '') : '',
'x-slice': customOptions?.slice ?? '' 'x-slice': getValueFromRefOrPrimitive(customOptions?.slice) ?? '',
'x-limit': getValueFromRefOrPrimitive(customOptions?.limit)?.toString() ?? '',
...parsedCustom
} }
}) })

View File

@@ -2,6 +2,10 @@
definePageMeta({ layout: 'dashboard' }); definePageMeta({ layout: 'dashboard' });
const { project } = useProject();
const { data: projects } = useProjectsList(); const { data: projects } = useProjectsList();
const activeProject = useActiveProject(); const activeProject = useActiveProject();
@@ -33,14 +37,14 @@ onMounted(async () => {
}); });
}); });
const firstInteraction = useFetch<boolean>('/api/project/first_interaction', {
const firstInteractionUrl = computed(() => { lazy: true, headers: useComputedHeaders({ useSnapshotDates: false })
return `/api/metrics/${activeProject.value?._id}/first_interaction`
}); });
const firstInteraction = useFetch<boolean>(firstInteractionUrl, {
...signHeaders(),
lazy: true const showDashboard = computed(() => {
return project.value && firstInteraction.data.value
}); });
const selectLabels = [ const selectLabels = [
@@ -68,11 +72,99 @@ function goToUpgrade() {
<div class="dashboard w-full h-full overflow-y-auto pb-20 md:pt-4 lg:pt-0"> <div class="dashboard w-full h-full overflow-y-auto pb-20 md:pt-4 lg:pt-0">
<div v-if="showDashboard">
<div class="w-full px-4 py-2 gap-2 flex flex-col">
<!-- <div v-if="limitsInfo && limitsInfo.limited"
class="w-full bg-[#fbbf2422] p-4 rounded-lg text-[.9rem] flex items-center">
<div class="flex flex-col grow">
<div class="poppins font-semibold text-[#fbbf24]">
Limit reached
</div>
<div class="poppins text-[#fbbf24]">
Litlyx cannot receive new data as you reached your plan's limit. Resume all the great
features and collect even more data with a higher plan.
</div>
</div>
<div>
<LyxUiButton type="outline" @click="goToUpgrade()"> Upgrade </LyxUiButton>
</div>
</div> -->
<!-- <div v-if="!isPremium" class="w-full bg-[#5680f822] p-4 rounded-lg text-[.9rem] flex items-center">
<div class="flex flex-col grow">
<div class="poppins font-semibold text-lyx-primary">
Launch offer: 25% off
</div>
<div class="poppins text-lyx-primary">
We're offering an exclusive 25% discount forever on all plans starting from the Acceleration
Plan for our first 100 users who believe in our project.
<br>
Redeem Code: <span class="text-white font-bold text-[1rem]">LIT25</span> at checkout to
claim your discount.
</div>
</div>
<div>
<LyxUiButton type="outline" @click="goToUpgrade()"> Upgrade </LyxUiButton>
</div>
</div> -->
</div>
<div> <div>
<DashboardTopSection :key="refreshKey"></DashboardTopSection> <DashboardTopSection :key="refreshKey"></DashboardTopSection>
<DashboardTopCards :key="refreshKey"></DashboardTopCards> <DashboardTopCards :key="refreshKey"></DashboardTopCards>
</div> </div>
<div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row w-full">
<DashboardActionableChart :key="refreshKey"></DashboardActionableChart>
</div>
<div class="flex w-full justify-center mt-6 px-6">
<div class="flex w-full gap-6 flex-col xl:flex-row">
<div class="flex-1">
<BarCardWebsites :key="refreshKey"></BarCardWebsites>
</div>
<div class="flex-1">
<BarCardEvents :key="refreshKey"></BarCardEvents>
</div>
</div>
</div>
<div class="flex w-full justify-center mt-6 px-6">
<div class="flex w-full gap-6 flex-col xl:flex-row">
<div class="flex-1">
<BarCardReferrers :key="refreshKey"></BarCardReferrers>
</div>
<div class="flex-1">
<BarCardBrowsers :key="refreshKey"></BarCardBrowsers>
</div>
</div>
</div>
<div class="flex w-full justify-center mt-6 px-6">
<div class="flex w-full gap-6 flex-col xl:flex-row">
<div class="flex-1">
<BarCardOperatingSystems :key="refreshKey"></BarCardOperatingSystems>
</div>
<div class="flex-1">
<BarCardGeolocations :key="refreshKey"></BarCardGeolocations>
</div>
</div>
</div>
<div class="flex w-full justify-center mt-6 px-6">
<div class="flex w-full gap-6 flex-col xl:flex-row">
<div class="flex-1">
<BarCardDevices :key="refreshKey"></BarCardDevices>
</div>
<div class="flex-1">
</div>
</div>
</div>
</div>
<!-- <div :key="'home-' + isLiveDemo()" <!-- <div :key="'home-' + isLiveDemo()"
v-if="projects && activeProject && (firstInteraction.data.value === true) && !justLogged"> v-if="projects && activeProject && (firstInteraction.data.value === true) && !justLogged">

View File

@@ -0,0 +1,34 @@
import { VisitModel } from "@schema/metrics/VisitSchema";
import { Redis } from "~/server/services/CacheService";
import { getRequestData } from "~/server/utils/getRequestData";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false });
if (!data) return;
const { pid, from, to, project_id, limit } = data;
const cacheKey = `countries:${pid}:${from}:${to}`;
const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
const result = await VisitModel.aggregate([
{
$match: {
project_id,
created_at: { $gte: new Date(from), $lte: new Date(to) }
}
},
{ $group: { _id: "$country", count: { $sum: 1, } } },
{ $sort: { count: -1 } },
{ $limit: limit }
]);
return result as { _id: string, count: number }[];
});
});

View File

@@ -0,0 +1,34 @@
import { VisitModel } from "@schema/metrics/VisitSchema";
import { Redis } from "~/server/services/CacheService";
import { getRequestData } from "~/server/utils/getRequestData";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false });
if (!data) return;
const { pid, from, to, project_id, limit } = data;
const cacheKey = `devices:${pid}:${from}:${to}`;
const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
const result = await VisitModel.aggregate([
{
$match: {
project_id,
created_at: { $gte: new Date(from), $lte: new Date(to) }
}
},
{ $group: { _id: "$device", count: { $sum: 1, } } },
{ $sort: { count: -1 } },
{ $limit: limit }
]);
return result as { _id: string, count: number }[];
});
});

View File

@@ -0,0 +1,34 @@
import { EventModel } from "@schema/metrics/EventSchema";
import { Redis } from "~/server/services/CacheService";
import { getRequestData } from "~/server/utils/getRequestData";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false });
if (!data) return;
const { pid, from, to, project_id, limit } = data;
const cacheKey = `events:${pid}:${from}:${to}`;
const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
const result = await EventModel.aggregate([
{
$match: {
project_id,
created_at: { $gte: new Date(from), $lte: new Date(to) }
}
},
{ $group: { _id: "$name", count: { $sum: 1, } } },
{ $sort: { count: -1 } },
{ $limit: limit }
]);
return result as { _id: string, count: number }[];
});
});

View File

@@ -0,0 +1,34 @@
import { VisitModel } from "@schema/metrics/VisitSchema";
import { Redis } from "~/server/services/CacheService";
import { getRequestData } from "~/server/utils/getRequestData";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false });
if (!data) return;
const { pid, from, to, project_id, limit } = data;
const cacheKey = `oss:${pid}:${from}:${to}`;
const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
const result = await VisitModel.aggregate([
{
$match: {
project_id,
created_at: { $gte: new Date(from), $lte: new Date(to) }
}
},
{ $group: { _id: "$os", count: { $sum: 1, } } },
{ $sort: { count: -1 } },
{ $limit: limit }
]);
return result as { _id: string, count: number }[];
});
});

View File

@@ -0,0 +1,34 @@
import { VisitModel } from "@schema/metrics/VisitSchema";
import { Redis } from "~/server/services/CacheService";
import { getRequestData } from "~/server/utils/getRequestData";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false });
if (!data) return;
const { pid, from, to, project_id, limit } = data;
const cacheKey = `referrers:${pid}:${from}:${to}`;
const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
const result = await VisitModel.aggregate([
{
$match: {
project_id,
created_at: { $gte: new Date(from), $lte: new Date(to) }
}
},
{ $group: { _id: "$referrer", count: { $sum: 1, } } },
{ $sort: { count: -1 } },
{ $limit: limit }
]);
return result as { _id: string, count: number }[];
});
});

View File

@@ -0,0 +1,34 @@
import { VisitModel } from "@schema/metrics/VisitSchema";
import { Redis } from "~/server/services/CacheService";
import { getRequestData } from "~/server/utils/getRequestData";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false });
if (!data) return;
const { pid, from, to, project_id, limit } = data;
const cacheKey = `websites:${pid}:${from}:${to}`;
const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
const result = await VisitModel.aggregate([
{
$match: {
project_id,
created_at: { $gte: new Date(from), $lte: new Date(to) }
}
},
{ $group: { _id: "$website", count: { $sum: 1, } } },
{ $sort: { count: -1 } },
{ $limit: limit }
]);
return result as { _id: string, count: number }[];
});
});

View File

@@ -0,0 +1,32 @@
import { VisitModel } from "@schema/metrics/VisitSchema";
import { Redis } from "~/server/services/CacheService";
import { getRequestData } from "~/server/utils/getRequestData";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false });
if (!data) return;
const { pid, from, to, project_id, limit } = data;
const websiteName = getHeader(event, 'x-website-name');
const cacheKey = `websites_pages:${websiteName}:${pid}:${from}:${to}`;
const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
const result = await VisitModel.aggregate([
{ $match: { project_id }, },
{ $match: { website: websiteName, }, },
{ $group: { _id: "$page", count: { $sum: 1, } } },
{ $sort: { count: -1 } },
{ $limit: limit }
]);
return result as { _id: string, count: number }[];
});
});

View File

@@ -0,0 +1,21 @@
import { EventModel } from "@schema/metrics/EventSchema";
import { VisitModel } from "@schema/metrics/VisitSchema";
export default defineEventHandler(async event => {
const data = await getRequestData(event, {
requireSchema: false,
allowLitlyx: false,
requireSlice: false
});
if (!data) return;
const { project_id } = data;
const hasEvent = await EventModel.exists({ project_id });
if (hasEvent) return true;
const hasVisit = await VisitModel.exists({ project_id });
if (hasVisit) return true;
return false;
});

View File

@@ -0,0 +1,29 @@
import { EventModel } from "@schema/metrics/EventSchema";
import { Redis } from "~/server/services/CacheService";
import { executeTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService";
export default defineEventHandler(async event => {
const data = await getRequestData(event, { requireSchema: false, requireSlice: true });
if (!data) return;
const { pid, from, to, slice, project_id } = data;
const cacheKey = `timeline:events:${pid}:${from}:${to}`;
const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
const timelineData = await executeTimelineAggregation({
projectId: project_id,
model: EventModel,
from, to, slice,
});
const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from, to);
return timelineFilledMerged;
});
});

View File

@@ -60,7 +60,7 @@ export async function getRequestData(event: H3Event<EventHandlerRequest>, option
if (!from || !to) return setResponseStatus(event, 400, 'x-from and x-to are required'); if (!from || !to) return setResponseStatus(event, 400, 'x-from and x-to are required');
let model: (Model<any> | undefined) = undefined; let model: Model<any> = undefined as any;
const schemaName = getRequestHeader(event, 'x-schema'); const schemaName = getRequestHeader(event, 'x-schema');
if (requireSchema) { if (requireSchema) {