mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 15:58:38 +01:00
implementing snapshots
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
34
dashboard/pages/settings.vue
Normal file
34
dashboard/pages/settings.vue
Normal 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>
|
||||||
@@ -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'}`,
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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'}`,
|
||||||
|
|||||||
@@ -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'}`,
|
||||||
|
|||||||
@@ -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'}`,
|
||||||
|
|||||||
@@ -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'}`,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user