Skip to content

Commit 72c1bc7

Browse files
authored
Merge pull request #26 from ethpandaops/bbusa/sort-by-column
feat: sort by column
2 parents ea44cc9 + 7adc25c commit 72c1bc7

7 files changed

Lines changed: 274 additions & 11 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ dist/
55
tmp-*
66
dugtrio-config.yaml
77
dugtrio-log.log
8+
.hack/devnet/generated-dugtrio-config.yaml

.hack/devnet/cleanup.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/bash -x
2+
__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
3+
ENCLAVE_NAME="${ENCLAVE_NAME:-dugtrio}"
4+
kurtosis enclave rm -f "$ENCLAVE_NAME"
5+
6+
echo "Cleaning up generated files..."
7+
rm -f ${__dir}/generated-*
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
participants_matrix:
2+
el:
3+
- el_type: geth
4+
el_image: ethpandaops/geth:master
5+
- el_type: besu
6+
el_image: ethpandaops/besu:main
7+
- el_type: nethermind
8+
el_image: ethpandaops/nethermind:master
9+
cl:
10+
- cl_type: lighthouse
11+
cl_image: ethpandaops/lighthouse:unstable
12+
supernode: true
13+
14+
additional_services:
15+
- dugtrio
16+

.hack/devnet/run.sh

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#!/bin/bash
2+
__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
3+
4+
if [ -f "${__dir}/custom-kurtosis.devnet.config.yaml" ]; then
5+
config_file="${__dir}/custom-kurtosis.devnet.config.yaml"
6+
else
7+
config_file="${__dir}/kurtosis.devnet.config.yaml"
8+
fi
9+
10+
## Run devnet using kurtosis
11+
ENCLAVE_NAME="${ENCLAVE_NAME:-dugtrio}"
12+
ETHEREUM_PACKAGE="${ETHEREUM_PACKAGE:-github.com/ethpandaops/ethereum-package}"
13+
if kurtosis enclave inspect "$ENCLAVE_NAME" > /dev/null; then
14+
echo "Kurtosis enclave '$ENCLAVE_NAME' is already up."
15+
else
16+
kurtosis run "$ETHEREUM_PACKAGE" \
17+
--image-download always \
18+
--enclave "$ENCLAVE_NAME" \
19+
--args-file "${config_file}"
20+
fi
21+
22+
## Generate Dugtrio config
23+
ENCLAVE_UUID=$(kurtosis enclave inspect "$ENCLAVE_NAME" --full-uuids | grep 'UUID:' | awk '{print $2}')
24+
25+
BEACON_NODES=$(docker ps -aq -f "label=kurtosis_enclave_uuid=$ENCLAVE_UUID" \
26+
-f "label=com.kurtosistech.app-id=kurtosis" \
27+
-f "label=com.kurtosistech.custom.ethereum-package.client-type=beacon" | tac)
28+
29+
cat <<EOF > "${__dir}/generated-dugtrio-config.yaml"
30+
logging:
31+
outputLevel: "info"
32+
33+
server:
34+
host: "0.0.0.0"
35+
port: "8080"
36+
37+
endpoints:
38+
EOF
39+
40+
# Add beacon endpoints (using same logic as dora)
41+
for node in $BEACON_NODES; do
42+
name=$(docker inspect -f "{{ with index .Config.Labels \"com.kurtosistech.id\"}}{{.}}{{end}}" $node)
43+
ip=$(echo '127.0.0.1')
44+
port=$(docker inspect --format='{{ (index (index .NetworkSettings.Ports "3500/tcp") 0).HostPort }}' $node 2>/dev/null)
45+
if [ -z "$port" ]; then
46+
port=$(docker inspect --format='{{ (index (index .NetworkSettings.Ports "4000/tcp") 0).HostPort }}' $node)
47+
fi
48+
if [ -z "$port" ]; then
49+
port="65535"
50+
fi
51+
echo " - name: \"$name\"" >> "${__dir}/generated-dugtrio-config.yaml"
52+
echo " url: \"http://$ip:$port\"" >> "${__dir}/generated-dugtrio-config.yaml"
53+
done
54+
55+
cat <<EOF >> "${__dir}/generated-dugtrio-config.yaml"
56+
57+
pool:
58+
schedulerMode: "rr"
59+
followDistance: 10
60+
maxHeadDistance: 2
61+
62+
proxy:
63+
proxyCount: 0
64+
callTimeout: 60s
65+
sessionTimeout: 10m
66+
stickyEndpoint: true
67+
callRateLimit: 100
68+
callRateBurst: 1000
69+
blockedPaths:
70+
- ^/eth/v[0-9]+/debug/.*
71+
authorization:
72+
require: false
73+
password: ""
74+
rebalanceInterval: 10s
75+
rebalanceThreshold: 0.1
76+
rebalanceAbsThreshold: 3
77+
rebalanceMaxSweep: 10
78+
79+
frontend:
80+
enabled: true
81+
minify: false
82+
siteName: "Dugtrio Devnet"
83+
pprof: true
84+
85+
metrics:
86+
enabled: true
87+
host: "0.0.0.0"
88+
port: "9090"
89+
EOF
90+
91+
cat <<EOF
92+
============================================================================================================
93+
Dugtrio devnet is ready!
94+
95+
Configuration file: ${__dir}/generated-dugtrio-config.yaml
96+
97+
To start dugtrio:
98+
make devnet-run
99+
100+
Or manually:
101+
go run cmd/dugtrio-proxy/main.go --config ${__dir}/generated-dugtrio-config.yaml
102+
103+
Health dashboard: http://localhost:8080/health
104+
Metrics: http://localhost:9090/metrics
105+
106+
Endpoints configured:
107+
EOF
108+
109+
# List the configured endpoints
110+
grep -A 1 "name:" "${__dir}/generated-dugtrio-config.yaml" | sed 's/^/ /'
111+
112+
echo "============================================================================================================"

Makefile

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ GOLDFLAGS += -X 'github.com/ethpandaops/dugtrio/utils.BuildVersion="$(VERSION)"'
66
GOLDFLAGS += -X 'github.com/ethpandaops/dugtrio/utils.BuildTime="$(BUILDTIME)"'
77
GOLDFLAGS += -X 'github.com/ethpandaops/dugtrio/utils.BuildRelease="$(RELEASE)"'
88

9-
.PHONY: all test clean
9+
.PHONY: all test clean devnet devnet-run devnet-clean
1010

1111
all: build
1212

@@ -19,3 +19,12 @@ build:
1919

2020
clean:
2121
rm -f bin/*
22+
23+
devnet:
24+
.hack/devnet/run.sh
25+
26+
devnet-run: devnet build
27+
go run cmd/dugtrio-proxy/main.go --config .hack/devnet/generated-dugtrio-config.yaml
28+
29+
devnet-clean:
30+
.hack/devnet/cleanup.sh

frontend/handlers/health.go

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package handlers
33
import (
44
"net/http"
55
"sort"
6+
"strings"
67
"time"
78

89
"github.com/ethpandaops/dugtrio/frontend"
@@ -60,7 +61,11 @@ func (fh *FrontendHandler) Health(w http.ResponseWriter, r *http.Request) {
6061

6162
var pageError error
6263

63-
data.Data, pageError = fh.getHealthPageData()
64+
// Parse sorting parameters
65+
sortBy := r.URL.Query().Get("sort")
66+
sortOrder := r.URL.Query().Get("order")
67+
68+
data.Data, pageError = fh.getHealthPageData(sortBy, sortOrder)
6469
if pageError != nil {
6570
frontend.HandlePageError(w, r, pageError)
6671
return
@@ -73,7 +78,7 @@ func (fh *FrontendHandler) Health(w http.ResponseWriter, r *http.Request) {
7378
}
7479
}
7580

76-
func (fh *FrontendHandler) getHealthPageData() (*HealthPage, error) {
81+
func (fh *FrontendHandler) getHealthPageData(sortBy, sortOrder string) (*HealthPage, error) {
7782
pageData := &HealthPage{
7883
Clients: []*HealthPageClient{},
7984
}
@@ -84,6 +89,9 @@ func (fh *FrontendHandler) getHealthPageData() (*HealthPage, error) {
8489
pageData.Clients = append(pageData.Clients, clientData)
8590
}
8691

92+
// Sort clients based on parameters
93+
fh.sortClients(pageData.Clients, sortBy, sortOrder)
94+
8795
pageData.ClientCount = uint64(len(pageData.Clients))
8896

8997
// get blocks
@@ -168,3 +176,44 @@ func (fh *FrontendHandler) getHealthPageClientData(client *pool.Client) *HealthP
168176

169177
return clientData
170178
}
179+
180+
// sortClients sorts the client slice based on the provided sort field and order
181+
func (fh *FrontendHandler) sortClients(clients []*HealthPageClient, sortBy, sortOrder string) {
182+
if sortBy == "" {
183+
return
184+
}
185+
186+
ascending := sortOrder != "desc"
187+
188+
sort.Slice(clients, func(i, j int) bool {
189+
var less bool
190+
191+
switch strings.ToLower(sortBy) {
192+
case "index", "#":
193+
less = clients[i].Index < clients[j].Index
194+
case "name":
195+
less = strings.ToLower(clients[i].Name) < strings.ToLower(clients[j].Name)
196+
case "headslot", "head_slot":
197+
less = clients[i].HeadSlot < clients[j].HeadSlot
198+
case "headroot", "head_root":
199+
less = string(clients[i].HeadRoot) < string(clients[j].HeadRoot)
200+
case "status":
201+
less = strings.ToLower(clients[i].Status) < strings.ToLower(clients[j].Status)
202+
case "useable", "ready":
203+
less = !clients[i].IsReady && clients[j].IsReady
204+
case "type":
205+
less = clients[i].Type < clients[j].Type
206+
case "version":
207+
less = strings.ToLower(clients[i].Version) < strings.ToLower(clients[j].Version)
208+
default:
209+
// Default to index sorting
210+
less = clients[i].Index < clients[j].Index
211+
}
212+
213+
if ascending {
214+
return less
215+
}
216+
217+
return !less
218+
})
219+
}

frontend/templates/health/health.html

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ <h2 class="px-2">Clients</h2>
88
<table class="table table-nobr" id="clients">
99
<thead>
1010
<tr>
11-
<th>#</th>
12-
<th>Name</th>
13-
<th>Head Slot</th>
14-
<th>Head Root</th>
15-
<th>Status</th>
16-
<th>Useable</th>
17-
<th>Type</th>
18-
<th>Version</th>
11+
<th><a href="#" class="sort-header text-decoration-none" data-sort="index"># <i class="fa fa-sort"></i></a></th>
12+
<th><a href="#" class="sort-header text-decoration-none" data-sort="name">Name <i class="fa fa-sort"></i></a></th>
13+
<th><a href="#" class="sort-header text-decoration-none" data-sort="headslot">Head Slot <i class="fa fa-sort"></i></a></th>
14+
<th><a href="#" class="sort-header text-decoration-none" data-sort="headroot">Head Root <i class="fa fa-sort"></i></a></th>
15+
<th><a href="#" class="sort-header text-decoration-none" data-sort="status">Status <i class="fa fa-sort"></i></a></th>
16+
<th><a href="#" class="sort-header text-decoration-none" data-sort="useable">Useable <i class="fa fa-sort"></i></a></th>
17+
<th><a href="#" class="sort-header text-decoration-none" data-sort="type">Type <i class="fa fa-sort"></i></a></th>
18+
<th><a href="#" class="sort-header text-decoration-none" data-sort="version">Version <i class="fa fa-sort"></i></a></th>
1919
</tr>
2020
</thead>
2121
<tbody>
@@ -189,6 +189,75 @@ <h2 class="px-2">Client Forks</h2>
189189

190190

191191
{{ define "js" }}
192+
<script>
193+
document.addEventListener('DOMContentLoaded', function() {
194+
// Handle sorting for client table
195+
const sortHeaders = document.querySelectorAll('.sort-header');
196+
const urlParams = new URLSearchParams(window.location.search);
197+
const currentSort = urlParams.get('sort');
198+
const currentOrder = urlParams.get('order') || 'asc';
199+
200+
// Update header appearance based on current sort
201+
if (currentSort) {
202+
sortHeaders.forEach(header => {
203+
const sortBy = header.getAttribute('data-sort');
204+
const icon = header.querySelector('i');
205+
if (sortBy === currentSort) {
206+
if (currentOrder === 'asc') {
207+
icon.className = 'fa fa-sort-up';
208+
} else {
209+
icon.className = 'fa fa-sort-down';
210+
}
211+
}
212+
});
213+
}
214+
215+
// Add click handlers
216+
sortHeaders.forEach(header => {
217+
header.addEventListener('click', function(e) {
218+
e.preventDefault();
219+
const sortBy = this.getAttribute('data-sort');
220+
let order = 'asc';
221+
222+
// Toggle order if clicking on currently sorted column
223+
if (currentSort === sortBy && currentOrder === 'asc') {
224+
order = 'desc';
225+
}
226+
227+
// Build new URL with sort parameters
228+
const newParams = new URLSearchParams(window.location.search);
229+
newParams.set('sort', sortBy);
230+
newParams.set('order', order);
231+
232+
// Redirect to sorted page
233+
window.location.href = window.location.pathname + '?' + newParams.toString();
234+
});
235+
});
236+
});
237+
</script>
192238
{{ end }}
193239
{{ define "css" }}
240+
<style>
241+
.sort-header {
242+
color: inherit !important;
243+
cursor: pointer;
244+
user-select: none;
245+
display: inline-block;
246+
width: 100%;
247+
}
248+
249+
.sort-header:hover {
250+
color: #0d6efd !important;
251+
text-decoration: none !important;
252+
}
253+
254+
.sort-header i {
255+
margin-left: 4px;
256+
opacity: 0.6;
257+
}
258+
259+
.sort-header:hover i {
260+
opacity: 1;
261+
}
262+
</style>
194263
{{ end }}

0 commit comments

Comments
 (0)