first public source commit

This commit is contained in:
Ben Phelps 2022-08-24 10:44:35 +03:00
parent 1a4fbb9d42
commit 3914fee775
65 changed files with 4697 additions and 312 deletions

View file

@ -0,0 +1,15 @@
import List from "components/services/list";
export default function ServicesGroup({ services }) {
return (
<div
key={services.name}
className="basis-full md:basis-1/2 lg:basis-1/3 xl:basis-1/4 flex-1 p-1"
>
<h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium">
{services.name}
</h2>
<List services={services.services} />
</div>
);
}

View file

@ -0,0 +1,58 @@
import Image from "next/image";
import { useState } from "react";
import { Disclosure, Transition } from "@headlessui/react";
import StatsList from "./stats/list";
import Status from "./status";
import Widget from "./widget";
export default function Item({ service }) {
const [statsOpen, setStatsOpen] = useState(false);
return (
<li key={service.name} className="">
<Disclosure>
<div className="transition-all h-15 overflow-hidden mb-3 cursor-pointer p-1 rounded-md font-medium text-theme-700 hover:text-theme-800 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900 bg-white/50 hover:bg-theme-300/10 dark:bg-white/5 dark:hover:bg-white/10">
<div className="flex">
<div
onClick={() => {
window.open(service.href, "_blank").focus();
}}
className="flex-shrink-0 flex items-center justify-center w-12 "
>
<Image
src={`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${service.icon}`}
width={32}
height={32}
alt="logo"
/>
</div>
<div
onClick={() => {
window.open(service.href, "_blank").focus();
}}
className="flex-1 flex items-center justify-between rounded-r-md "
>
<div className="flex-1 px-2 py-2 text-sm">
{service.name}
<p className="text-theme-500 dark:text-theme-400 text-xs font-extralight">{service.description}</p>
</div>
</div>
{service.container && (
<Disclosure.Button as="div" className="flex-shrink-0 flex items-center justify-center w-12 ">
<Status service={service} />
</Disclosure.Button>
)}
</div>
<Disclosure.Panel>
<div className="w-full">
<StatsList service={service} />
</div>
</Disclosure.Panel>
{service.widget && <Widget service={service} />}
</div>
</Disclosure>
</li>
);
}

View file

@ -0,0 +1,11 @@
import Item from "components/services/item";
export default function List({ services }) {
return (
<ul role="list" className="mt-3 flex flex-col">
{services.map((service) => (
<Item key={service.name} service={service} />
))}
</ul>
);
}

View file

@ -0,0 +1,70 @@
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>
);
}

View file

@ -0,0 +1,8 @@
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>
);
}

View file

@ -0,0 +1,29 @@
import useSWR from "swr";
export default function Status({ service }) {
const { data, error } = useSWR(
`/api/docker/status/${service.container}/${service.server || ""}`
);
if (error) {
return (
<div className="w-3 h-3 bg-rose-300 dark:bg-rose-500 rounded-full" />
);
}
if (data && data.status === "running") {
return (
<div className="w-3 h-3 bg-emerald-300 dark:bg-emerald-500 rounded-full" />
);
}
if (data && data.status === "not found") {
return (
<>
<div className="h-2.5 w-2.5 bg-orange-400/50 dark:bg-yellow-200/40 -rotate-45"></div>
</>
);
}
return <div className="w-3 h-3 bg-black/20 dark:bg-white/40 rounded-full" />;
}

View file

@ -0,0 +1,27 @@
import Sonarr from "./widgets/sonarr";
import Radarr from "./widgets/radarr";
import Ombi from "./widgets/ombi";
import Portainer from "./widgets/portainer";
const widgetMappings = {
sonarr: Sonarr,
radarr: Radarr,
ombi: Ombi,
portainer: Portainer,
};
export default function Widget({ service }) {
const ServiceWidget = widgetMappings[service.widget.type];
if (ServiceWidget) {
return <ServiceWidget service={service} />;
}
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">
Missing Widget Type: <strong>{service.widget.type}</strong>
</div>
</div>
);
}

View file

@ -0,0 +1,71 @@
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>
);
}

View file

@ -0,0 +1,81 @@
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>
);
}

View file

@ -0,0 +1,63 @@
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>
);
}

View file

@ -0,0 +1,53 @@
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>
);
}