From f725d495215e75ca43642fc797a34510bd0bcc64 Mon Sep 17 00:00:00 2001 From: Alexis Couvreur Date: Tue, 18 Oct 2022 20:52:40 +0000 Subject: [PATCH] feat: add `ghost`, `hacker-terminal`, `matrix` and `shuffle` themes --- app/http/pages/render.go | 100 ++++++++ app/http/pages/render_test.go | 165 +++++++++++++ app/http/pages/themes/ghost.html | 71 ++++++ app/http/pages/themes/hacker-terminal.html | 174 ++++++++++++++ app/http/pages/themes/matrix.html | 259 +++++++++++++++++++++ app/http/pages/themes/shuffle.html | 168 +++++++++++++ 6 files changed, 937 insertions(+) create mode 100644 app/http/pages/render.go create mode 100644 app/http/pages/render_test.go create mode 100644 app/http/pages/themes/ghost.html create mode 100644 app/http/pages/themes/hacker-terminal.html create mode 100644 app/http/pages/themes/matrix.html create mode 100644 app/http/pages/themes/shuffle.html diff --git a/app/http/pages/render.go b/app/http/pages/render.go new file mode 100644 index 0000000..fa69505 --- /dev/null +++ b/app/http/pages/render.go @@ -0,0 +1,100 @@ +package pages + +import ( + "io" + "io/fs" + + "fmt" + "html/template" + "math" + "time" + + "embed" +) + +//go:embed themes/* +var themes embed.FS + +type RenderOptionsRequestState struct { + Name string + CurrentReplicas int + DesiredReplicas int + Status string + Error error +} + +type RenderOptions struct { + DisplayName string + RequestStates []RenderOptionsRequestState + SessionDuration time.Duration + RefreshFrequency time.Duration + Theme string + CustomThemes fs.FS + Version string +} + +type TemplateValues struct { + DisplayName string + RequestStates []RenderOptionsRequestState + SessionDuration string + RefreshFrequency time.Duration + Version string +} + +func Render(options RenderOptions, writer io.Writer) error { + var tpl *template.Template + var err error + + // Load custom theme if provided + if options.CustomThemes != nil { + tpl, err = template.ParseFS(options.CustomThemes, options.Theme) + } else { + // Load selected theme + tpl, err = template.ParseFS(themes, fmt.Sprintf("themes/%s.html", options.Theme)) + } + + if err != nil { + return err + } + + return tpl.Execute(writer, TemplateValues{ + DisplayName: options.DisplayName, + RequestStates: options.RequestStates, + SessionDuration: humanizeDuration(options.SessionDuration), + RefreshFrequency: options.RefreshFrequency, + Version: options.Version, + }) +} + +// humanizeDuration humanizes time.Duration output to a meaningful value, +// golang's default “time.Duration“ output is badly formatted and unreadable. +func humanizeDuration(duration time.Duration) string { + if duration.Seconds() < 60.0 { + return fmt.Sprintf("%d seconds", int64(duration.Seconds())) + } + if duration.Minutes() < 60.0 { + remainingSeconds := math.Mod(duration.Seconds(), 60) + if remainingSeconds > 0 { + return fmt.Sprintf("%d minutes %d seconds", int64(duration.Minutes()), int64(remainingSeconds)) + } + return fmt.Sprintf("%d minutes", int64(duration.Minutes())) + } + if duration.Hours() < 24.0 { + remainingMinutes := math.Mod(duration.Minutes(), 60) + remainingSeconds := math.Mod(duration.Seconds(), 60) + + if remainingMinutes > 0 { + if remainingSeconds > 0 { + return fmt.Sprintf("%d hours %d minutes %d seconds", int64(duration.Hours()), int64(remainingMinutes), int64(remainingSeconds)) + } + return fmt.Sprintf("%d hours %d minutes", int64(duration.Hours()), int64(remainingMinutes)) + } + return fmt.Sprintf("%d hours", int64(duration.Hours())) + } + remainingHours := math.Mod(duration.Hours(), 24) + remainingMinutes := math.Mod(duration.Minutes(), 60) + remainingSeconds := math.Mod(duration.Seconds(), 60) + return fmt.Sprintf("%d days %d hours %d minutes %d seconds", + int64(duration.Hours()/24), int64(remainingHours), + int64(remainingMinutes), int64(remainingSeconds)) +} diff --git a/app/http/pages/render_test.go b/app/http/pages/render_test.go new file mode 100644 index 0000000..c131ea3 --- /dev/null +++ b/app/http/pages/render_test.go @@ -0,0 +1,165 @@ +package pages + +import ( + "bytes" + "fmt" + "testing" + "testing/fstest" + "time" +) + +var requestsStates []RenderOptionsRequestState = []RenderOptionsRequestState{ + { + Name: "nginx", + CurrentReplicas: 0, + DesiredReplicas: 4, + Status: "starting", + Error: nil, + }, + { + Name: "whoami", + CurrentReplicas: 4, + DesiredReplicas: 4, + Status: "started", + Error: nil, + }, + { + Name: "devil", + CurrentReplicas: 0, + DesiredReplicas: 4, + Status: "error", + Error: fmt.Errorf("devil service does not exist"), + }, +} + +func TestRender(t *testing.T) { + type args struct { + options RenderOptions + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Load ghost theme", + args: args{ + options: RenderOptions{ + DisplayName: "Test", + RequestStates: requestsStates, + Theme: "ghost", + SessionDuration: 10 * time.Minute, + RefreshFrequency: 5 * time.Second, + CustomThemes: nil, + Version: "v0.0.0", + }, + }, + wantErr: false, + }, + { + name: "Load hacker-terminal theme", + args: args{ + options: RenderOptions{ + DisplayName: "Test", + RequestStates: requestsStates, + Theme: "hacker-terminal", + SessionDuration: 10 * time.Minute, + RefreshFrequency: 5 * time.Second, + CustomThemes: nil, + Version: "v0.0.0", + }, + }, + wantErr: false, + }, + { + name: "Load matrix theme", + args: args{ + options: RenderOptions{ + DisplayName: "Test", + RequestStates: requestsStates, + Theme: "matrix", + SessionDuration: 10 * time.Minute, + RefreshFrequency: 5 * time.Second, + CustomThemes: nil, + Version: "v0.0.0", + }, + }, + wantErr: false, + }, + { + name: "Load shiffle theme", + args: args{ + options: RenderOptions{ + DisplayName: "Test", + RequestStates: requestsStates, + Theme: "shuffle", + SessionDuration: 10 * time.Minute, + RefreshFrequency: 5 * time.Second, + CustomThemes: nil, + Version: "v0.0.0", + }, + }, + wantErr: false, + }, + { + name: "Load non existant theme", + args: args{ + options: RenderOptions{ + DisplayName: "Test", + RequestStates: requestsStates, + Theme: "nonexistant", + SessionDuration: 10 * time.Minute, + RefreshFrequency: 5 * time.Second, + CustomThemes: nil, + Version: "v0.0.0", + }, + }, + wantErr: true, + }, + { + name: "Load custom theme", + args: args{ + options: RenderOptions{ + DisplayName: "Test", + RequestStates: requestsStates, + Theme: "dc-comics.html", + SessionDuration: 10 * time.Minute, + RefreshFrequency: 5 * time.Second, + CustomThemes: fstest.MapFS{ + "marvel.html": {Data: []byte("{{ .DisplayName }}")}, + "dc-comics.html": {Data: []byte("batman")}, + }, + Version: "v0.0.0", + }, + }, + wantErr: false, + }, + { + name: "Load non existant custom theme", + args: args{ + options: RenderOptions{ + DisplayName: "Test", + RequestStates: requestsStates, + Theme: "nonexistant", + SessionDuration: 10 * time.Minute, + RefreshFrequency: 5 * time.Second, + CustomThemes: fstest.MapFS{ + "marvel.html": {Data: []byte("thor")}, + "dc-comics.html": {Data: []byte("batman")}, + }, + Version: "v0.0.0", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + writer := &bytes.Buffer{} + if err := Render(tt.args.options, writer); (err != nil) != tt.wantErr { + t.Errorf("Render() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} diff --git a/app/http/pages/themes/ghost.html b/app/http/pages/themes/ghost.html new file mode 100644 index 0000000..96e21c3 --- /dev/null +++ b/app/http/pages/themes/ghost.html @@ -0,0 +1,71 @@ + + + + + + Sablier + + + + + + + + +
+
+ + + + + + + + + + + + +

+ + + +

+

Starting {{ .DisplayName }}

+

Your instance(s) will stop after {{ .SessionDuration }} of inactivity}

+
+ + {{- range $i, $request := .RequestStates }} + + + {{- if $request.Error }} + + {{- else }} + + {{- end}} + + {{ end -}} +
{{ $request.Name }}{{ $request.Error }}{{ $request.Status }} ({{ $request.CurrentReplicas }}/{{ $request.DesiredReplicas }})
+
] +
+
+ + \ No newline at end of file diff --git a/app/http/pages/themes/hacker-terminal.html b/app/http/pages/themes/hacker-terminal.html new file mode 100644 index 0000000..d3df01d --- /dev/null +++ b/app/http/pages/themes/hacker-terminal.html @@ -0,0 +1,174 @@ + + + + + + + + Sablier + + + + + + +
+
+

Starting {{ .DisplayName }}...

+

Your instance(s) will stop after {{ .SessionDuration }} of inactivity.

+ {{ range $i, $request := .RequestStates }} +
+

sablier status {{ $request.Name }}

+ {{ if $request.Error }}

An error occured: {{ $request.Error }}

+ {{ else }}

{{ $request.Name }} is {{ $request.Status }} ({{ $request.CurrentReplicas }}/{{ $request.DesiredReplicas }})

{{ end }} +
+ {{ end }} +
+ + \ No newline at end of file diff --git a/app/http/pages/themes/matrix.html b/app/http/pages/themes/matrix.html new file mode 100644 index 0000000..74fc2fb --- /dev/null +++ b/app/http/pages/themes/matrix.html @@ -0,0 +1,259 @@ + + + + + + + + + Sablier + + + + + + + +
+ + +
+

Starting {{ .DisplayName }}...

+

Your instance(s) will stop after {{ .SessionDuration }} of inactivity.

+ +
+
    + {{- range $i, $request := .RequestStates }} +
  • + {{ $request.Name }}: + {{- if $request.Error }} + {{ $request.Error }} + {{- else }} + {{ $request.Status }} ({{ $request.CurrentReplicas }}/{{ $request.DesiredReplicas }}) + {{- end}} +
  • + {{ end -}} +
+
+
+ + + + + + \ No newline at end of file diff --git a/app/http/pages/themes/shuffle.html b/app/http/pages/themes/shuffle.html new file mode 100644 index 0000000..d9f24e9 --- /dev/null +++ b/app/http/pages/themes/shuffle.html @@ -0,0 +1,168 @@ + + + + + + + + + Sablier + + + +
+
+
+ Starting {{ .DisplayName }}... + +
+ +
+
+ + + + \ No newline at end of file