github.com/sdqri/sequined@v0.0.0-20240421190656-fc6bf956f4d8/internal/dashboard/dashboard.go (about) 1 package dashboard 2 3 import ( 4 _ "embed" 5 "fmt" 6 "html/template" 7 "net/http" 8 "slices" 9 "time" 10 11 "github.com/go-echarts/go-echarts/v2/charts" 12 "github.com/go-echarts/go-echarts/v2/opts" 13 14 "github.com/sdqri/sequined/internal/dashboard/snippetrenderer" 15 hyr "github.com/sdqri/sequined/internal/hyperrenderer" 16 obs "github.com/sdqri/sequined/internal/observer" 17 ) 18 19 //go:embed templates/dashboard.html.tmpl 20 var dashboardTmpl string 21 22 type Dashboard struct { 23 observer *obs.Observer 24 root *hyr.Webpage 25 } 26 27 func NewDashboard(root *hyr.Webpage, observer *obs.Observer) *Dashboard { 28 return &Dashboard{ 29 observer: observer, 30 root: root, 31 } 32 } 33 34 func (dashboard *Dashboard) HandleBy(mux *http.ServeMux) { 35 mux.HandleFunc("/dashboard", dashboard.HandleMainPage) 36 mux.HandleFunc("/charts/freshness", dashboard.HandleFreshnessChart) 37 mux.HandleFunc("/charts/age", dashboard.HandleAgeChart) 38 mux.HandleFunc("/charts/tree", dashboard.HandleTreeChart) 39 } 40 41 func (dashboard *Dashboard) HandleMainPage(w http.ResponseWriter, r *http.Request) { 42 dashboardTemplate, err := template.New("dashboard-template").Parse(dashboardTmpl) 43 if err != nil { 44 http.Error(w, err.Error(), http.StatusInternalServerError) 45 return 46 } 47 err = dashboardTemplate.Execute(w, nil) 48 if err != nil { 49 http.Error(w, err.Error(), http.StatusInternalServerError) 50 return 51 } 52 } 53 54 func (dashboard *Dashboard) GetFreshnessChart(bucketDuration time.Duration, duration time.Duration, ip string) *charts.Line { 55 line := charts.NewLine() 56 line.SetGlobalOptions( 57 charts.WithTitleOpts(opts.Title{ 58 Title: "Freshness - Last " + duration.String(), 59 }), 60 charts.WithYAxisOpts(opts.YAxis{ 61 Type: "value", 62 Min: 0, 63 Max: 1, 64 }), 65 ) 66 67 now := time.Now().UTC() 68 numBuckets := int(duration / bucketDuration) 69 buckets := make([]time.Time, 0, numBuckets) 70 for i := 0; i < numBuckets; i++ { 71 buckets = append(buckets, now.Add(-time.Duration(i*int(bucketDuration)))) 72 } 73 slices.Reverse(buckets) 74 75 freshnessSeries := make([]opts.LineData, numBuckets) 76 77 for i := 0; i < numBuckets; i++ { 78 freshness := dashboard.observer.GetFreshness(ip, buckets[i]) 79 freshnessSeries[i] = opts.LineData{Value: freshness} 80 } 81 82 xs := ConvertToHHMMSS(buckets) 83 line.SetXAxis(xs). 84 AddSeries("Freshness", freshnessSeries). 85 SetSeriesOptions( 86 charts.WithLineChartOpts( 87 opts.LineChart{ 88 Stack: "stack", 89 }), 90 ) 91 92 return line 93 } 94 95 func (dashboard *Dashboard) HandleFreshnessChart(w http.ResponseWriter, r *http.Request) { 96 bucketDurationStr := r.URL.Query().Get("bucket-duration") 97 durationStr := r.URL.Query().Get("duration") 98 ip := r.URL.Query().Get("ip") 99 100 bucketDuration, err := time.ParseDuration(bucketDurationStr) 101 if err != nil { 102 http.Error(w, "Invalid bucketDuration", http.StatusBadRequest) 103 return 104 } 105 106 duration, err := time.ParseDuration(durationStr) 107 if err != nil { 108 http.Error(w, "Invalid duration", http.StatusBadRequest) 109 return 110 } 111 112 freshnessChart := dashboard.GetFreshnessChart(bucketDuration, duration, ip) 113 snippetRenderer := snippetrenderer.NewSnippetRenderer(freshnessChart, freshnessChart.Validate) 114 err = snippetRenderer.Render(w) 115 if err != nil { 116 http.Error(w, "Failed to render charts", http.StatusInternalServerError) 117 return 118 } 119 120 } 121 122 func (dashboard *Dashboard) GetAgeChart(bucketDuration time.Duration, duration time.Duration, ip string) *charts.Line { 123 line := charts.NewLine() 124 line.SetGlobalOptions( 125 charts.WithTitleOpts(opts.Title{ 126 Title: "Age - Last " + duration.String(), 127 }), 128 charts.WithYAxisOpts(opts.YAxis{ 129 Type: "value", 130 }), 131 ) 132 133 now := time.Now().UTC() 134 numBuckets := int(duration / bucketDuration) 135 buckets := make([]time.Time, 0, numBuckets) 136 for i := 0; i < numBuckets; i++ { 137 buckets = append(buckets, now.Add(-time.Duration(i*int(bucketDuration)))) 138 } 139 slices.Reverse(buckets) 140 141 ageSeries := make([]opts.LineData, numBuckets) 142 143 for i := 0; i < numBuckets; i++ { 144 age := dashboard.observer.GetAge(ip, buckets[i]) 145 ageSeries[i] = opts.LineData{Value: age.Seconds()} 146 } 147 148 xs := ConvertToHHMMSS(buckets) 149 line.SetXAxis(xs). 150 AddSeries("Age (seconds)", ageSeries). 151 SetSeriesOptions( 152 charts.WithLineChartOpts( 153 opts.LineChart{ 154 Stack: "stack", 155 }), 156 ) 157 158 return line 159 } 160 161 func (dashboard *Dashboard) HandleAgeChart(w http.ResponseWriter, r *http.Request) { 162 bucketDurationStr := r.URL.Query().Get("bucket-duration") 163 durationStr := r.URL.Query().Get("duration") 164 ip := r.URL.Query().Get("ip") 165 166 bucketDuration, err := time.ParseDuration(bucketDurationStr) 167 if err != nil { 168 http.Error(w, "Invalid bucketDuration", http.StatusBadRequest) 169 return 170 } 171 172 duration, err := time.ParseDuration(durationStr) 173 if err != nil { 174 http.Error(w, "Invalid duration", http.StatusBadRequest) 175 return 176 } 177 178 ageChart := dashboard.GetAgeChart(bucketDuration, duration, ip) 179 err = ageChart.Render(w) 180 if err != nil { 181 http.Error(w, "Failed to render charts", http.StatusInternalServerError) 182 return 183 } 184 } 185 186 func (dashboard *Dashboard) A() *[]opts.TreeData { 187 return GetTreeData(dashboard.root) 188 } 189 190 func GetTreeData(root *hyr.Webpage) *[]opts.TreeData { 191 Result := make([]*opts.TreeData, 0) 192 193 var f func(*hyr.Webpage, *[]*opts.TreeData) 194 f = func(node *hyr.Webpage, treeData *[]*opts.TreeData) { 195 children := make([]*opts.TreeData, 0) 196 for _, child := range node.Links { 197 f(child, &children) 198 } 199 200 td := opts.TreeData{ 201 Name: node.Faker().City(), 202 } 203 switch node.Type { 204 case hyr.WebpageTypeHub: 205 td.Symbol = "circle" 206 td.ItemStyle = &opts.ItemStyle{ 207 Color: "#4CAF50", // Green for Hub 208 } 209 case hyr.WebpageTypeAuthority: 210 td.Symbol = "rect" 211 td.ItemStyle = &opts.ItemStyle{ 212 Color: "#2196F3", // Blue for Authority 213 } 214 } 215 td.Children = children 216 217 *treeData = append(*treeData, &td) 218 fmt.Println(Result) 219 } 220 221 f(root, &Result) 222 223 fmt.Println("Result=", Result) 224 return &[]opts.TreeData{*Result[0]} 225 } 226 227 func (dashboard *Dashboard) GetTreeChart() *charts.Tree { 228 tree := charts.NewTree() 229 tree.SetGlobalOptions( 230 charts.WithTitleOpts(opts.Title{ 231 Title: "Tree Chart", 232 }), 233 charts.WithLegendOpts(opts.Legend{ 234 Show: true, 235 // Add legend data for Hub and Authority 236 Data: []string{"Hub", "Authority"}, 237 }), 238 ) 239 240 treeData := GetTreeData(dashboard.root) 241 tree.AddSeries("Root", *treeData).SetSeriesOptions( 242 charts.WithTreeOpts( 243 opts.TreeChart{ 244 Layout: "orthogonal", 245 Orient: "TB", 246 InitialTreeDepth: -1, 247 Leaves: &opts.TreeLeaves{ 248 Label: &opts.Label{Show: true, Position: "right", Color: "Black", }, 249 }, 250 }, 251 ), 252 charts.WithLabelOpts(opts.Label{Show: true, Position: "top", Color: "Black"}), 253 ) 254 255 return tree 256 } 257 258 func (dashboard *Dashboard) HandleTreeChart(w http.ResponseWriter, r *http.Request) { 259 treeChart := dashboard.GetTreeChart() 260 if err := treeChart.Render(w); err != nil { 261 http.Error(w, "Failed to render charts", http.StatusInternalServerError) 262 return 263 } 264 } 265 266 func ConvertToHHMMSS(times []time.Time) []string { 267 formattedTimes := make([]string, len(times)) 268 for i, t := range times { 269 formattedTimes[i] = t.Format("15:04:05") 270 } 271 return formattedTimes 272 }