starting of widget refactoring

pull/302/head
Ben Phelps 2 years ago
parent d6f6ea9dba
commit 562235f828

@ -2,6 +2,12 @@
"extends": ["airbnb", "next/core-web-vitals", "prettier"], "extends": ["airbnb", "next/core-web-vitals", "prettier"],
"plugins": ["prettier"], "plugins": ["prettier"],
"rules": { "rules": {
"import/no-cycle": [
"error",
{
"maxDepth": 1
}
],
"import/order": [ "import/order": [
"error", "error",
{ {

@ -1,72 +1,11 @@
import dynamic from "next/dynamic";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
const Sonarr = dynamic(() => import("./widgets/service/sonarr")); import components from "widgets/components";
const Radarr = dynamic(() => import("./widgets/service/radarr"));
const Lidarr = dynamic(() => import("./widgets/service/lidarr"));
const Readarr = dynamic(() => import("./widgets/service/readarr"));
const Bazarr = dynamic(() => import("./widgets/service/bazarr"));
const Ombi = dynamic(() => import("./widgets/service/ombi"));
const Portainer = dynamic(() => import("./widgets/service/portainer"));
const Emby = dynamic(() => import("./widgets/service/emby"));
const Nzbget = dynamic(() => import("./widgets/service/nzbget"));
const SABnzbd = dynamic(() => import("./widgets/service/sabnzbd"));
const Transmission = dynamic(() => import("./widgets/service/transmission"));
const QBittorrent = dynamic(() => import("./widgets/service/qbittorrent"));
const Docker = dynamic(() => import("./widgets/service/docker"));
const Pihole = dynamic(() => import("./widgets/service/pihole"));
const Rutorrent = dynamic(() => import("./widgets/service/rutorrent"));
const Jellyfin = dynamic(() => import("./widgets/service/jellyfin"));
const Speedtest = dynamic(() => import("./widgets/service/speedtest"));
const Traefik = dynamic(() => import("./widgets/service/traefik"));
const Jellyseerr = dynamic(() => import("./widgets/service/jellyseerr"));
const Overseerr = dynamic(() => import("./widgets/service/overseerr"));
const Npm = dynamic(() => import("./widgets/service/npm"));
const Tautulli = dynamic(() => import("./widgets/service/tautulli"));
const CoinMarketCap = dynamic(() => import("./widgets/service/coinmarketcap"));
const Gotify = dynamic(() => import("./widgets/service/gotify"));
const Prowlarr = dynamic(() => import("./widgets/service/prowlarr"));
const Jackett = dynamic(() => import("./widgets/service/jackett"));
const AdGuard = dynamic(() => import("./widgets/service/adguard"));
const StRelaySrv = dynamic(() => import("./widgets/service/strelaysrv"));
const Mastodon = dynamic(() => import("./widgets/service/mastodon"));
const widgetMappings = {
docker: Docker,
sonarr: Sonarr,
radarr: Radarr,
lidarr: Lidarr,
readarr: Readarr,
bazarr: Bazarr,
ombi: Ombi,
portainer: Portainer,
emby: Emby,
jellyfin: Jellyfin,
nzbget: Nzbget,
sabnzbd: SABnzbd,
transmission: Transmission,
qbittorrent: QBittorrent,
pihole: Pihole,
rutorrent: Rutorrent,
speedtest: Speedtest,
traefik: Traefik,
jellyseerr: Jellyseerr,
overseerr: Overseerr,
coinmarketcap: CoinMarketCap,
npm: Npm,
tautulli: Tautulli,
gotify: Gotify,
prowlarr: Prowlarr,
jackett: Jackett,
adguard: AdGuard,
strelaysrv: StRelaySrv,
mastodon: Mastodon,
};
export default function Widget({ service }) { export default function Widget({ service }) {
const { t } = useTranslation("common"); const { t } = useTranslation("common");
const ServiceWidget = widgetMappings[service.widget.type]; const ServiceWidget = components[service.widget.type];
if (ServiceWidget) { if (ServiceWidget) {
return <ServiceWidget service={service} />; return <ServiceWidget service={service} />;

@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function AdGuard({ service }) { export default function AdGuard({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: adguardData, error: adguardError } = useSWR(formatApiUrl(config, "stats")); const { data: adguardData, error: adguardError } = useSWR(formatProxyUrl(config, "stats"));
if (adguardError) { if (adguardError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;

@ -4,15 +4,15 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function Bazarr({ service }) { export default function Bazarr({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: episodesData, error: episodesError } = useSWR(formatApiUrl(config, "episodes")); const { data: episodesData, error: episodesError } = useSWR(formatProxyUrl(config, "episodes"));
const { data: moviesData, error: moviesError } = useSWR(formatApiUrl(config, "movies")); const { data: moviesData, error: moviesError } = useSWR(formatProxyUrl(config, "movies"));
if (episodesError || moviesError) { if (episodesError || moviesError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;

@ -7,7 +7,7 @@ import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import Dropdown from "components/services/dropdown"; import Dropdown from "components/services/dropdown";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function CoinMarketCap({ service }) { export default function CoinMarketCap({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
@ -26,7 +26,7 @@ export default function CoinMarketCap({ service }) {
const { symbols } = config; const { symbols } = config;
const { data: statsData, error: statsError } = useSWR( const { data: statsData, error: statsError } = useSWR(
formatApiUrl(config, `v1/cryptocurrency/quotes/latest?symbol=${symbols.join(",")}&convert=${currencyCode}`) formatProxyUrl(config, `v1/cryptocurrency/quotes/latest?symbol=${symbols.join(",")}&convert=${currencyCode}`)
); );
if (!symbols || symbols.length === 0) { if (!symbols || symbols.length === 0) {

@ -5,7 +5,7 @@ import { MdOutlineSmartDisplay } from "react-icons/md";
import Widget from "../widget"; import Widget from "../widget";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
function ticksToTime(ticks) { function ticksToTime(ticks) {
const milliseconds = ticks / 10000; const milliseconds = ticks / 10000;
@ -158,12 +158,12 @@ export default function Emby({ service }) {
data: sessionsData, data: sessionsData,
error: sessionsError, error: sessionsError,
mutate: sessionMutate, mutate: sessionMutate,
} = useSWR(formatApiUrl(config, "Sessions"), { } = useSWR(formatProxyUrl(config, "Sessions"), {
refreshInterval: 5000, refreshInterval: 5000,
}); });
async function handlePlayCommand(session, command) { async function handlePlayCommand(session, command) {
const url = formatApiUrl(config, `Sessions/${session.Id}/Playing/${command}`); const url = formatProxyUrl(config, `Sessions/${session.Id}/Playing/${command}`);
await fetch(url, { await fetch(url, {
method: "POST", method: "POST",
}).then(() => { }).then(() => {

@ -4,16 +4,16 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function Gotify({ service }) { export default function Gotify({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: appsData, error: appsError } = useSWR(formatApiUrl(config, `application`)); const { data: appsData, error: appsError } = useSWR(formatProxyUrl(config, `application`));
const { data: messagesData, error: messagesError } = useSWR(formatApiUrl(config, `message`)); const { data: messagesData, error: messagesError } = useSWR(formatProxyUrl(config, `message`));
const { data: clientsData, error: clientsError } = useSWR(formatApiUrl(config, `client`)); const { data: clientsData, error: clientsError } = useSWR(formatProxyUrl(config, `client`));
if (appsError || messagesError || clientsError) { if (appsError || messagesError || clientsError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;

@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function Jackett({ service }) { export default function Jackett({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: indexersData, error: indexersError } = useSWR(formatApiUrl(config, "indexers")); const { data: indexersData, error: indexersError } = useSWR(formatProxyUrl(config, "indexers"));
if (indexersError) { if (indexersError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;

@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function Jellyseerr({ service }) { export default function Jellyseerr({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `request/count`)); const { data: statsData, error: statsError } = useSWR(formatProxyUrl(config, `request/count`));
if (statsError) { if (statsError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;

@ -4,16 +4,16 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function Lidarr({ service }) { export default function Lidarr({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: albumsData, error: albumsError } = useSWR(formatApiUrl(config, "album")); const { data: albumsData, error: albumsError } = useSWR(formatProxyUrl(config, "album"));
const { data: wantedData, error: wantedError } = useSWR(formatApiUrl(config, "wanted/missing")); const { data: wantedData, error: wantedError } = useSWR(formatProxyUrl(config, "wanted/missing"));
const { data: queueData, error: queueError } = useSWR(formatApiUrl(config, "queue/status")); const { data: queueData, error: queueError } = useSWR(formatProxyUrl(config, "queue/status"));
if (albumsError || wantedError || queueError) { if (albumsError || wantedError || queueError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;

@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function Mastodon({ service }) { export default function Mastodon({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `instance`)); const { data: statsData, error: statsError } = useSWR(formatProxyUrl(config, `instance`));
if (statsError) { if (statsError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;
@ -29,7 +29,7 @@ export default function Mastodon({ service }) {
return ( return (
<Widget> <Widget>
<Block label={t("mastodon.user_count")} value={t("common.number", { value: statsData.stats.user_count })} /> <Block label={t("mastodon.user_count")} value={t("common.number", { value: statsData.stats.user_count })} />
<Block label={t("mastodon.status_count")} value={t("common.number", { value: statsData.stats.status_count })} /> <Block label={t("mastodon.status_count")} value={t("common.number", { value: statsData.stats.status_count })} />
<Block label={t("mastodon.domain_count")} value={t("common.number", { value: statsData.stats.domain_count })} /> <Block label={t("mastodon.domain_count")} value={t("common.number", { value: statsData.stats.domain_count })} />
</Widget> </Widget>

@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function Npm({ service }) { export default function Npm({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: infoData, error: infoError } = useSWR(formatApiUrl(config, "nginx/proxy-hosts")); const { data: infoData, error: infoError } = useSWR(formatProxyUrl(config, "nginx/proxy-hosts"));
if (infoError) { if (infoError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;

@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function Nzbget({ service }) { export default function Nzbget({ service }) {
const { t } = useTranslation("common"); const { t } = useTranslation("common");
const config = service.widget; const config = service.widget;
const { data: statusData, error: statusError } = useSWR(formatApiUrl(config, "status")); const { data: statusData, error: statusError } = useSWR(formatProxyUrl(config, "status"));
if (statusError) { if (statusError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;

@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function Ombi({ service }) { export default function Ombi({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `Request/count`)); const { data: statsData, error: statsError } = useSWR(formatProxyUrl(config, `Request/count`));
if (statsError) { if (statsError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;

@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function Overseerr({ service }) { export default function Overseerr({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `request/count`)); const { data: statsData, error: statsError } = useSWR(formatProxyUrl(config, `request/count`));
if (statsError) { if (statsError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;

@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function Pihole({ service }) { export default function Pihole({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: piholeData, error: piholeError } = useSWR(formatApiUrl(config, "api.php")); const { data: piholeData, error: piholeError } = useSWR(formatProxyUrl(config, "api.php"));
if (piholeError) { if (piholeError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;

@ -4,14 +4,16 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function Portainer({ service }) { export default function Portainer({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: containersData, error: containersError } = useSWR(formatApiUrl(config, `docker/containers/json?all=1`)); const { data: containersData, error: containersError } = useSWR(
formatProxyUrl(config, `docker/containers/json?all=1`)
);
if (containersError) { if (containersError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;

@ -4,16 +4,16 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function Prowlarr({ service }) { export default function Prowlarr({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: indexersData, error: indexersError } = useSWR(formatApiUrl(config, "indexer")); const { data: indexersData, error: indexersError } = useSWR(formatProxyUrl(config, "indexer"));
const { data: grabsData, error: grabsError } = useSWR(formatApiUrl(config, "indexerstats")); const { data: grabsData, error: grabsError } = useSWR(formatProxyUrl(config, "indexerstats"));
if (indexersError || grabsError) { if (indexersError || grabsError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;
} }
@ -32,11 +32,11 @@ export default function Prowlarr({ service }) {
const indexers = indexersData?.filter((indexer) => indexer.enable === true); const indexers = indexersData?.filter((indexer) => indexer.enable === true);
let numberOfGrabs = 0 let numberOfGrabs = 0;
let numberOfQueries = 0 let numberOfQueries = 0;
let numberOfFailedGrabs = 0 let numberOfFailedGrabs = 0;
let numberOfFailedQueries = 0 let numberOfFailedQueries = 0;
grabsData?.indexers?.forEach(element => { grabsData?.indexers?.forEach((element) => {
numberOfGrabs += element.numberOfGrabs; numberOfGrabs += element.numberOfGrabs;
numberOfQueries += element.numberOfQueries; numberOfQueries += element.numberOfQueries;
numberOfFailedGrabs += numberOfFailedGrabs + element.numberOfFailedGrabs; numberOfFailedGrabs += numberOfFailedGrabs + element.numberOfFailedGrabs;

@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function QBittorrent ({ service }) { export default function QBittorrent({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: torrentData, error: torrentError } = useSWR(formatApiUrl(config, "torrents/info")); const { data: torrentData, error: torrentError } = useSWR(formatProxyUrl(config, "torrents/info"));
if (torrentError) { if (torrentError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;

@ -4,14 +4,15 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatProxyUrl } from "utils/api-helpers";
export default function Radarr({ service }) { export default function Radarr({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: moviesData, error: moviesError } = useSWR(formatApiUrl(config, "movie")); const { data: moviesData, error: moviesError } = useSWR(formatProxyUrl(config, "movie"));
const { data: queuedData, error: queuedError } = useSWR(formatApiUrl(config, "queue/status")); const { data: queuedData, error: queuedError } = useSWR(formatProxyUrl(config, "queue/status"));
if (moviesError || queuedError) { if (moviesError || queuedError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;

@ -4,16 +4,16 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function Readarr({ service }) { export default function Readarr({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: booksData, error: booksError } = useSWR(formatApiUrl(config, "book")); const { data: booksData, error: booksError } = useSWR(formatProxyUrl(config, "book"));
const { data: wantedData, error: wantedError } = useSWR(formatApiUrl(config, "wanted/missing")); const { data: wantedData, error: wantedError } = useSWR(formatProxyUrl(config, "wanted/missing"));
const { data: queueData, error: queueError } = useSWR(formatApiUrl(config, "queue/status")); const { data: queueData, error: queueError } = useSWR(formatProxyUrl(config, "queue/status"));
if (booksError || wantedError || queueError) { if (booksError || wantedError || queueError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;

@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function Rutorrent({ service }) { export default function Rutorrent({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: statusData, error: statusError } = useSWR(formatApiUrl(config)); const { data: statusData, error: statusError } = useSWR(formatProxyUrl(config));
if (statusError) { if (statusError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;

@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function SABnzbd({ service }) { export default function SABnzbd({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: queueData, error: queueError } = useSWR(formatApiUrl(config, "queue")); const { data: queueData, error: queueError } = useSWR(formatProxyUrl(config, "queue"));
if (queueError) { if (queueError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;

@ -4,16 +4,16 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function Sonarr({ service }) { export default function Sonarr({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: wantedData, error: wantedError } = useSWR(formatApiUrl(config, "wanted/missing")); const { data: wantedData, error: wantedError } = useSWR(formatProxyUrl(config, "wanted/missing"));
const { data: queuedData, error: queuedError } = useSWR(formatApiUrl(config, "queue")); const { data: queuedData, error: queuedError } = useSWR(formatProxyUrl(config, "queue"));
const { data: seriesData, error: seriesError } = useSWR(formatApiUrl(config, "series")); const { data: seriesData, error: seriesError } = useSWR(formatProxyUrl(config, "series"));
if (wantedError || queuedError || seriesError) { if (wantedError || queuedError || seriesError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;

@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function Speedtest({ service }) { export default function Speedtest({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: speedtestData, error: speedtestError } = useSWR(formatApiUrl(config, "speedtest/latest")); const { data: speedtestData, error: speedtestError } = useSWR(formatProxyUrl(config, "speedtest/latest"));
if (speedtestError) { if (speedtestError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;

@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function StRelaySrv({ service }) { export default function StRelaySrv({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `status`)); const { data: statsData, error: statsError } = useSWR(formatProxyUrl(config, `status`));
if (statsError) { if (statsError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;
@ -29,10 +29,16 @@ export default function StRelaySrv({ service }) {
return ( return (
<Widget> <Widget>
<Block label={t("strelaysrv.numActiveSessions")} value={t("common.number", { value: statsData.numActiveSessions })} /> <Block
<Block label={t("strelaysrv.numConnections")} value={t("common.number", { value: statsData.numConnections })} /> label={t("strelaysrv.numActiveSessions")}
<Block label={t("strelaysrv.dataRelayed")} value={t("common.bytes", { value: statsData.bytesProxied })} /> value={t("common.number", { value: statsData.numActiveSessions })}
<Block label={t("strelaysrv.transferRate")} value={t("common.bitrate",{ value: statsData.kbps10s1m5m15m30m60m[5] })} /> />
<Block label={t("strelaysrv.numConnections")} value={t("common.number", { value: statsData.numConnections })} />
<Block label={t("strelaysrv.dataRelayed")} value={t("common.bytes", { value: statsData.bytesProxied })} />
<Block
label={t("strelaysrv.transferRate")}
value={t("common.bitrate", { value: statsData.kbps10s1m5m15m30m60m[5] })}
/>
</Widget> </Widget>
); );
} }

@ -6,7 +6,7 @@ import { MdOutlineSmartDisplay, MdSmartDisplay } from "react-icons/md";
import Widget from "../widget"; import Widget from "../widget";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
function millisecondsToTime(milliseconds) { function millisecondsToTime(milliseconds) {
const seconds = Math.floor((milliseconds / 1000) % 60); const seconds = Math.floor((milliseconds / 1000) % 60);
@ -120,7 +120,7 @@ export default function Tautulli({ service }) {
const config = service.widget; const config = service.widget;
const { data: activityData, error: activityError } = useSWR(formatApiUrl(config, "get_activity"), { const { data: activityData, error: activityError } = useSWR(formatProxyUrl(config, "get_activity"), {
refreshInterval: 5000, refreshInterval: 5000,
}); });

@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function Traefik({ service }) { export default function Traefik({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: traefikData, error: traefikError } = useSWR(formatApiUrl(config, "overview")); const { data: traefikData, error: traefikError } = useSWR(formatProxyUrl(config, "overview"));
if (traefikError) { if (traefikError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;

@ -4,14 +4,14 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget"; import Widget from "../widget";
import Block from "../block"; import Block from "../block";
import { formatApiUrl } from "utils/api-helpers"; import { formatProxyUrl } from "utils/api-helpers";
export default function Transmission({ service }) { export default function Transmission({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const config = service.widget; const config = service.widget;
const { data: torrentData, error: torrentError } = useSWR(formatApiUrl(config)); const { data: torrentData, error: torrentError } = useSWR(formatProxyUrl(config));
if (torrentError) { if (torrentError) {
return <Widget error={t("widget.api_error")} />; return <Widget error={t("widget.api_error")} />;
@ -37,7 +37,7 @@ export default function Transmission({ service }) {
const torrent = torrents[i]; const torrent = torrents[i];
rateDl += torrent.rateDownload; rateDl += torrent.rateDownload;
rateUl += torrent.rateUpload; rateUl += torrent.rateUpload;
if (torrent.percentDone === 1) { if (torrent.percentDone === 1) {
completed += 1; completed += 1;
} }
} }

@ -1,128 +1,45 @@
import createLogger from "utils/logger"; import createLogger from "utils/logger";
import genericProxyHandler from "utils/proxies/generic"; import genericProxyHandler from "utils/proxies/generic";
import credentialedProxyHandler from "utils/proxies/credentialed"; import widgets from "widgets/widgets";
import rutorrentProxyHandler from "utils/proxies/rutorrent";
import nzbgetProxyHandler from "utils/proxies/nzbget";
import npmProxyHandler from "utils/proxies/npm";
import transmissionProxyHandler from "utils/proxies/transmission";
import qbittorrentProxyHandler from "utils/proxies/qbittorrent";
const logger = createLogger('servicesProxy'); const logger = createLogger("servicesProxy");
function asJson(data) {
if (data?.length > 0) {
const json = JSON.parse(data.toString());
return json;
}
return data;
}
function jsonArrayTransform(data, transform) {
const json = asJson(data);
if (json instanceof Array) {
return transform(json);
}
return json;
}
function jsonArrayFilter(data, filter) {
return jsonArrayTransform(data, (items) => items.filter(filter));
}
const serviceProxyHandlers = {
// uses query param auth
emby: genericProxyHandler,
jellyfin: genericProxyHandler,
pihole: genericProxyHandler,
radarr: {
proxy: genericProxyHandler,
maps: {
movie: (data) => ({
wanted: jsonArrayFilter(data, (item) => item.isAvailable === false).length,
have: jsonArrayFilter(data, (item) => item.isAvailable === true).length,
}),
},
},
sonarr: {
proxy: genericProxyHandler,
maps: {
series: (data) => ({
total: asJson(data).length,
}),
},
},
lidarr: {
proxy: genericProxyHandler,
maps: {
album: (data) => ({
have: jsonArrayFilter(data, (item) => item?.statistics?.percentOfTracks === 100).length,
}),
},
},
readarr: {
proxy: genericProxyHandler,
maps: {
book: (data) => ({
have: jsonArrayFilter(data, (item) => item?.statistics?.bookFileCount > 0).length,
}),
},
},
bazarr: {
proxy: genericProxyHandler,
maps: {
movies: (data) => ({
total: asJson(data).total,
}),
episodes: (data) => ({
total: asJson(data).total,
}),
},
},
speedtest: genericProxyHandler,
tautulli: genericProxyHandler,
traefik: genericProxyHandler,
sabnzbd: genericProxyHandler,
jackett: genericProxyHandler,
adguard: genericProxyHandler,
strelaysrv: genericProxyHandler,
mastodon: genericProxyHandler,
// uses X-API-Key (or similar) header auth
gotify: credentialedProxyHandler,
portainer: credentialedProxyHandler,
jellyseerr: credentialedProxyHandler,
overseerr: credentialedProxyHandler,
ombi: credentialedProxyHandler,
coinmarketcap: credentialedProxyHandler,
prowlarr: credentialedProxyHandler,
// super specific handlers
rutorrent: rutorrentProxyHandler,
nzbget: nzbgetProxyHandler,
npm: npmProxyHandler,
transmission: transmissionProxyHandler,
qbittorrent: qbittorrentProxyHandler,
};
export default async function handler(req, res) { export default async function handler(req, res) {
try { try {
const { type } = req.query; const { type } = req.query;
const widget = widgets[type];
if (!widget) {
logger.debug("Unknown proxy service type: %s", type);
return res.status(403).json({ error: "Unkown proxy service type" });
}
const serviceProxyHandler = widget.proxyHandler || genericProxyHandler;
const serviceProxyHandler = serviceProxyHandlers[type]; if (serviceProxyHandler instanceof Function) {
// map opaque endpoints to their actual endpoint
const mapping = widget?.mappings?.[req.query.endpoint];
const map = mapping?.map;
const endpoint = mapping?.endpoint;
const endpointProxy = mapping?.proxyHandler;
if (serviceProxyHandler) { if (!endpoint) {
if (serviceProxyHandler instanceof Function) { logger.debug("Unsupported service endpoint: %s", type);
return serviceProxyHandler(req, res); return res.status(403).json({ error: "Unsupported service endpoint" });
} }
const { proxy, maps } = serviceProxyHandler; req.query.endpoint = endpoint;
if (proxy) {
return proxy(req, res, maps); if (endpointProxy instanceof Function) {
return endpointProxy(req, res, map);
} }
return serviceProxyHandler(req, res, map);
} }
logger.debug("Unknown proxy service type: %s", type); logger.debug("Unknown proxy service type: %s", type);
return res.status(403).json({ error: "Unkown proxy service type" }); return res.status(403).json({ error: "Unkown proxy service type" });
} } catch (ex) {
catch (ex) {
logger.error(ex); logger.error(ex);
return res.status(500).send({ error: "Unexpected error" }); return res.status(500).send({ error: "Unexpected error" });
} }

@ -1,44 +1,44 @@
const formats = { // const formats = {
emby: `{url}/emby/{endpoint}?api_key={key}`, // emby: `{url}/emby/{endpoint}?api_key={key}`,
jellyfin: `{url}/emby/{endpoint}?api_key={key}`, // jellyfin: `{url}/emby/{endpoint}?api_key={key}`,
pihole: `{url}/admin/{endpoint}`, // pihole: `{url}/admin/{endpoint}`,
radarr: `{url}/api/v3/{endpoint}?apikey={key}`, // radarr: `{url}/api/v3/{endpoint}?apikey={key}`,
sonarr: `{url}/api/v3/{endpoint}?apikey={key}`, // sonarr: `{url}/api/v3/{endpoint}?apikey={key}`,
speedtest: `{url}/api/{endpoint}`, // speedtest: `{url}/api/{endpoint}`,
tautulli: `{url}/api/v2?apikey={key}&cmd={endpoint}`, // tautulli: `{url}/api/v2?apikey={key}&cmd={endpoint}`,
traefik: `{url}/api/{endpoint}`, // traefik: `{url}/api/{endpoint}`,
portainer: `{url}/api/endpoints/{env}/{endpoint}`, // portainer: `{url}/api/endpoints/{env}/{endpoint}`,
rutorrent: `{url}/plugins/httprpc/action.php`, // rutorrent: `{url}/plugins/httprpc/action.php`,
transmission: `{url}/transmission/rpc`, // transmission: `{url}/transmission/rpc`,
qbittorrent: `{url}/api/v2/{endpoint}`, // qbittorrent: `{url}/api/v2/{endpoint}`,
jellyseerr: `{url}/api/v1/{endpoint}`, // jellyseerr: `{url}/api/v1/{endpoint}`,
overseerr: `{url}/api/v1/{endpoint}`, // overseerr: `{url}/api/v1/{endpoint}`,
ombi: `{url}/api/v1/{endpoint}`, // ombi: `{url}/api/v1/{endpoint}`,
npm: `{url}/api/{endpoint}`, // npm: `{url}/api/{endpoint}`,
lidarr: `{url}/api/v1/{endpoint}?apikey={key}`, // lidarr: `{url}/api/v1/{endpoint}?apikey={key}`,
readarr: `{url}/api/v1/{endpoint}?apikey={key}`, // readarr: `{url}/api/v1/{endpoint}?apikey={key}`,
bazarr: `{url}/api/{endpoint}/wanted?apikey={key}`, // bazarr: `{url}/api/{endpoint}/wanted?apikey={key}`,
sabnzbd: `{url}/api/?apikey={key}&output=json&mode={endpoint}`, // sabnzbd: `{url}/api/?apikey={key}&output=json&mode={endpoint}`,
coinmarketcap: `https://pro-api.coinmarketcap.com/{endpoint}`, // coinmarketcap: `https://pro-api.coinmarketcap.com/{endpoint}`,
gotify: `{url}/{endpoint}`, // gotify: `{url}/{endpoint}`,
prowlarr: `{url}/api/v1/{endpoint}`, // prowlarr: `{url}/api/v1/{endpoint}`,
jackett: `{url}/api/v2.0/{endpoint}?apikey={key}&configured=true`, // jackett: `{url}/api/v2.0/{endpoint}?apikey={key}&configured=true`,
adguard: `{url}/control/{endpoint}`, // adguard: `{url}/control/{endpoint}`,
strelaysrv: `{url}/{endpoint}`, // strelaysrv: `{url}/{endpoint}`,
mastodon: `{url}/api/v1/{endpoint}`, // mastodon: `{url}/api/v1/{endpoint}`,
}; // };
export function formatApiCall(api, args) { export function formatApiCall(url, args) {
const find = /\{.*?\}/g; const find = /\{.*?\}/g;
const replace = (match) => { const replace = (match) => {
const key = match.replace(/\{|\}/g, ""); const key = match.replace(/\{|\}/g, "");
return args[key]; return args[key];
}; };
return formats[api].replace(find, replace); return url.replace(find, replace);
} }
export function formatApiUrl(widget, endpoint) { export function formatProxyUrl(widget, endpoint) {
const params = new URLSearchParams({ const params = new URLSearchParams({
type: widget.type, type: widget.type,
group: widget.service_group, group: widget.service_group,
@ -47,3 +47,23 @@ export function formatApiUrl(widget, endpoint) {
}); });
return `/api/services/proxy?${params.toString()}`; return `/api/services/proxy?${params.toString()}`;
} }
export function asJson(data) {
if (data?.length > 0) {
const json = JSON.parse(data.toString());
return json;
}
return data;
}
export function jsonArrayTransform(data, transform) {
const json = asJson(data);
if (json instanceof Array) {
return transform(json);
}
return json;
}
export function jsonArrayFilter(data, filter) {
return jsonArrayTransform(data, (items) => items.filter(filter));
}

@ -1,6 +1,7 @@
import getServiceWidget from "utils/service-helpers"; import getServiceWidget from "utils/service-helpers";
import { formatApiCall } from "utils/api-helpers"; import { formatApiCall } from "utils/api-helpers";
import { httpProxy } from "utils/http"; import { httpProxy } from "utils/http";
import widgets from "widgets/widgets";
export default async function credentialedProxyHandler(req, res) { export default async function credentialedProxyHandler(req, res) {
const { group, service, endpoint } = req.query; const { group, service, endpoint } = req.query;
@ -8,8 +9,12 @@ export default async function credentialedProxyHandler(req, res) {
if (group && service) { if (group && service) {
const widget = await getServiceWidget(group, service); const widget = await getServiceWidget(group, service);
if (!widgets?.[widget.type]?.api) {
return res.status(403).json({ error: "Service does not support API calls" });
}
if (widget) { if (widget) {
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget })); const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",

@ -2,17 +2,22 @@ import getServiceWidget from "utils/service-helpers";
import { formatApiCall } from "utils/api-helpers"; import { formatApiCall } from "utils/api-helpers";
import { httpProxy } from "utils/http"; import { httpProxy } from "utils/http";
import createLogger from "utils/logger"; import createLogger from "utils/logger";
import widgets from "widgets/widgets";
const logger = createLogger('genericProxyHandler'); const logger = createLogger("genericProxyHandler");
export default async function genericProxyHandler(req, res, maps) { export default async function genericProxyHandler(req, res, map) {
const { group, service, endpoint } = req.query; const { group, service, endpoint } = req.query;
if (group && service) { if (group && service) {
const widget = await getServiceWidget(group, service); const widget = await getServiceWidget(group, service);
if (!widgets?.[widget.type]?.api) {
return res.status(403).json({ error: "Service does not support API calls" });
}
if (widget) { if (widget) {
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget })); const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
let headers; let headers;
if (widget.username && widget.password) { if (widget.username && widget.password) {
@ -27,8 +32,8 @@ export default async function genericProxyHandler(req, res, maps) {
}); });
let resultData = data; let resultData = data;
if ((status === 200) && (maps?.[endpoint])) { if (status === 200 && map) {
resultData = maps[endpoint](data); resultData = map(data);
} }
if (contentType) res.setHeader("Content-Type", contentType); if (contentType) res.setHeader("Content-Type", contentType);

@ -1,5 +1,6 @@
import getServiceWidget from "utils/service-helpers"; import getServiceWidget from "utils/service-helpers";
import { formatApiCall } from "utils/api-helpers"; import { formatApiCall } from "utils/api-helpers";
import widgets from "widgets/widgets";
export default async function npmProxyHandler(req, res) { export default async function npmProxyHandler(req, res) {
const { group, service, endpoint } = req.query; const { group, service, endpoint } = req.query;
@ -7,8 +8,12 @@ export default async function npmProxyHandler(req, res) {
if (group && service) { if (group && service) {
const widget = await getServiceWidget(group, service); const widget = await getServiceWidget(group, service);
if (!widgets?.[widget.type]?.api) {
return res.status(403).json({ error: "Service does not support API calls" });
}
if (widget) { if (widget) {
const url = new URL(formatApiCall(widget.type, { endpoint, ...widget })); const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
const loginUrl = `${widget.url}/api/tokens`; const loginUrl = `${widget.url}/api/tokens`;
const body = { identity: widget.username, secret: widget.password }; const body = { identity: widget.username, secret: widget.password };

@ -12,15 +12,15 @@ async function login(widget, params) {
return fetch(loginUrl, { return fetch(loginUrl, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" }, headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: loginBody body: loginBody,
}) })
.then(async response => { .then(async (response) => {
addCookieToJar(loginUrl, response.headers); addCookieToJar(loginUrl, response.headers);
setCookieHeader(loginUrl, params); setCookieHeader(loginUrl, params);
const data = await response.text(); const data = await response.text();
return ([response.status, data]); return [response.status, data];
}) })
.catch(err => ([500, err])); .catch((err) => [500, err]);
} }
export default async function qbittorrentProxyHandler(req, res) { export default async function qbittorrentProxyHandler(req, res) {
@ -46,7 +46,7 @@ export default async function qbittorrentProxyHandler(req, res) {
if (status !== 200) { if (status !== 200) {
return res.status(status).end(data); return res.status(status).end(data);
} }
if (data.toString() !== 'Ok.') { if (data.toString() !== "Ok.") {
return res.status(401).end(data); return res.status(401).end(data);
} }
} }
@ -55,4 +55,4 @@ export default async function qbittorrentProxyHandler(req, res) {
if (contentType) res.setHeader("Content-Type", contentType); if (contentType) res.setHeader("Content-Type", contentType);
return res.status(status).send(data); return res.status(status).send(data);
} }

@ -0,0 +1,8 @@
import dynamic from "next/dynamic";
const components = {
overseerr: dynamic(() => import("./overseerr/component")),
radarr: dynamic(() => import("./radarr/component")),
};
export default components;

@ -0,0 +1,36 @@
import useSWR from "swr";
import { useTranslation } from "next-i18next";
import Widget from "components/services/widgets/widget";
import Block from "components/services/widgets/block";
import { formatProxyUrl } from "utils/api-helpers";
export default function Component({ service }) {
const { t } = useTranslation();
const config = service.widget;
const { data: statsData, error: statsError } = useSWR(formatProxyUrl(config, "request/count"));
if (statsError) {
return <Widget error={t("widget.api_error")} />;
}
if (!statsData) {
return (
<Widget>
<Block label={t("overseerr.pending")} />
<Block label={t("overseerr.approved")} />
<Block label={t("overseerr.available")} />
</Widget>
);
}
return (
<Widget>
<Block label={t("overseerr.pending")} value={statsData.pending} />
<Block label={t("overseerr.approved")} value={statsData.approved} />
<Block label={t("overseerr.available")} value={statsData.available} />
</Widget>
);
}

@ -0,0 +1,14 @@
import credentialedProxyHandler from "utils/proxies/credentialed";
const widget = {
api: "{url}/api/v1/{endpoint}",
proxyHandler: credentialedProxyHandler,
mappings: {
"request/count": {
endpoint: "request/count",
},
},
};
export default widget;

@ -0,0 +1,37 @@
import useSWR from "swr";
import { useTranslation } from "next-i18next";
import Widget from "components/services/widgets/widget";
import Block from "components/services/widgets/block";
import { formatProxyUrl } from "utils/api-helpers";
export default function Component({ service }) {
const { t } = useTranslation();
const config = service.widget;
const { data: moviesData, error: moviesError } = useSWR(formatProxyUrl(config, "movie"));
const { data: queuedData, error: queuedError } = useSWR(formatProxyUrl(config, "queue/status"));
if (moviesError || queuedError) {
return <Widget error={t("widget.api_error")} />;
}
if (!moviesData || !queuedData) {
return (
<Widget>
<Block label={t("radarr.wanted")} />
<Block label={t("radarr.queued")} />
<Block label={t("radarr.movies")} />
</Widget>
);
}
return (
<Widget>
<Block label={t("radarr.wanted")} value={moviesData.wanted} />
<Block label={t("radarr.queued")} value={queuedData.totalCount} />
<Block label={t("radarr.movies")} value={moviesData.have} />
</Widget>
);
}

@ -0,0 +1,22 @@
import genericProxyHandler from "utils/proxies/generic";
import { jsonArrayFilter } from "utils/api-helpers";
const widget = {
api: "{url}/api/v3/{endpoint}?apikey={key}",
proxyHandler: genericProxyHandler,
mappings: {
movie: {
endpoint: "movie",
map: (data) => ({
wanted: jsonArrayFilter(data, (item) => item.isAvailable === false).length,
have: jsonArrayFilter(data, (item) => item.isAvailable === true).length,
}),
},
"queue/status": {
endpoint: "queue/status",
},
},
};
export default widget;

@ -0,0 +1,9 @@
import overseerr from "./overseerr/widget";
import radarr from "./radarr/widget";
const widgets = {
overseerr,
radarr,
};
export default widgets;

@ -4,7 +4,11 @@ const tailwindScrollbars = require("tailwind-scrollbar");
module.exports = { module.exports = {
darkMode: "class", darkMode: "class",
content: ["./src/pages/**/*.{js,ts,jsx,tsx}", "./src/components/**/*.{js,ts,jsx,tsx}"], content: [
"./src/pages/**/*.{js,ts,jsx,tsx}",
"./src/components/**/*.{js,ts,jsx,tsx}",
"./src/widgets/**/*.{js,ts,jsx,tsx}",
],
theme: { theme: {
extend: { extend: {
colors: { colors: {

Loading…
Cancel
Save