Ben Phelps 1 рік тому
батько
коміт
b77909a360

+ 0 - 46
src/widgets/glances/chart.jsx

@@ -1,46 +0,0 @@
-import { PureComponent } from "react";
-import { AreaChart, Area, ResponsiveContainer, Tooltip } from "recharts";
-
-import CustomTooltip from "./custom_tooltip";
-
-class Chart extends PureComponent {
-  render() {
-    const { dataPoints, formatter, label } = this.props;
-
-    return (
-      <div className="overflow-clip z-10 w-full h-full">
-        <ResponsiveContainer width="100%" height="100%">
-          <AreaChart data={dataPoints}>
-            <defs>
-              <linearGradient id="color" x1="0" y1="0" x2="0" y2="1">
-                <stop offset="5%" stopColor="rgb(var(--color-500))" stopOpacity={0.4}/>
-                <stop offset="95%" stopColor="rgb(var(--color-500))" stopOpacity={0.1}/>
-              </linearGradient>
-            </defs>
-            <Area
-              name={label[0]}
-              isAnimationActive={false}
-              type="monotoneX"
-              dataKey="value"
-              stroke="rgb(var(--color-500))"
-              fillOpacity={1} fill="url(#color)"
-              baseLine={0}
-            />
-            <Tooltip
-              allowEscapeViewBox={{ x: false, y: false }}
-              formatter={formatter}
-              content={<CustomTooltip formatter={formatter} />}
-              classNames="rounded-md text-xs p-0.5"
-              contentStyle={{
-                backgroundColor: "rgb(var(--color-800))",
-                color: "rgb(var(--color-100))"
-              }}
-            />
-          </AreaChart>
-        </ResponsiveContainer>
-      </div>
-    );
-  }
-}
-
-export default Chart;

+ 0 - 61
src/widgets/glances/chart_dual.jsx

@@ -1,61 +0,0 @@
-import { PureComponent } from "react";
-import { AreaChart, Area, ResponsiveContainer, Tooltip } from "recharts";
-
-import CustomTooltip from "./custom_tooltip";
-
-class ChartDual extends PureComponent {
-  render() {
-    const { dataPoints, formatter, stack, label } = this.props;
-
-    return (
-      <div className="overflow-clip z-10 w-full h-full">
-        <ResponsiveContainer width="100%" height="100%">
-          <AreaChart data={dataPoints}  stackOffset={stack ?? "none"}>
-            <defs>
-              <linearGradient id="colorA" x1="0" y1="0" x2="0" y2="1">
-                <stop offset="5%" stopColor="rgb(var(--color-800))" stopOpacity={0.8}/>
-                <stop offset="95%" stopColor="rgb(var(--color-800))" stopOpacity={0.5}/>
-              </linearGradient>
-              <linearGradient id="colorB" x1="0" y1="0" x2="0" y2="1">
-                <stop offset="5%" stopColor="rgb(var(--color-500))" stopOpacity={0.4}/>
-                <stop offset="95%" stopColor="rgb(var(--color-500))" stopOpacity={0.1}/>
-              </linearGradient>
-            </defs>
-
-            <Area
-              name={label[0]}
-              stackId="1"
-              isAnimationActive={false}
-              type="monotoneX"
-              dataKey="a"
-              stroke="rgb(var(--color-700))"
-              fillOpacity={1} fill="url(#colorA)"
-            />
-            <Area
-              name={label[1]}
-              stackId="1"
-              isAnimationActive={false}
-              type="monotoneX"
-              dataKey="b"
-              stroke="rgb(var(--color-500))"
-              fillOpacity={1} fill="url(#colorB)"
-            />
-            <Tooltip
-              allowEscapeViewBox={{ x: false, y: false }}
-              formatter={formatter}
-              content={<CustomTooltip formatter={formatter} />}
-              classNames="rounded-md text-xs p-0.5"
-              contentStyle={{
-                backgroundColor: "rgb(var(--color-800))",
-                color: "rgb(var(--color-100))"
-              }}
-
-            />
-          </AreaChart>
-        </ResponsiveContainer>
-      </div>
-    );
-  }
-}
-
-export default ChartDual;

+ 6 - 6
src/widgets/glances/component.jsx

@@ -1,9 +1,9 @@
-import Memory from "./memory";
-import Cpu from "./cpu";
-import Sensor from "./sensor";
-import Net from "./net";
-import Process from "./process";
-import Disk from "./disk";
+import Memory from "./metrics/memory";
+import Cpu from "./metrics/cpu";
+import Sensor from "./metrics/sensor";
+import Net from "./metrics/net";
+import Process from "./metrics/process";
+import Disk from "./metrics/disk";
 
 
 export default function Component({ service }) {
 export default function Component({ service }) {
   const { widget } = service;
   const { widget } = service;

+ 9 - 0
src/widgets/glances/components/block.jsx

@@ -0,0 +1,9 @@
+export default function Block({ position, children }) {
+  const positionClasses = Object.entries(position).map(([key, value]) => `${key}-${value}`).join(' ');
+
+  return (
+    <div className={`absolute ${positionClasses} z-20 text-sm`}>
+      {children}
+    </div>
+  );
+}

+ 48 - 0
src/widgets/glances/components/chart.jsx

@@ -0,0 +1,48 @@
+import { PureComponent } from "react";
+import { AreaChart, Area, ResponsiveContainer, Tooltip } from "recharts";
+
+import CustomTooltip from "./custom_tooltip";
+
+class Chart extends PureComponent {
+  render() {
+    const { dataPoints, formatter, label } = this.props;
+
+    return (
+      <div className="absolute -top-1 -left-1 h-[120px] w-[calc(100%+0.5em)] z-0">
+        <div className="overflow-clip z-10 w-full h-full">
+          <ResponsiveContainer width="100%" height="100%">
+            <AreaChart data={dataPoints}>
+              <defs>
+                <linearGradient id="color" x1="0" y1="0" x2="0" y2="1">
+                  <stop offset="5%" stopColor="rgb(var(--color-500))" stopOpacity={0.4}/>
+                  <stop offset="95%" stopColor="rgb(var(--color-500))" stopOpacity={0.1}/>
+                </linearGradient>
+              </defs>
+              <Area
+                name={label[0]}
+                isAnimationActive={false}
+                type="monotoneX"
+                dataKey="value"
+                stroke="rgb(var(--color-500))"
+                fillOpacity={1} fill="url(#color)"
+                baseLine={0}
+              />
+              <Tooltip
+                allowEscapeViewBox={{ x: false, y: false }}
+                formatter={formatter}
+                content={<CustomTooltip formatter={formatter} />}
+                classNames="rounded-md text-xs p-0.5"
+                contentStyle={{
+                  backgroundColor: "rgb(var(--color-800))",
+                  color: "rgb(var(--color-100))"
+                }}
+              />
+            </AreaChart>
+          </ResponsiveContainer>
+        </div>
+      </div>
+    );
+  }
+}
+
+export default Chart;

+ 63 - 0
src/widgets/glances/components/chart_dual.jsx

@@ -0,0 +1,63 @@
+import { PureComponent } from "react";
+import { AreaChart, Area, ResponsiveContainer, Tooltip } from "recharts";
+
+import CustomTooltip from "./custom_tooltip";
+
+class ChartDual extends PureComponent {
+  render() {
+    const { dataPoints, formatter, stack, label } = this.props;
+
+    return (
+      <div className="absolute -top-1 -left-1 h-[120px] w-[calc(100%+0.5em)] z-0">
+        <div className="overflow-clip z-10 w-full h-full">
+          <ResponsiveContainer width="100%" height="100%">
+            <AreaChart data={dataPoints}  stackOffset={stack ?? "none"}>
+              <defs>
+                <linearGradient id="colorA" x1="0" y1="0" x2="0" y2="1">
+                  <stop offset="5%" stopColor="rgb(var(--color-800))" stopOpacity={0.8}/>
+                  <stop offset="95%" stopColor="rgb(var(--color-800))" stopOpacity={0.5}/>
+                </linearGradient>
+                <linearGradient id="colorB" x1="0" y1="0" x2="0" y2="1">
+                  <stop offset="5%" stopColor="rgb(var(--color-500))" stopOpacity={0.4}/>
+                  <stop offset="95%" stopColor="rgb(var(--color-500))" stopOpacity={0.1}/>
+                </linearGradient>
+              </defs>
+
+              <Area
+                name={label[0]}
+                stackId="1"
+                isAnimationActive={false}
+                type="monotoneX"
+                dataKey="a"
+                stroke="rgb(var(--color-700))"
+                fillOpacity={1} fill="url(#colorA)"
+              />
+              <Area
+                name={label[1]}
+                stackId="1"
+                isAnimationActive={false}
+                type="monotoneX"
+                dataKey="b"
+                stroke="rgb(var(--color-500))"
+                fillOpacity={1} fill="url(#colorB)"
+              />
+              <Tooltip
+                allowEscapeViewBox={{ x: false, y: false }}
+                formatter={formatter}
+                content={<CustomTooltip formatter={formatter} />}
+                classNames="rounded-md text-xs p-0.5"
+                contentStyle={{
+                  backgroundColor: "rgb(var(--color-800))",
+                  color: "rgb(var(--color-100))"
+                }}
+
+              />
+            </AreaChart>
+          </ResponsiveContainer>
+        </div>
+      </div>
+    );
+  }
+}
+
+export default ChartDual;

+ 8 - 0
src/widgets/glances/components/container.jsx

@@ -0,0 +1,8 @@
+export default function Container({ children }) {
+  return (
+    <div>
+      {children}
+      <div className="h-[68px] overflow-clip" />
+    </div>
+  );
+}

+ 0 - 0
src/widgets/glances/custom_tooltip.jsx → src/widgets/glances/components/custom_tooltip.jsx


+ 9 - 0
src/widgets/glances/components/error.jsx

@@ -0,0 +1,9 @@
+import { useTranslation } from "next-i18next";
+
+export default function Error() {
+  const { t } = useTranslation();
+
+  return <div className="absolute bottom-2 left-2 z-20 text-red-400 text-xs opacity-75">
+    {t("widget.api_error")}
+  </div>;
+}

+ 0 - 100
src/widgets/glances/cpu.jsx

@@ -1,100 +0,0 @@
-import dynamic from "next/dynamic";
-import { useState, useEffect } from "react";
-import { useTranslation } from "next-i18next";
-
-import useWidgetAPI from "utils/proxy/use-widget-api";
-
-const Chart = dynamic(() => import("./chart"), { ssr: false });
-
-const pointsLimit = 15;
-
-export default function Component({ service }) {
-  const { t } = useTranslation();
-
-  const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
-
-  const { data, error } = useWidgetAPI(service.widget, 'cpu', {
-    refreshInterval: 1000,
-  });
-
-  const { data: systemData, error: systemError } = useWidgetAPI(service.widget, 'system');
-
-  useEffect(() => {
-    if (data) {
-      setDataPoints((prevDataPoints) => {
-        const newDataPoints = [...prevDataPoints, { value: data.total }];
-          if (newDataPoints.length > pointsLimit) {
-              newDataPoints.shift();
-          }
-          return newDataPoints;
-      });
-    }
-  }, [data]);
-
-  if (error) {
-    return <div>
-    <div className="h-[68px] overflow-clip">
-      <div className="absolute bottom-2 left-2 z-20 text-red-400 text-xs opacity-75">
-      {t("widget.api_error")}
-      </div>
-    </div>
-  </div>;
-  }
-
-  if (!data) {
-    return <div>
-    <div className="h-[68px] overflow-clip">
-      <div className="absolute bottom-2 left-2 z-20 text-xs opacity-75">
-        -
-      </div>
-    </div>
-  </div>;
-  }
-
-  return (
-    <>
-      <div className="absolute -top-1 -left-1 h-[120px] w-[calc(100%+0.5em)] z-0">
-      <Chart
-          dataPoints={dataPoints}
-          label={[t("resources.used")]}
-          formatter={(value) => t("common.number", {
-            value,
-            style: "unit",
-            unit: "percent",
-            maximumFractionDigits: 0,
-            })}
-        />
-      </div>
-      <div className="absolute bottom-3 left-3 opacity-50 z-10 pointer-events-none">
-        {systemData && !systemError && (
-          <>
-            {systemData.linux_distro && (
-              <div className="text-xs opacity-80">
-                {systemData.linux_distro}
-              </div>
-            )}
-            {systemData.os_version && (
-              <div className="text-xs opacity-80">
-                {systemData.os_version}
-              </div>
-            )}
-            {systemData.hostname && (
-              <div className="text-xs font-bold">
-                {systemData.hostname}
-              </div>
-            )}
-          </>
-        )}
-      </div>
-      <div className="absolute bottom-3 right-3 z-10 text-xs opacity-80 pointer-events-none">
-        {t("common.number", {
-          value: data.total,
-          style: "unit",
-          unit: "percent",
-          maximumFractionDigits: 0,
-        })} {t("resources.used")}
-      </div>
-      <div className="h-[68px] overflow-clip" />
-    </>
-  );
-}

+ 0 - 96
src/widgets/glances/memory.jsx

@@ -1,96 +0,0 @@
-import dynamic from "next/dynamic";
-import { useState, useEffect } from "react";
-import { useTranslation } from "next-i18next";
-
-import useWidgetAPI from "utils/proxy/use-widget-api";
-
-const ChartDual = dynamic(() => import("./chart_dual"), { ssr: false });
-
-const pointsLimit = 15;
-
-export default function Component({ service }) {
-  const { t } = useTranslation();
-
-  const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
-
-  const { data, error } = useWidgetAPI(service.widget, 'mem', {
-    refreshInterval: 1000,
-  });
-
-  useEffect(() => {
-    if (data) {
-      setDataPoints((prevDataPoints) => {
-        const newDataPoints = [...prevDataPoints, { a: data.used, b: data.free }];
-          if (newDataPoints.length > pointsLimit) {
-              newDataPoints.shift();
-          }
-          return newDataPoints;
-      });
-    }
-  }, [data]);
-
-  if (error) {
-    return <div>
-    <div className="h-[68px] overflow-clip">
-      <div className="absolute bottom-2 left-2 z-20 text-red-400 text-xs opacity-80">
-      {t("widget.api_error")}
-      </div>
-    </div>
-  </div>;
-  }
-  if (!data) {
-    return <div>
-    <div className="h-[68px] overflow-clip">
-      <div className="absolute bottom-2 left-2 z-20 text-xs opacity-80">
-        -
-      </div>
-    </div>
-  </div>;
-  }
-
-  return (
-    <>
-      <div className="absolute -top-1 -left-1 h-[120px] w-[calc(100%+0.5em)] z-0">
-        <ChartDual
-          dataPoints={dataPoints}
-          max={data.total}
-          label={[t("resources.used"), t("resources.free")]}
-          formatter={(value) => t("common.bytes", {
-            value,
-            maximumFractionDigits: 0,
-          })}
-        />
-      </div>
-      <div className="absolute bottom-3 left-3 z-10 opacity-50 pointer-events-none">
-        {data && !error && (
-          <>
-            {data.free && (
-              <div className="text-xs opacity-80">
-                {t("common.bytes", {
-                  value: data.free,
-                  maximumFractionDigits: 0,
-                })} {t("resources.free")}
-              </div>
-            )}
-
-            {data.total && (
-              <div className="text-xs font-bold">
-                {t("common.bytes", {
-                  value: data.total,
-                  maximumFractionDigits: 0,
-                })} {t("resources.total")}
-              </div>
-            )}
-          </>
-        )}
-      </div>
-      <div className="absolute bottom-3 right-3 z-10 text-xs opacity-80 pointer-events-none">
-        {t("common.bytes", {
-          value: data.used,
-          maximumFractionDigits: 0,
-        })} {t("resources.used")}
-      </div>
-      <div className="h-[68px] overflow-clip" />
-    </>
-  );
-}

+ 91 - 0
src/widgets/glances/metrics/cpu.jsx

@@ -0,0 +1,91 @@
+import dynamic from "next/dynamic";
+import { useState, useEffect } from "react";
+import { useTranslation } from "next-i18next";
+
+import Error from "../components/error";
+import Container from "../components/container";
+import Block from "../components/block";
+
+import useWidgetAPI from "utils/proxy/use-widget-api";
+
+const Chart = dynamic(() => import("../components/chart"), { ssr: false });
+
+const pointsLimit = 15;
+
+export default function Component({ service }) {
+  const { t } = useTranslation();
+
+  const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
+
+  const { data, error } = useWidgetAPI(service.widget, 'cpu', {
+    refreshInterval: 1000,
+  });
+
+  const { data: systemData, error: systemError } = useWidgetAPI(service.widget, 'system');
+
+  useEffect(() => {
+    if (data) {
+      setDataPoints((prevDataPoints) => {
+        const newDataPoints = [...prevDataPoints, { value: data.total }];
+          if (newDataPoints.length > pointsLimit) {
+              newDataPoints.shift();
+          }
+          return newDataPoints;
+      });
+    }
+  }, [data]);
+
+  if (error) {
+    return <Container><Error error={error} /></Container>;
+  }
+
+  if (!data) {
+    return <Container><Block position={{bottom: 2, left: 2}}>-</Block></Container>;
+  }
+
+  return (
+    <Container>
+      <Chart
+        dataPoints={dataPoints}
+        label={[t("resources.used")]}
+        formatter={(value) => t("common.number", {
+          value,
+          style: "unit",
+          unit: "percent",
+          maximumFractionDigits: 0,
+          })}
+      />
+
+      {systemData && !systemError && (
+        <Block position={{bottom: 3, left: 3}}>
+          {systemData.linux_distro && (
+            <div className="text-xs opacity-50">
+              {systemData.linux_distro}
+            </div>
+          )}
+          {systemData.os_version && (
+            <div className="text-xs opacity-50">
+              {systemData.os_version}
+            </div>
+          )}
+          {systemData.hostname && (
+            <div className="text-xs opacity-75">
+              {systemData.hostname}
+            </div>
+          )}
+        </Block>
+      )}
+
+      <Block position={{bottom: 3, right: 3}}>
+        <div className="text-xs font-bold opacity-75">
+            {t("common.number", {
+              value: data.total,
+              style: "unit",
+              unit: "percent",
+              maximumFractionDigits: 0,
+            })} {t("resources.used")}
+          </div>
+      </Block>
+    </Container>
+  );
+}

+ 40 - 51
src/widgets/glances/disk.jsx → src/widgets/glances/metrics/disk.jsx

@@ -2,9 +2,13 @@ import dynamic from "next/dynamic";
 import { useState, useEffect } from "react";
 import { useState, useEffect } from "react";
 import { useTranslation } from "next-i18next";
 import { useTranslation } from "next-i18next";
 
 
+import Error from "../components/error";
+import Container from "../components/container";
+import Block from "../components/block";
+
 import useWidgetAPI from "utils/proxy/use-widget-api";
 import useWidgetAPI from "utils/proxy/use-widget-api";
 
 
-const ChartDual = dynamic(() => import("./chart_dual"), { ssr: false });
+const ChartDual = dynamic(() => import("../components/chart_dual"), { ssr: false });
 
 
 const pointsLimit = 15;
 const pointsLimit = 15;
 
 
@@ -44,70 +48,55 @@ export default function Component({ service }) {
   }, [dataPoints]);
   }, [dataPoints]);
 
 
   if (error) {
   if (error) {
-    return <div>
-    <div className="h-[68px] overflow-clip">
-      <div className="absolute bottom-2 left-2 z-20 text-red-400 text-xs opacity-80">
-      {t("widget.api_error")}
-      </div>
-    </div>
-  </div>;
+    return <Container><Error error={error} /></Container>;
   }
   }
 
 
   if (!data) {
   if (!data) {
-    return <div>
-    <div className="h-[68px] overflow-clip">
-      <div className="absolute bottom-2 left-2 z-20 text-xs opacity-80">
-        -
-      </div>
-    </div>
-  </div>;
+    return <Container><Block position={{bottom: 2, left: 2}}>-</Block></Container>;
   }
   }
 
 
   const diskData = data.find((item) => item.disk_name === diskName);
   const diskData = data.find((item) => item.disk_name === diskName);
 
 
   if (!diskData) {
   if (!diskData) {
-    return <div>
-      <div className="h-[68px] overflow-clip" />
-    </div>;
+    return <Container><Block position={{bottom: 2, left: 2}}>-</Block></Container>;
   }
   }
 
 
   const diskRates = calculateRates(dataPoints);
   const diskRates = calculateRates(dataPoints);
   const currentRate = diskRates[diskRates.length - 1];
   const currentRate = diskRates[diskRates.length - 1];
 
 
   return (
   return (
-    <>
-      <div className="absolute -top-1 -left-1 h-[120px] w-[calc(100%+0.5em)] z-0">
+    <Container>
       <ChartDual
       <ChartDual
-          dataPoints={ratePoints}
-          label={[t("glances.read"), t("glances.write")]}
-          max={diskData.critical}
-          formatter={(value) => t("common.bitrate", {
-            value,
-            })}
-        />
-      </div>
-      <div className="absolute bottom-3 left-3 opacity-50 z-10 pointer-events-none">
-        {currentRate && !error && (
-          <>
-            <div className="text-xs opacity-80">
-              {t("common.bitrate", {
-                value: currentRate.a,
-              })} {t("glances.read")}
-            </div>
-            <div className="text-xs opacity-80">
-              {t("common.bitrate", {
-                value: currentRate.b,
-              })} {t("glances.write")}
-            </div>
-          </>
-        )}
-      </div>
-      <div className="absolute bottom-3 right-3 z-10 text-xs opacity-80 pointer-events-none">
-        {t("common.bitrate", {
-          value: currentRate.a + currentRate.b,
-        })}
-      </div>
-      <div className="h-[68px] overflow-clip" />
-    </>
+        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">
+            {t("common.bitrate", {
+              value: currentRate.a,
+            })} {t("glances.read")}
+          </div>
+          <div className="text-xs opacity-50">
+            {t("common.bitrate", {
+              value: currentRate.b,
+            })} {t("glances.write")}
+          </div>
+        </Block>
+      )}
+
+      <Block position={{bottom: 3, right: 3}}>
+        <div className="text-xs opacity-75">
+          {t("common.bitrate", {
+            value: currentRate.a + currentRate.b,
+          })}
+        </div>
+      </Block>
+    </Container>
   );
   );
 }
 }

+ 88 - 0
src/widgets/glances/metrics/memory.jsx

@@ -0,0 +1,88 @@
+import dynamic from "next/dynamic";
+import { useState, useEffect } from "react";
+import { useTranslation } from "next-i18next";
+
+import Error from "../components/error";
+import Container from "../components/container";
+import Block from "../components/block";
+
+import useWidgetAPI from "utils/proxy/use-widget-api";
+
+const ChartDual = dynamic(() => import("../components/chart_dual"), { ssr: false });
+
+const pointsLimit = 15;
+
+export default function Component({ service }) {
+  const { t } = useTranslation();
+
+  const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
+
+  const { data, error } = useWidgetAPI(service.widget, 'mem', {
+    refreshInterval: 1000,
+  });
+
+  useEffect(() => {
+    if (data) {
+      setDataPoints((prevDataPoints) => {
+        const newDataPoints = [...prevDataPoints, { a: data.used, b: data.free }];
+          if (newDataPoints.length > pointsLimit) {
+              newDataPoints.shift();
+          }
+          return newDataPoints;
+      });
+    }
+  }, [data]);
+
+  if (error) {
+    return <Container><Error error={error} /></Container>;
+  }
+
+  if (!data) {
+    return <Container><Block position={{bottom: 2, left: 2}}>-</Block></Container>;
+  }
+
+  return (
+    <Container>
+      <ChartDual
+        dataPoints={dataPoints}
+        max={data.total}
+        label={[t("resources.used"), t("resources.free")]}
+        formatter={(value) => t("common.bytes", {
+          value,
+          maximumFractionDigits: 0,
+        })}
+      />
+
+      {data && !error && (
+        <Block position={{bottom: 3, left: 3}}>
+          {data.free && (
+            <div className="text-xs opacity-50">
+              {t("common.bytes", {
+                value: data.free,
+                maximumFractionDigits: 0,
+              })} {t("resources.free")}
+            </div>
+          )}
+
+          {data.total && (
+            <div className="text-xs opacity-50">
+              {t("common.bytes", {
+                value: data.total,
+                maximumFractionDigits: 0,
+              })} {t("resources.total")}
+            </div>
+          )}
+        </Block>
+      )}
+
+      <Block position={{bottom: 3, right: 3}}>
+        <div className="text-xs font-bold opacity-75">
+          {t("common.bytes", {
+            value: data.used,
+            maximumFractionDigits: 0,
+          })} {t("resources.used")}
+        </div>
+      </Block>
+    </Container>
+  );
+}

+ 40 - 44
src/widgets/glances/net.jsx → src/widgets/glances/metrics/net.jsx

@@ -2,9 +2,13 @@ import dynamic from "next/dynamic";
 import { useState, useEffect } from "react";
 import { useState, useEffect } from "react";
 import { useTranslation } from "next-i18next";
 import { useTranslation } from "next-i18next";
 
 
+import Error from "../components/error";
+import Container from "../components/container";
+import Block from "../components/block";
+
 import useWidgetAPI from "utils/proxy/use-widget-api";
 import useWidgetAPI from "utils/proxy/use-widget-api";
 
 
-const ChartDual = dynamic(() => import("./chart_dual"), { ssr: false });
+const ChartDual = dynamic(() => import("../components/chart_dual"), { ssr: false });
 
 
 const pointsLimit = 15;
 const pointsLimit = 15;
 
 
@@ -36,61 +40,53 @@ export default function Component({ service }) {
   }, [data, interfaceName]);
   }, [data, interfaceName]);
 
 
   if (error) {
   if (error) {
-    return <div>
-    <div className="h-[68px] overflow-clip">
-      <div className="absolute bottom-2 left-2 z-20 text-red-400 text-xs opacity-80">
-      {t("widget.api_error")}
-      </div>
-    </div>
-  </div>;
+    return <Container><Error error={error} /></Container>;
   }
   }
+
   if (!data) {
   if (!data) {
-    return <div>
-    <div className="h-[68px] overflow-clip">
-      <div className="absolute bottom-2 left-2 z-20 text-xs opacity-80">
-        -
-      </div>
-    </div>
-  </div>;
+    return <Container><Block position={{bottom: 2, left: 2}}>-</Block></Container>;
   }
   }
+
   const interfaceData = data.find((item) => item[item.key] === interfaceName);
   const interfaceData = data.find((item) => item[item.key] === interfaceName);
 
 
   if (!interfaceData) {
   if (!interfaceData) {
-    return <div>
-      <div className="h-[68px] overflow-clip" />
-    </div>;
+    return <Container><Block position={{bottom: 2, left: 2}}>-</Block></Container>;
   }
   }
 
 
   return (
   return (
-    <>
-      <div className="absolute -top-1 -left-1 h-[120px] w-[calc(100%+0.5em)] z-0">
-        <ChartDual
-          dataPoints={dataPoints}
-          label={[t("docker.tx"), t("docker.rx")]}
-          formatter={(value) => t("common.byterate", {
-            value,
-            maximumFractionDigits: 0,
-          })}
-        />
-      </div>
-      <div className="absolute bottom-3 left-3 z-10 text-xs opacity-80 pointer-events-none">
+    <Container>
+      <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 && (
-            <div className="text-xs opacity-80">
+            <div className="text-xs opacity-50">
               {interfaceData.interface_name}
               {interfaceData.interface_name}
             </div>
             </div>
         )}
         )}
-        {t("common.bitrate", {
-          value: interfaceData.tx,
-          maximumFractionDigits: 0,
-        })} {t("docker.tx")}
-      </div>
-      <div className="absolute bottom-3 right-3 z-10 text-xs opacity-80 pointer-events-none">
-        {t("common.bitrate", {
-          value: interfaceData.rx,
-          maximumFractionDigits: 0,
-        })} {t("docker.rx")}
-      </div>
-      <div className="h-[68px] overflow-clip" />
-    </>
+
+        <div className="text-xs opacity-75">
+          {t("common.bitrate", {
+            value: interfaceData.tx,
+            maximumFractionDigits: 0,
+          })} {t("docker.tx")}
+        </div>
+      </Block>
+
+      <Block position={{bottom: 3, right: 3}}>
+        <div className="text-xs opacity-75">
+          {t("common.bitrate", {
+            value: interfaceData.rx,
+            maximumFractionDigits: 0,
+          })} {t("docker.rx")}
+        </div>
+      </Block>
+    </Container>
   );
   );
 }
 }

+ 20 - 26
src/widgets/glances/process.jsx → src/widgets/glances/metrics/process.jsx

@@ -1,5 +1,9 @@
 import { useTranslation } from "next-i18next";
 import { useTranslation } from "next-i18next";
 
 
+import Error from "../components/error";
+import Container from "../components/container";
+import Block from "../components/block";
+
 import useWidgetAPI from "utils/proxy/use-widget-api";
 import useWidgetAPI from "utils/proxy/use-widget-api";
 import ResolvedIcon from "components/resolvedicon";
 import ResolvedIcon from "components/resolvedicon";
 
 
@@ -21,52 +25,42 @@ export default function Component({ service }) {
   });
   });
 
 
   if (error) {
   if (error) {
-    return <div>
-    <div className="h-[68px] overflow-clip">
-      <div className="absolute bottom-2 left-2 z-20 text-red-400 text-xs opacity-80">
-      {t("widget.api_error")}
-      </div>
-    </div>
-  </div>;
+    return <Container><Error error={error} /></Container>;
   }
   }
 
 
   if (!data) {
   if (!data) {
-    return <div>
-    <div className="h-[68px] overflow-clip">
-      <div className="absolute bottom-2 left-2 z-20 text-xs opacity-80">
-        -
-      </div>
-    </div>
-  </div>;
+    return <Container><Block position={{bottom: 2, left: 2}}>-</Block></Container>;
   }
   }
 
 
   data.splice(5);
   data.splice(5);
 
 
   return (
   return (
-    <>
-      <div className="absolute top-4 right-3 left-3 opacity-30 z-10 pointer-events-none">
+    <Container>
+      <Block position={{top: 4, right: 3, left: 3}}>
         <div className="flex items-center text-xs">
         <div className="flex items-center text-xs">
           <div className="grow" />
           <div className="grow" />
           <div className="w-14 text-right italic">{t("resources.cpu")}</div>
           <div className="w-14 text-right italic">{t("resources.cpu")}</div>
           <div className="w-14 text-right">{t("resources.mem")}</div>
           <div className="w-14 text-right">{t("resources.mem")}</div>
         </div>
         </div>
-      </div>
-      <div className="absolute bottom-4 right-3 left-3 z-10 pointer-events-none text-theme-900 dark:text-theme-200">
-        { data.map((item) => <div key={item.pid} className="text-[0.75rem] h-[0.8rem]">
+      </Block>
+
+      <Block position={{bottom: 4, right: 3, left: 3}}>
+        <div className="pointer-events-none text-theme-900 dark:text-theme-200">
+          { data.map((item) => <div key={item.pid} className="text-[0.75rem] h-[0.8rem]">
             <div className="flex items-center">
             <div className="flex items-center">
-              <div className="w-3 h-3 mr-1.5 opacity-60">
+              <div className="w-3 h-3 mr-1.5 opacity-50">
                 {statusMap[item.status]}
                 {statusMap[item.status]}
               </div>
               </div>
-              <div className="opacity-60 grow">{item.name}</div>
-              <div className="opacity-30 w-14 text-right">{item.cpu_percent.toFixed(1)}%</div>
-              <div className="opacity-30 w-14 text-right">{t("common.bytes", {
+              <div className="opacity-75 grow">{item.name}</div>
+              <div className="opacity-25 w-14 text-right">{item.cpu_percent.toFixed(1)}%</div>
+              <div className="opacity-25 w-14 text-right">{t("common.bytes", {
                 value: item.memory_info[0],
                 value: item.memory_info[0],
                 maximumFractionDigits: 0,
                 maximumFractionDigits: 0,
               })}</div>
               })}</div>
             </div>
             </div>
           </div>) }
           </div>) }
-      </div>
-      <div className="h-[68px] overflow-clip" />
-    </>
+        </div>
+      </Block>
+    </Container>
   );
   );
 }
 }

+ 88 - 0
src/widgets/glances/metrics/sensor.jsx

@@ -0,0 +1,88 @@
+import dynamic from "next/dynamic";
+import { useState, useEffect } from "react";
+import { useTranslation } from "next-i18next";
+
+import Error from "../components/error";
+import Container from "../components/container";
+import Block from "../components/block";
+
+import useWidgetAPI from "utils/proxy/use-widget-api";
+
+const Chart = dynamic(() => import("../components/chart"), { ssr: false });
+
+const pointsLimit = 15;
+
+export default function Component({ service }) {
+  const { t } = useTranslation();
+  const { widget } = service;
+  const [, sensorName] = widget.metric.split(':');
+
+  const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
+
+  const { data, error } = useWidgetAPI(service.widget, 'sensors', {
+    refreshInterval: 1000,
+  });
+
+  useEffect(() => {
+    if (data) {
+      const sensorData = data.find((item) => item.label === sensorName);
+      setDataPoints((prevDataPoints) => {
+        const newDataPoints = [...prevDataPoints, { value: sensorData.value }];
+          if (newDataPoints.length > pointsLimit) {
+              newDataPoints.shift();
+          }
+          return newDataPoints;
+      });
+    }
+  }, [data, sensorName]);
+
+  if (error) {
+    return <Container><Error error={error} /></Container>;
+  }
+
+  if (!data) {
+    return <Container><Block position={{bottom: 2, left: 2}}>-</Block></Container>;
+  }
+
+  const sensorData = data.find((item) => item.label === sensorName);
+
+  if (!sensorData) {
+    return <Container><Block position={{bottom: 2, left: 2}}>-</Block></Container>;
+  }
+
+  return (
+    <Container>
+      <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 && (
+            <div className="text-xs opacity-50">
+              {sensorData.warning}{sensorData.unit} {t("glances.warn")}
+            </div>
+          )}
+          {sensorData.critical && (
+            <div className="text-xs opacity-50">
+              {sensorData.critical} {sensorData.unit} {t("glances.crit")}
+            </div>
+          )}
+        </Block>
+      )}
+
+      <Block position={{bottom: 3, right: 3}}>
+        <div className="text-xs opacity-75">
+          {t("common.number", {
+            value: sensorData.value,
+          })} {sensorData.unit}
+        </div>
+      </Block>
+    </Container>
+  );
+}

+ 0 - 99
src/widgets/glances/sensor.jsx

@@ -1,99 +0,0 @@
-import dynamic from "next/dynamic";
-import { useState, useEffect } from "react";
-import { useTranslation } from "next-i18next";
-
-import useWidgetAPI from "utils/proxy/use-widget-api";
-
-const Chart = dynamic(() => import("./chart"), { ssr: false });
-
-const pointsLimit = 15;
-
-export default function Component({ service }) {
-  const { t } = useTranslation();
-  const { widget } = service;
-  const [, sensorName] = widget.metric.split(':');
-
-  const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
-
-  const { data, error } = useWidgetAPI(service.widget, 'sensors', {
-    refreshInterval: 1000,
-  });
-
-  useEffect(() => {
-    if (data) {
-      const sensorData = data.find((item) => item.label === sensorName);
-      setDataPoints((prevDataPoints) => {
-        const newDataPoints = [...prevDataPoints, { value: sensorData.value }];
-          if (newDataPoints.length > pointsLimit) {
-              newDataPoints.shift();
-          }
-          return newDataPoints;
-      });
-    }
-  }, [data, sensorName]);
-
-  if (error) {
-    return <div>
-    <div className="h-[68px] overflow-clip">
-      <div className="absolute bottom-2 left-2 z-20 text-red-400 text-xs opacity-80">
-      {t("widget.api_error")}
-      </div>
-    </div>
-  </div>;
-  }
-
-  if (!data) {
-    return <div>
-    <div className="h-[68px] overflow-clip">
-      <div className="absolute bottom-2 left-2 z-20 text-xs opacity-80">
-        -
-      </div>
-    </div>
-  </div>;
-  }
-
-  const sensorData = data.find((item) => item.label === sensorName);
-
-  if (!sensorData) {
-    return <div>
-      <div className="h-[68px] overflow-clip" />
-    </div>;
-  }
-
-  return (
-    <>
-      <div className="absolute -top-1 -left-1 h-[120px] w-[calc(100%+0.5em)] z-0">
-      <Chart
-          dataPoints={dataPoints}
-          label={[sensorData.unit]}
-          max={sensorData.critical}
-          formatter={(value) => t("common.number", {
-            value,
-            })}
-        />
-      </div>
-      <div className="absolute bottom-3 left-3 opacity-50 z-10 pointer-events-none">
-        {sensorData && !error && (
-          <>
-            {sensorData.warning && (
-              <div className="text-xs opacity-80">
-                {sensorData.warning}{sensorData.unit} {t("glances.warn")}
-              </div>
-            )}
-            {sensorData.critical && (
-              <div className="text-xs opacity-80">
-                {sensorData.critical} {sensorData.unit} {t("glances.crit")}
-              </div>
-            )}
-          </>
-        )}
-      </div>
-      <div className="absolute bottom-3 right-3 z-10 text-xs opacity-80 pointer-events-none">
-        {t("common.number", {
-          value: sensorData.value,
-        })} {sensorData.unit}
-      </div>
-      <div className="h-[68px] overflow-clip" />
-    </>
-  );
-}