Added docker widget with documentation

This commit is contained in:
Andrejs Baranovskis
2024-11-19 17:58:55 +01:00
parent c8570d07ef
commit eacbb14279
12 changed files with 385 additions and 9 deletions

View File

@@ -1545,6 +1545,37 @@ details[open] .summary::after {
background: linear-gradient(0deg, var(--color-widget-background) 10%, transparent);
}
.docker-container-icon {
display: block;
opacity: 0.8;
filter: grayscale(0.4);
object-fit: contain;
aspect-ratio: 1 / 1;
width: 3.2rem;
position: relative;
top: -0.1rem;
transition: filter 0.3s, opacity 0.3s;
}
.docker-container-icon.simple-icon {
opacity: 0.7;
}
.docker-container:hover .docker-container-icon {
opacity: 1;
}
.docker-container:hover .docker-container-icon:not(.simple-icon) {
filter: grayscale(0);
}
.docker-container-status-icon {
flex-shrink: 0;
margin-left: auto;
width: 2rem;
height: 2rem;
}
@media (max-width: 1190px) {
.header-container {
display: none;

View File

@@ -42,6 +42,7 @@ var (
DNSStatsTemplate = compileTemplate("dns-stats.html", "widget-base.html")
SplitColumnTemplate = compileTemplate("split-column.html", "widget-base.html")
CustomAPITemplate = compileTemplate("custom-api.html", "widget-base.html")
DockerTemplate = compileTemplate("docker.html", "widget-base.html")
)
var GlobalTemplateFunctions = template.FuncMap{

View File

@@ -0,0 +1,44 @@
{{ template "widget-base.html" . }}
{{ define "widget-content" }}
<div class="dynamic-columns list-gap-20 list-with-separator">
{{ range .Containers }}
<div class="docker-container flex items-center gap-15">
{{ template "container" . }}
</div>
{{ end }}
</div>
{{ end }}
{{ define "container" }}
{{ if .Icon.URL }}
<img class="monitor-site-icon{{ if .Icon.IsFlatIcon }} flat-icon{{ end }}" src="{{ .Icon.URL }}" alt="" loading="lazy">
{{ end }}
<div class="min-width-0">
<a class="size-h3 color-highlight text-truncate block" href="{{ .URL }}" target="_blank" rel="noreferrer" title="{{ .Title }}">{{ .Title }}</a>
<div class="text-truncate" title="{{ .Image }}">{{ .Image }}</div>
<ul class="size-h6 color-subdue list-horizontal-text">
<li>{{ .StatusShort }}</li>
<li>{{ .StatusFull }}</li>
</ul>
</div>
{{ if eq .StatusStyle "success" }}
<div class="docker-container-status-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-positive)">
<path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clip-rule="evenodd" />
</svg>
</div>
{{ else if eq .StatusStyle "warning" }}
<div class="docker-container-status-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-positive)">
<path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z" clip-rule="evenodd" />
</svg>
</div>
{{ else }}
<div class="docker-container-status-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-negative)">
<path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z" clip-rule="evenodd" />
</svg>
</div>
{{ end }}
{{ end }}

69
internal/feed/docker.go Normal file
View File

@@ -0,0 +1,69 @@
package feed
import (
"context"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"strings"
)
const (
dockerAPIVersion = "1.24"
dockerGlanceEnable = "glance.enable"
dockerGlanceTitle = "glance.title"
dockerGlanceUrl = "glance.url"
dockerGlanceIconUrl = "glance.iconUrl"
)
type DockerContainer struct {
Id string
Image string
Title string
URL string
IconURL string
Status string
State string
}
func FetchDockerContainers(ctx context.Context) ([]DockerContainer, error) {
apiClient, err := client.NewClientWithOpts(client.WithVersion(dockerAPIVersion), client.FromEnv)
if err != nil {
return nil, err
}
defer apiClient.Close()
containers, err := apiClient.ContainerList(ctx, container.ListOptions{})
if err != nil {
return nil, err
}
var results []DockerContainer
for _, c := range containers {
isGlanceEnabled := getLabelValue(c.Labels, dockerGlanceEnable, "true")
if isGlanceEnabled != "true" {
continue
}
results = append(results, DockerContainer{
Id: c.ID,
Image: c.Image,
Title: getLabelValue(c.Labels, dockerGlanceTitle, strings.Join(c.Names, "")),
URL: getLabelValue(c.Labels, dockerGlanceUrl, ""),
IconURL: getLabelValue(c.Labels, dockerGlanceIconUrl, "si:docker"),
Status: c.Status,
State: c.State,
})
}
return results, nil
}
// getLabelValue get string value associated to a label.
func getLabelValue(labels map[string]string, labelName, defaultValue string) string {
if value, ok := labels[labelName]; ok && len(value) > 0 {
return value
}
return defaultValue
}

78
internal/widget/docker.go Normal file
View File

@@ -0,0 +1,78 @@
package widget
import (
"context"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"html/template"
"time"
"github.com/glanceapp/glance/internal/assets"
"github.com/glanceapp/glance/internal/feed"
)
type containerData struct {
Id string
Image string
URL string
Title string
Icon CustomIcon
StatusShort string
StatusFull string
StatusStyle string
}
type Docker struct {
widgetBase `yaml:",inline"`
Containers []containerData `yaml:"-"`
}
func (widget *Docker) Initialize() error {
widget.withTitle("Docker").withCacheDuration(1 * time.Minute)
return nil
}
func (widget *Docker) Update(ctx context.Context) {
containers, err := feed.FetchDockerContainers(ctx)
if !widget.canContinueUpdateAfterHandlingErr(err) {
return
}
var items []containerData
for _, container := range containers {
var item containerData
item.Id = container.Id
item.Image = container.Image
item.URL = container.URL
item.Title = container.Title
_ = item.Icon.FromURL(container.IconURL)
switch container.State {
case "paused":
case "starting":
case "unhealthy":
item.StatusStyle = "warning"
break
case "stopped":
case "dead":
case "exited":
item.StatusStyle = "error"
break
default:
item.StatusStyle = "success"
}
item.StatusFull = container.Status
item.StatusShort = cases.Title(language.English, cases.Compact).String(container.State)
items = append(items, item)
}
widget.Containers = items
}
func (widget *Docker) Render() template.HTML {
return widget.render(widget, assets.DockerTemplate)
}

View File

@@ -185,15 +185,10 @@ type CustomIcon struct {
// invert the color based on the theme being light or dark
}
func (i *CustomIcon) UnmarshalYAML(node *yaml.Node) error {
var value string
if err := node.Decode(&value); err != nil {
return err
}
prefix, icon, found := strings.Cut(value, ":")
func (i *CustomIcon) FromURL(url string) error {
prefix, icon, found := strings.Cut(url, ":")
if !found {
i.URL = value
i.URL = url
return nil
}
@@ -218,8 +213,16 @@ func (i *CustomIcon) UnmarshalYAML(node *yaml.Node) error {
i.URL = "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/" + ext + "/" + basename + "." + ext
default:
i.URL = value
i.URL = url
}
return nil
}
func (i *CustomIcon) UnmarshalYAML(node *yaml.Node) error {
var value string
if err := node.Decode(&value); err != nil {
return err
}
return i.FromURL(value)
}

View File

@@ -71,6 +71,8 @@ func New(widgetType string) (Widget, error) {
widget = &SplitColumn{}
case "custom-api":
widget = &CustomApi{}
case "docker":
widget = &Docker{}
default:
return nil, fmt.Errorf("unknown widget type: %s", widgetType)
}