Skip to content

Commit 9060774

Browse files
committed
backend: cmd/pagination: Add pagination implementation to return only
page that was requested from the client. This improves the pagination functionality, which help to return only page that was requested from the client not the full list, this help the frontend to fetch only page that the user wants to see the page not the full list. Currently the page size is limit =15, we can make it dynamic according to the headlamp.
1 parent 84bf2a2 commit 9060774

File tree

1 file changed

+101
-45
lines changed

1 file changed

+101
-45
lines changed

backend/cmd/pagination.go

Lines changed: 101 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,20 @@ package main
22

33
import (
44
"bytes"
5+
"compress/gzip"
56
"encoding/json"
7+
"fmt"
8+
"io"
9+
"log"
610
"net/http"
11+
"strconv"
12+
"time"
13+
14+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
715

816
"github.com/gorilla/mux"
917
)
1018

11-
var limit string = "15"
12-
1319
type responseCapture struct {
1420
http.ResponseWriter
1521
StatusCode int
@@ -22,8 +28,8 @@ func (r *responseCapture) WriteHeader(code int) {
2228
}
2329

2430
func (r *responseCapture) Write(b []byte) (int, error) {
25-
r.Body.Write(b)
26-
return r.ResponseWriter.Write(b)
31+
return r.Body.Write(b)
32+
// return r.ResponseWriter.Write(b)
2733
}
2834

2935
// CreateResponseCapture initializes responseCapture with a http.ResponseWriter and empty bytes.Buffer for the body.
@@ -35,73 +41,123 @@ func CreateResponseCapture(w http.ResponseWriter) *responseCapture {
3541
}
3642
}
3743

38-
type ResourceMetadata struct {
39-
ResourceVersion string `json:"resourceVersion"`
40-
ContinueToken string `json:"continue"`
44+
type Item struct {
45+
Metadata metav1.ObjectMeta `json:"metadata"`
4146
}
47+
type ResourceResponse struct {
48+
Kind string `json:"kind"`
49+
Verison string `json:"apiVersion"`
50+
Metadata metav1.ListMeta `json:"metadata"`
51+
Items []Item `json:"items"`
52+
}
53+
54+
// handleGzip function compress response if the response header have Content-Encoding as gzip.
55+
func handleGzip(rcw *responseCapture) ([]byte, error) {
56+
bodyBytes := rcw.Body.Bytes()
57+
if rcw.Header().Get("Content-Encoding") == "gzip" {
58+
reader, err := gzip.NewReader(bytes.NewReader(bodyBytes))
59+
if err != nil {
60+
return nil, err
61+
}
62+
defer reader.Close()
63+
bodyBytes, err = io.ReadAll(reader)
64+
if err != nil {
65+
return nil, err
66+
}
67+
}
4268

43-
type Metadata struct {
44-
Name string `json:"name"`
45-
Namespace string `json:"namespace"`
46-
Uuid string `json:"uuid"`
69+
return bodyBytes, nil
4770
}
4871

49-
type Item struct {
50-
Metadata Metadata `json:"metadata"`
72+
// returnResponseToClient helps to return the response to the client.
73+
func returnResponseToClient(rcw *responseCapture, v any, w http.ResponseWriter) error {
74+
var err error
75+
if rcw.Header().Get("Content-Encoding") == "gzip" {
76+
w.Header().Set("Content-Encoding", "gzip")
77+
gz := gzip.NewWriter(w)
78+
defer gz.Close()
79+
err = json.NewEncoder(gz).Encode(v)
80+
} else {
81+
err = json.NewEncoder(w).Encode(v)
82+
}
83+
84+
return err
5185
}
52-
type ResourceResponse struct {
53-
Metadata ResourceMetadata `json:"metadata"`
54-
Items []Item `json:"items"`
86+
87+
// sliceTheResponse helps to slice the full list to identify the startIndex and endIndex that will be
88+
// equal to the full page, which is going to be sliced from the full list.
89+
func sliceTheResponse(page int, pageSize int, sizeOfResponse int) (int, int) {
90+
start := (page - 1) * pageSize
91+
if start >= sizeOfResponse {
92+
return sizeOfResponse, sizeOfResponse // slice empty if page out of range
93+
}
94+
end := start + pageSize
95+
if end > sizeOfResponse {
96+
end = sizeOfResponse
97+
}
98+
return start, end
5599
}
56100

57-
var paginationMap = make(map[int]*ResourceResponse)
101+
// This helps to unmarshal the response that is coming from K8s server, so we can
102+
// further slice the full list of response into the pages.
103+
func UnmarshalCacheData(bodyBytes []byte) (ResourceResponse, error) {
104+
var resourceResponseFullList ResourceResponse
105+
106+
err := json.Unmarshal(bodyBytes, &resourceResponseFullList)
107+
if err != nil {
108+
return ResourceResponse{}, err
109+
}
58110

111+
return resourceResponseFullList, nil
112+
}
113+
114+
var limit = 15
115+
116+
// handlePagination is the middleware which will help to paginate the response coming from
117+
// the K8's server.
59118
func handlePagination(c *HeadlampConfig) mux.MiddlewareFunc {
60119
return func(h http.Handler) http.Handler {
61120
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
121+
start := time.Now()
62122

63123
rcw := CreateResponseCapture(w)
64124

65125
q := r.URL.Query()
66-
q.Set("limit", limit)
67-
r.URL.RawQuery = q.Encode()
126+
127+
pageNo := q.Get("pageNo")
68128

69129
h.ServeHTTP(rcw, r)
70130

71-
var currentPageResponse ResourceResponse
72-
json.Unmarshal(rcw.Body.Bytes(), &currentPageResponse)
131+
bodyBytes, err := handleGzip(rcw)
132+
if err != nil {
133+
log.Fatalf("error while converting from gzip: %v ", err)
134+
}
73135

74-
paginationMap[0] = &currentPageResponse
136+
responseFullList, err := UnmarshalCacheData(bodyBytes)
137+
if err != nil {
138+
log.Fatalf("error while unmarshalling bodybytes: %v", responseFullList)
139+
}
75140

76-
// Loop to pre-fetch pages 1 through 4
77-
currentContinueToken := currentPageResponse.Metadata.ContinueToken
78-
for i := 1; i <= 4; i++ {
79-
// Stop if there is no next page
80-
if currentContinueToken == "" {
81-
break
141+
if pageNo == "" {
142+
if err := returnResponseToClient(rcw, responseFullList, w); err != nil {
143+
log.Fatalf("error while returning response to client: %v", err)
82144
}
145+
return
146+
}
83147

84-
// Create a new request for the next page
85-
query := r.URL.Query()
86-
query.Set("continue", currentContinueToken)
87-
r.URL.RawQuery = query.Encode()
88-
89-
// Clear the response capture buffer before the next request
90-
rcw.Body.Reset()
91-
92-
// Serve the next request
93-
h.ServeHTTP(rcw, r)
148+
page, _ := strconv.Atoi(pageNo)
94149

95-
// Unmarshal the new response
96-
var nextResponse ResourceResponse
97-
json.Unmarshal(rcw.Body.Bytes(), &nextResponse)
150+
startPage, endPage := sliceTheResponse(page, limit, len(responseFullList.Items))
98151

99-
// Store the new response in the map
100-
paginationMap[i] = &nextResponse
152+
returnedData := responseFullList.Items[startPage:endPage]
101153

102-
// Update the continue token for the next loop iteration
103-
currentContinueToken = nextResponse.Metadata.ContinueToken
154+
if err := returnResponseToClient(rcw, returnedData, w); err != nil {
155+
log.Fatalf("error while returning response to client: %v", err)
104156
}
157+
158+
// To check the response time, this help understand how much time is it taking to send
159+
// the desired page that the client wants to access in the frontend.
160+
fmt.Println("time: ", time.Since(start))
105161
})
106162
}
107163
}

0 commit comments

Comments
 (0)