Skip to content

Commit 1948ce1

Browse files
authored
Merge pull request #753 from slok/slok/ui
Add SLO list filtering on UI
2 parents b2837cb + d5452a3 commit 1948ce1

File tree

6 files changed

+147
-49
lines changed

6 files changed

+147
-49
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- UI: Support SLO grouped by labels.
1616
- UI: Redirect unmarshaled ID of grouped SLO labels to proper SLO ID.
1717
- UI: Support service list sort by name and alert status.
18+
- UI: Support alert firing, burning over budget and budget consumed in period SLO filtering on SLO listing.
1819

1920
### Changed
2021

internal/http/ui/common_urls.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,10 @@ func (u urlManager) RemoveQueryParam(uri, key string) string {
100100
urlParsed.RawQuery = q.Encode()
101101
return urlParsed.String()
102102
}
103+
104+
func (u urlManager) RemoveQueryParams(uri string, keys ...string) string {
105+
for _, key := range keys {
106+
uri = u.RemoveQueryParam(uri, key)
107+
}
108+
return uri
109+
}

internal/http/ui/handler_select_slo.go

Lines changed: 52 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ func (u ui) handlerSelectSLO() http.HandlerFunc {
1414
)
1515

1616
const (
17-
queryParamSLOSearch = "slo-search"
18-
queryParamSLOSortMode = "slo-sort-mode"
17+
queryParamSLOSearch = "slo-search"
18+
queryParamSLOSortMode = "slo-sort-mode"
19+
queryParamFilterAlertsFiring = "slo-filter-alerts-firing"
20+
queryParamFilterBurningOverThreshold = "slo-filter-burning-over-threshold"
21+
queryParamFilterPeriodBudgetConsumed = "slo-filter-period-budget-consumed"
1922
)
2023

2124
var (
@@ -57,11 +60,19 @@ func (u ui) handlerSelectSLO() http.HandlerFunc {
5760
}
5861

5962
type tplData struct {
60-
SLOs []tplDataSLO
61-
SLOPagination tplPaginationData
63+
SLOs []tplDataSLO
64+
SLOPagination tplPaginationData
65+
66+
// Search.
6267
SLOSearchURL string
6368
SLOSearchInput string
6469

70+
// Filter.
71+
SLOFilterURL string
72+
SLOFilterFiringAlerts bool
73+
SLOFilterBurningOverThreshold bool
74+
SLOFilterPeriodBudgetConsumed bool
75+
6576
// Sorting info.
6677
SortSLONameURL string
6778
SortSLONameTitleIcon string
@@ -118,15 +129,34 @@ func (u ui) handlerSelectSLO() http.HandlerFunc {
118129
nextCursor := urls.ForwardCursorFromRequest(r)
119130
prevCursor := urls.BackwardCursorFromRequest(r)
120131
data.SLOSearchInput = r.URL.Query().Get(queryParamSLOSearch)
132+
data.SLOFilterFiringAlerts = r.URL.Query().Get(queryParamFilterAlertsFiring) == "on"
133+
data.SLOFilterBurningOverThreshold = r.URL.Query().Get(queryParamFilterBurningOverThreshold) == "on"
134+
data.SLOFilterPeriodBudgetConsumed = r.URL.Query().Get(queryParamFilterPeriodBudgetConsumed) == "on"
121135

122136
currentURL := urls.AppURL("/slos")
123137
currentURL = urls.AddQueryParm(currentURL, queryParamSLOSearch, data.SLOSearchInput)
124138
currentURL = urls.AddQueryParm(currentURL, queryParamSLOSortMode, sortModeS)
139+
if data.SLOFilterFiringAlerts {
140+
currentURL = urls.AddQueryParm(currentURL, queryParamFilterAlertsFiring, "on")
141+
}
142+
if data.SLOFilterBurningOverThreshold {
143+
currentURL = urls.AddQueryParm(currentURL, queryParamFilterBurningOverThreshold, "on")
144+
}
145+
if data.SLOFilterPeriodBudgetConsumed {
146+
currentURL = urls.AddQueryParm(currentURL, queryParamFilterPeriodBudgetConsumed, "on")
147+
}
148+
125149
htmx.NewResponse().WithPushURL(currentURL).SetHeaders(w) // Always push URL with search or no search param.
126150

127151
// Searching required data for logic.
128152
data.SLOSearchURL = urls.RemoveQueryParam(urls.URLWithComponent(currentURL, componentSLOList), queryParamSLOSearch)
129153

154+
// Filtering required data for logic.
155+
data.SLOFilterURL = urls.RemoveQueryParams(urls.URLWithComponent(currentURL, componentSLOList),
156+
queryParamFilterAlertsFiring,
157+
queryParamFilterBurningOverThreshold,
158+
queryParamFilterPeriodBudgetConsumed)
159+
130160
// Sorting required data for logic.
131161
{
132162
currentURLForSort := urls.RemoveQueryParam(urls.URLWithComponent(currentURL, componentSLOList), queryParamSLOSortMode)
@@ -181,50 +211,22 @@ func (u ui) handlerSelectSLO() http.HandlerFunc {
181211
}
182212

183213
switch {
184-
// Snippet SLO list next.
185-
case isHTMXCall && component == componentSLOList && nextCursor != "":
186-
// Get SLOs for service.
187-
slosResp, err := u.serviceApp.ListSLOs(ctx, app.ListSLOsRequest{
188-
Cursor: nextCursor,
189-
FilterSearchInput: data.SLOSearchInput,
190-
SortMode: sortMode,
191-
})
192-
if err != nil {
193-
u.logger.Errorf("could not get SLOs: %s", err)
194-
http.Error(w, "could not get SLOs", http.StatusInternalServerError)
195-
return
196-
}
197-
198-
data.SLOs = mapSLOsToTPL(slosResp.SLOs)
199-
data.SLOPagination = mapPaginationToTPL(slosResp.PaginationCursors, urls.URLWithComponent(currentURL, componentSLOList))
200-
201-
u.tplRenderer.RenderResponse(ctx, w, r, "app_slos_comp_slo_list", data)
202-
203-
// Snippet SLO list previous.
204-
case isHTMXCall && component == componentSLOList && prevCursor != "":
205-
// Get SLOs for service.
206-
slosResp, err := u.serviceApp.ListSLOs(ctx, app.ListSLOsRequest{
207-
Cursor: prevCursor,
208-
FilterSearchInput: data.SLOSearchInput,
209-
SortMode: sortMode,
210-
})
211-
if err != nil {
212-
u.logger.Errorf("could not get SLOs: %s", err)
213-
http.Error(w, "could not get SLOs", http.StatusInternalServerError)
214-
return
215-
}
216-
217-
data.SLOs = mapSLOsToTPL(slosResp.SLOs)
218-
data.SLOPagination = mapPaginationToTPL(slosResp.PaginationCursors, urls.URLWithComponent(currentURL, componentSLOList))
219-
220-
u.tplRenderer.RenderResponse(ctx, w, r, "app_slos_comp_slo_list", data)
221-
222214
// Snippet SLO list.
223215
case isHTMXCall && component == componentSLOList:
224-
// Get SLOs for service.
216+
cursor := ""
217+
if nextCursor != "" {
218+
cursor = nextCursor
219+
} else if prevCursor != "" {
220+
cursor = prevCursor
221+
}
222+
225223
slosResp, err := u.serviceApp.ListSLOs(ctx, app.ListSLOsRequest{
226-
FilterSearchInput: data.SLOSearchInput,
227-
SortMode: sortMode,
224+
Cursor: cursor,
225+
FilterSearchInput: data.SLOSearchInput,
226+
SortMode: sortMode,
227+
FilterAlertFiring: data.SLOFilterFiringAlerts,
228+
FilterCurrentBurningBudgetOver100: data.SLOFilterBurningOverThreshold,
229+
FilterPeriodBudgetConsumed: data.SLOFilterPeriodBudgetConsumed,
228230
})
229231
if err != nil {
230232
u.logger.Errorf("could not get SLOs: %s", err)
@@ -245,8 +247,11 @@ func (u ui) handlerSelectSLO() http.HandlerFunc {
245247
default:
246248
// Get SLOs for service.
247249
slosResp, err := u.serviceApp.ListSLOs(ctx, app.ListSLOsRequest{
248-
FilterSearchInput: data.SLOSearchInput,
249-
SortMode: sortMode,
250+
FilterSearchInput: data.SLOSearchInput,
251+
SortMode: sortMode,
252+
FilterAlertFiring: data.SLOFilterFiringAlerts,
253+
FilterCurrentBurningBudgetOver100: data.SLOFilterBurningOverThreshold,
254+
FilterPeriodBudgetConsumed: data.SLOFilterPeriodBudgetConsumed,
250255
})
251256
if err != nil {
252257
u.logger.Errorf("could not get SLOs: %s", err)

internal/http/ui/handler_select_slo_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ func TestHandlerSelectSLO(t *testing.T) {
123123
`<!DOCTYPE html>`, // We rendered a full page.
124124
`<div class="container"> <nav>`, // We have the menu.
125125
`<input type="search" name="slo-search" value="" placeholder="Search" aria-label="Search" hx-get="/u/app/slos?component=slo-list&slo-sort-mode=slo-name-asc" hx-trigger="change, keyup changed delay:500ms, search" hx-target="#slo-list" hx-include="this" />`, // We have the search bar with HTMX.
126+
`<form hx-get="/u/app/slos?component=slo-list&slo-search=&slo-sort-mode=slo-name-asc" hx-include="this" hx-trigger="change" hx-target="#slo-list"> <details class="dropdown"> <summary> <i data-lucide="list-filter"></i> Filter </summary>`, // We have the filters.
127+
`<li> <label><input type="checkbox" name="slo-filter-alerts-firing" /> <i data-lucide="megaphone"></i> Alerts firing</label> </li>`, // We have the firing alerts filter.
128+
`<li> <label><input type="checkbox" name="slo-filter-burning-over-threshold" /> <i data-lucide="flame"></i> Burning over threshold</label> </li>`, // We have the burning over threshold filter.
129+
`<li> <label><input type="checkbox" name="slo-filter-period-budget-consumed" /> <i data-lucide="circle-slash-2"></i> Period Budget consumed</label> </li>`, // We have the period budget consumed filter.
126130
`<th scope="col"> Type </th>`, // We have the icon column.
127131
`<th scope="col"> <div hx-get="/u/app/slos?component=slo-list&slo-search=&slo-sort-mode=slo-name-desc" hx-target="#slo-list" hx-swap="innerHTML show:window:top"> SLO ↑</div> </th> `, // We have the SLO name column with HTMX.
128132
`<th scope="col"> Grouped labels </th>`, // We have the grouped labels column.
@@ -442,6 +446,50 @@ func TestHandlerSelectSLO(t *testing.T) {
442446
},
443447
},
444448

449+
"Filtering the SLOs with HTMX should render the snippet.": {
450+
request: func() *http.Request {
451+
r := httptest.NewRequest(http.MethodGet, "/u/app/slos?component=slo-list&slo-filter-alerts-firing=on&slo-filter-burning-over-threshold=on&slo-filter-period-budget-consumed=on", nil)
452+
r.Header.Add("HX-Request", "true")
453+
return r
454+
},
455+
mock: func(m mocks) {
456+
expReq := app.ListSLOsRequest{
457+
SortMode: app.SLOListSortModeSLOIDAsc,
458+
FilterAlertFiring: true,
459+
FilterCurrentBurningBudgetOver100: true,
460+
FilterPeriodBudgetConsumed: true,
461+
}
462+
m.ServiceApp.On("ListSLOs", mock.Anything, expReq).Once().Return(&app.ListSLOsResponse{
463+
PaginationCursors: app.PaginationCursors{},
464+
SLOs: []app.RealTimeSLODetails{
465+
{
466+
SLO: model.SLO{
467+
ID: "test-svc1-slo1",
468+
ServiceID: "test-svc1",
469+
Name: "Test SLO 1",
470+
},
471+
Alerts: model.SLOAlerts{
472+
FiringPage: &model.Alert{Name: "page-1"},
473+
FiringWarning: &model.Alert{Name: "warn-2"},
474+
},
475+
Budget: model.SLOBudgetDetails{
476+
SLOID: "test-svc1-slo1",
477+
BurningBudgetPercent: 75.0,
478+
BurnedBudgetWindowPercent: 80.0,
479+
},
480+
},
481+
}}, nil)
482+
},
483+
expHeaders: http.Header{
484+
"Content-Type": {"text/html; charset=utf-8"},
485+
"Hx-Push-Url": {"/u/app/slos?slo-search=&slo-sort-mode=slo-name-asc&slo-filter-alerts-firing=on&slo-filter-burning-over-threshold=on&slo-filter-period-budget-consumed=on"},
486+
},
487+
expCode: 200,
488+
expBody: []string{
489+
`<tr> <td> <span data-tooltip="Individual SLO"><i data-lucide="goal"></i></span> </td> <td> <a href="/u/app/slos/test-svc1-slo1">Test SLO 1</a> </td> <td> </td> <td><a href="/u/app/services/test-svc1">test-svc1</a></td> <td class="is-ok">75%</td> <td class="is-ok">20%</td> <td> <div class="is-critical">Critical</div> </td> </tr>`, // SLO row should be ok.
490+
},
491+
},
492+
445493
"Listing the slos with unknown HTMX.": {
446494
request: func() *http.Request {
447495
r := httptest.NewRequest(http.MethodGet, "/u/app/slos", nil)

internal/http/ui/static/css/main.css

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
--sloth-ok: rgb(58, 135, 19);
2727
--sloth-warning: rgb(253, 147, 3);
2828
--sloth-critical: rgb(197, 47, 33);
29-
--pico-mark-background-color: rgb(223, 227, 235);
29+
/*--pico-mark-background-color: rgb(223, 227, 235);*/
3030
}
3131

3232
/* Dark color scheme (Auto) */
@@ -80,6 +80,11 @@ td span svg.lucide {
8080
height: 1.3em;
8181
}
8282

83+
li label svg.lucide {
84+
width: 1.3em;
85+
height: 1.3em;
86+
}
87+
8388
span[data-tooltip] {
8489
border-bottom: none !important;
8590
}

internal/http/ui/templates/app/slos/page.tmpl

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,39 @@
1919
hx-target="#slo-list"
2020
hx-include="this"
2121
/>
22-
<div></div>
22+
<div>
23+
<div class="grid">
24+
<form
25+
hx-get="{{ .Data.SLOFilterURL }}"
26+
hx-include="this"
27+
hx-trigger="change"
28+
hx-target="#slo-list">
29+
<details class="dropdown">
30+
<summary>
31+
<i data-lucide="list-filter"></i> Filter
32+
</summary>
33+
<ul>
34+
<li>
35+
<label><input type="checkbox" name="slo-filter-alerts-firing"
36+
{{if .Data.SLOFilterFiringAlerts}}checked{{end}}
37+
/> <i data-lucide="megaphone"></i> Alerts firing</label>
38+
</li>
39+
<li>
40+
<label><input type="checkbox" name="slo-filter-burning-over-threshold"
41+
{{if .Data.SLOFilterBurningOverThreshold}}checked{{end}}
42+
/> <i data-lucide="flame"></i> Burning over threshold</label>
43+
</li>
44+
<li>
45+
<label><input type="checkbox" name="slo-filter-period-budget-consumed"
46+
{{if .Data.SLOFilterPeriodBudgetConsumed}}checked{{end}}
47+
/> <i data-lucide="circle-slash-2"></i> Period Budget consumed</label>
48+
</li>
49+
</ul>
50+
</details>
51+
</form>
52+
<div></div>
53+
</div>
54+
</div>
2355
<div></div>
2456
</div>
2557
{{template "app_slos_comp_slo_list" .}}

0 commit comments

Comments
 (0)