mirror of
				https://github.com/gethomepage/homepage.git
				synced 2025-11-04 03:27:02 -05:00 
			
		
		
		
	refactor service widgets
This commit is contained in:
		
							parent
							
								
									7807a38a9c
								
							
						
					
					
						commit
						94e9d66bec
					
				@ -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 (
 | 
			
		||||
    <li key={service.name} className="">
 | 
			
		||||
      <Disclosure>
 | 
			
		||||
@ -46,7 +45,7 @@ export default function Item({ service }) {
 | 
			
		||||
 | 
			
		||||
          <Disclosure.Panel>
 | 
			
		||||
            <div className="w-full">
 | 
			
		||||
              <StatsList service={service} />
 | 
			
		||||
              <Docker service={{ widget: { container: service.container, server: service.server } }} />
 | 
			
		||||
            </div>
 | 
			
		||||
          </Disclosure.Panel>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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 (
 | 
			
		||||
      <div className="flex flex-row w-full">
 | 
			
		||||
        <Stat label="STATUS" value="Error Fetching Data" />
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // handle the case where we get a docker error
 | 
			
		||||
  if (statusData.status !== "running") {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="flex flex-row w-full">
 | 
			
		||||
        <Stat label="STATUS" value="Error Fetching Data" />
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // handle the case where the container is offline
 | 
			
		||||
  if (statusData.status !== "running") {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="flex flex-row w-full">
 | 
			
		||||
        <Stat label="STATUS" value="Offline" />
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // handle the case where we don't have anything yet
 | 
			
		||||
  if (!statsData || !statusData) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="flex flex-row w-full">
 | 
			
		||||
        <Stat label="CPU" value="-" />
 | 
			
		||||
        <Stat label="MEM" value="-" />
 | 
			
		||||
        <Stat label="RX" value="-" />
 | 
			
		||||
        <Stat label="TX" value="-" />
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // we have stats and the container is running
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-row w-full">
 | 
			
		||||
      <Stat label="CPU" value={calculateCPUPercent(statsData.stats) + "%"} />
 | 
			
		||||
      <Stat label="MEM" value={formatBytes(statsData.stats.memory_stats.usage, 0)} />
 | 
			
		||||
      <Stat label="RX" value={formatBytes(statsData.stats.networks.eth0.rx_bytes, 0)} />
 | 
			
		||||
      <Stat label="TX" value={formatBytes(statsData.stats.networks.eth0.tx_bytes, 0)} />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -1,8 +0,0 @@
 | 
			
		||||
export default function Stat({ value, label }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
      <div className="font-thin text-sm">{value}</div>
 | 
			
		||||
      <div className="font-bold text-xs">{label}</div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8
									
								
								src/components/services/widgets/block.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/components/services/widgets/block.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
export default function Block({ value, label }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
      <div className="font-thin text-sm">{value === undefined || value === null ? "-" : value}</div>
 | 
			
		||||
      <div className="font-bold text-xs uppercase">{label}</div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -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 (
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">Emby API Error</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!sessionsData) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="flex flex-row w-full">
 | 
			
		||||
        <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
          <div className="font-thin text-sm">-</div>
 | 
			
		||||
          <div className="font-bold text-xs">PLAYING</div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
          <div className="font-thin text-sm">-</div>
 | 
			
		||||
          <div className="font-bold text-xs">TRANSCODE</div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
          <div className="font-thin text-sm">-</div>
 | 
			
		||||
          <div className="font-bold text-xs">BITRATE</div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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 (
 | 
			
		||||
    <div className="flex flex-row w-full">
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">{playing.length}</div>
 | 
			
		||||
        <div className="font-bold text-xs">PLAYING</div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">{transcoding.length}</div>
 | 
			
		||||
        <div className="font-bold text-xs">TRANSCODE</div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">{Math.round((bitrate / 1024 / 1024) * 100) / 100} Mbps</div>
 | 
			
		||||
        <div className="font-bold text-xs">BITRATE</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -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 (
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">Nzbget API Error</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!statusData) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="flex flex-row w-full">
 | 
			
		||||
        <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
          <div className="font-thin text-sm">-</div>
 | 
			
		||||
          <div className="font-bold text-xs">RATE</div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
          <div className="font-thin text-sm">-</div>
 | 
			
		||||
          <div className="font-bold text-xs">REMAINING</div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
          <div className="font-thin text-sm">-</div>
 | 
			
		||||
          <div className="font-bold text-xs">DOWNLOADED</div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-row w-full">
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">{formatBytes(statusData.DownloadRate)}/s</div>
 | 
			
		||||
        <div className="font-bold text-xs">RATE</div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">{Math.round((statusData.RemainingSizeMB / 1024) * 100) / 100} GB</div>
 | 
			
		||||
        <div className="font-bold text-xs">REMAINING</div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">{Math.round((statusData.DownloadedSizeMB / 1024) * 100) / 100} GB</div>
 | 
			
		||||
        <div className="font-bold text-xs">DOWNLOADED</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -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 (
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">Ombi API Error</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!statsData) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="flex flex-row w-full">
 | 
			
		||||
        <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
          <div className="font-thin text-sm">-</div>
 | 
			
		||||
          <div className="font-bold text-xs">COMPLETED</div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
          <div className="font-thin text-sm">-</div>
 | 
			
		||||
          <div className="font-bold text-xs">QUEUED</div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
          <div className="font-thin text-sm">-</div>
 | 
			
		||||
          <div className="font-bold text-xs">TOTAL</div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-row w-full">
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">{statsData.pending}</div>
 | 
			
		||||
        <div className="font-bold text-xs">PENDING</div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">{statsData.approved}</div>
 | 
			
		||||
        <div className="font-bold text-xs">APPROVED</div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">{statsData.available}</div>
 | 
			
		||||
        <div className="font-bold text-xs">AVAILABLE</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -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 (
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">Portainer API Error</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!containersData) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="flex flex-row w-full">
 | 
			
		||||
        <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
          <div className="font-thin text-sm">-</div>
 | 
			
		||||
          <div className="font-bold text-xs">RUNNING</div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
          <div className="font-thin text-sm">-</div>
 | 
			
		||||
          <div className="font-bold text-xs">STOPPED</div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
          <div className="font-thin text-sm">-</div>
 | 
			
		||||
          <div className="font-bold text-xs">TOTAL</div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (containersData.error) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">Portainer API Error</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const running = containersData.filter((c) => c.State === "running").length;
 | 
			
		||||
  const stopped = containersData.filter((c) => c.State === "exited").length;
 | 
			
		||||
  const total = containersData.length;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-row w-full">
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">{running}</div>
 | 
			
		||||
        <div className="font-bold text-xs">RUNNING</div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">{stopped}</div>
 | 
			
		||||
        <div className="font-bold text-xs">STOPPED</div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">{total}</div>
 | 
			
		||||
        <div className="font-bold text-xs">TOTAL</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -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 (
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">Radarr API Error</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!moviesData || !queuedData) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="flex flex-row w-full">
 | 
			
		||||
        <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
          <div className="font-thin text-sm">-</div>
 | 
			
		||||
          <div className="font-bold text-xs">WANTED</div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
          <div className="font-thin text-sm">-</div>
 | 
			
		||||
          <div className="font-bold text-xs">QUEUED</div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
          <div className="font-thin text-sm">-</div>
 | 
			
		||||
          <div className="font-bold text-xs">MOVIES</div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const wanted = moviesData.filter((movie) => movie.isAvailable === false);
 | 
			
		||||
  const have = moviesData.filter((movie) => movie.isAvailable === true);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-row w-full">
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">{wanted.length}</div>
 | 
			
		||||
        <div className="font-bold text-xs">WANTED</div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">{queuedData.totalCount}</div>
 | 
			
		||||
        <div className="font-bold text-xs">QUEUED</div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">{moviesData.length}</div>
 | 
			
		||||
        <div className="font-bold text-xs">MOVIES</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										56
									
								
								src/components/services/widgets/service/docker.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/components/services/widgets/service/docker.jsx
									
									
									
									
									
										Normal file
									
								
							@ -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 <Widget error="Error Fetching Data" />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (statusData && statusData.status !== "running") {
 | 
			
		||||
    return (
 | 
			
		||||
      <Widget>
 | 
			
		||||
        <Block label="Status" value="Offline" />
 | 
			
		||||
      </Widget>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!statsData || !statusData) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Widget>
 | 
			
		||||
        <Block label="CPU" />
 | 
			
		||||
        <Block label="MEM" />
 | 
			
		||||
        <Block label="RX" />
 | 
			
		||||
        <Block label="TX" />
 | 
			
		||||
      </Widget>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Widget>
 | 
			
		||||
      <Block label="CPU" value={`${calculateCPUPercent(statsData.stats)}%`} />
 | 
			
		||||
      <Block label="MEM" value={formatBytes(statsData.stats.memory_stats.usage, 0)} />
 | 
			
		||||
      <Block label="RX" value={formatBytes(statsData.stats.networks.eth0.rx_bytes, 0)} />
 | 
			
		||||
      <Block label="TX" value={formatBytes(statsData.stats.networks.eth0.tx_bytes, 0)} />
 | 
			
		||||
    </Widget>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										45
									
								
								src/components/services/widgets/service/emby.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/components/services/widgets/service/emby.jsx
									
									
									
									
									
										Normal file
									
								
							@ -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 <Widget error="Emby API Error" />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!sessionsData) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Widget>
 | 
			
		||||
        <Block label="Playing" />
 | 
			
		||||
        <Block label="Transcoding" />
 | 
			
		||||
        <Block label="Bitrate" />
 | 
			
		||||
      </Widget>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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 (
 | 
			
		||||
    <Widget>
 | 
			
		||||
      <Block label="Playing" value={playing.length} />
 | 
			
		||||
      <Block label="Transcoding" value={transcoding.length} />
 | 
			
		||||
      <Block label="Bitrate" value={`${Math.round((bitrate / 1024 / 1024) * 100) / 100} Mbps`} />
 | 
			
		||||
    </Widget>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										64
									
								
								src/components/services/widgets/service/nzbget.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/components/services/widgets/service/nzbget.jsx
									
									
									
									
									
										Normal file
									
								
							@ -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 <Widget error="Nzbget API Error" />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!statusData) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Widget>
 | 
			
		||||
        <Block label="Rate" />
 | 
			
		||||
        <Block label="Remaining" />
 | 
			
		||||
        <Block label="Downloaded" />
 | 
			
		||||
      </Widget>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Widget>
 | 
			
		||||
      <Block label="Rate" value={`${formatBytes(statusData.DownloadRate)}/s`} />
 | 
			
		||||
      <Block label="Remaining" value={`${Math.round((statusData.RemainingSizeMB / 1024) * 100) / 100} GB`} />
 | 
			
		||||
      <Block label="Downloaded" value={`${Math.round((statusData.DownloadedSizeMB / 1024) * 100) / 100} GB`} />
 | 
			
		||||
    </Widget>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										49
									
								
								src/components/services/widgets/service/ombi.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/components/services/widgets/service/ombi.jsx
									
									
									
									
									
										Normal file
									
								
							@ -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 <Widget error="Ombi API Error" />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!statsData) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Widget>
 | 
			
		||||
        <Block label="Pending" />
 | 
			
		||||
        <Block label="Approved" />
 | 
			
		||||
        <Block label="Available" />
 | 
			
		||||
      </Widget>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Widget>
 | 
			
		||||
      <Block label="Pending" value={statsData.pending} />
 | 
			
		||||
      <Block label="Approved" value={statsData.approved} />
 | 
			
		||||
      <Block label="Available" value={statsData.available} />
 | 
			
		||||
    </Widget>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										59
									
								
								src/components/services/widgets/service/portainer.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/components/services/widgets/service/portainer.jsx
									
									
									
									
									
										Normal file
									
								
							@ -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 <Widget error="Portainer API Error" />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!containersData) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Widget>
 | 
			
		||||
        <Block label="Running" />
 | 
			
		||||
        <Block label="Stopped" />
 | 
			
		||||
        <Block label="Total" />
 | 
			
		||||
      </Widget>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (containersData.error) {
 | 
			
		||||
    return <Widget error="Portainer API Error" />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const running = containersData.filter((c) => c.State === "running").length;
 | 
			
		||||
  const stopped = containersData.filter((c) => c.State === "exited").length;
 | 
			
		||||
  const total = containersData.length;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Widget>
 | 
			
		||||
      <Block label="Running" value={running} />
 | 
			
		||||
      <Block label="Stopped" value={stopped} />
 | 
			
		||||
      <Block label="Total" value={total} />
 | 
			
		||||
    </Widget>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								src/components/services/widgets/service/radarr.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/components/services/widgets/service/radarr.jsx
									
									
									
									
									
										Normal file
									
								
							@ -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 <Widget error="Radarr API Error" />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!moviesData || !queuedData) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Widget>
 | 
			
		||||
        <Block label="Wanted" />
 | 
			
		||||
        <Block label="Queued" />
 | 
			
		||||
        <Block label="Movies" />
 | 
			
		||||
      </Widget>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const wanted = moviesData.filter((movie) => movie.isAvailable === false);
 | 
			
		||||
  const have = moviesData.filter((movie) => movie.isAvailable === true);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Widget>
 | 
			
		||||
      <Block label="Wanted" value={wanted.length} />
 | 
			
		||||
      <Block label="Queued" value={queuedData.totalCount} />
 | 
			
		||||
      <Block label="Movies" value={moviesData.length} />
 | 
			
		||||
    </Widget>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										39
									
								
								src/components/services/widgets/service/sonarr.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/components/services/widgets/service/sonarr.jsx
									
									
									
									
									
										Normal file
									
								
							@ -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 <Widget error="Sonar API Error" />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!wantedData || !queuedData || !seriesData) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Widget>
 | 
			
		||||
        <Block label="Wanted" />
 | 
			
		||||
        <Block label="Queued" />
 | 
			
		||||
        <Block label="Series" />
 | 
			
		||||
      </Widget>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Widget>
 | 
			
		||||
      <Block label="Wanted" value={wantedData.totalRecords} />
 | 
			
		||||
      <Block label="Queued" value={queuedData.totalRecords} />
 | 
			
		||||
      <Block label="Series" value={seriesData.length} />
 | 
			
		||||
    </Widget>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -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 (
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">Sonarr API Error</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!wantedData || !queuedData || !seriesData) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">Loading</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-row w-full">
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">{wantedData.totalRecords}</div>
 | 
			
		||||
        <div className="font-bold text-xs">WANTED</div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">{queuedData.totalRecords}</div>
 | 
			
		||||
        <div className="font-bold text-xs">QUEUED</div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">{seriesData.length}</div>
 | 
			
		||||
        <div className="font-bold text-xs">SERIES</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								src/components/services/widgets/widget.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/components/services/widgets/widget.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
			
		||||
export default function Widget({ error = false, children }) {
 | 
			
		||||
  if (error) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
 | 
			
		||||
        <div className="font-thin text-sm">{error}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return <div className="flex flex-row w-full">{children}</div>;
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user