plum-e2e 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/backend/lib/runnerProcess.js +50 -4
- package/backend/logs/runner-cmqneqerz0000qq01i5ap2rvl.log +22 -0
- package/backend/logs/runner-cmqnfv7kr0000r101aeocm8eu.log +20 -0
- package/backend/logs/runner-cmqnfvb560001r101qoi0phau.log +43 -0
- package/backend/logs/runner-cmqnfvlm20002r101gsyqb837.log +20 -0
- package/backend/logs/runner-cmqnfvqfy0003r101fh41pzx3.log +20 -0
- package/backend/logs/runner-cmqnfvvwo0004r101q4dtqxd2.log +20 -0
- package/backend/prisma/migrations/20260621000000_add_notifications/migration.sql +8 -0
- package/backend/prisma/schema.prisma +12 -7
- package/backend/routes/node.routes.js +9 -0
- package/backend/routes/runners.routes.js +10 -0
- package/backend/routes/settings.routes.js +27 -0
- package/backend/scripts/manage-runners.mjs +49 -8
- package/backend/server.js +22 -1
- package/backend/services/cronService.js +91 -7
- package/backend/services/notificationService.js +163 -0
- package/backend/services/settingsService.js +29 -1
- package/backend/websockets/socketHandler.js +82 -6
- package/frontend/src/lib/api/schedules.js +5 -1
- package/frontend/src/lib/api/settings.js +15 -0
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +79 -3
- package/frontend/src/lib/stores/runner.js +4 -2
- package/frontend/src/routes/scheduled-tests/+page.svelte +65 -7
- package/frontend/src/routes/settings/+page.svelte +92 -2
- package/package.json +1 -1
|
@@ -48,7 +48,7 @@ export const runsVersion = writable(0);
|
|
|
48
48
|
// Map of taskName → true for every cron job currently executing
|
|
49
49
|
export const activeCronJobs = writable({});
|
|
50
50
|
|
|
51
|
-
export function triggerRun(id, testRunId) {
|
|
51
|
+
export function triggerRun(id, testRunId, notify = {}) {
|
|
52
52
|
const s = get(socket);
|
|
53
53
|
if (!s) return;
|
|
54
54
|
|
|
@@ -72,7 +72,9 @@ export function triggerRun(id, testRunId) {
|
|
|
72
72
|
workers,
|
|
73
73
|
browser,
|
|
74
74
|
runners: selectedRunners,
|
|
75
|
-
testRunId: testRunId ?? null
|
|
75
|
+
testRunId: testRunId ?? null,
|
|
76
|
+
notifyDiscord: notify.notifyDiscord ?? false,
|
|
77
|
+
notifySlack: notify.notifySlack ?? false
|
|
76
78
|
});
|
|
77
79
|
}
|
|
78
80
|
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
toggleCronJob
|
|
26
26
|
} from '$lib/api/schedules';
|
|
27
27
|
import { fetchRunners } from '$lib/api/runners';
|
|
28
|
+
import { fetchIntegrations } from '$lib/api/settings';
|
|
28
29
|
import { activeCronJobs } from '$lib/stores/runner';
|
|
29
30
|
import { BROWSERS, TOAST_TIMEOUT_MS } from '$lib/constants';
|
|
30
31
|
import { stagger } from '$lib/utils/format';
|
|
@@ -50,6 +51,7 @@
|
|
|
50
51
|
|
|
51
52
|
let cronJobs = [];
|
|
52
53
|
let availableRunners = [];
|
|
54
|
+
let integrations = { discordWebhookUrl: '', slackWebhookUrl: '', notifyPublicUrl: '' };
|
|
53
55
|
let toast = null;
|
|
54
56
|
|
|
55
57
|
let modalOpen = false;
|
|
@@ -64,7 +66,9 @@
|
|
|
64
66
|
tags: '',
|
|
65
67
|
workers: 1,
|
|
66
68
|
browser: 'chromium',
|
|
67
|
-
runnerIds: ['built-in']
|
|
69
|
+
runnerIds: ['built-in'],
|
|
70
|
+
notifyDiscord: false,
|
|
71
|
+
notifySlack: false
|
|
68
72
|
};
|
|
69
73
|
let selectedSchedule = '';
|
|
70
74
|
let useCustomCron = false;
|
|
@@ -137,7 +141,9 @@
|
|
|
137
141
|
tags: '',
|
|
138
142
|
workers: 1,
|
|
139
143
|
browser: 'chromium',
|
|
140
|
-
runnerIds: ['built-in']
|
|
144
|
+
runnerIds: ['built-in'],
|
|
145
|
+
notifyDiscord: false,
|
|
146
|
+
notifySlack: false
|
|
141
147
|
};
|
|
142
148
|
selectedSchedule = '';
|
|
143
149
|
useCustomCron = false;
|
|
@@ -157,7 +163,9 @@
|
|
|
157
163
|
tags: job.tags,
|
|
158
164
|
workers: job.workers ?? 1,
|
|
159
165
|
browser: job.browser ?? 'chromium',
|
|
160
|
-
runnerIds: prunedIds.length > 0 ? prunedIds : ['built-in']
|
|
166
|
+
runnerIds: prunedIds.length > 0 ? prunedIds : ['built-in'],
|
|
167
|
+
notifyDiscord: job.notifyDiscord ?? false,
|
|
168
|
+
notifySlack: job.notifySlack ?? false
|
|
161
169
|
};
|
|
162
170
|
const isPreset = scheduleOptions.some((o) => o.value === job.cronExpression);
|
|
163
171
|
useCustomCron = !isPreset;
|
|
@@ -231,10 +239,15 @@
|
|
|
231
239
|
}
|
|
232
240
|
|
|
233
241
|
onMount(async () => {
|
|
234
|
-
cronJobs = await
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
242
|
+
[cronJobs, availableRunners, integrations] = await Promise.all([
|
|
243
|
+
fetchCronJobs(),
|
|
244
|
+
fetchRunners().catch(() => []),
|
|
245
|
+
fetchIntegrations().catch(() => ({
|
|
246
|
+
discordWebhookUrl: '',
|
|
247
|
+
slackWebhookUrl: '',
|
|
248
|
+
notifyPublicUrl: ''
|
|
249
|
+
}))
|
|
250
|
+
]);
|
|
238
251
|
});
|
|
239
252
|
</script>
|
|
240
253
|
|
|
@@ -373,6 +386,26 @@
|
|
|
373
386
|
</div>
|
|
374
387
|
</div>
|
|
375
388
|
|
|
389
|
+
{#if integrations.discordWebhookUrl || integrations.slackWebhookUrl}
|
|
390
|
+
<div class="field">
|
|
391
|
+
<div class="field-label"><span>Notifications</span></div>
|
|
392
|
+
<div class="notify-checks">
|
|
393
|
+
{#if integrations.discordWebhookUrl}
|
|
394
|
+
<label class="notify-check-option">
|
|
395
|
+
<input type="checkbox" bind:checked={form.notifyDiscord} />
|
|
396
|
+
<span>Discord</span>
|
|
397
|
+
</label>
|
|
398
|
+
{/if}
|
|
399
|
+
{#if integrations.slackWebhookUrl}
|
|
400
|
+
<label class="notify-check-option">
|
|
401
|
+
<input type="checkbox" bind:checked={form.notifySlack} />
|
|
402
|
+
<span>Slack</span>
|
|
403
|
+
</label>
|
|
404
|
+
{/if}
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
{/if}
|
|
408
|
+
|
|
376
409
|
{#if formError}
|
|
377
410
|
<p class="form-error">{formError}</p>
|
|
378
411
|
{/if}
|
|
@@ -859,4 +892,29 @@
|
|
|
859
892
|
text-overflow: ellipsis;
|
|
860
893
|
max-width: 180px;
|
|
861
894
|
}
|
|
895
|
+
|
|
896
|
+
/* Notification checkboxes in modal */
|
|
897
|
+
.notify-checks {
|
|
898
|
+
display: flex;
|
|
899
|
+
flex-direction: row;
|
|
900
|
+
gap: 1rem;
|
|
901
|
+
padding: 0.375rem 0;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
.notify-check-option {
|
|
905
|
+
display: flex;
|
|
906
|
+
align-items: center;
|
|
907
|
+
gap: 0.5rem;
|
|
908
|
+
font-size: 0.8125rem;
|
|
909
|
+
color: var(--text);
|
|
910
|
+
cursor: pointer;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
.notify-check-option input[type='checkbox'] {
|
|
914
|
+
accent-color: var(--accent);
|
|
915
|
+
width: 13px;
|
|
916
|
+
height: 13px;
|
|
917
|
+
flex-shrink: 0;
|
|
918
|
+
cursor: pointer;
|
|
919
|
+
}
|
|
862
920
|
</style>
|
|
@@ -19,7 +19,14 @@
|
|
|
19
19
|
import { onMount } from 'svelte';
|
|
20
20
|
import { fly } from 'svelte/transition';
|
|
21
21
|
import { goto } from '$app/navigation';
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
fetchProject,
|
|
24
|
+
saveProject,
|
|
25
|
+
exportBackup,
|
|
26
|
+
importBackup,
|
|
27
|
+
fetchIntegrations,
|
|
28
|
+
saveIntegrations
|
|
29
|
+
} from '$lib/api/settings';
|
|
23
30
|
import {
|
|
24
31
|
fetchRunners,
|
|
25
32
|
createRunner,
|
|
@@ -43,7 +50,7 @@
|
|
|
43
50
|
import Toast from '$lib/components/ui/Toast.svelte';
|
|
44
51
|
import ConfirmModal from '$lib/components/ui/ConfirmModal.svelte';
|
|
45
52
|
|
|
46
|
-
/** @type {'project' | 'runners' | 'repository' | 'account' | 'users' | 'backup'} */
|
|
53
|
+
/** @type {'project' | 'runners' | 'repository' | 'integrations' | 'account' | 'users' | 'backup'} */
|
|
47
54
|
let section =
|
|
48
55
|
(typeof sessionStorage !== 'undefined' && sessionStorage.getItem('plum:settings:section')) ||
|
|
49
56
|
'project';
|
|
@@ -84,6 +91,9 @@
|
|
|
84
91
|
let exporting = false;
|
|
85
92
|
let fileInput;
|
|
86
93
|
|
|
94
|
+
let integrations = { discordWebhookUrl: '', slackWebhookUrl: '', notifyPublicUrl: '' };
|
|
95
|
+
let integrationsSaving = false;
|
|
96
|
+
|
|
87
97
|
let runners = [];
|
|
88
98
|
let runnerForm = { name: '', url: '', token: '', browser: 'chromium' };
|
|
89
99
|
let runnerFormError = '';
|
|
@@ -121,6 +131,9 @@
|
|
|
121
131
|
testSuitePrefix: prefixes.testSuitePrefix
|
|
122
132
|
};
|
|
123
133
|
} catch {}
|
|
134
|
+
try {
|
|
135
|
+
integrations = await fetchIntegrations();
|
|
136
|
+
} catch {}
|
|
124
137
|
if ($auth.user) {
|
|
125
138
|
profileForm = { name: $auth.user.name, email: $auth.user.email };
|
|
126
139
|
}
|
|
@@ -394,10 +407,23 @@
|
|
|
394
407
|
goto('/login');
|
|
395
408
|
}
|
|
396
409
|
|
|
410
|
+
async function handleSaveIntegrations() {
|
|
411
|
+
integrationsSaving = true;
|
|
412
|
+
try {
|
|
413
|
+
integrations = await saveIntegrations(integrations);
|
|
414
|
+
showToast('success', 'Integration settings saved.');
|
|
415
|
+
} catch {
|
|
416
|
+
showToast('error', 'Failed to save integration settings.');
|
|
417
|
+
} finally {
|
|
418
|
+
integrationsSaving = false;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
397
422
|
$: navItems = [
|
|
398
423
|
{ id: 'project', label: 'Project' },
|
|
399
424
|
{ id: 'runners', label: 'Runners' },
|
|
400
425
|
{ id: 'repository', label: 'Repository' },
|
|
426
|
+
{ id: 'integrations', label: 'Integrations' },
|
|
401
427
|
{ id: 'account', label: 'Account' },
|
|
402
428
|
...($auth.user?.role === 'admin' ? [{ id: 'users', label: 'Users' }] : []),
|
|
403
429
|
{ id: 'backup', label: 'Backup' }
|
|
@@ -811,6 +837,70 @@
|
|
|
811
837
|
</div>
|
|
812
838
|
</div>
|
|
813
839
|
|
|
840
|
+
<!-- INTEGRATIONS -->
|
|
841
|
+
{:else if section === 'integrations'}
|
|
842
|
+
<div class="content-section" transition:fly={{ y: 6, duration: 180 }}>
|
|
843
|
+
<div class="content-header">
|
|
844
|
+
<h2>Integrations</h2>
|
|
845
|
+
<p class="content-desc">
|
|
846
|
+
Connect Discord and Slack to receive run notifications with pass/fail results and report
|
|
847
|
+
links.
|
|
848
|
+
</p>
|
|
849
|
+
</div>
|
|
850
|
+
|
|
851
|
+
<div class="card settings-card">
|
|
852
|
+
<p class="card-title">Webhooks</p>
|
|
853
|
+
|
|
854
|
+
<div class="field">
|
|
855
|
+
<label class="field-label" for="discord-url">
|
|
856
|
+
<span>Discord Webhook URL</span>
|
|
857
|
+
<span class="field-hint">Leave blank to disable Discord notifications</span>
|
|
858
|
+
</label>
|
|
859
|
+
<input
|
|
860
|
+
id="discord-url"
|
|
861
|
+
type="url"
|
|
862
|
+
class="field-input"
|
|
863
|
+
bind:value={integrations.discordWebhookUrl}
|
|
864
|
+
placeholder="https://discord.com/api/webhooks/…"
|
|
865
|
+
/>
|
|
866
|
+
</div>
|
|
867
|
+
|
|
868
|
+
<div class="field">
|
|
869
|
+
<label class="field-label" for="slack-url">
|
|
870
|
+
<span>Slack Webhook URL</span>
|
|
871
|
+
<span class="field-hint">Leave blank to disable Slack notifications</span>
|
|
872
|
+
</label>
|
|
873
|
+
<input
|
|
874
|
+
id="slack-url"
|
|
875
|
+
type="url"
|
|
876
|
+
class="field-input"
|
|
877
|
+
bind:value={integrations.slackWebhookUrl}
|
|
878
|
+
placeholder="https://hooks.slack.com/services/…"
|
|
879
|
+
/>
|
|
880
|
+
</div>
|
|
881
|
+
|
|
882
|
+
<div class="field">
|
|
883
|
+
<label class="field-label" for="public-url">
|
|
884
|
+
<span>Public URL</span>
|
|
885
|
+
<span class="field-hint"
|
|
886
|
+
>Base URL of this Plum instance, used to link reports in notifications</span
|
|
887
|
+
>
|
|
888
|
+
</label>
|
|
889
|
+
<input
|
|
890
|
+
id="public-url"
|
|
891
|
+
type="url"
|
|
892
|
+
class="field-input"
|
|
893
|
+
bind:value={integrations.notifyPublicUrl}
|
|
894
|
+
placeholder="https://plum.yourcompany.com"
|
|
895
|
+
/>
|
|
896
|
+
</div>
|
|
897
|
+
|
|
898
|
+
<Button on:click={handleSaveIntegrations} disabled={integrationsSaving}>
|
|
899
|
+
{integrationsSaving ? 'Saving…' : 'Save Integrations'}
|
|
900
|
+
</Button>
|
|
901
|
+
</div>
|
|
902
|
+
</div>
|
|
903
|
+
|
|
814
904
|
<!-- ACCOUNT -->
|
|
815
905
|
{:else if section === 'account'}
|
|
816
906
|
<div class="content-section" transition:fly={{ y: 6, duration: 180 }}>
|