mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 15:58:38 +01:00
change in progress
This commit is contained in:
@@ -27,8 +27,7 @@ type Props = {
|
||||
const route = useRoute();
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const { isAdmin } = useUserRoles();
|
||||
const loggedUser = useLoggedUser()
|
||||
const { user, userRoles, setLoggedUser } = useLoggedUser()
|
||||
|
||||
const debugMode = process.dev;
|
||||
|
||||
@@ -102,16 +101,8 @@ function onLogout() {
|
||||
}
|
||||
|
||||
const { projects } = useProjectsList();
|
||||
const { data: guestProjects } = useGuestProjectsList()
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
const selectorProjects = computed(() => {
|
||||
const result: TProject[] = [];
|
||||
if (projects.value) result.push(...projects.value);
|
||||
if (guestProjects.value) result.push(...guestProjects.value);
|
||||
return result;
|
||||
});
|
||||
|
||||
const { data: maxProjects } = useFetch("/api/user/max_projects", {
|
||||
headers: computed(() => {
|
||||
return {
|
||||
@@ -120,21 +111,10 @@ const { data: maxProjects } = useFetch("/api/user/max_projects", {
|
||||
})
|
||||
});
|
||||
|
||||
const selected = ref<TProject>(activeProject.value as TProject);
|
||||
watch(selected, () => {
|
||||
setActiveProject(selected.value._id.toString())
|
||||
})
|
||||
|
||||
const isPremium = computed(() => {
|
||||
return activeProject.value?.premium;
|
||||
})
|
||||
|
||||
function isProjectMine(owner?: string) {
|
||||
if (!owner) return false;
|
||||
if (!loggedUser.value?.logged) return;
|
||||
return loggedUser.value.id == owner;
|
||||
}
|
||||
|
||||
const pricingDrawer = usePricingDrawer();
|
||||
|
||||
</script>
|
||||
@@ -158,36 +138,7 @@ const pricingDrawer = usePricingDrawer();
|
||||
|
||||
<div class="flex items-center gap-2 w-full">
|
||||
|
||||
<USelectMenu :uiMenu="{
|
||||
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter',
|
||||
base: '!bg-lyx-widget',
|
||||
option: {
|
||||
base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
|
||||
active: '!bg-lyx-widget-lighter'
|
||||
}
|
||||
}" class="w-full" v-if="selectorProjects" v-model="selected" :options="selectorProjects">
|
||||
|
||||
<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 }} {{ !isProjectMine(option.owner) ? '(Guest)' : '' }}</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 || '-' }}
|
||||
{{ !isProjectMine(activeProject?.owner?.toString()) ? '(Guest)' : '' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</USelectMenu>
|
||||
<ProjectSelector></ProjectSelector>
|
||||
|
||||
<div class="grow flex justify-end text-[1.4rem] mr-2 lg:hidden">
|
||||
<i @click="close()" class="fas fa-close"></i>
|
||||
@@ -289,7 +240,7 @@ const pricingDrawer = usePricingDrawer();
|
||||
|
||||
<div v-for="entry of section.entries" :class="{ 'grow flex items-end': entry.grow }">
|
||||
|
||||
<div v-if="(!entry.adminOnly || (isAdmin && !isAdminHidden))"
|
||||
<div v-if="(!entry.adminOnly || (userRoles.isAdmin && !isAdminHidden))"
|
||||
class="bg-lyx-background cursor-pointer text-lyx-text-dark py-[.35rem] px-2 rounded-lg text-[.95rem] flex items-center"
|
||||
:class="{
|
||||
'!text-lyx-text-darker pointer-events-none': entry.disabled,
|
||||
@@ -337,7 +288,7 @@ const pricingDrawer = usePricingDrawer();
|
||||
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
|
||||
<i class="fab fa-dev"></i>
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/admin" v-if="isAdmin"
|
||||
<NuxtLink to="/admin" v-if="userRoles.isAdmin"
|
||||
class="cursor-pointer hover:text-lyx-text text-lyx-text-dark">
|
||||
<i class="fas fa-cat"></i>
|
||||
</NuxtLink>
|
||||
|
||||
62
dashboard/components/ProjectSelector.vue
Normal file
62
dashboard/components/ProjectSelector.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import type { TProject } from '@schema/ProjectSchema';
|
||||
|
||||
const { user } = useLoggedUser()
|
||||
|
||||
const { projectList, actions, project } = useProject();
|
||||
const { data: guestProjects } = useGuestProjectsList()
|
||||
|
||||
const selectorProjects = computed(() => {
|
||||
const result: TProject[] = [];
|
||||
if (projectList.value) result.push(...projectList.value);
|
||||
if (guestProjects.value) result.push(...guestProjects.value);
|
||||
return result;
|
||||
});
|
||||
|
||||
function isProjectMine(owner?: string) {
|
||||
if (!owner) return false;
|
||||
if (!user.value) return false;
|
||||
if (!user.value.logged) return;
|
||||
return user.value.id == owner;
|
||||
}
|
||||
|
||||
function onChange(e: TProject) {
|
||||
actions.setActiveProject(e._id.toString());
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<USelectMenu :uiMenu="{
|
||||
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter',
|
||||
base: '!bg-lyx-widget',
|
||||
option: {
|
||||
base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
|
||||
active: '!bg-lyx-widget-lighter'
|
||||
}
|
||||
}" class="w-full" v-if="selectorProjects" @change="onChange" :value="project" :options="selectorProjects">
|
||||
|
||||
<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 }} {{ !isProjectMine(option.owner) ? '(Guest)' : '' }}</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>
|
||||
{{ project?.name || '-' }}
|
||||
{{ !isProjectMine(project?.owner?.toString()) ? '(Guest)' : '' }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</USelectMenu>
|
||||
|
||||
</template>
|
||||
@@ -3,15 +3,45 @@
|
||||
import DateService from '@services/DateService';
|
||||
import type { Slice } from '@services/DateService';
|
||||
|
||||
const { data: metricsInfo } = useMetricsData();
|
||||
|
||||
const { snapshot, safeSnapshotDates } = useSnapshot()
|
||||
|
||||
const snapshotFrom = computed(() => new Date(snapshot.value?.from || '0').getTime());
|
||||
const snapshotTo = computed(() => new Date(snapshot.value?.to || Date.now()).getTime());
|
||||
|
||||
const snapshotDays = computed(() => {
|
||||
return (snapshotTo.value - snapshotFrom.value) / 1000 / 60 / 60 / 24;
|
||||
const to = new Date(safeSnapshotDates.value.to).getTime();
|
||||
const from = new Date(safeSnapshotDates.value.from).getTime();
|
||||
return (to - from) / 1000 / 60 / 60 / 24;
|
||||
});
|
||||
|
||||
const chartSlice = computed(() => {
|
||||
const snapshotSizeMs = new Date(snapshot.value.to).getTime() - new Date(snapshot.value.from).getTime();
|
||||
if (snapshotSizeMs < 1000 * 60 * 60 * 24 * 6) return 'hour' as Slice;
|
||||
if (snapshotSizeMs < 1000 * 60 * 60 * 24 * 30) return 'day' as Slice;
|
||||
if (snapshotSizeMs < 1000 * 60 * 60 * 24 * 90) return 'day' as Slice;
|
||||
return 'month' as Slice;
|
||||
});
|
||||
|
||||
|
||||
function transformResponse(input: { _id: string, count: number }[]) {
|
||||
const data = input.map(e => e.count || 0);
|
||||
const labels = input.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, chartSlice.value));
|
||||
const pool = [...input.map(e => e.count || 0)];
|
||||
pool.pop();
|
||||
const avg = pool.reduce((a, e) => a + e, 0) / pool.length;
|
||||
const diffPercent: number = (100 / avg * (input.at(-1)?.count || 0)) - 100;
|
||||
const trend = Math.max(Math.min(diffPercent, 99), -99);
|
||||
return { data, labels, trend }
|
||||
}
|
||||
|
||||
const visitsData = useFetch('/api/timeline/visits', {
|
||||
headers: useComputedHeaders({ slice: chartSlice.value }), lazy: true, transform: transformResponse
|
||||
});
|
||||
const sessionsData = useFetch('/api/timeline/sessions', {
|
||||
headers: useComputedHeaders({ slice: chartSlice.value }), lazy: true, transform: transformResponse
|
||||
});
|
||||
const sessionsDurationData = useFetch('/api/timeline/sessions_duration', {
|
||||
headers: useComputedHeaders({ slice: chartSlice.value }), lazy: true, transform: transformResponse
|
||||
});
|
||||
const bouncingRateData = useFetch('/api/timeline/bouncing_rate', {
|
||||
headers: useComputedHeaders({ slice: chartSlice.value }), lazy: true, transform: transformResponse
|
||||
});
|
||||
|
||||
const avgVisitDay = computed(() => {
|
||||
@@ -21,13 +51,6 @@ const avgVisitDay = computed(() => {
|
||||
return avg.toFixed(2);
|
||||
});
|
||||
|
||||
// const avgEventsDay = computed(() => {
|
||||
// if (!eventsData.data.value) return '0.00';
|
||||
// const counts = eventsData.data.value.data.reduce((a, e) => e + a, 0);
|
||||
// const avg = counts / Math.max(snapshotDays.value, 1);
|
||||
// return avg.toFixed(2);
|
||||
// });
|
||||
|
||||
const avgSessionsDay = computed(() => {
|
||||
if (!sessionsData.data.value) return '0.00';
|
||||
const counts = sessionsData.data.value.data.reduce((a, e) => e + a, 0);
|
||||
@@ -47,8 +70,14 @@ const avgBouncingRate = computed(() => {
|
||||
})
|
||||
|
||||
const avgSessionDuration = computed(() => {
|
||||
if (!metricsInfo.value) return '0.00';
|
||||
const avg = metricsInfo.value.avgSessionDuration;
|
||||
if (!sessionsDurationData.data.value) return '0.00 %'
|
||||
|
||||
const counts = sessionsDurationData.data.value.data
|
||||
.filter(e => e > 0)
|
||||
.reduce((a, e) => e + a, 0);
|
||||
|
||||
const avg = counts / Math.max(sessionsDurationData.data.value.data.filter(e => e > 0).length, 1);
|
||||
|
||||
let hours = 0;
|
||||
let minutes = 0;
|
||||
let seconds = 0;
|
||||
@@ -59,91 +88,6 @@ const avgSessionDuration = computed(() => {
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
const chartSlice = computed(() => {
|
||||
const snapshotSizeMs = new Date(snapshot.value.to).getTime() - new Date(snapshot.value.from).getTime();
|
||||
if (snapshotSizeMs < 1000 * 60 * 60 * 24 * 6) return 'hour' as Slice;
|
||||
if (snapshotSizeMs < 1000 * 60 * 60 * 24 * 30) return 'day' as Slice;
|
||||
if (snapshotSizeMs < 1000 * 60 * 60 * 24 * 90) return 'day' as Slice;
|
||||
return 'month' as Slice;
|
||||
});
|
||||
|
||||
|
||||
function transformResponse(input: { _id: string, count: number }[]) {
|
||||
const data = input.map(e => e.count);
|
||||
const labels = input.map(e => DateService.getChartLabelFromISO(e._id, navigator.language, chartSlice.value));
|
||||
const pool = [...input.map(e => e.count)];
|
||||
pool.pop();
|
||||
const avg = pool.reduce((a, e) => a + e, 0) / pool.length;
|
||||
const diffPercent: number = (100 / avg * (input.at(-1)?.count || 0)) - 100;
|
||||
const trend = Math.max(Math.min(diffPercent, 99), -99);
|
||||
return { data, labels, trend }
|
||||
}
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
function getBody() {
|
||||
return JSON.stringify({
|
||||
from: safeSnapshotDates.value.from,
|
||||
to: safeSnapshotDates.value.to,
|
||||
slice: chartSlice.value
|
||||
});
|
||||
}
|
||||
|
||||
const computedHeaders = computed(() => {
|
||||
return {
|
||||
...signHeaders().headers,
|
||||
'x-pid': activeProject.value?._id.toString() || '',
|
||||
'x-from': safeSnapshotDates.value.from,
|
||||
'x-to': safeSnapshotDates.value.to,
|
||||
'x-slice': chartSlice.value
|
||||
}
|
||||
})
|
||||
|
||||
const visitsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/visits`, {
|
||||
method: 'POST', ...signHeaders({ v2: 'true' }), body: getBody(), transform: transformResponse,
|
||||
lazy: true, immediate: false
|
||||
});
|
||||
|
||||
// const eventsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/events`, {
|
||||
// method: 'POST', ...signHeaders({ v2: 'true' }), body: getBody(), transform: transformResponse,
|
||||
// lazy: true, immediate: false
|
||||
// });
|
||||
|
||||
const sessionsData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/sessions`, {
|
||||
method: 'POST', ...signHeaders({ v2: 'true' }), body: getBody(), transform: transformResponse,
|
||||
lazy: true, immediate: false
|
||||
});
|
||||
|
||||
const sessionsDurationData = useFetch(`/api/metrics/${activeProject.value?._id}/timeline/sessions_duration`, {
|
||||
method: 'POST', ...signHeaders({ v2: 'true' }), body: getBody(), transform: transformResponse,
|
||||
lazy: true, immediate: false
|
||||
});
|
||||
|
||||
const bouncingRateData = useFetch(`/api/data/bouncing_rate`, {
|
||||
headers: computedHeaders, lazy: true, immediate: false,
|
||||
transform: (input: { data: string, value: number | null }[]) => {
|
||||
const data = input.map(e => e.value || 0);
|
||||
const labels = input.map(e => DateService.getChartLabelFromISO(e.data, navigator.language, chartSlice.value));
|
||||
const pool = [...input.map(e => e.value || 0)];
|
||||
pool.pop();
|
||||
const avg = pool.reduce((a, e) => a + e, 0) / pool.length;
|
||||
const diffPercent: number = (100 / avg * (input.at(-1)?.value || 0)) - 100;
|
||||
const trend = Math.max(Math.min(diffPercent, 99), -99);
|
||||
return { data, labels, trend }
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
visitsData.execute();
|
||||
bouncingRateData.execute();
|
||||
sessionsData.execute();
|
||||
sessionsDurationData.execute()
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@@ -157,8 +101,7 @@ onMounted(async () => {
|
||||
</DashboardCountCard>
|
||||
|
||||
<DashboardCountCard :ready="!bouncingRateData.pending.value" icon="far fa-chart-user" text="Bouncing rate"
|
||||
:value="avgBouncingRate" :trend="bouncingRateData.data.value?.trend"
|
||||
:slow="true"
|
||||
:value="avgBouncingRate" :trend="bouncingRateData.data.value?.trend" :slow="true"
|
||||
:data="bouncingRateData.data.value?.data" :labels="bouncingRateData.data.value?.labels" color="#1e9b86">
|
||||
</DashboardCountCard>
|
||||
|
||||
@@ -170,12 +113,12 @@ onMounted(async () => {
|
||||
</DashboardCountCard>
|
||||
|
||||
|
||||
<DashboardCountCard :ready="!sessionsDurationData.pending.value" icon="far fa-timer" text="Total avg session time"
|
||||
:value="avgSessionDuration" :trend="sessionsDurationData.data.value?.trend"
|
||||
<DashboardCountCard :ready="!sessionsDurationData.pending.value" icon="far fa-timer"
|
||||
text="Total avg session time" :value="avgSessionDuration" :trend="sessionsDurationData.data.value?.trend"
|
||||
:data="sessionsDurationData.data.value?.data" :labels="sessionsDurationData.data.value?.labels"
|
||||
color="#f56523">
|
||||
</DashboardCountCard>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</template>
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
const { project } = useProject();
|
||||
|
||||
const { onlineUsers, stopWatching, startWatching } = useOnlineUsers();
|
||||
onMounted(() => startWatching());
|
||||
onUnmounted(() => stopWatching());
|
||||
@@ -9,8 +11,9 @@ onUnmounted(() => stopWatching());
|
||||
const { createAlert } = useAlert();
|
||||
|
||||
function copyProjectId() {
|
||||
if (!navigator.clipboard) alert('You can\'t copy in HTTP');
|
||||
navigator.clipboard.writeText((activeProject.value?._id || 0).toString());
|
||||
if (!navigator.clipboard) return alert('You can\'t copy in HTTP');
|
||||
if (!project.value) return alert('Project not loaded');
|
||||
navigator.clipboard.writeText((project.value._id).toString());
|
||||
createAlert('Success', 'Project id copied successfully.', 'far fa-circle-check', 5000);
|
||||
}
|
||||
|
||||
@@ -42,7 +45,7 @@ function showAnomalyInfoAlert() {
|
||||
|
||||
<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="text-lyx-text poppins font-medium text-[1.2rem]"> {{ activeProject?.name || 'Loading...' }}
|
||||
<div class="text-lyx-text poppins font-medium text-[1.2rem]"> {{ project?.name || 'Loading...' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,7 +53,7 @@ function showAnomalyInfoAlert() {
|
||||
<div class="poppins font-medium text-lyx-text-darker text-[1.2rem]">Project id:</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="text-lyx-text poppins font-medium text-[1.2rem]">
|
||||
{{ activeProject?._id || 'Loading...' }}
|
||||
{{ project?._id || 'Loading...' }}
|
||||
</div>
|
||||
<div class="flex items-center ml-3">
|
||||
<i @click="copyProjectId()"
|
||||
|
||||
Reference in New Issue
Block a user