Skip to content

Commit 2929b22

Browse files
committed
add domain wipe
1 parent f06d7d7 commit 2929b22

File tree

11 files changed

+440
-5
lines changed

11 files changed

+440
-5
lines changed

dashboard/app.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ const { visible } = usePricingDrawer();
6767
</div>
6868
</div>
6969

70+
71+
<UModals />
72+
7073
<NuxtLayout>
7174
<NuxtPage></NuxtPage>
7275
</NuxtLayout>

dashboard/components/CVerticalNavigation.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,8 @@ const pricingDrawer = usePricingDrawer();
140140
<LyxUiButton to="/project_creation" v-if="projectList && (projectList.length < (maxProjects || 1))"
141141
type="outlined" class="w-full py-1 mt-2 text-[.8rem]">
142142
<div class="flex items-center gap-2 justify-center">
143-
<div><i class="fas fa-plus"></i></div>
144-
<div> Create new project </div>
143+
<div><i class="fas fa-plus text-[.7rem]"></i></div>
144+
<div class="poppins"> New Project </div>
145145
</div>
146146
</LyxUiButton>
147147

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<script lang="ts" setup>
2+
3+
const emit = defineEmits(['success', 'cancel'])
4+
5+
const props = defineProps<{
6+
buttonType: string,
7+
message: string,
8+
deleteData: { isAll: boolean, visits: boolean, sessions: boolean, events: boolean, domain: string }
9+
}>();
10+
11+
const isDone = ref<boolean>(false);
12+
13+
async function deleteData() {
14+
15+
try {
16+
if (props.deleteData.isAll) {
17+
18+
} else {
19+
await $fetch('/api/settings/delete_domain', {
20+
method: 'DELETE',
21+
headers: useComputedHeaders({ useSnapshotDates: false, custom: { 'Content-Type': 'application/json' } }).value,
22+
body: JSON.stringify({
23+
domain: props.deleteData.domain,
24+
visits: props.deleteData.visits,
25+
sessions: props.deleteData.sessions,
26+
events: props.deleteData.events,
27+
})
28+
})
29+
}
30+
} catch (ex) {
31+
alert('Something went wrong');
32+
console.error(ex);
33+
}
34+
35+
isDone.value = true;
36+
}
37+
38+
</script>
39+
40+
<template>
41+
<UModal :ui="{
42+
strategy: 'override',
43+
overlay: {
44+
background: 'bg-lyx-background/85'
45+
},
46+
background: 'bg-lyx-widget',
47+
ring: 'border-solid border-[1px] border-[#262626]'
48+
}">
49+
<div class="h-full flex flex-col gap-2 p-4">
50+
51+
<div class="font-semibold text-[1.2rem]"> {{ isDone ? "Data Deletion Scheduled" : "Are you sure ?" }}</div>
52+
53+
<div v-if="!isDone">
54+
{{ message }}
55+
</div>
56+
57+
<div v-if="isDone">
58+
Your data deletion request is being processed and will be reflected in your project dashboard within a
59+
few minutes.
60+
</div>
61+
62+
<div class="grow"></div>
63+
<div v-if="!isDone" class="flex justify-end gap-2">
64+
<LyxUiButton type="secondary" @click="emit('cancel')"> Cancel </LyxUiButton>
65+
<LyxUiButton @click="deleteData()" :type="buttonType"> Confirm </LyxUiButton>
66+
</div>
67+
68+
<div v-if="isDone" class="flex justify-end w-full">
69+
<LyxUiButton type="secondary" @click="emit('success')"> Dismiss </LyxUiButton>
70+
</div>
71+
</div>
72+
</UModal>
73+
</template>
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<script lang="ts" setup>
2+
import DeleteDomainData from '../dialog/DeleteDomainData.vue';
3+
import type { SettingsTemplateEntry } from './Template.vue';
4+
5+
const entries: SettingsTemplateEntry[] = [
6+
{ id: 'delete_dns', title: 'Delete domain data', text: 'Delete data of a specific domain from this project' },
7+
{ id: 'delete_data', title: 'Delete project data', text: 'Delete all data from this project' },
8+
]
9+
10+
const domains = useFetch('/api/settings/domains', {
11+
headers: useComputedHeaders({ useSnapshotDates: false }),
12+
transform: (e) => {
13+
if (!e) return [];
14+
return e.sort((a, b) => {
15+
return a.count - b.count;
16+
}).map(e => {
17+
return { id: e._id, label: `${e._id} - ${e.count} visits` }
18+
})
19+
}
20+
})
21+
22+
const selectedDomain = ref<{ id: string, label: string }>();
23+
const selectedVisits = ref<boolean>(true);
24+
const selectedSessions = ref<boolean>(true);
25+
const selectedEvents = ref<boolean>(true);
26+
27+
28+
const domainCounts = useFetch(() => `/api/settings/domain_counts?domain=${selectedDomain.value?.id}`, {
29+
headers: useComputedHeaders({ useSnapshotDates: false }),
30+
})
31+
32+
33+
const { setToken } = useAccessToken();
34+
35+
36+
const modal = useModal();
37+
38+
function openDeleteDomainDataDialog() {
39+
modal.open(DeleteDomainData, {
40+
preventClose: true,
41+
deleteData: {
42+
isAll: false,
43+
domain: selectedDomain.value?.id as string,
44+
visits: selectedVisits.value,
45+
sessions: selectedSessions.value,
46+
events: selectedEvents.value,
47+
},
48+
buttonType: 'primary',
49+
message: 'This action is irreversable and will wipe all the data from the selected domain.',
50+
onSuccess: () => {
51+
modal.close()
52+
},
53+
onCancel: () => {
54+
modal.close()
55+
},
56+
});
57+
}
58+
59+
function openDeleteAllDomainDataDialog() {
60+
modal.open(DeleteDomainData, {
61+
preventClose: true,
62+
deleteData: {
63+
isAll: true,
64+
domain: '',
65+
visits: false,
66+
sessions: false,
67+
events: false,
68+
},
69+
buttonType: 'danger',
70+
message: 'This action is irreversable and will wipe all the data from the entire project.',
71+
onSuccess: () => {
72+
modal.close()
73+
},
74+
onCancel: () => {
75+
modal.close()
76+
},
77+
});
78+
}
79+
80+
81+
const visitsLabel = computed(() => {
82+
if (domainCounts.pending.value === true) return 'Visits loading...';
83+
if (domainCounts.data.value?.error === true) return 'Visits (too many to compute)';
84+
return 'Visits ' + (domainCounts.data.value?.visits ?? '');
85+
})
86+
87+
const eventsLabel = computed(() => {
88+
if (domainCounts.pending.value === true) return 'Events loading...';
89+
if (domainCounts.data.value?.error === true) return 'Events (too many to compute)';
90+
return 'Events ' + (domainCounts.data.value?.events ?? '');
91+
})
92+
93+
const sessionsLabel = computed(() => {
94+
if (domainCounts.pending.value === true) return 'Sessions loading...';
95+
if (domainCounts.data.value?.error === true) return 'Sessions (too many to compute)';
96+
return 'Sessions ' + (domainCounts.data.value?.sessions ?? '');
97+
})
98+
99+
</script>
100+
101+
102+
<template>
103+
<SettingsTemplate :entries="entries">
104+
<template #delete_dns>
105+
<div class="flex flex-col">
106+
107+
<!-- <div class="text-[.9rem] text-lyx-text-darker"> Select a domain </div> -->
108+
<USelectMenu placeholder="Select a domain" :uiMenu="{
109+
select: '!bg-lyx-widget-light !shadow-none focus:!ring-lyx-widget-lighter !ring-lyx-widget-lighter',
110+
base: '!bg-lyx-widget',
111+
option: {
112+
base: 'hover:!bg-lyx-widget-lighter cursor-pointer',
113+
active: '!bg-lyx-widget-lighter'
114+
}
115+
}" :options="domains.data.value ?? []" v-model="selectedDomain"></USelectMenu>
116+
117+
<div v-if="selectedDomain" class="flex flex-col gap-2 mt-4">
118+
<div class="text-[.9rem] text-lyx-text-dark"> Select data to delete </div>
119+
120+
<div class="flex flex-col gap-1">
121+
122+
123+
<UCheckbox :ui="{ color: 'actionable-visits-color-checkbox' }" v-model="selectedVisits"
124+
:label="visitsLabel" />
125+
<UCheckbox :ui="{ color: 'actionable-sessions-color-checkbox' }" v-model="selectedSessions"
126+
:label="sessionsLabel" />
127+
<UCheckbox :ui="{ color: 'actionable-events-color-checkbox' }" v-model="selectedEvents"
128+
:label="eventsLabel" />
129+
130+
</div>
131+
132+
<LyxUiButton class="mt-2" v-if="selectedVisits || selectedSessions || selectedEvents"
133+
@click="openDeleteDomainDataDialog()" type="outline">
134+
Delete data
135+
</LyxUiButton>
136+
<div class="text-lyx-text-dark">
137+
This action will delete all data from the project creation date.
138+
</div>
139+
</div>
140+
</div>
141+
</template>
142+
<template #delete_data>
143+
<div
144+
class="outline rounded-lg w-full px-8 py-4 flex flex-col gap-4 outline-[1px] outline-[#541c15] bg-[#1e1412]">
145+
<div class="poppins font-semibold"> This operation will reset this project to it's initial state (0
146+
visits 0 events 0 sessions)</div>
147+
<div @click="openDeleteAllDomainDataDialog()"
148+
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]">
149+
Delete all data
150+
</div>
151+
</div>
152+
</template>
153+
</SettingsTemplate>
154+
</template>

dashboard/composables/useCustomDialog.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export type CustomDialogOptions = {
1616
params?: any,
1717
width?: string,
1818
height?: string,
19-
closable?: boolean
19+
closable?: boolean,
2020
}
2121

2222
function openDialogEx(component: Component, options?: CustomDialogOptions) {

dashboard/pages/settings.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ definePageMeta({ layout: 'dashboard' });
55
66
const items = [
77
{ label: 'General', slot: 'general' },
8+
{ label: 'Data', slot: 'data' },
89
{ label: 'Members', slot: 'members' },
910
{ label: 'Billing', slot: 'billing' },
1011
{ label: 'Codes', slot: 'codes' },
@@ -22,6 +23,9 @@ const items = [
2223
<template #general>
2324
<SettingsGeneral :key="refreshKey"></SettingsGeneral>
2425
</template>
26+
<template #data>
27+
<SettingsData :key="refreshKey"></SettingsData>
28+
</template>
2529
<template #members>
2630
<SettingsMembers :key="refreshKey"></SettingsMembers>
2731
</template>
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
2+
import { EventModel } from "@schema/metrics/EventSchema";
3+
import { SessionModel } from "@schema/metrics/SessionSchema";
4+
import { VisitModel } from "@schema/metrics/VisitSchema";
5+
import { Types } from "mongoose";
6+
import { getRequestData } from "~/server/utils/getRequestData";
7+
8+
export default defineEventHandler(async event => {
9+
10+
const data = await getRequestData(event, { requireSchema: false });
11+
if (!data) return;
12+
13+
const { project_id } = data;
14+
15+
const { domain, visits, events, sessions } = await readBody(event);
16+
17+
taskDeleteDomain(project_id, domain, visits, events, sessions);
18+
19+
return { ok: true }
20+
21+
});
22+
23+
24+
async function taskDeleteDomain(project_id: Types.ObjectId, domain: string, deleteVisits: boolean, deleteEvents: boolean, deleteSessions: boolean) {
25+
26+
console.log('Deletation started');
27+
28+
const start = Date.now();
29+
30+
const data = await VisitModel.aggregate([
31+
{
32+
$match: {
33+
project_id,
34+
website: domain
35+
}
36+
},
37+
{
38+
$group: {
39+
_id: "$session",
40+
count: { $sum: 1 }
41+
}
42+
},
43+
{
44+
$lookup: {
45+
from: "events",
46+
let: { sessionId: "$_id" },
47+
pipeline: [
48+
{ $match: { $expr: { $eq: ["$session", "$$sessionId"] } } },
49+
{ $match: { project_id } },
50+
{ $project: { _id: 1 } }
51+
],
52+
as: "events"
53+
}
54+
},
55+
{
56+
$lookup: {
57+
from: "sessions",
58+
let: { sessionId: "$_id" },
59+
pipeline: [
60+
{ $match: { $expr: { $eq: ["$session", "$$sessionId"] } } },
61+
{ $match: { project_id } },
62+
{ $project: { _id: 1 } }
63+
],
64+
as: "sessions"
65+
}
66+
},
67+
{
68+
$project: {
69+
_id: 1,
70+
count: 1,
71+
"events._id": 1,
72+
"sessions._id": 1
73+
}
74+
}
75+
]) as { _id: string, events: { _id: string }[], sessions: { _id: string }[] }[]
76+
77+
78+
if (deleteSessions === true) {
79+
const sessions = data.flatMap(e => e.sessions).map(e => e._id.toString());
80+
const batchSize = 1000;
81+
for (let i = 0; i < sessions.length; i += batchSize) {
82+
const batch = sessions.slice(i, i + batchSize);
83+
await SessionModel.deleteMany({ _id: { $in: batch } });
84+
}
85+
}
86+
87+
if (deleteEvents === true) {
88+
const sessions = data.flatMap(e => e.sessions).map(e => e._id.toString());
89+
const batchSize = 1000;
90+
for (let i = 0; i < sessions.length; i += batchSize) {
91+
const batch = sessions.slice(i, i + batchSize);
92+
await EventModel.deleteMany({ _id: { $in: batch } });
93+
}
94+
}
95+
96+
if (deleteVisits === true) {
97+
await VisitModel.deleteMany({ project_id, website: domain })
98+
}
99+
100+
const s = (Date.now() - start) / 1000;
101+
102+
console.log(`Deletation done in ${s.toFixed(2)} seconds`);
103+
104+
}

0 commit comments

Comments
 (0)