mirror of
https://github.com/Xevion/glance.git
synced 2025-12-09 10:07:24 -06:00
Merge branch 'release/v0.7.0' into main
This commit is contained in:
48
internal/widget/container.go
Normal file
48
internal/widget/container.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package widget
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type containerWidgetBase struct {
|
||||
Widgets Widgets `yaml:"widgets"`
|
||||
}
|
||||
|
||||
func (widget *containerWidgetBase) Update(ctx context.Context) {
|
||||
var wg sync.WaitGroup
|
||||
now := time.Now()
|
||||
|
||||
for w := range widget.Widgets {
|
||||
widget := widget.Widgets[w]
|
||||
|
||||
if !widget.RequiresUpdate(&now) {
|
||||
continue
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
widget.Update(ctx)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (widget *containerWidgetBase) SetProviders(providers *Providers) {
|
||||
for i := range widget.Widgets {
|
||||
widget.Widgets[i].SetProviders(providers)
|
||||
}
|
||||
}
|
||||
|
||||
func (widget *containerWidgetBase) RequiresUpdate(now *time.Time) bool {
|
||||
for i := range widget.Widgets {
|
||||
if widget.Widgets[i].RequiresUpdate(now) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
70
internal/widget/custom-api.go
Normal file
70
internal/widget/custom-api.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package widget
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/glanceapp/glance/internal/assets"
|
||||
"github.com/glanceapp/glance/internal/feed"
|
||||
)
|
||||
|
||||
type CustomApi struct {
|
||||
widgetBase `yaml:",inline"`
|
||||
URL OptionalEnvString `yaml:"url"`
|
||||
Template string `yaml:"template"`
|
||||
Frameless bool `yaml:"frameless"`
|
||||
Headers map[string]OptionalEnvString `yaml:"headers"`
|
||||
APIRequest *http.Request `yaml:"-"`
|
||||
compiledTemplate *template.Template `yaml:"-"`
|
||||
CompiledHTML template.HTML `yaml:"-"`
|
||||
}
|
||||
|
||||
func (widget *CustomApi) Initialize() error {
|
||||
widget.withTitle("Custom API").withCacheDuration(1 * time.Hour)
|
||||
|
||||
if widget.URL == "" {
|
||||
return errors.New("URL is required for the custom API widget")
|
||||
}
|
||||
|
||||
if widget.Template == "" {
|
||||
return errors.New("template is required for the custom API widget")
|
||||
}
|
||||
|
||||
compiledTemplate, err := template.New("").Funcs(feed.CustomAPITemplateFuncs).Parse(widget.Template)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed parsing custom API widget template: %w", err)
|
||||
}
|
||||
|
||||
widget.compiledTemplate = compiledTemplate
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, widget.URL.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for key, value := range widget.Headers {
|
||||
req.Header.Add(key, value.String())
|
||||
}
|
||||
|
||||
widget.APIRequest = req
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (widget *CustomApi) Update(ctx context.Context) {
|
||||
compiledHTML, err := feed.FetchAndParseCustomAPI(widget.APIRequest, widget.compiledTemplate)
|
||||
if !widget.canContinueUpdateAfterHandlingErr(err) {
|
||||
return
|
||||
}
|
||||
|
||||
widget.CompiledHTML = compiledHTML
|
||||
}
|
||||
|
||||
func (widget *CustomApi) Render() template.HTML {
|
||||
return widget.render(widget, assets.CustomAPITemplate)
|
||||
}
|
||||
@@ -12,12 +12,13 @@ import (
|
||||
)
|
||||
|
||||
type Extension struct {
|
||||
widgetBase `yaml:",inline"`
|
||||
URL string `yaml:"url"`
|
||||
Parameters map[string]string `yaml:"parameters"`
|
||||
AllowHtml bool `yaml:"allow-potentially-dangerous-html"`
|
||||
Extension feed.Extension `yaml:"-"`
|
||||
cachedHTML template.HTML `yaml:"-"`
|
||||
widgetBase `yaml:",inline"`
|
||||
URL string `yaml:"url"`
|
||||
FallbackContentType string `yaml:"fallback-content-type"`
|
||||
Parameters map[string]string `yaml:"parameters"`
|
||||
AllowHtml bool `yaml:"allow-potentially-dangerous-html"`
|
||||
Extension feed.Extension `yaml:"-"`
|
||||
cachedHTML template.HTML `yaml:"-"`
|
||||
}
|
||||
|
||||
func (widget *Extension) Initialize() error {
|
||||
@@ -38,9 +39,10 @@ func (widget *Extension) Initialize() error {
|
||||
|
||||
func (widget *Extension) Update(ctx context.Context) {
|
||||
extension, err := feed.FetchExtension(feed.ExtensionRequestOptions{
|
||||
URL: widget.URL,
|
||||
Parameters: widget.Parameters,
|
||||
AllowHtml: widget.AllowHtml,
|
||||
URL: widget.URL,
|
||||
FallbackContentType: widget.FallbackContentType,
|
||||
Parameters: widget.Parameters,
|
||||
AllowHtml: widget.AllowHtml,
|
||||
})
|
||||
|
||||
widget.canContinueUpdateAfterHandlingErr(err)
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
var HSLColorPattern = regexp.MustCompile(`^(?:hsla?\()?(\d{1,3})(?: |,)+(\d{1,3})%?(?: |,)+(\d{1,3})%?\)?$`)
|
||||
var EnvFieldPattern = regexp.MustCompile(`^\${([A-Z_]+)}$`)
|
||||
var EnvFieldPattern = regexp.MustCompile(`(^|.)\$\{([A-Z_]+)\}`)
|
||||
|
||||
const (
|
||||
HSLHueMax = 360
|
||||
@@ -133,21 +133,42 @@ func (f *OptionalEnvString) UnmarshalYAML(node *yaml.Node) error {
|
||||
return err
|
||||
}
|
||||
|
||||
matches := EnvFieldPattern.FindStringSubmatch(value)
|
||||
replaced := EnvFieldPattern.ReplaceAllStringFunc(value, func(whole string) string {
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(matches) != 2 {
|
||||
*f = OptionalEnvString(value)
|
||||
groups := EnvFieldPattern.FindStringSubmatch(whole)
|
||||
|
||||
return nil
|
||||
if len(groups) != 3 {
|
||||
return whole
|
||||
}
|
||||
|
||||
prefix, key := groups[1], groups[2]
|
||||
|
||||
if prefix == `\` {
|
||||
if len(whole) >= 2 {
|
||||
return whole[1:]
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
value, found := os.LookupEnv(key)
|
||||
|
||||
if !found {
|
||||
err = fmt.Errorf("environment variable %s not found", key)
|
||||
return ""
|
||||
}
|
||||
|
||||
return prefix + value
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
value, found := os.LookupEnv(matches[1])
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("environment variable %s not found", matches[1])
|
||||
}
|
||||
|
||||
*f = OptionalEnvString(value)
|
||||
*f = OptionalEnvString(replaced)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,15 +4,14 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"html/template"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/glanceapp/glance/internal/assets"
|
||||
)
|
||||
|
||||
type Group struct {
|
||||
widgetBase `yaml:",inline"`
|
||||
Widgets Widgets `yaml:"widgets"`
|
||||
widgetBase `yaml:",inline"`
|
||||
containerWidgetBase `yaml:",inline"`
|
||||
}
|
||||
|
||||
func (widget *Group) Initialize() error {
|
||||
@@ -23,7 +22,9 @@ func (widget *Group) Initialize() error {
|
||||
widget.Widgets[i].SetHideHeader(true)
|
||||
|
||||
if widget.Widgets[i].GetType() == "group" {
|
||||
return errors.New("nested groups are not allowed")
|
||||
return errors.New("nested groups are not supported")
|
||||
} else if widget.Widgets[i].GetType() == "split-column" {
|
||||
return errors.New("split columns inside of groups are not supported")
|
||||
}
|
||||
|
||||
if err := widget.Widgets[i].Initialize(); err != nil {
|
||||
@@ -35,40 +36,15 @@ func (widget *Group) Initialize() error {
|
||||
}
|
||||
|
||||
func (widget *Group) Update(ctx context.Context) {
|
||||
var wg sync.WaitGroup
|
||||
now := time.Now()
|
||||
|
||||
for w := range widget.Widgets {
|
||||
widget := widget.Widgets[w]
|
||||
|
||||
if !widget.RequiresUpdate(&now) {
|
||||
continue
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
widget.Update(ctx)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
widget.containerWidgetBase.Update(ctx)
|
||||
}
|
||||
|
||||
func (widget *Group) SetProviders(providers *Providers) {
|
||||
for i := range widget.Widgets {
|
||||
widget.Widgets[i].SetProviders(providers)
|
||||
}
|
||||
widget.containerWidgetBase.SetProviders(providers)
|
||||
}
|
||||
|
||||
func (widget *Group) RequiresUpdate(now *time.Time) bool {
|
||||
for i := range widget.Widgets {
|
||||
if widget.Widgets[i].RequiresUpdate(now) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return widget.containerWidgetBase.RequiresUpdate(now)
|
||||
}
|
||||
|
||||
func (widget *Group) Render() template.HTML {
|
||||
|
||||
@@ -38,6 +38,10 @@ func (widget *Markets) Update(ctx context.Context) {
|
||||
markets.SortByAbsChange()
|
||||
}
|
||||
|
||||
if widget.Sort == "change" {
|
||||
markets.SortByChange()
|
||||
}
|
||||
|
||||
widget.Markets = markets
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package widget
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -10,8 +11,8 @@ import (
|
||||
"github.com/glanceapp/glance/internal/feed"
|
||||
)
|
||||
|
||||
func statusCodeToText(status int) string {
|
||||
if status == 200 {
|
||||
func statusCodeToText(status int, altStatusCodes []int) string {
|
||||
if status == 200 || slices.Contains(altStatusCodes, status) {
|
||||
return "OK"
|
||||
}
|
||||
if status == 404 {
|
||||
@@ -33,8 +34,8 @@ func statusCodeToText(status int) string {
|
||||
return strconv.Itoa(status)
|
||||
}
|
||||
|
||||
func statusCodeToStyle(status int) string {
|
||||
if status == 200 {
|
||||
func statusCodeToStyle(status int, altStatusCodes []int) string {
|
||||
if status == 200 || slices.Contains(altStatusCodes, status) {
|
||||
return "ok"
|
||||
}
|
||||
|
||||
@@ -51,6 +52,7 @@ type Monitor struct {
|
||||
SameTab bool `yaml:"same-tab"`
|
||||
StatusText string `yaml:"-"`
|
||||
StatusStyle string `yaml:"-"`
|
||||
AltStatusCodes []int `yaml:"alt-status-codes"`
|
||||
} `yaml:"sites"`
|
||||
ShowFailingOnly bool `yaml:"show-failing-only"`
|
||||
HasFailing bool `yaml:"-"`
|
||||
@@ -82,13 +84,13 @@ func (widget *Monitor) Update(ctx context.Context) {
|
||||
status := &statuses[i]
|
||||
site.Status = status
|
||||
|
||||
if status.Code >= 400 || status.TimedOut || status.Error != nil {
|
||||
if !slices.Contains(site.AltStatusCodes, status.Code) && (status.Code >= 400 || status.TimedOut || status.Error != nil) {
|
||||
widget.HasFailing = true
|
||||
}
|
||||
|
||||
if !status.TimedOut {
|
||||
site.StatusText = statusCodeToText(status.Code)
|
||||
site.StatusStyle = statusCodeToStyle(status.Code)
|
||||
site.StatusText = statusCodeToText(status.Code, site.AltStatusCodes)
|
||||
site.StatusStyle = statusCodeToStyle(status.Code, site.AltStatusCodes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
47
internal/widget/split-column.go
Normal file
47
internal/widget/split-column.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package widget
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"time"
|
||||
|
||||
"github.com/glanceapp/glance/internal/assets"
|
||||
)
|
||||
|
||||
type SplitColumn struct {
|
||||
widgetBase `yaml:",inline"`
|
||||
containerWidgetBase `yaml:",inline"`
|
||||
MaxColumns int `yaml:"max-columns"`
|
||||
}
|
||||
|
||||
func (widget *SplitColumn) Initialize() error {
|
||||
widget.withError(nil).withTitle("Split Column").SetHideHeader(true)
|
||||
|
||||
for i := range widget.Widgets {
|
||||
if err := widget.Widgets[i].Initialize(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if widget.MaxColumns < 2 {
|
||||
widget.MaxColumns = 2
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (widget *SplitColumn) Update(ctx context.Context) {
|
||||
widget.containerWidgetBase.Update(ctx)
|
||||
}
|
||||
|
||||
func (widget *SplitColumn) SetProviders(providers *Providers) {
|
||||
widget.containerWidgetBase.SetProviders(providers)
|
||||
}
|
||||
|
||||
func (widget *SplitColumn) RequiresUpdate(now *time.Time) bool {
|
||||
return widget.containerWidgetBase.RequiresUpdate(now)
|
||||
}
|
||||
|
||||
func (widget *SplitColumn) Render() template.HTML {
|
||||
return widget.render(widget, assets.SplitColumnTemplate)
|
||||
}
|
||||
@@ -67,6 +67,10 @@ func New(widgetType string) (Widget, error) {
|
||||
widget = &Group{}
|
||||
case "dns-stats":
|
||||
widget = &DNSStats{}
|
||||
case "split-column":
|
||||
widget = &SplitColumn{}
|
||||
case "custom-api":
|
||||
widget = &CustomApi{}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown widget type: %s", widgetType)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user