commit
82490ef5dd
13 changed files with 483 additions and 24 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
|||
/assets
|
||||
/build
|
||||
/playground
|
||||
/.idea
|
||||
glance*.yml
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
* Twitch channels & top games
|
||||
* GitHub releases
|
||||
* Repository overview
|
||||
* Docker containers
|
||||
* Site monitor
|
||||
* Search box
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
- [Twitch Top Games](#twitch-top-games)
|
||||
- [iframe](#iframe)
|
||||
- [HTML](#html)
|
||||
- [Docker](#docker)
|
||||
|
||||
## Intro
|
||||
<!-- TODO: update -->
|
||||
|
@ -1793,3 +1794,75 @@ Example:
|
|||
```
|
||||
|
||||
Note the use of `|` after `source:`, this allows you to insert a multi-line string.
|
||||
|
||||
### Docker Containers
|
||||
<!-- TODO: update -->
|
||||
The Docker widget allows you to monitor your Docker containers.
|
||||
To enable this feature, ensure that your setup provides access to the **docker.sock** file (also you may use a TCP connection).
|
||||
|
||||
Add the following to your `docker-compose` or `docker run` command to enable the Docker widget:
|
||||
|
||||
**Docker Example:**
|
||||
```bash
|
||||
docker run -d -p 8080:8080 \
|
||||
-v ./glance.yml:/app/glance.yml \
|
||||
-v /etc/timezone:/etc/timezone:ro \
|
||||
-v /etc/localtime:/etc/localtime:ro \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||
glanceapp/glance
|
||||
```
|
||||
|
||||
**Docker Compose Example:**
|
||||
```yaml
|
||||
services:
|
||||
glance:
|
||||
image: glanceapp/glance
|
||||
volumes:
|
||||
- ./glance.yml:/app/glance.yml
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
ports:
|
||||
- 8080:8080
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
#### Configuration
|
||||
To integrate the Docker widget into your dashboard, include the following snippet in your `glance.yml` file:
|
||||
|
||||
```yaml
|
||||
- type: docker
|
||||
host-url: tcp://localhost:2375
|
||||
cache: 1m
|
||||
```
|
||||
|
||||
#### Properties
|
||||
|
||||
| Name | Type | Required | Default |
|
||||
| ---- | ---- | -------- | ------- |
|
||||
| host-url | string | no | `unix:///var/run/docker.sock` |
|
||||
|
||||
#### Leveraging Container Labels
|
||||
You can use container labels to control visibility, URLs, icons, and titles within the Docker widget. Add the following labels to your container configuration for enhanced customization:
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
- "glance.enable=true" # Enable or disable visibility of the container (default: true)
|
||||
- "glance.title=Glance" # Optional friendly name (defaults to container name)
|
||||
- "glance.url=https://app.example.com" # Optional URL associated with the container
|
||||
- "glance.iconUrl=si:docker" # Optional URL to an image which will be used as the icon for the site
|
||||
|
||||
```
|
||||
|
||||
**Default Values:**
|
||||
|
||||
| Name | Default |
|
||||
|----------------|------------|
|
||||
| glance.enable | true |
|
||||
| glance.title | Container name |
|
||||
| glance.url | (none) |
|
||||
| glance.iconUrl | si:docker |
|
||||
|
||||
Preview:
|
||||
|
||||

|
||||
|
|
BIN
docs/images/docker-widget-preview.png
Normal file
BIN
docs/images/docker-widget-preview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
2
go.sum
2
go.sum
|
@ -75,4 +75,4 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
|||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -180,22 +180,19 @@ type customIconField struct {
|
|||
// invert the color based on the theme being light or dark
|
||||
}
|
||||
|
||||
func (i *customIconField) UnmarshalYAML(node *yaml.Node) error {
|
||||
var value string
|
||||
if err := node.Decode(&value); err != nil {
|
||||
return err
|
||||
}
|
||||
func newCustomIconField(value string) customIconField {
|
||||
field := customIconField{}
|
||||
|
||||
prefix, icon, found := strings.Cut(value, ":")
|
||||
if !found {
|
||||
i.URL = value
|
||||
return nil
|
||||
field.URL = value
|
||||
return field
|
||||
}
|
||||
|
||||
switch prefix {
|
||||
case "si":
|
||||
i.URL = "https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/" + icon + ".svg"
|
||||
i.IsFlatIcon = true
|
||||
field.URL = "https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/" + icon + ".svg"
|
||||
field.IsFlatIcon = true
|
||||
case "di":
|
||||
// syntax: di:<icon_name>[.svg|.png]
|
||||
// if the icon name is specified without extension, it is assumed to be wanting the SVG icon
|
||||
|
@ -211,10 +208,20 @@ func (i *customIconField) UnmarshalYAML(node *yaml.Node) error {
|
|||
ext = "svg"
|
||||
}
|
||||
|
||||
i.URL = "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/" + ext + "/" + basename + "." + ext
|
||||
field.URL = "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/" + ext + "/" + basename + "." + ext
|
||||
default:
|
||||
i.URL = value
|
||||
field.URL = value
|
||||
}
|
||||
|
||||
return field
|
||||
}
|
||||
|
||||
func (i *customIconField) UnmarshalYAML(node *yaml.Node) error {
|
||||
var value string
|
||||
if err := node.Decode(&value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*i = newCustomIconField(value)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -123,11 +123,11 @@ function repositionContainer() {
|
|||
} else if (left + containerBounds.width > window.innerWidth) {
|
||||
containerElement.style.removeProperty("left");
|
||||
containerElement.style.right = 0;
|
||||
containerElement.style.setProperty("--triangle-offset", containerBounds.width - containerInlinePadding - (window.innerWidth - targetBounds.left - targetBoundsWidthOffset) + "px");
|
||||
containerElement.style.setProperty("--triangle-offset", containerBounds.width - containerInlinePadding - (window.innerWidth - targetBounds.left - targetBoundsWidthOffset) + -1 + "px");
|
||||
} else {
|
||||
containerElement.style.removeProperty("right");
|
||||
containerElement.style.left = left + "px";
|
||||
containerElement.style.setProperty("--triangle-offset", ((targetBounds.left + targetBoundsWidthOffset) - left - containerInlinePadding) + "px");
|
||||
containerElement.style.setProperty("--triangle-offset", ((targetBounds.left + targetBoundsWidthOffset) - left - containerInlinePadding) + -1 + "px");
|
||||
}
|
||||
|
||||
const distanceFromTarget = activeTarget.dataset.popoverMargin || defaultDistanceFromTarget;
|
||||
|
|
|
@ -723,6 +723,7 @@ details[open] .summary::after {
|
|||
justify-content: space-between;
|
||||
position: relative;
|
||||
margin-bottom: 1.8rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.widget-error-header::before {
|
||||
|
@ -738,19 +739,11 @@ details[open] .summary::after {
|
|||
.widget-error-icon {
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
border: 0.2rem solid var(--color-negative);
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 2rem;
|
||||
flex-shrink: 0;
|
||||
stroke: var(--color-negative);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.widget-error-icon::before {
|
||||
content: '!';
|
||||
color: var(--color-text-highlight);
|
||||
}
|
||||
|
||||
.widget-content {
|
||||
container-type: inline-size;
|
||||
container-name: widget;
|
||||
|
@ -1397,6 +1390,33 @@ details[open] .summary::after {
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.docker-container-icon {
|
||||
display: block;
|
||||
filter: grayscale(0.4);
|
||||
object-fit: contain;
|
||||
aspect-ratio: 1 / 1;
|
||||
width: 2.7rem;
|
||||
opacity: 0.8;
|
||||
transition: filter 0.3s, opacity 0.3s;
|
||||
}
|
||||
|
||||
.docker-container-icon.flat-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.docker-container:hover .docker-container-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.docker-container:hover .docker-container-icon:not(.flat-icon) {
|
||||
filter: grayscale(0);
|
||||
}
|
||||
|
||||
.docker-container-status-icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
filter: grayscale(0.2) contrast(0.9);
|
||||
opacity: 0.8;
|
||||
|
@ -1813,6 +1833,7 @@ details[open] .summary::after {
|
|||
.gap-35 { gap: 3.5rem; }
|
||||
.gap-45 { gap: 4.5rem; }
|
||||
.gap-55 { gap: 5.5rem; }
|
||||
.margin-left-auto { margin-left: auto; }
|
||||
.margin-top-3 { margin-top: 0.3rem; }
|
||||
.margin-top-5 { margin-top: 0.5rem; }
|
||||
.margin-top-7 { margin-top: 0.7rem; }
|
||||
|
|
65
internal/glance/templates/docker-containers.html
Normal file
65
internal/glance/templates/docker-containers.html
Normal file
|
@ -0,0 +1,65 @@
|
|||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<div class="dynamic-columns list-gap-24 list-with-separator">
|
||||
{{ range .Containers }}
|
||||
<div class="docker-container flex items-center gap-15">
|
||||
<div class="shrink-0" data-popover-type="html" data-popover-position="above" data-popover-offset="0.25" data-popover-margin="0.1rem">
|
||||
<img class="docker-container-icon{{ if .Icon.IsFlatIcon }} flat-icon{{ end }}" src="{{ .Icon.URL }}" alt="" loading="lazy">
|
||||
<div data-popover-html>
|
||||
<div class="color-highlight text-truncate block">{{ .Image }}</div>
|
||||
<div>{{ .StateText }}</div>
|
||||
{{ if .Children }}
|
||||
<ul class="list list-gap-4 margin-top-10">
|
||||
{{ range .Children }}
|
||||
<li class="flex gap-7 items-center">
|
||||
<div class="margin-bottom-3">{{ template "state-icon" .StateIcon }}</div>
|
||||
<div class="color-highlight">{{ .Title }} <span class="size-h5 color-base">{{ .StateText }}</span></div>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
<div class="margin-top-10">created <span {{ .Created | dynamicRelativeTimeAttrs }}></span> ago</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="min-width-0">
|
||||
{{ if .URL }}
|
||||
<a href="{{ .URL }}" class="color-highlight size-title-dynamic block text-truncate" {{ if not .SameTab }}target="_blank"{{ end }} rel="noreferrer">{{ .Title }}</a>
|
||||
{{ else }}
|
||||
<div class="color-highlight text-truncate size-title-dynamic">{{ .Title }}</div>
|
||||
{{ end }}
|
||||
{{ if .Description }}
|
||||
<div class="text-truncate">{{ .Description }}</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div class="margin-left-auto shrink-0" data-popover-type="text" data-popover-position="above" data-popover-text="{{ .State }}">
|
||||
{{ template "state-icon" .StateIcon }}
|
||||
</div>
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="text-center">No containers available to show.</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ define "state-icon" }}
|
||||
{{ if eq . "ok" }}
|
||||
<svg class="docker-container-status-icon" fill="var(--color-positive)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
{{ else if eq . "warn" }}
|
||||
<svg class="docker-container-status-icon" fill="var(--color-negative)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495ZM10 5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 10 5Zm0 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
{{ else if eq . "paused" }}
|
||||
<svg class="docker-container-status-icon" fill="var(--color-text-base)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M2 10a8 8 0 1 1 16 0 8 8 0 0 1-16 0Zm5-2.25A.75.75 0 0 1 7.75 7h.5a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1-.75-.75v-4.5Zm4 0a.75.75 0 0 1 .75-.75h.5a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1-.75-.75v-4.5Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
{{ else }}
|
||||
<svg class="docker-container-status-icon" fill="var(--color-text-base)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0ZM8.94 6.94a.75.75 0 1 1-1.061-1.061 3 3 0 1 1 2.871 5.026v.345a.75.75 0 0 1-1.5 0v-.5c0-.72.57-1.172 1.081-1.287A1.5 1.5 0 1 0 8.94 6.94ZM10 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
{{ end }}
|
||||
{{ end }}
|
|
@ -15,7 +15,9 @@
|
|||
{{ else }}
|
||||
<div class="widget-error-header">
|
||||
<div class="color-negative size-h3">ERROR</div>
|
||||
<div class="widget-error-icon"></div>
|
||||
<svg class="widget-error-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="break-all">{{ if .Error }}{{ .Error }}{{ else }}No error information provided{{ end }}</p>
|
||||
{{ end}}
|
||||
|
|
|
@ -166,3 +166,15 @@ func executeTemplateToHTML(t *template.Template, data interface{}) (template.HTM
|
|||
|
||||
return template.HTML(b.String()), nil
|
||||
}
|
||||
|
||||
func stringToBool(s string) bool {
|
||||
return s == "true" || s == "yes"
|
||||
}
|
||||
|
||||
func itemAtIndexOrDefault[T any](items []T, index int, def T) T {
|
||||
if index >= len(items) {
|
||||
return def
|
||||
}
|
||||
|
||||
return items[index]
|
||||
}
|
||||
|
|
275
internal/glance/widget-docker-containers.go
Normal file
275
internal/glance/widget-docker-containers.go
Normal file
|
@ -0,0 +1,275 @@
|
|||
package glance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var dockerContainersWidgetTemplate = mustParseTemplate("docker-containers.html", "widget-base.html")
|
||||
|
||||
type dockerContainersWidget struct {
|
||||
widgetBase `yaml:",inline"`
|
||||
HideByDefault bool `yaml:"hide-by-default"`
|
||||
SockPath string `yaml:"sock-path"`
|
||||
Containers dockerContainerList `yaml:"-"`
|
||||
}
|
||||
|
||||
func (widget *dockerContainersWidget) initialize() error {
|
||||
widget.withTitle("Docker Containers").withCacheDuration(1 * time.Minute)
|
||||
|
||||
if widget.SockPath == "" {
|
||||
widget.SockPath = "/var/run/docker.sock"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (widget *dockerContainersWidget) update(ctx context.Context) {
|
||||
containers, err := fetchDockerContainers(widget.SockPath, widget.HideByDefault)
|
||||
if !widget.canContinueUpdateAfterHandlingErr(err) {
|
||||
return
|
||||
}
|
||||
|
||||
containers.sortByStateIconThenTitle()
|
||||
widget.Containers = containers
|
||||
}
|
||||
|
||||
func (widget *dockerContainersWidget) Render() template.HTML {
|
||||
return widget.renderTemplate(widget, dockerContainersWidgetTemplate)
|
||||
}
|
||||
|
||||
const (
|
||||
dockerContainerLabelHide = "glance.hide"
|
||||
dockerContainerLabelTitle = "glance.title"
|
||||
dockerContainerLabelURL = "glance.url"
|
||||
dockerContainerLabelDescription = "glance.description"
|
||||
dockerContainerLabelSameTab = "glance.same-tab"
|
||||
dockerContainerLabelIcon = "glance.icon"
|
||||
dockerContainerLabelID = "glance.id"
|
||||
dockerContainerLabelParent = "glance.parent"
|
||||
)
|
||||
|
||||
const (
|
||||
dockerContainerStateIconOK = "ok"
|
||||
dockerContainerStateIconPaused = "paused"
|
||||
dockerContainerStateIconWarn = "warn"
|
||||
dockerContainerStateIconOther = "other"
|
||||
)
|
||||
|
||||
var dockerContainerStateIconPriorities = map[string]int{
|
||||
dockerContainerStateIconWarn: 0,
|
||||
dockerContainerStateIconOther: 1,
|
||||
dockerContainerStateIconPaused: 2,
|
||||
dockerContainerStateIconOK: 3,
|
||||
}
|
||||
|
||||
type dockerContainerJsonResponse struct {
|
||||
Names []string `json:"Names"`
|
||||
Image string `json:"Image"`
|
||||
State string `json:"State"`
|
||||
Status string `json:"Status"`
|
||||
Labels dockerContainerLabels `json:"Labels"`
|
||||
Created int64 `json:"Created"`
|
||||
}
|
||||
|
||||
type dockerContainerLabels map[string]string
|
||||
|
||||
func (l *dockerContainerLabels) getOrDefault(label, def string) string {
|
||||
if l == nil {
|
||||
return def
|
||||
}
|
||||
|
||||
v, ok := (*l)[label]
|
||||
if !ok {
|
||||
return def
|
||||
}
|
||||
|
||||
if v == "" {
|
||||
return def
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
type dockerContainer struct {
|
||||
Title string
|
||||
URL string
|
||||
SameTab bool
|
||||
Image string
|
||||
State string
|
||||
StateText string
|
||||
StateIcon string
|
||||
Description string
|
||||
Icon customIconField
|
||||
Children dockerContainerList
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
type dockerContainerList []dockerContainer
|
||||
|
||||
func (containers dockerContainerList) sortByStateIconThenTitle() {
|
||||
sort.SliceStable(containers, func(a, b int) bool {
|
||||
p := &dockerContainerStateIconPriorities
|
||||
if containers[a].StateIcon != containers[b].StateIcon {
|
||||
return (*p)[containers[a].StateIcon] < (*p)[containers[b].StateIcon]
|
||||
}
|
||||
|
||||
return strings.ToLower(containers[a].Title) < strings.ToLower(containers[b].Title)
|
||||
})
|
||||
}
|
||||
|
||||
func dockerContainerStateToStateIcon(state string) string {
|
||||
switch state {
|
||||
case "running":
|
||||
return dockerContainerStateIconOK
|
||||
case "paused":
|
||||
return dockerContainerStateIconPaused
|
||||
case "exited", "unhealthy", "dead":
|
||||
return dockerContainerStateIconWarn
|
||||
default:
|
||||
return dockerContainerStateIconOther
|
||||
}
|
||||
}
|
||||
|
||||
func fetchDockerContainers(socketPath string, hideByDefault bool) (dockerContainerList, error) {
|
||||
containers, err := fetchAllDockerContainersFromSock(socketPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching containers: %w", err)
|
||||
}
|
||||
|
||||
containers, children := groupDockerContainerChildren(containers, hideByDefault)
|
||||
dockerContainers := make(dockerContainerList, 0, len(containers))
|
||||
|
||||
for i := range containers {
|
||||
container := &containers[i]
|
||||
|
||||
dc := dockerContainer{
|
||||
Title: deriveDockerContainerTitle(container),
|
||||
URL: container.Labels.getOrDefault(dockerContainerLabelURL, ""),
|
||||
Description: container.Labels.getOrDefault(dockerContainerLabelDescription, ""),
|
||||
SameTab: stringToBool(container.Labels.getOrDefault(dockerContainerLabelSameTab, "false")),
|
||||
Image: container.Image,
|
||||
State: strings.ToLower(container.State),
|
||||
StateText: strings.ToLower(container.Status),
|
||||
Icon: newCustomIconField(container.Labels.getOrDefault(dockerContainerLabelIcon, "si:docker")),
|
||||
Created: time.Unix(container.Created, 0),
|
||||
}
|
||||
|
||||
if idValue := container.Labels.getOrDefault(dockerContainerLabelID, ""); idValue != "" {
|
||||
if children, ok := children[idValue]; ok {
|
||||
for i := range children {
|
||||
child := &children[i]
|
||||
dc.Children = append(dc.Children, dockerContainer{
|
||||
Title: deriveDockerContainerTitle(child),
|
||||
StateText: child.Status,
|
||||
StateIcon: dockerContainerStateToStateIcon(strings.ToLower(child.State)),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dc.Children.sortByStateIconThenTitle()
|
||||
|
||||
stateIconSupersededByChild := false
|
||||
for i := range dc.Children {
|
||||
if dc.Children[i].StateIcon == dockerContainerStateIconWarn {
|
||||
dc.StateIcon = dockerContainerStateIconWarn
|
||||
stateIconSupersededByChild = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !stateIconSupersededByChild {
|
||||
dc.StateIcon = dockerContainerStateToStateIcon(dc.State)
|
||||
}
|
||||
|
||||
dockerContainers = append(dockerContainers, dc)
|
||||
}
|
||||
|
||||
return dockerContainers, nil
|
||||
}
|
||||
|
||||
func deriveDockerContainerTitle(container *dockerContainerJsonResponse) string {
|
||||
if v := container.Labels.getOrDefault(dockerContainerLabelTitle, ""); v != "" {
|
||||
return v
|
||||
}
|
||||
|
||||
return strings.TrimLeft(itemAtIndexOrDefault(container.Names, 0, "n/a"), "/")
|
||||
}
|
||||
|
||||
func groupDockerContainerChildren(
|
||||
containers []dockerContainerJsonResponse,
|
||||
hideByDefault bool,
|
||||
) (
|
||||
[]dockerContainerJsonResponse,
|
||||
map[string][]dockerContainerJsonResponse,
|
||||
) {
|
||||
parents := make([]dockerContainerJsonResponse, 0, len(containers))
|
||||
children := make(map[string][]dockerContainerJsonResponse)
|
||||
|
||||
for i := range containers {
|
||||
container := &containers[i]
|
||||
|
||||
if isDockerContainerHidden(container, hideByDefault) {
|
||||
continue
|
||||
}
|
||||
|
||||
isParent := container.Labels.getOrDefault(dockerContainerLabelID, "") != ""
|
||||
parent := container.Labels.getOrDefault(dockerContainerLabelParent, "")
|
||||
|
||||
if !isParent && parent != "" {
|
||||
children[parent] = append(children[parent], *container)
|
||||
} else {
|
||||
parents = append(parents, *container)
|
||||
}
|
||||
}
|
||||
|
||||
return parents, children
|
||||
}
|
||||
|
||||
func isDockerContainerHidden(container *dockerContainerJsonResponse, hideByDefault bool) bool {
|
||||
if v := container.Labels.getOrDefault(dockerContainerLabelHide, ""); v != "" {
|
||||
return stringToBool(v)
|
||||
}
|
||||
|
||||
return hideByDefault
|
||||
}
|
||||
|
||||
func fetchAllDockerContainersFromSock(socketPath string) ([]dockerContainerJsonResponse, error) {
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
|
||||
return net.Dial("unix", socketPath)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
request, err := http.NewRequest("GET", "http://docker/containers/json?all=true", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sending request to socket: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("non-200 response status: %s", response.Status)
|
||||
}
|
||||
|
||||
var containers []dockerContainerJsonResponse
|
||||
if err := json.NewDecoder(response.Body).Decode(&containers); err != nil {
|
||||
return nil, fmt.Errorf("decoding response: %w", err)
|
||||
}
|
||||
|
||||
return containers, nil
|
||||
}
|
|
@ -69,6 +69,8 @@ func newWidget(widgetType string) (widget, error) {
|
|||
w = &splitColumnWidget{}
|
||||
case "custom-api":
|
||||
w = &customAPIWidget{}
|
||||
case "docker-containers":
|
||||
w = &dockerContainersWidget{}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown widget type: %s", widgetType)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue