mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 07:48:37 +01:00
[NOT READY] fix dates + charts + ui
This commit is contained in:
@@ -6,6 +6,23 @@ import { useLineChart, LineChart } from 'vue-chart-3';
|
|||||||
|
|
||||||
const errorData = ref<{ errored: boolean, text: string }>({ errored: false, text: '' })
|
const errorData = ref<{ errored: boolean, text: string }>({ errored: false, text: '' })
|
||||||
|
|
||||||
|
|
||||||
|
function createGradient(startColor: string) {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
const ctx = c.getContext("2d");
|
||||||
|
let gradient: any = `${startColor}22`;
|
||||||
|
if (ctx) {
|
||||||
|
gradient = ctx.createLinearGradient(0, 25, 0, 300);
|
||||||
|
gradient.addColorStop(0, `${startColor}99`);
|
||||||
|
gradient.addColorStop(0.35, `${startColor}66`);
|
||||||
|
gradient.addColorStop(1, `${startColor}22`);
|
||||||
|
} else {
|
||||||
|
console.warn('Cannot get context for gradient');
|
||||||
|
}
|
||||||
|
|
||||||
|
return gradient;
|
||||||
|
}
|
||||||
|
|
||||||
const chartOptions = ref<ChartOptions<'line'>>({
|
const chartOptions = ref<ChartOptions<'line'>>({
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
@@ -68,12 +85,32 @@ const chartData = ref<ChartData<'line' | 'bar' | 'bubble'>>({
|
|||||||
borderColor: '#5655d7',
|
borderColor: '#5655d7',
|
||||||
borderWidth: 4,
|
borderWidth: 4,
|
||||||
fill: true,
|
fill: true,
|
||||||
tension: 0.45,
|
tension: 0.35,
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
pointHoverRadius: 10,
|
pointHoverRadius: 10,
|
||||||
hoverBackgroundColor: '#5655d7',
|
hoverBackgroundColor: '#5655d7',
|
||||||
hoverBorderColor: 'white',
|
hoverBorderColor: 'white',
|
||||||
hoverBorderWidth: 2,
|
hoverBorderWidth: 2,
|
||||||
|
segment: {
|
||||||
|
borderColor(ctx, options) {
|
||||||
|
const todayIndex = visitsData.data.value?.todayIndex;
|
||||||
|
if (!todayIndex || todayIndex == -1) return '#5655d7';
|
||||||
|
if (ctx.p1DataIndex >= todayIndex) return '#5655d700';
|
||||||
|
return '#5655d7'
|
||||||
|
},
|
||||||
|
borderDash(ctx, options) {
|
||||||
|
const todayIndex = visitsData.data.value?.todayIndex;
|
||||||
|
if (!todayIndex || todayIndex == -1) return undefined;
|
||||||
|
if (ctx.p1DataIndex == todayIndex - 1) return [3, 5];
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
backgroundColor(ctx, options) {
|
||||||
|
const todayIndex = visitsData.data.value?.todayIndex;
|
||||||
|
if (!todayIndex || todayIndex == -1) return createGradient('#5655d7');
|
||||||
|
if (ctx.p1DataIndex >= todayIndex) return '#5655d700';
|
||||||
|
return createGradient('#5655d7');
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Unique sessions',
|
label: 'Unique sessions',
|
||||||
@@ -85,18 +122,20 @@ const chartData = ref<ChartData<'line' | 'bar' | 'bubble'>>({
|
|||||||
hoverBorderColor: '#4abde8',
|
hoverBorderColor: '#4abde8',
|
||||||
hoverBorderWidth: 2,
|
hoverBorderWidth: 2,
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
|
// barThickness: 20,
|
||||||
|
borderSkipped: ['bottom']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Events',
|
label: 'Events',
|
||||||
data: [],
|
data: [],
|
||||||
backgroundColor: ['#fbbf24'],
|
backgroundColor: ['#fbbf24'],
|
||||||
borderColor: '#fbbf24',
|
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
hoverBackgroundColor: '#fbbf24',
|
hoverBackgroundColor: '#fbbf24',
|
||||||
hoverBorderColor: '#fbbf24',
|
hoverBorderColor: '#fbbf24',
|
||||||
hoverBorderWidth: 2,
|
hoverBorderWidth: 2,
|
||||||
type: 'bubble',
|
type: 'bubble',
|
||||||
stack: 'combined'
|
stack: 'combined',
|
||||||
|
borderColor: ["#fbbf24"]
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -109,6 +148,17 @@ function externalTooltipHandler(context: { chart: any, tooltip: TooltipModel<'li
|
|||||||
const { chart, tooltip } = context;
|
const { chart, tooltip } = context;
|
||||||
const tooltipEl = externalTooltipElement.value;
|
const tooltipEl = externalTooltipElement.value;
|
||||||
|
|
||||||
|
const currentIndex = tooltip.dataPoints[0].parsed.x;
|
||||||
|
|
||||||
|
const todayIndex = visitsData.data.value?.todayIndex;
|
||||||
|
if (todayIndex && todayIndex >= 0) {
|
||||||
|
if (currentIndex > todayIndex - 1) {
|
||||||
|
if (!tooltipEl) return;
|
||||||
|
return tooltipEl.style.opacity = '0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
currentTooltipData.value.visits = (tooltip.dataPoints.find(e => e.datasetIndex == 0)?.raw) as number;
|
currentTooltipData.value.visits = (tooltip.dataPoints.find(e => e.datasetIndex == 0)?.raw) as number;
|
||||||
currentTooltipData.value.sessions = (tooltip.dataPoints.find(e => e.datasetIndex == 1)?.raw) as number;
|
currentTooltipData.value.sessions = (tooltip.dataPoints.find(e => e.datasetIndex == 1)?.raw) as number;
|
||||||
currentTooltipData.value.events = ((tooltip.dataPoints.find(e => e.datasetIndex == 2)?.raw) as any)?.r2 as number;
|
currentTooltipData.value.events = ((tooltip.dataPoints.find(e => e.datasetIndex == 2)?.raw) as any)?.r2 as number;
|
||||||
@@ -142,14 +192,19 @@ const selectLabels: { label: string, value: Slice }[] = [
|
|||||||
{ label: 'Month', value: 'month' },
|
{ label: 'Month', value: 'month' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const selectLablesAvailable = computed<{ label: string, value: Slice, disabled: boolean }[]>(() => {
|
const selectLabelsAvailable = computed<{ label: string, value: Slice, disabled: boolean }[]>(() => {
|
||||||
return selectLabels.map(e => {
|
return selectLabels.map(e => {
|
||||||
return { ...e, disabled: !DateService.canUseSliceFromDays(snapshotDuration.value, e.value)[0] }
|
return { ...e, disabled: !DateService.canUseSliceFromDays(snapshotDuration.value, e.value)[0] }
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
const selectedSlice = computed<Slice>(() => {
|
const selectedSlice = computed<Slice>(() => {
|
||||||
return selectLablesAvailable.value[selectedLabelIndex.value].value
|
const targetValue = selectLabelsAvailable.value[selectedLabelIndex.value];
|
||||||
|
if (!targetValue) return 'day';
|
||||||
|
if (targetValue.disabled) {
|
||||||
|
selectedLabelIndex.value = selectLabelsAvailable.value.findIndex(e => !e.disabled);
|
||||||
|
}
|
||||||
|
return selectLabelsAvailable.value[selectedLabelIndex.value].value
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedLabelIndex = ref<number>(1);
|
const selectedLabelIndex = ref<number>(1);
|
||||||
@@ -158,13 +213,12 @@ 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);
|
||||||
|
|
||||||
console.log('RESPONSE', input);
|
|
||||||
const labels = input.map(e => DateService.getChartLabelFromISO(e._id, new Date().getTimezoneOffset(), selectedSlice.value));
|
const labels = input.map(e => DateService.getChartLabelFromISO(e._id, new Date().getTimezoneOffset(), selectedSlice.value));
|
||||||
console.log('LABELS', input);
|
|
||||||
|
|
||||||
if (input.length > 0) allDatesFull.value = input.map(e => e._id.toString());
|
if (input.length > 0) allDatesFull.value = input.map(e => e._id.toString());
|
||||||
return { data, labels }
|
|
||||||
|
const todayIndex = input.findIndex(e => new Date(e._id).getTime() > (Date.now() - new Date().getTimezoneOffset() * 1000 * 60));
|
||||||
|
|
||||||
|
return { data, labels, todayIndex }
|
||||||
}
|
}
|
||||||
|
|
||||||
function onResponseError(e: any) {
|
function onResponseError(e: any) {
|
||||||
@@ -200,21 +254,7 @@ watch(readyToDisplay, () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
function createGradient(startColor: string) {
|
|
||||||
const c = document.createElement('canvas');
|
|
||||||
const ctx = c.getContext("2d");
|
|
||||||
let gradient: any = `${startColor}22`;
|
|
||||||
if (ctx) {
|
|
||||||
gradient = ctx.createLinearGradient(0, 25, 0, 300);
|
|
||||||
gradient.addColorStop(0, `${startColor}99`);
|
|
||||||
gradient.addColorStop(0.35, `${startColor}66`);
|
|
||||||
gradient.addColorStop(1, `${startColor}22`);
|
|
||||||
} else {
|
|
||||||
console.warn('Cannot get context for gradient');
|
|
||||||
}
|
|
||||||
|
|
||||||
return gradient;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDataReady() {
|
function onDataReady() {
|
||||||
if (!visitsData.data.value) return;
|
if (!visitsData.data.value) return;
|
||||||
@@ -228,9 +268,10 @@ function onDataReady() {
|
|||||||
|
|
||||||
chartData.value.datasets[0].data = visitsData.data.value.data;
|
chartData.value.datasets[0].data = visitsData.data.value.data;
|
||||||
chartData.value.datasets[1].data = sessionsData.data.value.data;
|
chartData.value.datasets[1].data = sessionsData.data.value.data;
|
||||||
|
|
||||||
chartData.value.datasets[2].data = eventsData.data.value.data.map(e => {
|
chartData.value.datasets[2].data = eventsData.data.value.data.map(e => {
|
||||||
const rValue = 25 / maxEventSize * e;
|
const rValue = 20 / maxEventSize * e;
|
||||||
return { x: 0, y: maxChartY + 70, r: isNaN(rValue) ? 0 : rValue, r2: e }
|
return { x: 0, y: maxChartY + 20, r: isNaN(rValue) ? 0 : rValue, r2: e }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -238,6 +279,22 @@ 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')];
|
||||||
|
|
||||||
|
|
||||||
|
(chartData.value.datasets[1] as any).borderSkipped = sessionsData.data.value.data.map((e, i) => {
|
||||||
|
const todayIndex = eventsData.data.value?.todayIndex || 0;
|
||||||
|
if (i == todayIndex - 1) return true;
|
||||||
|
return 'bottom';
|
||||||
|
});
|
||||||
|
|
||||||
|
chartData.value.datasets[2].borderColor = eventsData.data.value.data.map((e, i) => {
|
||||||
|
const todayIndex = eventsData.data.value?.todayIndex || 0;
|
||||||
|
if (i == todayIndex - 1) return '#fbbf2400';
|
||||||
|
return '#fbbf24';
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
updateChart();
|
updateChart();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,7 +308,8 @@ const currentTooltipData = ref<{ visits: number, events: number, sessions: numbe
|
|||||||
const tooltipNameIndex = ['visits', 'sessions', 'events'];
|
const tooltipNameIndex = ['visits', 'sessions', 'events'];
|
||||||
|
|
||||||
function onLegendChange(dataset: any, index: number, checked: any) {
|
function onLegendChange(dataset: any, index: number, checked: any) {
|
||||||
dataset.hidden = !checked;
|
const newValue = !checked;
|
||||||
|
dataset.hidden = newValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const legendColors = ref<string[]>(['#5655d7', '#4abde8', '#fbbf24'])
|
const legendColors = ref<string[]>(['#5655d7', '#4abde8', '#fbbf24'])
|
||||||
@@ -268,7 +326,7 @@ const legendClasses = ref<string[]>([
|
|||||||
<CardTitled title="Trend chart" sub="Easily match Visits, Unique sessions and Events trends." class="w-full">
|
<CardTitled title="Trend chart" sub="Easily match Visits, Unique sessions and Events trends." class="w-full">
|
||||||
<template #header>
|
<template #header>
|
||||||
<SelectButton class="w-fit" @changeIndex="selectedLabelIndex = $event" :currentIndex="selectedLabelIndex"
|
<SelectButton class="w-fit" @changeIndex="selectedLabelIndex = $event" :currentIndex="selectedLabelIndex"
|
||||||
:options="selectLablesAvailable">
|
:options="selectLabelsAvailable">
|
||||||
</SelectButton>
|
</SelectButton>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ const props = defineProps<{
|
|||||||
data?: number[],
|
data?: number[],
|
||||||
labels?: string[],
|
labels?: string[],
|
||||||
ready?: boolean,
|
ready?: boolean,
|
||||||
slow?: boolean
|
slow?: boolean,
|
||||||
|
todayIndex: number
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { snapshotDuration } = useSnapshot()
|
const { snapshotDuration } = useSnapshot()
|
||||||
@@ -58,7 +59,7 @@ const uTooltipText = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="absolute bottom-0 left-0 w-full h-[50%] flex items-end"
|
<div class="absolute bottom-0 left-0 w-full h-[50%] flex items-end"
|
||||||
v-if="((props.data?.length || 0) > 0) && ready">
|
v-if="((props.data?.length || 0) > 0) && ready">
|
||||||
<DashboardEmbedChartCard v-if="ready" :data="props.data || []" :labels="props.labels || []"
|
<DashboardEmbedChartCard v-if="ready" :todayIndex="todayIndex" :data="props.data || []" :labels="props.labels || []"
|
||||||
:color="props.color">
|
:color="props.color">
|
||||||
</DashboardEmbedChartCard>
|
</DashboardEmbedChartCard>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
icon: string,
|
|
||||||
title: string,
|
|
||||||
text: string,
|
|
||||||
sub: string,
|
|
||||||
color: string
|
|
||||||
}>();
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
|
|
||||||
<div class="bg-menu p-4 rounded-xl flex flex-col gap-2 w-full lg:w-[20rem] relative pb-2 lg:pb-4">
|
|
||||||
|
|
||||||
<!-- <div class="absolute flex items-center justify-center right-4 top-4 cursor-pointer hover:text-blue-400">
|
|
||||||
<i class="fal fa-info-circle text-[.9rem] lg:text-[1.4rem]"></i>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<div class="gap-4 flex flex-row items-center lg:items-start lg:gap-2 lg:flex-col">
|
|
||||||
<div class="w-[2.5rem] h-[2.5rem] lg:w-[3.5rem] lg:h-[3.5rem] flex items-center justify-center rounded-lg"
|
|
||||||
:style="`background: ${props.color}`">
|
|
||||||
<i :class="icon" class="text-[1rem] lg:text-[1.5rem]"></i>
|
|
||||||
</div>
|
|
||||||
<div class="text-[1rem] lg:text-[1.3rem] text-text-sub/90 poppins">
|
|
||||||
{{ title }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2 items-center lg:items-end">
|
|
||||||
<div class="brockmann text-text text-[2rem] lg:text-[2.8rem] grow">
|
|
||||||
{{ text }}
|
|
||||||
</div>
|
|
||||||
<div class="poppins text-text-sub/90 text-[.9rem] lg:text-[1rem]"> {{ sub }} </div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
@@ -7,8 +7,10 @@ const props = defineProps<{
|
|||||||
data: any[],
|
data: any[],
|
||||||
labels: string[]
|
labels: string[]
|
||||||
color: string,
|
color: string,
|
||||||
|
todayIndex: number
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
||||||
const chartOptions = ref<ChartOptions<'line'>>({
|
const chartOptions = ref<ChartOptions<'line'>>({
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
@@ -48,10 +50,22 @@ const chartData = ref<ChartData<'line'>>({
|
|||||||
data: props.data,
|
data: props.data,
|
||||||
backgroundColor: [props.color + '77'],
|
backgroundColor: [props.color + '77'],
|
||||||
borderColor: props.color,
|
borderColor: props.color,
|
||||||
borderWidth: 4,
|
borderWidth: 2,
|
||||||
fill: true,
|
fill: false,
|
||||||
tension: 0.45,
|
tension: 0.35,
|
||||||
pointRadius: 0
|
pointRadius: 0,
|
||||||
|
segment: {
|
||||||
|
borderColor(ctx, options) {
|
||||||
|
if (!props.todayIndex || props.todayIndex == -1) return props.color;
|
||||||
|
if (ctx.p1DataIndex >= props.todayIndex) return props.color + '00';
|
||||||
|
return props.color;
|
||||||
|
},
|
||||||
|
borderDash(ctx, options) {
|
||||||
|
if (!props.todayIndex || props.todayIndex == -1) return undefined;
|
||||||
|
if (ctx.p1DataIndex == props.todayIndex -1) return [2, 4];
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ const { snapshot, safeSnapshotDates, snapshotDuration } = useSnapshot()
|
|||||||
|
|
||||||
const chartSlice = computed(() => {
|
const chartSlice = computed(() => {
|
||||||
if (snapshotDuration.value <= 3) return 'hour' as Slice;
|
if (snapshotDuration.value <= 3) return 'hour' as Slice;
|
||||||
if (snapshotDuration.value <= 3 * 7) return 'day' as Slice;
|
if (snapshotDuration.value <= 32) return 'day' as Slice;
|
||||||
if (snapshotDuration.value <= 3 * 30) return 'week' as Slice;
|
|
||||||
return 'month' as Slice;
|
return 'month' as Slice;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,7 +30,7 @@ function transformResponse(input: { _id: string, count: number }[]) {
|
|||||||
|
|
||||||
const trend = Math.max(Math.min(diffPercent, 99), -99);
|
const trend = Math.max(Math.min(diffPercent, 99), -99);
|
||||||
|
|
||||||
return { data, labels, trend }
|
return { data, labels, trend, input }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +91,11 @@ const avgSessionDuration = computed(() => {
|
|||||||
return `${hours > 0 ? hours + 'h ' : ''}${minutes}m ${seconds.toFixed()}s`
|
return `${hours > 0 ? hours + 'h ' : ''}${minutes}m ${seconds.toFixed()}s`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const todayIndex = computed(()=>{
|
||||||
|
if (!visitsData.data.value) return -1;
|
||||||
|
return visitsData.data.value.input.findIndex(e => new Date(e._id).getTime() > (Date.now() - new Date().getTimezoneOffset() * 1000 * 60));
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -99,26 +103,26 @@ const avgSessionDuration = computed(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="gap-6 px-6 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-2 m-cards-wrap:grid-cols-4">
|
<div class="gap-6 px-6 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-2 m-cards-wrap:grid-cols-4">
|
||||||
|
|
||||||
<DashboardCountCard :ready="!visitsData.pending.value" icon="far fa-earth" text="Total visits"
|
<DashboardCountCard :todayIndex="todayIndex" :ready="!visitsData.pending.value" icon="far fa-earth" text="Total visits"
|
||||||
:value="formatNumberK(visitsData.data.value?.data.reduce((a, e) => a + e, 0) || '...')"
|
:value="formatNumberK(visitsData.data.value?.data.reduce((a, e) => a + e, 0) || '...')"
|
||||||
:avg="formatNumberK(avgVisitDay) + '/day'" :trend="visitsData.data.value?.trend"
|
:avg="formatNumberK(avgVisitDay) + '/day'" :trend="visitsData.data.value?.trend"
|
||||||
:data="visitsData.data.value?.data" :labels="visitsData.data.value?.labels" color="#5655d7">
|
:data="visitsData.data.value?.data" :labels="visitsData.data.value?.labels" color="#5655d7">
|
||||||
</DashboardCountCard>
|
</DashboardCountCard>
|
||||||
|
|
||||||
<DashboardCountCard :ready="!bouncingRateData.pending.value" icon="far fa-chart-user" text="Bouncing rate"
|
<DashboardCountCard :todayIndex="todayIndex" :ready="!bouncingRateData.pending.value" icon="far fa-chart-user" text="Bouncing rate"
|
||||||
:value="avgBouncingRate" :trend="bouncingRateData.data.value?.trend" :slow="true"
|
:value="avgBouncingRate" :trend="bouncingRateData.data.value?.trend" :slow="true"
|
||||||
:data="bouncingRateData.data.value?.data" :labels="bouncingRateData.data.value?.labels" color="#1e9b86">
|
:data="bouncingRateData.data.value?.data" :labels="bouncingRateData.data.value?.labels" color="#1e9b86">
|
||||||
</DashboardCountCard>
|
</DashboardCountCard>
|
||||||
|
|
||||||
|
|
||||||
<DashboardCountCard :ready="!sessionsData.pending.value" icon="far fa-user" text="Unique visitors"
|
<DashboardCountCard :todayIndex="todayIndex" :ready="!sessionsData.pending.value" icon="far fa-user" text="Unique visitors"
|
||||||
:value="formatNumberK(sessionsData.data.value?.data.reduce((a, e) => a + e, 0) || '...')"
|
:value="formatNumberK(sessionsData.data.value?.data.reduce((a, e) => a + e, 0) || '...')"
|
||||||
:avg="formatNumberK(avgSessionsDay) + '/day'" :trend="sessionsData.data.value?.trend"
|
:avg="formatNumberK(avgSessionsDay) + '/day'" :trend="sessionsData.data.value?.trend"
|
||||||
:data="sessionsData.data.value?.data" :labels="sessionsData.data.value?.labels" color="#4abde8">
|
:data="sessionsData.data.value?.data" :labels="sessionsData.data.value?.labels" color="#4abde8">
|
||||||
</DashboardCountCard>
|
</DashboardCountCard>
|
||||||
|
|
||||||
|
|
||||||
<DashboardCountCard :ready="!sessionsDurationData.pending.value" icon="far fa-timer" text="Visit duration"
|
<DashboardCountCard :todayIndex="todayIndex" :ready="!sessionsDurationData.pending.value" icon="far fa-timer" text="Visit duration"
|
||||||
:value="avgSessionDuration" :trend="sessionsDurationData.data.value?.trend"
|
:value="avgSessionDuration" :trend="sessionsDurationData.data.value?.trend"
|
||||||
:data="sessionsDurationData.data.value?.data" :labels="sessionsDurationData.data.value?.labels"
|
:data="sessionsDurationData.data.value?.data" :labels="sessionsDurationData.data.value?.labels"
|
||||||
color="#f56523">
|
color="#f56523">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const { closeDialog } = useCustomDialog();
|
const { closeDialog } = useCustomDialog();
|
||||||
|
|
||||||
import { sub, format, isSameDay, type Duration } from 'date-fns'
|
import { sub, format, isSameDay, type Duration, startOfDay, endOfDay } from 'date-fns'
|
||||||
|
|
||||||
const ranges = [
|
const ranges = [
|
||||||
{ label: 'Last 7 days', duration: { days: 7 } },
|
{ label: 'Last 7 days', duration: { days: 7 } },
|
||||||
@@ -46,14 +46,14 @@ async function confirmSnapshot() {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: snapshotName.value,
|
name: snapshotName.value,
|
||||||
color: currentColor.value,
|
color: currentColor.value,
|
||||||
from: selected.value.start.toISOString(),
|
from: startOfDay(selected.value.start),
|
||||||
to: selected.value.end.toISOString()
|
to: endOfDay(selected.value.end)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
await updateSnapshots();
|
await updateSnapshots();
|
||||||
closeDialog();
|
closeDialog();
|
||||||
createAlert('Snapshot created','Snapshot created successfully', 'far fa-circle-check', 5000);
|
createAlert('Snapshot created', 'Snapshot created successfully', 'far fa-circle-check', 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -14,50 +14,20 @@ export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id'],
|
|||||||
name: 'Today',
|
name: 'Today',
|
||||||
from: fns.startOfDay(Date.now()),
|
from: fns.startOfDay(Date.now()),
|
||||||
to: fns.endOfDay(Date.now()),
|
to: fns.endOfDay(Date.now()),
|
||||||
color: '#CC11CC',
|
color: '#FFA600',
|
||||||
default: true
|
default: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastDay: DefaultSnapshot = {
|
const lastDay: DefaultSnapshot = {
|
||||||
project_id,
|
project_id,
|
||||||
_id: '___lastDay' as any,
|
_id: '___lastDay' as any,
|
||||||
name: 'Last Day',
|
name: 'Yesterday',
|
||||||
from: fns.startOfDay(fns.subDays(Date.now(), 1)),
|
from: fns.startOfDay(fns.subDays(Date.now(), 1)),
|
||||||
to: fns.endOfDay(fns.subDays(Date.now(), 1)),
|
to: fns.endOfDay(fns.subDays(Date.now(), 1)),
|
||||||
color: '#CC11CC',
|
color: '#FF8531',
|
||||||
default: true
|
default: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentWeek: DefaultSnapshot = {
|
|
||||||
project_id,
|
|
||||||
_id: '___currentWeek' as any,
|
|
||||||
name: 'Current Week',
|
|
||||||
from: fns.startOfWeek(Date.now()),
|
|
||||||
to: fns.endOfWeek(Date.now()),
|
|
||||||
color: '#CC11CC',
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastWeek: DefaultSnapshot = {
|
|
||||||
project_id,
|
|
||||||
_id: '___lastWeek' as any,
|
|
||||||
name: 'Last Week',
|
|
||||||
from: fns.startOfWeek(fns.subWeeks(Date.now(), 1)),
|
|
||||||
to: fns.endOfWeek(fns.subWeeks(Date.now(), 1)),
|
|
||||||
color: '#CC11CC',
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const currentMonth: DefaultSnapshot = {
|
|
||||||
project_id,
|
|
||||||
_id: '___currentMonth' as any,
|
|
||||||
name: 'Current Month',
|
|
||||||
from: fns.startOfMonth(Date.now()),
|
|
||||||
to: fns.endOfMonth(Date.now()),
|
|
||||||
color: '#CC11CC',
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastMonth: DefaultSnapshot = {
|
const lastMonth: DefaultSnapshot = {
|
||||||
project_id,
|
project_id,
|
||||||
@@ -65,9 +35,41 @@ export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id'],
|
|||||||
name: 'Last Month',
|
name: 'Last Month',
|
||||||
from: fns.startOfMonth(fns.subMonths(Date.now(), 1)),
|
from: fns.startOfMonth(fns.subMonths(Date.now(), 1)),
|
||||||
to: fns.endOfMonth(fns.subMonths(Date.now(), 1)),
|
to: fns.endOfMonth(fns.subMonths(Date.now(), 1)),
|
||||||
color: '#CC11CC',
|
color: '#BC5090',
|
||||||
default: true
|
default: true
|
||||||
}
|
}
|
||||||
|
const currentMonth: DefaultSnapshot = {
|
||||||
|
project_id,
|
||||||
|
_id: '___currentMonth' as any,
|
||||||
|
name: 'Current Month',
|
||||||
|
from: fns.startOfMonth(Date.now()),
|
||||||
|
to: fns.endOfMonth(Date.now()),
|
||||||
|
color: '#58508D',
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const lastWeek: DefaultSnapshot = {
|
||||||
|
project_id,
|
||||||
|
_id: '___lastWeek' as any,
|
||||||
|
name: 'Last Week',
|
||||||
|
from: fns.startOfWeek(fns.subWeeks(Date.now(), 1)),
|
||||||
|
to: fns.endOfWeek(fns.subWeeks(Date.now(), 1)),
|
||||||
|
color: '#3E909D',
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const currentWeek: DefaultSnapshot = {
|
||||||
|
project_id,
|
||||||
|
_id: '___currentWeek' as any,
|
||||||
|
name: 'Current Week',
|
||||||
|
from: fns.startOfWeek(Date.now()),
|
||||||
|
to: fns.endOfWeek(Date.now()),
|
||||||
|
color: '#007896',
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const allTime: DefaultSnapshot = {
|
const allTime: DefaultSnapshot = {
|
||||||
@@ -76,7 +78,7 @@ export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id'],
|
|||||||
name: 'All Time',
|
name: 'All Time',
|
||||||
from: new Date(project_created_at.toString()),
|
from: new Date(project_created_at.toString()),
|
||||||
to: new Date(Date.now()),
|
to: new Date(Date.now()),
|
||||||
color: '#CC11CC',
|
color: '#9362FF',
|
||||||
default: true
|
default: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { StringExpressionOperator } from "mongoose";
|
|
||||||
|
|
||||||
type RefOrPrimitive<T> = T | Ref<T> | ComputedRef<T>
|
type RefOrPrimitive<T> = T | Ref<T> | ComputedRef<T>
|
||||||
|
|
||||||
export type CustomOptions = {
|
export type CustomOptions = {
|
||||||
useSnapshotDates?: boolean,
|
useSnapshotDates?: boolean,
|
||||||
useActivePid?: boolean,
|
useActivePid?: boolean,
|
||||||
|
useTimeOffset?: boolean,
|
||||||
slice?: RefOrPrimitive<string>,
|
slice?: RefOrPrimitive<string>,
|
||||||
limit?: RefOrPrimitive<number | string>,
|
limit?: RefOrPrimitive<number | string>,
|
||||||
custom?: Record<string, RefOrPrimitive<string>>
|
custom?: Record<string, RefOrPrimitive<string>>
|
||||||
@@ -23,6 +23,7 @@ function getValueFromRefOrPrimitive<T>(data?: T | Ref<T> | ComputedRef<T>) {
|
|||||||
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 useTimeOffset = customOptions?.useTimeOffset || true;
|
||||||
|
|
||||||
const headers = computed<Record<string, string>>(() => {
|
const headers = computed<Record<string, string>>(() => {
|
||||||
console.trace('Computed recalculated');
|
console.trace('Computed recalculated');
|
||||||
@@ -37,6 +38,7 @@ export function useComputedHeaders(customOptions?: CustomOptions) {
|
|||||||
'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-time-offset': useTimeOffset ? (new Date().getTimezoneOffset().toString()) : '',
|
||||||
'x-slice': getValueFromRefOrPrimitive(customOptions?.slice) ?? '',
|
'x-slice': getValueFromRefOrPrimitive(customOptions?.slice) ?? '',
|
||||||
'x-limit': getValueFromRefOrPrimitive(customOptions?.limit)?.toString() ?? '',
|
'x-limit': getValueFromRefOrPrimitive(customOptions?.limit)?.toString() ?? '',
|
||||||
...parsedCustom
|
...parsedCustom
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const remoteSnapshots = useFetch<TProjectSnapshot[]>('/api/project/snapshots', {
|
|||||||
|
|
||||||
watch(project, async () => {
|
watch(project, async () => {
|
||||||
await remoteSnapshots.refresh();
|
await remoteSnapshots.refresh();
|
||||||
snapshot.value = isLiveDemo.value ? snapshots.value[2] : snapshots.value[2];
|
snapshot.value = isLiveDemo.value ? snapshots.value[3] : snapshots.value[3];
|
||||||
});
|
});
|
||||||
|
|
||||||
const snapshots = computed<GenericSnapshot[]>(() => {
|
const snapshots = computed<GenericSnapshot[]>(() => {
|
||||||
@@ -23,7 +23,7 @@ const snapshots = computed<GenericSnapshot[]>(() => {
|
|||||||
return [...defaultSnapshots, ...(remoteSnapshots.data.value || [])];
|
return [...defaultSnapshots, ...(remoteSnapshots.data.value || [])];
|
||||||
})
|
})
|
||||||
|
|
||||||
const snapshot = ref<GenericSnapshot>(snapshots.value[1]);
|
const snapshot = ref<GenericSnapshot>(snapshots.value[3]);
|
||||||
|
|
||||||
const safeSnapshotDates = computed(() => {
|
const safeSnapshotDates = computed(() => {
|
||||||
const from = new Date(snapshot.value?.from || 0).toISOString();
|
const from = new Date(snapshot.value?.from || 0).toISOString();
|
||||||
@@ -37,7 +37,7 @@ async function updateSnapshots() {
|
|||||||
|
|
||||||
const snapshotDuration = computed(() => {
|
const snapshotDuration = computed(() => {
|
||||||
const from = new Date(snapshot.value?.from || 0).getTime();
|
const from = new Date(snapshot.value?.from || 0).getTime();
|
||||||
const to = new Date(snapshot.value?.to || 0).getTime();
|
const to = new Date(snapshot.value?.to || 0).getTime() + 1000;
|
||||||
return fns.differenceInDays(to, from);
|
return fns.differenceInDays(to, from);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -44,16 +44,16 @@ const selfhosted = useSelfhosted();
|
|||||||
<BannerLimitsInfo v-if="!selfhosted" :key="refreshKey"></BannerLimitsInfo>
|
<BannerLimitsInfo v-if="!selfhosted" :key="refreshKey"></BannerLimitsInfo>
|
||||||
<BannerOffer v-if="!selfhosted" :key="refreshKey"></BannerOffer>
|
<BannerOffer v-if="!selfhosted" :key="refreshKey"></BannerOffer>
|
||||||
</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">
|
<div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row w-full">
|
||||||
<DashboardActionableChart :key="refreshKey"></DashboardActionableChart>
|
<DashboardActionableChart :key="refreshKey"></DashboardActionableChart>
|
||||||
</div>
|
</div>
|
||||||
<!--
|
|
||||||
<div class="flex w-full justify-center mt-6 px-6">
|
<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 w-full gap-6 flex-col xl:flex-row">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
@@ -85,7 +85,7 @@ const selfhosted = useSelfhosted();
|
|||||||
<BarCardOperatingSystems :key="refreshKey"></BarCardOperatingSystems>
|
<BarCardOperatingSystems :key="refreshKey"></BarCardOperatingSystems>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default defineEventHandler(async event => {
|
|||||||
const data = await getRequestData(event, { requireSchema: false, requireSlice: true });
|
const data = await getRequestData(event, { requireSchema: false, requireSlice: true });
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, slice, project_id } = data;
|
const { pid, from, to, slice, project_id, timeOffset } = data;
|
||||||
|
|
||||||
const cacheKey = `timeline:events:${pid}:${slice}:${from}:${to}`;
|
const cacheKey = `timeline:events:${pid}:${slice}:${from}:${to}`;
|
||||||
const cacheExp = 60;
|
const cacheExp = 60;
|
||||||
@@ -16,7 +16,7 @@ export default defineEventHandler(async event => {
|
|||||||
const timelineData = await executeTimelineAggregation({
|
const timelineData = await executeTimelineAggregation({
|
||||||
projectId: project_id,
|
projectId: project_id,
|
||||||
model: EventModel,
|
model: EventModel,
|
||||||
from, to, slice
|
from, to, slice, timeOffset
|
||||||
});
|
});
|
||||||
return timelineData;
|
return timelineData;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { EventModel } from "@schema/metrics/EventSchema";
|
import { EventModel } from "@schema/metrics/EventSchema";
|
||||||
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
|
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
|
||||||
import { executeAdvancedTimelineAggregation} from "~/server/services/TimelineService";
|
import { executeAdvancedTimelineAggregation } from "~/server/services/TimelineService";
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, { requireSchema: false, requireSlice: true });
|
const data = await getRequestData(event, { requireSchema: false, requireSlice: true });
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { from, to, slice, project_id } = data;
|
const { from, to, slice, project_id, timeOffset } = data;
|
||||||
|
|
||||||
return await Redis.useCache({ key: `timeline:events_stacked:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`, exp: TIMELINE_EXPIRE_TIME }, async () => {
|
return await Redis.useCache({ key: `timeline:events_stacked:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`, exp: TIMELINE_EXPIRE_TIME }, async () => {
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ export default defineEventHandler(async event => {
|
|||||||
from, to, slice,
|
from, to, slice,
|
||||||
customProjection: { name: "$_id.name" },
|
customProjection: { name: "$_id.name" },
|
||||||
customIdGroup: { name: '$name' },
|
customIdGroup: { name: '$name' },
|
||||||
|
timeOffset
|
||||||
})
|
})
|
||||||
|
|
||||||
return timelineStackedEvents;
|
return timelineStackedEvents;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default defineEventHandler(async event => {
|
|||||||
const data = await getRequestData(event, { requireSchema: false, requireSlice: true });
|
const data = await getRequestData(event, { requireSchema: false, requireSlice: true });
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, slice, project_id } = data;
|
const { pid, from, to, slice, project_id, timeOffset } = data;
|
||||||
|
|
||||||
const cacheKey = `timeline:sessions:${pid}:${slice}:${from}:${to}`;
|
const cacheKey = `timeline:sessions:${pid}:${slice}:${from}:${to}`;
|
||||||
const cacheExp = 60;
|
const cacheExp = 60;
|
||||||
@@ -16,7 +16,7 @@ export default defineEventHandler(async event => {
|
|||||||
const timelineData = await executeTimelineAggregation({
|
const timelineData = await executeTimelineAggregation({
|
||||||
projectId: project_id,
|
projectId: project_id,
|
||||||
model: SessionModel,
|
model: SessionModel,
|
||||||
from, to, slice,
|
from, to, slice, timeOffset
|
||||||
});
|
});
|
||||||
return timelineData;
|
return timelineData;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default defineEventHandler(async event => {
|
|||||||
const data = await getRequestData(event, { requireSchema: false, requireSlice: true });
|
const data = await getRequestData(event, { requireSchema: false, requireSlice: true });
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, slice, project_id } = data;
|
const { pid, from, to, slice, project_id, timeOffset } = data;
|
||||||
|
|
||||||
const cacheKey = `timeline:sessions_duration:${pid}:${slice}:${from}:${to}`;
|
const cacheKey = `timeline:sessions_duration:${pid}:${slice}:${from}:${to}`;
|
||||||
const cacheExp = 60;
|
const cacheExp = 60;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default defineEventHandler(async event => {
|
|||||||
const data = await getRequestData(event, { requireSchema: false, requireSlice: true });
|
const data = await getRequestData(event, { requireSchema: false, requireSlice: true });
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
const { pid, from, to, slice, project_id } = data;
|
const { pid, from, to, slice, project_id, timeOffset } = data;
|
||||||
|
|
||||||
const cacheKey = `timeline:visits:${pid}:${slice}:${from}:${to}`;
|
const cacheKey = `timeline:visits:${pid}:${slice}:${from}:${to}`;
|
||||||
const cacheExp = 60;
|
const cacheExp = 60;
|
||||||
@@ -16,7 +16,7 @@ export default defineEventHandler(async event => {
|
|||||||
const timelineData = await executeTimelineAggregation({
|
const timelineData = await executeTimelineAggregation({
|
||||||
projectId: project_id,
|
projectId: project_id,
|
||||||
model: VisitModel,
|
model: VisitModel,
|
||||||
from, to, slice
|
from, to, slice, timeOffset
|
||||||
});
|
});
|
||||||
return timelineData;
|
return timelineData;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export type TimelineAggregationOptions = {
|
|||||||
from: string | number,
|
from: string | number,
|
||||||
to: string | number,
|
to: string | number,
|
||||||
slice: Slice,
|
slice: Slice,
|
||||||
dateOffset?: number,
|
timeOffset?: number,
|
||||||
debug?: boolean
|
debug?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,14 +29,13 @@ export async function executeAdvancedTimelineAggregation<T = {}>(options: Advanc
|
|||||||
options.customIdGroup = options.customIdGroup || {};
|
options.customIdGroup = options.customIdGroup || {};
|
||||||
|
|
||||||
const { dateFromParts, granularity } = DateService.getGranularityData(options.slice, '$tmpDate');
|
const { dateFromParts, granularity } = DateService.getGranularityData(options.slice, '$tmpDate');
|
||||||
|
|
||||||
if (!dateFromParts) throw Error('Slice is probably not correct');
|
if (!dateFromParts) throw Error('Slice is probably not correct');
|
||||||
|
|
||||||
|
|
||||||
const [sliceValid, errorOrDays] = checkSliceValidity(options.from, options.to, options.slice);
|
const [sliceValid, errorOrDays] = checkSliceValidity(options.from, options.to, options.slice);
|
||||||
|
|
||||||
if (!sliceValid) throw Error(errorOrDays);
|
if (!sliceValid) throw Error(errorOrDays);
|
||||||
|
|
||||||
|
const timeOffset = options.timeOffset || 0;
|
||||||
|
|
||||||
const aggregation = [
|
const aggregation = [
|
||||||
{
|
{
|
||||||
$match: {
|
$match: {
|
||||||
@@ -54,7 +53,7 @@ export async function executeAdvancedTimelineAggregation<T = {}>(options: Advanc
|
|||||||
$dateSubtract: {
|
$dateSubtract: {
|
||||||
startDate: "$created_at",
|
startDate: "$created_at",
|
||||||
unit: "minute",
|
unit: "minute",
|
||||||
amount: options.dateOffset || -60
|
amount: timeOffset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,8 +75,8 @@ export async function executeAdvancedTimelineAggregation<T = {}>(options: Advanc
|
|||||||
step: 1,
|
step: 1,
|
||||||
unit: granularity,
|
unit: granularity,
|
||||||
bounds: [
|
bounds: [
|
||||||
new Date(options.from),
|
new Date(new Date(options.from).getTime() - (timeOffset * 1000 * 60)),
|
||||||
new Date(options.to)
|
new Date(new Date(options.to).getTime() - (timeOffset * 1000 * 60) + 1),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export type GetRequestDataOptions = {
|
|||||||
/** @default true */ allowLitlyx?: boolean,
|
/** @default true */ allowLitlyx?: boolean,
|
||||||
/** @default false */ requireSlice?: boolean,
|
/** @default false */ requireSlice?: boolean,
|
||||||
/** @default true */ requireRange?: boolean,
|
/** @default true */ requireRange?: boolean,
|
||||||
|
/** @default false */ requireOffset?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
async function hasAccessToProject(user_id: string, project: TProject) {
|
async function hasAccessToProject(user_id: string, project: TProject) {
|
||||||
@@ -49,6 +50,7 @@ export async function getRequestData(event: H3Event<EventHandlerRequest>, option
|
|||||||
const allowLitlyx = options?.allowLitlyx || true;
|
const allowLitlyx = options?.allowLitlyx || true;
|
||||||
const requireSlice = options?.requireSlice || false;
|
const requireSlice = options?.requireSlice || false;
|
||||||
const requireRange = options?.requireRange || false;
|
const requireRange = options?.requireRange || false;
|
||||||
|
const requireOffset = options?.requireOffset || false;
|
||||||
|
|
||||||
const pid = getHeader(event, 'x-pid');
|
const pid = getHeader(event, 'x-pid');
|
||||||
if (!pid) return setResponseStatus(event, 400, 'x-pid is required');
|
if (!pid) return setResponseStatus(event, 400, 'x-pid is required');
|
||||||
@@ -62,6 +64,12 @@ 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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const offsetRaw = getRequestHeader(event, 'x-time-offset');
|
||||||
|
const offset = parseInt(offsetRaw?.toString() as string);
|
||||||
|
if (requireOffset) {
|
||||||
|
if (offset === null || offset === undefined || isNaN(offset)) return setResponseStatus(event, 400, 'x-time-offset is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
let model: Model<any> = undefined as any;
|
let model: Model<any> = undefined as any;
|
||||||
|
|
||||||
@@ -99,7 +107,7 @@ export async function getRequestData(event: H3Event<EventHandlerRequest>, option
|
|||||||
return {
|
return {
|
||||||
from: from as string,
|
from: from as string,
|
||||||
to: to as string,
|
to: to as string,
|
||||||
pid, project_id, project, user, limit, slice, schemaName, model
|
pid, project_id, project, user, limit, slice, schemaName, model, timeOffset: offset
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ class DateService {
|
|||||||
public slicesData = slicesData;
|
public slicesData = slicesData;
|
||||||
|
|
||||||
getChartLabelFromISO(iso: string, offset: number, slice: Slice) {
|
getChartLabelFromISO(iso: string, offset: number, slice: Slice) {
|
||||||
const date = new Date(new Date(iso).getTime() - offset * 1000 * 60);
|
const date = new Date(new Date(iso).getTime() + offset * 1000 * 60);
|
||||||
if (slice === 'hour') return fns.format(date, 'HH:mm');
|
if (slice === 'hour') return fns.format(date, 'HH:mm');
|
||||||
if (slice === 'day') return fns.format(date, 'dd/MM');
|
if (slice === 'day') return fns.format(date, 'dd/MM');
|
||||||
if (slice === 'week') return fns.format(date, 'dd/MM');
|
if (slice === 'week') return fns.format(date, 'dd/MM');
|
||||||
@@ -43,19 +43,29 @@ class DateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
canUseSlice(from: string | number | Date, to: string | number | Date, slice: Slice) {
|
canUseSlice(from: string | number | Date, to: string | number | Date, slice: Slice) {
|
||||||
const daysDiff = fns.differenceInDays(new Date(to), new Date(from));
|
|
||||||
|
const daysDiff = fns.differenceInDays(
|
||||||
|
new Date(new Date(to).getTime() + 1000),
|
||||||
|
new Date(from)
|
||||||
|
);
|
||||||
|
|
||||||
return this.canUseSliceFromDays(daysDiff, slice);
|
return this.canUseSliceFromDays(daysDiff, slice);
|
||||||
}
|
}
|
||||||
|
|
||||||
canUseSliceFromDays(days: number, slice: Slice): [false, string] | [true, number] {
|
canUseSliceFromDays(days: number, slice: Slice): [false, string] | [true, number] {
|
||||||
|
|
||||||
// 3 Days
|
// 3 Days
|
||||||
if (slice === 'hour' && (days > 3)) return [false, 'Date gap too big for this slice'];
|
if (slice === 'hour' && (days > 3)) return [false, 'Date gap too big for this slice'];
|
||||||
// 3 Weeks
|
// 3 Weeks
|
||||||
if (slice === 'day' && (days > 31)) return [false, 'Date gap too big for this slice'];
|
if (slice === 'day' && (days > 31)) return [false, 'Date gap too big for this slice'];
|
||||||
// 3 Months
|
|
||||||
if (slice === 'week' && (days > 30 * 3)) return [false, 'Date gap too big for this slice'];
|
|
||||||
// 3 Years
|
// 3 Years
|
||||||
if (slice === 'month' && (days > 365 * 3)) return [false, 'Date gap too big for this slice'];
|
if (slice === 'month' && (days > 365 * 3)) return [false, 'Date gap too big for this slice'];
|
||||||
|
|
||||||
|
// 2 days
|
||||||
|
if (slice === 'day' && (days < 2)) return [false, 'Date gap too small for this slice'];
|
||||||
|
// 2 month
|
||||||
|
if (slice === 'month' && (days < 31 * 2)) return [false, 'Date gap too small for this slice'];
|
||||||
|
|
||||||
return [true, days]
|
return [true, days]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user