implementing snapshots

This commit is contained in:
Emily
2024-08-02 16:09:11 +02:00
parent 376b39e247
commit 93f22dfc54
16 changed files with 195 additions and 53 deletions

View File

@@ -1,5 +1,6 @@
<script lang="ts" setup>
import type { TProject } from '@schema/ProjectSchema';
import CreateSnapshot from './dialog/CreateSnapshot.vue';
export type Entry = {
@@ -94,7 +95,13 @@ function onLogout() {
router.push('/login');
}
const { projects } = useProjectsList();
const activeProject = useActiveProject();
const selected = ref<TProject>(activeProject.value as TProject);
watch(selected, () => {
setActiveProject(selected.value._id.toString())
})
</script>
@@ -104,8 +111,7 @@ function onLogout() {
'absolute top-0 w-full md:w-[20rem] z-[45] open': isOpen,
'hidden lg:flex': !isOpen
}">
<div class="p-4 gap-6 flex flex-col w-full">
<div class="py-4 px-2 gap-6 flex flex-col w-full">
<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">
@@ -114,7 +120,31 @@ function onLogout() {
<!-- <div class="font-bold text-[1.4rem] text-gray-300"> Litlyx </div> -->
<div class="text-center w-full"> PROJECT SELECTOR </div>
<USelectMenu class="w-full" v-if="projects" v-model="selected" :options="projects">
<template #option="{ option, active, selected }">
<div class="flex items-center gap-2">
<div>
<img class="h-5 bg-black rounded-full" :src="'logo_32.png'" alt="Litlyx logo">
</div>
<div> {{ option.name }} </div>
</div>
</template>
<template #label>
<div class="flex items-center gap-2">
<div>
<img class="h-5 bg-black rounded-full" :src="'logo_32.png'" alt="Litlyx logo">
</div>
<div> {{ activeProject?.name || '???' }} </div>
</div>
</template>
</USelectMenu>
<div class="grow flex justify-end text-[1.4rem] mr-2 lg:hidden">
<i @click="close()" class="fas fa-close"></i>
@@ -198,15 +228,16 @@ function onLogout() {
<div v-for="entry of section.entries">
<div v-if="(!entry.adminOnly || (isAdmin && !isAdminHidden))"
class="bg-lyx-background text-gray-300 py-2 px-4 rounded-lg" :class="{
class="bg-lyx-background cursor-pointer text-lyx-text-dark py-[.35rem] px-2 rounded-lg text-[.95rem] flex items-center"
:class="{
'text-gray-700 pointer-events-none': entry.disabled,
'bg-lyx-background-lighter': route.path == (entry.to || '#'),
'hover:bg-lyx-background-light': route.path != (entry.to || '#'),
'bg-lyx-background-lighter !text-lyx-text/90': route.path == (entry.to || '#'),
'hover:bg-lyx-background-light hover:!text-lyx-text/90': route.path != (entry.to || '#'),
}">
<NuxtLink @click="close() && entry.action?.()" :target="entry.external ? '_blank' : ''"
tag="div" class="flex" :to="entry.to || '/'">
<div class="flex items-center w-[1.8rem] justify-start">
tag="div" class="flex items-center" :to="entry.to || '/'">
<div class="flex items-center w-[1.4rem] mr-2 text-[1.1rem] justify-center">
<i :class="entry.icon"></i>
</div>
<div class="manrope">
@@ -221,17 +252,32 @@ function onLogout() {
</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 class="text-lyx-text-dark poppins text-[.8rem] px-4">
Litlyx is in Beta version.
</div>
<div class="manrope">
Logout
<div class="bg-lyx-widget-lighter h-[2px] px-4 w-full"></div>
<div class="flex justify-end px-2">
<div class="grow flex gap-3">
<NuxtLink to="https://github.com/litlyx/litlyx" target="_blank"
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
<i class="fab fa-github"></i>
</NuxtLink>
<NuxtLink to="https://discord.gg/9cQykjsmWX" target="_blank"
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
<i class="fab fa-discord"></i>
</NuxtLink>
</div>
<UTooltip text="Logout" :popper="{ arrow: true, placement: 'top' }">
<div @click="onLogout()" class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
<i class="far fa-arrow-right-from-bracket scale-x-[-100%]"></i>
</div>
</UTooltip>
</div>
</div>
</div>
</div>
@@ -239,10 +285,6 @@ function onLogout() {
<style lang="scss" scoped>
.CVerticalNavigation * {
font-family: 'Geist';
}
input:focus {
outline: none;
}

View File

@@ -91,6 +91,12 @@ async function loadData(timelineEndpointName: string, target: Data) {
const response = useTimeline(timelineEndpointName as any, chartSlice);
response.onRequest(() => {
target.ready = false;
target.data = [];
target.labels = [];
})
response.onResponse(data => {
if (!data.value) return;

View File

@@ -9,7 +9,7 @@ onUnmounted(() => stopWatching());
const { createAlert } = useAlert();
function copyProjectId() {
if (!navigator.clipboard) alert('NON PUOI COPIARE IN HTTP');
if (!navigator.clipboard) alert('You can\'t copy in HTTP');
navigator.clipboard.writeText((activeProject.value?._id || 0).toString());
createAlert('Success', 'Project id copied successfully.', 'far fa-circle-check', 5000);
}

View File

@@ -26,11 +26,26 @@ async function getMetadataFields() {
currentSearchText.value = "";
}
const { safeSnapshotDates } = useSnapshot();
async function getMetadataFieldGrouped() {
if (!selectedMetadataField.value) return;
metadataFieldGrouped.value = await $fetch<string[]>(`/api/metrics/${activeProject.value?._id.toString()}/events/metadata_field_group?name=${selectedEventName.value}&field=${selectedMetadataField.value}`, signHeaders());
const queryParams: Record<string, any> = {
from: safeSnapshotDates.value.from,
to: safeSnapshotDates.value.to,
name: selectedEventName.value,
field: selectedMetadataField.value
}
const queryParamsString = Object.keys(queryParams).map((key) => `${key}=${queryParams[key]}`).join('&');
metadataFieldGrouped.value = await $fetch<string[]>(`/api/metrics/${activeProject.value?._id.toString()}/events/metadata_field_group?${queryParamsString}`, signHeaders());
}
const metadataFieldGroupedFiltered = computed(() => {
if (currentSearchText.value.length == 0) return metadataFieldGrouped.value;

View File

@@ -12,13 +12,28 @@ onMounted(async () => {
const userFlowData = ref<any>();
const analyzing = ref<boolean>(false);
async function analyzeEvent() {
const { safeSnapshotDates } = useSnapshot();
async function getUserFlowData() {
userFlowData.value = undefined;
analyzing.value = true;
userFlowData.value = await $fetch(`/api/metrics/${activeProject.value?._id.toString()}/events/flow_from_name?name=${selectedEventName.value}`, signHeaders());
const queryParams: Record<string, any> = {
from: safeSnapshotDates.value.from,
to: safeSnapshotDates.value.to,
name: selectedEventName.value
}
const queryParamsString = Object.keys(queryParams).map((key) => `${key}=${queryParams[key]}`).join('&');
userFlowData.value = await $fetch(`/api/metrics/${activeProject.value?._id.toString()}/events/flow_from_name?${queryParamsString}`, signHeaders());
analyzing.value = false;
}
async function analyzeEvent() {
getUserFlowData();
}
</script>
<template>
@@ -41,13 +56,15 @@ async function analyzeEvent() {
</div>
<div class="flex flex-col gap-2" v-if="userFlowData">
<div class="flex gap-4 items-center bg-bg py-1 px-2 rounded-lg" v-for="(count, referrer) in userFlowData">
<div class="flex gap-4 items-center bg-bg py-1 px-2 rounded-lg"
v-for="(count, referrer) in userFlowData">
<div class="w-5 h-5 flex items-center justify-center">
<img :src="`https://s2.googleusercontent.com/s2/favicons?domain=${referrer}&sz=64`" :alt="'referrer'">
<img :src="`https://s2.googleusercontent.com/s2/favicons?domain=${referrer}&sz=64`"
:alt="'referrer'">
</div>
<div> {{ referrer }} </div>
<div class="grow"></div>
<div> {{ count }} </div>
<div> {{ count.toFixed(2).replace('.', ',') }} % </div>
</div>
</div>

View File

@@ -49,6 +49,11 @@ export function useActiveProject() {
export async function setActiveProject(project_id: string) {
changingProject.value = true;
await new Promise(e => setTimeout(e, 500));
await $fetch<string>(`/api/user/set_active_project?project_id=${project_id}`, signHeaders());
await activeProjectId.refresh();
changingProject.value = false;
}
export const changingProject = ref<boolean>(false);

View File

@@ -1,5 +1,5 @@
import type { InternalApi } from 'nitropack';
import type { WatchSource } from 'vue';
import type { WatchSource, WatchStopHandle } from 'vue';
type NitroFetchRequest = Exclude<keyof InternalApi, `/_${string}` | `/api/_${string}`> | (string & {});
@@ -8,10 +8,15 @@ export type CustomFetchOptions = {
watchProps?: WatchSource[],
lazy?: boolean,
method?: string,
getBody?: () => Record<string, any>
getBody?: () => Record<string, any>,
watchKey?: string
}
type OnResponseCallback<TData> = (data: Ref<TData | undefined>) => any
type OnRequestCallback = () => any
const watchStopHandles: Record<string, WatchStopHandle> = {}
export function useCustomFetch<T>(url: NitroFetchRequest, getHeaders: () => Record<string, string>, options?: CustomFetchOptions) {
@@ -20,12 +25,18 @@ export function useCustomFetch<T>(url: NitroFetchRequest, getHeaders: () => Reco
const error = ref<Error | undefined>();
let onResponseCallback: OnResponseCallback<T> = () => { }
let onRequestCallback: OnRequestCallback = () => { }
const onResponse = (callback: OnResponseCallback<T>) => {
onResponseCallback = callback;
}
const onRequest = (callback: OnRequestCallback) => {
onRequestCallback = callback;
}
const execute = async () => {
onRequestCallback();
pending.value = true;
error.value = undefined;
try {
@@ -49,12 +60,21 @@ export function useCustomFetch<T>(url: NitroFetchRequest, getHeaders: () => Reco
}
if (options?.watchProps) {
watch(options.watchProps, () => {
const watchStop = watch(options.watchProps, () => {
execute();
});
const key = options?.watchKey || `${url}`;
if (watchStopHandles[key]) watchStopHandles[key]();
watchStopHandles[key] = watchStop;
console.log('Watchers:', Object.keys(watchStopHandles).length);
}
const refresh = execute;
return { pending, execute, data, error, refresh, onResponse };
return { pending, execute, data, error, refresh, onResponse, onRequest };
}

View File

@@ -167,4 +167,3 @@ export function useDevicesData(limit: number = 10) {
);
return res;
}

View File

@@ -19,7 +19,7 @@ const sections: Section[] = [
{
title: 'Project',
entries: [
{ label: 'Dashboard', to: '/', icon: 'far fa-home' },
{ label: 'Dashboard', to: '/', icon: 'far fa-table-layout' },
{ label: 'Events', to: '/events', icon: 'far fa-bolt' },
{ label: 'Analyst', to: '/analyst', icon: 'far fa-microchip-ai' },
{ label: 'Settings', to: '/settings', icon: 'far fa-gear' },
@@ -36,10 +36,10 @@ const sections: Section[] = [
label: 'Docs', to: 'https://docs.litlyx.com', icon: 'far fa-book-open', external: true,
action() { Lit.event('docs_clicked') },
},
{
label: 'Github', to: 'https://github.com/litlyx/litlyx', icon: 'fab fa-github', external: true,
action() { Lit.event('git_clicked') },
},
// {
// label: 'Github', to: 'https://github.com/litlyx/litlyx', icon: 'fab fa-github', external: true,
// action() { Lit.event('git_clicked') },
// },
// { label: 'Billing', to: '/plans', icon: 'far fa-wallet' },
// { label: 'Book a demo', to: '/book_demo', icon: 'far fa-calendar' },
]

View File

@@ -15,7 +15,6 @@ const eventsStackedSelectIndex = ref<number>(0);
<template>
<div class="w-full h-full overflow-y-auto pb-20 p-6 gap-6 flex flex-col">
<div class="flex gap-6 flex-col xl:flex-row">
<CardTitled class="p-4 flex-[4] w-full" title="Events" sub="Events stacked bar chart.">

View File

@@ -31,14 +31,14 @@ const { createAlert } = useAlert();
function copyProjectId() {
if (!navigator.clipboard) alert('NON PUOI COPIARE IN HTTP');
if (!navigator.clipboard) alert('You can\'t copy in HTTP');
navigator.clipboard.writeText(activeProject.value?._id?.toString() || '');
createAlert('Success', 'Project id copied successfully.', 'far fa-circle-check', 5000);
}
function copyScript() {
if (!navigator.clipboard) alert('NON PUOI COPIARE IN HTTP');
if (!navigator.clipboard) alert('You can\'t copy in HTTP');
const createScriptText = () => {
@@ -56,7 +56,6 @@ function copyScript() {
const { data: firstInteraction, pending, refresh } = useFirstInteractionData();
watch(pending, () => {
if (pending.value === true) return;
if (firstInteraction.value === false) {
@@ -77,7 +76,9 @@ const selectLabels = [
<div class="dashboard w-full h-full overflow-y-auto pb-20 md:pt-4 lg:pt-0">
<div :key="'home-' + isLiveDemo()" v-if="projects && activeProject && firstInteraction">
{{ changingProject }}
<div :key="'home-' + isLiveDemo()" v-if="projects && activeProject && firstInteraction && !changingProject">
<div class="w-full px-4 py-2">
<div v-if="limitsInfo && limitsInfo.limited"

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 B

View File

@@ -15,12 +15,24 @@ export default defineEventHandler(async event => {
const project = await getUserProjectFromId(project_id, user);
if (!project) return;
const { name: eventName } = getQuery(event);
if (!eventName) return [];
const { name: eventName, from, to } = getQuery(event);
if (!from) return setResponseStatus(event, 400, 'from is required');
if (!to) return setResponseStatus(event, 400, 'to is required');
if (!eventName) return setResponseStatus(event, 400, 'name is required');
const allEvents = await EventModel.find({ project_id: project_id, name: eventName }, { flowHash: 1 });
const allEvents = await EventModel.find({
project_id: project_id,
name: eventName,
created_at: {
$gte: new Date(from.toString()),
$lte: new Date(to.toString()),
}
}, { flowHash: 1 });
const allFlowHashes = new Map<string, number>();
allEvents.forEach(e => {
@@ -71,6 +83,17 @@ export default defineEventHandler(async event => {
grouped[referrer]++;
}
const eventsCount = allEvents.length;
const allGroupedValue = Object.keys(grouped)
.map(key => grouped[key])
.reduce((a, e) => a + e, 0);
for (const key in grouped) {
grouped[key] = 100 / allGroupedValue * grouped[key];
}
return grouped;
});

View File

@@ -15,11 +15,24 @@ export default defineEventHandler(async event => {
const project = await getUserProjectFromId(project_id, user);
if (!project) return;
const { name: eventName, field } = getQuery(event);
if (!eventName || !field) return [];
const { name: eventName, field, from, to } = getQuery(event);
if (!from) return setResponseStatus(event, 400, 'from is required');
if (!to) return setResponseStatus(event, 400, 'to is required');
if (!eventName) return setResponseStatus(event, 400, 'name is required');
if (!field) return setResponseStatus(event, 400, 'field is required');
const aggregation: PipelineStage[] = [
{ $match: { project_id: project._id, name: eventName } },
{
$match: {
project_id: project._id, name: eventName,
created_at: {
$gte: new Date(from.toString()),
$lte: new Date(to.toString()),
}
}
},
{ $group: { _id: `$metadata.${field}`, count: { $sum: 1 } } },
{ $sort: { count: -1 } }
]

View File

@@ -84,7 +84,6 @@ export function fixMetrics(result: { data: MetricsTimeline[], from: string, to:
return returnObject;
});
console.log({ allKeys })
if (slice === 'day' || slice == 'hour') fixed.pop();

View File

@@ -1,5 +1,8 @@
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
@import url("https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0");
.poppins {
font-family: 'Poppins' !important;
}