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  }