From a1a70d4a3c6a8314dd2ad35344d440e4ba8e86e8 Mon Sep 17 00:00:00 2001
From: Evan Steinkerchner <esteinkerchner@gmail.com>
Date: Tue, 1 Mar 2022 20:55:45 -0500
Subject: [PATCH] Added Portainer custom service

---
 docs/customservices.md                |  16 ++++
 src/components/services/Portainer.vue | 122 ++++++++++++++++++++++++++
 2 files changed, 138 insertions(+)
 create mode 100644 src/components/services/Portainer.vue

diff --git a/docs/customservices.md b/docs/customservices.md
index 7e3e6b3..cb68e1d 100644
--- a/docs/customservices.md
+++ b/docs/customservices.md
@@ -125,3 +125,19 @@ For Prometheus you need to set the type to Prometheus and provide a url.
   url: "http://192.168.0.151/"
   # subtitle: "Monitor data server"
 ```
+
+## Portainer
+
+This service displays info about the total number of containers managed by your Portainer instance.
+In order to use it, you must be using Portainer version 1.11 or later. Generate an access token from the UI and pass
+it to the apikey field.
+
+See https://docs.portainer.io/v/ce-2.11/user/account-settings#access-tokens
+
+```yaml
+- name: "Portainer"
+  logo: "assets/tools/sample.png"
+  url: "http://192.168.0.151/"
+  type: "Portainer"
+  apikey: "MY-SUPER-SECRET-API-KEY"
+```
diff --git a/src/components/services/Portainer.vue b/src/components/services/Portainer.vue
new file mode 100644
index 0000000..0af30eb
--- /dev/null
+++ b/src/components/services/Portainer.vue
@@ -0,0 +1,122 @@
+<template>
+  <Generic :item="item">
+    <template #indicator>
+      <div class="notifs">
+        <strong v-if="running > 0" class="notif running" title="Running">
+          {{ running }}
+        </strong>
+        <strong v-if="dead > 0" class="notif dead" title="Dead">
+          {{ dead }}
+        </strong>
+        <strong v-if="misc > 0" class="notif misc" title="Other (creating, paused, exited, etc.)">
+          {{ misc }}
+        </strong>
+      </div>
+    </template>
+  </Generic>
+</template>
+
+<script>
+import service from "@/mixins/service.js";
+import Generic from "./Generic.vue";
+
+export default {
+  name: "Portainer",
+  mixins: [service],
+  props: {
+    item: Object,
+  },
+  components: {
+    Generic,
+  },
+  data: () => ({
+    endpoints: null,
+    containers: null,
+  }),
+  computed: {
+    running: function () {
+      if (!this.containers) {
+        return "";
+      }
+      return this.containers.filter((container) => {
+        return container.State.toLowerCase() === "running";
+      }).length;
+    },
+    dead: function () {
+      if (!this.containers) {
+        return "";
+      }
+      return this.containers.filter((container) => {
+        return container.State.toLowerCase() === "dead";
+      }).length || 1;
+    },
+    misc: function () {
+      if (!this.containers) {
+        return "";
+      }
+      return this.containers.filter((container) => {
+        return container.State.toLowerCase() !== "running" && container.State.toLowerCase() !== "dead";
+      }).length;
+    },
+  },
+  created() {
+    this.fetchStatus();
+  },
+  methods: {
+    fetchStatus: async function () {
+      const headers = {
+        "X-Api-Key": this.item.apikey,
+      };
+
+      this.endpoints = await this.fetch("/api/endpoints", { headers })
+        .catch((e) => {
+          console.error(e);
+        });
+
+      let containers = [];
+      for (let endpoint of this.endpoints) {
+        const uri = `/api/endpoints/${endpoint.Id}/docker/containers/json?all=1`;
+        const endpointContainers = await this.fetch(uri, { headers })
+          .catch((e) => {
+            console.error(e);
+          });
+
+        containers = containers.concat(endpointContainers);
+      }
+
+      this.containers = containers;
+    },
+  },
+};
+</script>
+
+<style scoped lang="scss">
+.notifs {
+  position: absolute;
+  color: white;
+  font-family: sans-serif;
+  top: 0.3em;
+  right: 0.5em;
+
+  .notif {
+    display: inline-block;
+    padding: 0.2em 0.35em;
+    border-radius: 0.25em;
+    position: relative;
+    margin-left: 0.3em;
+    font-size: 0.8em;
+
+    &.running {
+      background-color: #4fd671;
+    }
+
+    &.dead {
+      background-color: #e51111;
+    }
+
+    &.misc {
+      background-color: #2ed0c8;
+    }
+  }
+}
+</style>