mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 07:48:37 +01:00
add domain wipe
This commit is contained in:
@@ -67,6 +67,9 @@ const { visible } = usePricingDrawer();
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<UModals />
|
||||
|
||||
<NuxtLayout>
|
||||
<NuxtPage></NuxtPage>
|
||||
</NuxtLayout>
|
||||
|
||||
@@ -140,8 +140,8 @@ const pricingDrawer = usePricingDrawer();
|
||||
<LyxUiButton to="/project_creation" v-if="projectList && (projectList.length < (maxProjects || 1))"
|
||||
type="outlined" class="w-full py-1 mt-2 text-[.8rem]">
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<div><i class="fas fa-plus"></i></div>
|
||||
<div> Create new project </div>
|
||||
<div><i class="fas fa-plus text-[.7rem]"></i></div>
|
||||
<div class="poppins"> New Project </div>
|
||||
</div>
|
||||
</LyxUiButton>
|
||||
|
||||
|
||||
73
dashboard/components/dialog/DeleteDomainData.vue
Normal file
73
dashboard/components/dialog/DeleteDomainData.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const emit = defineEmits(['success', 'cancel'])
|
||||
|
||||
const props = defineProps<{
|
||||
buttonType: string,
|
||||
message: string,
|
||||
deleteData: { isAll: boolean, visits: boolean, sessions: boolean, events: boolean, domain: string }
|
||||
}>();
|
||||
|
||||
const isDone = ref<boolean>(false);
|
||||
|
||||
async function deleteData() {
|
||||
|
||||
try {
|
||||
if (props.deleteData.isAll) {
|
||||
|
||||
} else {
|
||||
await $fetch('/api/settings/delete_domain', {
|
||||
method: 'DELETE',
|
||||
headers: useComputedHeaders({ useSnapshotDates: false, custom: { 'Content-Type': 'application/json' } }).value,
|
||||
body: JSON.stringify({
|
||||
domain: props.deleteData.domain,
|
||||
visits: props.deleteData.visits,
|
||||
sessions: props.deleteData.sessions,
|
||||
events: props.deleteData.events,
|
||||
})
|
||||
})
|
||||
}
|
||||
} catch (ex) {
|
||||
alert('Something went wrong');
|
||||
console.error(ex);
|
||||
}
|
||||
|
||||
isDone.value = true;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal :ui="{
|
||||
strategy: 'override',
|
||||
overlay: {
|
||||
background: 'bg-lyx-background/85'
|
||||
},
|
||||
background: 'bg-lyx-widget',
|
||||
ring: 'border-solid border-[1px] border-[#262626]'
|
||||
}">
|
||||
<div class="h-full flex flex-col gap-2 p-4">
|
||||
|
||||
<div class="font-semibold text-[1.2rem]"> {{ isDone ? "Data Deletion Scheduled" : "Are you sure ?" }}</div>
|
||||
|
||||
<div v-if="!isDone">
|
||||
{{ message }}
|
||||
</div>
|
||||
|
||||
<div v-if="isDone">
|
||||
Your data deletion request is being processed and will be reflected in your project dashboard within a
|
||||
few minutes.
|
||||
</div>
|
||||
|
||||
<div class="grow"></div>
|
||||
<div v-if="!isDone" class="flex justify-end gap-2">
|
||||
<LyxUiButton type="secondary" @click="emit('cancel')"> Cancel </LyxUiButton>
|
||||
<LyxUiButton @click="deleteData()" :type="buttonType"> Confirm </LyxUiButton>
|
||||
</div>
|
||||
|
||||
<div v-if="isDone" class="flex justify-end w-full">
|
||||
<LyxUiButton type="secondary" @click="emit('success')"> Dismiss </LyxUiButton>
|
||||
</div>
|
||||
</div>
|
||||
</UModal>
|
||||
</template>
|
||||
154
dashboard/components/settings/Data.vue
Normal file
154
dashboard/components/settings/Data.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<script lang="ts" setup>
|
||||
import DeleteDomainData from '../dialog/DeleteDomainData.vue';
|
||||
import type { SettingsTemplateEntry } from './Template.vue';
|
||||
|
||||
const entries: SettingsTemplateEntry[] = [
|
||||
{ id: 'delete_dns', title: 'Delete domain data', text: 'Delete data of a specific domain from this project' },
|
||||
{ id: 'delete_data', title: 'Delete project data', text: 'Delete all data from this project' },
|
||||
]
|
||||
|
||||
const domains = useFetch('/api/settings/domains', {
|
||||
headers: useComputedHeaders({ useSnapshotDates: false }),
|
||||
transform: (e) => {
|
||||
if (!e) return [];
|
||||
return e.sort((a, b) => {
|
||||
return a.count - b.count;
|
||||
}).map(e => {
|
||||
return { id: e._id, label: `${e._id} - ${e.count} visits` }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const selectedDomain = ref<{ id: string, label: string }>();
|
||||
const selectedVisits = ref<boolean>(true);
|
||||
const selectedSessions = ref<boolean>(true);
|
||||
const selectedEvents = ref<boolean>(true);
|
||||
|
||||
|
||||
const domainCounts = useFetch(() => `/api/settings/domain_counts?domain=${selectedDomain.value?.id}`, {
|
||||
headers: useComputedHeaders({ useSnapshotDates: false }),
|
||||
})
|
||||
|
||||
|
||||
const { setToken } = useAccessToken();
|
||||
|
||||
|
||||
const modal = useModal();
|
||||
|
||||
function openDeleteDomainDataDialog() {
|
||||
modal.open(DeleteDomainData, {
|
||||
preventClose: true,
|
||||
deleteData: {
|
||||
isAll: false,
|
||||
domain: selectedDomain.value?.id as string,
|
||||
visits: selectedVisits.value,
|
||||
sessions: selectedSessions.value,
|
||||
events: selectedEvents.value,
|
||||
},
|
||||
buttonType: 'primary',
|
||||
message: 'This action is irreversable and will wipe all the data from the selected domain.',
|
||||
onSuccess: () => {
|
||||
modal.close()
|
||||
},
|
||||
onCancel: () => {
|
||||
modal.close()
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function openDeleteAllDomainDataDialog() {
|
||||
modal.open(DeleteDomainData, {
|
||||
preventClose: true,
|
||||
deleteData: {
|
||||
isAll: true,
|
||||
domain: '',
|
||||
visits: false,
|
||||
sessions: false,
|
||||
events: false,
|
||||
},
|
||||
buttonType: 'danger',
|
||||
message: 'This action is irreversable and will wipe all the data from the entire project.',
|
||||
onSuccess: () => {
|
||||
modal.close()
|
||||
},
|
||||
onCancel: () => {
|
||||
modal.close()
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const visitsLabel = computed(() => {
|
||||
if (domainCounts.pending.value === true) return 'Visits loading...';
|
||||
if (domainCounts.data.value?.error === true) return 'Visits (too many to compute)';
|
||||
return 'Visits ' + (domainCounts.data.value?.visits ?? '');
|
||||
})
|
||||
|
||||
const eventsLabel = computed(() => {
|
||||
if (domainCounts.pending.value === true) return 'Events loading...';
|
||||
if (domainCounts.data.value?.error === true) return 'Events (too many to compute)';
|
||||
return 'Events ' + (domainCounts.data.value?.events ?? '');
|
||||
})
|
||||
|
||||
const sessionsLabel = computed(() => {
|
||||
if (domainCounts.pending.value === true) return 'Sessions loading...';
|
||||
if (domainCounts.data.value?.error === true) return 'Sessions (too many to compute)';
|
||||
return 'Sessions ' + (domainCounts.data.value?.sessions ?? '');
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<SettingsTemplate :entries="entries">
|
||||
<template #delete_dns>
|
||||
<div class="flex flex-col">
|
||||
|
||||
<!-- <div class="text-[.9rem] text-lyx-text-darker"> Select a domain </div> -->
|
||||
<USelectMenu placeholder="Select a domain" :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'
|
||||
}
|
||||
}" :options="domains.data.value ?? []" v-model="selectedDomain"></USelectMenu>
|
||||
|
||||
<div v-if="selectedDomain" class="flex flex-col gap-2 mt-4">
|
||||
<div class="text-[.9rem] text-lyx-text-dark"> Select data to delete </div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
|
||||
|
||||
<UCheckbox :ui="{ color: 'actionable-visits-color-checkbox' }" v-model="selectedVisits"
|
||||
:label="visitsLabel" />
|
||||
<UCheckbox :ui="{ color: 'actionable-sessions-color-checkbox' }" v-model="selectedSessions"
|
||||
:label="sessionsLabel" />
|
||||
<UCheckbox :ui="{ color: 'actionable-events-color-checkbox' }" v-model="selectedEvents"
|
||||
:label="eventsLabel" />
|
||||
|
||||
</div>
|
||||
|
||||
<LyxUiButton class="mt-2" v-if="selectedVisits || selectedSessions || selectedEvents"
|
||||
@click="openDeleteDomainDataDialog()" type="outline">
|
||||
Delete data
|
||||
</LyxUiButton>
|
||||
<div class="text-lyx-text-dark">
|
||||
This action will delete all data from the project creation date.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #delete_data>
|
||||
<div
|
||||
class="outline rounded-lg w-full px-8 py-4 flex flex-col gap-4 outline-[1px] outline-[#541c15] bg-[#1e1412]">
|
||||
<div class="poppins font-semibold"> This operation will reset this project to it's initial state (0
|
||||
visits 0 events 0 sessions)</div>
|
||||
<div @click="openDeleteAllDomainDataDialog()"
|
||||
class="text-[#e95b61] poppins font-semibold cursor-pointer hover:text-black hover:bg-red-700 outline rounded-lg w-fit px-8 py-2 outline-[1px] outline-[#532b26] bg-[#291415]">
|
||||
Delete all data
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</SettingsTemplate>
|
||||
</template>
|
||||
@@ -16,7 +16,7 @@ export type CustomDialogOptions = {
|
||||
params?: any,
|
||||
width?: string,
|
||||
height?: string,
|
||||
closable?: boolean
|
||||
closable?: boolean,
|
||||
}
|
||||
|
||||
function openDialogEx(component: Component, options?: CustomDialogOptions) {
|
||||
|
||||
@@ -5,6 +5,7 @@ definePageMeta({ layout: 'dashboard' });
|
||||
|
||||
const items = [
|
||||
{ label: 'General', slot: 'general' },
|
||||
{ label: 'Data', slot: 'data' },
|
||||
{ label: 'Members', slot: 'members' },
|
||||
{ label: 'Billing', slot: 'billing' },
|
||||
{ label: 'Codes', slot: 'codes' },
|
||||
@@ -22,6 +23,9 @@ const items = [
|
||||
<template #general>
|
||||
<SettingsGeneral :key="refreshKey"></SettingsGeneral>
|
||||
</template>
|
||||
<template #data>
|
||||
<SettingsData :key="refreshKey"></SettingsData>
|
||||
</template>
|
||||
<template #members>
|
||||
<SettingsMembers :key="refreshKey"></SettingsMembers>
|
||||
</template>
|
||||
|
||||
104
dashboard/server/api/settings/delete_domain.delete.ts
Normal file
104
dashboard/server/api/settings/delete_domain.delete.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
|
||||
import { EventModel } from "@schema/metrics/EventSchema";
|
||||
import { SessionModel } from "@schema/metrics/SessionSchema";
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { Types } from "mongoose";
|
||||
import { getRequestData } from "~/server/utils/getRequestData";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, { requireSchema: false });
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
const { domain, visits, events, sessions } = await readBody(event);
|
||||
|
||||
taskDeleteDomain(project_id, domain, visits, events, sessions);
|
||||
|
||||
return { ok: true }
|
||||
|
||||
});
|
||||
|
||||
|
||||
async function taskDeleteDomain(project_id: Types.ObjectId, domain: string, deleteVisits: boolean, deleteEvents: boolean, deleteSessions: boolean) {
|
||||
|
||||
console.log('Deletation started');
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
const data = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
website: domain
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$session",
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "events",
|
||||
let: { sessionId: "$_id" },
|
||||
pipeline: [
|
||||
{ $match: { $expr: { $eq: ["$session", "$$sessionId"] } } },
|
||||
{ $match: { project_id } },
|
||||
{ $project: { _id: 1 } }
|
||||
],
|
||||
as: "events"
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "sessions",
|
||||
let: { sessionId: "$_id" },
|
||||
pipeline: [
|
||||
{ $match: { $expr: { $eq: ["$session", "$$sessionId"] } } },
|
||||
{ $match: { project_id } },
|
||||
{ $project: { _id: 1 } }
|
||||
],
|
||||
as: "sessions"
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 1,
|
||||
count: 1,
|
||||
"events._id": 1,
|
||||
"sessions._id": 1
|
||||
}
|
||||
}
|
||||
]) as { _id: string, events: { _id: string }[], sessions: { _id: string }[] }[]
|
||||
|
||||
|
||||
if (deleteSessions === true) {
|
||||
const sessions = data.flatMap(e => e.sessions).map(e => e._id.toString());
|
||||
const batchSize = 1000;
|
||||
for (let i = 0; i < sessions.length; i += batchSize) {
|
||||
const batch = sessions.slice(i, i + batchSize);
|
||||
await SessionModel.deleteMany({ _id: { $in: batch } });
|
||||
}
|
||||
}
|
||||
|
||||
if (deleteEvents === true) {
|
||||
const sessions = data.flatMap(e => e.sessions).map(e => e._id.toString());
|
||||
const batchSize = 1000;
|
||||
for (let i = 0; i < sessions.length; i += batchSize) {
|
||||
const batch = sessions.slice(i, i + batchSize);
|
||||
await EventModel.deleteMany({ _id: { $in: batch } });
|
||||
}
|
||||
}
|
||||
|
||||
if (deleteVisits === true) {
|
||||
await VisitModel.deleteMany({ project_id, website: domain })
|
||||
}
|
||||
|
||||
const s = (Date.now() - start) / 1000;
|
||||
|
||||
console.log(`Deletation done in ${s.toFixed(2)} seconds`);
|
||||
|
||||
}
|
||||
79
dashboard/server/api/settings/domain_counts.ts
Normal file
79
dashboard/server/api/settings/domain_counts.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { getRequestData } from "~/server/utils/getRequestData";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, { requireSchema: false });
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
const { domain } = getQuery(event);
|
||||
|
||||
try {
|
||||
const resultData = await VisitModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
project_id,
|
||||
website: domain
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$session",
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "events",
|
||||
let: { sessionId: "$_id" },
|
||||
pipeline: [
|
||||
{ $match: { $expr: { $eq: ["$session", "$$sessionId"] } } },
|
||||
{ $match: { project_id } },
|
||||
{ $project: { _id: 1 } }
|
||||
],
|
||||
as: "events"
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "sessions",
|
||||
let: { sessionId: "$_id" },
|
||||
pipeline: [
|
||||
{ $match: { $expr: { $eq: ["$session", "$$sessionId"] } } },
|
||||
{ $match: { project_id } },
|
||||
{ $project: { _id: 1 } }
|
||||
],
|
||||
as: "sessions"
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 1,
|
||||
count: 1,
|
||||
"events._id": 1,
|
||||
"sessions._id": 1
|
||||
}
|
||||
}
|
||||
], { maxTimeMS: 5000 }) as { _id: string, count: number, events: { _id: string }[], sessions: { _id: string }[] }[]
|
||||
|
||||
|
||||
const visits = resultData.reduce((a, e) => a + e.count, 0);
|
||||
|
||||
const sessions = resultData.reduce((a, e) => {
|
||||
const count = e.sessions.length;
|
||||
return a + count;
|
||||
}, 0);
|
||||
|
||||
const events = resultData.reduce((a, e) => {
|
||||
const count = e.events.length;
|
||||
return a + count;
|
||||
}, 0);
|
||||
|
||||
return { visits, sessions, events, error: false, message: '' };
|
||||
} catch (ex: any) {
|
||||
return { error: true, message: ex.message.toString(), visits: -1, sessions: -1, events: -1 }
|
||||
}
|
||||
});
|
||||
18
dashboard/server/api/settings/domains.ts
Normal file
18
dashboard/server/api/settings/domains.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
import { VisitModel } from "@schema/metrics/VisitSchema";
|
||||
import { getRequestData } from "~/server/utils/getRequestData";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const data = await getRequestData(event, { requireSchema: false });
|
||||
if (!data) return;
|
||||
|
||||
const { project_id } = data;
|
||||
|
||||
const result = await VisitModel.aggregate([
|
||||
{ $match: { project_id } },
|
||||
{ $group: { _id: "$website", count: { $sum: 1 } } },
|
||||
]);
|
||||
|
||||
return result as { _id: string, count: number }[];
|
||||
});
|
||||
@@ -13,7 +13,7 @@ const EventSchema = new Schema<TEvent>({
|
||||
project_id: { type: Types.ObjectId, index: 1 },
|
||||
name: { type: String, required: true, index: 1 },
|
||||
metadata: Schema.Types.Mixed,
|
||||
session: { type: String },
|
||||
session: { type: String, index: 1 },
|
||||
flowHash: { type: String },
|
||||
created_at: { type: Date, default: () => Date.now(), index: true },
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@ export type TSession = {
|
||||
|
||||
const SessionSchema = new Schema<TSession>({
|
||||
project_id: { type: Types.ObjectId, index: 1 },
|
||||
session: { type: String, required: true },
|
||||
session: { type: String, required: true, index: 1 },
|
||||
flowHash: { type: String },
|
||||
duration: { type: Number, required: true, default: 0 },
|
||||
updated_at: { type: Date, default: () => Date.now() },
|
||||
|
||||
Reference in New Issue
Block a user