Przeglądaj źródła

Enhanced glances widget (#1534)

* Enhanced glances widget (resource match)

* Make widget clickable + cleanup helperrs

* Prevent unused glances API calls

---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
Georges-Antoine Assi 2 lat temu
rodzic
commit
cdd7b2d44b

+ 5 - 1
public/locales/en/common.json

@@ -357,10 +357,14 @@
     },
     "glances": {
         "cpu": "CPU",
-        "mem": "MEM",
+        "load": "Load",
         "wait": "Please wait",
         "temp": "TEMP",
+        "warn": "Warn",
         "uptime": "UP",
+        "total": "Total",
+        "free": "Free",
+        "used": "Used",
         "days": "d",
         "hours": "h"
     },

+ 78 - 12
src/components/widgets/glances/glances.jsx

@@ -1,11 +1,14 @@
 import useSWR from "swr";
+import { useContext } from "react";
 import { BiError } from "react-icons/bi";
 import { FaMemory, FaRegClock, FaThermometerHalf } from "react-icons/fa";
-import { FiCpu } from "react-icons/fi";
+import { FiCpu, FiHardDrive } from "react-icons/fi";
 import { useTranslation } from "next-i18next";
 
 import UsageBar from "../resources/usage-bar";
 
+import { SettingsContext } from "utils/contexts/settings";
+
 const cpuSensorLabels = ["cpu_thermal", "Core", "Tctl"];
 
 function convertToFahrenheit(t) {
@@ -14,6 +17,7 @@ function convertToFahrenheit(t) {
 
 export default function Widget({ options }) {
   const { t, i18n } = useTranslation();
+  const { settings } = useContext(SettingsContext);
 
   const { data, error } = useSWR(
     `/api/widgets/glances?${new URLSearchParams({ lang: i18n.language, ...options }).toString()}`, {
@@ -88,8 +92,16 @@ export default function Widget({ options }) {
   }
   const tempPercent = Math.round((mainTemp / maxTemp) * 100);
 
+  let disks = [];
+
+  if (options.disk) {
+    disks = Array.isArray(options.disk)
+      ? options.disk.map((disk) => data.fs.find((d) => d.mnt_point === disk)).filter((d) => d)
+      : [data.fs.find((d) => d.mnt_point === options.disk)].filter((d) => d);
+  }
+
   return (
-    <div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap ml-4">
+    <a href={options.url} target={settings.target ?? "_blank"} className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
       <div className="flex flex-row self-center flex-wrap justify-between">
          <div className="flex-none flex flex-row items-center mr-3 py-1.5">
           <FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
@@ -97,7 +109,7 @@ export default function Widget({ options }) {
             <div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
               <div className="pl-0.5">
                 {t("common.number", {
-                  value: data.quicklook.cpu,
+                  value: data.cpu.total,
                   style: "unit",
                   unit: "percent",
                   maximumFractionDigits: 0,
@@ -105,7 +117,20 @@ export default function Widget({ options }) {
               </div>
               <div className="pr-1">{t("glances.cpu")}</div>
             </div>
-            <UsageBar percent={data.quicklook.cpu} />
+            {options.expanded && (
+              <span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
+                <div className="pl-0.5 pr-1">
+                {t("common.number", {
+                  value: data.load.min15,
+                  style: "unit",
+                  unit: "percent",
+                  maximumFractionDigits: 0,
+                })}
+                </div>
+                <div className="pr-1">{t("glances.load")}</div>
+              </span>
+            )}
+            <UsageBar percent={data.cpu.total} />
           </div>
         </div>
         <div className="flex-none flex flex-row items-center mr-3 py-1.5">
@@ -113,18 +138,46 @@ export default function Widget({ options }) {
           <div className="flex flex-col ml-3 text-left min-w-[85px]">
             <div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
               <div className="pl-0.5">
-                {t("common.number", {
-                  value: data.quicklook.mem,
-                  style: "unit",
-                  unit: "percent",
-                  maximumFractionDigits: 0,
+                {t("common.bytes", {
+                  value: data.mem.free,
+                  maximumFractionDigits: 1,
+                  binary: true,
                 })}
               </div>
-              <div className="pr-1">{t("glances.mem")}</div>
+              <div className="pr-1">{t("glances.free")}</div>
             </div>
-            <UsageBar percent={data.quicklook.mem} />
+            {options.expanded && (
+              <span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
+                <div className="pl-0.5 pr-1">
+                  {t("common.bytes", {
+                    value: data.mem.total,
+                    maximumFractionDigits: 1,
+                    binary: true,
+                  })}
+                </div>
+                <div className="pr-1">{t("glances.total")}</div>
+              </span>
+            )}
+            <UsageBar percent={data.mem.percent} />
           </div>
         </div>
+        {disks.map((disk) => (
+          <div key={disk.mnt_point} className="flex-none flex flex-row items-center mr-3 py-1.5">
+            <FiHardDrive className="text-theme-800 dark:text-theme-200 w-5 h-5" />
+            <div className="flex flex-col ml-3 text-left min-w-[85px]">
+              <span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
+                <div className="pl-0.5">{t("common.bytes", { value: disk.free })}</div>
+                <div className="pr-1">{t("glances.free")}</div>
+              </span>
+              {options.expanded && (
+                <span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
+                  <div className="pl-0.5 pr-1">{t("common.bytes", { value: disk.size })}</div>
+                  <div className="pr-1">{t("glances.total")}</div>
+                </span>
+              )}
+              <UsageBar percent={disk.percent} />
+            </div>
+          </div>))}
         {options.cputemp && mainTemp > 0 &&
             (<div className="flex-none flex flex-row items-center mr-3 py-1.5">
             <FaThermometerHalf className="text-theme-800 dark:text-theme-200 w-5 h-5" />
@@ -140,6 +193,19 @@ export default function Widget({ options }) {
                 </div>
                 <div className="pr-1">{t("glances.temp")}</div>
               </span>
+              {options.expanded && (
+                <span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
+                  <div className="pl-0.5 pr-1">
+                  {t("common.number", { 
+                    value: maxTemp,
+                    maximumFractionDigits: 1,
+                    style: "unit",
+                    unit
+                  })}
+                  </div>
+                  <div className="pr-1">{t("glances.warn")}</div>
+                </span>
+              )}
               <UsageBar percent={tempPercent} />
             </div>
           </div>)}
@@ -160,6 +226,6 @@ export default function Widget({ options }) {
       {options.label && (
         <div className="pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{options.label}</div>
       )}
-    </div>
+    </a>
   );
 }

+ 20 - 8
src/pages/api/widgets/glances.js

@@ -40,20 +40,32 @@ async function retrieveFromGlancesAPI(privateWidgetOptions, endpoint) {
 }
 
 export default async function handler(req, res) {
-  const { index } = req.query;
+  const { index, cputemp: includeCpuTemp, uptime: includeUptime, disk: includeDisks } = req.query;
 
   const privateWidgetOptions = await getPrivateWidgetOptions("glances", index);
 
   try {
-    const quicklookData = await retrieveFromGlancesAPI(privateWidgetOptions, "quicklook");
-
+    const cpuData = await retrieveFromGlancesAPI(privateWidgetOptions, "cpu");
+    const loadData = await retrieveFromGlancesAPI(privateWidgetOptions, "load");
+    const memoryData = await retrieveFromGlancesAPI(privateWidgetOptions, "mem");
     const data = {
-      quicklook: quicklookData
+      cpu: cpuData,
+      load: loadData,
+      mem: memoryData,
+    }
+
+    // Disabled by default, dont call unless needed
+    if (includeUptime) {
+      data.uptime = await retrieveFromGlancesAPI(privateWidgetOptions, "uptime");
+    }
+
+    if (includeCpuTemp) {
+      data.sensors = await retrieveFromGlancesAPI(privateWidgetOptions, "sensors");
+    }
+
+    if (includeDisks) {
+      data.fs = await retrieveFromGlancesAPI(privateWidgetOptions, "fs");
     }
-    
-    data.uptime = await retrieveFromGlancesAPI(privateWidgetOptions, "uptime");
-    
-    data.sensors = await retrieveFromGlancesAPI(privateWidgetOptions, "sensors");
 
     return res.status(200).send(data);
   } catch (e) {

+ 12 - 12
src/utils/config/widget-helpers.js

@@ -5,8 +5,6 @@ import yaml from "js-yaml";
 
 import checkAndCopyConfig, { substituteEnvironmentVars } from "utils/config/config";
 
-const exemptWidgets = ["search"];
-
 export async function widgetsFromConfig() {
     checkAndCopyConfig("widgets.yaml");
 
@@ -32,15 +30,17 @@ export async function cleanWidgetGroups(widgets) {
     return widgets.map((widget, index) => {
         const sanitizedOptions = widget.options;
         const optionKeys = Object.keys(sanitizedOptions);
-        if (!exemptWidgets.includes(widget.type)) {
-            ["url", "username", "password", "key"].forEach((pO) => { 
-                if (optionKeys.includes(pO)) {
-                    // allow URL in search
-                    if (widget.type !== "search" && pO !== "key") {
-                        delete sanitizedOptions[pO];
-                    }
-                }
-            });
+        
+        // delete private options from the sanitized options
+        ["username", "password", "key"].forEach((pO) => { 
+            if (optionKeys.includes(pO)) {
+                delete sanitizedOptions[pO];
+            }
+        });
+        
+        // delete url from the sanitized options if the widget is not a search or glances widgeth
+        if (widget.type !== "search" && widget.type !== "glances" && optionKeys.includes("url")) {
+            delete sanitizedOptions.url;
         }
 
         return {
@@ -78,4 +78,4 @@ export async function getPrivateWidgetOptions(type, widgetIndex) {
     });
   
     return (type !== undefined && widgetIndex !== undefined) ? privateOptions.find(o => o.type === type && o.options.index === parseInt(widgetIndex, 10))?.options : privateOptions;
-}
+}