feat(monitors,workergroups): design for deletion and stauts

This commit is contained in:
Tine 2024-02-28 12:06:24 +01:00
parent 306f583418
commit d9dafd766a
Signed by: mentos1386
SSH key fingerprint: SHA256:MNtTsLbihYaWF8j1fkOHfkKNlnN1JQfxEU/rBU8nCGw
8 changed files with 236 additions and 52 deletions

View file

@ -28,15 +28,20 @@ type UpdateMonitor struct {
Script string `validate:"required"` Script string `validate:"required"`
} }
type MonitorWithWorkerGroupsAndStatus struct {
*models.MonitorWithWorkerGroups
Status services.MonitorStatus
}
type SettingsMonitors struct { type SettingsMonitors struct {
*Settings *Settings
Monitors []*models.MonitorWithWorkerGroups Monitors []*MonitorWithWorkerGroupsAndStatus
MonitorsLength int MonitorsLength int
} }
type SettingsMonitor struct { type SettingsMonitor struct {
*Settings *Settings
Monitor *models.MonitorWithWorkerGroups Monitor *MonitorWithWorkerGroupsAndStatus
History []*models.MonitorHistory History []*models.MonitorHistory
} }
@ -48,14 +53,26 @@ func (h *BaseHandler) SettingsMonitorsGET(c echo.Context) error {
return err return err
} }
monitorsWithStatus := make([]*MonitorWithWorkerGroupsAndStatus, len(monitors))
for i, monitor := range monitors {
status, err := services.GetMonitorStatus(context.Background(), h.temporal, monitor.Slug)
if err != nil {
return err
}
monitorsWithStatus[i] = &MonitorWithWorkerGroupsAndStatus{
MonitorWithWorkerGroups: monitor,
Status: status,
}
}
return c.Render(http.StatusOK, "settings_monitors.tmpl", &SettingsMonitors{ return c.Render(http.StatusOK, "settings_monitors.tmpl", &SettingsMonitors{
Settings: NewSettings( Settings: NewSettings(
cc.Principal.User, cc.Principal.User,
GetPageByTitle(SettingsPages, "Monitors"), GetPageByTitle(SettingsPages, "Monitors"),
[]*components.Page{GetPageByTitle(SettingsPages, "Monitors")}, []*components.Page{GetPageByTitle(SettingsPages, "Monitors")},
), ),
Monitors: monitors, Monitors: monitorsWithStatus,
MonitorsLength: len(monitors), MonitorsLength: len(monitorsWithStatus),
}) })
} }
@ -69,6 +86,16 @@ func (h *BaseHandler) SettingsMonitorsDescribeGET(c echo.Context) error {
return err return err
} }
status, err := services.GetMonitorStatus(context.Background(), h.temporal, monitor.Slug)
if err != nil {
return err
}
monitorWithStatus := &MonitorWithWorkerGroupsAndStatus{
MonitorWithWorkerGroups: monitor,
Status: status,
}
history, err := services.GetMonitorHistoryForMonitor(context.Background(), h.db, slug) history, err := services.GetMonitorHistoryForMonitor(context.Background(), h.db, slug)
if err != nil { if err != nil {
return err return err
@ -91,7 +118,7 @@ func (h *BaseHandler) SettingsMonitorsDescribeGET(c echo.Context) error {
Breadcrumb: monitor.Name, Breadcrumb: monitor.Name,
}, },
}), }),
Monitor: monitor, Monitor: monitorWithStatus,
History: history[:maxElements], History: history[:maxElements],
}) })
} }

View file

@ -13,8 +13,31 @@ import (
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
) )
func getScheduleId(monitor *models.Monitor) string { type MonitorStatus string
return "monitor-" + monitor.Slug
const (
MonitorStatusUnknown MonitorStatus = "UNKNOWN"
MonitorStatusPaused MonitorStatus = "PAUSED"
MonitorStatusActive MonitorStatus = "ACTIVE"
)
func getScheduleId(slug string) string {
return "monitor-" + slug
}
func GetMonitorStatus(ctx context.Context, temporal client.Client, slug string) (MonitorStatus, error) {
schedule := temporal.ScheduleClient().GetHandle(ctx, getScheduleId(slug))
description, err := schedule.Describe(ctx)
if err != nil {
return MonitorStatusUnknown, err
}
if description.Schedule.State.Paused {
return MonitorStatusPaused, nil
}
return MonitorStatusActive, nil
} }
func CreateMonitor(ctx context.Context, db *sqlx.DB, monitor *models.Monitor) error { func CreateMonitor(ctx context.Context, db *sqlx.DB, monitor *models.Monitor) error {
@ -202,13 +225,13 @@ func CreateOrUpdateMonitorSchedule(
} }
options := client.ScheduleOptions{ options := client.ScheduleOptions{
ID: getScheduleId(monitor), ID: getScheduleId(monitor.Slug),
Spec: client.ScheduleSpec{ Spec: client.ScheduleSpec{
CronExpressions: []string{monitor.Schedule}, CronExpressions: []string{monitor.Schedule},
Jitter: time.Second * 10, Jitter: time.Second * 10,
}, },
Action: &client.ScheduleWorkflowAction{ Action: &client.ScheduleWorkflowAction{
ID: getScheduleId(monitor), ID: getScheduleId(monitor.Slug),
Workflow: workflows.NewWorkflows(nil).MonitorWorkflowDefinition, Workflow: workflows.NewWorkflows(nil).MonitorWorkflowDefinition,
Args: args, Args: args,
TaskQueue: "default", TaskQueue: "default",
@ -218,7 +241,7 @@ func CreateOrUpdateMonitorSchedule(
}, },
} }
schedule := t.ScheduleClient().GetHandle(ctx, getScheduleId(monitor)) schedule := t.ScheduleClient().GetHandle(ctx, getScheduleId(monitor.Slug))
// If exists, we update // If exists, we update
_, err := schedule.Describe(ctx) _, err := schedule.Describe(ctx)

View file

@ -80,14 +80,11 @@ code {
@apply grid grid-cols-1 gap-5 grid-rows-[min-content] h-fit; @apply grid grid-cols-1 gap-5 grid-rows-[min-content] h-fit;
} }
.settings section { .settings section {
@apply relative overflow-x-auto shadow-md sm:rounded-lg text-gray-500 bg-white; @apply relative overflow-x-auto shadow-md sm:rounded-lg text-gray-500 bg-white h-min;
} }
.settings section h2 { .settings section h2 {
@apply flex flex-col text-lg font-semibold text-gray-900; @apply text-lg font-semibold text-gray-900;
}
.settings section h2 span {
@apply text-sm font-medium text-gray-500;
} }
.settings section form { .settings section form {

View file

@ -705,6 +705,16 @@ video {
height: 24rem; height: 24rem;
} }
.h-fit {
height: -moz-fit-content;
height: fit-content;
}
.h-min {
height: -moz-min-content;
height: min-content;
}
.w-20 { .w-20 {
width: 5rem; width: 5rem;
} }
@ -721,6 +731,11 @@ video {
width: 1.25rem; width: 1.25rem;
} }
.w-fit {
width: -moz-fit-content;
width: fit-content;
}
.w-full { .w-full {
width: 100%; width: 100%;
} }
@ -737,6 +752,10 @@ video {
max-width: 1280px; max-width: 1280px;
} }
.flex-1 {
flex: 1 1 0%;
}
.flex-auto { .flex-auto {
flex: 1 1 auto; flex: 1 1 auto;
} }
@ -753,6 +772,10 @@ video {
grid-template-columns: auto min-content; grid-template-columns: auto min-content;
} }
.flex-row {
flex-direction: row;
}
.flex-col { .flex-col {
flex-direction: column; flex-direction: column;
} }
@ -791,6 +814,10 @@ video {
margin-bottom: calc(1rem * var(--tw-space-y-reverse)); margin-bottom: calc(1rem * var(--tw-space-y-reverse));
} }
.self-center {
align-self: center;
}
.justify-self-start { .justify-self-start {
justify-self: start; justify-self: start;
} }
@ -839,11 +866,25 @@ video {
border-width: 1px; border-width: 1px;
} }
.border-4 {
border-width: 4px;
}
.border-gray-200 {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
}
.border-gray-300 { .border-gray-300 {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity)); border-color: rgb(209 213 219 / var(--tw-border-opacity));
} }
.border-red-300 {
--tw-border-opacity: 1;
border-color: rgb(252 165 165 / var(--tw-border-opacity));
}
.bg-blue-100 { .bg-blue-100 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(219 234 254 / var(--tw-bg-opacity)); background-color: rgb(219 234 254 / var(--tw-bg-opacity));
@ -899,6 +940,11 @@ video {
background-color: rgb(248 113 113 / var(--tw-bg-opacity)); background-color: rgb(248 113 113 / var(--tw-bg-opacity));
} }
.bg-red-700 {
--tw-bg-opacity: 1;
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
}
.bg-white { .bg-white {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity)); background-color: rgb(255 255 255 / var(--tw-bg-opacity));
@ -945,6 +991,16 @@ video {
padding-bottom: 0.25rem; padding-bottom: 0.25rem;
} }
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.py-2\.5 {
padding-top: 0.625rem;
padding-bottom: 0.625rem;
}
.py-3 { .py-3 {
padding-top: 0.75rem; padding-top: 0.75rem;
padding-bottom: 0.75rem; padding-bottom: 0.75rem;
@ -1342,6 +1398,8 @@ code {
.settings section { .settings section {
position: relative; position: relative;
height: -moz-min-content;
height: min-content;
overflow-x: auto; overflow-x: auto;
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity)); background-color: rgb(255 255 255 / var(--tw-bg-opacity));
@ -1359,8 +1417,6 @@ code {
} }
.settings section h2 { .settings section h2 {
display: flex;
flex-direction: column;
font-size: 1.125rem; font-size: 1.125rem;
line-height: 1.75rem; line-height: 1.75rem;
font-weight: 600; font-weight: 600;
@ -1368,14 +1424,6 @@ code {
color: rgb(17 24 39 / var(--tw-text-opacity)); color: rgb(17 24 39 / var(--tw-text-opacity));
} }
.settings section h2 span {
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
--tw-text-opacity: 1;
color: rgb(107 114 128 / var(--tw-text-opacity));
}
.settings section form { .settings section form {
display: grid; display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr)); grid-template-columns: repeat(1, minmax(0, 1fr));
@ -1562,6 +1610,11 @@ code {
background-color: rgb(30 64 175 / var(--tw-bg-opacity)); background-color: rgb(30 64 175 / var(--tw-bg-opacity));
} }
.hover\:bg-gray-100:hover {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
}
.hover\:bg-gray-500:hover { .hover\:bg-gray-500:hover {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(107 114 128 / var(--tw-bg-opacity)); background-color: rgb(107 114 128 / var(--tw-bg-opacity));
@ -1577,15 +1630,29 @@ code {
background-color: rgb(239 68 68 / var(--tw-bg-opacity)); background-color: rgb(239 68 68 / var(--tw-bg-opacity));
} }
.hover\:bg-red-800:hover {
--tw-bg-opacity: 1;
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
}
.hover\:text-blue-600:hover { .hover\:text-blue-600:hover {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(37 99 235 / var(--tw-text-opacity)); color: rgb(37 99 235 / var(--tw-text-opacity));
} }
.hover\:text-blue-700:hover {
--tw-text-opacity: 1;
color: rgb(29 78 216 / var(--tw-text-opacity));
}
.hover\:underline:hover { .hover\:underline:hover {
text-decoration-line: underline; text-decoration-line: underline;
} }
.focus\:z-10:focus {
z-index: 10;
}
.focus\:outline-none:focus { .focus\:outline-none:focus {
outline: 2px solid transparent; outline: 2px solid transparent;
outline-offset: 2px; outline-offset: 2px;
@ -1602,6 +1669,16 @@ code {
--tw-ring-color: rgb(147 197 253 / var(--tw-ring-opacity)); --tw-ring-color: rgb(147 197 253 / var(--tw-ring-opacity));
} }
.focus\:ring-gray-100:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(243 244 246 / var(--tw-ring-opacity));
}
.focus\:ring-red-300:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(252 165 165 / var(--tw-ring-opacity));
}
@media (min-width: 640px) { @media (min-width: 640px) {
.sm\:ml-2 { .sm\:ml-2 {
margin-left: 0.5rem; margin-left: 0.5rem;
@ -1648,6 +1725,10 @@ code {
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.md\:flex-row {
flex-direction: row;
}
.md\:px-40 { .md\:px-40 {
padding-left: 10rem; padding-left: 10rem;
padding-right: 10rem; padding-right: 10rem;

View file

@ -65,12 +65,19 @@
{{end}} {{end}}
</td> </td>
<td> <td>
{{ if eq .Status "ACTIVE" }}
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"> <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
ACTIVE ACTIVE
</span> </span>
{{ else if eq .Status "PAUSED" }}
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800"> <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
PAUSED PAUSED
</span> </span>
{{ else }}
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">
UNKNOWN
</span>
{{ end }}
</td> </td>
<td> <td>
{{.Schedule}} {{.Schedule}}

View file

@ -8,7 +8,7 @@
<input type="text" name="workergroups" id="workergroups" placeholder="NA EU"/> <input type="text" name="workergroups" id="workergroups" placeholder="NA EU"/>
<p>Worker groups are used to distribute the monitor to specific workers.</p> <p>Worker groups are used to distribute the monitor to specific workers.</p>
<label for="schedule">Schedule</label> <label for="schedule">Schedule</label>
<input type="text" name="schedule" id="schedule" placeholder="@every 1m"/> <input type="text" name="schedule" id="schedule" placeholder="@every 1m" value="@every 1m"/>
<p> <p>
Schedule is a cron expression that defines when the monitor should be executed. Schedule is a cron expression that defines when the monitor should be executed.
</br> </br>
@ -41,8 +41,6 @@ export const options = {
// http errors should be less than 1% // http errors should be less than 1%
http_req_failed: ['rate<0.01'], http_req_failed: ['rate<0.01'],
}, },
vus: 10,
duration: '5s',
}; };
export default function () { export default function () {

View file

@ -26,6 +26,34 @@
</form> </form>
</section> </section>
<div class="flex md:flex-row flex-col gap-4 h-min">
<section class="p-5 flex-1">
<h2 class="mb-2 flex flex-row gap-2">
Status
{{ if eq .Monitor.Status "ACTIVE" }}
<span class="self-center h-fit w-fit px-2 text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
ACTIVE
</span>
{{ else if eq .Monitor.Status "PAUSED" }}
<span class="self-center h-fit w-fit px-2 text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
PAUSED
</span>
{{ end }}
</h2>
<p class="text-sm mb-2">Pausing the monitor will stop it from executing. This can be useful in cases of expected downtime.</p>
<a class="block text-center py-2.5 px-5 me-2 mb-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100" href="/settings/monitors/{{ .Monitor.Slug }}/enable">Pause</a>
<a class="block text-center py-2.5 px-5 me-2 mb-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100" href="/settings/monitors/{{ .Monitor.Slug }}/disable">Resume</a>
</section>
<section class="p-2 flex-1 border-4 border-red-300">
<h2 class="mb-2">
Danger Zone
</h2>
<p class="text-sm mb-2">Permanently delete this monitor.</p>
<a class="block text-center focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2" href="/settings/monitors/{{ .Monitor.Slug }}/delete">Delete</a>
</section>
</div>
<section> <section>
<table> <table>
<caption> <caption>

View file

@ -16,30 +16,53 @@
</div> </div>
</section> </section>
<section> <div class="flex md:flex-row flex-col gap-4 h-min">
<table> <section class="flex-1">
<caption> <table>
Active Workers <caption>
<p > <span>
Current workers that were online in last minutes. Active Workers
</p> {{ if eq ( len .Worker.ActiveWorkers) 0 }}
</caption> <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">
<thead> NONE
<tr> </span>
<th>Identity</th> {{ else }}
</tr> <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
</thead> {{ len .Worker.ActiveWorkers }} ONLINE
<tbody> </span>
{{range .Worker.ActiveWorkers }} {{ end }}
<tr> </span>
<td> <p>
{{ . }} Current workers that were online in last minutes.
</td> </p>
</tr> </caption>
{{end}} {{ if eq ( len .Worker.ActiveWorkers) 0 }}
</tbody> <thead><tr><th>No workers online for this worker group.</th></tr></thead>
</table> {{ else }}
</section> <thead>
<tr>
<th>Identity</th>
</tr>
</thead>
<tbody>
{{range .Worker.ActiveWorkers }}
<tr>
<th>{{ . }}</th>
</tr>
{{end}}
</tbody>
{{end}}
</table>
</section>
<section class="p-2 flex-1 border-4 border-red-300">
<h2 class="mb-2">
Danger Zone
</h2>
<p class="text-sm mb-2">Permanently delete this worker group. Workers will not be able to connect anymore.</p>
<a class="block text-center focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2" href="/settings/worker-groups/{{ .Worker.Slug }}/delete">Delete</a>
</section>
</div>
<script> <script>
const copyTokenButton = document.getElementById('copy-token'); const copyTokenButton = document.getElementById('copy-token');