rewrite litlyx

This commit is contained in:
Emily
2024-10-08 15:12:04 +02:00
parent b27cacf4e6
commit 79e956e930
33 changed files with 231 additions and 214 deletions

13
TODO
View File

@@ -1,7 +1,14 @@
- Slice change on Actionable Chart
- Show more on Dashboard cards
- Fix devices Dashboard card, replace empty with "unkwnwn" - Fix devices Dashboard card, replace empty with "unkwnwn"
- Component first interaction must make the request inside - Component first interaction must make the request inside
- Reactivity on project delete (update dropdown) - Reactivity on project delete (update dropdown)
- Fix CSV Endpoint
- Devices remove empty
- docs vertical navigation not wfull
- project creation
- Event funnel / metadata analyzer / user flow
- Test guest actions
- Refactor UI Data analyst
- Remove Top Events from web analytics and move to custom events (with raw data access)
- Fix email on plan upgrade and resub

View File

@@ -1,19 +1,24 @@
<script lang="ts" setup> <script lang="ts" setup>
const isShowMore = ref<boolean>(false);
const browsersData = useFetch('/api/data/browsers', { const browsersData = useFetch('/api/data/browsers', {
headers: useComputedHeaders({ headers: useComputedHeaders({ limit: 10, }), lazy: true
limit: computed(() => isShowMore.value ? '200' : '10'),
}), lazy: true
}); });
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog(); const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
function showMore() { async function showMore() {
isShowMore.value = true; dialogBarData.value=[];
showDialog.value = true; showDialog.value = true;
dialogBarData.value = browsersData.data.value || []; isDataLoading.value = true;
const res = await $fetch('/api/data/browsers', {
headers: useComputedHeaders({ limit: 1000 }).value
});
dialogBarData.value = res || [];
isDataLoading.value = false;
} }
@@ -22,9 +27,9 @@ function showMore() {
<template> <template>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<BarCardBase @showMore="showMore()" @dataReload="browsersData.refresh()" <BarCardBase @showMore="showMore()" @dataReload="browsersData.refresh()" :data="browsersData.data.value || []"
:data="browsersData.data.value || []" desc="The browsers most used to search your website." desc="The browsers most used to search your website." :dataIcons="false"
:dataIcons="false" :loading="browsersData.pending.value" label="Top Browsers" sub-label="Browsers"> :loading="browsersData.pending.value" label="Top Browsers" sub-label="Browsers">
</BarCardBase> </BarCardBase>
</div> </div>
</template> </template>

View File

@@ -1,21 +1,25 @@
<script lang="ts" setup> <script lang="ts" setup>
const isShowMore = ref<boolean>(false);
const devicesData = useFetch('/api/data/devices', { const devicesData = useFetch('/api/data/devices', {
headers: useComputedHeaders({ headers: useComputedHeaders({ limit: 10, }), lazy: true
limit: computed(() => isShowMore.value ? '200' : '10'),
}), lazy: true
}); });
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog(); const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
async function showMore() {
function showMore() { dialogBarData.value=[];
isShowMore.value = true;
showDialog.value = true; showDialog.value = true;
dialogBarData.value = devicesData.data.value || []; isDataLoading.value = true;
const res = await $fetch('/api/data/devices', {
headers: useComputedHeaders({ limit: 1000 }).value
});
dialogBarData.value = res || [];
isDataLoading.value = false;
} }

View File

@@ -6,20 +6,27 @@ function goToView() {
router.push('/dashboard/events'); router.push('/dashboard/events');
} }
const isShowMore = ref<boolean>(false);
const eventsData = useFetch('/api/data/events', { const eventsData = useFetch('/api/data/events', {
headers: useComputedHeaders({ headers: useComputedHeaders({
limit: computed(() => isShowMore.value ? '200' : '10'), limit: 10,
}), lazy: true }), lazy: true
}); });
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog(); const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
function showMore() { async function showMore() {
isShowMore.value = true; dialogBarData.value=[];
showDialog.value = true; showDialog.value = true;
dialogBarData.value = eventsData.data.value || []; isDataLoading.value = true;
const res = await $fetch('/api/data/events', {
headers: useComputedHeaders({ limit: 1000 }).value
});
dialogBarData.value = res || [];
isDataLoading.value = false;
} }
</script> </script>

View File

@@ -12,25 +12,26 @@ function iconProvider(id: string): ReturnType<IconProvider> {
const customIconStyle = `width: 2rem; padding: 1px;` const customIconStyle = `width: 2rem; padding: 1px;`
const isShowMore = ref<boolean>(false);
const geolocationData = useFetch('/api/data/countries', { const geolocationData = useFetch('/api/data/countries', {
headers: useComputedHeaders({ headers: useComputedHeaders({ limit: 10, }), lazy: true
limit: computed(() => isShowMore.value ? '200' : '10'),
}), lazy: true
}); });
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog(); const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
function showMore() { async function showMore() {
dialogBarData.value=[];
isShowMore.value = true;
showDialog.value = true; showDialog.value = true;
isDataLoading.value = true;
dialogBarData.value = geolocationData.data.value?.map(e => { const res = await $fetch('/api/data/countries', {
headers: useComputedHeaders({limit: 1000}).value
});
dialogBarData.value = res?.map(e => {
return { ...e, icon: iconProvider(e._id) } return { ...e, icon: iconProvider(e._id) }
}) || []; }) || [];
isDataLoading.value = false; isDataLoading.value = false;
} }
@@ -40,9 +41,10 @@ function showMore() {
<template> <template>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<BarCardBase @showMore="showMore()" @dataReload="geolocationData.refresh()" :data="geolocationData.data.value || []" :dataIcons="false" <BarCardBase @showMore="showMore()" @dataReload="geolocationData.refresh()"
:loading="geolocationData.pending.value" label="Top Countries" sub-label="Countries" :iconProvider="iconProvider" :data="geolocationData.data.value || []" :dataIcons="false" :loading="geolocationData.pending.value"
:customIconStyle="customIconStyle" desc=" Lists the countries where users access your website."> label="Top Countries" sub-label="Countries" :iconProvider="iconProvider" :customIconStyle="customIconStyle"
desc=" Lists the countries where users access your website.">
</BarCardBase> </BarCardBase>
</div> </div>
</template> </template>

View File

@@ -1,23 +1,29 @@
<script lang="ts" setup> <script lang="ts" setup>
const isShowMore = ref<boolean>(false);
const ossData = useFetch('/api/data/oss', { const ossData = useFetch('/api/data/oss', {
headers: useComputedHeaders({ headers: useComputedHeaders({
limit: computed(() => isShowMore.value ? '200' : '10'), limit: 10,
}), lazy: true }), lazy: true
}); });
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog(); const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
function showMore() { async function showMore() {
isShowMore.value = true; dialogBarData.value=[];
showDialog.value = true; showDialog.value = true;
dialogBarData.value = ossData.data.value || []; isDataLoading.value = true;
const res = await $fetch('/api/data/oss', {
headers: useComputedHeaders({ limit: 1000 }).value
});
dialogBarData.value = res || [];
isDataLoading.value = false;
} }
</script> </script>

View File

@@ -12,22 +12,31 @@ function elementTextTransformer(element: string) {
return element; return element;
} }
const isShowMore = ref<boolean>(false);
const referrersData = useFetch('/api/data/referrers', { const referrersData = useFetch('/api/data/referrers', {
headers: useComputedHeaders({ headers: useComputedHeaders({
limit: computed(() => isShowMore.value ? '200' : '10'), limit: 10,
}), lazy: true }), lazy: true
}); });
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog(); const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
function showMore() { async function showMore() {
isShowMore.value = true;
dialogBarData.value=[];
showDialog.value = true; showDialog.value = true;
dialogBarData.value = referrersData.data.value?.map(e => { isDataLoading.value = true;
const res = await $fetch('/api/data/referrers', {
headers: useComputedHeaders({limit: 1000}).value
});
dialogBarData.value = res?.map(e => {
return { ...e, icon: iconProvider(e._id) } return { ...e, icon: iconProvider(e._id) }
}) || []; }) || [];
isDataLoading.value = false;
} }
</script> </script>

View File

@@ -1,18 +1,16 @@
<script lang="ts" setup> <script lang="ts" setup>
const isShowMore = ref<boolean>(false);
const currentWebsite = ref<string>(""); const currentWebsite = ref<string>("");
const websitesData = useFetch('/api/data/websites', { const websitesData = useFetch('/api/data/websites', {
headers: useComputedHeaders({ headers: useComputedHeaders({
limit: computed(() => isShowMore.value ? '200' : '10'), limit: 10,
}), lazy: true }), lazy: true
}); });
const pagesData = useFetch('/api/data/websites_pages', { const pagesData = useFetch('/api/data/websites_pages', {
headers: useComputedHeaders({ headers: useComputedHeaders({
limit: computed(() => isShowMore.value ? '200' : '10'), limit: 10,
custom: { custom: {
'x-website-name': currentWebsite 'x-website-name': currentWebsite
} }

View File

@@ -114,7 +114,7 @@ const pricingDrawer = usePricingDrawer();
</script> </script>
<template> <template>
<div class="CVerticalNavigation h-full w-[20rem] bg-lyx-background flex shadow-[1px_0_10px_#000000] rounded-r-lg" <div class="CVerticalNavigation border-solid border-[#202020] border-r-[1px] h-full w-[20rem] bg-lyx-background flex shadow-[1px_0_10px_#000000] rounded-r-lg"
:class="{ :class="{
'absolute top-0 w-full md:w-[20rem] z-[45] open': isOpen, 'absolute top-0 w-full md:w-[20rem] z-[45] open': isOpen,
'hidden lg:flex': !isOpen 'hidden lg:flex': !isOpen
@@ -141,11 +141,13 @@ const pricingDrawer = usePricingDrawer();
</div> </div>
<NuxtLink to="/project_creation" v-if="projectList && (projectList.length < (maxProjects || 1))" <LyxUiButton to="/project_creation" v-if="projectList && (projectList.length < (maxProjects || 1))"
class="flex items-center text-[.8rem] gap-1 justify-end pt-2 pr-2 text-lyx-text-dark hover:text-lyx-text cursor-pointer"> type="outlined" class="w-full py-1 mt-2 text-[.8rem]">
<div><i class="fas fa-plus"></i></div> <div class="flex items-center gap-2 justify-center">
<div> Create new project </div> <div><i class="fas fa-plus"></i></div>
</NuxtLink> <div> Create new project </div>
</div>
</LyxUiButton>
</div> </div>
@@ -156,52 +158,59 @@ const pricingDrawer = usePricingDrawer();
<div class="poppins text-[.8rem]"> <div class="poppins text-[.8rem]">
Snapshots Snapshots
</div> </div>
<div @click="openSnapshotDialog()"
class="poppins text-[.8rem] px-2 rounded-lg outline outline-[2px] outline-lyx-widget-lighter cursor-pointer hover:bg-lyx-widget-lighter"> <div class="flex gap-2">
<i class="far fa-plus"></i> <UTooltip text="Download report">
Add <LyxUiButton @click="generatePDF()" type="outlined" class="!px-3 !py-1">
<div><i class="far fa-download text-[.8rem]"></i></div>
</LyxUiButton>
</UTooltip>
<UTooltip text="Create new snapshot">
<LyxUiButton @click="openSnapshotDialog()" type="outlined" class="!px-3 !py-1">
<div><i class="fas fa-plus text-[.9rem]"></i></div>
</LyxUiButton>
</UTooltip>
</div> </div>
</div> </div>
<USelectMenu :uiMenu="{ <div class="flex items-center gap-2">
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter', <USelectMenu :uiMenu="{
base: '!bg-lyx-widget', select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter',
option: { base: '!bg-lyx-widget',
base: 'hover:!bg-lyx-widget-lighter cursor-pointer', option: {
active: '!bg-lyx-widget-lighter' base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
} active: '!bg-lyx-widget-lighter'
}" class="w-full" v-model="snapshot" :options="snapshotsItems"> }
<template #label> }" class="w-full" v-model="snapshot" :options="snapshotsItems">
<div class="flex items-center gap-2"> <template #label>
<div :style="'background-color:' + snapshot?.color" class="w-2 h-2 rounded-full"> <div class="flex items-center gap-2">
<div :style="'background-color:' + snapshot?.color" class="w-2 h-2 rounded-full">
</div>
<div class="poppins"> {{ snapshot?.name }} </div>
</div> </div>
<div class="poppins"> {{ snapshot?.name }} </div> </template>
</div> <template #option="{ option }">
</template> <div class="flex items-center gap-2">
<template #option="{ option }"> <div :style="'background-color:' + option.color" class="w-2 h-2 rounded-full">
<div class="flex items-center gap-2"> </div>
<div :style="'background-color:' + option.color" class="w-2 h-2 rounded-full"> <div class="poppins"> {{ option.name }} </div>
</div> </div>
<div class="poppins"> {{ option.name }} </div> </template>
</div> </USelectMenu>
</template> </div>
</USelectMenu>
<div v-if="snapshot" class="flex flex-col text-[.8rem] mt-2"> <div v-if="snapshot" class="flex flex-col text-[.7rem] mt-2">
<div class="flex"> <div class="flex gap-1 items-center justify-center text-lyx-text-dark">
<div class="grow poppins"> From:</div> <div class="poppins">
<div class="poppins"> {{ new Date(snapshot.from).toLocaleString('it-IT').split(',')[0].trim() }} {{ new Date(snapshot.from).toLocaleString('it-IT').split(',')[0].trim().replace(/\//g, '-')
}}
</div>
<div class="poppins"> to </div>
<div class="poppins">
{{ new Date(snapshot.to).toLocaleString('it-IT').split(',')[0].trim().replace(/\//g, '-') }}
</div> </div>
</div> </div>
<div class="flex">
<div class="grow poppins"> To:</div>
<div class="poppins"> {{ new Date(snapshot.to).toLocaleString('it-IT').split(',')[0].trim() }}
</div>
</div>
<LyxUiButton @click="generatePDF()" type="secondary" class="w-full text-center mt-4">
Download report
</LyxUiButton>
<div class="mt-2" v-if="snapshot._id.toString().startsWith('default') === false"> <div class="mt-2" v-if="snapshot._id.toString().startsWith('default') === false">
<UPopover placement="bottom"> <UPopover placement="bottom">
@@ -226,7 +235,7 @@ const pricingDrawer = usePricingDrawer();
</div> </div>
</div> </div>
<div class="bg-lyx-widget-lighter h-[2px] w-full"></div> <div class="bg-[#202020] h-[1px] w-full"></div>
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
@@ -262,26 +271,28 @@ const pricingDrawer = usePricingDrawer();
</div> </div>
<div class="grow"></div> <div class="grow"></div>
<div class="bg-lyx-widget-lighter h-[2px] px-4 w-full mb-3"></div>
<div class="bg-[#202020] h-[1px] w-full px-4 mb-3"></div>
<div class="flex justify-end px-2"> <div class="flex justify-end px-2">
<div class="grow flex gap-3"> <div class="grow flex gap-3">
<NuxtLink to="https://github.com/litlyx/litlyx" target="_blank" <!-- <NuxtLink to="https://github.com/litlyx/litlyx" target="_blank"
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark"> class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
<i class="fab fa-github"></i> <i class="fab fa-github"></i>
</NuxtLink> </NuxtLink> -->
<NuxtLink to="https://discord.gg/9cQykjsmWX" target="_blank" <!-- <NuxtLink to="https://discord.gg/9cQykjsmWX" target="_blank"
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark"> class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
<i class="fab fa-discord"></i> <i class="fab fa-discord"></i>
</NuxtLink> </NuxtLink> -->
<NuxtLink to="https://x.com/litlyx" target="_blank" <NuxtLink to="https://x.com/litlyx" target="_blank"
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark"> class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
<i class="fab fa-x-twitter"></i> <i class="fab fa-x-twitter"></i>
</NuxtLink> </NuxtLink>
<NuxtLink to="https://dev.to/litlyx-org" target="_blank" <!-- <NuxtLink to="https://dev.to/litlyx-org" target="_blank"
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark"> class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
<i class="fab fa-dev"></i> <i class="fab fa-dev"></i>
</NuxtLink> </NuxtLink> -->
<NuxtLink to="/admin" v-if="userRoles.isAdmin" <NuxtLink to="/admin" v-if="userRoles.isAdmin"
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark"> class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
<i class="fas fa-cat"></i> <i class="fas fa-cat"></i>

View File

@@ -136,6 +136,8 @@ const selectLabels: { label: string, value: Slice }[] = [
{ label: 'Month', value: 'month' }, { label: 'Month', value: 'month' },
]; ];
const selectedSlice = computed(()=>selectLabels[selectedLabelIndex.value].value);
const selectedLabelIndex = ref<number>(1); const selectedLabelIndex = ref<number>(1);
const allDatesFull = ref<string[]>([]); const allDatesFull = ref<string[]>([]);
@@ -157,17 +159,17 @@ function onResponse(e: any) {
const visitsData = useFetch('/api/timeline/visits', { const visitsData = useFetch('/api/timeline/visits', {
headers: useComputedHeaders({ slice: selectLabels[selectedLabelIndex.value].value }), lazy: true, headers: useComputedHeaders({ slice: selectedSlice }), lazy: true,
transform: transformResponse, onResponseError, onResponse transform: transformResponse, onResponseError, onResponse
}); });
const sessionsData = useFetch('/api/timeline/sessions', { const sessionsData = useFetch('/api/timeline/sessions', {
headers: useComputedHeaders({ slice: selectLabels[selectedLabelIndex.value].value }), lazy: true, headers: useComputedHeaders({ slice: selectedSlice }), lazy: true,
transform: transformResponse, onResponseError, onResponse transform: transformResponse, onResponseError, onResponse
}); });
const eventsData = useFetch('/api/timeline/events', { const eventsData = useFetch('/api/timeline/events', {
headers: useComputedHeaders({ slice: selectLabels[selectedLabelIndex.value].value }), lazy: true, headers: useComputedHeaders({ slice: selectedSlice }), lazy: true,
transform: transformResponse, onResponseError, onResponse transform: transformResponse, onResponseError, onResponse
}); });

View File

@@ -98,19 +98,8 @@ function transformResponse(input: CustomEventsAggregated[]) {
} }
} }
const headers = computed(() => { const eventsData = useFetch(`/api/data/events`, {
return { method: 'POST', headers: useComputedHeaders({ limit: 6 }), lazy: true, immediate: false, transform: transformResponse
'x-from': safeSnapshotDates.value.from,
'x-to': safeSnapshotDates.value.to,
'Authorization': authorizationHeaderComputed.value,
'x-schema': 'events',
'x-limit': "6",
'x-pid': projectId.value || ''
}
});
const eventsData = useFetch(`/api/data/query`, {
method: 'POST', headers, lazy: true, immediate: false, transform: transformResponse
}); });
onMounted(() => { onMounted(() => {

View File

@@ -34,37 +34,37 @@ function showAnomalyInfoAlert() {
<template> <template>
<div <div
class="w-full px-6 py-2 lg:py-6 font-bold text-text-sub/40 flex flex-col xl:flex-row text-lg gap-2 xl:gap-12 lg:text-2xl"> class="w-full px-6 pb-2 lg:pb-6 font-bold text-text-sub/40 flex flex-col xl:flex-row text-lg gap-2 xl:gap-12 lg:text-2xl">
<div class="flex gap-2 items-center text-text/90 justify-center md:justify-start"> <div 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-medium text-[1.2rem]"> {{ onlineUsers.data }} Online users</div> <div class="poppins font-medium text-[.9rem]"> {{ onlineUsers.data }} Online users</div>
</div> </div>
<div class="grow"></div> <div class="grow"></div>
<div class="flex md:gap-2 items-center md:justify-start flex-col md:flex-row"> <!-- <div class="flex md:gap-2 items-center md:justify-start flex-col md:flex-row">
<div class="poppins font-medium text-lyx-text-darker text-[1.2rem]">Project:</div> <div class="poppins font-medium text-lyx-text-darker text-[.9rem]">Project:</div>
<div class="text-lyx-text poppins font-medium text-[1.2rem]"> {{ project?.name || 'Loading...' }} <div class="text-lyx-text poppins font-medium text-[.9rem]"> {{ project?.name || 'Loading...' }}
</div> </div>
</div> </div>
<div class="flex flex-col md:flex-row md:gap-2 items-center md:justify-start"> <div class="flex flex-col md:flex-row md:gap-2 items-center md:justify-start">
<div class="poppins font-medium text-lyx-text-darker text-[1.2rem]">Project id:</div> <div class="poppins font-medium text-lyx-text-darker text-[.9rem]">Project id:</div>
<div class="flex gap-2"> <div class="flex gap-2">
<div class="text-lyx-text poppins font-medium text-[1.2rem]"> <div class="text-lyx-text poppins font-medium text-[.9rem]">
{{ project?._id || 'Loading...' }} {{ project?._id || 'Loading...' }}
</div> </div>
<div class="flex items-center ml-3"> <div class="flex items-center ml-3">
<i @click="copyProjectId()" <i @click="copyProjectId()"
class="far fa-copy text-lyx-text hover:text-lyx-primary cursor-pointer text-[1.2rem]"></i> class="far fa-copy text-lyx-text hover:text-lyx-primary cursor-pointer text-[.9rem]"></i>
</div> </div>
</div> </div>
</div> </div> -->
<div class="flex gap-2 items-center text-text/90 justify-center md:justify-start"> <div 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-[1rem]"> 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">
<i class="far fa-info-circle text-[.9rem] hover:text-lyx-primary cursor-pointer" <i class="far fa-info-circle text-[.9rem] hover:text-lyx-primary cursor-pointer"
@click="showAnomalyInfoAlert"></i> @click="showAnomalyInfoAlert"></i>

View File

@@ -8,15 +8,6 @@ const slice = computed(() => props.slice);
const { safeSnapshotDates } = useSnapshot() const { safeSnapshotDates } = useSnapshot()
const body = computed(() => {
return {
from: safeSnapshotDates.value.from,
to: safeSnapshotDates.value.to,
slice: slice.value,
}
});
function transformResponse(input: { _id: string, name: string, count: number }[]) { function transformResponse(input: { _id: string, name: string, count: number }[]) {
const fixed = fixMetrics({ const fixed = fixMetrics({
@@ -90,9 +81,9 @@ function onResponse(e: any) {
} }
const eventsStackedData = useFetch(`/api/timeline/events_stacked`, { const eventsStackedData = useFetch(`/api/timeline/events_stacked`, {
method: 'POST', body, lazy: true, immediate: false, lazy: true, immediate: false,
transform: transformResponse, transform: transformResponse,
headers: useComputedHeaders(), headers: useComputedHeaders({slice}),
onResponseError, onResponseError,
onResponse onResponse
}); });

View File

@@ -2,7 +2,7 @@
import type { TApiSettings } from '@schema/ApiSettingsSchema'; import type { TApiSettings } from '@schema/ApiSettingsSchema';
import type { SettingsTemplateEntry } from './Template.vue'; import type { SettingsTemplateEntry } from './Template.vue';
const { project, actions, projectList } = useProject(); const { project, actions, projectList, isGuest, projectId } = useProject();
const entries: SettingsTemplateEntry[] = [ const entries: SettingsTemplateEntry[] = [
{ id: 'pname', title: 'Name', text: 'Project name' }, { id: 'pname', title: 'Name', text: 'Project name' },
@@ -107,7 +107,7 @@ async function deleteProject() {
const firstProjectId = projectList.value?.[0]?._id.toString(); const firstProjectId = projectList.value?.[0]?._id.toString();
if (firstProjectId) { if (firstProjectId) {
await setActiveProject(firstProjectId); await actions.setActiveProject(firstProjectId);
} }
@@ -120,8 +120,6 @@ async function deleteProject() {
const { createAlert } = useAlert() const { createAlert } = useAlert()
const activeProjectId = useActiveProjectId()
function copyScript() { function copyScript() {
if (!navigator.clipboard) alert('You can\'t copy in HTTP'); if (!navigator.clipboard) alert('You can\'t copy in HTTP');
@@ -129,7 +127,7 @@ function copyScript() {
const createScriptText = () => { const createScriptText = () => {
return [ return [
'<script defer ', '<script defer ',
`data-project="${activeProjectId.data.value}" `, `data-project="${projectId.value}" `,
'src="https://cdn.jsdelivr.net/gh/litlyx/litlyx-js/browser/litlyx.js"></', 'src="https://cdn.jsdelivr.net/gh/litlyx/litlyx-js/browser/litlyx.js"></',
'script>' 'script>'
].join('') ].join('')
@@ -142,7 +140,7 @@ function copyScript() {
function copyProjectId() { function copyProjectId() {
if (!navigator.clipboard) alert('You can\'t copy in HTTP'); if (!navigator.clipboard) alert('You can\'t copy in HTTP');
navigator.clipboard.writeText(activeProjectId.data.value || ''); navigator.clipboard.writeText(projectId.value || '');
createAlert('Success', 'Project id copied successfully.', 'far fa-circle-check', 5000); createAlert('Success', 'Project id copied successfully.', 'far fa-circle-check', 5000);
} }

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { SettingsTemplateEntry } from './Template.vue'; import type { SettingsTemplateEntry } from './Template.vue';
const { projectId } = useProject(); const { projectId, isGuest } = useProject();
definePageMeta({ layout: 'dashboard' }); definePageMeta({ layout: 'dashboard' });

View File

@@ -63,7 +63,7 @@ const project = computed(() => {
const isGuest = computed(() => { const isGuest = computed(() => {
return (projectList.value || []).find(e => e._id.toString() === activeProjectId.value); return (projectList.value || []).find(e => e._id.toString() === activeProjectId.value) == undefined;
}) })
export function useProject() { export function useProject() {

View File

@@ -1,7 +1,6 @@
import type { TProjectSnapshot } from "@schema/ProjectSnapshot"; import type { TProjectSnapshot } from "@schema/ProjectSnapshot";
const { projectId, project } = useProject(); const { projectId, project } = useProject();
const headers = computed(() => { const headers = computed(() => {

View File

@@ -13,8 +13,8 @@ const sections: Section[] = [
{ {
title: '', title: '',
entries: [ entries: [
{ label: 'Dashboard', to: '/', icon: 'fal fa-table-layout' }, { label: 'Web Analytics', to: '/', icon: 'fal fa-table-layout' },
{ label: 'Events', to: '/events', icon: 'fal fa-square-bolt' }, { label: 'Custom Events', to: '/events', icon: 'fal fa-square-bolt' },
{ label: 'AI Analyst', to: '/analyst', icon: 'fal fa-sparkles' }, { label: 'AI Analyst', to: '/analyst', icon: 'fal fa-sparkles' },
{ label: 'Security', to: '/security', icon: 'fal fa-shield' }, { label: 'Security', to: '/security', icon: 'fal fa-shield' },
// { label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true }, // { label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true },
@@ -23,22 +23,27 @@ const sections: Section[] = [
{ label: 'Settings', to: '/settings', icon: 'fal fa-gear' }, { label: 'Settings', to: '/settings', icon: 'fal fa-gear' },
{ {
grow: true, grow: true,
label: 'Documentation', to: 'https://docs.litlyx.com', icon: 'fal fa-book', external: true, label: 'Docs', to: 'https://docs.litlyx.com', icon: 'fal fa-book', external: true,
action() { Lit.event('docs_clicked') }, action() { Lit.event('docs_clicked') },
}, },
{ {
label: 'Slack support', icon: 'fab fa-slack', label: 'Discord support', icon: 'fab fa-discord',
to: '#', to: 'https://discord.gg/9cQykjsmWX',
premiumOnly: true, external: true,
action() {
if (isLogged.value === true) return;
if (userRoles.isPremium.value === true) {
window.open('https://join.slack.com/t/litlyx/shared_invite/zt-2q3oawn29-hZlu_fBUBlc4052Ooe3FZg', '_blank');
} else {
pricingDrawer.visible.value = true;
}
},
}, },
// {
// label: 'Slack support', icon: 'fab fa-slack',
// to: '#',
// premiumOnly: true,
// action() {
// if (isLogged.value === true) return;
// if (userRoles.isPremium.value === true) {
// window.open('https://join.slack.com/t/litlyx/shared_invite/zt-2q3oawn29-hZlu_fBUBlc4052Ooe3FZg', '_blank');
// } else {
// pricingDrawer.visible.value = true;
// }
// },
// },
] ]
} }
]; ];
@@ -76,7 +81,7 @@ const { isOpen, close, open } = useMenu();
</CVerticalNavigation> </CVerticalNavigation>
<div class="overflow-hidden w-full bg-lyx-background-light relative h-full"> <div class="overflow-hidden w-full bg-lyx-background relative h-full">
<div v-if="showDialog" class="barrier w-full h-full z-[34] absolute bg-black/50 backdrop-blur-[2px]"> <div v-if="showDialog" class="barrier w-full h-full z-[34] absolute bg-black/50 backdrop-blur-[2px]">
<i <i

View File

@@ -4,9 +4,9 @@ import type { MetricsCounts } from '~/server/api/metrics/[project_id]/counts';
definePageMeta({ layout: 'dashboard' }); definePageMeta({ layout: 'dashboard' });
const activeProject = useActiveProject(); const {project} = useProject();
const isPremium = computed(() => (activeProject.value?.premium_type || 0) > 0); const isPremium = computed(() => (project.value?.premium_type || 0) > 0);
const metricsInfo = ref<number>(0); const metricsInfo = ref<number>(0);
@@ -29,12 +29,12 @@ const totalItems = computed(() => metricsInfo.value);
const { data: tableData, pending: loadingData } = await useLazyFetch<any[]>(() => const { data: tableData, pending: loadingData } = await useLazyFetch<any[]>(() =>
`/api/metrics/${activeProject.value?._id}/query?type=1&orderBy=${sort.value.column}&order=${sort.value.direction}&page=${page.value}&limit=${itemsPerPage}`, { `/api/metrics/${project.value?._id}/query?type=1&orderBy=${sort.value.column}&order=${sort.value.direction}&page=${page.value}&limit=${itemsPerPage}`, {
...signHeaders(), ...signHeaders(),
}) })
onMounted(async () => { onMounted(async () => {
const counts = await $fetch<MetricsCounts>(`/api/metrics/${activeProject.value?._id}/counts`, signHeaders()); const counts = await $fetch<MetricsCounts>(`/api/metrics/${project.value?._id}/counts`, signHeaders());
metricsInfo.value = counts.eventsCount; metricsInfo.value = counts.eventsCount;
}); });

View File

@@ -4,9 +4,9 @@ import type { MetricsCounts } from '~/server/api/metrics/[project_id]/counts';
definePageMeta({ layout: 'dashboard' }); definePageMeta({ layout: 'dashboard' });
const activeProject = useActiveProject(); const {project} = useProject();
const isPremium = computed(() => (activeProject.value?.premium_type || 0) > 0); const isPremium = computed(() => (project.value?.premium_type || 0) > 0);
const metricsInfo = ref<number>(0); const metricsInfo = ref<number>(0);
@@ -35,12 +35,12 @@ const totalItems = computed(() => metricsInfo.value);
const { data: tableData, pending: loadingData } = await useLazyFetch<any[]>(() => const { data: tableData, pending: loadingData } = await useLazyFetch<any[]>(() =>
`/api/metrics/${activeProject.value?._id}/query?type=0&orderBy=${sort.value.column}&order=${sort.value.direction}&page=${page.value}&limit=${itemsPerPage}`, { `/api/metrics/${project.value?._id}/query?type=0&orderBy=${sort.value.column}&order=${sort.value.direction}&page=${page.value}&limit=${itemsPerPage}`, {
...signHeaders(), ...signHeaders(),
}) })
onMounted(async () => { onMounted(async () => {
const counts = await $fetch<MetricsCounts>(`/api/metrics/${activeProject.value?._id}/counts`, signHeaders()); const counts = await $fetch<MetricsCounts>(`/api/metrics/${project.value?._id}/counts`, signHeaders());
metricsInfo.value = counts.visitsCount; metricsInfo.value = counts.visitsCount;
}); });

View File

@@ -10,21 +10,7 @@ const selectLabelsEvents = [
]; ];
const eventsStackedSelectIndex = ref<number>(0); const eventsStackedSelectIndex = ref<number>(0);
const { projectId } = useProject(); const eventsData = await useFetch(`/api/data/count`, { headers: useComputedHeaders({ custom: { 'x-schema': 'events' } }), lazy: true });
const { snapshot, safeSnapshotDates } = useSnapshot();
const headers = computed(() => {
return {
'x-from': safeSnapshotDates.value.from,
'x-to': safeSnapshotDates.value.to,
'Authorization': authorizationHeaderComputed.value,
'x-schema': 'events',
'x-pid': projectId.value ?? ''
}
});
const eventsData = await useFetch(`/api/data/count`, { method: 'POST', headers, lazy: true });
</script> </script>
@@ -36,9 +22,9 @@ const eventsData = await useFetch(`/api/data/count`, { method: 'POST', headers,
<LyxUiCard class="w-full flex justify-between items-center"> <LyxUiCard class="w-full flex justify-between items-center">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div> <div>
Total events: {{ eventsData.data.value?.[0]?.total || '0' }} Total events: {{ eventsData.data.value?.[0]?.count || '0' }}
</div> </div>
<div v-if="(eventsData.data.value?.[0]?.total || 0) === 0"> <div v-if="(eventsData.data.value?.[0]?.count || 0) === 0">
Waiting for your first event... Waiting for your first event...
</div> </div>
</div> </div>
@@ -62,7 +48,7 @@ const eventsData = await useFetch(`/api/data/count`, { method: 'POST', headers,
<EventsStackedBarChart :slice="(selectLabelsEvents[eventsStackedSelectIndex].value as any)"> <EventsStackedBarChart :slice="(selectLabelsEvents[eventsStackedSelectIndex].value as any)">
</EventsStackedBarChart> </EventsStackedBarChart>
</div> </div>
</CardTitled> </CardTitled>
<CardTitled :key="refreshKey" class="p-4 flex-[2] w-full h-full" title="Top events" <CardTitled :key="refreshKey" class="p-4 flex-[2] w-full h-full" title="Top events"
sub="Displays key events."> sub="Displays key events.">
@@ -71,7 +57,7 @@ const eventsData = await useFetch(`/api/data/count`, { method: 'POST', headers,
</div> </div>
<div class="flex"> <!-- <div class="flex">
<EventsFunnelChart :key="refreshKey" class="w-full"></EventsFunnelChart> <EventsFunnelChart :key="refreshKey" class="w-full"></EventsFunnelChart>
</div> </div>
@@ -81,7 +67,7 @@ const eventsData = await useFetch(`/api/data/count`, { method: 'POST', headers,
<div class="flex"> <div class="flex">
<EventsMetadataAnalyzer :key="refreshKey"></EventsMetadataAnalyzer> <EventsMetadataAnalyzer :key="refreshKey"></EventsMetadataAnalyzer>
</div> </div> -->

View File

@@ -3,8 +3,6 @@
definePageMeta({ layout: 'dashboard' }); definePageMeta({ layout: 'dashboard' });
const activeProject = useActiveProject();
const items = [ const items = [
{ label: 'General', slot: 'general' }, { label: 'General', slot: 'general' },
{ label: 'Members', slot: 'members' }, { label: 'Members', slot: 'members' },
@@ -20,16 +18,16 @@ const items = [
<CustomTab :items="items" class="mt-8"> <CustomTab :items="items" class="mt-8">
<template #general> <template #general>
<SettingsGeneral :key="activeProject?._id.toString()"></SettingsGeneral> <SettingsGeneral :key="refreshKey"></SettingsGeneral>
</template> </template>
<template #members> <template #members>
<SettingsMembers :key="activeProject?._id.toString()"></SettingsMembers> <SettingsMembers :key="refreshKey"></SettingsMembers>
</template> </template>
<template #billing> <template #billing>
<SettingsBilling :key="activeProject?._id.toString()"></SettingsBilling> <SettingsBilling :key="refreshKey"></SettingsBilling>
</template> </template>
<template #account> <template #account>
<SettingsAccount :key="activeProject?._id.toString()"></SettingsAccount> <SettingsAccount :key="refreshKey"></SettingsAccount>
</template> </template>
</CustomTab> </CustomTab>

View File

@@ -10,7 +10,7 @@ export default defineEventHandler(async event => {
const { pid, from, to, project_id, limit } = data; const { pid, from, to, project_id, limit } = data;
const cacheKey = `browsers:${pid}:${from}:${to}`; const cacheKey = `browsers:${pid}:${limit}:${from}:${to}`;
const cacheExp = 60; const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => { return await Redis.useCacheV2(cacheKey, cacheExp, async () => {

View File

@@ -5,7 +5,7 @@ import { getRequestData } from "~/server/utils/getRequestData";
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const data = await getRequestData(event); const data = await getRequestData(event, { requireSchema: true });
if (!data) return; if (!data) return;
const { schemaName, pid, from, to, model, project_id } = data; const { schemaName, pid, from, to, model, project_id } = data;

View File

@@ -10,7 +10,7 @@ export default defineEventHandler(async event => {
const { pid, from, to, project_id, limit } = data; const { pid, from, to, project_id, limit } = data;
const cacheKey = `countries:${pid}:${from}:${to}`; const cacheKey = `countries:${pid}:${limit}:${from}:${to}`;
const cacheExp = 60; const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => { return await Redis.useCacheV2(cacheKey, cacheExp, async () => {

View File

@@ -10,7 +10,7 @@ export default defineEventHandler(async event => {
const { pid, from, to, project_id, limit } = data; const { pid, from, to, project_id, limit } = data;
const cacheKey = `devices:${pid}:${from}:${to}`; const cacheKey = `devices:${pid}:${limit}:${from}:${to}`;
const cacheExp = 60; const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => { return await Redis.useCacheV2(cacheKey, cacheExp, async () => {

View File

@@ -10,7 +10,7 @@ export default defineEventHandler(async event => {
const { pid, from, to, project_id, limit } = data; const { pid, from, to, project_id, limit } = data;
const cacheKey = `events:${pid}:${from}:${to}`; const cacheKey = `events:${pid}:${limit}:${from}:${to}`;
const cacheExp = 60; const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => { return await Redis.useCacheV2(cacheKey, cacheExp, async () => {

View File

@@ -10,7 +10,7 @@ export default defineEventHandler(async event => {
const { pid, from, to, project_id, limit } = data; const { pid, from, to, project_id, limit } = data;
const cacheKey = `oss:${pid}:${from}:${to}`; const cacheKey = `oss:${pid}:${limit}:${from}:${to}`;
const cacheExp = 60; const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => { return await Redis.useCacheV2(cacheKey, cacheExp, async () => {

View File

@@ -10,7 +10,7 @@ export default defineEventHandler(async event => {
const { pid, from, to, project_id, limit } = data; const { pid, from, to, project_id, limit } = data;
const cacheKey = `referrers:${pid}:${from}:${to}`; const cacheKey = `referrers:${pid}:${limit}:${from}:${to}`;
const cacheExp = 60; const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => { return await Redis.useCacheV2(cacheKey, cacheExp, async () => {

View File

@@ -10,7 +10,7 @@ export default defineEventHandler(async event => {
const { pid, from, to, project_id, limit } = data; const { pid, from, to, project_id, limit } = data;
const cacheKey = `websites:${pid}:${from}:${to}`; const cacheKey = `websites:${pid}:${limit}:${from}:${to}`;
const cacheExp = 60; const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => { return await Redis.useCacheV2(cacheKey, cacheExp, async () => {

View File

@@ -12,7 +12,7 @@ export default defineEventHandler(async event => {
const websiteName = getHeader(event, 'x-website-name'); const websiteName = getHeader(event, 'x-website-name');
const cacheKey = `websites_pages:${websiteName}:${pid}:${from}:${to}`; const cacheKey = `websites_pages:${websiteName}:${pid}:${limit}:${from}:${to}`;
const cacheExp = 60; const cacheExp = 60;
return await Redis.useCacheV2(cacheKey, cacheExp, async () => { return await Redis.useCacheV2(cacheKey, cacheExp, async () => {

View File

@@ -40,7 +40,7 @@ module.exports = {
darker: '#6A6A6A' darker: '#6A6A6A'
}, },
"lyx-widget": { "lyx-widget": {
DEFAULT: '#151515', DEFAULT: '#0E0E0E',
light: '#1E1E1E', light: '#1E1E1E',
lighter: '#262626' lighter: '#262626'
}, },