mirror of
https://github.com/sablierapp/sablier.git
synced 2025-12-27 23:46:36 +01:00
feat: add ghost, hacker-terminal, matrix and shuffle themes
This commit is contained in:
100
app/http/pages/render.go
Normal file
100
app/http/pages/render.go
Normal file
@@ -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))
|
||||
}
|
||||
165
app/http/pages/render_test.go
Normal file
165
app/http/pages/render_test.go
Normal file
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
71
app/http/pages/themes/ghost.html
Normal file
71
app/http/pages/themes/ghost.html
Normal file
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<title>Sablier</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<meta http-equiv="refresh" content="{{ .RefreshFrequency }}" />
|
||||
<link rel="preconnect" href="https://fonts.bunny.net" crossorigin>
|
||||
<link rel="dns-prefetch" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css2?family=Open+Sans:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
html,body {background-color:#1a1a1a;color:#fff;font-family:'Open Sans',sans-serif;height:100vh;margin:0;font-size:0}
|
||||
.container {height:100vh;align-items:center;display:flex;justify-content:center;position:relative}
|
||||
.wrap {text-align:center}
|
||||
.ghost {animation:float 3s ease-out infinite}
|
||||
@keyframes float { 50% {transform:translate(0,20px)}}
|
||||
.shadowFrame {width:130px;margin: 10px auto 0 auto}
|
||||
.shadow {animation:shrink 3s ease-out infinite;transform-origin:center center}
|
||||
@keyframes shrink {0%{width:90%;margin:0 5%} 50% {width:60%;margin:0 18%} 100% {width:90%;margin:0 5%}}
|
||||
h3 {font-size:17px;text-transform: uppercase;margin:0.3em auto}
|
||||
.description {font-size:13px;color:#aaa}
|
||||
.details {color:#999;width:100%}
|
||||
.details table {width:100%}
|
||||
.details td {white-space:nowrap;font-size:11px}
|
||||
.details .name {text-align:right;padding-right:.6em;width:50%}
|
||||
.details .value {text-align:left;padding-left:.6em;font-family:'Lucida Console','Courier New',monospace}
|
||||
.details .value.error {color: rgb(231, 89, 82)}
|
||||
.details .value.success {color: rgb(82, 231, 142)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="wrap">
|
||||
<svg class="ghost" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="127.433px" height="132.743px" viewBox="0 0 127.433 132.743" xml:space="preserve">
|
||||
<path fill="#FFF6F4" d="M116.223,125.064c1.032-1.183,1.323-2.73,1.391-3.747V54.76c0,0-4.625-34.875-36.125-44.375 s-66,6.625-72.125,44l-0.781,63.219c0.062,4.197,1.105,6.177,1.808,7.006c1.94,1.811,5.408,3.465,10.099-0.6 c7.5-6.5,8.375-10,12.75-6.875s5.875,9.75,13.625,9.25s12.75-9,13.75-9.625s4.375-1.875,7,1.25s5.375,8.25,12.875,7.875 s12.625-8.375,12.625-8.375s2.25-3.875,7.25,0.375s7.625,9.75,14.375,8.125C114.739,126.01,115.412,125.902,116.223,125.064z"></path>
|
||||
<circle fill="#1a1a1a" cx="86.238" cy="57.885" r="6.667"></circle>
|
||||
<circle fill="#1a1a1a" cx="40.072" cy="57.885" r="6.667"></circle>
|
||||
<path fill="#1a1a1a" d="M71.916,62.782c0.05-1.108-0.809-2.046-1.917-2.095c-0.673-0.03-1.28,0.279-1.667,0.771 c-0.758,0.766-2.483,2.235-4.696,2.358c-1.696,0.094-3.438-0.625-5.191-2.137c-0.003-0.003-0.007-0.006-0.011-0.009l0.002,0.005 c-0.332-0.294-0.757-0.488-1.235-0.509c-1.108-0.049-2.046,0.809-2.095,1.917c-0.032,0.724,0.327,1.37,0.887,1.749 c-0.001,0-0.002-0.001-0.003-0.001c2.221,1.871,4.536,2.88,6.912,2.986c0.333,0.014,0.67,0.012,1.007-0.01 c3.163-0.191,5.572-1.942,6.888-3.166l0.452-0.453c0.021-0.019,0.04-0.041,0.06-0.061l0.034-0.034 c-0.007,0.007-0.015,0.014-0.021,0.02C71.666,63.771,71.892,63.307,71.916,62.782z"></path>
|
||||
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="18.614" cy="99.426" r="3.292"></circle>
|
||||
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="95.364" cy="28.676" r="3.291"></circle>
|
||||
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="24.739" cy="93.551" r="2.667"></circle>
|
||||
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="101.489" cy="33.051" r="2.666"></circle>
|
||||
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="18.738" cy="87.717" r="2.833"></circle>
|
||||
<path fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" d="M116.279,55.814c-0.021-0.286-2.323-28.744-30.221-41.012 c-7.806-3.433-15.777-5.173-23.691-5.173c-16.889,0-30.283,7.783-37.187,15.067c-9.229,9.736-13.84,26.712-14.191,30.259 l-0.748,62.332c0.149,2.133,1.389,6.167,5.019,6.167c1.891,0,4.074-1.083,6.672-3.311c4.96-4.251,7.424-6.295,9.226-6.295 c1.339,0,2.712,1.213,5.102,3.762c4.121,4.396,7.461,6.355,10.833,6.355c2.713,0,5.311-1.296,7.942-3.962 c3.104-3.145,5.701-5.239,8.285-5.239c2.116,0,4.441,1.421,7.317,4.473c2.638,2.8,5.674,4.219,9.022,4.219 c4.835,0,8.991-2.959,11.27-5.728l0.086-0.104c1.809-2.2,3.237-3.938,5.312-3.938c2.208,0,5.271,1.942,9.359,5.936 c0.54,0.743,3.552,4.674,6.86,4.674c1.37,0,2.559-0.65,3.531-1.932l0.203-0.268L116.279,55.814z M114.281,121.405 c-0.526,0.599-1.096,0.891-1.734,0.891c-2.053,0-4.51-2.82-5.283-3.907l-0.116-0.136c-4.638-4.541-7.975-6.566-10.82-6.566 c-3.021,0-4.884,2.267-6.857,4.667l-0.086,0.104c-1.896,2.307-5.582,4.999-9.725,4.999c-2.775,0-5.322-1.208-7.567-3.59 c-3.325-3.528-6.03-5.102-8.772-5.102c-3.278,0-6.251,2.332-9.708,5.835c-2.236,2.265-4.368,3.366-6.518,3.366 c-2.772,0-5.664-1.765-9.374-5.723c-2.488-2.654-4.29-4.395-6.561-4.395c-2.515,0-5.045,2.077-10.527,6.777 c-2.727,2.337-4.426,2.828-5.37,2.828c-2.662,0-3.017-4.225-3.021-4.225l0.745-62.163c0.332-3.321,4.767-19.625,13.647-28.995 c3.893-4.106,10.387-8.632,18.602-11.504c-0.458,0.503-0.744,1.165-0.744,1.898c0,1.565,1.269,2.833,2.833,2.833 c1.564,0,2.833-1.269,2.833-2.833c0-1.355-0.954-2.485-2.226-2.764c4.419-1.285,9.269-2.074,14.437-2.074 c7.636,0,15.336,1.684,22.887,5.004c26.766,11.771,29.011,39.047,29.027,39.251V121.405z"></path>
|
||||
</svg>
|
||||
<p class="shadowFrame">
|
||||
<svg class="shadow" xmlns="http://www.w3.org/2000/svg" x="61px" y="20px" width="122.436px" height="39.744px" viewBox="0 0 122.436 39.744" xml:space="preserve">
|
||||
<ellipse fill="#262626" cx="61.128" cy="19.872" rx="49.25" ry="8.916"></ellipse>
|
||||
</svg>
|
||||
</p>
|
||||
<h3><span>Starting</span> {{ .DisplayName }}</h3>
|
||||
<p class="description">Your instance(s) will stop after {{ .SessionDuration }} of inactivity}</p>
|
||||
<div class="details">
|
||||
<table>
|
||||
{{- range $i, $request := .RequestStates }}
|
||||
<tr>
|
||||
<td class="name">{{ $request.Name }}</td>
|
||||
{{- if $request.Error }}
|
||||
<td class="value error">{{ $request.Error }}</td>
|
||||
{{- else }}
|
||||
<td class="value success">{{ $request.Status }} ({{ $request.CurrentReplicas }}/{{ $request.DesiredReplicas }})</td>
|
||||
{{- end}}
|
||||
</tr>
|
||||
{{ end -}}
|
||||
</table>
|
||||
</div>]
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
174
app/http/pages/themes/hacker-terminal.html
Normal file
174
app/http/pages/themes/hacker-terminal.html
Normal file
@@ -0,0 +1,174 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta http-equiv="refresh" content="{{ .RefreshFrequency }}" />
|
||||
<title>Sablier</title>
|
||||
<link rel="preconnect" href="https://fonts.bunny.net" crossorigin>
|
||||
<link rel="dns-prefetch" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css2?family=Inconsolata:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
/** Idea author: https://codepen.io/robinselmer */
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
box-sizing: border-box;
|
||||
background-color: #000000;
|
||||
background-image: radial-gradient(#11581E, #041607);
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
font-family: 'Inconsolata', Helvetica, sans-serif;
|
||||
color: rgba(128, 255, 128, 0.8);
|
||||
text-shadow:
|
||||
0 0 11px rgba(51, 255, 51, 1),
|
||||
0 0 2px rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.overlay {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background:
|
||||
repeating-linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 0, 0, 0) 0,
|
||||
rgba(0, 0, 0, 0.3) 50%,
|
||||
rgba(0, 0, 0, 0) 100%);
|
||||
background-size: auto 4px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.overlay::before {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: linear-gradient(
|
||||
0deg,
|
||||
transparent 0%,
|
||||
rgba(32, 128, 32, 0.2) 2%,
|
||||
rgba(32, 128, 32, 0.8) 3%,
|
||||
rgba(32, 128, 32, 0.2) 3%,
|
||||
transparent 100%);
|
||||
background-repeat: no-repeat;
|
||||
animation: scan 7.5s linear 0s infinite;
|
||||
}
|
||||
|
||||
@keyframes scan {
|
||||
0% { background-position: 0 -100vh; }
|
||||
35%, 100% { background-position: 0 100vh; }
|
||||
}
|
||||
|
||||
.terminal {
|
||||
box-sizing: inherit;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 1000px;
|
||||
max-width: 100%;
|
||||
padding: 64px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.output {
|
||||
color: rgba(128, 255, 128, 0.8);
|
||||
text-shadow:
|
||||
0 0 1px rgba(51, 255, 51, 0.4),
|
||||
0 0 2px rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.output.error {
|
||||
color: rgba(255, 128, 128, 0.8);
|
||||
text-shadow:
|
||||
0 0 1px rgba(255, 51, 51, 0.4),
|
||||
0 0 2px rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.output::before {
|
||||
content: "> ";
|
||||
}
|
||||
|
||||
.output.error::before {
|
||||
content: "X ";
|
||||
}
|
||||
|
||||
.output.success::before {
|
||||
content: "√ ";
|
||||
}
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a::before {
|
||||
content: "[";
|
||||
}
|
||||
|
||||
a::after {
|
||||
content: "]";
|
||||
}
|
||||
|
||||
.error_code {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.details {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.details p {
|
||||
margin-top: .5em;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
.details * {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.details .command::before {
|
||||
content: "$ ";
|
||||
}
|
||||
|
||||
.details code {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="overlay"></div>
|
||||
<div class="terminal">
|
||||
<h1><span>Starting </span> <span class="error_code">{{ .DisplayName }}</span>...</h1>
|
||||
<p class="output"><span>Your instance(s) will stop after {{ .SessionDuration }} of inactivity</span>.</p>
|
||||
{{ range $i, $request := .RequestStates }}
|
||||
<div class="details">
|
||||
<p class="output small command"><span>sablier status <span class="error_code">{{ $request.Name }}</span></span></code></p>
|
||||
{{ if $request.Error }}<p class="output small error">An error occured</span>: <code>{{ $request.Error }}</code></p>
|
||||
{{ else }}<p class="output small success"><span>{{ $request.Name }}</span> is {{ $request.Status }} <code>({{ $request.CurrentReplicas }}/{{ $request.DesiredReplicas }})</code></p>{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
259
app/http/pages/themes/matrix.html
Normal file
259
app/http/pages/themes/matrix.html
Normal file
@@ -0,0 +1,259 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta http-equiv="refresh" content="{{ .RefreshFrequency }}" />
|
||||
<title>Sablier</title>
|
||||
<link rel="preconnect" href="https://fonts.bunny.net" crossorigin>
|
||||
<link rel="dns-prefetch" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css2?family=Inconsolata:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{--matrix-glyph-size:15px;--matrix-glyph-font-size:15px;--matrix-glyph-front-color:rgba(255, 255, 255, 0.8);--matrix-glyph-tail-color:#0f0;--matrix-overlay-color:rgba(0, 0, 0, 0.12)}
|
||||
body,html{margin:0;padding:0;background-color:#000;height:100vh}
|
||||
#matrix{display:block;position:fixed;width:100vw;height:100vh}
|
||||
.container{align-items:center;display:flex;justify-content:center;position:absolute;top:0;left:0;width:100vw;height:100vh;z-index:1}
|
||||
.container .message{background-color:rgba(0,0,0,.85);border:2px solid var(--matrix-glyph-tail-color);padding:15px 20px;margin:0 20px;font-family:Inconsolata,Helvetica,sans-serif;text-align:center;font-size:0;color:var(--matrix-glyph-tail-color);text-shadow:1px 0 2px var(--matrix-glyph-tail-color),-1px 0 2px var(--matrix-glyph-tail-color);box-shadow:1px 0 5px var(--matrix-glyph-tail-color),-1px 0 2px var(--matrix-glyph-tail-color);max-width:640px}
|
||||
.container .message h1{margin:0;font-size:52px}
|
||||
.container .message p{margin:.3em 0 0 0;font-size:17px;color:var(--matrix-glyph-front-color)}
|
||||
.container .details{margin-top:20px}
|
||||
.container .details ul{padding:0}
|
||||
.container .details code,.container .details span{font-size:11px}
|
||||
.container .details code{padding-left:7px}
|
||||
.hidden {display:none}
|
||||
span.success {color:var(--matrix-glyph-front-color)}
|
||||
span.error {color:#A5524C;text-shadow:1px 0 2px #b44038,-1px 0 2px #b44038}
|
||||
@media screen and (max-width:820px){
|
||||
:root{--matrix-glyph-size:10px;--matrix-glyph-font-size:10px}
|
||||
.container .message h1{font-size:38px}
|
||||
.container .message p{font-size:13px}
|
||||
.container .details code,.container .details span{font-size:11px}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<ul id="matrix-words" class="hidden">
|
||||
<li>{{ .DisplayName }}</li>
|
||||
<li>{{ .SessionDuration }}</li>
|
||||
<li>{{ .Version }}</li>
|
||||
</ul>
|
||||
|
||||
<div class="message">
|
||||
<h1>Starting <span>{{ .DisplayName }}...</span></h1>
|
||||
<p>Your instance(s) will stop after {{ .SessionDuration }} of inactivity.</p>
|
||||
|
||||
<div class="details">
|
||||
<ul>
|
||||
{{- range $i, $request := .RequestStates }}
|
||||
<li>
|
||||
<span><span>{{ $request.Name }}</span>:</span>
|
||||
{{- if $request.Error }}
|
||||
<span class="error">{{ $request.Error }}</span>
|
||||
{{- else }}
|
||||
<span class="success">{{ $request.Status }} ({{ $request.CurrentReplicas }}/{{ $request.DesiredReplicas }})</span>
|
||||
{{- end}}
|
||||
</li>
|
||||
{{ end -}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<canvas id="matrix"></canvas>
|
||||
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @param {HTMLCanvasElement} $canvas
|
||||
* @constructor
|
||||
*/
|
||||
const Matrix = function ($canvas) {
|
||||
const symbols = 'ラドクリフマラソンわたしワタシんょンョたばこタバコとうきょうトウキョウ '.split('');
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
const getRandomSymbol = function () {
|
||||
return symbols[Math.floor(Math.random() * symbols.length)];
|
||||
}
|
||||
|
||||
const ctx = $canvas.getContext('2d');
|
||||
ctx.globalCompositeOperation = 'lighter'; // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
|
||||
|
||||
this.redrawInterval = 90; // fade oud speed
|
||||
this.glyphSize = 0;
|
||||
this.rowsCapacity = 0;
|
||||
this.columnsCapacity = 0;
|
||||
|
||||
/**
|
||||
* @return {void}
|
||||
*/
|
||||
this.init = () => {
|
||||
$canvas.width = $canvas.clientWidth;
|
||||
$canvas.height = $canvas.clientHeight;
|
||||
|
||||
this.glyphSize = parseInt(getComputedStyle($canvas).getPropertyValue('--matrix-glyph-size'), 10);
|
||||
this.rowsCapacity = Math.ceil($canvas.clientHeight / this.glyphSize);
|
||||
this.columnsCapacity = Math.ceil($canvas.clientWidth / this.glyphSize);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} symbol
|
||||
* @param {number} row
|
||||
* @param {number} column
|
||||
* @param {string} color
|
||||
*/
|
||||
const drawSymbol = (symbol, row, column, color) => {
|
||||
if (row > this.rowsCapacity || column > this.columnsCapacity) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.font = getComputedStyle($canvas).getPropertyValue('--matrix-glyph-font-size') + ' monospace';
|
||||
|
||||
if (symbol.length > 1) {
|
||||
symbol = symbol.charAt(0); // only one char is allowed
|
||||
}
|
||||
|
||||
let xOffset = 0, charCode = symbol.charCodeAt(0);
|
||||
|
||||
if (charCode >= 33 && charCode <= 126) { // is ascii
|
||||
xOffset = this.glyphSize / 5;
|
||||
}
|
||||
|
||||
ctx.fillText(symbol, (column * this.glyphSize) + xOffset, row * this.glyphSize);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {number} column
|
||||
* @param {number} speed Lowest = fastest, largest = slowest
|
||||
* @param {string?} text
|
||||
* @param {number?} offset
|
||||
*/
|
||||
const drawLine = (column, speed, text, offset) => {
|
||||
let cursor = 0;
|
||||
|
||||
const tailColor = getComputedStyle($canvas).getPropertyValue('--matrix-glyph-tail-color'),
|
||||
frontColor = getComputedStyle($canvas).getPropertyValue('--matrix-glyph-front-color');
|
||||
|
||||
const handler = window.setInterval(() => {
|
||||
if (column > this.columnsCapacity) {
|
||||
return window.clearInterval(handler);
|
||||
}
|
||||
|
||||
if (cursor <= this.rowsCapacity) {
|
||||
let symbol = getRandomSymbol();
|
||||
|
||||
if (typeof text === 'string' && typeof offset === 'number') {
|
||||
if (cursor >= offset && text.length >= cursor - offset) {
|
||||
symbol = text.charAt(cursor - offset);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof symbol === 'string' && symbol !== ' ') {
|
||||
const prev = cursor;
|
||||
|
||||
window.setTimeout(() => { // redraw with a green color
|
||||
drawSymbol(symbol, prev, column, tailColor);
|
||||
}, speed / 1.3);
|
||||
|
||||
drawSymbol(symbol, cursor, column, frontColor); // white color first
|
||||
}
|
||||
|
||||
cursor++;
|
||||
} else {
|
||||
window.clearInterval(handler);
|
||||
}
|
||||
}, speed);
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {void}
|
||||
*/
|
||||
this.redraw = () => {
|
||||
ctx.fillStyle = getComputedStyle($canvas).getPropertyValue('--matrix-overlay-color');
|
||||
ctx.fillRect(0, 0, $canvas.clientWidth, $canvas.clientHeight);
|
||||
};
|
||||
|
||||
let redrawIntervalHandler = undefined, dropsIntervalHandler = undefined;
|
||||
|
||||
/**
|
||||
* @param {HTMLUListElement?} $linesList
|
||||
*/
|
||||
this.run = ($linesList) => {
|
||||
if (redrawIntervalHandler === undefined) {
|
||||
redrawIntervalHandler = window.setInterval(this.redraw, this.redrawInterval);
|
||||
}
|
||||
|
||||
if (dropsIntervalHandler === undefined) {
|
||||
const fn = () => {
|
||||
const randomColumn = Math.floor(Math.random() * (this.columnsCapacity + 1)),
|
||||
minSpeed = 200, maxSpeed = 50,
|
||||
randomSpeed = Math.floor(Math.random() * (maxSpeed - minSpeed + 1)) + minSpeed;
|
||||
|
||||
const list = [];
|
||||
let line = undefined, offset = undefined;
|
||||
|
||||
if ($linesList !== undefined) {
|
||||
Array.prototype.forEach.call($linesList.querySelectorAll('li'), $li => {
|
||||
const text = $li.innerText.trim();
|
||||
|
||||
if (text.length > 0) {
|
||||
list.push(text);
|
||||
}
|
||||
});
|
||||
|
||||
if (list.length > 0 && Math.random() > 0.4) {
|
||||
line = list[Math.floor(Math.random() * list.length)];
|
||||
offset = Math.floor(Math.random() * line.length);
|
||||
|
||||
if (offset <= 5) {
|
||||
offset *= 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawLine(randomColumn, randomSpeed, line, offset);
|
||||
|
||||
if (dropsIntervalHandler !== undefined) {
|
||||
window.clearInterval(dropsIntervalHandler);
|
||||
dropsIntervalHandler = undefined;
|
||||
}
|
||||
|
||||
dropsIntervalHandler = window.setInterval(fn, ((minSpeed + maxSpeed) / 2 * this.rowsCapacity) / this.columnsCapacity / 0.5);
|
||||
};
|
||||
|
||||
fn();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {void}
|
||||
*/
|
||||
this.stop = () => {
|
||||
if (redrawIntervalHandler !== undefined) {
|
||||
window.clearInterval(redrawIntervalHandler);
|
||||
redrawIntervalHandler = undefined;
|
||||
}
|
||||
|
||||
if (dropsIntervalHandler !== undefined) {
|
||||
window.clearInterval(dropsIntervalHandler);
|
||||
dropsIntervalHandler = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof ResizeObserver === 'function') {
|
||||
(new ResizeObserver(this.init)).observe($canvas);
|
||||
} else {
|
||||
this.init();
|
||||
}
|
||||
};
|
||||
|
||||
(new Matrix(document.getElementById('matrix'))).run(document.getElementById('matrix-words'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
168
app/http/pages/themes/shuffle.html
Normal file
168
app/http/pages/themes/shuffle.html
Normal file
@@ -0,0 +1,168 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta http-equiv="refresh" content="{{ .RefreshFrequency }}" />
|
||||
<title>Sablier</title>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
background-color: #222;
|
||||
color: #aaa;
|
||||
font-family: 'Hack', monospace;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.full-height {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#error_text {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
#details table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
box-sizing: border-box;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#details.hidden td {
|
||||
opacity: 0;
|
||||
font-size: 0;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
#details td {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
padding-top: .5em;
|
||||
transition: opacity 1s, font-size .3s, color 1.2s;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#details td.name {
|
||||
text-align: right;
|
||||
padding-right: .3em;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
#details td.value {
|
||||
text-align: left;
|
||||
padding-left: .3em;
|
||||
font-family: 'Lucida Console', 'Courier New', monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="flex-center full-height">
|
||||
<div>
|
||||
<div id="error_text">
|
||||
<span class="source">Starting <span>{{ .DisplayName }}...</span></span>
|
||||
<span class="target"></span>
|
||||
</div>
|
||||
<div class="hidden" id="details">
|
||||
<table>
|
||||
{{- range $i, $request := .RequestStates }}
|
||||
<tr>
|
||||
<td class="name">{{ $request.Name }}</td>
|
||||
{{- if $request.Error }}
|
||||
<td class="value error">{{ $request.Error }}</td>
|
||||
{{- else }}
|
||||
<td class="value success">{{ $request.Status }} ({{ $request.CurrentReplicas }}/{{ $request.DesiredReplicas }})</td>
|
||||
{{- end}}
|
||||
</tr>
|
||||
{{ end -}}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} $el
|
||||
*/
|
||||
const Shuffle = function ($el) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-=+<>,./?[{()}]!@#$%^&*~`\|'.split(''),
|
||||
$source = $el.querySelector('.source'), $target = $el.querySelector('.target');
|
||||
|
||||
let cursor = 0, scrambleInterval = undefined, cursorDelayInterval = undefined, cursorInterval = undefined;
|
||||
|
||||
/**
|
||||
* @param {Number} len
|
||||
* @return {string}
|
||||
*/
|
||||
const getRandomizedString = function (len) {
|
||||
let s = '';
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
s += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
|
||||
return s;
|
||||
};
|
||||
|
||||
this.start = function () {
|
||||
$source.style.display = 'none';
|
||||
$target.style.display = 'block';
|
||||
|
||||
scrambleInterval = window.setInterval(() => {
|
||||
if (cursor <= $source.innerText.length) {
|
||||
$target.innerText = $source.innerText.substring(0, cursor) + getRandomizedString($source.innerText.length - cursor);
|
||||
}
|
||||
}, 200 / 30);
|
||||
|
||||
cursorDelayInterval = window.setTimeout(() => {
|
||||
cursorInterval = window.setInterval(() => {
|
||||
if (cursor > $source.innerText.length - 1) {
|
||||
this.stop();
|
||||
}
|
||||
|
||||
cursor++;
|
||||
}, 40);
|
||||
}, 200);
|
||||
};
|
||||
|
||||
this.stop = function () {
|
||||
$source.style.display = 'block';
|
||||
$target.style.display = 'none';
|
||||
$target.innerText = '';
|
||||
cursor = 0;
|
||||
|
||||
if (scrambleInterval !== undefined) {
|
||||
window.clearInterval(scrambleInterval);
|
||||
scrambleInterval = undefined;
|
||||
}
|
||||
|
||||
if (cursorInterval !== undefined) {
|
||||
window.clearInterval(cursorInterval);
|
||||
cursorInterval = undefined;
|
||||
}
|
||||
|
||||
if (cursorDelayInterval !== undefined) {
|
||||
window.clearInterval(cursorDelayInterval);
|
||||
cursorDelayInterval = undefined;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
(new Shuffle(document.getElementById('error_text'))).start();
|
||||
|
||||
window.setTimeout(function () {
|
||||
document.getElementById('details').classList.remove('hidden');
|
||||
}, 200);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user