ソースを参照

Merge pull request #1917 from benphelps/non-graphs

mini-non-chart charts
Ben Phelps 1 年間 前
コミット
99f60eab29

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

@@ -365,6 +365,7 @@
         "load": "Load",
         "wait": "Please wait",
         "temp": "TEMP",
+        "_temp": "Temp",
         "warn": "Warn",
         "uptime": "UP",
         "total": "Total",

+ 91 - 59
src/utils/config/service-helpers.js

@@ -13,7 +13,6 @@ import getKubeConfig from "utils/config/kubernetes";
 
 const logger = createLogger("service-helpers");
 
-
 export async function servicesFromConfig() {
   checkAndCopyConfig("services.yaml");
 
@@ -32,14 +31,14 @@ export async function servicesFromConfig() {
     services: servicesGroup[Object.keys(servicesGroup)[0]].map((entries) => ({
       name: Object.keys(entries)[0],
       ...entries[Object.keys(entries)[0]],
-      type: 'service'
+      type: "service",
     })),
   }));
 
   // add default weight to services based on their position in the configuration
   servicesArray.forEach((group, groupIndex) => {
     group.services.forEach((service, serviceIndex) => {
-      if(!service.weight) {
+      if (!service.weight) {
         servicesArray[groupIndex].services[serviceIndex].weight = (serviceIndex + 1) * 100;
       }
     });
@@ -66,7 +65,9 @@ export async function servicesFromDocker() {
         const isSwarm = !!servers[serverName].swarm;
         const docker = new Docker(getDockerArguments(serverName).conn);
         const listProperties = { all: true };
-        const containers = await ((isSwarm) ? docker.listServices(listProperties) : docker.listContainers(listProperties));
+        const containers = await (isSwarm
+          ? docker.listServices(listProperties)
+          : docker.listContainers(listProperties));
 
         // bad docker connections can result in a <Buffer ...> object?
         // in any case, this ensures the result is the expected array
@@ -76,8 +77,8 @@ export async function servicesFromDocker() {
 
         const discovered = containers.map((container) => {
           let constructedService = null;
-          const containerLabels = isSwarm ? shvl.get(container, 'Spec.Labels') : container.Labels;
-          const containerName = isSwarm ? shvl.get(container, 'Spec.Name') : container.Names[0];
+          const containerLabels = isSwarm ? shvl.get(container, "Spec.Labels") : container.Labels;
+          const containerName = isSwarm ? shvl.get(container, "Spec.Name") : container.Names[0];
 
           Object.keys(containerLabels).forEach((label) => {
             if (label.startsWith("homepage.")) {
@@ -85,10 +86,14 @@ export async function servicesFromDocker() {
                 constructedService = {
                   container: containerName.replace(/^\//, ""),
                   server: serverName,
-                  type: 'service'
+                  type: "service",
                 };
               }
-              shvl.set(constructedService, label.replace("homepage.", ""), substituteEnvironmentVars(containerLabels[label]));
+              shvl.set(
+                constructedService,
+                label.replace("homepage.", ""),
+                substituteEnvironmentVars(containerLabels[label])
+              );
             }
           });
 
@@ -132,12 +137,12 @@ export async function servicesFromDocker() {
 function getUrlFromIngress(ingress) {
   const urlHost = ingress.spec.rules[0].host;
   const urlPath = ingress.spec.rules[0].http.paths[0].path;
-  const urlSchema = ingress.spec.tls ? 'https' : 'http';
+  const urlSchema = ingress.spec.tls ? "https" : "http";
   return `${urlSchema}://${urlHost}${urlPath}`;
 }
 
 export async function servicesFromKubernetes() {
-  const ANNOTATION_BASE = 'gethomepage.dev';
+  const ANNOTATION_BASE = "gethomepage.dev";
   const ANNOTATION_WIDGET_BASE = `${ANNOTATION_BASE}/widget.`;
   const ANNOTATION_POD_SELECTOR = `${ANNOTATION_BASE}/pod-selector`;
 
@@ -151,39 +156,52 @@ export async function servicesFromKubernetes() {
     const networking = kc.makeApiClient(NetworkingV1Api);
     const crd = kc.makeApiClient(CustomObjectsApi);
 
-    const ingressList = await networking.listIngressForAllNamespaces(null, null, null, null)
+    const ingressList = await networking
+      .listIngressForAllNamespaces(null, null, null, null)
       .then((response) => response.body)
       .catch((error) => {
         logger.error("Error getting ingresses: %d %s %s", error.statusCode, error.body, error.response);
         return null;
       });
 
-    const traefikIngressListContaino = await crd.listClusterCustomObject("traefik.containo.us", "v1alpha1", "ingressroutes")
+    const traefikIngressListContaino = await crd
+      .listClusterCustomObject("traefik.containo.us", "v1alpha1", "ingressroutes")
       .then((response) => response.body)
       .catch(async (error) => {
         if (error.statusCode !== 404) {
-          logger.error("Error getting traefik ingresses from traefik.containo.us: %d %s %s", error.statusCode, error.body, error.response);
+          logger.error(
+            "Error getting traefik ingresses from traefik.containo.us: %d %s %s",
+            error.statusCode,
+            error.body,
+            error.response
+          );
         }
 
         return [];
       });
 
-    const traefikIngressListIo = await crd.listClusterCustomObject("traefik.io", "v1alpha1", "ingressroutes")
+    const traefikIngressListIo = await crd
+      .listClusterCustomObject("traefik.io", "v1alpha1", "ingressroutes")
       .then((response) => response.body)
       .catch(async (error) => {
         if (error.statusCode !== 404) {
-          logger.error("Error getting traefik ingresses from traefik.io: %d %s %s", error.statusCode, error.body, error.response);
-        }        
-        
+          logger.error(
+            "Error getting traefik ingresses from traefik.io: %d %s %s",
+            error.statusCode,
+            error.body,
+            error.response
+          );
+        }
+
         return [];
       });
-    
-    
+
     const traefikIngressList = [...traefikIngressListContaino, ...traefikIngressListIo];
 
     if (traefikIngressList && traefikIngressList.items.length > 0) {
-      const traefikServices = traefikIngressList.items
-      .filter((ingress) => ingress.metadata.annotations && ingress.metadata.annotations[`${ANNOTATION_BASE}/href`])
+      const traefikServices = traefikIngressList.items.filter(
+        (ingress) => ingress.metadata.annotations && ingress.metadata.annotations[`${ANNOTATION_BASE}/href`]
+      );
       ingressList.items.push(...traefikServices);
     }
 
@@ -191,43 +209,51 @@ export async function servicesFromKubernetes() {
       return [];
     }
     const services = ingressList.items
-      .filter((ingress) => ingress.metadata.annotations && ingress.metadata.annotations[`${ANNOTATION_BASE}/enabled`] === 'true')
+      .filter(
+        (ingress) =>
+          ingress.metadata.annotations && ingress.metadata.annotations[`${ANNOTATION_BASE}/enabled`] === "true"
+      )
       .map((ingress) => {
-      let constructedService = {
-        app: ingress.metadata.name,
-        namespace: ingress.metadata.namespace,
-        href: ingress.metadata.annotations[`${ANNOTATION_BASE}/href`] || getUrlFromIngress(ingress),
-        name: ingress.metadata.annotations[`${ANNOTATION_BASE}/name`] || ingress.metadata.name,
-        group: ingress.metadata.annotations[`${ANNOTATION_BASE}/group`] || "Kubernetes",
-        weight: ingress.metadata.annotations[`${ANNOTATION_BASE}/weight`] || '0',
-        icon: ingress.metadata.annotations[`${ANNOTATION_BASE}/icon`] || '',
-        description: ingress.metadata.annotations[`${ANNOTATION_BASE}/description`] || '',
-        external: false,
-        type: 'service'
-      };
-      if (ingress.metadata.annotations[`${ANNOTATION_BASE}/external`]) {
-        constructedService.external = String(ingress.metadata.annotations[`${ANNOTATION_BASE}/external`]).toLowerCase() === "true"
-      }
-      if (ingress.metadata.annotations[ANNOTATION_POD_SELECTOR]) {
-        constructedService.podSelector = ingress.metadata.annotations[ANNOTATION_POD_SELECTOR];
-      }
-      if (ingress.metadata.annotations[`${ANNOTATION_BASE}/ping`]) {
-        constructedService.ping = ingress.metadata.annotations[`${ANNOTATION_BASE}/ping`];
-      }
-      Object.keys(ingress.metadata.annotations).forEach((annotation) => {
-        if (annotation.startsWith(ANNOTATION_WIDGET_BASE)) {
-          shvl.set(constructedService, annotation.replace(`${ANNOTATION_BASE}/`, ""), ingress.metadata.annotations[annotation]);
+        let constructedService = {
+          app: ingress.metadata.name,
+          namespace: ingress.metadata.namespace,
+          href: ingress.metadata.annotations[`${ANNOTATION_BASE}/href`] || getUrlFromIngress(ingress),
+          name: ingress.metadata.annotations[`${ANNOTATION_BASE}/name`] || ingress.metadata.name,
+          group: ingress.metadata.annotations[`${ANNOTATION_BASE}/group`] || "Kubernetes",
+          weight: ingress.metadata.annotations[`${ANNOTATION_BASE}/weight`] || "0",
+          icon: ingress.metadata.annotations[`${ANNOTATION_BASE}/icon`] || "",
+          description: ingress.metadata.annotations[`${ANNOTATION_BASE}/description`] || "",
+          external: false,
+          type: "service",
+        };
+        if (ingress.metadata.annotations[`${ANNOTATION_BASE}/external`]) {
+          constructedService.external =
+            String(ingress.metadata.annotations[`${ANNOTATION_BASE}/external`]).toLowerCase() === "true";
         }
-      });
+        if (ingress.metadata.annotations[ANNOTATION_POD_SELECTOR]) {
+          constructedService.podSelector = ingress.metadata.annotations[ANNOTATION_POD_SELECTOR];
+        }
+        if (ingress.metadata.annotations[`${ANNOTATION_BASE}/ping`]) {
+          constructedService.ping = ingress.metadata.annotations[`${ANNOTATION_BASE}/ping`];
+        }
+        Object.keys(ingress.metadata.annotations).forEach((annotation) => {
+          if (annotation.startsWith(ANNOTATION_WIDGET_BASE)) {
+            shvl.set(
+              constructedService,
+              annotation.replace(`${ANNOTATION_BASE}/`, ""),
+              ingress.metadata.annotations[annotation]
+            );
+          }
+        });
 
-      try {
-        constructedService = JSON.parse(substituteEnvironmentVars(JSON.stringify(constructedService)));
-      } catch (e) {
-        logger.error("Error attempting k8s environment variable substitution.");
-      }
+        try {
+          constructedService = JSON.parse(substituteEnvironmentVars(JSON.stringify(constructedService)));
+        } catch (e) {
+          logger.error("Error attempting k8s environment variable substitution.");
+        }
 
-      return constructedService;
-    });
+        return constructedService;
+      });
 
     const mappedServiceGroups = [];
 
@@ -251,7 +277,6 @@ export async function servicesFromKubernetes() {
     });
 
     return mappedServiceGroups;
-
   } catch (e) {
     logger.error(e);
     throw e;
@@ -264,7 +289,7 @@ export function cleanServiceGroups(groups) {
     services: serviceGroup.services.map((service) => {
       const cleanedService = { ...service };
       if (cleanedService.showStats !== undefined) cleanedService.showStats = JSON.parse(cleanedService.showStats);
-      if (typeof service.weight === 'string') {
+      if (typeof service.weight === "string") {
         const weight = parseInt(service.weight, 10);
         if (Number.isNaN(weight)) {
           cleanedService.weight = 0;
@@ -303,6 +328,7 @@ export function cleanServiceGroups(groups) {
           userEmail, // azuredevops
           repositoryId,
           metric, // glances
+          chart, // glances
           stream, // mjpeg
           fit,
           method, // openmediavault widget
@@ -311,9 +337,10 @@ export function cleanServiceGroups(groups) {
         } = cleanedService.widget;
 
         let fieldsList = fields;
-        if (typeof fields === 'string') {
-          try { JSON.parse(fields) }
-          catch (e) {
+        if (typeof fields === "string") {
+          try {
+            JSON.parse(fields);
+          } catch (e) {
             logger.error("Invalid fields list detected in config for service '%s'", service.name);
             fieldsList = null;
           }
@@ -373,6 +400,11 @@ export function cleanServiceGroups(groups) {
         }
         if (type === "glances") {
           if (metric) cleanedService.widget.metric = metric;
+          if (chart !== undefined) {
+            cleanedService.widget.chart = chart;
+          } else {
+            cleanedService.widget.chart = true;
+          }
         }
         if (type === "mjpeg") {
           if (stream) cleanedService.widget.stream = stream;

+ 3 - 2
src/widgets/glances/components/container.jsx

@@ -1,9 +1,10 @@
-export default function Container({ children, className = "" }) {
+export default function Container({ children, chart = true, className = "" }) {
   return (
     <div>
       {children}
       <div className={`absolute top-0 right-0 bottom-0 left-0 overflow-clip pointer-events-none ${className}`} />
-      <div className="h-[68px] overflow-clip" />
+      { chart && <div className="h-[68px] overflow-clip" /> }
+      { !chart && <div className="h-[16px] overflow-clip" /> }
     </div>
   );
 }

+ 29 - 14
src/widgets/glances/metrics/cpu.jsx

@@ -14,6 +14,8 @@ const pointsLimit = 15;
 
 export default function Component({ service }) {
   const { t } = useTranslation();
+  const { widget } = service;
+  const { chart } = widget;
 
   const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
 
@@ -44,32 +46,45 @@ export default function Component({ service }) {
   }
 
   return (
-    <Container>
-      <Chart
-        dataPoints={dataPoints}
-        label={[t("resources.used")]}
-        formatter={(value) => t("common.number", {
-          value,
-          style: "unit",
-          unit: "percent",
-          maximumFractionDigits: 0,
-          })}
-      />
+    <Container chart={chart}>
+      { chart && (
+        <Chart
+          dataPoints={dataPoints}
+          label={[t("resources.used")]}
+          formatter={(value) => t("common.number", {
+            value,
+            style: "unit",
+            unit: "percent",
+            maximumFractionDigits: 0,
+            })}
+        />
+      )}
+
+      { !chart && (
+        <Block position="top-3 right-3">
+          <div className="text-xs opacity-50">
+            {systemData.linux_distro && `${systemData.linux_distro} - ` }
+            {systemData.os_version && systemData.os_version }
+          </div>
+        </Block>
+      )}
 
       {systemData && !systemError && (
         <Block position="bottom-3 left-3">
-          {systemData.linux_distro && (
+          {systemData.linux_distro && chart && (
             <div className="text-xs opacity-50">
               {systemData.linux_distro}
             </div>
           )}
-          {systemData.os_version && (
+
+          {systemData.os_version && chart && (
             <div className="text-xs opacity-50">
               {systemData.os_version}
             </div>
           )}
+
           {systemData.hostname && (
-            <div className="text-xs opacity-75">
+            <div className="text-xs opacity-50">
               {systemData.hostname}
             </div>
           )}

+ 16 - 13
src/widgets/glances/metrics/disk.jsx

@@ -15,6 +15,7 @@ const pointsLimit = 15;
 export default function Component({ service }) {
   const { t } = useTranslation();
   const { widget } = service;
+  const { chart } = widget;
   const [, diskName] = widget.metric.split(':');
 
   const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ read_bytes: 0, write_bytes: 0, time_since_update: 0 }, 0, pointsLimit));
@@ -65,24 +66,26 @@ export default function Component({ service }) {
   const currentRate = diskRates[diskRates.length - 1];
 
   return (
-    <Container>
-      <ChartDual
-        dataPoints={ratePoints}
-        label={[t("glances.read"), t("glances.write")]}
-        max={diskData.critical}
-        formatter={(value) => t("common.bitrate", {
-          value,
-          })}
-      />
+    <Container chart={chart}>
+      { chart && (
+        <ChartDual
+          dataPoints={ratePoints}
+          label={[t("glances.read"), t("glances.write")]}
+          max={diskData.critical}
+          formatter={(value) => t("common.bitrate", {
+            value,
+            })}
+        />
+      )}
 
       {currentRate && !error && (
-        <Block position="bottom-3 left-3">
-          <div className="text-xs opacity-50">
+        <Block position={chart ? "bottom-3 left-3" : "bottom-3 right-3"}>
+          <div className="text-xs opacity-50 text-right">
             {t("common.bitrate", {
               value: currentRate.a,
             })} {t("glances.read")}
           </div>
-          <div className="text-xs opacity-50">
+          <div className="text-xs opacity-50 text-right">
             {t("common.bitrate", {
               value: currentRate.b,
             })} {t("glances.write")}
@@ -90,7 +93,7 @@ export default function Component({ service }) {
         </Block>
       )}
 
-      <Block position="bottom-3 right-3">
+      <Block position={chart ? "bottom-3 right-3" : "bottom-3 left-3"}>
         <div className="text-xs opacity-75">
           {t("common.bitrate", {
             value: currentRate.a + currentRate.b,

+ 37 - 19
src/widgets/glances/metrics/fs.jsx

@@ -9,6 +9,7 @@ import useWidgetAPI from "utils/proxy/use-widget-api";
 export default function Component({ service }) {
   const { t } = useTranslation();
   const { widget } = service;
+  const { chart } = widget;
   const [, fsName] = widget.metric.split(':');
 
   const { data, error } = useWidgetAPI(widget, 'fs', {
@@ -30,35 +31,52 @@ export default function Component({ service }) {
   }
 
   return (
-    <Container>
-      <div className="absolute top-0 left-0 right-0 bottom-0">
-        <div style={{
-          height: `${Math.max(20, (fsData.size/fsData.free))}%`,
-        }} className="absolute bottom-0 border-t border-t-theme-500 bg-gradient-to-b from-theme-500/40 to-theme-500/10 w-full" />
+    <Container chart={chart}>
+      { chart && (
+        <div className="absolute top-0 left-0 right-0 bottom-0">
           <div style={{
-            top: `${100-Math.max(18, (fsData.size/fsData.free))}%`,
-          }} className="relative -my-5 ml-2.5 text-xs opacity-50">
+            height: `${Math.max(20, (fsData.size/fsData.free))}%`,
+          }} className="absolute bottom-0 border-t border-t-theme-500 bg-gradient-to-b from-theme-500/40 to-theme-500/10 w-full" />
+        </div>
+      )}
+
+      <Block position="bottom-3 left-3">
+        { fsData.used && chart && (
+          <div className="text-xs opacity-50">
             {t("common.bbytes", {
               value: fsData.used,
               maximumFractionDigits: 0,
             })} {t("resources.used")}
           </div>
-          <div style={{
-            top: `${100-Math.max(22, (fsData.size/fsData.free))}%`,
-          }} className="relative my-7 ml-2.5 text-xs opacity-50">
-            {t("common.bbytes", {
-              value: fsData.free,
-              maximumFractionDigits: 0,
-            })} {t("resources.free")}
-          </div>
-      </div>
+        )}
+
+        <div className="text-xs opacity-75">
+          {t("common.bbytes", {
+            value: fsData.free,
+            maximumFractionDigits: 0,
+          })} {t("resources.free")}
+        </div>
+      </Block>
+
+      { !chart && (
+        <Block position="top-3 right-3">
+          {fsData.used && (
+            <div className="text-xs opacity-50">
+              {t("common.bbytes", {
+                value: fsData.used,
+                maximumFractionDigits: 0,
+              })} {t("resources.used")}
+            </div>
+          )}
+        </Block>
+      )}
 
-      <Block position="top-3 right-3">
-        <div className="border rounded-md px-1.5 py-0.5 bg-theme-400/30 border-white/30 font-bold opacity-75">
+      <Block position="bottom-3 right-3">
+        <div className="text-xs opacity-75">
           {t("common.bbytes", {
             value: fsData.size,
             maximumFractionDigits: 1,
-          })}
+          })} {t("resources.total")}
         </div>
       </Block>
     </Container>

+ 71 - 34
src/widgets/glances/metrics/gpu.jsx

@@ -15,6 +15,7 @@ const pointsLimit = 15;
 export default function Component({ service }) {
   const { t } = useTranslation();
   const { widget } = service;
+  const { chart } = widget;
   const [, gpuName] = widget.metric.split(':');
 
   const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ a: 0, b: 0 }, 0, pointsLimit));
@@ -56,48 +57,84 @@ export default function Component({ service }) {
   }
 
   return (
-    <Container>
-      <ChartDual
-        dataPoints={dataPoints}
-        label={[t("glances.mem"), t("glances.gpu")]}
-        stack={['mem', 'proc']}
-        formatter={(value) => t("common.percent", {
-          value,
-          maximumFractionDigits: 1,
-        })}
-      />
-
-      <Block position="bottom-3 left-3">
-        {gpuData && gpuData.name && (
-            <div className="text-xs opacity-50">
-              {gpuData.name}
-            </div>
-        )}
-
-        <div className="text-xs opacity-75">
-          {t("common.number", {
-            value: gpuData.mem,
+    <Container chart={chart}>
+      { chart && (
+          <ChartDual
+          dataPoints={dataPoints}
+          label={[t("glances.mem"), t("glances.gpu")]}
+          stack={['mem', 'proc']}
+          formatter={(value) => t("common.percent", {
+            value,
             maximumFractionDigits: 1,
-          })}% {t("glances.mem")} {t("resources.used")}
-        </div>
-      </Block>
+          })}
+        />
+      )}
+
+      { chart && (
+        <Block position="bottom-3 left-3">
+          {gpuData && gpuData.name && (
+              <div className="text-xs opacity-50">
+                {gpuData.name}
+              </div>
+          )}
+
+          <div className="text-xs opacity-50">
+            {t("common.number", {
+              value: gpuData.mem,
+              maximumFractionDigits: 1,
+            })}% {t("resources.mem")}
+          </div>
+        </Block>
+      )}
+
+      { !chart && (
+        <Block position="bottom-3 left-3">
+          <div className="text-xs opacity-50">
+            {t("common.number", {
+              value: gpuData.temperature,
+              maximumFractionDigits: 1,
+            })}&deg; C
+          </div>
+        </Block>
+      )}
 
       <Block position="bottom-3 right-3">
         <div className="text-xs opacity-75">
-          {t("common.number", {
-            value: gpuData.proc,
-            maximumFractionDigits: 1,
-          })}% {t("glances.gpu")}
+          {!chart && (
+            <div className="inline-block mr-1">
+              {t("common.number", {
+                value: gpuData.proc,
+                maximumFractionDigits: 1,
+              })}% {t("glances.gpu")}
+            </div>
+          )}
+          { !chart && (
+            <>&bull;</>
+          )}
+          <div className="inline-block ml-1">
+            {t("common.number", {
+              value: gpuData.proc,
+              maximumFractionDigits: 1,
+            })}% {t("glances.gpu")}
+          </div>
         </div>
       </Block>
 
       <Block position="top-3 right-3">
-        <div className="text-xs opacity-75">
-          {t("common.number", {
-            value: gpuData.temperature,
-            maximumFractionDigits: 1,
-          })}&deg;
-        </div>
+        { chart && (
+          <div className="text-xs opacity-50">
+            {t("common.number", {
+              value: gpuData.temperature,
+              maximumFractionDigits: 1,
+            })}&deg; C
+          </div>
+        )}
+
+        {gpuData && gpuData.name && !chart && (
+          <div className="text-xs opacity-50">
+            {gpuData.name}
+          </div>
+        )}
       </Block>
     </Container>
   );

+ 98 - 57
src/widgets/glances/metrics/info.jsx

@@ -6,9 +6,65 @@ import Block from "../components/block";
 
 import useWidgetAPI from "utils/proxy/use-widget-api";
 
-export default function Component({ service }) {
+
+function Swap({ quicklookData, className = "" }) {
   const { t } = useTranslation();
 
+  return quicklookData && quicklookData.swap !== 0 && (
+    <div className="text-xs flex place-content-between">
+      <div className={className}>{t("glances.swap")}</div>
+      <div className={className}>
+        {t("common.number", {
+          value: quicklookData.swap,
+          style: "unit",
+          unit: "percent",
+          maximumFractionDigits: 0,
+        })}
+      </div>
+    </div>
+  );
+}
+
+function CPU({ quicklookData, className = "" }) {
+  const { t } = useTranslation();
+
+  return quicklookData && quicklookData.cpu && (
+    <div className="text-xs flex place-content-between">
+      <div className={className}>{t("glances.cpu")}</div>
+      <div className={className}>
+        {t("common.number", {
+          value: quicklookData.cpu,
+          style: "unit",
+          unit: "percent",
+          maximumFractionDigits: 0,
+        })}
+      </div>
+    </div>
+  );
+}
+
+function Mem({ quicklookData, className = "" }) {
+  const { t } = useTranslation();
+
+  return quicklookData && quicklookData.mem && (
+    <div className="text-xs flex place-content-between">
+      <div className={className}>{t("glances.mem")}</div>
+      <div className={className}>
+        {t("common.number", {
+          value: quicklookData.mem,
+          style: "unit",
+          unit: "percent",
+          maximumFractionDigits: 0,
+        })}
+      </div>
+    </div>
+  );
+}
+
+export default function Component({ service }) {
+  const { widget } = service;
+  const { chart } = widget;
+
   const { data: quicklookData, errorL: quicklookError } = useWidgetAPI(service.widget, 'quicklook', {
     refreshInterval: 1000,
   });
@@ -41,74 +97,59 @@ export default function Component({ service }) {
 
 
   return (
-    <Container className="bg-gradient-to-br from-theme-500/30 via-theme-600/20 to-theme-700/10">
+    <Container chart={chart} className="bg-gradient-to-br from-theme-500/30 via-theme-600/20 to-theme-700/10">
       <Block position="top-3 right-3">
-        {quicklookData && quicklookData.cpu_name && (
+        {quicklookData && quicklookData.cpu_name && chart && (
           <div className="text-[0.6rem] opacity-50">
             {quicklookData.cpu_name}
           </div>
         )}
-      </Block>
-      <Block position="bottom-3 left-3">
-        {systemData && systemData.linux_distro && (
-          <div className="text-xs opacity-50">
-            {systemData.linux_distro}
-          </div>
-        )}
-        {systemData && systemData.os_version && (
-          <div className="text-xs opacity-50">
-            {systemData.os_version}
-          </div>
-        )}
-        {systemData && systemData.hostname && (
-          <div className="text-xs opacity-75">
-            {systemData.hostname}
+
+        { !chart && quicklookData?.swap === 0 && (
+          <div className="text-[0.6rem] opacity-50">
+            {quicklookData.cpu_name}
           </div>
         )}
+
+        <div className="w-[4rem]">
+          { !chart && <Swap quicklookData={quicklookData} className="opacity-25" /> }
+        </div>
       </Block>
 
-      <Block position="bottom-3 right-3 w-[4rem]">
-        {quicklookData && quicklookData.cpu && (
-          <div className="text-xs opacity-25 flex place-content-between">
-            <div>{t("glances.cpu")}</div>
-            <div className="opacity-75">
-              {t("common.number", {
-                value: quicklookData.cpu,
-                style: "unit",
-                unit: "percent",
-                maximumFractionDigits: 0,
-              })}
-            </div>
-          </div>
-        )}
 
-        {quicklookData && quicklookData.mem && (
-          <div className="text-xs opacity-25 flex place-content-between">
-            <div>{t("glances.mem")}</div>
-            <div className="opacity-75">
-              {t("common.number", {
-                value: quicklookData.mem,
-                style: "unit",
-                unit: "percent",
-                maximumFractionDigits: 0,
-              })}
+      {chart && (
+        <Block position="bottom-3 left-3">
+          {systemData && systemData.linux_distro && (
+            <div className="text-xs opacity-50">
+              {systemData.linux_distro}
             </div>
-          </div>
-        )}
-
-        {quicklookData && quicklookData.swap !== 0 && (
-          <div className="text-xs opacity-25 flex place-content-between">
-            <div>{t("glances.swap")}</div>
-            <div className="opacity-75">
-              {t("common.number", {
-                value: quicklookData.swap,
-                style: "unit",
-                unit: "percent",
-                maximumFractionDigits: 0,
-              })}
+          )}
+          {systemData && systemData.os_version && (
+            <div className="text-xs opacity-50">
+              {systemData.os_version}
             </div>
-          </div>
-        )}
+          )}
+          {systemData && systemData.hostname && (
+            <div className="text-xs opacity-75">
+              {systemData.hostname}
+            </div>
+          )}
+        </Block>
+      )}
+
+      {!chart && (
+        <Block position="bottom-3 left-3 w-[3rem]">
+          <CPU quicklookData={quicklookData} className="opacity-75" />
+        </Block>
+      )}
+
+      <Block position="bottom-3 right-3 w-[4rem]">
+        { chart && <CPU quicklookData={quicklookData} className="opacity-50" /> }
+
+        { chart && <Mem quicklookData={quicklookData} className="opacity-50" /> }
+        { !chart && <Mem quicklookData={quicklookData} className="opacity-75" /> }
+
+        { chart && <Swap quicklookData={quicklookData} className="opacity-50" /> }
       </Block>
     </Container>
   );

+ 32 - 13
src/widgets/glances/metrics/memory.jsx

@@ -14,11 +14,14 @@ const pointsLimit = 15;
 
 export default function Component({ service }) {
   const { t } = useTranslation();
+  const { widget } = service;
+  const { chart } = widget;
+
 
   const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
 
   const { data, error } = useWidgetAPI(service.widget, 'mem', {
-    refreshInterval: 1000,
+    refreshInterval: chart ? 1000 : 5000,
   });
 
   useEffect(() => {
@@ -42,21 +45,23 @@ export default function Component({ service }) {
   }
 
   return (
-    <Container>
-      <ChartDual
-        dataPoints={dataPoints}
-        max={data.total}
-        label={[t("resources.used"), t("resources.free")]}
-        formatter={(value) => t("common.bytes", {
-          value,
-          maximumFractionDigits: 0,
-          binary: true,
-        })}
-      />
+    <Container chart={chart} >
+      {chart && (
+        <ChartDual
+          dataPoints={dataPoints}
+          max={data.total}
+          label={[t("resources.used"), t("resources.free")]}
+          formatter={(value) => t("common.bytes", {
+            value,
+            maximumFractionDigits: 0,
+            binary: true,
+          })}
+        />
+      )}
 
       {data && !error && (
         <Block position="bottom-3 left-3">
-          {data.free && (
+          {data.free && chart && (
             <div className="text-xs opacity-50">
               {t("common.bytes", {
                 value: data.free,
@@ -78,6 +83,20 @@ export default function Component({ service }) {
         </Block>
       )}
 
+      { !chart && (
+        <Block position="top-3 right-3">
+          {data.free && (
+            <div className="text-xs opacity-50">
+              {t("common.bytes", {
+                value: data.free,
+                maximumFractionDigits: 0,
+                binary: true,
+              })} {t("resources.free")}
+            </div>
+          )}
+        </Block>
+      )}
+
       <Block position="bottom-3 right-3">
         <div className="text-xs font-bold opacity-75">
           {t("common.bytes", {

+ 25 - 12
src/widgets/glances/metrics/net.jsx

@@ -15,12 +15,13 @@ const pointsLimit = 15;
 export default function Component({ service }) {
   const { t } = useTranslation();
   const { widget } = service;
-  const [, interfaceName] = widget.metric.split(':');
+  const { chart, metric } = widget;
+  const [, interfaceName] = metric.split(':');
 
   const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
 
   const { data, error } = useWidgetAPI(widget, 'network', {
-    refreshInterval: 1000,
+    refreshInterval: chart ? 1000 : 5000,
   });
 
   useEffect(() => {
@@ -54,18 +55,20 @@ export default function Component({ service }) {
   }
 
   return (
-    <Container>
-      <ChartDual
-        dataPoints={dataPoints}
-        label={[t("docker.tx"), t("docker.rx")]}
-        formatter={(value) => t("common.byterate", {
-          value,
-          maximumFractionDigits: 0,
-        })}
-      />
+    <Container chart={chart}>
+      { chart && (
+        <ChartDual
+          dataPoints={dataPoints}
+          label={[t("docker.tx"), t("docker.rx")]}
+          formatter={(value) => t("common.byterate", {
+            value,
+            maximumFractionDigits: 0,
+          })}
+        />
+      )}
 
       <Block position="bottom-3 left-3">
-        {interfaceData && interfaceData.interface_name && (
+        {interfaceData && interfaceData.interface_name && chart && (
             <div className="text-xs opacity-50">
               {interfaceData.interface_name}
             </div>
@@ -79,6 +82,16 @@ export default function Component({ service }) {
         </div>
       </Block>
 
+      { !chart && (
+        <Block position="top-3 right-3">
+          {interfaceData && interfaceData.interface_name && (
+              <div className="text-xs opacity-50">
+                {interfaceData.interface_name}
+              </div>
+          )}
+        </Block>
+      )}
+
       <Block position="bottom-3 right-3">
         <div className="text-xs opacity-75">
           {t("common.bitrate", {

+ 4 - 2
src/widgets/glances/metrics/process.jsx

@@ -19,6 +19,8 @@ const statusMap = {
 
 export default function Component({ service }) {
   const { t } = useTranslation();
+  const { widget } = service;
+  const { chart } = widget;
 
   const { data, error } = useWidgetAPI(service.widget, 'processlist', {
     refreshInterval: 1000,
@@ -32,10 +34,10 @@ export default function Component({ service }) {
     return <Container><Block position="bottom-3 left-3">-</Block></Container>;
   }
 
-  data.splice(5);
+  data.splice(chart ? 5 : 1);
 
   return (
-    <Container>
+    <Container chart={chart}>
       <Block position="top-4 right-3 left-3">
         <div className="flex items-center text-xs">
           <div className="grow" />

+ 27 - 17
src/widgets/glances/metrics/sensor.jsx

@@ -15,6 +15,7 @@ const pointsLimit = 15;
 export default function Component({ service }) {
   const { t } = useTranslation();
   const { widget } = service;
+  const { chart } = widget;
   const [, sensorName] = widget.metric.split(':');
 
   const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
@@ -51,37 +52,46 @@ export default function Component({ service }) {
   }
 
   return (
-    <Container>
-      <Chart
-        dataPoints={dataPoints}
-        label={[sensorData.unit]}
-        max={sensorData.critical}
-        formatter={(value) => t("common.number", {
-          value,
-          })}
-      />
+    <Container chart={chart}>
+      { chart && (
+        <Chart
+          dataPoints={dataPoints}
+          label={[sensorData.unit]}
+          max={sensorData.critical}
+          formatter={(value) => t("common.number", {
+            value,
+            })}
+        />
+      )}
 
       {sensorData && !error && (
         <Block position="bottom-3 left-3">
-          {sensorData.warning && (
+          {sensorData.warning && chart && (
             <div className="text-xs opacity-50">
-              {sensorData.warning}{sensorData.unit} {t("glances.warn")}
+              {t("glances.warn")} {sensorData.warning} {sensorData.unit}
             </div>
           )}
           {sensorData.critical && (
             <div className="text-xs opacity-50">
-              {sensorData.critical} {sensorData.unit} {t("glances.crit")}
+              {t("glances.crit")} {sensorData.critical} {sensorData.unit}
             </div>
           )}
         </Block>
       )}
 
       <Block position="bottom-3 right-3">
-        <div className="text-xs opacity-75">
-          {t("common.number", {
-            value: sensorData.value,
-          })} {sensorData.unit}
-        </div>
+          <div className="text-xs opacity-50">
+            {sensorData.warning && !chart && (
+              <>
+                {t("glances.warn")} {sensorData.warning} {sensorData.unit}
+              </>
+            )}
+          </div>
+          <div className="text-xs opacity-75">
+            {t("glances.temp")} {t("common.number", {
+              value: sensorData.value,
+            })} {sensorData.unit}
+          </div>
       </Block>
     </Container>
   );