plum-e2e 1.0.1 → 1.0.3

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.
Files changed (43) hide show
  1. package/.prettierrc +15 -15
  2. package/README.md +5 -5
  3. package/backend/_scaffold/pages/LoginPage.js +22 -22
  4. package/backend/_scaffold/step_definitions/LoginSteps.js +9 -9
  5. package/backend/_scaffold/utils/constants.js +3 -3
  6. package/backend/_scaffold/utils/hooks.js +65 -65
  7. package/backend/_scaffold/utils/utils.js +10 -10
  8. package/backend/app.js +37 -37
  9. package/backend/config/scripts/create-settings.js +60 -60
  10. package/backend/config/scripts/generate-report.js +135 -135
  11. package/backend/config/scripts/run-tests.js +37 -37
  12. package/backend/cucumber.json +6 -6
  13. package/backend/package.json +29 -29
  14. package/backend/playwright.config.js +97 -97
  15. package/backend/routes/cron.routes.js +127 -127
  16. package/backend/routes/reports.routes.js +42 -42
  17. package/backend/routes/schedules.routes.js +32 -32
  18. package/backend/routes/tests.routes.js +33 -33
  19. package/backend/server.js +39 -39
  20. package/backend/services/cronService.js +127 -127
  21. package/backend/services/envService.js +43 -43
  22. package/backend/services/reportService.js +50 -50
  23. package/backend/services/scheduleService.js +34 -34
  24. package/backend/services/testService.js +70 -70
  25. package/backend/websockets/socketHandler.js +46 -46
  26. package/bin/plum.js +198 -198
  27. package/docker-compose.yml +41 -41
  28. package/frontend/jsconfig.json +13 -13
  29. package/frontend/package.json +26 -26
  30. package/frontend/postcss.config.js +23 -23
  31. package/frontend/src/app.css +35 -35
  32. package/frontend/src/app.html +28 -28
  33. package/frontend/src/lib/index.js +18 -18
  34. package/frontend/src/routes/+layout.svelte +34 -34
  35. package/frontend/src/routes/+page.svelte +188 -188
  36. package/frontend/src/routes/components/Navigation.svelte +53 -53
  37. package/frontend/src/routes/reports/+page.svelte +160 -160
  38. package/frontend/src/routes/scheduled-tests/+page.svelte +363 -363
  39. package/frontend/svelte.config.js +30 -30
  40. package/frontend/tailwind.config.js +44 -44
  41. package/frontend/vite.config.js +23 -23
  42. package/license-config.json +37 -37
  43. package/package.json +32 -28
@@ -1,363 +1,363 @@
1
- <!--
2
- * This file is part of Plum.
3
- *
4
- * Plum is free software: you can redistribute it and/or modify
5
- * it under the terms of the GNU General Public License as published by
6
- * the Free Software Foundation, either version 3 of the License, or
7
- * (at your option) any later version.
8
- *
9
- * Plum is distributed in the hope that it will be useful,
10
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
- * GNU General Public License for more details.
13
- *
14
- * You should have received a copy of the GNU General Public License
15
- * along with Plum. If not, see https://www.gnu.org/licenses/.
16
- -->
17
-
18
- <script>
19
- import { onMount } from 'svelte';
20
-
21
- let scheduledTests = [];
22
- let schedules = [];
23
- let cronExpression = '';
24
- let taskName = '';
25
- let tags = '';
26
- let successMessage = '';
27
- let errorMessage = '';
28
- let isModalOpen = false;
29
- let isEditing = false; // To track if we are editing a cron job
30
- let editTaskName = ''; // To store the task name of the cron job being edited
31
- let isDeleting = false; // To track if the user is confirming deletion
32
- let taskToDelete = ''; // The task name of the cron job being deleted
33
-
34
- const cronRegex =
35
- /^(\*|([0-5]?[0-9])) (\*|([01]?[0-9]|2[0-3])) (\*|([01]?[0-9]|3[01])) (\*|([1-9]|1[0-2])) (\*|[0-6])$/;
36
-
37
- // Fetch schedules from the settings.json through backend API
38
- async function fetchSchedules() {
39
- try {
40
- const res = await fetch('http://localhost:3001/schedules');
41
- const data = await res.json();
42
-
43
- if (data.schedules) {
44
- schedules = data.schedules;
45
- }
46
- } catch (error) {
47
- errorMessage = 'Error fetching schedules.';
48
- console.error('Error fetching schedules', error);
49
- }
50
- }
51
-
52
- // Fetch scheduled tests (cron jobs) from the backend API
53
- async function fetchScheduledTests() {
54
- try {
55
- const res = await fetch('http://localhost:3001/cron-jobs');
56
- const data = await res.json();
57
- if (data.cronJobs) {
58
- scheduledTests = data.cronJobs;
59
- }
60
- } catch (error) {
61
- errorMessage = 'Error fetching scheduled tests.';
62
- console.error('Error fetching scheduled tests:', error);
63
- }
64
- }
65
-
66
- // Add or edit a cron job via the API
67
- async function saveCronJob() {
68
- if (!cronExpression || !taskName || !tags) {
69
- errorMessage = 'Please fill in all fields';
70
- return;
71
- }
72
-
73
- // Validate cron expression
74
- if (!cronRegex.test(cronExpression)) {
75
- errorMessage = 'Invalid cron expression. Please use a valid cron format.';
76
- return;
77
- }
78
-
79
- // Lowercase " OR " while keeping other tags intact
80
- const formattedTags = tags.replace(/\sOR\s/gi, (match) => match.toLowerCase());
81
-
82
- try {
83
- let res;
84
- if (isEditing) {
85
- // Edit cron job
86
- res = await fetch(`http://localhost:3001/cron-jobs/${editTaskName}`, {
87
- method: 'PUT',
88
- headers: {
89
- 'Content-Type': 'application/json'
90
- },
91
- body: JSON.stringify({ cronExpression, taskName, tags: formattedTags })
92
- });
93
- } else {
94
- // Add new cron job
95
- res = await fetch('http://localhost:3001/cron-jobs', {
96
- method: 'POST',
97
- headers: {
98
- 'Content-Type': 'application/json'
99
- },
100
- body: JSON.stringify({ cronExpression, taskName, tags: formattedTags })
101
- });
102
- }
103
-
104
- const data = await res.json();
105
- if (data.message) {
106
- successMessage = `Cron job ${isEditing ? 'updated' : 'added'}: ${data.message}`;
107
- fetchScheduledTests(); // Refresh the list
108
- setTimeout(() => {
109
- successMessage = ''; // Clear success message after 5 seconds
110
- }, 5000);
111
- clearModal();
112
- } else {
113
- errorMessage = 'Failed to save cron job';
114
- }
115
- } catch (error) {
116
- errorMessage = 'Error saving cron job.';
117
- console.error('Error saving cron job:', error);
118
- }
119
- }
120
-
121
- // Delete a cron job
122
- async function deleteCronJob(taskName) {
123
- try {
124
- const res = await fetch(`http://localhost:3001/cron-jobs/${taskName}`, {
125
- method: 'DELETE'
126
- });
127
- const data = await res.json();
128
- if (data.message) {
129
- successMessage = `Cron job deleted: ${data.message}`;
130
- // Refresh the list
131
- fetchScheduledTests();
132
- setTimeout(() => {
133
- successMessage = '';
134
- }, 5000);
135
- } else {
136
- errorMessage = 'Failed to delete cron job';
137
- setTimeout(() => {
138
- errorMessage = '';
139
- }, 5000);
140
- }
141
- } catch (error) {
142
- errorMessage = 'Error deleting cron job.';
143
- console.error('Error deleting cron job:', error);
144
- }
145
- }
146
-
147
- // Open modal to edit cron job
148
- function openEditModal(cronJob) {
149
- isEditing = true;
150
- editTaskName = cronJob.taskName;
151
- taskName = cronJob.taskName;
152
- cronExpression = cronJob.cronExpression;
153
- tags = cronJob.tags;
154
- isModalOpen = true;
155
- }
156
-
157
- // Reset modal values
158
- function clearModal() {
159
- isEditing = false;
160
- editTaskName = '';
161
- taskName = '';
162
- cronExpression = '';
163
- tags = '';
164
- isModalOpen = false;
165
- }
166
-
167
- // Open confirmation modal for deleting cron job
168
- function openDeleteConfirmation(taskName) {
169
- isDeleting = true;
170
- taskToDelete = taskName;
171
- }
172
-
173
- // Confirm deletion
174
- function confirmDelete() {
175
- deleteCronJob(taskToDelete);
176
- isDeleting = false;
177
- taskToDelete = '';
178
- }
179
-
180
- // Cancel deletion
181
- function cancelDelete() {
182
- isDeleting = false;
183
- taskToDelete = '';
184
- }
185
-
186
- // Get human-readable label for cron expression
187
- function getCronLabel(cronExpression) {
188
- const match = schedules.find((option) => option.value === cronExpression);
189
- return match ? match.label : cronExpression;
190
- }
191
-
192
- onMount(() => {
193
- fetchScheduledTests();
194
- fetchSchedules(); // <-- also call this here
195
- });
196
- </script>
197
-
198
- <!-- Add/Modify modal -->
199
- <dialog id="my_modal_1" class="modal" class:modal-open={isModalOpen}>
200
- <div class="modal-box">
201
- <div class="modal-header">
202
- <button class="btn btn-sm btn-circle z-10 absolute right-2 top-2" on:click={clearModal}>
203
-
204
- </button>
205
- </div>
206
-
207
- <h2 class="card-title mb-4">{isEditing ? 'Edit' : 'Add New'} Cron Job</h2>
208
-
209
- <form on:submit|preventDefault={saveCronJob} class="space-y-4">
210
- <div>
211
- <div for="taskName" class="label">
212
- <span class="label-text">Task Name</span>
213
- <span class="label-text-alt"
214
- >{isEditing
215
- ? 'Name is used as ID. This cannot be changed.'
216
- : 'Use a meaningful name'}</span
217
- >
218
- </div>
219
- <input
220
- type="text"
221
- id="taskName"
222
- bind:value={taskName}
223
- class="input input-bordered w-full"
224
- placeholder="Task Name"
225
- required
226
- disabled={isEditing}
227
- />
228
- </div>
229
- <div>
230
- <div for="cronExpression" class="label">
231
- <span class="label-text">Cron Expression</span>
232
- <span class="label-text-alt">You can use AI to construct your expression.</span>
233
- </div>
234
- <select
235
- id="cronExpression"
236
- bind:value={cronExpression}
237
- class="input input-bordered w-full"
238
- required
239
- >
240
- <option value="" disabled selected>Select Cron Expression</option>
241
- {#each schedules as schedule}
242
- <option value={schedule.value}>{schedule.label}</option>
243
- {/each}
244
- </select>
245
- </div>
246
- <div>
247
- <div for="tags" class="label">
248
- <span class="label-text">Tags</span>
249
- <span class="label-text-alt">If using multiple tags, "@test-1 or @test-2".</span>
250
- </div>
251
- <input
252
- type="text"
253
- id="tags"
254
- bind:value={tags}
255
- class="input input-bordered w-full"
256
- placeholder="Tags"
257
- required
258
- />
259
- </div>
260
- <button type="submit" class="btn btn-success w-full">
261
- {isEditing ? 'Edit' : 'Add'} Cron Job
262
- </button>
263
- </form>
264
- </div>
265
- </dialog>
266
-
267
- <!-- Delete Confirmation Modal -->
268
- <dialog id="delete_confirmation_modal" class="modal" class:modal-open={isDeleting}>
269
- <div class="modal-box">
270
- <h2 class="card-title mb-4">Are you sure you want to delete this cron job?</h2>
271
- <div class="flex justify-center gap-4">
272
- <button class="btn btn-error" on:click={confirmDelete}>Yes</button>
273
- <button class="btn" on:click={cancelDelete}>No</button>
274
- </div>
275
- </div>
276
- </dialog>
277
-
278
- <div class="flex justify-center items-center w-full my-4">
279
- <div class="card bg-base-300 rounded-box p-4">
280
- <div class="card-body text-left">
281
- <h2 class="card-title sticky top-0 bg-base-300 z-10">Scheduled Tests</h2>
282
- {#if successMessage}
283
- <div role="alert" class="alert alert-success z-0">
284
- <svg
285
- xmlns="http://www.w3.org/2000/svg"
286
- class="h-6 w-6 shrink-0 stroke-current"
287
- fill="none"
288
- viewBox="0 0 24 24"
289
- >
290
- <path
291
- stroke-linecap="round"
292
- stroke-linejoin="round"
293
- stroke-width="2"
294
- d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
295
- />
296
- </svg>
297
- <span>{successMessage}</span>
298
- </div>
299
- {/if}
300
-
301
- {#if errorMessage}
302
- <div role="alert" class="alert alert-error">
303
- <svg
304
- xmlns="http://www.w3.org/2000/svg"
305
- class="h-6 w-6 shrink-0 stroke-current"
306
- fill="none"
307
- viewBox="0 0 24 24"
308
- >
309
- <path
310
- stroke-linecap="round"
311
- stroke-linejoin="round"
312
- stroke-width="2"
313
- d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
314
- />
315
- </svg>
316
- <span>{errorMessage}</span>
317
- </div>
318
- {/if}
319
- <div class="mt-4">
320
- {#if scheduledTests.length > 0}
321
- <div class="overflow-x-auto">
322
- <table class="table table-zebra">
323
- <thead>
324
- <tr>
325
- <th>Name</th>
326
- <th>Frequency</th>
327
- <th>Tags</th>
328
- <th>Action</th>
329
- </tr>
330
- </thead>
331
- <tbody>
332
- {#each scheduledTests as scheduledTest}
333
- <tr>
334
- <td>{scheduledTest.taskName}</td>
335
- <td>{getCronLabel(scheduledTest.cronExpression)}</td>
336
- <td>{scheduledTest.tags}</td>
337
- <td>
338
- <button
339
- class="btn btn-warning btn-sm"
340
- on:click={() => openEditModal(scheduledTest)}
341
- >
342
- Edit
343
- </button>
344
- <button
345
- class="btn btn-error btn-sm"
346
- on:click={() => openDeleteConfirmation(scheduledTest.taskName)}
347
- >
348
- Delete
349
- </button>
350
- </td>
351
- </tr>
352
- {/each}
353
- </tbody>
354
- </table>
355
- </div>
356
- {:else}
357
- <p>No Scheduled Tests Available.</p>
358
- {/if}
359
- </div>
360
- <button class="btn mt-2" on:click={() => (isModalOpen = true)}>Add New Cron Job</button>
361
- </div>
362
- </div>
363
- </div>
1
+ <!--
2
+ * This file is part of Plum.
3
+ *
4
+ * Plum is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * Plum is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
16
+ -->
17
+
18
+ <script>
19
+ import { onMount } from 'svelte';
20
+
21
+ let scheduledTests = [];
22
+ let schedules = [];
23
+ let cronExpression = '';
24
+ let taskName = '';
25
+ let tags = '';
26
+ let successMessage = '';
27
+ let errorMessage = '';
28
+ let isModalOpen = false;
29
+ let isEditing = false; // To track if we are editing a cron job
30
+ let editTaskName = ''; // To store the task name of the cron job being edited
31
+ let isDeleting = false; // To track if the user is confirming deletion
32
+ let taskToDelete = ''; // The task name of the cron job being deleted
33
+
34
+ const cronRegex =
35
+ /^(\*|([0-5]?[0-9])) (\*|([01]?[0-9]|2[0-3])) (\*|([01]?[0-9]|3[01])) (\*|([1-9]|1[0-2])) (\*|[0-6])$/;
36
+
37
+ // Fetch schedules from the settings.json through backend API
38
+ async function fetchSchedules() {
39
+ try {
40
+ const res = await fetch('http://localhost:3001/schedules');
41
+ const data = await res.json();
42
+
43
+ if (data.schedules) {
44
+ schedules = data.schedules;
45
+ }
46
+ } catch (error) {
47
+ errorMessage = 'Error fetching schedules.';
48
+ console.error('Error fetching schedules', error);
49
+ }
50
+ }
51
+
52
+ // Fetch scheduled tests (cron jobs) from the backend API
53
+ async function fetchScheduledTests() {
54
+ try {
55
+ const res = await fetch('http://localhost:3001/cron-jobs');
56
+ const data = await res.json();
57
+ if (data.cronJobs) {
58
+ scheduledTests = data.cronJobs;
59
+ }
60
+ } catch (error) {
61
+ errorMessage = 'Error fetching scheduled tests.';
62
+ console.error('Error fetching scheduled tests:', error);
63
+ }
64
+ }
65
+
66
+ // Add or edit a cron job via the API
67
+ async function saveCronJob() {
68
+ if (!cronExpression || !taskName || !tags) {
69
+ errorMessage = 'Please fill in all fields';
70
+ return;
71
+ }
72
+
73
+ // Validate cron expression
74
+ if (!cronRegex.test(cronExpression)) {
75
+ errorMessage = 'Invalid cron expression. Please use a valid cron format.';
76
+ return;
77
+ }
78
+
79
+ // Lowercase " OR " while keeping other tags intact
80
+ const formattedTags = tags.replace(/\sOR\s/gi, (match) => match.toLowerCase());
81
+
82
+ try {
83
+ let res;
84
+ if (isEditing) {
85
+ // Edit cron job
86
+ res = await fetch(`http://localhost:3001/cron-jobs/${editTaskName}`, {
87
+ method: 'PUT',
88
+ headers: {
89
+ 'Content-Type': 'application/json'
90
+ },
91
+ body: JSON.stringify({ cronExpression, taskName, tags: formattedTags })
92
+ });
93
+ } else {
94
+ // Add new cron job
95
+ res = await fetch('http://localhost:3001/cron-jobs', {
96
+ method: 'POST',
97
+ headers: {
98
+ 'Content-Type': 'application/json'
99
+ },
100
+ body: JSON.stringify({ cronExpression, taskName, tags: formattedTags })
101
+ });
102
+ }
103
+
104
+ const data = await res.json();
105
+ if (data.message) {
106
+ successMessage = `Cron job ${isEditing ? 'updated' : 'added'}: ${data.message}`;
107
+ fetchScheduledTests(); // Refresh the list
108
+ setTimeout(() => {
109
+ successMessage = ''; // Clear success message after 5 seconds
110
+ }, 5000);
111
+ clearModal();
112
+ } else {
113
+ errorMessage = 'Failed to save cron job';
114
+ }
115
+ } catch (error) {
116
+ errorMessage = 'Error saving cron job.';
117
+ console.error('Error saving cron job:', error);
118
+ }
119
+ }
120
+
121
+ // Delete a cron job
122
+ async function deleteCronJob(taskName) {
123
+ try {
124
+ const res = await fetch(`http://localhost:3001/cron-jobs/${taskName}`, {
125
+ method: 'DELETE'
126
+ });
127
+ const data = await res.json();
128
+ if (data.message) {
129
+ successMessage = `Cron job deleted: ${data.message}`;
130
+ // Refresh the list
131
+ fetchScheduledTests();
132
+ setTimeout(() => {
133
+ successMessage = '';
134
+ }, 5000);
135
+ } else {
136
+ errorMessage = 'Failed to delete cron job';
137
+ setTimeout(() => {
138
+ errorMessage = '';
139
+ }, 5000);
140
+ }
141
+ } catch (error) {
142
+ errorMessage = 'Error deleting cron job.';
143
+ console.error('Error deleting cron job:', error);
144
+ }
145
+ }
146
+
147
+ // Open modal to edit cron job
148
+ function openEditModal(cronJob) {
149
+ isEditing = true;
150
+ editTaskName = cronJob.taskName;
151
+ taskName = cronJob.taskName;
152
+ cronExpression = cronJob.cronExpression;
153
+ tags = cronJob.tags;
154
+ isModalOpen = true;
155
+ }
156
+
157
+ // Reset modal values
158
+ function clearModal() {
159
+ isEditing = false;
160
+ editTaskName = '';
161
+ taskName = '';
162
+ cronExpression = '';
163
+ tags = '';
164
+ isModalOpen = false;
165
+ }
166
+
167
+ // Open confirmation modal for deleting cron job
168
+ function openDeleteConfirmation(taskName) {
169
+ isDeleting = true;
170
+ taskToDelete = taskName;
171
+ }
172
+
173
+ // Confirm deletion
174
+ function confirmDelete() {
175
+ deleteCronJob(taskToDelete);
176
+ isDeleting = false;
177
+ taskToDelete = '';
178
+ }
179
+
180
+ // Cancel deletion
181
+ function cancelDelete() {
182
+ isDeleting = false;
183
+ taskToDelete = '';
184
+ }
185
+
186
+ // Get human-readable label for cron expression
187
+ function getCronLabel(cronExpression) {
188
+ const match = schedules.find((option) => option.value === cronExpression);
189
+ return match ? match.label : cronExpression;
190
+ }
191
+
192
+ onMount(() => {
193
+ fetchScheduledTests();
194
+ fetchSchedules(); // <-- also call this here
195
+ });
196
+ </script>
197
+
198
+ <!-- Add/Modify modal -->
199
+ <dialog id="my_modal_1" class="modal" class:modal-open={isModalOpen}>
200
+ <div class="modal-box">
201
+ <div class="modal-header">
202
+ <button class="btn btn-sm btn-circle z-10 absolute right-2 top-2" on:click={clearModal}>
203
+
204
+ </button>
205
+ </div>
206
+
207
+ <h2 class="card-title mb-4">{isEditing ? 'Edit' : 'Add New'} Cron Job</h2>
208
+
209
+ <form on:submit|preventDefault={saveCronJob} class="space-y-4">
210
+ <div>
211
+ <div for="taskName" class="label">
212
+ <span class="label-text">Task Name</span>
213
+ <span class="label-text-alt"
214
+ >{isEditing
215
+ ? 'Name is used as ID. This cannot be changed.'
216
+ : 'Use a meaningful name'}</span
217
+ >
218
+ </div>
219
+ <input
220
+ type="text"
221
+ id="taskName"
222
+ bind:value={taskName}
223
+ class="input input-bordered w-full"
224
+ placeholder="Task Name"
225
+ required
226
+ disabled={isEditing}
227
+ />
228
+ </div>
229
+ <div>
230
+ <div for="cronExpression" class="label">
231
+ <span class="label-text">Cron Expression</span>
232
+ <span class="label-text-alt">You can use AI to construct your expression.</span>
233
+ </div>
234
+ <select
235
+ id="cronExpression"
236
+ bind:value={cronExpression}
237
+ class="input input-bordered w-full"
238
+ required
239
+ >
240
+ <option value="" disabled selected>Select Cron Expression</option>
241
+ {#each schedules as schedule}
242
+ <option value={schedule.value}>{schedule.label}</option>
243
+ {/each}
244
+ </select>
245
+ </div>
246
+ <div>
247
+ <div for="tags" class="label">
248
+ <span class="label-text">Tags</span>
249
+ <span class="label-text-alt">If using multiple tags, "@test-1 or @test-2".</span>
250
+ </div>
251
+ <input
252
+ type="text"
253
+ id="tags"
254
+ bind:value={tags}
255
+ class="input input-bordered w-full"
256
+ placeholder="Tags"
257
+ required
258
+ />
259
+ </div>
260
+ <button type="submit" class="btn btn-success w-full">
261
+ {isEditing ? 'Edit' : 'Add'} Cron Job
262
+ </button>
263
+ </form>
264
+ </div>
265
+ </dialog>
266
+
267
+ <!-- Delete Confirmation Modal -->
268
+ <dialog id="delete_confirmation_modal" class="modal" class:modal-open={isDeleting}>
269
+ <div class="modal-box">
270
+ <h2 class="card-title mb-4">Are you sure you want to delete this cron job?</h2>
271
+ <div class="flex justify-center gap-4">
272
+ <button class="btn btn-error" on:click={confirmDelete}>Yes</button>
273
+ <button class="btn" on:click={cancelDelete}>No</button>
274
+ </div>
275
+ </div>
276
+ </dialog>
277
+
278
+ <div class="flex justify-center items-center w-full my-4">
279
+ <div class="card bg-base-300 rounded-box p-4">
280
+ <div class="card-body text-left">
281
+ <h2 class="card-title sticky top-0 bg-base-300 z-10">Scheduled Tests</h2>
282
+ {#if successMessage}
283
+ <div role="alert" class="alert alert-success z-0">
284
+ <svg
285
+ xmlns="http://www.w3.org/2000/svg"
286
+ class="h-6 w-6 shrink-0 stroke-current"
287
+ fill="none"
288
+ viewBox="0 0 24 24"
289
+ >
290
+ <path
291
+ stroke-linecap="round"
292
+ stroke-linejoin="round"
293
+ stroke-width="2"
294
+ d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
295
+ />
296
+ </svg>
297
+ <span>{successMessage}</span>
298
+ </div>
299
+ {/if}
300
+
301
+ {#if errorMessage}
302
+ <div role="alert" class="alert alert-error">
303
+ <svg
304
+ xmlns="http://www.w3.org/2000/svg"
305
+ class="h-6 w-6 shrink-0 stroke-current"
306
+ fill="none"
307
+ viewBox="0 0 24 24"
308
+ >
309
+ <path
310
+ stroke-linecap="round"
311
+ stroke-linejoin="round"
312
+ stroke-width="2"
313
+ d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
314
+ />
315
+ </svg>
316
+ <span>{errorMessage}</span>
317
+ </div>
318
+ {/if}
319
+ <div class="mt-4">
320
+ {#if scheduledTests.length > 0}
321
+ <div class="overflow-x-auto">
322
+ <table class="table table-zebra">
323
+ <thead>
324
+ <tr>
325
+ <th>Name</th>
326
+ <th>Frequency</th>
327
+ <th>Tags</th>
328
+ <th>Action</th>
329
+ </tr>
330
+ </thead>
331
+ <tbody>
332
+ {#each scheduledTests as scheduledTest}
333
+ <tr>
334
+ <td>{scheduledTest.taskName}</td>
335
+ <td>{getCronLabel(scheduledTest.cronExpression)}</td>
336
+ <td>{scheduledTest.tags}</td>
337
+ <td>
338
+ <button
339
+ class="btn btn-warning btn-sm"
340
+ on:click={() => openEditModal(scheduledTest)}
341
+ >
342
+ Edit
343
+ </button>
344
+ <button
345
+ class="btn btn-error btn-sm"
346
+ on:click={() => openDeleteConfirmation(scheduledTest.taskName)}
347
+ >
348
+ Delete
349
+ </button>
350
+ </td>
351
+ </tr>
352
+ {/each}
353
+ </tbody>
354
+ </table>
355
+ </div>
356
+ {:else}
357
+ <p>No Scheduled Tests Available.</p>
358
+ {/if}
359
+ </div>
360
+ <button class="btn mt-2" on:click={() => (isModalOpen = true)}>Add New Cron Job</button>
361
+ </div>
362
+ </div>
363
+ </div>