Repository: amir20/dozzle (language: go)

Issue:
Search using labels
**Is your feature request related to a problem? Please describe.**
No, just missing possibility for my usecase.

**Describe the solution you'd like**
I would like to be able to search in containers according labels. I have some labels (i.e. **hostname=app.example.com** or keys like traefik.http.routers.appname.rule) attached to most of my containers.

I would like to search in container labels using values, or "key=value" pair.

**Describe alternatives you've considered**
Another solution for my usecase is to spawn dozzle with filter parameter for each "usual" subset of machines, which I use and use different instances of dozzle on different url addresse to show different docker containers.

**Additional context**
There seems to be large portion of labels now (including small html pages, links to icons, description, homepages, ...) when I start even small container. See:
```
docker run --detach --label skuska=ano traefik/whoami:latest  
docker ps --format "{{.Image}}: {{.Labels}}" | grep whoami
```

Sample output (manually changed format slightly):
```traefik/whoami:latest:
org.opencontainers.image.revision=87f25fc35b3e9051117dddfd11bbae5fbc986581,
org.opencontainers.image.source=https://github.com/traefik/whoami,
org.opencontainers.image.version=1.10.1,
skuska=ano,
org.opencontainers.image.created=2023-07-12T14:02:18Z,
org.opencontainers.image.description=Tiny Go webserver that prints OS information and HTTP request to output,
org.opencontainers.image.documentation=https://github.com/traefik/whoami,
org.opencontainers.image.title=whoami,
org.opencontainers.image.url=https://github.com/traefik/whoami
```

So there can be problem sending all labels in first stream event. Consider sending them separately and attach them to containers in second event. Also intelligent fuzzy search can have problem handling so much more data.



Discussion / hints:

Fix patch:
diff --git a/assets/components/FuzzySearchModal.vue b/assets/components/FuzzySearchModal.vue
index caf198b59950..fd1eb081f46a 100644
--- a/assets/components/FuzzySearchModal.vue
+++ b/assets/components/FuzzySearchModal.vue
@@ -58,22 +58,24 @@ const store = useContainerStore();
 const { containers } = storeToRefs(store);
 
 const list = computed(() => {
-  return containers.value.map(({ id, created, name, state, hostLabel: host }) => {
+  return containers.value.map(({ id, created, name, state, labels, hostLabel: host }) => {
     return {
       id,
       created,
       name,
       state,
       host,
+      labels: Object.entries(labels).map(([_, value]) => value),
     };
   });
 });
 
 const { results } = useFuse(query, list, {
   fuseOptions: {
-    keys: ["name", "host"],
+    keys: ["name", "host", "labels"],
     includeScore: true,
     useExtendedSearch: true,
+    threshold: 0.3,
   },
   resultLimit,
   matchAllWhenSearchEmpty: true,
diff --git a/assets/models/Container.spec.ts b/assets/models/Container.spec.ts
index ffa6cb9f3c16..e91a09619527 100644
--- a/assets/models/Container.spec.ts
+++ b/assets/models/Container.spec.ts
@@ -18,7 +18,7 @@ describe("Container", () => {
   ];
 
   test.each(names)("name %s should be %s and %s", (name, expectedName, expectedSwarmId) => {
-    const c = new Container("id", new Date(), "image", name!, "command", "host", "status", "created");
+    const c = new Container("id", new Date(), "image", name!, "command", "host", {}, "status", "created");
     expect(c.name).toBe(expectedName);
     expect(c.swarmId).toBe(expectedSwarmId);
   });
diff --git a/assets/models/Container.ts b/assets/models/Container.ts
index 2471f3393f56..8c6d8e07b214 100644
--- a/assets/models/Container.ts
+++ b/assets/models/Container.ts
@@ -31,6 +31,7 @@ export class Container {
     public readonly name: string,
     public readonly command: string,
     public readonly host: string,
+    public readonly labels = {} as Record<string, string>,
     public status: string,
     public state: ContainerState,
     public health?: ContainerHealth,
diff --git a/assets/stores/container.ts b/assets/stores/container.ts
index beada1fca40a..41089a4acd76 100644
--- a/assets/stores/container.ts
+++ b/assets/stores/container.ts
@@ -124,6 +124,7 @@ export const useContainerStore = defineStore("container", () => {
           c.name,
           c.command,
           c.host,
+          c.labels,
           c.status,
           c.state,
           c.health,
diff --git a/assets/types/Container.d.ts b/assets/types/Container.d.ts
index 5faf2bdb7078..16b2e624feab 100644
--- a/assets/types/Container.d.ts
+++ b/assets/types/Container.d.ts
@@ -14,6 +14,7 @@ export type ContainerJson = {
   readonly status: string;
   readonly state: ContainerState;
   readonly host: string;
+  readonly labels: Record<string, string>;
   readonly health?: ContainerHealth;
 };
 
diff --git a/internal/docker/client.go b/internal/docker/client.go
index 0e5d9466351f..913fb5aa0542 100644
--- a/internal/docker/client.go
+++ b/internal/docker/client.go
@@ -190,6 +190,7 @@ func (d *Client) ListContainers() ([]Container, error) {
 			Status:  c.Status,
 			Host:    d.host.ID,
 			Health:  findBetweenParentheses(c.Status),
+			Labels:  c.Labels,
 		}
 		containers = append(containers, container)
 	}
diff --git a/internal/docker/types.go b/internal/docker/types.go
index 9f21f5e6341e..1efc8da25674 100644
--- a/internal/docker/types.go
+++ b/internal/docker/types.go
@@ -6,18 +6,19 @@ import (
 
 // Container represents an internal representation of docker containers
 type Container struct {
-	ID      string   `json:"id"`
-	Names   []string `json:"names"`
-	Name    string   `json:"name"`
-	Image   string   `json:"image"`
-	ImageID string   `json:"imageId"`
-	Command string   `json:"command"`
-	Created int64    `json:"created"`
-	State   string   `json:"state"`
-	Status  string   `json:"status"`
-	Health  string   `json:"health,omitempty"`
-	Host    string   `json:"host,omitempty"`
-	Tty     bool     `json:"-"`
+	ID      string            `json:"id"`
+	Names   []string          `json:"names"`
+	Name    string            `json:"name"`
+	Image   string            `json:"image"`
+	ImageID string            `json:"imageId"`
+	Command string            `json:"command"`
+	Created int64             `json:"created"`
+	State   string            `json:"state"`
+	Status  string            `json:"status"`
+	Health  string            `json:"health,omitempty"`
+	Host    string            `json:"host,omitempty"`
+	Tty     bool              `json:"-"`
+	Labels  map[string]string `json:"labels,omitempty"`
 }
 
 // ContainerStat represent stats instant for a container


Test patch: