mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 07:48:37 +01:00
implementing snapshots
This commit is contained in:
@@ -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,17 +111,40 @@ 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">
|
||||
<img class="h-[2rem]" :src="'/logo.png'">
|
||||
</div> -->
|
||||
|
||||
|
||||
<!-- <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,16 +252,31 @@ 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>
|
||||
<div class="manrope">
|
||||
Logout
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-lyx-text-dark poppins text-[.8rem] px-4">
|
||||
Litlyx is in Beta version.
|
||||
</div>
|
||||
<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>
|
||||
@@ -239,10 +285,6 @@ function onLogout() {
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.CVerticalNavigation * {
|
||||
font-family: 'Geist';
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -26,12 +26,27 @@ 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;
|
||||
return metadataFieldGrouped.value.filter(e => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -167,4 +167,3 @@ export function useDevicesData(limit: number = 10) {
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
@@ -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' },
|
||||
]
|
||||
|
||||
@@ -14,8 +14,7 @@ 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.">
|
||||
|
||||
@@ -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"
|
||||
|
||||
BIN
dashboard/public/logo_32.png
Normal file
BIN
dashboard/public/logo_32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 568 B |
@@ -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;
|
||||
|
||||
});
|
||||
|
||||
@@ -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 } }
|
||||
]
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user