feat(docs): add template preview (#1777)

pull/1783/head^2 v1.6.0
nils måsén 1 year ago committed by GitHub
parent 9b28fbc24d
commit 9180e9558e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -17,6 +17,12 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: 1.18.x
- name: Build tplprev
run: scripts/build-tplprev.sh
- name: Setup python - name: Setup python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:

3
.gitignore vendored

@ -8,3 +8,6 @@ dist
/site /site
coverage.out coverage.out
*.coverprofile *.coverprofile
docs/assets/wasm_exec.js
docs/assets/*.wasm

@ -0,0 +1,219 @@
<style>
#tplprev {
margin: 0;
display: flex;
flex-direction: column;
row-gap: 1rem;
box-sizing: border-box;
position: relative;
margin-right: -13.3rem
}
#tplprev textarea {
box-decoration-break: slice;
overflow: auto;
padding: 0.77em 1.18em;
scrollbar-color: var(--md-default-fg-color--lighter) transparent;
scrollbar-width: thin;
touch-action: auto;
word-break: normal;
height: 420px;
flex: 1;
}
#tplprev .controls {
display: flex;
flex-direction: row;
column-gap: 0.5rem
}
#tplprev textarea, #tplprev input {
background-color: var(--md-code-bg-color);
border-width: 0;
border-radius: 0.1rem;
color: var(--md-code-fg-color);
font-feature-settings: "kern";
font-family: var(--md-code-font-family);
}
.numfield {
font-size: .7rem;
display: flex;
flex-direction: column;
justify-content: space-between;
}
#tplprev button {
border-radius: 0.1rem;
color: var(--md-typeset-color);
background-color: var(--md-primary-fg-color);
flex:1;
min-width: 12ch;
padding: 0.5rem
}
#tplprev button:hover {
background-color: var(--md-accent-fg-color);
}
#tplprev input[type="number"] { width: 5ch; flex: 1; font-size: 1rem; }
#tplprev fieldset {
margin-top: -0.5rem;
display: flex;
flex: 1;
column-gap: 0.5rem;
}
#tplprev .template-wrapper {
display: flex;
flex:1;
column-gap: 1rem;
}
#tplprev .result-wrapper {
flex: 1;
display: flex
}
#result {
font-size: 0.7rem;
background-color: var(--md-code-bg-color);
scrollbar-color: var(--md-default-fg-color--lighter) transparent;
scrollbar-width: thin;
touch-action: auto;
overflow: auto;
padding: 0.77em 1.18em;
margin:0;
height: 540px;
flex:1;
width:100%
}
#tplprev .loading {
position: absolute;
inset: 0;
display: flex;
padding: 1rem;
box-sizing: border-box;
background: var(--md-code-bg-color);
margin-top: 0
}
</style>
<script src="../assets/wasm_exec.js"></script>
<script>
const updatePreview = () => {
const form = document.querySelector('#tplprev');
const input = form.template.value;
console.log('Input: %o', input);
const arrFromCount = (key) => Array.from(Array(form[key]?.valueAsNumber ?? 0), () => key);
const states = form.enablereport.checked ? [
...arrFromCount("skipped"),
...arrFromCount("scanned"),
...arrFromCount("updated"),
...arrFromCount("failed" ),
...arrFromCount("fresh" ),
...arrFromCount("stale" ),
] : [];
console.log("States: %o", states);
const levels = form.enablelog.checked ? [
...arrFromCount("error"),
...arrFromCount("warning"),
...arrFromCount("info"),
...arrFromCount("debug"),
] : [];
console.log("Levels: %o", levels);
const output = WATCHTOWER.tplprev(input, states, levels);
console.log('Output: \n%o', output);
if (output.length) {
document.querySelector('#result').innerText = output;
} else {
document.querySelector('#result').innerHTML = '<i>empty (would not be sent as a notification)</i>';
}
}
const formSubmitted = (e) => {
e.preventDefault();
updatePreview();
}
let debounce;
const inputUpdated = () => {
if(debounce) clearTimeout(debounce);
debounce = setTimeout(() => updatePreview(), 400);
}
const formChanged = (e) => {
console.log('form changed: %o', e);
}
const go = new Go();
WebAssembly.instantiateStreaming(fetch("../assets/tplprev.wasm"), go.importObject).then((result) => {
document.querySelector('#tplprev .loading').style.display = "none";
go.run(result.instance);
updatePreview();
});
</script>
<form id="tplprev" onchange="updatePreview()" onsubmit="formSubmitted(event)">
<pre class="loading">loading wasm...</pre>
<div class="template-wrapper">
<textarea name="template" type="text" style="flex: 1" onkeyup="inputUpdated()">{{- with .Report -}}
{{- if ( or .Updated .Failed ) -}}
{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed
{{- range .Updated}}
- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}}
{{- end -}}
{{- range .Fresh}}
- {{.Name}} ({{.ImageName}}): {{.State}}
{{- end -}}
{{- range .Skipped}}
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
{{- end -}}
{{- range .Failed}}
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- if (and .Entries .Report) }}
Logs:
{{ end -}}
{{range .Entries -}}{{.Time.Format "2006-01-02T15:04:05Z07:00"}} [{{.Level}}] {{.Message}}{{"\n"}}{{- end -}}</textarea>
</div>
<div class="controls">
<fieldset>
<legend><label><input type="checkbox" name="enablereport" checked /> Container report</label></legend>
<label class="numfield">
Skipped:
<input type="number" name="skipped" value="3" />
</label>
<label class="numfield">
Scanned:
<input type="number" name="scanned" value="3" />
</label>
<label class="numfield">
Updated:
<input type="number" name="updated" value="3" />
</label>
<label class="numfield">
Failed:
<input type="number" name="failed" value="3" />
</label>
<label class="numfield">
Fresh:
<input type="number" name="fresh" value="3" />
</label>
<label class="numfield">
Stale:
<input type="number" name="stale" value="3" />
</label>
</fieldset>
<fieldset>
<legend><label><input type="checkbox" name="enablelog" checked /> Log entries</label></legend>
<label class="numfield">
Error:
<input type="number" name="error" value="1" />
</label>
<label class="numfield">
Warning:
<input type="number" name="warning" value="2" />
</label>
<label class="numfield">
Info:
<input type="number" name="info" value="3" />
</label>
<label class="numfield">
Debug:
<input type="number" name="debug" value="4" />
</label>
</fieldset>
<button type="submit">Update preview</button>
</div>
<div style="result-wrapper">
<pre id="result"></pre>
</div>
</form>

@ -59,13 +59,3 @@ func marshalReports(reports []t.ContainerReport) []jsonMap {
} }
var _ json.Marshaler = &Data{} var _ json.Marshaler = &Data{}
func toJSON(v interface{}) string {
var bytes []byte
var err error
if bytes, err = json.MarshalIndent(v, "", " "); err != nil {
LocalLog.Errorf("failed to marshal JSON in notification template: %v", err)
return ""
}
return string(bytes)
}

@ -0,0 +1,143 @@
package data
import (
"encoding/hex"
"errors"
"math/rand"
"strconv"
"time"
"github.com/containrrr/watchtower/pkg/types"
)
type previewData struct {
rand *rand.Rand
lastTime time.Time
report *report
containerCount int
Entries []*logEntry
StaticData staticData
}
type staticData struct {
Title string
Host string
}
// New initializes a new preview data struct
func New() *previewData {
return &previewData{
rand: rand.New(rand.NewSource(1)),
lastTime: time.Now().Add(-30 * time.Minute),
report: nil,
containerCount: 0,
Entries: []*logEntry{},
StaticData: staticData{
Title: "Title",
Host: "Host",
},
}
}
// AddFromState adds a container status entry to the report with the given state
func (pb *previewData) AddFromState(state State) {
cid := types.ContainerID(pb.generateID())
old := types.ImageID(pb.generateID())
new := types.ImageID(pb.generateID())
name := pb.generateName()
image := pb.generateImageName(name)
var err error
if state == FailedState {
err = errors.New(pb.randomEntry(errorMessages))
} else if state == SkippedState {
err = errors.New(pb.randomEntry(skippedMessages))
}
pb.addContainer(containerStatus{
containerID: cid,
oldImage: old,
newImage: new,
containerName: name,
imageName: image,
error: err,
state: state,
})
}
func (pb *previewData) addContainer(c containerStatus) {
if pb.report == nil {
pb.report = &report{}
}
switch c.state {
case ScannedState:
pb.report.scanned = append(pb.report.scanned, &c)
case UpdatedState:
pb.report.updated = append(pb.report.updated, &c)
case FailedState:
pb.report.failed = append(pb.report.failed, &c)
case SkippedState:
pb.report.skipped = append(pb.report.skipped, &c)
case StaleState:
pb.report.stale = append(pb.report.stale, &c)
case FreshState:
pb.report.fresh = append(pb.report.fresh, &c)
default:
return
}
pb.containerCount += 1
}
// AddLogEntry adds a preview log entry of the given level
func (pd *previewData) AddLogEntry(level LogLevel) {
var msg string
switch level {
case FatalLevel:
fallthrough
case ErrorLevel:
fallthrough
case WarnLevel:
msg = pd.randomEntry(logErrors)
default:
msg = pd.randomEntry(logMessages)
}
pd.Entries = append(pd.Entries, &logEntry{
Message: msg,
Data: map[string]any{},
Time: pd.generateTime(),
Level: level,
})
}
// Report returns a preview report
func (pb *previewData) Report() types.Report {
return pb.report
}
func (pb *previewData) generateID() string {
buf := make([]byte, 32)
_, _ = pb.rand.Read(buf)
return hex.EncodeToString(buf)
}
func (pb *previewData) generateTime() time.Time {
pb.lastTime = pb.lastTime.Add(time.Duration(pb.rand.Intn(30)) * time.Second)
return pb.lastTime
}
func (pb *previewData) randomEntry(arr []string) string {
return arr[pb.rand.Intn(len(arr))]
}
func (pb *previewData) generateName() string {
index := pb.containerCount
if index <= len(containerNames) {
return "/" + containerNames[index]
}
suffix := index / len(containerNames)
index %= len(containerNames)
return "/" + containerNames[index] + strconv.FormatInt(int64(suffix), 10)
}
func (pb *previewData) generateImageName(name string) string {
index := pb.containerCount % len(organizationNames)
return organizationNames[index] + name + ":latest"
}

@ -0,0 +1,56 @@
package data
import (
"time"
)
type logEntry struct {
Message string
Data map[string]any
Time time.Time
Level LogLevel
}
// LogLevel is the analog of logrus.Level
type LogLevel string
const (
TraceLevel LogLevel = "trace"
DebugLevel LogLevel = "debug"
InfoLevel LogLevel = "info"
WarnLevel LogLevel = "warning"
ErrorLevel LogLevel = "error"
FatalLevel LogLevel = "fatal"
PanicLevel LogLevel = "panic"
)
// LevelsFromString parses a string of level characters and returns a slice of the corresponding log levels
func LevelsFromString(str string) []LogLevel {
levels := make([]LogLevel, 0, len(str))
for _, c := range str {
switch c {
case 'p':
levels = append(levels, PanicLevel)
case 'f':
levels = append(levels, FatalLevel)
case 'e':
levels = append(levels, ErrorLevel)
case 'w':
levels = append(levels, WarnLevel)
case 'i':
levels = append(levels, InfoLevel)
case 'd':
levels = append(levels, DebugLevel)
case 't':
levels = append(levels, TraceLevel)
default:
continue
}
}
return levels
}
// String returns the log level as a string
func (level LogLevel) String() string {
return string(level)
}

@ -0,0 +1,178 @@
package data
var containerNames = []string{
"cyberscribe",
"datamatrix",
"nexasync",
"quantumquill",
"aerosphere",
"virtuos",
"fusionflow",
"neuralink",
"pixelpulse",
"synthwave",
"codecraft",
"zapzone",
"robologic",
"dreamstream",
"infinisync",
"megamesh",
"novalink",
"xenogenius",
"ecosim",
"innovault",
"techtracer",
"fusionforge",
"quantumquest",
"neuronest",
"codefusion",
"datadyno",
"pixelpioneer",
"vortexvision",
"cybercraft",
"synthsphere",
"infinitescript",
"roborhythm",
"dreamengine",
"aquasync",
"geniusgrid",
"megamind",
"novasync-pro",
"xenonwave",
"ecologic",
"innoscan",
}
var organizationNames = []string{
"techwave",
"codecrafters",
"innotechlabs",
"fusionsoft",
"cyberpulse",
"quantumscribe",
"datadynamo",
"neuralink",
"pixelpro",
"synthwizards",
"virtucorplabs",
"robologic",
"dreamstream",
"novanest",
"megamind",
"xenonwave",
"ecologic",
"innosync",
"techgenius",
"nexasoft",
"codewave",
"zapzone",
"techsphere",
"aquatech",
"quantumcraft",
"neuronest",
"datafusion",
"pixelpioneer",
"synthsphere",
"infinitescribe",
"roborhythm",
"dreamengine",
"vortexvision",
"geniusgrid",
"megamesh",
"novasync",
"xenogeniuslabs",
"ecosim",
"innovault",
}
var errorMessages = []string{
"Error 404: Resource not found",
"Critical Error: System meltdown imminent",
"Error 500: Internal server error",
"Invalid input: Please check your data",
"Access denied: Unauthorized access detected",
"Network connection lost: Please check your connection",
"Error 403: Forbidden access",
"Fatal error: System crash imminent",
"File not found: Check the file path",
"Invalid credentials: Authentication failed",
"Error 502: Bad Gateway",
"Database connection failed: Please try again later",
"Security breach detected: Take immediate action",
"Error 400: Bad request",
"Out of memory: Close unnecessary applications",
"Invalid configuration: Check your settings",
"Error 503: Service unavailable",
"File is read-only: Cannot modify",
"Data corruption detected: Backup your data",
"Error 401: Unauthorized",
"Disk space full: Free up disk space",
"Connection timeout: Retry your request",
"Error 504: Gateway timeout",
"File access denied: Permission denied",
"Unexpected error: Please contact support",
"Error 429: Too many requests",
"Invalid URL: Check the URL format",
"Database query failed: Try again later",
"Error 408: Request timeout",
"File is in use: Close the file and try again",
"Invalid parameter: Check your input",
"Error 502: Proxy error",
"Database connection lost: Reconnect and try again",
"File size exceeds limit: Reduce the file size",
"Error 503: Overloaded server",
"Operation aborted: Try again",
"Invalid API key: Check your API key",
"Error 507: Insufficient storage",
"Database deadlock: Retry your transaction",
"Error 405: Method not allowed",
"File format not supported: Choose a different format",
"Unknown error: Contact system administrator",
}
var skippedMessages = []string{
"Fear of introducing new bugs",
"Don't have time for the update process",
"Current version works fine for my needs",
"Concerns about compatibility with other software",
"Limited bandwidth for downloading updates",
"Worries about losing custom settings or configurations",
"Lack of trust in the software developer's updates",
"Dislike changes to the user interface",
"Avoiding potential subscription fees",
"Suspicion of hidden data collection in updates",
"Apprehension about changes in privacy policies",
"Prefer the older version's features or design",
"Worry about software becoming more resource-intensive",
"Avoiding potential changes in licensing terms",
"Waiting for initial bugs to be resolved in the update",
"Concerns about update breaking third-party plugins or extensions",
"Belief that the software is already secure enough",
"Don't want to relearn how to use the software",
"Fear of losing access to older file formats",
"Avoiding the hassle of having to update multiple devices",
}
var logMessages = []string{
"Checking for available updates...",
"Downloading update package...",
"Verifying update integrity...",
"Preparing to install update...",
"Backing up existing configuration...",
"Installing update...",
"Update installation complete.",
"Applying configuration settings...",
"Cleaning up temporary files...",
"Update successful! Software is now up-to-date.",
"Restarting the application...",
"Restart complete. Enjoy the latest features!",
"Update rollback complete. Your software remains at the previous version.",
}
var logErrors = []string{
"Unable to check for updates. Please check your internet connection.",
"Update package download failed. Try again later.",
"Update verification failed. Please contact support.",
"Update installation failed. Rolling back to the previous version...",
"Your configuration settings may have been reset to defaults.",
}

@ -0,0 +1,110 @@
package data
import (
"sort"
"github.com/containrrr/watchtower/pkg/types"
)
// State is the outcome of a container in a session report
type State string
const (
ScannedState State = "scanned"
UpdatedState State = "updated"
FailedState State = "failed"
SkippedState State = "skipped"
StaleState State = "stale"
FreshState State = "fresh"
)
// StatesFromString parses a string of state characters and returns a slice of the corresponding report states
func StatesFromString(str string) []State {
states := make([]State, 0, len(str))
for _, c := range str {
switch c {
case 'c':
states = append(states, ScannedState)
case 'u':
states = append(states, UpdatedState)
case 'e':
states = append(states, FailedState)
case 'k':
states = append(states, SkippedState)
case 't':
states = append(states, StaleState)
case 'f':
states = append(states, FreshState)
default:
continue
}
}
return states
}
type report struct {
scanned []types.ContainerReport
updated []types.ContainerReport
failed []types.ContainerReport
skipped []types.ContainerReport
stale []types.ContainerReport
fresh []types.ContainerReport
}
func (r *report) Scanned() []types.ContainerReport {
return r.scanned
}
func (r *report) Updated() []types.ContainerReport {
return r.updated
}
func (r *report) Failed() []types.ContainerReport {
return r.failed
}
func (r *report) Skipped() []types.ContainerReport {
return r.skipped
}
func (r *report) Stale() []types.ContainerReport {
return r.stale
}
func (r *report) Fresh() []types.ContainerReport {
return r.fresh
}
func (r *report) All() []types.ContainerReport {
allLen := len(r.scanned) + len(r.updated) + len(r.failed) + len(r.skipped) + len(r.stale) + len(r.fresh)
all := make([]types.ContainerReport, 0, allLen)
presentIds := map[types.ContainerID][]string{}
appendUnique := func(reports []types.ContainerReport) {
for _, cr := range reports {
if _, found := presentIds[cr.ID()]; found {
continue
}
all = append(all, cr)
presentIds[cr.ID()] = nil
}
}
appendUnique(r.updated)
appendUnique(r.failed)
appendUnique(r.skipped)
appendUnique(r.stale)
appendUnique(r.fresh)
appendUnique(r.scanned)
sort.Sort(sortableContainers(all))
return all
}
type sortableContainers []types.ContainerReport
// Len implements sort.Interface.Len
func (s sortableContainers) Len() int { return len(s) }
// Less implements sort.Interface.Less
func (s sortableContainers) Less(i, j int) bool { return s[i].ID() < s[j].ID() }
// Swap implements sort.Interface.Swap
func (s sortableContainers) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

@ -0,0 +1,44 @@
package data
import wt "github.com/containrrr/watchtower/pkg/types"
type containerStatus struct {
containerID wt.ContainerID
oldImage wt.ImageID
newImage wt.ImageID
containerName string
imageName string
error
state State
}
func (u *containerStatus) ID() wt.ContainerID {
return u.containerID
}
func (u *containerStatus) Name() string {
return u.containerName
}
func (u *containerStatus) CurrentImageID() wt.ImageID {
return u.oldImage
}
func (u *containerStatus) LatestImageID() wt.ImageID {
return u.newImage
}
func (u *containerStatus) ImageName() string {
return u.imageName
}
func (u *containerStatus) Error() string {
if u.error == nil {
return ""
}
return u.error.Error()
}
func (u *containerStatus) State() string {
return string(u.state)
}

@ -0,0 +1,36 @@
package preview
import (
"fmt"
"strings"
"text/template"
"github.com/containrrr/watchtower/pkg/notifications/preview/data"
"github.com/containrrr/watchtower/pkg/notifications/templates"
)
func Render(input string, states []data.State, loglevels []data.LogLevel) (string, error) {
data := data.New()
tpl, err := template.New("").Funcs(templates.Funcs).Parse(input)
if err != nil {
return "", fmt.Errorf("failed to parse template: %e", err)
}
for _, state := range states {
data.AddFromState(state)
}
for _, level := range loglevels {
data.AddLogEntry(level)
}
var buf strings.Builder
err = tpl.Execute(&buf, data)
if err != nil {
return "", fmt.Errorf("failed to execute template: %e", err)
}
return buf.String(), nil
}

@ -10,10 +10,9 @@ import (
"github.com/containrrr/shoutrrr" "github.com/containrrr/shoutrrr"
"github.com/containrrr/shoutrrr/pkg/types" "github.com/containrrr/shoutrrr/pkg/types"
"github.com/containrrr/watchtower/pkg/notifications/templates"
t "github.com/containrrr/watchtower/pkg/types" t "github.com/containrrr/watchtower/pkg/types"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/text/cases"
"golang.org/x/text/language"
) )
// LocalLog is a logrus logger that does not send entries as notifications // LocalLog is a logrus logger that does not send entries as notifications
@ -208,13 +207,8 @@ func (n *shoutrrrTypeNotifier) Fire(entry *log.Entry) error {
} }
func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template, err error) { func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template, err error) {
funcs := template.FuncMap{
"ToUpper": strings.ToUpper, tplBase := template.New("").Funcs(templates.Funcs)
"ToLower": strings.ToLower,
"ToJSON": toJSON,
"Title": cases.Title(language.AmericanEnglish).String,
}
tplBase := template.New("").Funcs(funcs)
if builtin, found := commonTemplates[tplString]; found { if builtin, found := commonTemplates[tplString]; found {
log.WithField(`template`, tplString).Debug(`Using common template`) log.WithField(`template`, tplString).Debug(`Using common template`)

@ -0,0 +1,27 @@
package templates
import (
"encoding/json"
"fmt"
"strings"
"text/template"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
var Funcs = template.FuncMap{
"ToUpper": strings.ToUpper,
"ToLower": strings.ToLower,
"ToJSON": toJSON,
"Title": cases.Title(language.AmericanEnglish).String,
}
func toJSON(v interface{}) string {
var bytes []byte
var err error
if bytes, err = json.MarshalIndent(v, "", " "); err != nil {
return fmt.Sprintf("failed to marshal JSON in notification template: %v", err)
}
return string(bytes)
}

@ -0,0 +1,7 @@
#!/bin/bash
cd $(git rev-parse --show-toplevel)
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" ./docs/assets/
GOARCH=wasm GOOS=js go build -o ./docs/assets/tplprev.wasm ./tplprev

@ -0,0 +1,49 @@
//go:build !wasm
package main
import (
"flag"
"fmt"
"os"
"github.com/containrrr/watchtower/internal/meta"
"github.com/containrrr/watchtower/pkg/notifications/preview"
"github.com/containrrr/watchtower/pkg/notifications/preview/data"
)
func main() {
fmt.Fprintf(os.Stderr, "watchtower/tplprev %v\n\n", meta.Version)
var states string
var entries string
flag.StringVar(&states, "states", "cccuuueeekkktttfff", "sCanned, Updated, failEd, sKipped, sTale, Fresh")
flag.StringVar(&entries, "entries", "ewwiiidddd", "Fatal,Error,Warn,Info,Debug,Trace")
flag.Parse()
if len(flag.Args()) < 1 {
fmt.Fprintln(os.Stderr, "Missing required argument TEMPLATE")
flag.Usage()
os.Exit(1)
return
}
input, err := os.ReadFile(flag.Arg(0))
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to read template file %q: %v\n", flag.Arg(0), err)
os.Exit(1)
return
}
result, err := preview.Render(string(input), data.StatesFromString(states), data.LevelsFromString(entries))
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to read template file %q: %v\n", flag.Arg(0), err)
os.Exit(1)
return
}
fmt.Println(result)
}

@ -0,0 +1,62 @@
//go:build wasm
package main
import (
"fmt"
"github.com/containrrr/watchtower/internal/meta"
"github.com/containrrr/watchtower/pkg/notifications/preview"
"github.com/containrrr/watchtower/pkg/notifications/preview/data"
"syscall/js"
)
func main() {
fmt.Println("watchtower/tplprev v" + meta.Version)
js.Global().Set("WATCHTOWER", js.ValueOf(map[string]any{
"tplprev": js.FuncOf(jsTplPrev),
}))
<-make(chan bool)
}
func jsTplPrev(this js.Value, args []js.Value) any {
if len(args) < 3 {
return "Requires 3 arguments passed"
}
input := args[0].String()
statesArg := args[1]
var states []data.State
if statesArg.Type() == js.TypeString {
states = data.StatesFromString(statesArg.String())
} else {
for i := 0; i < statesArg.Length(); i++ {
state := data.State(statesArg.Index(i).String())
states = append(states, state)
}
}
levelsArg := args[2]
var levels []data.LogLevel
if levelsArg.Type() == js.TypeString {
levels = data.LevelsFromString(statesArg.String())
} else {
for i := 0; i < levelsArg.Length(); i++ {
level := data.LogLevel(levelsArg.Index(i).String())
levels = append(levels, level)
}
}
result, err := preview.Render(input, states, levels)
if err != nil {
return "Error: " + err.Error()
}
return result
}
Loading…
Cancel
Save