Skip to content

Commit 4cf7247

Browse files
authored
fix: edge-function cors, and allow internal calls with service role key (#2619)
* fix: edge-function cors, and allow internal calls with service role key * improve source ui * fix bug in weekly-passive-report * improve sources ui * add edge-function delete mining source * set source as valid on initial fetch * apply deepsource recommendations * fix formatting * improve page sources ui
1 parent 189760c commit 4cf7247

File tree

12 files changed

+586
-64
lines changed

12 files changed

+586
-64
lines changed

frontend/src/pages/sources.vue

Lines changed: 326 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,210 @@
11
<template>
2-
<div class="flex flex-col grow">
3-
<div
4-
class="flex flex-col grow border border-surface-200 rounded-md px-2 pt-6"
2+
<div
3+
class="flex flex-col grow border border-surface-200 rounded-md p-4 gap-4"
4+
>
5+
<div class="flex items-center justify-between">
6+
<h1 class="text-xl font-semibold">{{ t('sources') }}</h1>
7+
</div>
8+
9+
<DataView
10+
:value="$leadminer.miningSources"
11+
data-key="email"
12+
:paginator="true"
13+
:rows="10"
514
>
6-
<DataTable :value="$leadminer.miningSources">
7-
<Column field="email" :header="t('email')"> </Column>
8-
<Column field="type" :header="t('type')">
9-
<template #body="slotProps">
10-
<i
11-
:class="getIcon(slotProps.data.type)"
12-
class="text-secondary text-sm"
13-
></i>
14-
<span class="ml-2">{{ slotProps.data.type }}</span>
15-
</template>
16-
</Column>
17-
<Column field="passive_mining" :header="t('passive_mining')">
18-
<template #body="slotProps">
19-
<div v-if="slotProps.data.passive_mining">
20-
<span class="relative flex h-3 w-3">
21-
<span
22-
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
23-
></span>
24-
<span
25-
class="relative inline-flex h-3 w-3 rounded-full bg-green-500"
26-
></span>
27-
</span>
15+
<template #empty>
16+
<div class="text-center py-8 text-surface-500">
17+
{{ t('no_sources') }}
18+
</div>
19+
</template>
20+
<template #list="slotProps">
21+
<div class="grid gap-2">
22+
<div
23+
v-for="source in slotProps.items"
24+
:key="source.email"
25+
class="border border-surface-200 rounded-md p-4"
26+
>
27+
<div class="flex items-center justify-between gap-2">
28+
<div class="flex items-center gap-2">
29+
<div class="font-medium">{{ source.email }}</div>
30+
<Tag
31+
:value="
32+
source.isValid ? t('connected') : t('credential_expired')
33+
"
34+
:severity="source.isValid ? 'success' : 'warn'"
35+
/>
36+
</div>
37+
<div class="flex items-center gap-2">
38+
<Button
39+
v-if="isActiveMiningSource(source)"
40+
size="small"
41+
outlined
42+
severity="warning"
43+
icon="pi pi-stop"
44+
:label="t('stop_mining')"
45+
:loading="isStoppingMining"
46+
@click="confirmStopMining"
47+
/>
48+
<Button
49+
size="small"
50+
outlined
51+
severity="danger"
52+
icon="pi pi-trash"
53+
:label="t('delete_source')"
54+
:loading="
55+
isDeleting && deletingSource?.email === source.email
56+
"
57+
@click="openDeleteDialog(source)"
58+
/>
59+
</div>
2860
</div>
29-
<div v-else>
30-
<span class="inline-flex h-3 w-3 rounded-full bg-gray-500"></span>
61+
62+
<div class="grid grid-cols-2 lg:grid-cols-4 gap-2 mt-4 text-sm">
63+
<div class="p-2 rounded bg-surface-50">
64+
<div class="text-surface-500">{{ t('type') }}</div>
65+
<div class="flex items-center gap-2 font-medium">
66+
<i
67+
:class="getIcon(source.type)"
68+
class="text-secondary size-xs"
69+
></i>
70+
<span>{{ source.type }}</span>
71+
</div>
72+
</div>
73+
74+
<div class="p-2 rounded bg-surface-50">
75+
<div class="text-surface-500">{{ t('passive_mining') }}</div>
76+
<div class="flex items-center gap-2">
77+
<span
78+
v-if="source.passive_mining"
79+
class="relative flex h-3 w-3"
80+
>
81+
<span
82+
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
83+
></span>
84+
<span
85+
class="relative inline-flex h-3 w-3 rounded-full bg-green-500"
86+
></span>
87+
</span>
88+
<span
89+
v-else
90+
class="inline-flex h-3 w-3 rounded-full bg-gray-500"
91+
></span>
92+
<span>{{
93+
source.passive_mining ? t('enabled') : t('disabled')
94+
}}</span>
95+
</div>
96+
</div>
97+
98+
<div class="p-2 rounded bg-surface-50">
99+
<div class="text-surface-500">{{ t('total_contacts') }}</div>
100+
<div class="font-medium">{{ source.totalContacts || 0 }}</div>
101+
</div>
102+
103+
<div class="p-2 rounded bg-surface-50">
104+
<div class="text-surface-500">{{ t('last_mining') }}</div>
105+
<div class="font-medium">
106+
{{
107+
source.lastMiningDate
108+
? formatDate(source.lastMiningDate)
109+
: '-'
110+
}}
111+
</div>
112+
<div
113+
v-if="source.totalFromLastMining"
114+
class="text-xs text-surface-500"
115+
>
116+
{{ source.totalFromLastMining }} {{ t('contacts') }}
117+
</div>
118+
</div>
31119
</div>
32-
</template>
33-
</Column>
34-
</DataTable>
35-
</div>
120+
121+
<div
122+
v-if="isActiveMiningSource(source)"
123+
class="mt-4 p-3 rounded bg-surface-50 border border-primary/20"
124+
>
125+
<div class="flex items-center justify-between flex-wrap gap-2">
126+
<div class="flex items-center gap-2">
127+
<span class="relative flex h-2 w-2">
128+
<span
129+
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"
130+
></span>
131+
<span
132+
class="relative inline-flex h-2 w-2 rounded-full bg-primary"
133+
></span>
134+
</span>
135+
<span class="text-sm font-medium text-primary">{{
136+
t('mining_in_progress')
137+
}}</span>
138+
</div>
139+
<div class="flex items-center gap-2 text-sm text-surface-600">
140+
<span
141+
>{{ t('emails_scanned') }}:
142+
{{ $leadminer.scannedEmails }}</span
143+
>
144+
<span class="text-surface-400">|</span>
145+
<span
146+
>{{ t('emails_extracted') }}:
147+
{{ $leadminer.extractedEmails }}</span
148+
>
149+
<span class="text-surface-400">|</span>
150+
<span
151+
>{{ t('emails_cleaned') }}:
152+
{{ $leadminer.verifiedContacts }}</span
153+
>
154+
<Button
155+
size="small"
156+
severity="secondary"
157+
:label="t('view_mining')"
158+
class="ml-2"
159+
@click="navigateTo('/mine')"
160+
/>
161+
</div>
162+
</div>
163+
</div>
164+
</div>
165+
</div>
166+
</template>
167+
</DataView>
168+
169+
<Dialog
170+
v-model:visible="deleteDialogVisible"
171+
modal
172+
:header="t('delete_source')"
173+
:style="{ width: '28rem', maxWidth: '95vw' }"
174+
>
175+
<div class="text-sm text-surface-700">
176+
{{ t('delete_source_confirm') }}
177+
</div>
178+
179+
<template #footer>
180+
<div class="flex justify-end gap-2 w-full">
181+
<Button outlined :label="t('cancel')" @click="closeDeleteDialog" />
182+
<Button
183+
severity="danger"
184+
:label="t('delete_source')"
185+
:loading="isDeleting"
186+
@click="confirmDelete"
187+
/>
188+
</div>
189+
</template>
190+
</Dialog>
36191
</div>
37192
</template>
38193

39194
<script setup lang="ts">
195+
import type { MiningSource } from '~/types/mining';
196+
40197
const $leadminer = useLeadminerStore();
41198
const { t } = useI18n({
42199
useScope: 'local',
43200
});
201+
const { $saasEdgeFunctions } = useNuxtApp();
202+
const $toast = useToast();
203+
204+
const deleteDialogVisible = ref(false);
205+
const deletingSource = ref<MiningSource | null>(null);
206+
const isDeleting = ref(false);
207+
const isStoppingMining = ref(false);
44208
45209
function getIcon(type: string) {
46210
switch (type) {
@@ -52,22 +216,151 @@ function getIcon(type: string) {
52216
return 'pi pi-inbox';
53217
}
54218
}
219+
220+
function isActiveMiningSource(source: MiningSource): boolean {
221+
return Boolean(
222+
$leadminer.activeMiningSource?.email === source.email &&
223+
$leadminer.miningTask,
224+
);
225+
}
226+
227+
function formatDate(dateString: string) {
228+
return new Date(dateString).toLocaleDateString();
229+
}
230+
231+
function openDeleteDialog(source: MiningSource) {
232+
deletingSource.value = source;
233+
deleteDialogVisible.value = true;
234+
}
235+
236+
function closeDeleteDialog() {
237+
deleteDialogVisible.value = false;
238+
deletingSource.value = null;
239+
}
240+
241+
async function confirmDelete() {
242+
if (!deletingSource.value) return;
243+
244+
isDeleting.value = true;
245+
246+
try {
247+
await $saasEdgeFunctions('delete-mining-source', {
248+
method: 'DELETE',
249+
body: { email: deletingSource.value.email },
250+
});
251+
252+
$toast.add({
253+
severity: 'success',
254+
summary: t('source_deleted'),
255+
detail: t('source_deleted_detail'),
256+
life: 3500,
257+
});
258+
259+
closeDeleteDialog();
260+
await $leadminer.fetchMiningSources();
261+
} catch (error) {
262+
$toast.add({
263+
severity: 'error',
264+
summary: t('delete_source_failed'),
265+
detail: (error as Error).message,
266+
life: 4500,
267+
});
268+
} finally {
269+
isDeleting.value = false;
270+
}
271+
}
272+
273+
async function confirmStopMining() {
274+
isStoppingMining.value = true;
275+
try {
276+
await $leadminer.stopMining(true, null);
277+
$toast.add({
278+
severity: 'success',
279+
summary: t('mining_stopped'),
280+
detail: t('mining_stopped_detail'),
281+
life: 3500,
282+
});
283+
} catch (error) {
284+
$toast.add({
285+
severity: 'error',
286+
summary: t('stop_mining_failed'),
287+
detail: (error as Error).message,
288+
life: 4500,
289+
});
290+
} finally {
291+
isStoppingMining.value = false;
292+
}
293+
}
294+
55295
onMounted(async () => {
56296
await $leadminer.fetchMiningSources();
297+
await $leadminer.getCurrentRunningMining();
57298
});
58299
</script>
59300

60301
<i18n lang="json">
61302
{
62303
"en": {
304+
"sources": "Sources",
305+
"no_sources": "No sources yet",
63306
"email": "Email",
64307
"type": "Type",
65-
"passive_mining": "Passive mining"
308+
"passive_mining": "Passive mining",
309+
"credentials": "Credentials",
310+
"status": "Status",
311+
"connected": "Connected",
312+
"credential_expired": "Credential expired",
313+
"enabled": "Enabled",
314+
"disabled": "Disabled",
315+
"delete_source": "Delete",
316+
"delete_source_confirm": "Delete this mining source permanently? This action cannot be undone.",
317+
"cancel": "Cancel",
318+
"source_deleted": "Source deleted",
319+
"source_deleted_detail": "The mining source has been permanently deleted.",
320+
"delete_source_failed": "Unable to delete source",
321+
"stop_mining": "Stop mining",
322+
"view_mining": "View mining",
323+
"mining_in_progress": "Mining in progress",
324+
"emails_scanned": "Scanned",
325+
"emails_extracted": "Extracted",
326+
"emails_cleaned": "Cleaned",
327+
"mining_stopped": "Mining stopped",
328+
"mining_stopped_detail": "The mining process has been stopped.",
329+
"stop_mining_failed": "Unable to stop mining",
330+
"total_contacts": "Total contacts",
331+
"last_mining": "Last mining",
332+
"contacts": "contacts"
66333
},
67334
"fr": {
335+
"sources": "Sources",
336+
"no_sources": "Aucune source",
68337
"email": "Email",
69338
"type": "Type",
70-
"passive_mining": "Extraction passive"
339+
"passive_mining": "Extraction passive",
340+
"credentials": "Identifiants",
341+
"status": "Statut",
342+
"connected": "Connecté",
343+
"credential_expired": "Identifiant expiré",
344+
"enabled": "Activé",
345+
"disabled": "Désactivé",
346+
"delete_source": "Supprimer",
347+
"delete_source_confirm": "Supprimer définitivement cette source de minage ? Cette action est irréversible.",
348+
"cancel": "Annuler",
349+
"source_deleted": "Source supprimée",
350+
"source_deleted_detail": "La source de minage a été supprimée définitivement.",
351+
"delete_source_failed": "Impossible de supprimer la source",
352+
"stop_mining": "Arrêter le minage",
353+
"view_mining": "Voir le minage",
354+
"mining_in_progress": "Extraction en cours",
355+
"emails_scanned": "Scannés",
356+
"emails_extracted": "Extracts",
357+
"emails_cleaned": "Nettoyés",
358+
"mining_stopped": "Extraction stoppée",
359+
"mining_stopped_detail": "Le processus d'extraction a été stoppé.",
360+
"stop_mining_failed": "Impossible de stopper le minage",
361+
"total_contacts": "Total contacts",
362+
"last_mining": "Dernier minage",
363+
"contacts": "contacts"
71364
}
72365
}
73366
</i18n>

0 commit comments

Comments
 (0)