mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 07:48:37 +01:00
add selfhosted env + start fix dates
This commit is contained in:
@@ -24,9 +24,12 @@ const chartOptions = ref<ChartOptions<'line'>>({
|
|||||||
color: '#CCCCCC22',
|
color: '#CCCCCC22',
|
||||||
// borderDash: [5, 10]
|
// borderDash: [5, 10]
|
||||||
},
|
},
|
||||||
|
beginAtZero: true,
|
||||||
},
|
},
|
||||||
x: {
|
x: {
|
||||||
ticks: { display: true },
|
ticks: { display: true },
|
||||||
|
stacked: false,
|
||||||
|
offset: false,
|
||||||
grid: {
|
grid: {
|
||||||
display: true,
|
display: true,
|
||||||
drawBorder: false,
|
drawBorder: false,
|
||||||
@@ -136,7 +139,7 @@ const { snapshotDuration } = useSnapshot();
|
|||||||
const selectLabels: { label: string, value: Slice }[] = [
|
const selectLabels: { label: string, value: Slice }[] = [
|
||||||
{ label: 'Hour', value: 'hour' },
|
{ label: 'Hour', value: 'hour' },
|
||||||
{ label: 'Day', value: 'day' },
|
{ label: 'Day', value: 'day' },
|
||||||
{ label: 'Week', value: 'week' },
|
// { label: 'Week', value: 'week' },
|
||||||
{ label: 'Month', value: 'month' },
|
{ label: 'Month', value: 'month' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -220,6 +223,9 @@ function onDataReady() {
|
|||||||
const maxChartY = Math.max(...visitsData.data.value.data, ...sessionsData.data.value.data);
|
const maxChartY = Math.max(...visitsData.data.value.data, ...sessionsData.data.value.data);
|
||||||
const maxEventSize = Math.max(...eventsData.data.value.data)
|
const maxEventSize = Math.max(...eventsData.data.value.data)
|
||||||
|
|
||||||
|
|
||||||
|
const currentDateTime = Date.now();
|
||||||
|
|
||||||
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 => {
|
||||||
@@ -227,6 +233,7 @@ function onDataReady() {
|
|||||||
return { x: 0, y: maxChartY + 70, r: isNaN(rValue) ? 0 : rValue, r2: e }
|
return { x: 0, y: maxChartY + 70, r: isNaN(rValue) ? 0 : rValue, r2: e }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
chartData.value.datasets[0].backgroundColor = [createGradient('#5655d7')];
|
chartData.value.datasets[0].backgroundColor = [createGradient('#5655d7')];
|
||||||
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')];
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const { onlineUsers, stopWatching, startWatching } = useOnlineUsers();
|
|||||||
onMounted(() => startWatching());
|
onMounted(() => startWatching());
|
||||||
onUnmounted(() => stopWatching());
|
onUnmounted(() => stopWatching());
|
||||||
|
|
||||||
|
const selfhosted = useSelfhosted();
|
||||||
|
|
||||||
const { createAlert } = useAlert();
|
const { createAlert } = useAlert();
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ function showAnomalyInfoAlert() {
|
|||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
|
|
||||||
<div class="flex gap-2 items-center text-text/90 justify-center md:justify-start">
|
<div v-if="!selfhosted" class="flex gap-2 items-center text-text/90 justify-center md:justify-start">
|
||||||
<div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div>
|
<div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div>
|
||||||
<div class="poppins font-regular text-[.9rem]"> AI Anomaly Detector </div>
|
<div class="poppins font-regular text-[.9rem]"> AI Anomaly Detector </div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import * as fns from 'date-fns';
|
|||||||
export type DefaultSnapshot = TProjectSnapshot & { default: true }
|
export type DefaultSnapshot = TProjectSnapshot & { default: true }
|
||||||
export type GenericSnapshot = TProjectSnapshot | DefaultSnapshot;
|
export type GenericSnapshot = TProjectSnapshot | DefaultSnapshot;
|
||||||
|
|
||||||
export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id']) {
|
export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id'], project_created_at: Date | string) {
|
||||||
|
|
||||||
const today: DefaultSnapshot = {
|
const today: DefaultSnapshot = {
|
||||||
project_id,
|
project_id,
|
||||||
@@ -70,9 +70,19 @@ export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id'])
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const allTime: DefaultSnapshot = {
|
||||||
|
project_id,
|
||||||
|
_id: '___allTime' as any,
|
||||||
|
name: 'All Time',
|
||||||
|
from: new Date(project_created_at.toString()),
|
||||||
|
to: new Date(Date.now()),
|
||||||
|
color: '#CC11CC',
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const snapshotList = [lastDay, today, lastMonth, currentMonth, lastWeek, currentWeek]
|
|
||||||
|
const snapshotList = [lastDay, today, lastMonth, currentMonth, lastWeek, currentWeek, allTime]
|
||||||
|
|
||||||
return snapshotList;
|
return snapshotList;
|
||||||
|
|
||||||
|
|||||||
7
dashboard/composables/useSelfhosted.ts
Normal file
7
dashboard/composables/useSelfhosted.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
const app = useRuntimeConfig();
|
||||||
|
|
||||||
|
export function useSelfhosted() {
|
||||||
|
return app.public.SELFHOSTED === 'TRUE';
|
||||||
|
}
|
||||||
@@ -15,11 +15,11 @@ 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[0] : snapshots.value[1];
|
snapshot.value = isLiveDemo.value ? snapshots.value[2] : snapshots.value[2];
|
||||||
});
|
});
|
||||||
|
|
||||||
const snapshots = computed<GenericSnapshot[]>(() => {
|
const snapshots = computed<GenericSnapshot[]>(() => {
|
||||||
const defaultSnapshots: GenericSnapshot[] = project.value?._id ? getDefaultSnapshots(project.value._id as any) : [];
|
const defaultSnapshots: GenericSnapshot[] = project.value?._id ? getDefaultSnapshots(project.value._id as any, project.value.created_at) : [];
|
||||||
return [...defaultSnapshots, ...(remoteSnapshots.data.value || [])];
|
return [...defaultSnapshots, ...(remoteSnapshots.data.value || [])];
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ const { project } = useProject();
|
|||||||
|
|
||||||
const pricingDrawer = usePricingDrawer();
|
const pricingDrawer = usePricingDrawer();
|
||||||
|
|
||||||
|
const selfhosted = useSelfhosted();
|
||||||
|
|
||||||
const sections: Section[] = [
|
const sections: Section[] = [
|
||||||
{
|
{
|
||||||
title: '',
|
title: '',
|
||||||
@@ -16,10 +18,12 @@ const sections: Section[] = [
|
|||||||
{ label: 'Web Analytics', to: '/', icon: 'fal fa-table-layout' },
|
{ label: 'Web Analytics', to: '/', icon: 'fal fa-table-layout' },
|
||||||
{ label: 'Custom Events', to: '/events', icon: 'fal fa-square-bolt' },
|
{ label: 'Custom Events', to: '/events', icon: 'fal fa-square-bolt' },
|
||||||
{ label: 'Ask AI', to: '/analyst', icon: 'fal fa-sparkles' },
|
{ label: 'Ask AI', to: '/analyst', icon: 'fal fa-sparkles' },
|
||||||
{ label: 'Security', to: '/security', icon: 'fal fa-shield' },
|
{ label: 'Security', to: '/security', icon: 'fal fa-shield', disabled: selfhosted },
|
||||||
|
|
||||||
// { label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true },
|
// { label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true },
|
||||||
// { label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true },
|
// { label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true },
|
||||||
// { label: 'Integrations (soon)', to: '/integrations', icon: 'fal fa-cube', disabled: true },
|
// { label: 'Integrations (soon)', to: '/integrations', icon: 'fal fa-cube', disabled: true },
|
||||||
|
|
||||||
{ label: 'Settings', to: '/settings', icon: 'fal fa-gear' },
|
{ label: 'Settings', to: '/settings', icon: 'fal fa-gear' },
|
||||||
{
|
{
|
||||||
grow: true,
|
grow: true,
|
||||||
|
|||||||
@@ -55,10 +55,12 @@ export default defineNuxtConfig({
|
|||||||
STRIPE_SECRET_TEST: process.env.STRIPE_SECRET_TEST,
|
STRIPE_SECRET_TEST: process.env.STRIPE_SECRET_TEST,
|
||||||
STRIPE_WH_SECRET_TEST: process.env.STRIPE_WH_SECRET_TEST,
|
STRIPE_WH_SECRET_TEST: process.env.STRIPE_WH_SECRET_TEST,
|
||||||
NOAUTH_USER_EMAIL: process.env.NOAUTH_USER_EMAIL,
|
NOAUTH_USER_EMAIL: process.env.NOAUTH_USER_EMAIL,
|
||||||
NOAUTH_USER_NAME: process.env.NOAUTH_USER_NAME,
|
NOAUTH_USER_NAME: process.env.NOAUTH_USER_NAME || 'FALSE',
|
||||||
|
SELFHOSTED: process.env.SELFHOSTED,
|
||||||
public: {
|
public: {
|
||||||
AUTH_MODE: process.env.AUTH_MODE,
|
AUTH_MODE: process.env.AUTH_MODE,
|
||||||
GITHUB_CLIENT_ID: process.env.GITHUB_AUTH_CLIENT_ID || 'NONE'
|
GITHUB_CLIENT_ID: process.env.GITHUB_AUTH_CLIENT_ID || 'NONE',
|
||||||
|
SELFHOSTED: process.env.SELFHOSTED || 'FALSE',
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -60,12 +60,21 @@ async function sendMessage() {
|
|||||||
|
|
||||||
|
|
||||||
} catch (ex: any) {
|
} catch (ex: any) {
|
||||||
|
|
||||||
if (ex.message.includes('CHAT_LIMIT_REACHED')) {
|
if (ex.message.includes('CHAT_LIMIT_REACHED')) {
|
||||||
currentChatMessages.value.push({
|
currentChatMessages.value.push({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: 'You have reached your current tier chat limit.\n Upgrade to an higher tier. <a style="color: blue; text-decoration: underline;" href="/plans"> Upgrade now. </a>',
|
content: 'You have reached your current tier chat limit.\n Upgrade to an higher tier. <a style="color: blue; text-decoration: underline;" href="/plans"> Upgrade now. </a>',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ex.message.includes('Unauthorized')) {
|
||||||
|
currentChatMessages.value.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: 'To use AI you need to provide AI_ORG, AI_PROJECT and AI_KEY in docker compose',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -89,7 +98,7 @@ async function openChat(chat_id?: string) {
|
|||||||
}
|
}
|
||||||
currentChatId.value = chat_id;
|
currentChatId.value = chat_id;
|
||||||
const messages = await $fetch(`/api/ai/${chat_id}/get_messages`, {
|
const messages = await $fetch(`/api/ai/${chat_id}/get_messages`, {
|
||||||
headers: useComputedHeaders({useSnapshotDates:false}).value
|
headers: useComputedHeaders({ useSnapshotDates: false }).value
|
||||||
});
|
});
|
||||||
if (!messages) return;
|
if (!messages) return;
|
||||||
|
|
||||||
@@ -132,7 +141,7 @@ async function deleteChat(chat_id: string) {
|
|||||||
currentChatMessages.value = [];
|
currentChatMessages.value = [];
|
||||||
}
|
}
|
||||||
await $fetch(`/api/ai/${chat_id}/delete`, {
|
await $fetch(`/api/ai/${chat_id}/delete`, {
|
||||||
headers: useComputedHeaders({useSnapshotDates:false}).value
|
headers: useComputedHeaders({ useSnapshotDates: false }).value
|
||||||
});
|
});
|
||||||
await reloadChatsList();
|
await reloadChatsList();
|
||||||
}
|
}
|
||||||
@@ -148,6 +157,7 @@ const { visible: pricingDrawerVisible } = usePricingDrawer()
|
|||||||
|
|
||||||
<div class="flex-[5] py-8 flex h-full flex-col items-center relative overflow-y-hidden">
|
<div class="flex-[5] py-8 flex h-full flex-col items-center relative overflow-y-hidden">
|
||||||
|
|
||||||
|
|
||||||
<div class="flex flex-col items-center xl:mt-[20vh] px-8 xl:px-28"
|
<div class="flex flex-col items-center xl:mt-[20vh] px-8 xl:px-28"
|
||||||
v-if="currentChatMessages.length == 0">
|
v-if="currentChatMessages.length == 0">
|
||||||
<div class="w-[7rem] xl:w-[10rem]">
|
<div class="w-[7rem] xl:w-[10rem]">
|
||||||
@@ -164,6 +174,7 @@ const { visible: pricingDrawerVisible } = usePricingDrawer()
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div ref="scroller" class="flex flex-col w-full gap-6 px-6 xl:px-28 overflow-y-auto pb-20">
|
<div ref="scroller" class="flex flex-col w-full gap-6 px-6 xl:px-28 overflow-y-auto pb-20">
|
||||||
|
|
||||||
<div class="flex w-full flex-col" v-for="message of currentChatMessages">
|
<div class="flex w-full flex-col" v-for="message of currentChatMessages">
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ definePageMeta({ layout: 'dashboard' });
|
|||||||
const { project } = useProject();
|
const { project } = useProject();
|
||||||
|
|
||||||
const isPremium = computed(() => (project.value?.premium_type || 0) > 0);
|
const isPremium = computed(() => (project.value?.premium_type || 0) > 0);
|
||||||
|
const selfhosted = useSelfhosted();
|
||||||
|
const canDownload = computed(() => {
|
||||||
|
if (selfhosted) return true;
|
||||||
|
return isPremium.value;
|
||||||
|
});
|
||||||
|
|
||||||
const metricsInfo = ref<number>(0);
|
const metricsInfo = ref<number>(0);
|
||||||
|
|
||||||
@@ -105,12 +110,12 @@ function goToUpgrade() {
|
|||||||
}" v-model="selectedTimeFrom" :options="options"></USelectMenu>
|
}" v-model="selectedTimeFrom" :options="options"></USelectMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isPremium" @click="downloadCSV()"
|
<div v-if="canDownload" @click="downloadCSV()"
|
||||||
class="bg-[#57c78fc0] hover:bg-[#57c78fab] cursor-pointer text-text poppins font-semibold px-8 py-1 rounded-lg">
|
class="bg-[#57c78fc0] hover:bg-[#57c78fab] cursor-pointer text-text poppins font-semibold px-8 py-1 rounded-lg">
|
||||||
Download CSV
|
Download CSV
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!isPremium" @click="goToUpgrade()"
|
<div v-if="!canDownload" @click="goToUpgrade()"
|
||||||
class="bg-[#57c78f46] hover:bg-[#57c78f42] flex gap-4 items-center cursor-pointer text-text poppins font-semibold px-8 py-2 rounded-lg">
|
class="bg-[#57c78f46] hover:bg-[#57c78f42] flex gap-4 items-center cursor-pointer text-text poppins font-semibold px-8 py-2 rounded-lg">
|
||||||
<i class="far fa-lock"></i>
|
<i class="far fa-lock"></i>
|
||||||
Upgrade plan for CSV
|
Upgrade plan for CSV
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ definePageMeta({ layout: 'dashboard' });
|
|||||||
const { project } = useProject();
|
const { project } = useProject();
|
||||||
|
|
||||||
const isPremium = computed(() => (project.value?.premium_type || 0) > 0);
|
const isPremium = computed(() => (project.value?.premium_type || 0) > 0);
|
||||||
|
const selfhosted = useSelfhosted();
|
||||||
|
const canDownload = computed(() => {
|
||||||
|
if (selfhosted) return true;
|
||||||
|
return isPremium.value;
|
||||||
|
});
|
||||||
|
|
||||||
const metricsInfo = ref<number>(0);
|
const metricsInfo = ref<number>(0);
|
||||||
|
|
||||||
@@ -110,12 +115,12 @@ function goToUpgrade() {
|
|||||||
}" v-model="selectedTimeFrom" :options="options"></USelectMenu>
|
}" v-model="selectedTimeFrom" :options="options"></USelectMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isPremium" @click="downloadCSV()"
|
<div v-if="canDownload" @click="downloadCSV()"
|
||||||
class="bg-[#57c78fc0] hover:bg-[#57c78fab] cursor-pointer text-text poppins font-semibold px-8 py-1 rounded-lg">
|
class="bg-[#57c78fc0] hover:bg-[#57c78fab] cursor-pointer text-text poppins font-semibold px-8 py-1 rounded-lg">
|
||||||
Download CSV
|
Download CSV
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!isPremium" @click="goToUpgrade()"
|
<div v-if="!canDownload" @click="goToUpgrade()"
|
||||||
class="bg-[#57c78f46] hover:bg-[#57c78f42] flex gap-4 items-center cursor-pointer text-text poppins font-semibold px-8 py-2 rounded-lg">
|
class="bg-[#57c78f46] hover:bg-[#57c78f42] flex gap-4 items-center cursor-pointer text-text poppins font-semibold px-8 py-2 rounded-lg">
|
||||||
<i class="far fa-lock"></i>
|
<i class="far fa-lock"></i>
|
||||||
Upgrade plan for CSV
|
Upgrade plan for CSV
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const firstInteraction = useFetch<boolean>('/api/project/first_interaction', {
|
|||||||
|
|
||||||
const showDashboard = computed(() => project.value && firstInteraction.data.value);
|
const showDashboard = computed(() => project.value && firstInteraction.data.value);
|
||||||
|
|
||||||
|
const selfhosted = useSelfhosted();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -40,8 +41,8 @@ const showDashboard = computed(() => project.value && firstInteraction.data.valu
|
|||||||
<div v-if="showDashboard">
|
<div v-if="showDashboard">
|
||||||
|
|
||||||
<div class="w-full px-4 py-2 gap-2 flex flex-col">
|
<div class="w-full px-4 py-2 gap-2 flex flex-col">
|
||||||
<BannerLimitsInfo :key="refreshKey"></BannerLimitsInfo>
|
<BannerLimitsInfo v-if="!selfhosted" :key="refreshKey"></BannerLimitsInfo>
|
||||||
<BannerOffer :key="refreshKey"></BannerOffer>
|
<BannerOffer v-if="!selfhosted" :key="refreshKey"></BannerOffer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ async function exportToGoogle(data: string, user_id: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { SELFHOSTED } = useRuntimeConfig();
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
const data = await getRequestData(event, { requireSchema: false });
|
const data = await getRequestData(event, { requireSchema: false });
|
||||||
@@ -63,9 +65,12 @@ export default defineEventHandler(async event => {
|
|||||||
|
|
||||||
const { project, project_id, user } = data;
|
const { project, project_id, user } = data;
|
||||||
|
|
||||||
const PREMIUM_TYPE = project.premium_type;
|
|
||||||
|
|
||||||
|
if (SELFHOSTED !== 'TRUE') {
|
||||||
|
const PREMIUM_TYPE = project.premium_type;
|
||||||
if (PREMIUM_TYPE === 0) return setResponseStatus(event, 400, 'Project not premium');
|
if (PREMIUM_TYPE === 0) return setResponseStatus(event, 400, 'Project not premium');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const { mode, slice } = getQuery(event);
|
const { mode, slice } = getQuery(event);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { VisitModel } from "@schema/metrics/VisitSchema";
|
|||||||
import { Redis } from "~/server/services/CacheService";
|
import { Redis } from "~/server/services/CacheService";
|
||||||
import DateService from "@services/DateService";
|
import DateService from "@services/DateService";
|
||||||
|
|
||||||
import { checkSliceValidity, generateDateSlices } from "~/server/services/TimelineService";
|
import { checkSliceValidity } from "~/server/services/TimelineService";
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ export default defineEventHandler(async event => {
|
|||||||
const [sliceValid, errorOrDays] = checkSliceValidity(from, to, slice);
|
const [sliceValid, errorOrDays] = checkSliceValidity(from, to, slice);
|
||||||
if (!sliceValid) throw Error(errorOrDays);
|
if (!sliceValid) throw Error(errorOrDays);
|
||||||
|
|
||||||
const allDates = generateDateSlices(slice, new Date(from), new Date(to));
|
const allDates = DateService.generateDateSlices(slice, new Date(from), new Date(to));
|
||||||
|
|
||||||
const result: { _id: string, count: number }[] = [];
|
const result: { _id: string, count: number }[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,15 @@ export default defineEventHandler(async event => {
|
|||||||
const cacheExp = 60;
|
const cacheExp = 60;
|
||||||
|
|
||||||
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
return await Redis.useCacheV2(cacheKey, cacheExp, async () => {
|
||||||
|
|
||||||
const timelineData = await executeTimelineAggregation({
|
const timelineData = await executeTimelineAggregation({
|
||||||
projectId: project_id,
|
projectId: project_id,
|
||||||
model: EventModel,
|
model: EventModel,
|
||||||
from, to, slice,
|
from, to, slice,
|
||||||
});
|
});
|
||||||
|
|
||||||
const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from, to);
|
const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from, to);
|
||||||
|
|
||||||
return timelineFilledMerged;
|
return timelineFilledMerged;
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -89,57 +89,11 @@ export function fillAndMergeTimelineAggregation(timeline: { _id: string, count:
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function fillAndMergeTimelineAggregationV2(timeline: { _id: string, count: number }[], slice: Slice, from: string, to: string) {
|
export function fillAndMergeTimelineAggregationV2(timeline: { _id: string, count: number }[], slice: Slice, from: string, to: string) {
|
||||||
const allDates = generateDateSlices(slice, new Date(from), new Date(to));
|
const allDates = DateService.generateDateSlices(slice, new Date(from), new Date(to));
|
||||||
const merged = mergeDates(timeline, allDates, slice);
|
const merged = DateService.mergeDates(timeline, allDates, slice);
|
||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkSliceValidity(from: string | number | Date, to: string | number | Date, slice: Slice): [false, string] | [true, number] {
|
export function checkSliceValidity(from: string | number | Date, to: string | number | Date, slice: Slice): [false, string] | [true, number] {
|
||||||
return DateService.canUseSlice(from, to, slice);
|
return DateService.canUseSlice(from, to, slice);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateDateSlices(slice: Slice, fromDate: Date, toDate: Date) {
|
|
||||||
const slices: Date[] = [];
|
|
||||||
let currentDate = fromDate;
|
|
||||||
const addFunctions: { [key in Slice]: any } = { hour: fns.addHours, day: fns.addDays, week: fns.addWeeks, month: fns.addMonths, year: fns.addYears };
|
|
||||||
const addFunction = addFunctions[slice];
|
|
||||||
if (!addFunction) { throw new Error(`Invalid slice: ${slice}`); }
|
|
||||||
while (fns.isBefore(currentDate, toDate) || currentDate.getTime() === toDate.getTime()) {
|
|
||||||
slices.push(currentDate);
|
|
||||||
currentDate = addFunction(currentDate, 1);
|
|
||||||
}
|
|
||||||
return slices;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeDates(timeline: { _id: string, count: number }[], dates: Date[], slice: Slice) {
|
|
||||||
|
|
||||||
const result: { _id: string, count: number }[] = [];
|
|
||||||
|
|
||||||
const isSames: { [key in Slice]: any } = { hour: fns.isSameHour, day: fns.isSameDay, week: fns.isSameWeek, month: fns.isSameMonth, year: fns.isSameYear, }
|
|
||||||
|
|
||||||
const isSame = isSames[slice];
|
|
||||||
|
|
||||||
if (!isSame) {
|
|
||||||
throw new Error(`Invalid slice: ${slice}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const element of timeline) {
|
|
||||||
const elementDate = new Date(element._id);
|
|
||||||
for (const date of dates) {
|
|
||||||
if (isSame(elementDate, date)) {
|
|
||||||
const existingEntry = result.find(item => isSame(new Date(item._id), date));
|
|
||||||
|
|
||||||
if (existingEntry) {
|
|
||||||
existingEntry.count += element.count;
|
|
||||||
} else {
|
|
||||||
result.push({
|
|
||||||
_id: date.toISOString(),
|
|
||||||
count: element.count,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -195,11 +195,15 @@ class DateService {
|
|||||||
return slices;
|
return slices;
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeDates(timeline: { _id: string, count: number }[], dates: Date[], slice: Slice) {
|
isSameDayUTC(a: Date, b: Date) {
|
||||||
|
return a.getUTCFullYear() === b.getUTCFullYear() && a.getUTCMonth() === b.getUTCMonth() && a.getUTCDate() === b.getUTCDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeDates(timeline: { _id: string, count: number }[], allDates: Date[], slice: Slice) {
|
||||||
|
|
||||||
const result: { _id: string, count: number }[] = [];
|
const result: { _id: string, count: number }[] = [];
|
||||||
|
|
||||||
const isSames: { [key in Slice]: any } = { hour: fns.isSameHour, day: fns.isSameDay, week: fns.isSameWeek, month: fns.isSameMonth, year: fns.isSameYear, }
|
const isSames: { [key in Slice]: any } = { hour: fns.isSameHour, day: this.isSameDayUTC, week: fns.isSameWeek, month: fns.isSameMonth, year: fns.isSameYear, }
|
||||||
|
|
||||||
const isSame = isSames[slice];
|
const isSame = isSames[slice];
|
||||||
|
|
||||||
@@ -207,20 +211,14 @@ class DateService {
|
|||||||
throw new Error(`Invalid slice: ${slice}`);
|
throw new Error(`Invalid slice: ${slice}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const date of allDates) {
|
||||||
|
result.push({ _id: date.toISOString(), count: 0 });
|
||||||
for (const element of timeline) {
|
for (const element of timeline) {
|
||||||
const elementDate = new Date(element._id);
|
const elementDate = new Date(element._id);
|
||||||
for (const date of dates) {
|
|
||||||
if (isSame(elementDate, date)) {
|
if (isSame(elementDate, date)) {
|
||||||
const existingEntry = result.find(item => isSame(new Date(item._id), date));
|
const existingEntry = result.find(item => isSame(date, new Date(item._id)));
|
||||||
|
if (!existingEntry) throw new Error('THIS CANNOT HAPPEN');
|
||||||
if (existingEntry) {
|
|
||||||
existingEntry.count += element.count;
|
existingEntry.count += element.count;
|
||||||
} else {
|
|
||||||
result.push({
|
|
||||||
_id: date.toISOString(),
|
|
||||||
count: element.count,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,6 +226,7 @@ class DateService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateServiceInstance = new DateService();
|
const dateServiceInstance = new DateService();
|
||||||
|
|||||||
Reference in New Issue
Block a user