mirror of
https://github.com/Xevion/glance.git
synced 2025-12-10 14:07:25 -06:00
Added docker widget with documentation ✨
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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{
|
||||
|
||||
44
internal/assets/templates/docker.html
Normal file
44
internal/assets/templates/docker.html
Normal 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
69
internal/feed/docker.go
Normal 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
78
internal/widget/docker.go
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user