diff --git a/src/components/services/item.jsx b/src/components/services/item.jsx
index cf85d279..162d616e 100644
--- a/src/components/services/item.jsx
+++ b/src/components/services/item.jsx
@@ -2,12 +2,11 @@ import Image from "next/future/image";
import { useState } from "react";
import { Disclosure, Transition } from "@headlessui/react";
-import StatsList from "./stats/list";
import Status from "./status";
import Widget from "./widget";
+import Docker from "./widgets/service/docker";
export default function Item({ service }) {
- const [statsOpen, setStatsOpen] = useState(false);
return (
@@ -46,7 +45,7 @@ export default function Item({ service }) {
-
+
diff --git a/src/components/services/stats/list.jsx b/src/components/services/stats/list.jsx
deleted file mode 100644
index cb2cdfaa..00000000
--- a/src/components/services/stats/list.jsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import useSWR from "swr";
-import { calculateCPUPercent, formatBytes } from "utils/stats-helpers";
-import Stat from "./stat";
-
-export default function Stats({ service }) {
- // fast
- const { data: statusData, error: statusError } = useSWR(
- `/api/docker/status/${service.container}/${service.server || ""}`,
- {
- refreshInterval: 1500,
- }
- );
-
- // takes a full second to collect stats
- const { data: statsData, error: statsError } = useSWR(
- `/api/docker/stats/${service.container}/${service.server || ""}`,
- {
- refreshInterval: 1500,
- }
- );
-
- // handle errors first
- if (statsError || statusError) {
- return (
-
-
-
- );
- }
-
- // handle the case where we get a docker error
- if (statusData.status !== "running") {
- return (
-
-
-
- );
- }
-
- // handle the case where the container is offline
- if (statusData.status !== "running") {
- return (
-
-
-
- );
- }
-
- // handle the case where we don't have anything yet
- if (!statsData || !statusData) {
- return (
-
-
-
-
-
-
- );
- }
-
- // we have stats and the container is running
- return (
-
-
-
-
-
-
- );
-}
diff --git a/src/components/services/stats/stat.jsx b/src/components/services/stats/stat.jsx
deleted file mode 100644
index 57674d4c..00000000
--- a/src/components/services/stats/stat.jsx
+++ /dev/null
@@ -1,8 +0,0 @@
-export default function Stat({ value, label }) {
- return (
-
- );
-}
diff --git a/src/components/services/widget.jsx b/src/components/services/widget.jsx
index ccbb9176..62df9301 100644
--- a/src/components/services/widget.jsx
+++ b/src/components/services/widget.jsx
@@ -1,11 +1,13 @@
-import Sonarr from "./widgets/sonarr";
-import Radarr from "./widgets/radarr";
-import Ombi from "./widgets/ombi";
-import Portainer from "./widgets/portainer";
-import Emby from "./widgets/emby";
-import Nzbget from "./widgets/nzbget";
+import Sonarr from "./widgets/service/sonarr";
+import Radarr from "./widgets/service/radarr";
+import Ombi from "./widgets/service/ombi";
+import Portainer from "./widgets/service/portainer";
+import Emby from "./widgets/service/emby";
+import Nzbget from "./widgets/service/nzbget";
+import Docker from "./widgets/service/docker";
const widgetMappings = {
+ docker: Docker,
sonarr: Sonarr,
radarr: Radarr,
ombi: Ombi,
diff --git a/src/components/services/widgets/block.jsx b/src/components/services/widgets/block.jsx
new file mode 100644
index 00000000..e0c1858a
--- /dev/null
+++ b/src/components/services/widgets/block.jsx
@@ -0,0 +1,8 @@
+export default function Block({ value, label }) {
+ return (
+
+
{value === undefined || value === null ? "-" : value}
+
{label}
+
+ );
+}
diff --git a/src/components/services/widgets/emby.jsx b/src/components/services/widgets/emby.jsx
deleted file mode 100644
index 0b8d1eb3..00000000
--- a/src/components/services/widgets/emby.jsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import useSWR from "swr";
-
-export default function Emby({ service }) {
- const config = service.widget;
-
- function buildApiUrl(endpoint) {
- const { url, key } = config;
- return `${url}/emby/${endpoint}?api_key=${key}`;
- }
-
- const { data: sessionsData, error: sessionsError } = useSWR(buildApiUrl(`Sessions`), {
- refreshInterval: 1000,
- });
-
- if (sessionsError) {
- return (
-
- );
- }
-
- if (!sessionsData) {
- return (
-
- );
- }
-
- const playing = sessionsData.filter((session) => session.hasOwnProperty("NowPlayingItem"));
- const transcoding = sessionsData.filter(
- (session) => session.hasOwnProperty("PlayState") && session.PlayState.PlayMethod === "Transcode"
- );
- const bitrate = playing.reduce((acc, session) => acc + session.NowPlayingItem.Bitrate, 0);
-
- return (
-
-
-
{playing.length}
-
PLAYING
-
-
-
{transcoding.length}
-
TRANSCODE
-
-
-
{Math.round((bitrate / 1024 / 1024) * 100) / 100} Mbps
-
BITRATE
-
-
- );
-}
diff --git a/src/components/services/widgets/nzbget.jsx b/src/components/services/widgets/nzbget.jsx
deleted file mode 100644
index ec79cb76..00000000
--- a/src/components/services/widgets/nzbget.jsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import useSWR from "swr";
-import { JSONRPCClient } from "json-rpc-2.0";
-
-import { formatBytes } from "utils/stats-helpers";
-
-export default function Nzbget({ service }) {
- const config = service.widget;
-
- const constructedUrl = new URL(config.url);
- constructedUrl.pathname = "jsonrpc";
-
- const client = new JSONRPCClient((jsonRPCRequest) =>
- fetch(constructedUrl.toString(), {
- method: "POST",
- headers: {
- "content-type": "application/json",
- authorization: `Basic ${btoa(`${config.username}:${config.password}`)}`,
- },
- body: JSON.stringify(jsonRPCRequest),
- }).then(async (response) => {
- if (response.status === 200) {
- const jsonRPCResponse = await response.json();
- return client.receive(jsonRPCResponse);
- } else if (jsonRPCRequest.id !== undefined) {
- return Promise.reject(new Error(response.statusText));
- }
- })
- );
-
- const { data: statusData, error: statusError } = useSWR(
- "status",
- (resource) => {
- return client.request(resource).then((response) => response);
- },
- {
- refreshInterval: 1000,
- }
- );
-
- if (statusError) {
- return (
-
- );
- }
-
- if (!statusData) {
- return (
-
- );
- }
-
- return (
-
-
-
{formatBytes(statusData.DownloadRate)}/s
-
RATE
-
-
-
{Math.round((statusData.RemainingSizeMB / 1024) * 100) / 100} GB
-
REMAINING
-
-
-
{Math.round((statusData.DownloadedSizeMB / 1024) * 100) / 100} GB
-
DOWNLOADED
-
-
- );
-}
diff --git a/src/components/services/widgets/ombi.jsx b/src/components/services/widgets/ombi.jsx
deleted file mode 100644
index e5527cb2..00000000
--- a/src/components/services/widgets/ombi.jsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import useSWR from "swr";
-
-export default function Ombi({ service }) {
- const config = service.widget;
-
- function buildApiUrl(endpoint) {
- const { url } = config;
- return `${url}/api/v1/${endpoint}`;
- }
-
- const fetcher = (url) => {
- return fetch(url, {
- method: "GET",
- withCredentials: true,
- credentials: "include",
- headers: {
- ApiKey: `${config.key}`,
- "Content-Type": "application/json",
- },
- }).then((res) => res.json());
- };
-
- const { data: statsData, error: statsError } = useSWR(
- buildApiUrl(`Request/count`),
- fetcher
- );
-
- if (statsError) {
- return (
-
- );
- }
-
- if (!statsData) {
- return (
-
- );
- }
-
- return (
-
-
-
{statsData.pending}
-
PENDING
-
-
-
{statsData.approved}
-
APPROVED
-
-
-
{statsData.available}
-
AVAILABLE
-
-
- );
-}
diff --git a/src/components/services/widgets/portainer.jsx b/src/components/services/widgets/portainer.jsx
deleted file mode 100644
index 7af630dc..00000000
--- a/src/components/services/widgets/portainer.jsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import useSWR from "swr";
-
-export default function Portainer({ service }) {
- const config = service.widget;
-
- function buildApiUrl(endpoint) {
- const { url, env } = config;
- const reqUrl = new URL(`/api/endpoints/${env}/${endpoint}`, url);
- return `/api/proxy?url=${encodeURIComponent(reqUrl)}`;
- }
-
- const fetcher = (url) => {
- return fetch(url, {
- method: "GET",
- withCredentials: true,
- credentials: "include",
- headers: {
- "X-API-Key": `${config.key}`,
- "Content-Type": "application/json",
- },
- }).then((res) => res.json());
- };
-
- const { data: containersData, error: containersError } = useSWR(buildApiUrl(`docker/containers/json`), fetcher);
-
- if (containersError) {
- return (
-
- );
- }
-
- if (!containersData) {
- return (
-
- );
- }
-
- if (containersData.error) {
- return (
-
- );
- }
-
- const running = containersData.filter((c) => c.State === "running").length;
- const stopped = containersData.filter((c) => c.State === "exited").length;
- const total = containersData.length;
-
- return (
-
- );
-}
diff --git a/src/components/services/widgets/radarr.jsx b/src/components/services/widgets/radarr.jsx
deleted file mode 100644
index 56213841..00000000
--- a/src/components/services/widgets/radarr.jsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import useSWR from "swr";
-
-export default function Radarr({ service }) {
- const config = service.widget;
-
- function buildApiUrl(endpoint) {
- const { url, key } = config;
- return `${url}/api/v3/${endpoint}?apikey=${key}`;
- }
-
- const { data: moviesData, error: moviesError } = useSWR(buildApiUrl("movie"));
-
- const { data: queuedData, error: queuedError } = useSWR(
- buildApiUrl("queue/status")
- );
-
- if (moviesError || queuedError) {
- return (
-
- );
- }
-
- if (!moviesData || !queuedData) {
- return (
-
- );
- }
-
- const wanted = moviesData.filter((movie) => movie.isAvailable === false);
- const have = moviesData.filter((movie) => movie.isAvailable === true);
-
- return (
-
-
-
{wanted.length}
-
WANTED
-
-
-
{queuedData.totalCount}
-
QUEUED
-
-
-
{moviesData.length}
-
MOVIES
-
-
- );
-}
diff --git a/src/components/services/widgets/service/docker.jsx b/src/components/services/widgets/service/docker.jsx
new file mode 100644
index 00000000..396398fb
--- /dev/null
+++ b/src/components/services/widgets/service/docker.jsx
@@ -0,0 +1,56 @@
+import useSWR from "swr";
+
+import { calculateCPUPercent, formatBytes } from "utils/stats-helpers";
+
+import Widget from "../widget";
+import Block from "../block";
+
+export default function Docker({ service }) {
+ const config = service.widget;
+
+ const { data: statusData, error: statusError } = useSWR(
+ `/api/docker/status/${config.container}/${config.server || ""}`,
+ {
+ refreshInterval: 1500,
+ }
+ );
+
+ const { data: statsData, error: statsError } = useSWR(
+ `/api/docker/stats/${config.container}/${config.server || ""}`,
+ {
+ refreshInterval: 1500,
+ }
+ );
+
+ if (statsError || statusError) {
+ return ;
+ }
+
+ if (statusData && statusData.status !== "running") {
+ return (
+
+
+
+ );
+ }
+
+ if (!statsData || !statusData) {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/services/widgets/service/emby.jsx b/src/components/services/widgets/service/emby.jsx
new file mode 100644
index 00000000..230be641
--- /dev/null
+++ b/src/components/services/widgets/service/emby.jsx
@@ -0,0 +1,45 @@
+import useSWR from "swr";
+
+import Widget from "../widget";
+import Block from "../block";
+
+export default function Emby({ service }) {
+ const config = service.widget;
+
+ function buildApiUrl(endpoint) {
+ const { url, key } = config;
+ return `${url}/emby/${endpoint}?api_key=${key}`;
+ }
+
+ const { data: sessionsData, error: sessionsError } = useSWR(buildApiUrl(`Sessions`), {
+ refreshInterval: 1000,
+ });
+
+ if (sessionsError) {
+ return ;
+ }
+
+ if (!sessionsData) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const playing = sessionsData.filter((session) => session.hasOwnProperty("NowPlayingItem"));
+ const transcoding = sessionsData.filter(
+ (session) => session.hasOwnProperty("PlayState") && session.PlayState.PlayMethod === "Transcode"
+ );
+ const bitrate = playing.reduce((acc, session) => acc + session.NowPlayingItem.Bitrate, 0);
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/components/services/widgets/service/nzbget.jsx b/src/components/services/widgets/service/nzbget.jsx
new file mode 100644
index 00000000..87d77b91
--- /dev/null
+++ b/src/components/services/widgets/service/nzbget.jsx
@@ -0,0 +1,64 @@
+import useSWR from "swr";
+import { JSONRPCClient } from "json-rpc-2.0";
+
+import { formatBytes } from "utils/stats-helpers";
+
+import Widget from "../widget";
+import Block from "../block";
+
+export default function Nzbget({ service }) {
+ const config = service.widget;
+
+ const constructedUrl = new URL(config.url);
+ constructedUrl.pathname = "jsonrpc";
+
+ const client = new JSONRPCClient((jsonRPCRequest) =>
+ fetch(constructedUrl.toString(), {
+ method: "POST",
+ headers: {
+ "content-type": "application/json",
+ authorization: `Basic ${btoa(`${config.username}:${config.password}`)}`,
+ },
+ body: JSON.stringify(jsonRPCRequest),
+ }).then(async (response) => {
+ if (response.status === 200) {
+ const jsonRPCResponse = await response.json();
+ return client.receive(jsonRPCResponse);
+ } else if (jsonRPCRequest.id !== undefined) {
+ return Promise.reject(new Error(response.statusText));
+ }
+ })
+ );
+
+ const { data: statusData, error: statusError } = useSWR(
+ "status",
+ (resource) => {
+ return client.request(resource).then((response) => response);
+ },
+ {
+ refreshInterval: 1000,
+ }
+ );
+
+ if (statusError) {
+ return ;
+ }
+
+ if (!statusData) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/components/services/widgets/service/ombi.jsx b/src/components/services/widgets/service/ombi.jsx
new file mode 100644
index 00000000..54c44f7a
--- /dev/null
+++ b/src/components/services/widgets/service/ombi.jsx
@@ -0,0 +1,49 @@
+import useSWR from "swr";
+
+import Widget from "../widget";
+import Block from "../block";
+
+export default function Ombi({ service }) {
+ const config = service.widget;
+
+ function buildApiUrl(endpoint) {
+ const { url } = config;
+ return `${url}/api/v1/${endpoint}`;
+ }
+
+ const fetcher = (url) => {
+ return fetch(url, {
+ method: "GET",
+ withCredentials: true,
+ credentials: "include",
+ headers: {
+ ApiKey: `${config.key}`,
+ "Content-Type": "application/json",
+ },
+ }).then((res) => res.json());
+ };
+
+ const { data: statsData, error: statsError } = useSWR(buildApiUrl(`Request/count`), fetcher);
+
+ if (statsError) {
+ return ;
+ }
+
+ if (!statsData) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/components/services/widgets/service/portainer.jsx b/src/components/services/widgets/service/portainer.jsx
new file mode 100644
index 00000000..d29ec0d0
--- /dev/null
+++ b/src/components/services/widgets/service/portainer.jsx
@@ -0,0 +1,59 @@
+import useSWR from "swr";
+
+import Widget from "../widget";
+import Block from "../block";
+
+export default function Portainer({ service }) {
+ const config = service.widget;
+
+ function buildApiUrl(endpoint) {
+ const { url, env } = config;
+ const reqUrl = new URL(`/api/endpoints/${env}/${endpoint}`, url);
+ return `/api/proxy?url=${encodeURIComponent(reqUrl)}`;
+ }
+
+ const fetcher = async (url) => {
+ const res = await fetch(url, {
+ method: "GET",
+ withCredentials: true,
+ credentials: "include",
+ headers: {
+ "X-API-Key": `${config.key}`,
+ "Content-Type": "application/json",
+ },
+ });
+ return await res.json();
+ };
+
+ const { data: containersData, error: containersError } = useSWR(buildApiUrl(`docker/containers/json`), fetcher);
+
+ if (containersError) {
+ return ;
+ }
+
+ if (!containersData) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (containersData.error) {
+ return ;
+ }
+
+ const running = containersData.filter((c) => c.State === "running").length;
+ const stopped = containersData.filter((c) => c.State === "exited").length;
+ const total = containersData.length;
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/components/services/widgets/service/radarr.jsx b/src/components/services/widgets/service/radarr.jsx
new file mode 100644
index 00000000..d6e42cb8
--- /dev/null
+++ b/src/components/services/widgets/service/radarr.jsx
@@ -0,0 +1,41 @@
+import useSWR from "swr";
+
+import Widget from "../widget";
+import Block from "../block";
+
+export default function Radarr({ service }) {
+ const config = service.widget;
+
+ function buildApiUrl(endpoint) {
+ const { url, key } = config;
+ return `${url}/api/v3/${endpoint}?apikey=${key}`;
+ }
+
+ const { data: moviesData, error: moviesError } = useSWR(buildApiUrl("movie"));
+ const { data: queuedData, error: queuedError } = useSWR(buildApiUrl("queue/status"));
+
+ if (moviesError || queuedError) {
+ return ;
+ }
+
+ if (!moviesData || !queuedData) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const wanted = moviesData.filter((movie) => movie.isAvailable === false);
+ const have = moviesData.filter((movie) => movie.isAvailable === true);
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/components/services/widgets/service/sonarr.jsx b/src/components/services/widgets/service/sonarr.jsx
new file mode 100644
index 00000000..36afee20
--- /dev/null
+++ b/src/components/services/widgets/service/sonarr.jsx
@@ -0,0 +1,39 @@
+import useSWR from "swr";
+
+import Widget from "../widget";
+import Block from "../block";
+
+export default function Sonarr({ service }) {
+ const config = service.widget;
+
+ function buildApiUrl(endpoint) {
+ const { url, key } = config;
+ return `${url}/api/v3/${endpoint}?apikey=${key}`;
+ }
+
+ const { data: wantedData, error: wantedError } = useSWR(buildApiUrl("wanted/missing"));
+ const { data: queuedData, error: queuedError } = useSWR(buildApiUrl("queue"));
+ const { data: seriesData, error: seriesError } = useSWR(buildApiUrl("series"));
+
+ if (wantedError || queuedError || seriesError) {
+ return ;
+ }
+
+ if (!wantedData || !queuedData || !seriesData) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/components/services/widgets/sonarr.jsx b/src/components/services/widgets/sonarr.jsx
deleted file mode 100644
index afdda396..00000000
--- a/src/components/services/widgets/sonarr.jsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import useSWR from "swr";
-
-export default function Sonarr({ service }) {
- const config = service.widget;
-
- function buildApiUrl(endpoint) {
- const { url, key } = config;
- return `${url}/api/v3/${endpoint}?apikey=${key}`;
- }
-
- const { data: wantedData, error: wantedError } = useSWR(
- buildApiUrl("wanted/missing")
- );
-
- const { data: queuedData, error: queuedError } = useSWR(buildApiUrl("queue"));
-
- const { data: seriesData, error: seriesError } = useSWR(
- buildApiUrl("series")
- );
-
- if (wantedError || queuedError || seriesError) {
- return (
-
- );
- }
-
- if (!wantedData || !queuedData || !seriesData) {
- return (
-
- );
- }
-
- return (
-
-
-
{wantedData.totalRecords}
-
WANTED
-
-
-
{queuedData.totalRecords}
-
QUEUED
-
-
-
{seriesData.length}
-
SERIES
-
-
- );
-}
diff --git a/src/components/services/widgets/widget.jsx b/src/components/services/widgets/widget.jsx
new file mode 100644
index 00000000..98e4683e
--- /dev/null
+++ b/src/components/services/widgets/widget.jsx
@@ -0,0 +1,11 @@
+export default function Widget({ error = false, children }) {
+ if (error) {
+ return (
+
+ );
+ }
+
+ return {children}
;
+}