fix ui + sessions + reactivity

This commit is contained in:
Emily
2024-08-06 15:32:46 +02:00
parent 46774bd114
commit 02db836003
14 changed files with 150 additions and 69 deletions

View File

@@ -22,7 +22,7 @@ export async function startStreamLoop() {
delay: { base: 100, empty: 5000 }, delay: { base: 100, empty: 5000 },
readBlock: 2500 readBlock: 2500
}, processStreamEvent); }, processStreamEvent);
} }
@@ -97,6 +97,11 @@ async function process_keep_alive(data: Record<string, string>, sessionHash: str
const { pid, instant, flowHash } = data; const { pid, instant, flowHash } = data;
const existingSession = await SessionModel.findOne({ project_id: pid }, { _id: 1 });
if (!existingSession) {
await ProjectCountModel.updateOne({ project_id: pid }, { $inc: { 'sessions': 1 } }, { upsert: true });
}
if (instant == "true") { if (instant == "true") {
await SessionModel.updateOne({ project_id: pid, session: sessionHash, }, { await SessionModel.updateOne({ project_id: pid, session: sessionHash, }, {
$inc: { duration: 0 }, $inc: { duration: 0 },

View File

@@ -98,6 +98,15 @@ function onLogout() {
const { projects } = useProjectsList(); const { projects } = useProjectsList();
const activeProject = useActiveProject(); const activeProject = useActiveProject();
const { data: maxProjects } = useFetch("/api/user/max_projects", {
headers: computed(() => {
return {
Authorization: authorizationHeaderComputed.value
}
})
});
const selected = ref<TProject>(activeProject.value as TProject); const selected = ref<TProject>(activeProject.value as TProject);
watch(selected, () => { watch(selected, () => {
setActiveProject(selected.value._id.toString()) setActiveProject(selected.value._id.toString())
@@ -112,56 +121,57 @@ watch(selected, () => {
'hidden lg:flex': !isOpen 'hidden lg:flex': !isOpen
}"> }">
<div class="py-4 px-2 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"> <div class="flex px-2 flex-col">
<img class="h-[2rem]" :src="'/logo.png'">
</div> -->
<!-- <div class="font-bold text-[1.4rem] text-gray-300"> Litlyx </div> --> <div class="flex items-center gap-2 w-full">
<USelectMenu :uiMenu="{ <USelectMenu :uiMenu="{
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter', select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter',
base: '!bg-lyx-widget', base: '!bg-lyx-widget',
option: { option: {
base: 'hover:!bg-lyx-widget-lighter cursor-pointer', base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
active: '!bg-lyx-widget-lighter' active: '!bg-lyx-widget-lighter'
} }
}" class="w-full" v-if="projects" v-model="selected" :options="projects"> }" class="w-full" v-if="projects" v-model="selected" :options="projects">
<template #option="{ option, active, selected }"> <template #option="{ option, active, selected }">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div> <div>
<img class="h-5 bg-black rounded-full" :src="'logo_32.png'" alt="Litlyx logo"> <img class="h-5 bg-black rounded-full" :src="'logo_32.png'" alt="Litlyx logo">
</div>
<div> {{ option.name }} </div>
</div> </div>
<div> {{ option.name }} </div> </template>
</div>
</template>
<template #label> <template #label>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div> <div>
<img class="h-5 bg-black rounded-full" :src="'logo_32.png'" alt="Litlyx logo"> <img class="h-5 bg-black rounded-full" :src="'logo_32.png'" alt="Litlyx logo">
</div>
<div> {{ activeProject?.name || '???' }} </div>
</div> </div>
<div> {{ activeProject?.name || '???' }} </div> </template>
</div> </USelectMenu>
</template>
</USelectMenu>
<div class="grow flex justify-end text-[1.4rem] mr-2 lg:hidden">
<i @click="close()" class="fas fa-close"></i>
</div>
<div class="grow flex justify-end text-[1.4rem] mr-2 lg:hidden">
<i @click="close()" class="fas fa-close"></i>
</div> </div>
<NuxtLink to="/project_creation" v-if="projects && (projects.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">
<div><i class="fas fa-plus"></i></div>
<div> Create new project </div>
</NuxtLink>
</div> </div>
<div class="px-2 w-full flex-col">
<div class="flex mb-2 px-2 items-center justify-between"> <div class="w-full flex-col px-2">
<div class="flex mb-2 items-center justify-between">
<div class="poppins text-[.8rem]"> <div class="poppins text-[.8rem]">
Snapshots Snapshots
</div> </div>
@@ -196,7 +206,7 @@ watch(selected, () => {
</template> </template>
</USelectMenu> </USelectMenu>
<div v-if="snapshot" class="flex flex-col text-[.8rem] mt-2 px-2"> <div v-if="snapshot" class="flex flex-col text-[.8rem] mt-2">
<div class="flex"> <div class="flex">
<div class="grow poppins"> From:</div> <div class="grow poppins"> From:</div>
<div class="poppins"> {{ new Date(snapshot.from).toLocaleString('it-IT').split(',')[0].trim() }} <div class="poppins"> {{ new Date(snapshot.from).toLocaleString('it-IT').split(',')[0].trim() }}
@@ -235,7 +245,9 @@ watch(selected, () => {
</div> </div>
</div> </div>
<div class="flex flex-col gap-4 h-full"> <div class="bg-lyx-widget-lighter h-[2px] w-full"></div>
<div class="flex flex-col h-full">
<div v-for="section of sections" class="flex flex-col gap-1"> <div v-for="section of sections" class="flex flex-col gap-1">
@@ -266,10 +278,10 @@ watch(selected, () => {
</div> </div>
<div class="grow"></div> <div class="grow"></div>
<div class="text-lyx-text-dark poppins text-[.8rem] px-4"> <div class="text-lyx-text-dark poppins text-[.8rem] px-4 pb-3">
Litlyx is in Beta version. Litlyx is in Beta version.
</div> </div>
<div class="bg-lyx-widget-lighter h-[2px] px-4 w-full"></div> <div class="bg-lyx-widget-lighter h-[2px] px-4 w-full 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">

View File

@@ -12,7 +12,7 @@ const activeTabIndex = ref<number>(0);
<div class="flex"> <div class="flex">
<div v-for="(tab, index) of items" @click="activeTabIndex = index" <div v-for="(tab, index) of items" @click="activeTabIndex = index"
class="px-6 pb-3 poppins font-medium text-lyx-text-darker border-b-[1px] border-lyx-text-darker" :class="{ class="px-6 pb-3 poppins font-medium text-lyx-text-darker border-b-[1px] border-lyx-text-darker" :class="{
'!border-[#88A7FF] text-[#88A7FF]': activeTabIndex === index, '!border-[#88A7FF] !text-[#88A7FF]': activeTabIndex === index,
'hover:border-lyx-text-dark hover:text-lyx-text-dark cursor-pointer': activeTabIndex !== index 'hover:border-lyx-text-dark hover:text-lyx-text-dark cursor-pointer': activeTabIndex !== index
}"> }">
{{ tab.label }} {{ tab.label }}

View File

@@ -12,6 +12,10 @@ const entries: SettingsTemplateEntry[] = [
const activeProject = useActiveProject(); const activeProject = useActiveProject();
const projectNameInputVal = ref<string>(activeProject.value?.name || ''); const projectNameInputVal = ref<string>(activeProject.value?.name || '');
watch(activeProject, () => {
projectNameInputVal.value = activeProject.value?.name || "";
})
const canChange = computed(() => { const canChange = computed(() => {
if (activeProject.value?.name == projectNameInputVal.value) return false; if (activeProject.value?.name == projectNameInputVal.value) return false;
if (projectNameInputVal.value.length === 0) return false; if (projectNameInputVal.value.length === 0) return false;
@@ -19,15 +23,25 @@ const canChange = computed(() => {
}); });
async function changeProjectName() {
await $fetch("/api/project/change_name", {
method: 'POST',
...signHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ name: projectNameInputVal.value })
});
location.reload();
}
</script> </script>
<template> <template>
<SettingsTemplate :entries="entries"> <SettingsTemplate :entries="entries" :key="activeProject?.name || 'NONE'">
<template #pname> <template #pname>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<LyxUiInput class="w-full px-4 py-2" v-model="projectNameInputVal"></LyxUiInput> <LyxUiInput class="w-full px-4 py-2" v-model="projectNameInputVal"></LyxUiInput>
<LyxUiButton :disabled="!canChange" type="primary"> Change </LyxUiButton> <LyxUiButton @click="changeProjectName()" :disabled="!canChange" type="primary"> Change </LyxUiButton>
</div> </div>
</template> </template>
<template #pid> <template #pid>

View File

@@ -22,6 +22,7 @@ type TProjectsGrouped = {
project_name: string, project_name: string,
total_visits: number, total_visits: number,
total_events: number, total_events: number,
total_sessions: number
}[] }[]
} }
@@ -47,7 +48,8 @@ const projectsGrouped = computed(() => {
premium: project.premium, premium: project.premium,
project_name: project.project_name, project_name: project.project_name,
total_events: project.total_events, total_events: project.total_events,
total_visits: project.total_visits total_visits: project.total_visits,
total_sessions: project.total_sessions
}); });
} else { } else {
@@ -61,7 +63,8 @@ const projectsGrouped = computed(() => {
premium_type: project.premium_type, premium_type: project.premium_type,
project_name: project.project_name, project_name: project.project_name,
total_events: project.total_events, total_events: project.total_events,
total_visits: project.total_visits total_visits: project.total_visits,
total_sessions: project.total_sessions
}] }]
} }
@@ -71,6 +74,12 @@ const projectsGrouped = computed(() => {
} }
result.sort((sa, sb) => {
const ca = sa.projects.reduce((a, e) => a + (e.total_visits + e.total_events), 0);
const cb = sb.projects.reduce((a, e) => a + (e.total_visits + e.total_events), 0);
return cb - ca;
})
return result; return result;
}); });
@@ -107,7 +116,6 @@ const totalEvents = computed(() => {
return projects.value?.reduce((a, e) => a + e.total_events, 0) || 0; return projects.value?.reduce((a, e) => a + e.total_events, 0) || 0;
}); });
const details = ref<any>(); const details = ref<any>();
const showDetails = ref<boolean>(false); const showDetails = ref<boolean>(false);
async function getProjectDetails(project_id: string) { async function getProjectDetails(project_id: string) {
@@ -188,17 +196,17 @@ async function resetCount(project_id: string) {
<div> {{ project.total_visits }} </div> <div> {{ project.total_visits }} </div>
<div> Events: </div> <div> Events: </div>
<div> {{ project.total_events }} </div> <div> {{ project.total_events }} </div>
<div> Sessions: </div>
<div> {{ project.total_sessions }} </div>
</div> </div>
<div class="flex gap-4"> <div class="flex gap-4 items-center mt-4">
<div class="bg-[#272727] hover:bg-[#313131] cursor-pointer px-8 py-2 mt-3 rounded-lg" <LyxUiButton type="secondary" @click="getProjectDetails(project._id)">
@click="getProjectDetails(project._id)"> Payment details
Get details </LyxUiButton>
</div> <LyxUiButton type="danger" @click="resetCount(project._id)">
<div class="bg-[#272727] hover:bg-[#313131] cursor-pointer px-8 py-2 mt-3 rounded-lg" Refresh counts
@click="resetCount(project._id)"> </LyxUiButton>
Reset counts
</div>
</div> </div>
</div> </div>

View File

@@ -14,7 +14,8 @@ export type AdminProjectsList = {
created_at: Date created_at: Date
}, },
total_visits: number, total_visits: number,
total_events: number total_events: number,
total_sessions: number
} }
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
@@ -54,6 +55,9 @@ export default defineEventHandler(async event => {
}, },
total_events: { total_events: {
$arrayElemAt: ["$counts.events", 0] $arrayElemAt: ["$counts.events", 0]
},
total_sessions: {
$arrayElemAt: ["$counts.sessions", 0]
} }
} }
} }

View File

@@ -1,6 +1,7 @@
import { ProjectCountModel } from "@schema/ProjectsCounts"; import { ProjectCountModel } from "@schema/ProjectsCounts";
import { EventModel } from "@schema/metrics/EventSchema"; import { EventModel } from "@schema/metrics/EventSchema";
import { SessionModel } from "@schema/metrics/SessionSchema";
import { VisitModel } from "@schema/metrics/VisitSchema"; import { VisitModel } from "@schema/metrics/VisitSchema";
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
@@ -13,8 +14,9 @@ export default defineEventHandler(async event => {
const events = await EventModel.countDocuments({ project_id }); const events = await EventModel.countDocuments({ project_id });
const visits = await VisitModel.countDocuments({ project_id }); const visits = await VisitModel.countDocuments({ project_id });
const sessions = await SessionModel.countDocuments({ project_id });
await ProjectCountModel.updateOne({ project_id, events, visits }, {}, { upsert: true }); await ProjectCountModel.updateOne({ project_id, events, visits, sessions }, {}, { upsert: true });
return { ok: true }; return { ok: true };
}); });

View File

@@ -36,12 +36,12 @@ export default defineEventHandler(async event => {
$group: { $group: {
_id: "$project_id", _id: "$project_id",
events: { $sum: "$events" }, events: { $sum: "$events" },
visits: { $sum: "$visits" } visits: { $sum: "$visits" },
sessions: { $sum: "$sessions" },
} }
} }
]); ]);
const sessionsVisitsCount: any[] = await Redis.useCache({ const sessionsVisitsCount: any[] = await Redis.useCache({
key: `counts:${project_id}:sessions_count`, key: `counts:${project_id}:sessions_count`,
exp: COUNTS_SESSIONS_EXPIRE_TIME exp: COUNTS_SESSIONS_EXPIRE_TIME

View File

@@ -3,9 +3,7 @@ import StripeService from '~/server/services/StripeService';
import type Event from 'stripe'; import type Event from 'stripe';
import { ProjectModel } from '@schema/ProjectSchema'; import { ProjectModel } from '@schema/ProjectSchema';
import { PREMIUM_DATA, PREMIUM_PLAN, getPlanFromId, getPlanFromPrice, getPlanFromTag } from '@data/PREMIUM'; import { PREMIUM_DATA, PREMIUM_PLAN, getPlanFromId, getPlanFromPrice, getPlanFromTag } from '@data/PREMIUM';
import { ProjectCountModel } from '@schema/ProjectsCounts';
import { ProjectLimitModel } from '@schema/ProjectsLimits'; import { ProjectLimitModel } from '@schema/ProjectsLimits';
import { UserModel } from '@schema/UserSchema';

View File

@@ -0,0 +1,30 @@
import { ProjectModel } from "@schema/ProjectSchema";
import { TeamMemberModel } from "@schema/TeamMemberSchema";
import { UserModel } from "@schema/UserSchema";
import { UserSettingsModel } from "@schema/UserSettings";
export default defineEventHandler(async event => {
const userData = getRequestUser(event);
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
const currentActiveProject = await UserSettingsModel.findOne({ user_id: userData.id });
if (!currentActiveProject) return setResponseStatus(event, 400, 'You need to select a project');
const project_id = currentActiveProject.active_project_id;
const project = await ProjectModel.findById(project_id);
if (!project) return setResponseStatus(event, 400, 'Project not found');
if (project.owner.toString() != userData.id) {
return setResponseStatus(event, 400, 'You are not the owner');
}
const { name } = await readBody(event);
project.name = name;
await project.save();
return { ok: true };
});

View File

@@ -39,7 +39,8 @@ export default defineEventHandler(async event => {
await ProjectCountModel.create({ await ProjectCountModel.create({
project_id: project._id, project_id: project._id,
events: 0, events: 0,
visits: 0 visits: 0,
sessions: 0
}); });
await ProjectLimitModel.updateOne({ project_id: project._id }, { await ProjectLimitModel.updateOne({ project_id: project._id }, {
@@ -76,7 +77,8 @@ export default defineEventHandler(async event => {
await ProjectCountModel.create({ await ProjectCountModel.create({
project_id: project._id, project_id: project._id,
events: 0, events: 0,
visits: 0 visits: 0,
sessions: 0
}); });
return project.toJSON() as TProject; return project.toJSON() as TProject;

View File

@@ -21,11 +21,14 @@ export class Redis {
url: runtimeConfig.REDIS_URL, url: runtimeConfig.REDIS_URL,
username: runtimeConfig.REDIS_USERNAME, username: runtimeConfig.REDIS_USERNAME,
password: runtimeConfig.REDIS_PASSWORD, password: runtimeConfig.REDIS_PASSWORD,
database: process.dev ? 1 : 0 database: process.dev ? 1 : 0,
}); });
static async init() { static async init() {
await this.client.connect(); await this.client.connect();
this.client.on('error', function (err) {
console.error('Redis error:', err);
});
} }
static async setString(key: string, value: string, exp: number) { static async setString(key: string, value: string, exp: number) {

View File

@@ -1,18 +1,19 @@
<script lang="ts" setup> <script lang="ts" setup>
export type ButtonType = 'primary' | 'secondary' | 'outline' | 'danger'; export type ButtonType = 'primary' | 'secondary' | 'outline' | 'outlined' | 'danger';
const props = defineProps<{ type: ButtonType, link?: string, target?: string }>(); const props = defineProps<{ type: ButtonType, link?: string, target?: string, disabled?: boolean }>();
</script> </script>
<template> <template>
<NuxtLink tag="div" :to="link" :target="target" <NuxtLink tag="div" :to="disabled ? '' : link" :target="target"
class="poppins w-fit cursor-pointer px-4 py-1 rounded-md outline outline-[1px] text-text" :class="{ class="poppins w-fit cursor-pointer px-4 py-1 rounded-md outline outline-[1px] text-text" :class="{
'bg-lyx-primary-dark outline-lyx-primary hover:bg-lyx-primary-hover': type === 'primary', 'bg-lyx-primary-dark outline-lyx-primary hover:bg-lyx-primary-hover': type === 'primary',
'bg-lyx-widget-lighter outline-lyx-widget-lighter hover:bg-lyx-widget-light': type === 'secondary', 'bg-lyx-widget-lighter outline-lyx-widget-lighter hover:bg-lyx-widget-light': type === 'secondary',
'bg-lyx-transparent outline-lyx-widget-lighter hover:bg-lyx-widget-light': type === 'outline', 'bg-lyx-transparent outline-lyx-widget-lighter hover:bg-lyx-widget-light': (type === 'outline' || type === 'outlined'),
'bg-lyx-danger-dark outline-lyx-danger hover:bg-lyx-danger': type === 'danger', 'bg-lyx-danger-dark outline-lyx-danger hover:bg-lyx-danger': type === 'danger',
'!bg-lyx-widget !outline-lyx-widget-lighter !cursor-not-allowed': disabled === true,
}"> }">
<slot></slot> <slot></slot>
</NuxtLink> </NuxtLink>

View File

@@ -5,12 +5,14 @@ export type TProjectCount = {
project_id: Schema.Types.ObjectId, project_id: Schema.Types.ObjectId,
events: number, events: number,
visits: number, visits: number,
sessions: number,
} }
const ProjectCountSchema = new Schema<TProjectCount>({ const ProjectCountSchema = new Schema<TProjectCount>({
project_id: { type: Types.ObjectId, index: true, unique: true }, project_id: { type: Types.ObjectId, index: true, unique: true },
events: { type: Number, required: true, default: 0 }, events: { type: Number, required: true, default: 0 },
visits: { type: Number, required: true, default: 0 }, visits: { type: Number, required: true, default: 0 },
sessions: { type: Number, required: true, default: 0 },
}); });
export const ProjectCountModel = model<TProjectCount>('project_counts', ProjectCountSchema); export const ProjectCountModel = model<TProjectCount>('project_counts', ProjectCountSchema);