implementing snapshots

This commit is contained in:
Emily
2024-08-01 23:35:32 +02:00
parent 6c32b64ac6
commit 376b39e247
19 changed files with 222 additions and 130 deletions

View File

@@ -36,7 +36,7 @@ const { showDialog, closeDialog, dialogComponent, dialogParams, dialogStyle, dia
</div> </div>
<div v-if="debugMode" <div v-if="debugMode"
class="absolute bottom-8 left-4 bg-red-400 text-white text-[.9rem] font-bold px-4 py-[.2rem] rounded-lg z-[100]"> class="absolute bottom-8 right-4 bg-red-400 text-white text-[.9rem] font-bold px-4 py-[.2rem] rounded-lg z-[100]">
<div class="poppins flex sm:hidden"> XS </div> <div class="poppins flex sm:hidden"> XS </div>
<div class="poppins hidden sm:max-md:flex"> SM - MOBILE </div> <div class="poppins hidden sm:max-md:flex"> SM - MOBILE </div>
<div class="poppins hidden md:max-lg:flex"> MD - TABLET </div> <div class="poppins hidden md:max-lg:flex"> MD - TABLET </div>

View File

@@ -9,7 +9,8 @@ export type Entry = {
icon?: string, icon?: string,
action?: () => any, action?: () => any,
adminOnly?: boolean, adminOnly?: boolean,
external?: boolean external?: boolean,
grow?: boolean
} }
export type Section = { export type Section = {
@@ -66,7 +67,7 @@ async function deleteSnapshot(close: () => any) {
async function generatePDF() { async function generatePDF() {
try { try {
const res = await $fetch<Blob>('/api/project/generate_pdf', { const res = await $fetch<Blob>('/api/project/generate_pdf', {
...signHeaders(), ...signHeaders(),
responseType: 'blob' responseType: 'blob'
@@ -78,12 +79,23 @@ try {
a.download = `Report.pdf`; a.download = `Report.pdf`;
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} catch (ex: any) { } catch (ex: any) {
alert(ex.message); alert(ex.message);
}
} }
const { setToken } = useAccessToken();
const router = useRouter();
function onLogout() {
console.log('LOGOUT')
setToken('');
setLoggedUser(undefined);
router.push('/login');
} }
</script> </script>
<template> <template>
@@ -95,10 +107,14 @@ try {
<div class="p-4 gap-6 flex flex-col w-full"> <div class="p-4 gap-6 flex flex-col w-full">
<div class="flex items-center gap-2 ml-2"> <div class="flex items-center gap-2 ml-2">
<div class="bg-black h-[2.4rem] aspect-[1/1] flex items-center justify-center rounded-lg">
<!-- <div class="bg-black h-[2.4rem] aspect-[1/1] flex items-center justify-center rounded-lg">
<img class="h-[2rem]" :src="'/logo.png'"> <img class="h-[2rem]" :src="'/logo.png'">
</div> </div> -->
<div class="font-bold text-[1.4rem] text-gray-300"> Litlyx </div>
<!-- <div class="font-bold text-[1.4rem] text-gray-300"> Litlyx </div> -->
<div class="text-center w-full"> PROJECT SELECTOR </div>
<div class="grow flex justify-end text-[1.4rem] mr-2 lg:hidden"> <div class="grow flex justify-end text-[1.4rem] mr-2 lg:hidden">
<i @click="close()" class="fas fa-close"></i> <i @click="close()" class="fas fa-close"></i>
@@ -175,7 +191,7 @@ try {
</div> </div>
</div> </div>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4 h-full">
<div v-for="section of sections" class="flex flex-col gap-1"> <div v-for="section of sections" class="flex flex-col gap-1">
@@ -185,7 +201,7 @@ try {
class="bg-lyx-background text-gray-300 py-2 px-4 rounded-lg" :class="{ class="bg-lyx-background text-gray-300 py-2 px-4 rounded-lg" :class="{
'text-gray-700 pointer-events-none': entry.disabled, 'text-gray-700 pointer-events-none': entry.disabled,
'bg-lyx-background-lighter': route.path == (entry.to || '#'), 'bg-lyx-background-lighter': route.path == (entry.to || '#'),
'hover:bg-lyx-background-light': route.path != (entry.to || '#') 'hover:bg-lyx-background-light': route.path != (entry.to || '#'),
}"> }">
<NuxtLink @click="close() && entry.action?.()" :target="entry.external ? '_blank' : ''" <NuxtLink @click="close() && entry.action?.()" :target="entry.external ? '_blank' : ''"
@@ -204,6 +220,18 @@ try {
</div> </div>
<div class="grow"></div>
<div class="bg-lyx-background hover:bg-lyx-background-light text-gray-300 py-2 px-4 rounded-lg">
<div @click="onLogout()" class="flex cursor-pointer">
<div class="flex items-center w-[1.8rem] justify-start">
<i class="far fa-arrow-right-from-bracket"></i>
</div>
<div class="manrope">
Logout
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -55,7 +55,7 @@ const chartData = ref<ChartData<'doughnut'>>({
{ {
rotation: 1, rotation: 1,
data: [], data: [],
backgroundColor: ['#6bbbe3','#5655d0', '#a6d5cb', '#fae0b9'], backgroundColor: ['#6bbbe3', '#5655d0', '#a6d5cb', '#fae0b9'],
borderColor: ['#1d1d1f'], borderColor: ['#1d1d1f'],
borderWidth: 2 borderWidth: 2
}, },
@@ -65,15 +65,19 @@ const chartData = ref<ChartData<'doughnut'>>({
const { doughnutChartProps, doughnutChartRef } = useDoughnutChart({ chartData: chartData, options: chartOptions }); const { doughnutChartProps, doughnutChartRef } = useDoughnutChart({ chartData: chartData, options: chartOptions });
const res = useEventsData();
onMounted(async () => { onMounted(async () => {
const activeProject = useActiveProject() res.onResponse(resData => {
if (!resData.value) return;
const eventsData = await $fetch<EventsPie[]>(`/api/metrics/${activeProject.value?._id}/visits/events`, signHeaders()); chartData.value.labels = resData.value.map(e => {
chartData.value.labels = eventsData.map(e => {
return `${e._id}`; return `${e._id}`;
}); });
chartData.value.datasets[0].data = eventsData.map(e => e.count);
chartData.value.datasets[0].data = resData.value.map(e => e.count);
doughnutChartRef.value?.update(); doughnutChartRef.value?.update();
if (window.innerWidth < 800) { if (window.innerWidth < 800) {
@@ -81,11 +85,25 @@ onMounted(async () => {
chartOptions.value.plugins.legend.display = false; chartOptions.value.plugins.legend.display = false;
} }
} }
});
})
const chartVisible = computed(() => {
if (res.pending.value) return false;
if (!res.data.value) return false;
return true;
}) })
</script> </script>
<template> <template>
<DoughnutChart v-bind="doughnutChartProps"> </DoughnutChart> <div>
<div v-if="!chartVisible" class="flex justify-center py-40">
<i class="fas fa-spinner text-[2rem] text-accent animate-[spin_1s_linear_infinite] duration-500"></i>
</div>
<DoughnutChart v-if="chartVisible" v-bind="doughnutChartProps"> </DoughnutChart>
</div>
</template> </template>

View File

@@ -1,17 +1,34 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Slice } from '@services/DateService';
import { onMounted } from 'vue'; import { onMounted } from 'vue';
const datasets = ref<any[]>([]); const datasets = ref<any[]>([]);
const labels = ref<string[]>([]); const labels = ref<string[]>([]);
const ready = ref<boolean>(false); const ready = ref<boolean>(false);
const props = defineProps<{ slice: Slice }>();
const props = defineProps<{ slice: SliceName }>(); const slice = computed(() => props.slice);
async function loadData() { const res = useEventsStackedTimeline(slice);
const response = await useTimelineDataRaw('events_stacked', props.slice);
if (!response) return;
const fixed = fixMetrics(response, props.slice, { advanced: true, advancedGroupKey: 'name' }); const { safeSnapshotDates } = useSnapshot()
onMounted(async () => {
res.execute();
res.onResponse(resData => {
if (!resData.value) return;
const fixed = fixMetrics({
data:resData.value,
from: safeSnapshotDates.value.from,
to: safeSnapshotDates.value.to
}, slice.value, {
advanced: true,
advancedGroupKey: 'name'
});
const parsedDatasets: any[] = []; const parsedDatasets: any[] = [];
const colors = ['#5655d0', '#6bbbe3', '#a6d5cb', '#fae0b9']; const colors = ['#5655d0', '#6bbbe3', '#a6d5cb', '#fae0b9'];
@@ -28,25 +45,30 @@ async function loadData() {
if (!target) return; if (!target) return;
line.data.push(target.value); line.data.push(target.value);
}); });
} }
datasets.value = parsedDatasets; datasets.value = parsedDatasets;
labels.value = fixed.labels; labels.value = fixed.labels;
ready.value = true; ready.value = true;
});
} })
onMounted(async () => {
await loadData(); const chartVisible = computed(() => {
watch(props, async () => { await loadData(); }); if (res.pending.value) return false;
if (!res.data.value) return false;
return true;
}) })
</script> </script>
<template> <template>
<div> <div>
<AdvancedStackedBarChart v-if="ready" :datasets="datasets" :labels="labels"> <div v-if="!chartVisible" class="flex justify-center py-40">
<i class="fas fa-spinner text-[2rem] text-accent animate-[spin_1s_linear_infinite] duration-500"></i>
</div>
<AdvancedStackedBarChart v-if="chartVisible" :datasets="datasets" :labels="labels">
</AdvancedStackedBarChart> </AdvancedStackedBarChart>
</div> </div>
</template> </template>

View File

@@ -46,8 +46,8 @@ export function useFirstInteractionData() {
} }
export function useTimelineAdvanced(endpoint: string, slice: Ref<Slice>, customBody: Object = {}) { export function useTimelineAdvanced<T>(endpoint: string, slice: Ref<Slice>, customBody: Object = {}) {
const response = useCustomFetch<{ _id: string, count: number }[]>( const response = useCustomFetch<T>(
`/api/metrics/${activeProject.value?._id}/timeline/${endpoint}`, `/api/metrics/${activeProject.value?._id}/timeline/${endpoint}`,
() => signHeaders({ 'Content-Type': 'application/json' }).headers, { () => signHeaders({ 'Content-Type': 'application/json' }).headers, {
method: 'POST', method: 'POST',
@@ -59,12 +59,16 @@ export function useTimelineAdvanced(endpoint: string, slice: Ref<Slice>, customB
} }
export function useTimeline(endpoint: 'visits' | 'sessions' | 'referrers', slice: Ref<Slice>) { export function useTimeline(endpoint: 'visits' | 'sessions' | 'referrers' | 'events_stacked', slice: Ref<Slice>) {
return useTimelineAdvanced(endpoint, slice); return useTimelineAdvanced<{ _id: string, count: number }[]>(endpoint, slice);
} }
export async function useReferrersTimeline(referrer: string, slice: Ref<Slice>) { export async function useReferrersTimeline(referrer: string, slice: Ref<Slice>) {
return await useTimelineAdvanced('referrers', slice, { referrer }); return await useTimelineAdvanced<{ _id: string, count: number }[]>('referrers', slice, { referrer });
}
export function useEventsStackedTimeline(slice: Ref<Slice>) {
return useTimelineAdvanced<{ _id: string, name: string, count: number }[]>('events_stacked', slice);
} }
@@ -163,3 +167,4 @@ export function useDevicesData(limit: number = 10) {
); );
return res; return res;
} }

View File

@@ -12,7 +12,7 @@ const sections: Section[] = [
title: 'General', title: 'General',
entries: [ entries: [
{ label: 'Projects', icon: 'far fa-table-layout', to: '/project_selector' }, { label: 'Projects', icon: 'far fa-table-layout', to: '/project_selector' },
{ label: 'Members', icon: 'far fa-users', to: '/members' }, // { label: 'Members', icon: 'far fa-users', to: '/members' },
{ label: 'Admin', icon: 'fas fa-cat', adminOnly: true, to: '/admin' }, { label: 'Admin', icon: 'fas fa-cat', adminOnly: true, to: '/admin' },
] ]
}, },
@@ -22,6 +22,7 @@ const sections: Section[] = [
{ label: 'Dashboard', to: '/', icon: 'far fa-home' }, { label: 'Dashboard', to: '/', icon: 'far fa-home' },
{ label: 'Events', to: '/events', icon: 'far fa-bolt' }, { label: 'Events', to: '/events', icon: 'far fa-bolt' },
{ label: 'Analyst', to: '/analyst', icon: 'far fa-microchip-ai' }, { label: 'Analyst', to: '/analyst', icon: 'far fa-microchip-ai' },
{ label: 'Settings', to: '/settings', icon: 'far fa-gear' },
// { label: 'Report', to: '/report', icon: 'far fa-notes' }, // { label: 'Report', to: '/report', icon: 'far fa-notes' },
// { label: 'AI', to: '/dashboard/settings', icon: 'far fa-robot brightness-[.4]' }, // { label: 'AI', to: '/dashboard/settings', icon: 'far fa-robot brightness-[.4]' },
// { label: 'Visits', to: '/dashboard/visits', icon: 'far fa-eye' }, // { label: 'Visits', to: '/dashboard/visits', icon: 'far fa-eye' },
@@ -39,23 +40,8 @@ const sections: Section[] = [
label: 'Github', to: 'https://github.com/litlyx/litlyx', icon: 'fab fa-github', external: true, label: 'Github', to: 'https://github.com/litlyx/litlyx', icon: 'fab fa-github', external: true,
action() { Lit.event('git_clicked') }, action() { Lit.event('git_clicked') },
}, },
{ label: 'Billing', to: '/plans', icon: 'far fa-wallet' }, // { label: 'Billing', to: '/plans', icon: 'far fa-wallet' },
{ label: 'Book a demo', to: '/book_demo', icon: 'far fa-calendar' }, // { label: 'Book a demo', to: '/book_demo', icon: 'far fa-calendar' },
]
},
{
title: 'Actions',
entries: [
{
label: 'Logout',
icon: 'far fa-arrow-right-from-bracket',
action: () => {
console.log('LOGOUT')
setToken('');
setLoggedUser(undefined);
router.push('/login');
}
},
] ]
} }
]; ];

View File

@@ -17,7 +17,8 @@ const eventsStackedSelectIndex = ref<number>(0);
<div class="flex gap-6 flex-col xl:flex-row"> <div class="flex gap-6 flex-col xl:flex-row">
<CardTitled class="p-4 flex-[4]" title="Events" sub="Events stacked bar chart.">
<CardTitled class="p-4 flex-[4] w-full" title="Events" sub="Events stacked bar chart.">
<template #header> <template #header>
<SelectButton @changeIndex="eventsStackedSelectIndex = $event" <SelectButton @changeIndex="eventsStackedSelectIndex = $event"
:currentIndex="eventsStackedSelectIndex" :options="selectLabelsEvents"> :currentIndex="eventsStackedSelectIndex" :options="selectLabelsEvents">
@@ -29,20 +30,11 @@ const eventsStackedSelectIndex = ref<number>(0);
</div> </div>
</CardTitled> </CardTitled>
<div class="bg-card p-4 rounded-xl flex-[2] flex flex-col gap-10 h-full"> <CardTitled class="p-4 flex-[2] w-full h-full" title="Top events" sub="Displays key events.">
<div class="flex flex-col gap-1">
<div class="poppins font-semibold text-[1.4rem] text-text">
Top events
</div>
<div class="poppins text-[1rem] text-text-sub/90">
Displays key events.
</div>
</div>
<DashboardEventsChart class="w-full"> </DashboardEventsChart> <DashboardEventsChart class="w-full"> </DashboardEventsChart>
</CardTitled>
</div> </div>
</div>
<div class="flex"> <div class="flex">
<EventsUserFlow></EventsUserFlow> <EventsUserFlow></EventsUserFlow>

View File

@@ -70,10 +70,6 @@ const selectLabels = [
// { label: 'Month', value: 'month' }, // { label: 'Month', value: 'month' },
]; ];
function testAlert() {
createAlert('test', 'test', 'fas fa-home', 40000);
}
</script> </script>
@@ -99,7 +95,7 @@ function testAlert() {
<div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row"> <div class="mt-6 px-6 flex gap-6 flex-col 2xl:flex-row">
<CardTitled class="p-4 flex-1" title="Visits trends" sub="Shows trends in page visits."> <CardTitled class="p-4 flex-1 w-full" title="Visits trends" sub="Shows trends in page visits.">
<template #header> <template #header>
<SelectButton @changeIndex="mainChartSelectIndex = $event" :currentIndex="mainChartSelectIndex" <SelectButton @changeIndex="mainChartSelectIndex = $event" :currentIndex="mainChartSelectIndex"
:options="selectLabels"> :options="selectLabels">
@@ -111,7 +107,7 @@ function testAlert() {
</div> </div>
</CardTitled> </CardTitled>
<CardTitled class="p-4 flex-1" title="Sessions" sub="Shows trends in sessions."> <CardTitled class="p-4 flex-1 w-full" title="Sessions" sub="Shows trends in sessions.">
<template #header> <template #header>
<SelectButton @changeIndex="sessionsChartSelectIndex = $event" <SelectButton @changeIndex="sessionsChartSelectIndex = $event"
:currentIndex="sessionsChartSelectIndex" :options="selectLabels"> :currentIndex="sessionsChartSelectIndex" :options="selectLabels">

View File

@@ -0,0 +1,34 @@
<script lang="ts" setup>
definePageMeta({ layout: 'dashboard' });
const items = [
{ label: 'General', slot: 'general' },
{ label: 'Members', slot: 'members' },
{ label: 'Billing', slot: 'billing' },
{ label: 'Account', slot: 'account' }
]
</script>
<template>
<div class="px-10 py-8 h-dvh overflow-y-auto">
<div class="poppins font-semibold text-[1.3rem]"> Settings </div>
<UTabs :items="items" class="mt-8">
<template #general>
TODO
</template>
<template #members>
<SettingsMembers></SettingsMembers>
</template>
<template #billing>
<SettingsBilling></SettingsBilling>
</template>
<template #account>
TODO
</template>
</UTabs>
</div>
</template>

View File

@@ -15,8 +15,8 @@ export default defineEventHandler(async event => {
const { slice, from, to } = await readBody(event); const { slice, from, to } = await readBody(event);
if (!from) return setResponseStatus(event, 400, 'from is required'); if (!from) return setResponseStatus(event, 400, 'from is required');
if (!from) return setResponseStatus(event, 400, 'to is required'); if (!to) return setResponseStatus(event, 400, 'to is required');
if (!from) return setResponseStatus(event, 400, 'slice is required'); if (!slice) return setResponseStatus(event, 400, 'slice is required');
return await Redis.useCache({ return await Redis.useCache({
key: `timeline:events:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`, key: `timeline:events:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`,

View File

@@ -2,6 +2,7 @@ import { EventModel } from "@schema/metrics/EventSchema";
import { getTimeline } from "./generic"; import { getTimeline } from "./generic";
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService"; import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA"; import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import { executeAdvancedTimelineAggregation } from "~/server/services/TimelineService";
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const project_id = getRequestProjectId(event); const project_id = getRequestProjectId(event);
@@ -11,16 +12,23 @@ export default defineEventHandler(async event => {
if (!project) return; if (!project) return;
const { slice, duration } = await readBody(event); const { slice, from, to } = await readBody(event);
if (!from) return setResponseStatus(event, 400, 'from is required');
if (!to) return setResponseStatus(event, 400, 'to is required');
if (!slice) return setResponseStatus(event, 400, 'slice is required');
return await Redis.useCache({ key: `timeline:events_stacked:${project_id}:${slice}`, 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 () => {
const timelineStackedEvents = await getTimeline(EventModel, project_id, slice, duration,
{}, const timelineStackedEvents = await executeAdvancedTimelineAggregation<{ name: String }>({
{}, model: EventModel,
{ name: "$_id.name" }, projectId: project._id,
{ name: '$name' } from, to, slice,
); customProjection: { name: "$_id.name" },
customIdGroup: { name: '$name' },
})
return timelineStackedEvents; return timelineStackedEvents;
}); });

View File

@@ -16,8 +16,8 @@ export default defineEventHandler(async event => {
const { slice, from, to, referrer } = await readBody(event); const { slice, from, to, referrer } = await readBody(event);
if (!from) return setResponseStatus(event, 400, 'from is required'); if (!from) return setResponseStatus(event, 400, 'from is required');
if (!from) return setResponseStatus(event, 400, 'to is required'); if (!to) return setResponseStatus(event, 400, 'to is required');
if (!from) return setResponseStatus(event, 400, 'slice is required'); if (!slice) return setResponseStatus(event, 400, 'slice is required');
return await Redis.useCache({ return await Redis.useCache({
key: `timeline:referrers:${referrer}:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`, key: `timeline:referrers:${referrer}:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`,

View File

@@ -16,8 +16,8 @@ export default defineEventHandler(async event => {
const { slice, from, to } = await readBody(event); const { slice, from, to } = await readBody(event);
if (!from) return setResponseStatus(event, 400, 'from is required'); if (!from) return setResponseStatus(event, 400, 'from is required');
if (!from) return setResponseStatus(event, 400, 'to is required'); if (!to) return setResponseStatus(event, 400, 'to is required');
if (!from) return setResponseStatus(event, 400, 'slice is required'); if (!slice) return setResponseStatus(event, 400, 'slice is required');
return await Redis.useCache({ return await Redis.useCache({
key: `timeline:sessions:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`, key: `timeline:sessions:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`,

View File

@@ -27,8 +27,8 @@ export default defineEventHandler(async event => {
const { slice, from, to } = await readBody(event); const { slice, from, to } = await readBody(event);
if (!from) return setResponseStatus(event, 400, 'from is required'); if (!from) return setResponseStatus(event, 400, 'from is required');
if (!from) return setResponseStatus(event, 400, 'to is required'); if (!to) return setResponseStatus(event, 400, 'to is required');
if (!from) return setResponseStatus(event, 400, 'slice is required'); if (!slice) return setResponseStatus(event, 400, 'slice is required');
return await Redis.useCache({ return await Redis.useCache({
key: `timeline:sessions_duration:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`, key: `timeline:sessions_duration:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`,

View File

@@ -17,8 +17,8 @@ export default defineEventHandler(async event => {
const { slice, from, to } = await readBody(event); const { slice, from, to } = await readBody(event);
if (!from) return setResponseStatus(event, 400, 'from is required'); if (!from) return setResponseStatus(event, 400, 'from is required');
if (!from) return setResponseStatus(event, 400, 'to is required'); if (!to) return setResponseStatus(event, 400, 'to is required');
if (!from) return setResponseStatus(event, 400, 'slice is required'); if (!slice) return setResponseStatus(event, 400, 'slice is required');
return await Redis.useCache({ return await Redis.useCache({
key: `timeline:visits:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`, key: `timeline:visits:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`,

View File

@@ -16,14 +16,16 @@ export type TimelineAggregationOptions = {
export type AdvancedTimelineAggregationOptions = TimelineAggregationOptions & { export type AdvancedTimelineAggregationOptions = TimelineAggregationOptions & {
customMatch?: Record<string, any>, customMatch?: Record<string, any>,
customGroup?: Record<string, any>, customGroup?: Record<string, any>,
customProjection?: Record<string, any> customProjection?: Record<string, any>,
customIdGroup?: Record<string, any>
} }
export async function executeAdvancedTimelineAggregation(options: AdvancedTimelineAggregationOptions) { export async function executeAdvancedTimelineAggregation<T = {}>(options: AdvancedTimelineAggregationOptions) {
options.customMatch = options.customMatch || {}; options.customMatch = options.customMatch || {};
options.customGroup = options.customGroup || {}; options.customGroup = options.customGroup || {};
options.customProjection = options.customProjection || {}; options.customProjection = options.customProjection || {};
options.customIdGroup = options.customIdGroup || {};
const { group, sort, fromParts } = DateService.getQueryDateRange(options.slice); const { group, sort, fromParts } = DateService.getQueryDateRange(options.slice);
@@ -35,7 +37,7 @@ export async function executeAdvancedTimelineAggregation(options: AdvancedTimeli
...options.customMatch ...options.customMatch
} }
}, },
{ $group: { _id: group, count: { $sum: 1 }, ...options.customGroup } }, { $group: { _id: { ...group, ...options.customIdGroup }, count: { $sum: 1 }, ...options.customGroup } },
{ $sort: sort }, { $sort: sort },
{ $project: { _id: { $dateFromParts: fromParts }, count: "$count", ...options.customProjection } } { $project: { _id: { $dateFromParts: fromParts }, count: "$count", ...options.customProjection } }
] ]
@@ -44,7 +46,7 @@ export async function executeAdvancedTimelineAggregation(options: AdvancedTimeli
console.log(JSON.stringify(aggregation, null, 2)); console.log(JSON.stringify(aggregation, null, 2));
} }
const timeline: { _id: string, count: number }[] = await options.model.aggregate(aggregation); const timeline: { _id: string, count: number & T }[] = await options.model.aggregate(aggregation);
return timeline; return timeline;

View File

@@ -60,7 +60,6 @@ export function fixMetrics(result: { data: MetricsTimeline[], from: string, to:
} }
} }
const allKeys = !options.advanced ? [] : Array.from(new Set(result.data.map((e: any) => e[options.advancedGroupKey])).values()); const allKeys = !options.advanced ? [] : Array.from(new Set(result.data.map((e: any) => e[options.advancedGroupKey])).values());
const fixed: any[] = allDates.map(matchDate => { const fixed: any[] = allDates.map(matchDate => {
@@ -85,6 +84,8 @@ export function fixMetrics(result: { data: MetricsTimeline[], from: string, to:
return returnObject; return returnObject;
}); });
console.log({ allKeys })
if (slice === 'day' || slice == 'hour') fixed.pop(); if (slice === 'day' || slice == 'hour') fixed.pop();
const data = fixed.map(e => e.count); const data = fixed.map(e => e.count);