Run pre-commit hooks over existing codebase

Co-Authored-By: Ben Phelps <ben@phelps.io>
This commit is contained in:
shamoon 2023-10-17 23:26:55 -07:00
parent fa50bbad9c
commit 19c25713c4
387 changed files with 4785 additions and 4109 deletions

View file

@ -1,6 +1,6 @@
import { useRef } from "react";
import classNames from "classnames";
import { Disclosure, Transition } from '@headlessui/react';
import { Disclosure, Transition } from "@headlessui/react";
import { MdKeyboardArrowDown } from "react-icons/md";
import ErrorBoundary from "components/errorboundry";
@ -15,7 +15,7 @@ export default function BookmarksGroup({ bookmarks, layout, disableCollapse }) {
className={classNames(
"bookmark-group",
layout?.style === "row" ? "basis-full" : "basis-full md:basis-1/4 lg:basis-1/5 xl:basis-1/6",
layout?.header === false ? "flex-1 px-1 -my-1" : "flex-1 p-1"
layout?.header === false ? "flex-1 px-1 -my-1" : "flex-1 p-1",
)}
>
<Disclosure defaultOpen>
@ -28,12 +28,14 @@ export default function BookmarksGroup({ bookmarks, layout, disableCollapse }) {
<ResolvedIcon icon={layout.icon} />
</div>
)}
<h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium bookmark-group-name">{bookmarks.name}</h2>
<h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium bookmark-group-name">
{bookmarks.name}
</h2>
<MdKeyboardArrowDown
className={classNames(
disableCollapse ? "hidden" : "",
"transition-all opacity-0 group-hover:opacity-100 ml-auto text-theme-800 dark:text-theme-300 text-xl",
open ? "" : "rotate-180"
open ? "" : "rotate-180",
)}
/>
</Disclosure.Button>

View file

@ -15,22 +15,24 @@ export default function Item({ bookmark }) {
title={bookmark.name}
target={bookmark.target ?? settings.target ?? "_blank"}
className={classNames(
settings.cardBlur !== undefined && `backdrop-blur${settings.cardBlur.length ? '-' : ""}${settings.cardBlur}`,
"block w-full text-left cursor-pointer transition-all h-15 mb-3 rounded-md font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 hover:bg-theme-300/20 dark:bg-white/5 dark:hover:bg-white/10"
settings.cardBlur !== undefined && `backdrop-blur${settings.cardBlur.length ? "-" : ""}${settings.cardBlur}`,
"block w-full text-left cursor-pointer transition-all h-15 mb-3 rounded-md font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 hover:bg-theme-300/20 dark:bg-white/5 dark:hover:bg-white/10",
)}
>
<div className="flex">
<div className="flex-shrink-0 flex items-center justify-center w-11 bg-theme-500/10 dark:bg-theme-900/50 text-theme-700 hover:text-theme-700 dark:text-theme-200 text-sm font-medium rounded-l-md bookmark-icon">
{bookmark.icon &&
{bookmark.icon && (
<div className="flex-shrink-0 w-5 h-5">
<ResolvedIcon icon={bookmark.icon} alt={bookmark.abbr} />
</div>
}
)}
{!bookmark.icon && bookmark.abbr}
</div>
<div className="flex-1 flex items-center justify-between rounded-r-md bookmark-text">
<div className="flex-1 grow pl-3 py-2 text-xs bookmark-name">{bookmark.name}</div>
<div className="px-2 py-2 truncate text-theme-500 dark:text-theme-300 text-xs bookmark-description">{description}</div>
<div className="px-2 py-2 truncate text-theme-500 dark:text-theme-300 text-xs bookmark-description">
{description}
</div>
</div>
</div>
</a>

View file

@ -9,7 +9,7 @@ export default function List({ bookmarks, layout }) {
<ul
className={classNames(
layout?.style === "row" ? `grid ${columnMap[layout?.columns]} gap-x-2` : "flex flex-col",
"mt-3 bookmark-list"
"mt-3 bookmark-list",
)}
>
{bookmarks.map((bookmark) => (

View file

@ -1,10 +1,10 @@
import useSWR from "swr"
export default function FileContent({ path, loadingValue, errorValue, emptyValue = '' }) {
const fetcher = (url) => fetch(url).then((res) => res.text())
const { data, error, isLoading } = useSWR(`/api/config/${ path }`, fetcher)
if (error) return (errorValue)
if (isLoading) return (loadingValue)
return (data || emptyValue)
}
import useSWR from "swr";
export default function FileContent({ path, loadingValue, errorValue, emptyValue = "" }) {
const fetcher = (url) => fetch(url).then((res) => res.text());
const { data, error, isLoading } = useSWR(`/api/config/${path}`, fetcher);
if (error) return errorValue;
if (isLoading) return loadingValue;
return data || emptyValue;
}

View file

@ -6,10 +6,19 @@ import ResolvedIcon from "./resolvedicon";
import { SettingsContext } from "utils/contexts/settings";
export default function QuickLaunch({servicesAndBookmarks, searchString, setSearchString, isOpen, close, searchProvider}) {
export default function QuickLaunch({
servicesAndBookmarks,
searchString,
setSearchString,
isOpen,
close,
searchProvider,
}) {
const { t } = useTranslation();
const { settings } = useContext(SettingsContext);
const { searchDescriptions, hideVisitURL } = settings?.quicklaunch ? settings.quicklaunch : { searchDescriptions: false, hideVisitURL: false };
const { searchDescriptions, hideVisitURL } = settings?.quicklaunch
? settings.quicklaunch
: { searchDescriptions: false, hideVisitURL: false };
const searchField = useRef();
@ -19,7 +28,7 @@ export default function QuickLaunch({servicesAndBookmarks, searchString, setSear
function openCurrentItem(newWindow) {
const result = results[currentItemIndex];
window.open(result.href, newWindow ? "_blank" : result.target ?? settings.target ?? "_blank", 'noreferrer');
window.open(result.href, newWindow ? "_blank" : result.target ?? settings.target ?? "_blank", "noreferrer");
}
const closeAndReset = useCallback(() => {
@ -35,7 +44,7 @@ export default function QuickLaunch({servicesAndBookmarks, searchString, setSear
try {
if (!/.+[.:].+/g.test(rawSearchString)) throw new Error(); // basic test for probably a url
let urlString = rawSearchString;
if (urlString.indexOf('http') !== 0) urlString = `https://${rawSearchString}`;
if (urlString.indexOf("http") !== 0) urlString = `https://${rawSearchString}`;
setUrl(new URL(urlString)); // basic validation
} catch (e) {
setUrl(null);
@ -83,12 +92,12 @@ export default function QuickLaunch({servicesAndBookmarks, searchString, setSear
useEffect(() => {
if (searchString.length === 0) setResults([]);
else {
let newResults = servicesAndBookmarks.filter(r => {
let newResults = servicesAndBookmarks.filter((r) => {
const nameMatch = r.name.toLowerCase().includes(searchString);
let descriptionMatch;
if (searchDescriptions) {
descriptionMatch = r.description?.toLowerCase().includes(searchString)
r.priority = nameMatch ? 2 * (+nameMatch) : +descriptionMatch; // eslint-disable-line no-param-reassign
descriptionMatch = r.description?.toLowerCase().includes(searchString);
r.priority = nameMatch ? 2 * +nameMatch : +descriptionMatch; // eslint-disable-line no-param-reassign
}
return nameMatch || descriptionMatch;
});
@ -98,23 +107,19 @@ export default function QuickLaunch({servicesAndBookmarks, searchString, setSear
}
if (searchProvider) {
newResults.push(
{
href: searchProvider.url + encodeURIComponent(searchString),
name: `${searchProvider.name ?? t("quicklaunch.custom")} ${t("quicklaunch.search")} `,
type: 'search',
}
)
newResults.push({
href: searchProvider.url + encodeURIComponent(searchString),
name: `${searchProvider.name ?? t("quicklaunch.custom")} ${t("quicklaunch.search")} `,
type: "search",
});
}
if (!hideVisitURL && url) {
newResults.unshift(
{
href: url.toString(),
name: `${t("quicklaunch.visit")} URL`,
type: 'url',
}
)
newResults.unshift({
href: url.toString(),
name: `${t("quicklaunch.visit")} URL`,
type: "url",
});
}
setResults(newResults);
@ -125,7 +130,6 @@ export default function QuickLaunch({servicesAndBookmarks, searchString, setSear
}
}, [searchString, servicesAndBookmarks, searchDescriptions, hideVisitURL, searchProvider, url, t]);
const [hidden, setHidden] = useState(true);
useEffect(() => {
function handleBackdropClick(event) {
@ -134,66 +138,103 @@ export default function QuickLaunch({servicesAndBookmarks, searchString, setSear
if (isOpen) {
searchField.current.focus();
document.body.addEventListener('click', handleBackdropClick);
document.body.addEventListener("click", handleBackdropClick);
setHidden(false);
} else {
document.body.removeEventListener('click', handleBackdropClick);
document.body.removeEventListener("click", handleBackdropClick);
searchField.current.blur();
setTimeout(() => {
setHidden(true);
}, 300); // disable on close
}
}, [isOpen, closeAndReset]);
function highlightText(text) {
const parts = text.split(new RegExp(`(${searchString})`, 'gi'));
// eslint-disable-next-line react/no-array-index-key
return <span>{parts.map((part, i) => part.toLowerCase() === searchString.toLowerCase() ? <span key={`${searchString}_${i}`} className="bg-theme-300/10">{part}</span> : part)}</span>;
const parts = text.split(new RegExp(`(${searchString})`, "gi"));
return (
<span>
{parts.map((part, i) =>
part.toLowerCase() === searchString.toLowerCase() ? (
// eslint-disable-next-line react/no-array-index-key
<span key={`${searchString}_${i}`} className="bg-theme-300/10">
{part}
</span>
) : (
part
),
)}
</span>
);
}
return (
<div className={classNames(
"relative z-40 ease-in-out duration-300 transition-opacity",
hidden && !isOpen && "hidden",
!hidden && isOpen && "opacity-100",
!isOpen && "opacity-0",
)} role="dialog" aria-modal="true">
<div
className={classNames(
"relative z-40 ease-in-out duration-300 transition-opacity",
hidden && !isOpen && "hidden",
!hidden && isOpen && "opacity-100",
!isOpen && "opacity-0",
)}
role="dialog"
aria-modal="true"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-50" />
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full min-w-full items-start justify-center text-center">
<dialog className="mt-[10%] min-w-[80%] max-w-[90%] md:min-w-[40%] rounded-md p-0 block font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-50 dark:bg-theme-800">
<input placeholder="Search" className={classNames(
results.length > 0 && "rounded-t-md",
results.length === 0 && "rounded-md",
"w-full p-4 m-0 border-0 border-b border-slate-700 focus:border-slate-700 focus:outline-0 focus:ring-0 text-sm md:text-xl text-theme-700 dark:text-theme-200 bg-theme-60 dark:bg-theme-800"
)} type="text" autoCorrect="false" ref={searchField} value={searchString} onChange={handleSearchChange} onKeyDown={handleSearchKeyDown} />
{results.length > 0 && <ul className="max-h-[60vh] overflow-y-auto m-2">
{results.map((r, i) => (
<li key={r.container ?? r.app ?? `${r.name}-${r.href}`}>
<button type="button" data-index={i} onMouseEnter={handleItemHover} onClick={handleItemClick} onKeyDown={handleItemKeyDown} className={classNames(
"flex flex-row w-full items-center justify-between rounded-md text-sm md:text-xl py-2 px-4 cursor-pointer text-theme-700 dark:text-theme-200",
i === currentItemIndex && "bg-theme-300/50 dark:bg-theme-700/50",
)}>
<div className="flex flex-row items-center mr-4 pointer-events-none">
{(r.icon || r.abbr) && <div className="w-5 text-xs mr-4">
{r.icon && <ResolvedIcon icon={r.icon} />}
{r.abbr && r.abbr}
</div>}
<div className="flex flex-col md:flex-row text-left items-baseline mr-4 pointer-events-none">
<span className="mr-4">{r.name}</span>
{r.description &&
<span className="text-xs text-theme-600 text-light">
{searchDescriptions && r.priority < 2 ? highlightText(r.description) : r.description}
</span>
}
<input
placeholder="Search"
className={classNames(
results.length > 0 && "rounded-t-md",
results.length === 0 && "rounded-md",
"w-full p-4 m-0 border-0 border-b border-slate-700 focus:border-slate-700 focus:outline-0 focus:ring-0 text-sm md:text-xl text-theme-700 dark:text-theme-200 bg-theme-60 dark:bg-theme-800",
)}
type="text"
autoCorrect="false"
ref={searchField}
value={searchString}
onChange={handleSearchChange}
onKeyDown={handleSearchKeyDown}
/>
{results.length > 0 && (
<ul className="max-h-[60vh] overflow-y-auto m-2">
{results.map((r, i) => (
<li key={r.container ?? r.app ?? `${r.name}-${r.href}`}>
<button
type="button"
data-index={i}
onMouseEnter={handleItemHover}
onClick={handleItemClick}
onKeyDown={handleItemKeyDown}
className={classNames(
"flex flex-row w-full items-center justify-between rounded-md text-sm md:text-xl py-2 px-4 cursor-pointer text-theme-700 dark:text-theme-200",
i === currentItemIndex && "bg-theme-300/50 dark:bg-theme-700/50",
)}
>
<div className="flex flex-row items-center mr-4 pointer-events-none">
{(r.icon || r.abbr) && (
<div className="w-5 text-xs mr-4">
{r.icon && <ResolvedIcon icon={r.icon} />}
{r.abbr && r.abbr}
</div>
)}
<div className="flex flex-col md:flex-row text-left items-baseline mr-4 pointer-events-none">
<span className="mr-4">{r.name}</span>
{r.description && (
<span className="text-xs text-theme-600 text-light">
{searchDescriptions && r.priority < 2 ? highlightText(r.description) : r.description}
</span>
)}
</div>
</div>
</div>
<div className="text-xs text-theme-600 font-bold pointer-events-none">{t(`quicklaunch.${r.type ? r.type.toLowerCase() : 'bookmark'}`)}</div>
</button>
</li>
))}
</ul>}
<div className="text-xs text-theme-600 font-bold pointer-events-none">
{t(`quicklaunch.${r.type ? r.type.toLowerCase() : "bookmark"}`)}
</div>
</button>
</li>
))}
</ul>
)}
</dialog>
</div>
</div>

View file

@ -5,8 +5,8 @@ import { SettingsContext } from "utils/contexts/settings";
import { ThemeContext } from "utils/contexts/theme";
const iconSetURLs = {
'mdi': "https://cdn.jsdelivr.net/npm/@mdi/svg@latest/svg/",
'si' : "https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/",
mdi: "https://cdn.jsdelivr.net/npm/@mdi/svg@latest/svg/",
si: "https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/",
};
export default function ResolvedIcon({ icon, width = 32, height = 32, alt = "logo" }) {
@ -38,12 +38,13 @@ export default function ResolvedIcon({ icon, width = 32, height = 32, alt = "log
if (prefix in iconSetURLs) {
// default to theme setting
let iconName = icon.replace(`${prefix}-`, "").replace(".svg", "");
let iconColor = settings.iconStyle === "theme" ?
`rgb(var(--color-${ theme === "dark" ? 300 : 900 }) / var(--tw-text-opacity, 1))` :
"linear-gradient(180deg, rgb(var(--color-logo-start)), rgb(var(--color-logo-stop)))";
let iconColor =
settings.iconStyle === "theme"
? `rgb(var(--color-${theme === "dark" ? 300 : 900}) / var(--tw-text-opacity, 1))`
: "linear-gradient(180deg, rgb(var(--color-logo-start)), rgb(var(--color-logo-stop)))";
// use custom hex color if provided
const colorMatches = icon.match(/[#][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9]$/i)
const colorMatches = icon.match(/[#][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9]$/i);
if (colorMatches?.length) {
iconName = icon.replace(`${prefix}-`, "").replace(".svg", "").replace(`-${colorMatches[0]}`, "");
iconColor = `${colorMatches[0]}`;
@ -56,8 +57,8 @@ export default function ResolvedIcon({ icon, width = 32, height = 32, alt = "log
style={{
width,
height,
maxWidth: '100%',
maxHeight: '100%',
maxWidth: "100%",
maxHeight: "100%",
background: `${iconColor}`,
mask: `url(${iconSource}) no-repeat center / contain`,
WebkitMask: `url(${iconSource}) no-repeat center / contain`,
@ -65,7 +66,7 @@ export default function ResolvedIcon({ icon, width = 32, height = 32, alt = "log
/>
);
}
// fallback to dashboard-icons
if (icon.endsWith(".svg")) {
const iconName = icon.replace(".svg", "");
@ -79,13 +80,13 @@ export default function ResolvedIcon({ icon, width = 32, height = 32, alt = "log
height,
objectFit: "contain",
maxHeight: "100%",
maxWidth: "100%"
maxWidth: "100%",
}}
alt={alt}
/>
);
}
const iconName = icon.replace(".png", "");
return (
<Image
@ -97,7 +98,7 @@ export default function ResolvedIcon({ icon, width = 32, height = 32, alt = "log
height,
objectFit: "contain",
maxHeight: "100%",
maxWidth: "100%"
maxWidth: "100%",
}}
alt={alt}
/>

View file

@ -33,7 +33,7 @@ export default function Dropdown({ options, value, setValue }) {
type="button"
className={classNames(
value === option.value ? "bg-theme-300/40 dark:bg-theme-900/40" : "",
"w-full block px-3 py-1.5 text-sm hover:bg-theme-300/70 hover:dark:bg-theme-900/70 text-left"
"w-full block px-3 py-1.5 text-sm hover:bg-theme-300/70 hover:dark:bg-theme-900/70 text-left",
)}
>
{option.label}

View file

@ -1,13 +1,12 @@
import { useRef } from "react";
import classNames from "classnames";
import { Disclosure, Transition } from '@headlessui/react';
import { Disclosure, Transition } from "@headlessui/react";
import { MdKeyboardArrowDown } from "react-icons/md";
import List from "components/services/list";
import ResolvedIcon from "components/resolvedicon";
export default function ServicesGroup({ group, services, layout, fiveColumns, disableCollapse }) {
const panel = useRef();
return (
@ -20,45 +19,55 @@ export default function ServicesGroup({ group, services, layout, fiveColumns, di
layout?.header === false ? "flex-1 px-1 -my-1" : "flex-1 p-1",
)}
>
<Disclosure defaultOpen>
<Disclosure defaultOpen>
{({ open }) => (
<>
{ layout?.header !== false &&
<Disclosure.Button disabled={disableCollapse} className="flex w-full select-none items-center group">
{layout?.icon &&
<div className="flex-shrink-0 mr-2 w-7 h-7 service-group-icon">
<ResolvedIcon icon={layout.icon} />
</div>
}
<h2 className="flex text-theme-800 dark:text-theme-300 text-xl font-medium service-group-name">{services.name}</h2>
<MdKeyboardArrowDown className={classNames(
disableCollapse ? 'hidden' : '',
'transition-all opacity-0 group-hover:opacity-100 ml-auto text-theme-800 dark:text-theme-300 text-xl',
open ? '' : 'rotate-180'
)} />
</Disclosure.Button>
}
<Transition
// Otherwise the transition group does display: none and cancels animation
className="!block"
unmount={false}
beforeLeave={() => {
panel.current.style.height = `${panel.current.scrollHeight}px`;
setTimeout(() => {panel.current.style.height = `0`}, 1);
}}
beforeEnter={() => {
panel.current.style.height = `0px`;
setTimeout(() => {panel.current.style.height = `${panel.current.scrollHeight}px`}, 1);
setTimeout(() => {panel.current.style.height = 'auto'}, 150); // animation is 150ms
}}
{layout?.header !== false && (
<Disclosure.Button disabled={disableCollapse} className="flex w-full select-none items-center group">
{layout?.icon && (
<div className="flex-shrink-0 mr-2 w-7 h-7 service-group-icon">
<ResolvedIcon icon={layout.icon} />
</div>
)}
<h2 className="flex text-theme-800 dark:text-theme-300 text-xl font-medium service-group-name">
{services.name}
</h2>
<MdKeyboardArrowDown
className={classNames(
disableCollapse ? "hidden" : "",
"transition-all opacity-0 group-hover:opacity-100 ml-auto text-theme-800 dark:text-theme-300 text-xl",
open ? "" : "rotate-180",
)}
/>
</Disclosure.Button>
)}
<Transition
// Otherwise the transition group does display: none and cancels animation
className="!block"
unmount={false}
beforeLeave={() => {
panel.current.style.height = `${panel.current.scrollHeight}px`;
setTimeout(() => {
panel.current.style.height = `0`;
}, 1);
}}
beforeEnter={() => {
panel.current.style.height = `0px`;
setTimeout(() => {
panel.current.style.height = `${panel.current.scrollHeight}px`;
}, 1);
setTimeout(() => {
panel.current.style.height = "auto";
}, 150); // animation is 150ms
}}
>
<Disclosure.Panel className="transition-all overflow-hidden duration-300 ease-out" ref={panel} static>
<List group={group} services={services.services} layout={layout} />
</Disclosure.Panel>
</Transition>
</Transition>
</>
)}
</Disclosure>
</Disclosure>
</div>
);
}

View file

@ -14,8 +14,8 @@ import ResolvedIcon from "components/resolvedicon";
export default function Item({ service, group }) {
const hasLink = service.href && service.href !== "#";
const { settings } = useContext(SettingsContext);
const showStats = (service.showStats === false) ? false : settings.showStats;
const statusStyle = (service.statusStyle !== undefined) ? service.statusStyle : settings.statusStyle;
const showStats = service.showStats === false ? false : settings.showStats;
const statusStyle = service.statusStyle !== undefined ? service.statusStyle : settings.statusStyle;
const [statsOpen, setStatsOpen] = useState(service.showStats);
const [statsClosing, setStatsClosing] = useState(false);
@ -34,9 +34,9 @@ export default function Item({ service, group }) {
<li key={service.name} id={service.id} className="service" data-name={service.name || ""}>
<div
className={classNames(
settings.cardBlur !== undefined && `backdrop-blur${settings.cardBlur.length ? '-' : ""}${settings.cardBlur}`,
settings.cardBlur !== undefined && `backdrop-blur${settings.cardBlur.length ? "-" : ""}${settings.cardBlur}`,
hasLink && "cursor-pointer",
"transition-all h-15 mb-2 p-1 rounded-md font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 hover:bg-theme-300/20 dark:bg-white/5 dark:hover:bg-white/10 relative overflow-clip service-card"
"transition-all h-15 mb-2 p-1 rounded-md font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 hover:bg-theme-300/20 dark:bg-white/5 dark:hover:bg-white/10 relative overflow-clip service-card",
)}
>
<div className="flex select-none z-0 service-title">
@ -65,46 +65,54 @@ export default function Item({ service, group }) {
>
<div className="flex-1 px-2 py-2 text-sm text-left z-10 service-name">
{service.name}
<p className="text-theme-500 dark:text-theme-300 text-xs font-light service-description">{service.description}</p>
<p className="text-theme-500 dark:text-theme-300 text-xs font-light service-description">
{service.description}
</p>
</div>
</a>
) : (
<div className="flex-1 flex items-center justify-between rounded-r-md service-title-text">
<div className="flex-1 px-2 py-2 text-sm text-left z-10 service-name">
{service.name}
<p className="text-theme-500 dark:text-theme-300 text-xs font-light service-description">{service.description}</p>
<p className="text-theme-500 dark:text-theme-300 text-xs font-light service-description">
{service.description}
</p>
</div>
</div>
)}
<div className={`absolute top-0 right-0 flex flex-row justify-end ${statusStyle === 'dot' ? 'gap-0' : 'gap-2 mr-2'} z-30 service-tags`}>
{service.ping && (
<div className="flex-shrink-0 flex items-center justify-center service-tag service-ping">
<Ping group={group} service={service.name} style={statusStyle} />
<span className="sr-only">Ping status</span>
</div>
)}
<div
className={`absolute top-0 right-0 flex flex-row justify-end ${
statusStyle === "dot" ? "gap-0" : "gap-2 mr-2"
} z-30 service-tags`}
>
{service.ping && (
<div className="flex-shrink-0 flex items-center justify-center service-tag service-ping">
<Ping group={group} service={service.name} style={statusStyle} />
<span className="sr-only">Ping status</span>
</div>
)}
{service.container && (
<button
type="button"
onClick={() => (statsOpen ? closeStats() : setStatsOpen(true))}
className="flex-shrink-0 flex items-center justify-center cursor-pointer service-tag service-container-stats"
>
<Status service={service} style={statusStyle} />
<span className="sr-only">View container stats</span>
</button>
)}
{(service.app && !service.external) && (
<button
type="button"
onClick={() => (statsOpen ? closeStats() : setStatsOpen(true))}
className="flex-shrink-0 flex items-center justify-center cursor-pointer service-tag service-app"
>
<KubernetesStatus service={service} style={statusStyle} />
<span className="sr-only">View container stats</span>
</button>
)}
{service.container && (
<button
type="button"
onClick={() => (statsOpen ? closeStats() : setStatsOpen(true))}
className="flex-shrink-0 flex items-center justify-center cursor-pointer service-tag service-container-stats"
>
<Status service={service} style={statusStyle} />
<span className="sr-only">View container stats</span>
</button>
)}
{service.app && !service.external && (
<button
type="button"
onClick={() => (statsOpen ? closeStats() : setStatsOpen(true))}
className="flex-shrink-0 flex items-center justify-center cursor-pointer service-tag service-app"
>
<KubernetesStatus service={service} style={statusStyle} />
<span className="sr-only">View container stats</span>
</button>
)}
</div>
</div>
@ -112,20 +120,28 @@ export default function Item({ service, group }) {
<div
className={classNames(
showStats || (statsOpen && !statsClosing) ? "max-h-[110px] opacity-100" : " max-h-[0] opacity-0",
"w-full overflow-hidden transition-all duration-300 ease-in-out service-stats"
"w-full overflow-hidden transition-all duration-300 ease-in-out service-stats",
)}
>
{(showStats || statsOpen) && <Docker service={{ widget: { container: service.container, server: service.server } }} />}
{(showStats || statsOpen) && (
<Docker service={{ widget: { container: service.container, server: service.server } }} />
)}
</div>
)}
{service.app && (
<div
className={classNames(
showStats || (statsOpen && !statsClosing) ? "max-h-[55px] opacity-100" : " max-h-[0] opacity-0",
"w-full overflow-hidden transition-all duration-300 ease-in-out service-stats"
"w-full overflow-hidden transition-all duration-300 ease-in-out service-stats",
)}
>
{(showStats || statsOpen) && <Kubernetes service={{ widget: { namespace: service.namespace, app: service.app, podSelector: service.podSelector } }} />}
{(showStats || statsOpen) && (
<Kubernetes
service={{
widget: { namespace: service.namespace, app: service.app, podSelector: service.podSelector },
}}
/>
)}
</div>
)}

View file

@ -20,7 +20,7 @@ export default function KubernetesStatus({ service, style }) {
statusLabel = statusTitle;
colorClass = "text-emerald-500/80";
}
if (data.status === "not found" || data.status === "down" || data.status === "partial") {
statusTitle = data.status;
statusLabel = statusTitle;
@ -28,17 +28,21 @@ export default function KubernetesStatus({ service, style }) {
}
}
if (style === 'dot') {
colorClass = colorClass.replace(/text-/g, 'bg-').replace(/\/\d\d/g, '');
if (style === "dot") {
colorClass = colorClass.replace(/text-/g, "bg-").replace(/\/\d\d/g, "");
backgroundClass = "p-4 hover:bg-theme-500/10 dark:hover:bg-theme-900/20";
}
return (
<div className={`w-auto text-center overflow-hidden ${backgroundClass} rounded-b-[3px] k8s-status`} title={statusTitle}>
{style !== 'dot' ?
<div className={`text-[8px] font-bold ${colorClass} uppercase`}>{statusLabel}</div> :
<div className={`rounded-full h-3 w-3 ${colorClass}`}/>
}
<div
className={`w-auto text-center overflow-hidden ${backgroundClass} rounded-b-[3px] k8s-status`}
title={statusTitle}
>
{style !== "dot" ? (
<div className={`text-[8px] font-bold ${colorClass} uppercase`}>{statusLabel}</div>
) : (
<div className={`rounded-full h-3 w-3 ${colorClass}`} />
)}
</div>
);
}

View file

@ -9,7 +9,7 @@ export default function List({ group, services, layout }) {
<ul
className={classNames(
layout?.style === "row" ? `grid ${columnMap[layout?.columns]} gap-x-2` : "flex flex-col",
"mt-3 services-list"
"mt-3 services-list",
)}
>
{services.map((service) => (

View file

@ -4,7 +4,7 @@ import useSWR from "swr";
export default function Ping({ group, service, style }) {
const { t } = useTranslation();
const { data, error } = useSWR(`api/ping?${new URLSearchParams({ group, service }).toString()}`, {
refreshInterval: 30000
refreshInterval: 30000,
});
let colorClass = "text-black/20 dark:text-white/40 opacity-20";
@ -29,7 +29,7 @@ export default function Ping({ group, service, style }) {
statusText = data.status;
}
} else if (data) {
const ping = t("common.ms", { value: data.latency, style: "unit", unit: "millisecond", maximumFractionDigits: 0 })
const ping = t("common.ms", { value: data.latency, style: "unit", unit: "millisecond", maximumFractionDigits: 0 });
statusTitle += ` ${data.status} (${ping})`;
colorClass = "text-emerald-500/80";
@ -42,14 +42,17 @@ export default function Ping({ group, service, style }) {
}
if (style === "dot") {
backgroundClass = 'p-4';
colorClass = colorClass.replace(/text-/g, 'bg-').replace(/\/\d\d/g, '');
backgroundClass = "p-4";
colorClass = colorClass.replace(/text-/g, "bg-").replace(/\/\d\d/g, "");
}
return (
<div className={`w-auto text-center rounded-b-[3px] overflow-hidden ping-status ${backgroundClass}`} title={statusTitle}>
{style !== 'dot' && <div className={`font-bold uppercase text-[8px] ${colorClass}`}>{statusText}</div>}
{style === 'dot' && <div className={`rounded-full h-3 w-3 ${colorClass}`}/>}
<div
className={`w-auto text-center rounded-b-[3px] overflow-hidden ping-status ${backgroundClass}`}
title={statusTitle}
>
{style !== "dot" && <div className={`font-bold uppercase text-[8px] ${colorClass}`}>{statusText}</div>}
{style === "dot" && <div className={`rounded-full h-3 w-3 ${colorClass}`} />}
</div>
);
}

View file

@ -17,13 +17,13 @@ export default function Status({ service, style }) {
} else if (data) {
if (data.status?.includes("running")) {
if (data.health === "starting") {
statusTitle = t("docker.starting");
colorClass = "text-blue-500/80";
statusTitle = t("docker.starting");
colorClass = "text-blue-500/80";
}
if (data.health === "unhealthy") {
statusTitle = t("docker.unhealthy");
colorClass = "text-orange-400/50 dark:text-orange-400/80";
statusTitle = t("docker.unhealthy");
colorClass = "text-orange-400/50 dark:text-orange-400/80";
}
if (!data.health) {
@ -35,27 +35,31 @@ export default function Status({ service, style }) {
statusTitle = statusLabel;
colorClass = "text-emerald-500/80";
}
if (data.status === "not found" || data.status === "exited" || data.status?.startsWith("partial")) {
if (data.status === "not found") statusLabel = t("docker.not_found")
else if (data.status === "exited") statusLabel = t("docker.exited")
else statusLabel = data.status.replace("partial", t("docker.partial"))
if (data.status === "not found") statusLabel = t("docker.not_found");
else if (data.status === "exited") statusLabel = t("docker.exited");
else statusLabel = data.status.replace("partial", t("docker.partial"));
colorClass = "text-orange-400/50 dark:text-orange-400/80";
}
}
if (style === 'dot') {
colorClass = colorClass.replace(/text-/g, 'bg-').replace(/\/\d\d/g, '');
if (style === "dot") {
colorClass = colorClass.replace(/text-/g, "bg-").replace(/\/\d\d/g, "");
backgroundClass = "p-4 hover:bg-theme-500/10 dark:hover:bg-theme-900/20";
statusTitle = statusLabel;
}
return (
<div className={`w-auto text-center overflow-hidden ${backgroundClass} rounded-b-[3px] docker-status`} title={statusTitle}>
{style !== 'dot' ?
<div className={`text-[8px] font-bold ${colorClass} uppercase`}>{statusLabel}</div> :
<div className={`rounded-full h-3 w-3 ${colorClass}`}/>
}
<div
className={`w-auto text-center overflow-hidden ${backgroundClass} rounded-b-[3px] docker-status`}
title={statusTitle}
>
{style !== "dot" ? (
<div className={`text-[8px] font-bold ${colorClass} uppercase`}>{statusLabel}</div>
) : (
<div className={`rounded-full h-3 w-3 ${colorClass}`} />
)}
</div>
);
}

View file

@ -9,7 +9,7 @@ export default function Block({ value, label }) {
className={classNames(
"bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center text-center p-1",
value === undefined ? "animate-pulse" : "",
"service-block"
"service-block",
)}
>
<div className="font-thin text-sm">{value === undefined || value === null ? "-" : value}</div>

View file

@ -12,14 +12,14 @@ export default function Container({ error = false, children, service }) {
return null;
}
return <Error service={service} error={error} />
return <Error service={service} error={error} />;
}
const childrenArray = Array.isArray(children) ? children : [children];
let visibleChildren = childrenArray;
let fields = service?.widget?.fields;
if (typeof fields === 'string') fields = JSON.parse(service.widget.fields);
if (typeof fields === "string") fields = JSON.parse(service.widget.fields);
const type = service?.widget?.type;
if (fields && type) {
// if the field contains a "." then it most likely contains a common loc value
@ -27,13 +27,15 @@ export default function Container({ error = false, children, service }) {
// fields: [ "resources.cpu", "resources.mem", "field"]
// or even
// fields: [ "resources.cpu", "widget_type.field" ]
visibleChildren = childrenArray?.filter(child => fields.some(field => {
let fullField = field;
if (!field.includes(".")) {
fullField = `${type}.${field}`;
}
return fullField === child?.props?.label;
}));
visibleChildren = childrenArray?.filter((child) =>
fields.some((field) => {
let fullField = field;
if (!field.includes(".")) {
fullField = `${type}.${field}`;
}
return fullField === child?.props?.label;
}),
);
}
return <div className="relative flex flex-row w-full service-container">{visibleChildren}</div>;

View file

@ -6,7 +6,7 @@ function displayError(error) {
}
function displayData(data) {
return (data.type === 'Buffer') ? Buffer.from(data).toString() : JSON.stringify(data, 4);
return data.type === "Buffer" ? Buffer.from(data).toString() : JSON.stringify(data, 4);
}
export default function Error({ error }) {
@ -20,29 +20,34 @@ export default function Error({ error }) {
<details className="px-1 pb-1">
<summary className="block text-center mt-1 mb-0 mx-auto p-3 rounded bg-rose-900/80 hover:bg-rose-900/95 text-theme-900 cursor-pointer">
<div className="flex items-center justify-center text-xs font-bold">
<IoAlertCircle className="mr-1 w-5 h-5"/>{t("widget.api_error")} {error.message && t("widget.information")}
<IoAlertCircle className="mr-1 w-5 h-5" />
{t("widget.api_error")} {error.message && t("widget.information")}
</div>
</summary>
<div className="bg-white dark:bg-theme-200/50 mt-2 rounded text-rose-900 text-xs font-mono whitespace-pre-wrap break-all">
<ul className="p-4">
{error.message && <li>
<span className="text-black">{t("widget.api_error")}:</span> {error.message}
</li>}
{error.url && <li className="mt-2">
<span className="text-black">{t("widget.url")}:</span> {error.url}
</li>}
{error.rawError && <li className="mt-2">
<span className="text-black">{t("widget.raw_error")}:</span>
<div className="ml-2">
{displayError(error.rawError)}
</div>
</li>}
{error.data && <li className="mt-2">
<span className="text-black">{t("widget.response_data")}:</span>
<div className="ml-2">
{displayData(error.data)}
</div>
</li>}
{error.message && (
<li>
<span className="text-black">{t("widget.api_error")}:</span> {error.message}
</li>
)}
{error.url && (
<li className="mt-2">
<span className="text-black">{t("widget.url")}:</span> {error.url}
</li>
)}
{error.rawError && (
<li className="mt-2">
<span className="text-black">{t("widget.raw_error")}:</span>
<div className="ml-2">{displayError(error.rawError)}</div>
</li>
)}
{error.data && (
<li className="mt-2">
<span className="text-black">{t("widget.response_data")}:</span>
<div className="ml-2">{displayData(error.data)}</div>
</li>
)}
</ul>
</div>
</details>

View file

@ -4,28 +4,37 @@ import classNames from "classnames";
import { TabContext } from "utils/contexts/tab";
export function slugify(tabName) {
return tabName !== undefined ? encodeURIComponent(tabName.toString().replace(/\s+/g, '-').toLowerCase()) : ''
return tabName !== undefined ? encodeURIComponent(tabName.toString().replace(/\s+/g, "-").toLowerCase()) : "";
}
export default function Tab({ tab }) {
const { activeTab, setActiveTab } = useContext(TabContext);
return (
<li key={tab} role="presentation"
className={classNames(
"text-theme-700 dark:text-theme-200 relative h-8 w-full rounded-md flex m-1",
)}>
<button id={`${tab}-tab`} type="button" role="tab"
aria-controls={`#${tab}`} aria-selected={activeTab === slugify(tab) ? "true" : "false"}
className={classNames(
"h-full w-full rounded-md",
activeTab === slugify(tab) ? "bg-theme-300/20 dark:bg-white/10" : "hover:bg-theme-100/20 dark:hover:bg-white/5",
)}
onClick={() => {
setActiveTab(slugify(tab));
window.location.hash = `#${slugify(tab)}`;
}}
>{tab}</button>
<li
key={tab}
role="presentation"
className={classNames("text-theme-700 dark:text-theme-200 relative h-8 w-full rounded-md flex m-1")}
>
<button
id={`${tab}-tab`}
type="button"
role="tab"
aria-controls={`#${tab}`}
aria-selected={activeTab === slugify(tab) ? "true" : "false"}
className={classNames(
"h-full w-full rounded-md",
activeTab === slugify(tab)
? "bg-theme-300/20 dark:bg-white/10"
: "hover:bg-theme-100/20 dark:hover:bg-white/5",
)}
onClick={() => {
setActiveTab(slugify(tab));
window.location.hash = `#${slugify(tab)}`;
}}
>
{tab}
</button>
</li>
);
}

View file

@ -65,7 +65,7 @@ export default function ColorToggle() {
title={color}
className={classNames(
active === color ? "border-2" : "border-0",
`rounded-md w-5 h-5 border-black/50 dark:border-white/50 theme-${color} bg-theme-400`
`rounded-md w-5 h-5 border-black/50 dark:border-white/50 theme-${color} bg-theme-400`,
)}
/>
<span className="sr-only">{color}</span>

View file

@ -6,9 +6,11 @@ import { MdNewReleases } from "react-icons/md";
export default function Version() {
const { t, i18n } = useTranslation();
const buildTime = process.env.NEXT_PUBLIC_BUILDTIME?.length ? process.env.NEXT_PUBLIC_BUILDTIME : new Date().toISOString();
const buildTime = process.env.NEXT_PUBLIC_BUILDTIME?.length
? process.env.NEXT_PUBLIC_BUILDTIME
: new Date().toISOString();
const revision = process.env.NEXT_PUBLIC_REVISION?.length ? process.env.NEXT_PUBLIC_REVISION : "dev";
const version = process.env.NEXT_PUBLIC_VERSION?.length ? process.env.NEXT_PUBLIC_VERSION : "dev";
const version = process.env.NEXT_PUBLIC_VERSION?.length ? process.env.NEXT_PUBLIC_VERSION : "dev";
const { data: releaseData } = useSWR("api/releases");
@ -44,7 +46,8 @@ export default function Version() {
</span>
{version === "main" || version === "dev" || version === "nightly"
? null
: releaseData && latestRelease &&
: releaseData &&
latestRelease &&
compareVersions(latestRelease.tag_name, version) > 0 && (
<a
href={latestRelease.html_url}

View file

@ -15,7 +15,7 @@ import { SettingsContext } from "utils/contexts/settings";
const cpuSensorLabels = ["cpu_thermal", "Core", "Tctl"];
function convertToFahrenheit(t) {
return t * 9/5 + 32
return (t * 9) / 5 + 32;
}
export default function Widget({ options }) {
@ -23,35 +23,49 @@ export default function Widget({ options }) {
const { settings } = useContext(SettingsContext);
const { data, error } = useSWR(
`api/widgets/glances?${new URLSearchParams({ lang: i18n.language, ...options }).toString()}`, {
`api/widgets/glances?${new URLSearchParams({ lang: i18n.language, ...options }).toString()}`,
{
refreshInterval: 1500,
}
},
);
if (error || data?.error) {
return <Error options={options} />
return <Error options={options} />;
}
if (!data) {
return <Resources options={options} additionalClassNames="information-widget-glances">
{ options.cpu !== false && <Resource icon={FiCpu} label={t("glances.wait")} percentage="0" /> }
{ options.mem !== false && <Resource icon={FaMemory} label={t("glances.wait")} percentage="0" /> }
{ options.cputemp && <Resource icon={FaThermometerHalf} label={t("glances.wait")} percentage="0" /> }
{ options.disk && !Array.isArray(options.disk) && <Resource key={options.disk} icon={FiHardDrive} label={t("glances.wait")} percentage="0" /> }
{ options.disk && Array.isArray(options.disk) && options.disk.map((disk) => <Resource key={`disk_${disk}`} icon={FiHardDrive} label={t("glances.wait")} percentage="0" /> ) }
{ options.uptime && <Resource icon={FaRegClock} label={t("glances.wait")} percentage="0" /> }
{ options.label && <WidgetLabel label={options.label} /> }
</Resources>;
return (
<Resources options={options} additionalClassNames="information-widget-glances">
{options.cpu !== false && <Resource icon={FiCpu} label={t("glances.wait")} percentage="0" />}
{options.mem !== false && <Resource icon={FaMemory} label={t("glances.wait")} percentage="0" />}
{options.cputemp && <Resource icon={FaThermometerHalf} label={t("glances.wait")} percentage="0" />}
{options.disk && !Array.isArray(options.disk) && (
<Resource key={options.disk} icon={FiHardDrive} label={t("glances.wait")} percentage="0" />
)}
{options.disk &&
Array.isArray(options.disk) &&
options.disk.map((disk) => (
<Resource key={`disk_${disk}`} icon={FiHardDrive} label={t("glances.wait")} percentage="0" />
))}
{options.uptime && <Resource icon={FaRegClock} label={t("glances.wait")} percentage="0" />}
{options.label && <WidgetLabel label={options.label} />}
</Resources>
);
}
const unit = options.units === "imperial" ? "fahrenheit" : "celsius";
let mainTemp = 0;
let maxTemp = 80;
const cpuSensors = data.sensors?.filter(s => cpuSensorLabels.some(label => s.label.startsWith(label)) && s.type === "temperature_core");
const cpuSensors = data.sensors?.filter(
(s) => cpuSensorLabels.some((label) => s.label.startsWith(label)) && s.type === "temperature_core",
);
if (options.cputemp && cpuSensors) {
try {
mainTemp = cpuSensors.reduce((acc, s) => acc + s.value, 0) / cpuSensors.length;
maxTemp = Math.max(cpuSensors.reduce((acc, s) => acc + (s.warning > 0 ? s.warning : 0), 0) / cpuSensors.length, maxTemp);
maxTemp = Math.max(
cpuSensors.reduce((acc, s) => acc + (s.warning > 0 ? s.warning : 0), 0) / cpuSensors.length,
maxTemp,
);
if (unit === "fahrenheit") {
mainTemp = convertToFahrenheit(mainTemp);
maxTemp = convertToFahrenheit(maxTemp);
@ -70,48 +84,53 @@ export default function Widget({ options }) {
: [data.fs.find((d) => d.mnt_point === options.disk)].filter((d) => d);
}
const addedClasses = classNames('information-widget-glances', { 'expanded': options.expanded })
const addedClasses = classNames("information-widget-glances", { expanded: options.expanded });
return (
<Resources options={options} target={settings.target ?? "_blank"} additionalClassNames={addedClasses}>
{options.cpu !== false && <Resource
icon={FiCpu}
value={t("common.number", {
value: data.cpu.total,
style: "unit",
unit: "percent",
maximumFractionDigits: 0,
})}
label={t("glances.cpu")}
expandedValue={t("common.number", {
value: data.load.min15,
style: "unit",
unit: "percent",
maximumFractionDigits: 0
})}
expandedLabel={t("glances.load")}
percentage={data.cpu.total}
expanded={options.expanded}
/>}
{options.mem !== false && <Resource
icon={FaMemory}
value={t("common.bytes", {
value: data.mem.free,
maximumFractionDigits: 1,
binary: true,
})}
label={t("glances.free")}
expandedValue={t("common.bytes", {
value: data.mem.total,
maximumFractionDigits: 1,
binary: true,
})}
expandedLabel={t("glances.total")}
percentage={data.mem.percent}
expanded={options.expanded}
/>}
{options.cpu !== false && (
<Resource
icon={FiCpu}
value={t("common.number", {
value: data.cpu.total,
style: "unit",
unit: "percent",
maximumFractionDigits: 0,
})}
label={t("glances.cpu")}
expandedValue={t("common.number", {
value: data.load.min15,
style: "unit",
unit: "percent",
maximumFractionDigits: 0,
})}
expandedLabel={t("glances.load")}
percentage={data.cpu.total}
expanded={options.expanded}
/>
)}
{options.mem !== false && (
<Resource
icon={FaMemory}
value={t("common.bytes", {
value: data.mem.free,
maximumFractionDigits: 1,
binary: true,
})}
label={t("glances.free")}
expandedValue={t("common.bytes", {
value: data.mem.total,
maximumFractionDigits: 1,
binary: true,
})}
expandedLabel={t("glances.total")}
percentage={data.mem.percent}
expanded={options.expanded}
/>
)}
{disks.map((disk) => (
<Resource key={`disk_${disk.mnt_point ?? disk.device_name}`}
<Resource
key={`disk_${disk.mnt_point ?? disk.device_name}`}
icon={FiHardDrive}
value={t("common.bytes", { value: disk.free })}
label={t("glances.free")}
@ -121,35 +140,35 @@ export default function Widget({ options }) {
expanded={options.expanded}
/>
))}
{options.cputemp && mainTemp > 0 &&
{options.cputemp && mainTemp > 0 && (
<Resource
icon={FaThermometerHalf}
value={t("common.number", {
value: mainTemp,
maximumFractionDigits: 1,
style: "unit",
unit
unit,
})}
label={t("glances.temp")}
expandedValue={t("common.number", {
value: maxTemp,
maximumFractionDigits: 1,
style: "unit",
unit
unit,
})}
expandedLabel={t("glances.warn")}
percentage={tempPercent}
expanded={options.expanded}
/>
}
{options.uptime && data.uptime &&
)}
{options.uptime && data.uptime && (
<Resource
icon={FaRegClock}
value={data.uptime.replace(" days,", t("glances.days")).replace(/:\d\d:\d\d$/g, t("glances.hours"))}
label={t("glances.uptime")}
percentage={Math.round((new Date().getSeconds() / 60) * 100).toString()}
/>
}
)}
{options.label && <WidgetLabel label={options.label} />}
</Resources>
);

View file

@ -14,12 +14,14 @@ const textSizes = {
export default function Greeting({ options }) {
if (options.text) {
return <Container options={options} additionalClassNames="information-widget-greeting">
<Raw>
<span className={`text-theme-800 dark:text-theme-200 mr-3 ${textSizes[options.text_size || "xl"]}`}>
{options.text}
</span>
</Raw>
</Container>;
return (
<Container options={options} additionalClassNames="information-widget-greeting">
<Raw>
<span className={`text-theme-800 dark:text-theme-200 mr-3 ${textSizes[options.text_size || "xl"]}`}>
{options.text}
</span>
</Raw>
</Container>
);
}
}

View file

@ -15,52 +15,47 @@ export default function Widget({ options }) {
cpu: {
load: 0,
total: 0,
percent: 0
percent: 0,
},
memory: {
used: 0,
total: 0,
free: 0,
percent: 0
}
percent: 0,
},
};
const { data, error } = useSWR(
`api/widgets/kubernetes?${new URLSearchParams({ lang: i18n.language }).toString()}`, {
refreshInterval: 1500
}
);
const { data, error } = useSWR(`api/widgets/kubernetes?${new URLSearchParams({ lang: i18n.language }).toString()}`, {
refreshInterval: 1500,
});
if (error || data?.error) {
return <Error options={options} />
return <Error options={options} />;
}
if (!data) {
return <Container options={options} additionalClassNames="information-widget-kubernetes">
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between">
{cluster.show &&
<Node type="cluster" key="cluster" options={options.cluster} data={defaultData} />
}
{nodes.show &&
<Node type="node" key="nodes" options={options.nodes} data={defaultData} />
}
</div>
</Raw>
</Container>;
return (
<Container options={options} additionalClassNames="information-widget-kubernetes">
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between">
{cluster.show && <Node type="cluster" key="cluster" options={options.cluster} data={defaultData} />}
{nodes.show && <Node type="node" key="nodes" options={options.nodes} data={defaultData} />}
</div>
</Raw>
</Container>
);
}
return <Container options={options} additionalClassNames="information-widget-kubernetes">
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between">
{cluster.show &&
<Node key="cluster" type="cluster" options={options.cluster} data={data.cluster} />
}
{nodes.show && data.nodes &&
data.nodes.map((node) =>
<Node key={node.name} type="node" options={options.nodes} data={node} />)
}
</div>
</Raw>
</Container>;
return (
<Container options={options} additionalClassNames="information-widget-kubernetes">
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between">
{cluster.show && <Node key="cluster" type="cluster" options={options.cluster} data={data.cluster} />}
{nodes.show &&
data.nodes &&
data.nodes.map((node) => <Node key={node.name} type="node" options={options.nodes} data={node} />)}
</div>
</Raw>
</Container>
);
}

View file

@ -8,7 +8,6 @@ import UsageBar from "../resources/usage-bar";
export default function Node({ type, options, data }) {
const { t } = useTranslation();
function icon() {
if (type === "cluster") {
return <SiKubernetes className="text-theme-800 dark:text-theme-200 w-5 h-5" />;
@ -31,7 +30,7 @@ export default function Node({ type, options, data }) {
value: data?.cpu?.percent ?? 0,
style: "unit",
unit: "percent",
maximumFractionDigits: 0
maximumFractionDigits: 0,
})}
</div>
<FiCpu className="text-theme-800 dark:text-theme-200 w-3 h-3" />
@ -42,14 +41,16 @@ export default function Node({ type, options, data }) {
{t("common.bytes", {
value: data?.memory?.free ?? 0,
maximumFractionDigits: 0,
binary: true
binary: true,
})}
</div>
<FaMemory className="text-theme-800 dark:text-theme-200 w-3 h-3" />
</div>
<UsageBar percent={data?.memory?.percent} />
{options.showLabel && (
<div className="pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{type === "cluster" ? options.label : data.name}</div>
<div className="pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">
{type === "cluster" ? options.label : data.name}
</div>
)}
</div>
</div>

View file

@ -1,71 +1,75 @@
import Container from "../widget/container";
import Raw from "../widget/raw";
import ResolvedIcon from "components/resolvedicon"
import ResolvedIcon from "components/resolvedicon";
export default function Logo({ options }) {
return (
<Container options={options} additionalClassNames={`information-widget-logo ${ options.icon ? 'resolved' : 'fallback'}`}>
<Container
options={options}
additionalClassNames={`information-widget-logo ${options.icon ? "resolved" : "fallback"}`}
>
<Raw>
{options.icon ?
<div className="resolved mr-3">
<ResolvedIcon icon={options.icon} width={48} height={48} />
</div> :
// fallback to homepage logo
<div className="fallback w-12 h-12">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1024 1024"
style={{
enableBackground: "new 0 0 1024 1024",
}}
xmlSpace="preserve"
className="w-full h-full"
>
<style>
{
".st0{display:none}.st3{stroke-linecap:square}.st3,.st4{fill:none;stroke:#fff;stroke-miterlimit:10}.st6{display:inline;fill:#333}.st7{fill:#fff}"
}
</style>
<g id="Icon">
<path
d="M771.9 191c27.7 0 50.1 26.5 50.1 59.3v186.4l-100.2.3V250.3c0-32.8 22.4-59.3 50.1-59.3z"
style={{
fill: "rgba(var(--color-logo-start))",
}}
/>
<linearGradient
id="homepage_logo_gradient"
gradientUnits="userSpaceOnUse"
x1={200.746}
y1={225.015}
x2={764.986}
y2={789.255}
>
<stop
offset={0}
{options.icon ? (
<div className="resolved mr-3">
<ResolvedIcon icon={options.icon} width={48} height={48} />
</div>
) : (
// fallback to homepage logo
<div className="fallback w-12 h-12">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1024 1024"
style={{
enableBackground: "new 0 0 1024 1024",
}}
xmlSpace="preserve"
className="w-full h-full"
>
<style>
{
".st0{display:none}.st3{stroke-linecap:square}.st3,.st4{fill:none;stroke:#fff;stroke-miterlimit:10}.st6{display:inline;fill:#333}.st7{fill:#fff}"
}
</style>
<g id="Icon">
<path
d="M771.9 191c27.7 0 50.1 26.5 50.1 59.3v186.4l-100.2.3V250.3c0-32.8 22.4-59.3 50.1-59.3z"
style={{
stopColor: "rgba(var(--color-logo-start))",
fill: "rgba(var(--color-logo-start))",
}}
/>
<stop
offset={1}
<linearGradient
id="homepage_logo_gradient"
gradientUnits="userSpaceOnUse"
x1={200.746}
y1={225.015}
x2={764.986}
y2={789.255}
>
<stop
offset={0}
style={{
stopColor: "rgba(var(--color-logo-start))",
}}
/>
<stop
offset={1}
style={{
stopColor: "rgba(var(--color-logo-stop))",
}}
/>
</linearGradient>
<path
d="M721.8 250.3c0-32.7 22.4-59.3 50.1-59.3H253.1c-27.7 0-50.1 26.5-50.1 59.3v582.2l90.2-75.7-.1-130.3H375v61.8l88-73.8 258.8 217.9V250.6"
style={{
stopColor: "rgba(var(--color-logo-stop))",
fill: "url(#homepage_logo_gradient)",
}}
/>
</linearGradient>
<path
d="M721.8 250.3c0-32.7 22.4-59.3 50.1-59.3H253.1c-27.7 0-50.1 26.5-50.1 59.3v582.2l90.2-75.7-.1-130.3H375v61.8l88-73.8 258.8 217.9V250.6"
style={{
fill: "url(#homepage_logo_gradient)",
}}
/>
</g>
</svg>
</div>
}
</g>
</svg>
</div>
)}
</Raw>
</Container>
)
);
}

View file

@ -9,43 +9,47 @@ import Node from "./node";
export default function Longhorn({ options }) {
const { expanded, total, labels, include, nodes } = options;
const { data, error } = useSWR(`api/widgets/longhorn`, {
refreshInterval: 1500
refreshInterval: 1500,
});
if (error || data?.error) {
return <Error options={options} />
return <Error options={options} />;
}
if (!data) {
return <Container options={options} additionalClassNames="infomation-widget-longhorn">
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between" />
</Raw>
</Container>;
return (
<Container options={options} additionalClassNames="infomation-widget-longhorn">
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between" />
</Raw>
</Container>
);
}
return <Container options={options} additionalClassNames="infomation-widget-longhorn">
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between">
{data.nodes
.filter((node) => {
if (node.id === 'total' && total) {
return (
<Container options={options} additionalClassNames="infomation-widget-longhorn">
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between">
{data.nodes
.filter((node) => {
if (node.id === "total" && total) {
return true;
}
if (!nodes) {
return false;
}
if (include && !include.includes(node.id)) {
return false;
}
return true;
}
if (!nodes) {
return false;
}
if (include && !include.includes(node.id)) {
return false;
}
return true;
})
.map((node) =>
<div key={node.id}>
<Node data={{ node }} expanded={expanded} labels={labels} />
</div>
)}
</div>
</Raw>
</Container>;
})
.map((node) => (
<div key={node.id}>
<Node data={{ node }} expanded={expanded} labels={labels} />
</div>
))}
</div>
</Raw>
</Container>
);
}

View file

@ -7,15 +7,18 @@ import WidgetLabel from "../widget/widget_label";
export default function Node({ data, expanded, labels }) {
const { t } = useTranslation();
return <Resource
additionalClassNames="information-widget-longhorn-node"
icon={FaThermometerHalf}
value={t("common.bytes", { value: data.node.available })}
label={t("resources.free")}
expandedValue={t("common.bytes", { value: data.node.maximum })}
expandedLabel={t("resources.total")}
percentage={Math.round(((data.node.maximum - data.node.available) / data.node.maximum) * 100)}
expanded={expanded}
>{ labels && <WidgetLabel label={data.node.id} /> }
</Resource>
return (
<Resource
additionalClassNames="information-widget-longhorn-node"
icon={FaThermometerHalf}
value={t("common.bytes", { value: data.node.available })}
label={t("resources.free")}
expandedValue={t("common.bytes", { value: data.node.maximum })}
expandedLabel={t("resources.total")}
percentage={Math.round(((data.node.maximum - data.node.available) / data.node.maximum) * 100)}
expanded={expanded}
>
{labels && <WidgetLabel label={data.node.id} />}
</Resource>
);
}

View file

@ -15,38 +15,43 @@ import mapIcon from "../../../utils/weather/openmeteo-condition-map";
function Widget({ options }) {
const { t } = useTranslation();
const { data, error } = useSWR(
`api/widgets/openmeteo?${new URLSearchParams({ ...options }).toString()}`
);
const { data, error } = useSWR(`api/widgets/openmeteo?${new URLSearchParams({ ...options }).toString()}`);
if (error || data?.error) {
return <Error options={options} />
return <Error options={options} />;
}
if (!data) {
return <Container options={options} additionalClassNames="information-widget-openmeteo">
<PrimaryText>{t("weather.updating")}</PrimaryText>
<SecondaryText>{t("weather.wait")}</SecondaryText>
<WidgetIcon icon={WiCloudDown} size="l" />
</Container>;
return (
<Container options={options} additionalClassNames="information-widget-openmeteo">
<PrimaryText>{t("weather.updating")}</PrimaryText>
<SecondaryText>{t("weather.wait")}</SecondaryText>
<WidgetIcon icon={WiCloudDown} size="l" />
</Container>
);
}
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
const condition = data.current_weather.weathercode;
const timeOfDay = data.current_weather.time > data.daily.sunrise[0] && data.current_weather.time < data.daily.sunset[0] ? "day" : "night";
const timeOfDay =
data.current_weather.time > data.daily.sunrise[0] && data.current_weather.time < data.daily.sunset[0]
? "day"
: "night";
return <Container options={options} additionalClassNames="information-widget-openmeteo">
<PrimaryText>
{options.label && `${options.label}, `}
{t("common.number", {
value: data.current_weather.temperature,
style: "unit",
unit,
})}
</PrimaryText>
<SecondaryText>{t(`wmo.${data.current_weather.weathercode}-${timeOfDay}`)}</SecondaryText>
<WidgetIcon icon={mapIcon(condition, timeOfDay)} size="xl" />
</Container>;
return (
<Container options={options} additionalClassNames="information-widget-openmeteo">
<PrimaryText>
{options.label && `${options.label}, `}
{t("common.number", {
value: data.current_weather.temperature,
style: "unit",
unit,
})}
</PrimaryText>
<SecondaryText>{t(`wmo.${data.current_weather.weathercode}-${timeOfDay}`)}</SecondaryText>
<WidgetIcon icon={mapIcon(condition, timeOfDay)} size="xl" />
</Container>
);
}
export default function OpenMeteo({ options }) {
@ -73,7 +78,7 @@ export default function OpenMeteo({ options }) {
enableHighAccuracy: true,
maximumAge: 1000 * 60 * 60 * 3,
timeout: 1000 * 30,
}
},
);
}
};
@ -81,11 +86,17 @@ export default function OpenMeteo({ options }) {
// if (!requesting && !location) requestLocation();
if (!location) {
return <ContainerButton options={options} callback={requestLocation} additionalClassNames="information-widget-openmeteo-location-button">
<PrimaryText>{t("weather.current")}</PrimaryText>
<SecondaryText>{t("weather.allow")}</SecondaryText>
<WidgetIcon icon={ requesting ? MdLocationSearching : MdLocationDisabled} size="m" pulse />
</ContainerButton>;
return (
<ContainerButton
options={options}
callback={requestLocation}
additionalClassNames="information-widget-openmeteo-location-button"
>
<PrimaryText>{t("weather.current")}</PrimaryText>
<SecondaryText>{t("weather.allow")}</SecondaryText>
<WidgetIcon icon={requesting ? MdLocationSearching : MdLocationDisabled} size="m" pulse />
</ContainerButton>
);
}
return <Widget options={{ ...location, ...options }} />;

View file

@ -16,19 +16,21 @@ function Widget({ options }) {
const { t, i18n } = useTranslation();
const { data, error } = useSWR(
`api/widgets/openweathermap?${new URLSearchParams({ lang: i18n.language, ...options }).toString()}`
`api/widgets/openweathermap?${new URLSearchParams({ lang: i18n.language, ...options }).toString()}`,
);
if (error || data?.cod === 401 || data?.error) {
return <Error options={options} />
return <Error options={options} />;
}
if (!data) {
return <Container options={options} additionalClassNames="information-widget-openweathermap">
<PrimaryText>{t("weather.updating")}</PrimaryText>
<SecondaryText>{t("weather.wait")}</SecondaryText>
<WidgetIcon icon={WiCloudDown} size="l" />
</Container>;
return (
<Container options={options} additionalClassNames="information-widget-openweathermap">
<PrimaryText>{t("weather.updating")}</PrimaryText>
<SecondaryText>{t("weather.wait")}</SecondaryText>
<WidgetIcon icon={WiCloudDown} size="l" />
</Container>
);
}
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
@ -36,11 +38,16 @@ function Widget({ options }) {
const condition = data.weather[0].id;
const timeOfDay = data.dt > data.sys.sunrise && data.dt < data.sys.sunset ? "day" : "night";
return <Container options={options} additionalClassNames="information-widget-openweathermap">
<PrimaryText>{options.label && `${options.label}, ` }{t("common.number", { value: data.main.temp, style: "unit", unit })}</PrimaryText>
<SecondaryText>{data.weather[0].description}</SecondaryText>
<WidgetIcon icon={mapIcon(condition, timeOfDay)} size="xl" />
</Container>;
return (
<Container options={options} additionalClassNames="information-widget-openweathermap">
<PrimaryText>
{options.label && `${options.label}, `}
{t("common.number", { value: data.main.temp, style: "unit", unit })}
</PrimaryText>
<SecondaryText>{data.weather[0].description}</SecondaryText>
<WidgetIcon icon={mapIcon(condition, timeOfDay)} size="xl" />
</Container>
);
}
export default function OpenWeatherMap({ options }) {
@ -67,17 +74,19 @@ export default function OpenWeatherMap({ options }) {
enableHighAccuracy: true,
maximumAge: 1000 * 60 * 60 * 3,
timeout: 1000 * 30,
}
},
);
}
};
if (!location) {
return <ContainerButton options={options} callback={requestLocation} >
<PrimaryText>{t("weather.current")}</PrimaryText>
<SecondaryText>{t("weather.allow")}</SecondaryText>
<WidgetIcon icon={requesting ? MdLocationSearching : MdLocationDisabled} size="m" pulse />
</ContainerButton>;
return (
<ContainerButton options={options} callback={requestLocation}>
<PrimaryText>{t("weather.current")}</PrimaryText>
<SecondaryText>{t("weather.allow")}</SecondaryText>
<WidgetIcon icon={requesting ? MdLocationSearching : MdLocationDisabled} size="m" pulse />
</ContainerButton>
);
}
return <Widget options={{ ...location, ...options }} />;

View file

@ -1,4 +1,4 @@
export default function QueueEntry({ title, activity, timeLeft, progress}) {
export default function QueueEntry({ title, activity, timeLeft, progress }) {
return (
<div className="text-theme-700 dark:text-theme-200 relative h-5 rounded-md bg-theme-200/50 dark:bg-theme-900/20 m-1 px-1 flex">
<div

View file

@ -13,29 +13,40 @@ export default function Cpu({ expanded, refresh = 1500 }) {
});
if (error || data?.error) {
return <Error />
return <Error />;
}
if (!data) {
return <Resource icon={FiCpu} value="-" label={t("resources.cpu")} expandedValue="-"
expandedLabel={t("resources.load")} percentage="0" expanded={expanded} />
return (
<Resource
icon={FiCpu}
value="-"
label={t("resources.cpu")}
expandedValue="-"
expandedLabel={t("resources.load")}
percentage="0"
expanded={expanded}
/>
);
}
return <Resource
icon={FiCpu}
value={t("common.number", {
value: data.cpu.usage,
style: "unit",
unit: "percent",
maximumFractionDigits: 0,
})}
label={t("resources.cpu")}
expandedValue={t("common.number", {
value: data.cpu.load,
maximumFractionDigits: 2,
})}
expandedLabel={t("resources.load")}
percentage={data.cpu.usage}
expanded={expanded}
/>
return (
<Resource
icon={FiCpu}
value={t("common.number", {
value: data.cpu.usage,
style: "unit",
unit: "percent",
maximumFractionDigits: 0,
})}
label={t("resources.cpu")}
expandedValue={t("common.number", {
value: data.cpu.load,
maximumFractionDigits: 2,
})}
expandedLabel={t("resources.load")}
percentage={data.cpu.usage}
expanded={expanded}
/>
);
}

View file

@ -6,7 +6,7 @@ import Resource from "../widget/resource";
import Error from "../widget/error";
function convertToFahrenheit(t) {
return t * 9/5 + 32
return (t * 9) / 5 + 32;
}
export default function CpuTemp({ expanded, units, refresh = 1500 }) {
@ -17,18 +17,20 @@ export default function CpuTemp({ expanded, units, refresh = 1500 }) {
});
if (error || data?.error) {
return <Error />
return <Error />;
}
if (!data || !data.cputemp) {
return <Resource
icon={FaThermometerHalf}
value="-"
label={t("resources.temp")}
expandedValue="-"
expandedLabel={t("resources.max")}
expanded={expanded}
/>;
return (
<Resource
icon={FaThermometerHalf}
value="-"
label={t("resources.temp")}
expandedValue="-"
expandedLabel={t("resources.max")}
expanded={expanded}
/>
);
}
let mainTemp = data.cputemp.main;
@ -36,26 +38,28 @@ export default function CpuTemp({ expanded, units, refresh = 1500 }) {
mainTemp = data.cputemp.cores.reduce((a, b) => a + b) / data.cputemp.cores.length;
}
const unit = units === "imperial" ? "fahrenheit" : "celsius";
mainTemp = (unit === "celsius") ? mainTemp : convertToFahrenheit(mainTemp);
const maxTemp = (unit === "celsius") ? data.cputemp.max : convertToFahrenheit(data.cputemp.max);
mainTemp = unit === "celsius" ? mainTemp : convertToFahrenheit(mainTemp);
const maxTemp = unit === "celsius" ? data.cputemp.max : convertToFahrenheit(data.cputemp.max);
return <Resource
icon={FaThermometerHalf}
value={t("common.number", {
value: mainTemp,
maximumFractionDigits: 1,
style: "unit",
unit
})}
label={t("resources.temp")}
expandedValue={t("common.number", {
value: maxTemp,
maximumFractionDigits: 1,
style: "unit",
unit
})}
expandedLabel={t("resources.max")}
percentage={Math.round((mainTemp / maxTemp) * 100)}
expanded={expanded}
/>;
return (
<Resource
icon={FaThermometerHalf}
value={t("common.number", {
value: mainTemp,
maximumFractionDigits: 1,
style: "unit",
unit,
})}
label={t("resources.temp")}
expandedValue={t("common.number", {
value: maxTemp,
maximumFractionDigits: 1,
style: "unit",
unit,
})}
expandedLabel={t("resources.max")}
percentage={Math.round((mainTemp / maxTemp) * 100)}
expanded={expanded}
/>
);
}

View file

@ -13,31 +13,35 @@ export default function Disk({ options, expanded, refresh = 1500 }) {
});
if (error || data?.error) {
return <Error options={options} />
return <Error options={options} />;
}
if (!data || !data.drive) {
return <Resource
icon={FiHardDrive}
value="-"
label={t("resources.free")}
expandedValue="-"
expandedLabel={t("resources.total")}
expanded={expanded}
percentage="0"
/>;
return (
<Resource
icon={FiHardDrive}
value="-"
label={t("resources.free")}
expandedValue="-"
expandedLabel={t("resources.total")}
expanded={expanded}
percentage="0"
/>
);
}
// data.drive.used not accurate?
const percent = Math.round(((data.drive.size - data.drive.available) / data.drive.size) * 100);
return <Resource
icon={FiHardDrive}
value={t("common.bytes", { value: data.drive.available })}
label={t("resources.free")}
expandedValue={t("common.bytes", { value: data.drive.size })}
expandedLabel={t("resources.total")}
percentage={percent}
expanded={expanded}
/>;
return (
<Resource
icon={FiHardDrive}
value={t("common.bytes", { value: data.drive.available })}
label={t("resources.free")}
expandedValue={t("common.bytes", { value: data.drive.size })}
expandedLabel={t("resources.total")}
percentage={percent}
expanded={expanded}
/>
);
}

View file

@ -13,30 +13,34 @@ export default function Memory({ expanded, refresh = 1500 }) {
});
if (error || data?.error) {
return <Error />
return <Error />;
}
if (!data) {
return <Resource
icon={FaMemory}
value="-"
label={t("resources.free")}
expandedValue="-"
expandedLabel={t("resources.total")}
expanded={expanded}
percentage="0"
/>;
return (
<Resource
icon={FaMemory}
value="-"
label={t("resources.free")}
expandedValue="-"
expandedLabel={t("resources.total")}
expanded={expanded}
percentage="0"
/>
);
}
const percent = Math.round((data.memory.active / data.memory.total) * 100);
return <Resource
icon={FaMemory}
value={t("common.bytes", { value: data.memory.available, maximumFractionDigits: 1, binary: true })}
label={t("resources.free")}
expandedValue={t("common.bytes", { value: data.memory.total, maximumFractionDigits: 1, binary: true })}
expandedLabel={t("resources.total")}
percentage={percent}
expanded={expanded}
/>;
return (
<Resource
icon={FaMemory}
value={t("common.bytes", { value: data.memory.available, maximumFractionDigits: 1, binary: true })}
label={t("resources.free")}
expandedValue={t("common.bytes", { value: data.memory.total, maximumFractionDigits: 1, binary: true })}
expandedLabel={t("resources.total")}
percentage={percent}
expanded={expanded}
/>
);
}

View file

@ -12,20 +12,22 @@ export default function Resources({ options }) {
let { refresh } = options;
if (!refresh) refresh = 1500;
refresh = Math.max(refresh, 1000);
return <Container options={options}>
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between">
{options.cpu && <Cpu expanded={expanded} refresh={refresh} />}
{options.memory && <Memory expanded={expanded} refresh={refresh} />}
{Array.isArray(options.disk)
? options.disk.map((disk) => <Disk key={disk} options={{ disk }} expanded={expanded} refresh={refresh} />)
: options.disk && <Disk options={options} expanded={expanded} refresh={refresh} />}
{options.cputemp && <CpuTemp expanded={expanded} units={units} refresh={refresh} />}
{options.uptime && <Uptime refresh={refresh} />}
</div>
{options.label && (
<div className="ml-6 pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{options.label}</div>
)}
</Raw>
</Container>;
return (
<Container options={options}>
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between">
{options.cpu && <Cpu expanded={expanded} refresh={refresh} />}
{options.memory && <Memory expanded={expanded} refresh={refresh} />}
{Array.isArray(options.disk)
? options.disk.map((disk) => <Disk key={disk} options={{ disk }} expanded={expanded} refresh={refresh} />)
: options.disk && <Disk options={options} expanded={expanded} refresh={refresh} />}
{options.cputemp && <CpuTemp expanded={expanded} units={units} refresh={refresh} />}
{options.uptime && <Uptime refresh={refresh} />}
</div>
{options.label && (
<div className="ml-6 pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{options.label}</div>
)}
</Raw>
</Container>
);
}

View file

@ -13,7 +13,7 @@ export default function Uptime({ refresh = 1500 }) {
});
if (error || data?.error) {
return <Error />
return <Error />;
}
if (!data) {
@ -21,9 +21,9 @@ export default function Uptime({ refresh = 1500 }) {
}
const mo = Math.floor(data.uptime / (3600 * 24 * 31));
const d = Math.floor(data.uptime % (3600 * 24 * 31) / (3600 * 24));
const h = Math.floor(data.uptime % (3600 * 24) / 3600);
const m = Math.floor(data.uptime % 3600 / 60);
const d = Math.floor((data.uptime % (3600 * 24 * 31)) / (3600 * 24));
const h = Math.floor((data.uptime % (3600 * 24)) / 3600);
const m = Math.floor((data.uptime % 3600) / 60);
let uptime;
if (mo > 0) uptime = `${mo}${t("resources.months")} ${d}${t("resources.days")}`;

View file

@ -1,4 +1,4 @@
export default function UsageBar({ percent, additionalClassNames='' }) {
export default function UsageBar({ percent, additionalClassNames = "" }) {
return (
<div className={`mt-0.5 w-full bg-theme-800/30 rounded-full h-1 dark:bg-theme-200/20 ${additionalClassNames}`}>
<div

View file

@ -54,7 +54,7 @@ function getAvailableProviderIds(options) {
const localStorageKey = "search-name";
export function getStoredProvider() {
if (typeof window !== 'undefined') {
if (typeof window !== "undefined") {
const storedName = localStorage.getItem(localStorageKey);
if (storedName) {
return Object.values(searchProviders).find((el) => el.name === storedName);
@ -69,7 +69,9 @@ export default function Search({ options }) {
const availableProviderIds = getAvailableProviderIds(options);
const [query, setQuery] = useState("");
const [selectedProvider, setSelectedProvider] = useState(searchProviders[availableProviderIds[0] ?? searchProviders.google]);
const [selectedProvider, setSelectedProvider] = useState(
searchProviders[availableProviderIds[0] ?? searchProviders.google],
);
useEffect(() => {
const storedProvider = getStoredProvider();
@ -80,19 +82,22 @@ export default function Search({ options }) {
}
}, [availableProviderIds]);
const submitCallback = useCallback(event => {
const q = encodeURIComponent(query);
const { url } = selectedProvider;
if (url) {
window.open(`${url}${q}`, options.target || "_blank");
} else {
window.open(`${options.url}${q}`, options.target || "_blank");
}
const submitCallback = useCallback(
(event) => {
const q = encodeURIComponent(query);
const { url } = selectedProvider;
if (url) {
window.open(`${url}${q}`, options.target || "_blank");
} else {
window.open(`${options.url}${q}`, options.target || "_blank");
}
event.preventDefault();
event.target.reset();
setQuery("");
}, [options.target, options.url, query, selectedProvider]);
event.preventDefault();
event.target.reset();
setQuery("");
},
[options.target, options.url, query, selectedProvider],
);
if (!availableProviderIds) {
return null;
@ -101,15 +106,16 @@ export default function Search({ options }) {
const onChangeProvider = (provider) => {
setSelectedProvider(provider);
localStorage.setItem(localStorageKey, provider.name);
}
};
return <ContainerForm options={options} callback={submitCallback} additionalClassNames="grow information-widget-search" >
<Raw>
<div className="flex-col relative h-8 my-4 min-w-fit">
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none w-full text-theme-800 dark:text-white" />
<input
type="text"
className="
return (
<ContainerForm options={options} callback={submitCallback} additionalClassNames="grow information-widget-search">
<Raw>
<div className="flex-col relative h-8 my-4 min-w-fit">
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none w-full text-theme-800 dark:text-white" />
<input
type="text"
className="
overflow-hidden w-full h-full rounded-md
text-xs text-theme-900 dark:text-white
placeholder-theme-900 dark:placeholder-white/80
@ -117,65 +123,72 @@ export default function Search({ options }) {
focus:ring-theme-500 dark:focus:ring-white/50
focus:border-theme-500 dark:focus:border-white/50
border border-theme-300 dark:border-theme-200/50"
placeholder={t("search.placeholder")}
onChange={(s) => setQuery(s.currentTarget.value)}
required
autoCapitalize="off"
autoCorrect="off"
autoComplete="off"
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={options.focus}
/>
<Listbox as="div" value={selectedProvider} onChange={onChangeProvider} className="relative text-left" disabled={availableProviderIds?.length === 1}>
<div>
<Listbox.Button
className="
placeholder={t("search.placeholder")}
onChange={(s) => setQuery(s.currentTarget.value)}
required
autoCapitalize="off"
autoCorrect="off"
autoComplete="off"
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={options.focus}
/>
<Listbox
as="div"
value={selectedProvider}
onChange={onChangeProvider}
className="relative text-left"
disabled={availableProviderIds?.length === 1}
>
<div>
<Listbox.Button
className="
absolute right-0.5 bottom-0.5 rounded-r-md px-4 py-2 border-1
text-white font-medium text-sm
bg-theme-600/40 dark:bg-white/10
focus:ring-theme-500 dark:focus:ring-white/50"
>
<selectedProvider.icon className="text-white w-3 h-3" />
<span className="sr-only">{t("search.search")}</span>
</Listbox.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<selectedProvider.icon className="text-white w-3 h-3" />
<span className="sr-only">{t("search.search")}</span>
</Listbox.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Listbox.Options
className="absolute right-0 z-10 mt-1 origin-top-right rounded-md
<Listbox.Options
className="absolute right-0 z-10 mt-1 origin-top-right rounded-md
bg-theme-100 dark:bg-theme-600 shadow-lg
ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<div className="flex flex-col">
{availableProviderIds.map((providerId) => {
const p = searchProviders[providerId];
return (
<Listbox.Option key={providerId} value={p} as={Fragment}>
{({ active }) => (
<li
className={classNames(
"rounded-md cursor-pointer",
active ? "bg-theme-600/10 dark:bg-white/10 dark:text-gray-900" : "dark:text-gray-100"
)}
>
<p.icon className="h-4 w-4 mx-4 my-2" />
</li>
)}
</Listbox.Option>
);
})}
</div>
</Listbox.Options>
</Transition>
</Listbox>
</div>
</Raw>
</ContainerForm>;
>
<div className="flex flex-col">
{availableProviderIds.map((providerId) => {
const p = searchProviders[providerId];
return (
<Listbox.Option key={providerId} value={p} as={Fragment}>
{({ active }) => (
<li
className={classNames(
"rounded-md cursor-pointer",
active ? "bg-theme-600/10 dark:bg-white/10 dark:text-gray-900" : "dark:text-gray-100",
)}
>
<p.icon className="h-4 w-4 mx-4 my-2" />
</li>
)}
</Listbox.Option>
);
})}
</div>
</Listbox.Options>
</Transition>
</Listbox>
</div>
</Raw>
</ContainerForm>
);
}

View file

@ -19,117 +19,151 @@ export default function Widget({ options }) {
const { data: statsData, error: statsError } = useWidgetAPI(options, "stat/sites", { index: options.index });
if (statsError) {
return <Error options={options} />
return <Error options={options} />;
}
const defaultSite = options.site ? statsData?.data.find(s => s.desc === options.site) : statsData?.data?.find(s => s.name === "default");
const defaultSite = options.site
? statsData?.data.find((s) => s.desc === options.site)
: statsData?.data?.find((s) => s.name === "default");
if (!defaultSite) {
return <Container options={options} additionalClassNames="information-widget-unifi-console">
<PrimaryText>{t("unifi.wait")}</PrimaryText>
<WidgetIcon icon={SiUbiquiti} />
</Container>;
return (
<Container options={options} additionalClassNames="information-widget-unifi-console">
<PrimaryText>{t("unifi.wait")}</PrimaryText>
<WidgetIcon icon={SiUbiquiti} />
</Container>
);
}
const wan = defaultSite.health.find(h => h.subsystem === "wan");
const lan = defaultSite.health.find(h => h.subsystem === "lan");
const wlan = defaultSite.health.find(h => h.subsystem === "wlan");
[wan, lan, wlan].forEach(s => {
s.up = s.status === "ok" // eslint-disable-line no-param-reassign
s.show = s.status !== "unknown" // eslint-disable-line no-param-reassign
const wan = defaultSite.health.find((h) => h.subsystem === "wan");
const lan = defaultSite.health.find((h) => h.subsystem === "lan");
const wlan = defaultSite.health.find((h) => h.subsystem === "wlan");
[wan, lan, wlan].forEach((s) => {
s.up = s.status === "ok"; // eslint-disable-line no-param-reassign
s.show = s.status !== "unknown"; // eslint-disable-line no-param-reassign
});
const name = wan.gw_name ?? defaultSite.desc;
const uptime = wan["gw_system-stats"] ? wan["gw_system-stats"].uptime : null;
const dataEmpty = !(wan.show || lan.show || wlan.show || uptime);
return <Container options={options} additionalClassNames="information-widget-unifi-console">
<Raw>
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<div className="flex flex-col">
<div className="flex flex-row ml-3 mb-0.5">
<SiUbiquiti className="text-theme-800 dark:text-theme-200 w-3 h-3 mr-1" />
<div className="text-theme-800 dark:text-theme-200 text-xs font-bold flex flex-row justify-between">
{name}
return (
<Container options={options} additionalClassNames="information-widget-unifi-console">
<Raw>
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<div className="flex flex-col">
<div className="flex flex-row ml-3 mb-0.5">
<SiUbiquiti className="text-theme-800 dark:text-theme-200 w-3 h-3 mr-1" />
<div className="text-theme-800 dark:text-theme-200 text-xs font-bold flex flex-row justify-between">
{name}
</div>
</div>
{dataEmpty && (
<div className="flex flex-row ml-3 text-[8px] justify-between">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-row">
<BiError className="w-4 h-4 text-theme-800 dark:text-theme-200" />
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("unifi.empty_data")}</span>
</div>
</div>
</div>
)}
<div className="flex flex-row ml-3 text-[10px] justify-between">
{uptime && (
<div className="flex flex-row" title={t("unifi.uptime")}>
<div className="pr-0.5 text-theme-800 dark:text-theme-200">
{t("common.number", {
value: uptime / 86400,
maximumFractionDigits: 1,
})}
</div>
<div className="pr-1 text-theme-800 dark:text-theme-200">{t("unifi.days")}</div>
</div>
)}
{wan.show && (
<div className="flex flex-row">
<div className="pr-1 text-theme-800 dark:text-theme-200">{t("unifi.wan")}</div>
{wan.up ? (
<BiCheckCircle className="text-theme-800 dark:text-theme-200 h-4 w-3" />
) : (
<BiXCircle className="text-theme-800 dark:text-theme-200 h-4 w-3" />
)}
</div>
)}
{!wan.show && !lan.show && wlan.show && (
<div className="flex flex-row">
<div className="pr-1 text-theme-800 dark:text-theme-200">{t("unifi.wlan")}</div>
{wlan.up ? (
<BiCheckCircle className="text-theme-800 dark:text-theme-200 h-4 w-3" />
) : (
<BiXCircle className="text-theme-800 dark:text-theme-200 h-4 w-3" />
)}
</div>
)}
{!wan.show && !wlan.show && lan.show && (
<div className="flex flex-row">
<div className="pr-1 text-theme-800 dark:text-theme-200">{t("unifi.lan")}</div>
{lan.up ? (
<BiCheckCircle className="text-theme-800 dark:text-theme-200 h-4 w-3" />
) : (
<BiXCircle className="text-theme-800 dark:text-theme-200 h-4 w-3" />
)}
</div>
)}
</div>
</div>
<div className="flex flex-col">
{wlan.show && (
<div className="flex flex-row ml-3 py-0.5">
<BiWifi className="text-theme-800 dark:text-theme-200 w-4 h-4 mr-1" />
<div
className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between"
title={t("unifi.users")}
>
<div className="pr-0.5">
{t("common.number", {
value: wlan.num_user,
maximumFractionDigits: 0,
})}
</div>
</div>
</div>
)}
{lan.show && (
<div className="flex flex-row ml-3 pb-0.5">
<MdSettingsEthernet className="text-theme-800 dark:text-theme-200 w-4 h-4 mr-1" />
<div
className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between"
title={t("unifi.users")}
>
<div className="pr-0.5">
{t("common.number", {
value: lan.num_user,
maximumFractionDigits: 0,
})}
</div>
</div>
</div>
)}
{((wlan.show && !lan.show) || (!wlan.show && lan.show)) && (
<div className="flex flex-row ml-3 py-0.5">
<BiNetworkChart className="text-theme-800 dark:text-theme-200 w-4 h-4 mr-1" />
<div
className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between"
title={t("unifi.devices")}
>
<div className="pr-0.5">
{t("common.number", {
value: wlan.show ? wlan.num_adopted : lan.num_adopted,
maximumFractionDigits: 0,
})}
</div>
</div>
</div>
)}
</div>
</div>
{dataEmpty && <div className="flex flex-row ml-3 text-[8px] justify-between">
<div className="flex flex-row items-center justify-end">
<div className="flex flex-row">
<BiError className="w-4 h-4 text-theme-800 dark:text-theme-200" />
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("unifi.empty_data")}</span>
</div>
</div>
</div>}
<div className="flex flex-row ml-3 text-[10px] justify-between">
{uptime && <div className="flex flex-row" title={t("unifi.uptime")}>
<div className="pr-0.5 text-theme-800 dark:text-theme-200">
{t("common.number", {
value: uptime / 86400,
maximumFractionDigits: 1,
})}
</div>
<div className="pr-1 text-theme-800 dark:text-theme-200">{t("unifi.days")}</div>
</div>}
{wan.show && <div className="flex flex-row">
<div className="pr-1 text-theme-800 dark:text-theme-200">{t("unifi.wan")}</div>
{wan.up
? <BiCheckCircle className="text-theme-800 dark:text-theme-200 h-4 w-3" />
: <BiXCircle className="text-theme-800 dark:text-theme-200 h-4 w-3" />
}
</div>}
{!wan.show && !lan.show && wlan.show && <div className="flex flex-row">
<div className="pr-1 text-theme-800 dark:text-theme-200">{t("unifi.wlan")}</div>
{wlan.up
? <BiCheckCircle className="text-theme-800 dark:text-theme-200 h-4 w-3" />
: <BiXCircle className="text-theme-800 dark:text-theme-200 h-4 w-3" />
}
</div>}
{!wan.show && !wlan.show && lan.show && <div className="flex flex-row">
<div className="pr-1 text-theme-800 dark:text-theme-200">{t("unifi.lan")}</div>
{lan.up
? <BiCheckCircle className="text-theme-800 dark:text-theme-200 h-4 w-3" />
: <BiXCircle className="text-theme-800 dark:text-theme-200 h-4 w-3" />
}
</div>}
</div>
</div>
<div className="flex flex-col">
{wlan.show && <div className="flex flex-row ml-3 py-0.5">
<BiWifi className="text-theme-800 dark:text-theme-200 w-4 h-4 mr-1" />
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between" title={t("unifi.users")}>
<div className="pr-0.5">
{t("common.number", {
value: wlan.num_user,
maximumFractionDigits: 0,
})}
</div>
</div>
</div>}
{lan.show && <div className="flex flex-row ml-3 pb-0.5">
<MdSettingsEthernet className="text-theme-800 dark:text-theme-200 w-4 h-4 mr-1" />
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between" title={t("unifi.users")}>
<div className="pr-0.5">
{t("common.number", {
value: lan.num_user,
maximumFractionDigits: 0,
})}
</div>
</div>
</div>}
{(wlan.show && !lan.show || !wlan.show && lan.show) && <div className="flex flex-row ml-3 py-0.5">
<BiNetworkChart className="text-theme-800 dark:text-theme-200 w-4 h-4 mr-1" />
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between" title={t("unifi.devices")}>
<div className="pr-0.5">
{t("common.number", {
value: wlan.show ? wlan.num_adopted : lan.num_adopted,
maximumFractionDigits: 0,
})}
</div>
</div>
</div>}
</div>
</div>
</Raw>
</Container>
</Raw>
</Container>
);
}

View file

@ -16,37 +16,41 @@ function Widget({ options }) {
const { t, i18n } = useTranslation();
const { data, error } = useSWR(
`api/widgets/weather?${new URLSearchParams({ lang: i18n.language, ...options }).toString()}`
`api/widgets/weather?${new URLSearchParams({ lang: i18n.language, ...options }).toString()}`,
);
if (error || data?.error) {
return <Error options={options} />
return <Error options={options} />;
}
if (!data) {
return <Container options={options} additionalClassNames="information-widget-weather">
<PrimaryText>{t("weather.updating")}</PrimaryText>
<SecondaryText>{t("weather.wait")}</SecondaryText>
<WidgetIcon icon={WiCloudDown} size="l" />
</Container>;
return (
<Container options={options} additionalClassNames="information-widget-weather">
<PrimaryText>{t("weather.updating")}</PrimaryText>
<SecondaryText>{t("weather.wait")}</SecondaryText>
<WidgetIcon icon={WiCloudDown} size="l" />
</Container>
);
}
const unit = options.units === "metric" ? "celsius" : "fahrenheit";
const condition = data.current.condition.code;
const timeOfDay = data.current.is_day ? "day" : "night";
return <Container options={options} additionalClassNames="information-widget-weather">
<PrimaryText>
{options.label && `${options.label}, `}
{t("common.number", {
value: options.units === "metric" ? data.current.temp_c : data.current.temp_f,
style: "unit",
unit,
})}
</PrimaryText>
<SecondaryText>{data.current.condition.text}</SecondaryText>
<WidgetIcon icon={mapIcon(condition, timeOfDay)} size="xl" />
</Container>;
return (
<Container options={options} additionalClassNames="information-widget-weather">
<PrimaryText>
{options.label && `${options.label}, `}
{t("common.number", {
value: options.units === "metric" ? data.current.temp_c : data.current.temp_f,
style: "unit",
unit,
})}
</PrimaryText>
<SecondaryText>{data.current.condition.text}</SecondaryText>
<WidgetIcon icon={mapIcon(condition, timeOfDay)} size="xl" />
</Container>
);
}
export default function WeatherApi({ options }) {
@ -73,17 +77,19 @@ export default function WeatherApi({ options }) {
enableHighAccuracy: true,
maximumAge: 1000 * 60 * 60 * 3,
timeout: 1000 * 30,
}
},
);
}
};
if (!location) {
return <ContainerButton options={options} callback={requestLocation} >
<PrimaryText>{t("weather.current")}</PrimaryText>
<SecondaryText>{t("weather.allow")}</SecondaryText>
<WidgetIcon icon={requesting ? MdLocationSearching : MdLocationDisabled} size="m" pulse />
</ContainerButton>;
return (
<ContainerButton options={options} callback={requestLocation}>
<PrimaryText>{t("weather.current")}</PrimaryText>
<SecondaryText>{t("weather.allow")}</SecondaryText>
<WidgetIcon icon={requesting ? MdLocationSearching : MdLocationDisabled} size="m" pulse />
</ContainerButton>
);
}
return <Widget options={{ ...location, ...options }} />;

View file

@ -5,20 +5,20 @@ import PrimaryText from "./primary_text";
import SecondaryText from "./secondary_text";
import Raw from "./raw";
export function getAllClasses(options, additionalClassNames = '') {
export function getAllClasses(options, additionalClassNames = "") {
if (options?.style?.header === "boxedWidgets") {
if (options?.style?.cardBlur !== undefined) {
// eslint-disable-next-line no-param-reassign
additionalClassNames = [
additionalClassNames,
`backdrop-blur${options.style.cardBlur.length ? '-' : ""}${options.style.cardBlur}`
].join(' ')
`backdrop-blur${options.style.cardBlur.length ? "-" : ""}${options.style.cardBlur}`,
].join(" ");
}
return classNames(
"flex flex-col justify-center ml-2 mr-2",
"mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-2 pl-3 pr-3",
additionalClassNames
additionalClassNames,
);
}
@ -27,34 +27,37 @@ export function getAllClasses(options, additionalClassNames = '') {
widgetAlignedClasses = "flex flex-col justify-center first:ml-auto ml-2 mr-2 ";
}
return classNames(
widgetAlignedClasses,
additionalClassNames
);
return classNames(widgetAlignedClasses, additionalClassNames);
}
export function getInnerBlock(children) {
// children won't be an array if it's Raw component
return Array.isArray(children) && <div className="flex flex-row items-center justify-end widget-inner">
<div className="flex flex-col items-center widget-inner-icon">{children.find(child => child.type === WidgetIcon)}</div>
<div className="flex flex-col ml-3 text-left widget-inner-text">
{children.find(child => child.type === PrimaryText)}
{children.find(child => child.type === SecondaryText)}
</div>
</div>;
return (
Array.isArray(children) && (
<div className="flex flex-row items-center justify-end widget-inner">
<div className="flex flex-col items-center widget-inner-icon">
{children.find((child) => child.type === WidgetIcon)}
</div>
<div className="flex flex-col ml-3 text-left widget-inner-text">
{children.find((child) => child.type === PrimaryText)}
{children.find((child) => child.type === SecondaryText)}
</div>
</div>
)
);
}
export function getBottomBlock(children) {
if (children.type !== Raw) {
return children.find(child => child.type === Raw) || [];
return children.find((child) => child.type === Raw) || [];
}
return [children];
}
export default function Container({ children = [], options, additionalClassNames = '' }) {
export default function Container({ children = [], options, additionalClassNames = "" }) {
return (
<div className={getAllClasses(options, `${ additionalClassNames } widget-container`)}>
<div className={getAllClasses(options, `${additionalClassNames} widget-container`)}>
{getInnerBlock(children)}
{getBottomBlock(children)}
</div>

View file

@ -1,8 +1,12 @@
import { getAllClasses, getInnerBlock, getBottomBlock } from "./container";
export default function ContainerButton ({ children = [], options, additionalClassNames = '', callback }) {
export default function ContainerButton({ children = [], options, additionalClassNames = "", callback }) {
return (
<button type="button" onClick={callback} className={`${ getAllClasses(options, additionalClassNames) } information-widget-container-button`}>
<button
type="button"
onClick={callback}
className={`${getAllClasses(options, additionalClassNames)} information-widget-container-button`}
>
{getInnerBlock(children)}
{getBottomBlock(children)}
</button>

View file

@ -1,8 +1,12 @@
import { getAllClasses, getInnerBlock, getBottomBlock } from "./container";
export default function ContainerForm ({ children = [], options, additionalClassNames = '', callback }) {
export default function ContainerForm({ children = [], options, additionalClassNames = "", callback }) {
return (
<form type="button" onSubmit={callback} className={`${ getAllClasses(options, additionalClassNames) } information-widget-form`}>
<form
type="button"
onSubmit={callback}
className={`${getAllClasses(options, additionalClassNames)} information-widget-form`}
>
{getInnerBlock(children)}
{getBottomBlock(children)}
</form>

View file

@ -1,8 +1,12 @@
import { getAllClasses, getInnerBlock, getBottomBlock } from "./container";
export default function ContainerLink ({ children = [], options, additionalClassNames = '', target }) {
export default function ContainerLink({ children = [], options, additionalClassNames = "", target }) {
return (
<a href={options.url} target={target} className={`${ getAllClasses(options, additionalClassNames) } information-widget-link`}>
<a
href={options.url}
target={target}
className={`${getAllClasses(options, additionalClassNames)} information-widget-link`}
>
{getInnerBlock(children)}
{getBottomBlock(children)}
</a>

View file

@ -8,8 +8,10 @@ import WidgetIcon from "./widget_icon";
export default function Error({ options }) {
const { t } = useTranslation();
return <Container options={options} additionalClassNames="information-widget-error">
<PrimaryText>{t("widget.api_error")}</PrimaryText>
<WidgetIcon icon={BiError} size="l" />
</Container>;
return (
<Container options={options} additionalClassNames="information-widget-error">
<PrimaryText>{t("widget.api_error")}</PrimaryText>
<WidgetIcon icon={BiError} size="l" />
</Container>
);
}

View file

@ -1,5 +1,3 @@
export default function PrimaryText({ children }) {
return (
<span className="primary-text text-theme-800 dark:text-theme-200 text-sm">{children}</span>
);
return <span className="primary-text text-theme-800 dark:text-theme-200 text-sm">{children}</span>;
}

View file

@ -1,6 +1,6 @@
export default function Raw({ children }) {
if (children.type === Raw) {
return [children];
return [children];
}
return children;

View file

@ -1,22 +1,37 @@
import UsageBar from "../resources/usage-bar";
export default function Resource({ children, icon, value, label, expandedValue = "", expandedLabel = "", percentage, expanded = false, additionalClassNames='' }) {
export default function Resource({
children,
icon,
value,
label,
expandedValue = "",
expandedLabel = "",
percentage,
expanded = false,
additionalClassNames = "",
}) {
const Icon = icon;
return <div className={`flex-none flex flex-row items-center mr-3 py-1.5 information-widget-resource ${ additionalClassNames }`}>
<Icon className="text-theme-800 dark:text-theme-200 w-5 h-5 resource-icon"/>
<div className={ `flex flex-col ml-3 text-left min-w-[85px] ${ expanded ? ' expanded' : ''}`}>
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5">{value}</div>
<div className="pr-1">{label}</div>
</div>
{ expanded && <div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5">{expandedValue}</div>
<div className="pr-1">{expandedLabel}</div>
return (
<div
className={`flex-none flex flex-row items-center mr-3 py-1.5 information-widget-resource ${additionalClassNames}`}
>
<Icon className="text-theme-800 dark:text-theme-200 w-5 h-5 resource-icon" />
<div className={`flex flex-col ml-3 text-left min-w-[85px] ${expanded ? " expanded" : ""}`}>
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5">{value}</div>
<div className="pr-1">{label}</div>
</div>
}
{ percentage >= 0 && <UsageBar percent={percentage} additionalClassNames="resource-usage" /> }
{ children }
{expanded && (
<div className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5">{expandedValue}</div>
<div className="pr-1">{expandedLabel}</div>
</div>
)}
{percentage >= 0 && <UsageBar percent={percentage} additionalClassNames="resource-usage" />}
{children}
</div>
</div>
</div>;
);
}

View file

@ -7,14 +7,16 @@ import WidgetLabel from "./widget_label";
export default function Resources({ options, children, target, additionalClassNames }) {
const widgetParts = [].concat(...children);
const addedClassNames = classNames('information-widget-resources', additionalClassNames);
const addedClassNames = classNames("information-widget-resources", additionalClassNames);
return <ContainerLink options={options} target={target} additionalClassNames={ addedClassNames }>
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between">
{ widgetParts.filter(child => child && child.type === Resource) }
</div>
{ widgetParts.filter(child => child && child.type === WidgetLabel) }
</Raw>
</ContainerLink>;
return (
<ContainerLink options={options} target={target} additionalClassNames={addedClassNames}>
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between">
{widgetParts.filter((child) => child && child.type === Resource)}
</div>
{widgetParts.filter((child) => child && child.type === WidgetLabel)}
</Raw>
</ContainerLink>
);
}

View file

@ -1,5 +1,3 @@
export default function SecondaryText({ children }) {
return (
<span className="secondary-text text-theme-800 dark:text-theme-200 text-xs">{children}</span>
);
return <span className="secondary-text text-theme-800 dark:text-theme-200 text-xs">{children}</span>;
}

View file

@ -3,10 +3,17 @@ export default function WidgetIcon({ icon, size = "s", pulse = false }) {
let additionalClasses = "information-widget-icon text-theme-800 dark:text-theme-200 ";
switch (size) {
case "m": additionalClasses += "w-6 h-6 "; break;
case "l": additionalClasses += "w-8 h-8 "; break;
case "xl": additionalClasses += "w-10 h-10 "; break;
default: additionalClasses += "w-5 h-5 ";
case "m":
additionalClasses += "w-6 h-6 ";
break;
case "l":
additionalClasses += "w-8 h-8 ";
break;
case "xl":
additionalClasses += "w-10 h-10 ";
break;
default:
additionalClasses += "w-5 h-5 ";
}
if (pulse) {

View file

@ -1,3 +1,5 @@
export default function WidgetLabel({ label = "" }) {
return <div className="information-widget-label pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{label}</div>
return (
<div className="information-widget-label pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{label}</div>
);
}

View file

@ -23,7 +23,10 @@ function MyApp({ Component, pageProps }) {
>
<Head>
{/* https://nextjs.org/docs/messages/no-document-viewport-meta */}
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
</Head>
<ColorProvider>
<ThemeProvider>

View file

@ -1,35 +1,34 @@
import path from "path";
import fs from "fs";
import { CONF_DIR } from "utils/config/config";
import createLogger from "utils/logger";
const logger = createLogger("configFileService");
/**
* @param {import("next").NextApiRequest} req
* @param {import("next").NextApiResponse} res
*/
export default async function handler(req, res) {
const { path: relativePath } = req.query;
// only two supported files, for now
if (!['custom.css', 'custom.js'].includes(relativePath))
{
return res.status(422).end('Unsupported file');
}
const filePath = path.join(CONF_DIR, relativePath);
try {
// Read the content of the file or return empty content
const fileContent = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : '';
// hard-coded since we only support two known files for now
const mimeType = (relativePath === 'custom.css') ? 'text/css' : 'text/javascript';
res.setHeader('Content-Type', mimeType);
return res.status(200).send(fileContent);
} catch (error) {
logger.error(error);
return res.status(500).end('Internal Server Error');
}
}
import path from "path";
import fs from "fs";
import { CONF_DIR } from "utils/config/config";
import createLogger from "utils/logger";
const logger = createLogger("configFileService");
/**
* @param {import("next").NextApiRequest} req
* @param {import("next").NextApiResponse} res
*/
export default async function handler(req, res) {
const { path: relativePath } = req.query;
// only two supported files, for now
if (!["custom.css", "custom.js"].includes(relativePath)) {
return res.status(422).end("Unsupported file");
}
const filePath = path.join(CONF_DIR, relativePath);
try {
// Read the content of the file or return empty content
const fileContent = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : "";
// hard-coded since we only support two known files for now
const mimeType = relativePath === "custom.css" ? "text/css" : "text/javascript";
res.setHeader("Content-Type", mimeType);
return res.status(200).send(fileContent);
} catch (error) {
logger.error(error);
return res.status(500).end("Internal Server Error");
}
}

View file

@ -44,7 +44,8 @@ export default async function handler(req, res) {
// Try with a service deployed in Docker Swarm, if enabled
if (dockerArgs.swarm) {
const tasks = await docker.listTasks({
const tasks = await docker
.listTasks({
filters: {
service: [containerName],
// A service can have several offline containers, so we only look for an active one.
@ -55,10 +56,10 @@ export default async function handler(req, res) {
// TODO: Show the result for all replicas/containers?
// We can only get stats for 'local' containers so try to find one
const localContainerIDs = containers.map(c => c.Id);
const task = tasks.find(t => localContainerIDs.includes(t.Status?.ContainerStatus?.ContainerID)) ?? tasks.at(0);
const localContainerIDs = containers.map((c) => c.Id);
const task = tasks.find((t) => localContainerIDs.includes(t.Status?.ContainerStatus?.ContainerID)) ?? tasks.at(0);
const taskContainerId = task?.Status?.ContainerStatus?.ContainerID;
if (taskContainerId) {
try {
const container = docker.getContainer(taskContainerId);
@ -69,8 +70,8 @@ export default async function handler(req, res) {
});
} catch (e) {
return res.status(200).json({
error: "Unable to retrieve stats"
})
error: "Unable to retrieve stats",
});
}
}
}
@ -81,7 +82,7 @@ export default async function handler(req, res) {
} catch (e) {
logger.error(e);
return res.status(500).send({
error: {message: e?.message ?? "Unknown error"},
error: { message: e?.message ?? "Unknown error" },
});
}
}

View file

@ -44,7 +44,9 @@ export default async function handler(req, res) {
}
if (dockerArgs.swarm) {
const serviceInfo = await docker.getService(containerName).inspect()
const serviceInfo = await docker
.getService(containerName)
.inspect()
.catch(() => undefined);
if (!serviceInfo) {
@ -77,15 +79,16 @@ export default async function handler(req, res) {
}
} else {
// Global service, prefer 'local' containers
const localContainerIDs = containers.map(c => c.Id);
const task = tasks.find(t => localContainerIDs.includes(t.Status?.ContainerStatus?.ContainerID)) ?? tasks.at(0);
const localContainerIDs = containers.map((c) => c.Id);
const task =
tasks.find((t) => localContainerIDs.includes(t.Status?.ContainerStatus?.ContainerID)) ?? tasks.at(0);
const taskContainerId = task?.Status?.ContainerStatus?.ContainerID;
if (taskContainerId) {
try {
const container = docker.getContainer(taskContainerId);
const info = await container.inspect();
return res.status(200).json({
status: info.State.Status,
health: info.State.Health?.Status,
@ -93,8 +96,8 @@ export default async function handler(req, res) {
} catch (e) {
if (task) {
return res.status(200).json({
status: task.Status.State
})
status: task.Status.State,
});
}
}
}
@ -107,7 +110,7 @@ export default async function handler(req, res) {
} catch (e) {
logger.error(e);
return res.status(500).send({
error: {message: e?.message ?? "Unknown error"},
error: { message: e?.message ?? "Unknown error" },
});
}
}

View file

@ -4,7 +4,15 @@ import { readFileSync } from "fs";
import checkAndCopyConfig, { CONF_DIR } from "utils/config/config";
const configs = ["docker.yaml", "settings.yaml", "services.yaml", "bookmarks.yaml", "widgets.yaml", "custom.css", "custom.js"];
const configs = [
"docker.yaml",
"settings.yaml",
"services.yaml",
"bookmarks.yaml",
"widgets.yaml",
"custom.css",
"custom.js",
];
function hash(buffer) {
const hashSum = createHash("sha256");
@ -20,7 +28,7 @@ export default async function handler(req, res) {
});
// set to date by docker entrypoint, will force revalidation between restarts/recreates
const buildTime = process.env.HOMEPAGE_BUILDTIME?.length ? process.env.HOMEPAGE_BUILDTIME : '';
const buildTime = process.env.HOMEPAGE_BUILDTIME?.length ? process.env.HOMEPAGE_BUILDTIME : "";
const combinedHash = hash(hashes.join("") + buildTime);

View file

@ -13,7 +13,7 @@ export default async function handler(req, res) {
const [namespace, appName] = service;
if (!namespace && !appName) {
res.status(400).send({
error: "kubernetes query parameters are required"
error: "kubernetes query parameters are required",
});
return;
}
@ -23,13 +23,14 @@ export default async function handler(req, res) {
const kc = getKubeConfig();
if (!kc) {
res.status(500).send({
error: "No kubernetes configuration"
error: "No kubernetes configuration",
});
return;
}
const coreApi = kc.makeApiClient(CoreV1Api);
const metricsApi = new Metrics(kc);
const podsResponse = await coreApi.listNamespacedPod(namespace, null, null, null, null, labelSelector)
const podsResponse = await coreApi
.listNamespacedPod(namespace, null, null, null, null, labelSelector)
.then((response) => response.body)
.catch((err) => {
logger.error("Error getting pods: %d %s %s", err.statusCode, err.body, err.response);
@ -37,7 +38,7 @@ export default async function handler(req, res) {
});
if (!podsResponse) {
res.status(500).send({
error: "Error communicating with kubernetes"
error: "Error communicating with kubernetes",
});
return;
}
@ -63,33 +64,36 @@ export default async function handler(req, res) {
});
});
const podStatsList = await Promise.all(pods.map(async (pod) => {
let depMem = 0;
let depCpu = 0;
const podMetrics = await metricsApi.getPodMetrics(namespace, pod.metadata.name)
.then((response) => response)
.catch((err) => {
// 404 generally means that the metrics have not been populated yet
if (err.statusCode !== 404) {
logger.error("Error getting pod metrics: %d %s %s", err.statusCode, err.body, err.response);
}
return null;
});
if (podMetrics) {
podMetrics.containers.forEach((container) => {
depMem += parseMemory(container.usage.memory);
depCpu += parseCpu(container.usage.cpu);
});
}
return {
mem: depMem,
cpu: depCpu
};
}));
const podStatsList = await Promise.all(
pods.map(async (pod) => {
let depMem = 0;
let depCpu = 0;
const podMetrics = await metricsApi
.getPodMetrics(namespace, pod.metadata.name)
.then((response) => response)
.catch((err) => {
// 404 generally means that the metrics have not been populated yet
if (err.statusCode !== 404) {
logger.error("Error getting pod metrics: %d %s %s", err.statusCode, err.body, err.response);
}
return null;
});
if (podMetrics) {
podMetrics.containers.forEach((container) => {
depMem += parseMemory(container.usage.memory);
depCpu += parseCpu(container.usage.cpu);
});
}
return {
mem: depMem,
cpu: depCpu,
};
}),
);
const stats = {
mem: 0,
cpu: 0
}
cpu: 0,
};
podStatsList.forEach((podStat) => {
stats.mem += podStat.mem;
stats.cpu += podStat.cpu;
@ -99,12 +103,12 @@ export default async function handler(req, res) {
stats.cpuUsage = cpuLimit ? stats.cpu / cpuLimit : 0;
stats.memUsage = memLimit ? stats.mem / memLimit : 0;
res.status(200).json({
stats
stats,
});
} catch (e) {
logger.error(e);
res.status(500).send({
error: "unknown error"
error: "unknown error",
});
}
}

View file

@ -6,7 +6,7 @@ import createLogger from "../../../../utils/logger";
const logger = createLogger("kubernetesStatusService");
export default async function handler(req, res) {
const APP_LABEL = "app.kubernetes.io/name";
const APP_LABEL = "app.kubernetes.io/name";
const { service, podSelector } = req.query;
const [namespace, appName] = service;
@ -21,12 +21,13 @@ export default async function handler(req, res) {
const kc = getKubeConfig();
if (!kc) {
res.status(500).send({
error: "No kubernetes configuration"
error: "No kubernetes configuration",
});
return;
}
const coreApi = kc.makeApiClient(CoreV1Api);
const podsResponse = await coreApi.listNamespacedPod(namespace, null, null, null, null, labelSelector)
const podsResponse = await coreApi
.listNamespacedPod(namespace, null, null, null, null, labelSelector)
.then((response) => response.body)
.catch((err) => {
logger.error("Error getting pods: %d %s %s", err.statusCode, err.body, err.response);
@ -34,7 +35,7 @@ export default async function handler(req, res) {
});
if (!podsResponse) {
res.status(500).send({
error: "Error communicating with kubernetes"
error: "Error communicating with kubernetes",
});
return;
}
@ -46,7 +47,7 @@ export default async function handler(req, res) {
});
return;
}
const someReady = pods.find(pod => pod.status.phase === "Running");
const someReady = pods.find((pod) => pod.status.phase === "Running");
const allReady = pods.every((pod) => pod.status.phase === "Running");
let status = "down";
if (allReady) {
@ -55,7 +56,7 @@ export default async function handler(req, res) {
status = "partial";
}
res.status(200).json({
status
status,
});
} catch (e) {
logger.error(e);

View file

@ -7,46 +7,46 @@ import { httpProxy } from "utils/proxy/http";
const logger = createLogger("ping");
export default async function handler(req, res) {
const { group, service } = req.query;
const serviceItem = await getServiceItem(group, service);
if (!serviceItem) {
logger.debug(`No service item found for group ${group} named ${service}`);
return res.status(400).send({
error: "Unable to find service, see log for details.",
});
const { group, service } = req.query;
const serviceItem = await getServiceItem(group, service);
if (!serviceItem) {
logger.debug(`No service item found for group ${group} named ${service}`);
return res.status(400).send({
error: "Unable to find service, see log for details.",
});
}
const { ping: pingURL } = serviceItem;
if (!pingURL) {
logger.debug("No ping URL specified");
return res.status(400).send({
error: "No ping URL given",
});
}
try {
let startTime = performance.now();
let [status] = await httpProxy(pingURL, {
method: "HEAD",
});
let endTime = performance.now();
if (status > 403) {
// try one more time as a GET in case HEAD is rejected for whatever reason
startTime = performance.now();
[status] = await httpProxy(pingURL);
endTime = performance.now();
}
const { ping: pingURL } = serviceItem;
if (!pingURL) {
logger.debug("No ping URL specified");
return res.status(400).send({
error: "No ping URL given",
});
}
try {
let startTime = performance.now();
let [status] = await httpProxy(pingURL, {
method: "HEAD"
});
let endTime = performance.now();
if (status > 403) {
// try one more time as a GET in case HEAD is rejected for whatever reason
startTime = performance.now();
[status] = await httpProxy(pingURL);
endTime = performance.now();
}
return res.status(200).json({
status,
latency: endTime - startTime
});
} catch (e) {
logger.debug("Error attempting ping: %s", JSON.stringify(e));
return res.status(400).send({
error: 'Error attempting ping, see logs.',
});
}
return res.status(200).json({
status,
latency: endTime - startTime,
});
} catch (e) {
logger.debug("Error attempting ping: %s", JSON.stringify(e));
return res.status(400).send({
error: "Error attempting ping, see logs.",
});
}
}

View file

@ -44,13 +44,13 @@ export default async function handler(req, res) {
if (req.query.query && (mappingParams || optionalParams)) {
const queryParams = JSON.parse(req.query.query);
let filteredOptionalParams = []
if (optionalParams) filteredOptionalParams = optionalParams.filter(p => queryParams[p] !== undefined);
let filteredOptionalParams = [];
if (optionalParams) filteredOptionalParams = optionalParams.filter((p) => queryParams[p] !== undefined);
let params = [];
if (mappingParams) params = params.concat(mappingParams);
if (filteredOptionalParams) params = params.concat(filteredOptionalParams);
const query = new URLSearchParams(params.map((p) => [p, queryParams[p]]));
req.query.endpoint = `${req.query.endpoint}?${query}`;
}

View file

@ -15,23 +15,25 @@ async function retrieveFromGlancesAPI(privateWidgetOptions, endpoint) {
const apiUrl = `${url}/api/3/${endpoint}`;
const headers = {
"Accept-Encoding": "application/json"
"Accept-Encoding": "application/json",
};
if (privateWidgetOptions.username && privateWidgetOptions.password) {
headers.Authorization = `Basic ${Buffer.from(`${privateWidgetOptions.username}:${privateWidgetOptions.password}`).toString("base64")}`
headers.Authorization = `Basic ${Buffer.from(
`${privateWidgetOptions.username}:${privateWidgetOptions.password}`,
).toString("base64")}`;
}
const params = { method: "GET", headers };
const [status, , data] = await httpProxy(apiUrl, params);
if (status === 401) {
errorMessage = `Authorization failure getting data from glances API. Data: ${data.toString()}`
errorMessage = `Authorization failure getting data from glances API. Data: ${data.toString()}`;
logger.error(errorMessage);
throw new Error(errorMessage);
}
if (status !== 200) {
errorMessage = `HTTP ${status} getting data from glances API. Data: ${data.toString()}`
errorMessage = `HTTP ${status} getting data from glances API. Data: ${data.toString()}`;
logger.error(errorMessage);
throw new Error(errorMessage);
}
@ -52,7 +54,7 @@ export default async function handler(req, res) {
cpu: cpuData,
load: loadData,
mem: memoryData,
}
};
// Disabled by default, dont call unless needed
if (includeUptime) {

View file

@ -11,13 +11,14 @@ export default async function handler(req, res) {
const kc = getKubeConfig();
if (!kc) {
return res.status(500).send({
error: "No kubernetes configuration"
error: "No kubernetes configuration",
});
}
const coreApi = kc.makeApiClient(CoreV1Api);
const metricsApi = new Metrics(kc);
const nodes = await coreApi.listNode()
const nodes = await coreApi
.listNode()
.then((response) => response.body)
.catch((error) => {
logger.error("Error getting ingresses: %d %s %s", error.statusCode, error.body, error.response);
@ -25,7 +26,7 @@ export default async function handler(req, res) {
});
if (!nodes) {
return res.status(500).send({
error: "unknown error"
error: "unknown error",
});
}
let cpuTotal = 0;
@ -37,16 +38,18 @@ export default async function handler(req, res) {
nodes.items.forEach((node) => {
const cpu = Number.parseInt(node.status.capacity.cpu, 10);
const mem = parseMemory(node.status.capacity.memory);
const ready = node.status.conditions.filter(condition => condition.type === "Ready" && condition.status === "True").length > 0;
const ready =
node.status.conditions.filter((condition) => condition.type === "Ready" && condition.status === "True").length >
0;
nodeMap[node.metadata.name] = {
name: node.metadata.name,
ready,
cpu: {
total: cpu
total: cpu,
},
memory: {
total: mem
}
total: mem,
},
};
cpuTotal += cpu;
memTotal += mem;
@ -68,7 +71,7 @@ export default async function handler(req, res) {
} catch (error) {
logger.error("Error getting metrics, ensure you have metrics-server installed: s", JSON.stringify(error));
return res.status(500).send({
error: "Error getting metrics, check logs for more details"
error: "Error getting metrics, check logs for more details",
});
}
@ -76,24 +79,24 @@ export default async function handler(req, res) {
cpu: {
load: cpuUsage,
total: cpuTotal,
percent: (cpuUsage / cpuTotal) * 100
percent: (cpuUsage / cpuTotal) * 100,
},
memory: {
used: memUsage,
total: memTotal,
free: (memTotal - memUsage),
percent: (memUsage / memTotal) * 100
}
free: memTotal - memUsage,
percent: (memUsage / memTotal) * 100,
},
};
return res.status(200).json({
cluster,
nodes: Object.entries(nodeMap).map(([name, node]) => ({ name, ...node }))
nodes: Object.entries(nodeMap).map(([name, node]) => ({ name, ...node })),
});
} catch (e) {
logger.error("exception %s", e);
return res.status(500).send({
error: "unknown error"
error: "unknown error",
});
}
}

View file

@ -47,7 +47,7 @@ function parseLonghornData(data) {
export default async function handler(req, res) {
const settings = getSettings();
const longhornSettings = settings?.providers?.longhorn || {};
const {url, username, password} = longhornSettings;
const { url, username, password } = longhornSettings;
if (!url) {
const errorMessage = "Missing Longhorn URL";
@ -57,10 +57,10 @@ export default async function handler(req, res) {
const apiUrl = `${url}/v1/nodes`;
const headers = {
"Accept-Encoding": "application/json"
"Accept-Encoding": "application/json",
};
if (username && password) {
headers.Authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
headers.Authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
}
const params = { method: "GET", headers };

View file

@ -3,7 +3,7 @@ import cachedFetch from "utils/proxy/cached-fetch";
export default async function handler(req, res) {
const { latitude, longitude, units, cache, timezone } = req.query;
const degrees = units === "imperial" ? "fahrenheit" : "celsius";
const timezeone = timezone ?? 'auto'
const timezeone = timezone ?? "auto";
const apiUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&daily=sunrise,sunset&current_weather=true&temperature_unit=${degrees}&timezone=${timezeone}`;
return res.send(await cachedFetch(apiUrl, cache));
}
}

View file

@ -1,6 +1,6 @@
import { existsSync } from "fs";
const si = require('systeminformation');
const si = require("systeminformation");
export default async function handler(req, res) {
const { type, target } = req.query;
@ -25,7 +25,7 @@ export default async function handler(req, res) {
const fsSize = await si.fsSize();
return res.status(200).json({
drive: fsSize.find(fs => fs.mount === target) ?? fsSize.find(fs => fs.mount === "/")
drive: fsSize.find((fs) => fs.mount === target) ?? fsSize.find((fs) => fs.mount === "/"),
});
}
@ -44,7 +44,7 @@ export default async function handler(req, res) {
if (type === "uptime") {
const timeData = await si.time();
return res.status(200).json({
uptime: timeData.uptime
uptime: timeData.uptime,
});
}

View file

@ -183,7 +183,10 @@ function Home({ initialSettings }) {
const { data: bookmarks } = useSWR("api/bookmarks");
const { data: widgets } = useSWR("api/widgets");
const servicesAndBookmarks = [...services.map(sg => sg.services).flat(), ...bookmarks.map(bg => bg.bookmarks).flat()].filter(i => i?.href);
const servicesAndBookmarks = [
...services.map((sg) => sg.services).flat(),
...bookmarks.map((bg) => bg.bookmarks).flat(),
].filter((i) => i?.href);
useEffect(() => {
if (settings.language) {
@ -202,15 +205,15 @@ function Home({ initialSettings }) {
const [searching, setSearching] = useState(false);
const [searchString, setSearchString] = useState("");
let searchProvider = null;
const searchWidget = Object.values(widgets).find(w => w.type === "search");
const searchWidget = Object.values(widgets).find((w) => w.type === "search");
if (searchWidget) {
if (Array.isArray(searchWidget.options?.provider)) {
// if search provider is a list, try to retrieve from localstorage, fall back to the first
searchProvider = getStoredProvider() ?? searchProviders[searchWidget.options.provider[0]];
} else if (searchWidget.options?.provider === 'custom') {
} else if (searchWidget.options?.provider === "custom") {
searchProvider = {
url: searchWidget.options.url
}
url: searchWidget.options.url,
};
} else {
searchProvider = searchProviders[searchWidget.options?.provider];
}
@ -229,35 +232,38 @@ function Home({ initialSettings }) {
}
}
document.addEventListener('keydown', handleKeyDown);
document.addEventListener("keydown", handleKeyDown);
return function cleanup() {
document.removeEventListener('keydown', handleKeyDown);
}
})
document.removeEventListener("keydown", handleKeyDown);
};
});
const tabs = useMemo( () => [
...new Set(
Object.keys(settings.layout ?? {}).map(
(groupName) => settings.layout[groupName]?.tab?.toString()
).filter(group => group)
)
], [settings.layout]);
const tabs = useMemo(
() => [
...new Set(
Object.keys(settings.layout ?? {})
.map((groupName) => settings.layout[groupName]?.tab?.toString())
.filter((group) => group),
),
],
[settings.layout],
);
useEffect(() => {
if (!activeTab) {
const initialTab = decodeURI(asPath.substring(asPath.indexOf("#") + 1));
setActiveTab(initialTab === '/' ? slugify(tabs['0']) : initialTab)
setActiveTab(initialTab === "/" ? slugify(tabs["0"]) : initialTab);
}
})
});
const servicesAndBookmarksGroups = useMemo(() => {
const tabGroupFilter = g => g && [activeTab, ''].includes(slugify(settings.layout?.[g.name]?.tab));
const undefinedGroupFilter = g => settings.layout?.[g.name] === undefined;
const tabGroupFilter = (g) => g && [activeTab, ""].includes(slugify(settings.layout?.[g.name]?.tab));
const undefinedGroupFilter = (g) => settings.layout?.[g.name] === undefined;
const layoutGroups = Object.keys(settings.layout ?? {}).map(
(groupName) => services?.find(g => g.name === groupName) ?? bookmarks?.find(b => b.name === groupName)
).filter(tabGroupFilter);
const layoutGroups = Object.keys(settings.layout ?? {})
.map((groupName) => services?.find((g) => g.name === groupName) ?? bookmarks?.find((b) => b.name === groupName))
.filter(tabGroupFilter);
if (!settings.layout && JSON.stringify(settings.layout) !== JSON.stringify(initialSettings.layout)) {
// wait for settings to populate (if different from initial settings), otherwise all the widgets will be requested initially even if we are on a single tab
@ -267,58 +273,77 @@ function Home({ initialSettings }) {
const serviceGroups = services?.filter(tabGroupFilter).filter(undefinedGroupFilter);
const bookmarkGroups = bookmarks.filter(tabGroupFilter).filter(undefinedGroupFilter);
return <>
{tabs.length > 0 && <div key="tabs" id="tabs" className="m-6 sm:m-9 sm:mt-4 sm:mb-0">
<ul className={classNames(
"sm:flex rounded-md bg-theme-100/20 dark:bg-white/5",
settings.cardBlur !== undefined && `backdrop-blur${settings.cardBlur.length ? '-': "" }${settings.cardBlur}`
)} id="myTab" data-tabs-toggle="#myTabContent" role="tablist" >
{tabs.map(tab => <Tab key={tab} tab={tab} />)}
</ul>
</div>}
{layoutGroups.length > 0 && <div key="layoutGroups" id="layout-groups" className="flex flex-wrap m-4 sm:m-8 sm:mt-4 items-start mb-2">
{layoutGroups.map((group) => (
group.services ?
(<ServicesGroup
key={group.name}
group={group.name}
services={group}
layout={settings.layout?.[group.name]}
fiveColumns={settings.fiveColumns}
disableCollapse={settings.disableCollapse}
/>) :
(<BookmarksGroup
key={group.name}
bookmarks={group}
layout={settings.layout?.[group.name]}
disableCollapse={settings.disableCollapse}
/>)
)
)}
</div>}
{serviceGroups?.length > 0 && <div key="services" id="services" className="flex flex-wrap m-4 sm:m-8 sm:mt-4 items-start mb-2">
{serviceGroups.map((group) => (
<ServicesGroup
key={group.name}
group={group.name}
services={group}
layout={settings.layout?.[group.name]}
fiveColumns={settings.fiveColumns}
disableCollapse={settings.disableCollapse}
/>
))}
</div>}
{bookmarkGroups?.length > 0 && <div key="bookmarks" id="bookmarks" className="flex flex-wrap m-4 sm:m-8 sm:mt-4 items-start mb-2">
{bookmarkGroups.map((group) => (
<BookmarksGroup
key={group.name}
bookmarks={group}
layout={settings.layout?.[group.name]}
disableCollapse={settings.disableCollapse}
/>
))}
</div>}
return (
<>
{tabs.length > 0 && (
<div key="tabs" id="tabs" className="m-6 sm:m-9 sm:mt-4 sm:mb-0">
<ul
className={classNames(
"sm:flex rounded-md bg-theme-100/20 dark:bg-white/5",
settings.cardBlur !== undefined &&
`backdrop-blur${settings.cardBlur.length ? "-" : ""}${settings.cardBlur}`,
)}
id="myTab"
data-tabs-toggle="#myTabContent"
role="tablist"
>
{tabs.map((tab) => (
<Tab key={tab} tab={tab} />
))}
</ul>
</div>
)}
{layoutGroups.length > 0 && (
<div key="layoutGroups" id="layout-groups" className="flex flex-wrap m-4 sm:m-8 sm:mt-4 items-start mb-2">
{layoutGroups.map((group) =>
group.services ? (
<ServicesGroup
key={group.name}
group={group.name}
services={group}
layout={settings.layout?.[group.name]}
fiveColumns={settings.fiveColumns}
disableCollapse={settings.disableCollapse}
/>
) : (
<BookmarksGroup
key={group.name}
bookmarks={group}
layout={settings.layout?.[group.name]}
disableCollapse={settings.disableCollapse}
/>
),
)}
</div>
)}
{serviceGroups?.length > 0 && (
<div key="services" id="services" className="flex flex-wrap m-4 sm:m-8 sm:mt-4 items-start mb-2">
{serviceGroups.map((group) => (
<ServicesGroup
key={group.name}
group={group.name}
services={group}
layout={settings.layout?.[group.name]}
fiveColumns={settings.fiveColumns}
disableCollapse={settings.disableCollapse}
/>
))}
</div>
)}
{bookmarkGroups?.length > 0 && (
<div key="bookmarks" id="bookmarks" className="flex flex-wrap m-4 sm:m-8 sm:mt-4 items-start mb-2">
{bookmarkGroups.map((group) => (
<BookmarksGroup
key={group.name}
bookmarks={group}
layout={settings.layout?.[group.name]}
disableCollapse={settings.disableCollapse}
/>
))}
</div>
)}
</>
);
}, [
tabs,
activeTab,
@ -328,7 +353,7 @@ function Home({ initialSettings }) {
settings.fiveColumns,
settings.disableCollapse,
settings.cardBlur,
initialSettings.layout
initialSettings.layout,
]);
return (
@ -355,7 +380,8 @@ function Home({ initialSettings }) {
<link rel="preload" href="api/config/custom.css" as="fetch" crossOrigin="anonymous" />
<style data-name="custom.css">
<FileContent path="custom.css"
<FileContent
path="custom.css"
loadingValue="/* Loading custom CSS... */"
errorValue="/* Failed to load custom CSS... */"
emptyValue="/* No custom CSS */"
@ -378,31 +404,43 @@ function Home({ initialSettings }) {
className={classNames(
"flex flex-row flex-wrap justify-between",
headerStyles[headerStyle],
settings.cardBlur !== undefined && headerStyle === "boxed" && `backdrop-blur${settings.cardBlur.length ? '-' : ""}${settings.cardBlur}`
settings.cardBlur !== undefined &&
headerStyle === "boxed" &&
`backdrop-blur${settings.cardBlur.length ? "-" : ""}${settings.cardBlur}`,
)}
>
<div id="widgets-wrap"
style={{width: 'calc(100% + 1rem)'}}
className={classNames(
"flex flex-row w-full flex-wrap justify-between -ml-2 -mr-2"
)}
<div
id="widgets-wrap"
style={{ width: "calc(100% + 1rem)" }}
className={classNames("flex flex-row w-full flex-wrap justify-between -ml-2 -mr-2")}
>
{widgets && (
<>
{widgets
.filter((widget) => !rightAlignedWidgets.includes(widget.type))
.map((widget, i) => (
<Widget key={i} widget={widget} style={{ header: headerStyle, isRightAligned: false, cardBlur: settings.cardBlur }} />
<Widget
key={i}
widget={widget}
style={{ header: headerStyle, isRightAligned: false, cardBlur: settings.cardBlur }}
/>
))}
<div id="information-widgets-right" className={classNames(
"m-auto flex flex-wrap grow sm:basis-auto justify-between md:justify-end",
headerStyle === "boxedWidgets" ? "sm:ml-4" : "sm:ml-2"
)}>
<div
id="information-widgets-right"
className={classNames(
"m-auto flex flex-wrap grow sm:basis-auto justify-between md:justify-end",
headerStyle === "boxedWidgets" ? "sm:ml-4" : "sm:ml-2",
)}
>
{widgets
.filter((widget) => rightAlignedWidgets.includes(widget.type))
.map((widget, i) => (
<Widget key={i} widget={widget} style={{ header: headerStyle, isRightAligned: true, cardBlur: settings.cardBlur }} />
<Widget
key={i}
widget={widget}
style={{ header: headerStyle, isRightAligned: true, cardBlur: settings.cardBlur }}
/>
))}
</div>
</>
@ -436,7 +474,7 @@ export default function Wrapper({ initialSettings, fallback }) {
if (initialSettings && initialSettings.background) {
let opacity = initialSettings.backgroundOpacity ?? 1;
let backgroundImage = initialSettings.background;
if (typeof initialSettings.background === 'object') {
if (typeof initialSettings.background === "object") {
backgroundImage = initialSettings.background.image;
backgroundBlur = initialSettings.background.blur !== undefined;
backgroundSaturate = initialSettings.background.saturate !== undefined;
@ -460,7 +498,7 @@ export default function Wrapper({ initialSettings, fallback }) {
className={classNames(
"relative",
initialSettings.theme && initialSettings.theme,
initialSettings.color && `theme-${initialSettings.color}`
initialSettings.color && `theme-${initialSettings.color}`,
)}
>
<div
@ -469,14 +507,16 @@ export default function Wrapper({ initialSettings, fallback }) {
style={wrappedStyle}
>
<div
id="inner_wrapper"
tabIndex="-1"
className={classNames(
'fixed overflow-auto w-full h-full',
backgroundBlur && `backdrop-blur${initialSettings.background.blur.length ? '-' : ""}${initialSettings.background.blur}`,
backgroundSaturate && `backdrop-saturate-${initialSettings.background.saturate}`,
backgroundBrightness && `backdrop-brightness-${initialSettings.background.brightness}`,
)}>
id="inner_wrapper"
tabIndex="-1"
className={classNames(
"fixed overflow-auto w-full h-full",
backgroundBlur &&
`backdrop-blur${initialSettings.background.blur.length ? "-" : ""}${initialSettings.background.blur}`,
backgroundSaturate && `backdrop-saturate-${initialSettings.background.saturate}`,
backgroundBrightness && `backdrop-brightness-${initialSettings.background.brightness}`,
)}
>
<Index initialSettings={initialSettings} fallback={fallback} />
</div>
</div>

View file

@ -57,4 +57,4 @@ body {
::-webkit-details-marker {
display: none;
}
}

View file

@ -9,7 +9,7 @@ import {
servicesFromConfig,
servicesFromDocker,
cleanServiceGroups,
servicesFromKubernetes
servicesFromKubernetes,
} from "utils/config/service-helpers";
import { cleanWidgetGroups, widgetsFromConfig } from "utils/config/widget-helpers";
@ -59,7 +59,7 @@ export async function bookmarksResponse() {
bookmarksArray.forEach((group) => {
if (definedLayouts) {
const layoutIndex = definedLayouts.findIndex(layout => layout === group.name);
const layoutIndex = definedLayouts.findIndex((layout) => layout === group.name);
if (layoutIndex > -1) sortedGroups[layoutIndex] = group;
else unsortedGroups.push(group);
} else {
@ -67,7 +67,7 @@ export async function bookmarksResponse() {
}
});
return [...sortedGroups.filter(g => g), ...unsortedGroups];
return [...sortedGroups.filter((g) => g), ...unsortedGroups];
}
export async function widgetsResponse() {
@ -126,11 +126,13 @@ export async function servicesResponse() {
}
const mergedGroupsNames = [
...new Set([
discoveredDockerServices.map((group) => group.name),
discoveredKubernetesServices.map((group) => group.name),
configuredServices.map((group) => group.name),
].flat()),
...new Set(
[
discoveredDockerServices.map((group) => group.name),
discoveredKubernetesServices.map((group) => group.name),
configuredServices.map((group) => group.name),
].flat(),
),
];
const sortedGroups = [];
@ -138,22 +140,23 @@ export async function servicesResponse() {
const definedLayouts = initialSettings.layout ? Object.keys(initialSettings.layout) : null;
mergedGroupsNames.forEach((groupName) => {
const discoveredDockerGroup = discoveredDockerServices.find((group) => group.name === groupName) || { services: [] };
const discoveredKubernetesGroup = discoveredKubernetesServices.find((group) => group.name === groupName) || { services: [] };
const discoveredDockerGroup = discoveredDockerServices.find((group) => group.name === groupName) || {
services: [],
};
const discoveredKubernetesGroup = discoveredKubernetesServices.find((group) => group.name === groupName) || {
services: [],
};
const configuredGroup = configuredServices.find((group) => group.name === groupName) || { services: [] };
const mergedGroup = {
name: groupName,
services: [
...discoveredDockerGroup.services,
...discoveredKubernetesGroup.services,
...configuredGroup.services
].filter((service) => service)
services: [...discoveredDockerGroup.services, ...discoveredKubernetesGroup.services, ...configuredGroup.services]
.filter((service) => service)
.sort(compareServices),
};
if (definedLayouts) {
const layoutIndex = definedLayouts.findIndex(layout => layout === mergedGroup.name);
const layoutIndex = definedLayouts.findIndex((layout) => layout === mergedGroup.name);
if (layoutIndex > -1) sortedGroups[layoutIndex] = mergedGroup;
else unsortedGroups.push(mergedGroup);
} else {
@ -161,5 +164,5 @@ export async function servicesResponse() {
}
});
return [...sortedGroups.filter(g => g), ...unsortedGroups];
return [...sortedGroups.filter((g) => g), ...unsortedGroups];
}

View file

@ -9,22 +9,24 @@ const cacheKey = "homepageEnvironmentVariables";
const homepageVarPrefix = "HOMEPAGE_VAR_";
const homepageFilePrefix = "HOMEPAGE_FILE_";
export const CONF_DIR = process.env.HOMEPAGE_CONFIG_DIR ? process.env.HOMEPAGE_CONFIG_DIR : join(process.cwd(), "config");
export const CONF_DIR = process.env.HOMEPAGE_CONFIG_DIR
? process.env.HOMEPAGE_CONFIG_DIR
: join(process.cwd(), "config");
export default function checkAndCopyConfig(config) {
if (!existsSync(CONF_DIR)) {
mkdirSync(CONF_DIR, { recursive: true });
mkdirSync(CONF_DIR, { recursive: true });
}
const configYaml = join(CONF_DIR, config);
if (!existsSync(configYaml)) {
const configSkeleton = join(process.cwd(), "src", "skeleton", config);
try {
copyFileSync(configSkeleton, configYaml)
copyFileSync(configSkeleton, configYaml);
console.info("%s was copied to the config folder", config);
} catch (err) {
console.error("error copying config", err);
throw err;
console.error("error copying config", err);
throw err;
}
return true;
@ -42,7 +44,9 @@ function getCachedEnvironmentVars() {
let cachedVars = cache.get(cacheKey);
if (!cachedVars) {
// initialize cache
cachedVars = Object.entries(process.env).filter(([key, ]) => key.includes(homepageVarPrefix) || key.includes(homepageFilePrefix));
cachedVars = Object.entries(process.env).filter(
([key]) => key.includes(homepageVarPrefix) || key.includes(homepageFilePrefix),
);
cache.put(cacheKey, cachedVars);
}
return cachedVars;
@ -50,7 +54,8 @@ function getCachedEnvironmentVars() {
export function substituteEnvironmentVars(str) {
let result = str;
if (result.includes('{{')) { // crude check if we have vars to replace
if (result.includes("{{")) {
// crude check if we have vars to replace
const cachedVars = getCachedEnvironmentVars();
cachedVars.forEach(([key, value]) => {
if (key.startsWith(homepageVarPrefix)) {
@ -77,13 +82,13 @@ export function getSettings() {
// support yaml list but old spec was object so convert to that
// see https://github.com/gethomepage/homepage/issues/1546
if (Array.isArray(initialSettings.layout)) {
const layoutItems = initialSettings.layout
initialSettings.layout = {}
layoutItems.forEach(i => {
const name = Object.keys(i)[0]
initialSettings.layout[name] = i[name]
})
const layoutItems = initialSettings.layout;
initialSettings.layout = {};
layoutItems.forEach((i) => {
const name = Object.keys(i)[0];
initialSettings.layout[name] = i[name];
});
}
}
return initialSettings
return initialSettings;
}

View file

@ -27,16 +27,16 @@ export default function getDockerArguments(server) {
}
if (servers[server].host) {
const res ={
const res = {
conn: { host: servers[server].host },
swarm: !!servers[server].swarm,
}
};
if (servers[server].port){
if (servers[server].port) {
res.conn.port = servers[server].port;
}
if (servers[server].tls){
if (servers[server].tls) {
res.conn.ca = readFileSync(path.join(CONF_DIR, servers[server].tls.caFile));
res.conn.cert = readFileSync(path.join(CONF_DIR, servers[server].tls.certFile));
res.conn.key = readFileSync(path.join(CONF_DIR, servers[server].tls.keyFile));

View file

@ -16,13 +16,13 @@ export default function getKubeConfig() {
const kc = new KubeConfig();
switch (config?.mode) {
case 'cluster':
case "cluster":
kc.loadFromCluster();
break;
case 'default':
case "default":
kc.loadFromDefault();
break;
case 'disabled':
case "disabled":
default:
return null;
}

View file

@ -92,7 +92,7 @@ export async function servicesFromDocker() {
shvl.set(
constructedService,
label.replace("homepage.", ""),
substituteEnvironmentVars(containerLabels[label])
substituteEnvironmentVars(containerLabels[label]),
);
}
});
@ -105,7 +105,7 @@ export async function servicesFromDocker() {
// a server failed, but others may succeed
return { server: serverName, services: [] };
}
})
}),
);
const mappedServiceGroups = [];
@ -152,13 +152,13 @@ export async function checkCRD(kc, name) {
"Error checking if CRD %s exists. Make sure to add the following permission to your RBAC: %d %s %s",
name,
error.statusCode,
error.body.message
error.body.message,
);
}
return false
return false;
});
return exist
return exist;
}
export async function servicesFromKubernetes() {
@ -195,7 +195,7 @@ export async function servicesFromKubernetes() {
"Error getting traefik ingresses from traefik.containo.us: %d %s %s",
error.statusCode,
error.body,
error.response
error.response,
);
}
@ -211,18 +211,18 @@ export async function servicesFromKubernetes() {
"Error getting traefik ingresses from traefik.io: %d %s %s",
error.statusCode,
error.body,
error.response
error.response,
);
}
return [];
});
const traefikIngressList = [...traefikIngressListContaino?.items ?? [], ...traefikIngressListIo?.items ?? []];
const traefikIngressList = [...(traefikIngressListContaino?.items ?? []), ...(traefikIngressListIo?.items ?? [])];
if (traefikIngressList.length > 0) {
const traefikServices = traefikIngressList.filter(
(ingress) => ingress.metadata.annotations && ingress.metadata.annotations[`${ANNOTATION_BASE}/href`]
(ingress) => ingress.metadata.annotations && ingress.metadata.annotations[`${ANNOTATION_BASE}/href`],
);
ingressList.items.push(...traefikServices);
}
@ -233,7 +233,7 @@ export async function servicesFromKubernetes() {
const services = ingressList.items
.filter(
(ingress) =>
ingress.metadata.annotations && ingress.metadata.annotations[`${ANNOTATION_BASE}/enabled`] === "true"
ingress.metadata.annotations && ingress.metadata.annotations[`${ANNOTATION_BASE}/enabled`] === "true",
)
.map((ingress) => {
let constructedService = {
@ -266,7 +266,7 @@ export async function servicesFromKubernetes() {
shvl.set(
constructedService,
annotation.replace(`${ANNOTATION_BASE}/`, ""),
ingress.metadata.annotations[annotation]
ingress.metadata.annotations[annotation],
);
}
});

View file

@ -27,16 +27,14 @@ SOFTWARE.
*/
export function get(object, path, def) {
return (
// Split the path into keys and reduce the object to the target value
object = path.split(/[.[\]]+/).reduce(function (obj, p) {
// Check each nested object to see if the key exists
return obj && obj[p] !== undefined ? obj[p] : undefined;
}, object)
) === undefined
// If the final value is undefined, return the default value
? def
: object; // Otherwise, return the value found
// Split the path into keys and reduce the object to the target value
return (object = path.split(/[.[\]]+/).reduce(function (obj, p) {
// Check each nested object to see if the key exists
return obj && obj[p] !== undefined ? obj[p] : undefined;
}, object)) === undefined
? // If the final value is undefined, return the default value
def
: object; // Otherwise, return the value found
}
export function set(obj, path, val) {
@ -58,13 +56,11 @@ export function set(obj, path, val) {
const isIndex = /^\d+$/.test(keys[i + 1]);
// If current key doesn't exist, initialise it as an array or object
acc[key] = Array.isArray(acc[key])
? acc[key]
: (isIndex ? [] : acc[key] || {});
acc[key] = Array.isArray(acc[key]) ? acc[key] : isIndex ? [] : acc[key] || {};
// Return nested object for next iteration
return acc[key];
}, obj)[lastKey] = val; // Finally set the value
}, obj)[lastKey] = val; // Finally set the value
return obj;
}

View file

@ -6,76 +6,72 @@ import yaml from "js-yaml";
import checkAndCopyConfig, { CONF_DIR, substituteEnvironmentVars } from "utils/config/config";
export async function widgetsFromConfig() {
checkAndCopyConfig("widgets.yaml");
checkAndCopyConfig("widgets.yaml");
const widgetsYaml = path.join(CONF_DIR, "widgets.yaml");
const rawFileContents = await fs.readFile(widgetsYaml, "utf8");
const fileContents = substituteEnvironmentVars(rawFileContents);
const widgets = yaml.load(fileContents);
const widgetsYaml = path.join(CONF_DIR, "widgets.yaml");
const rawFileContents = await fs.readFile(widgetsYaml, "utf8");
const fileContents = substituteEnvironmentVars(rawFileContents);
const widgets = yaml.load(fileContents);
if (!widgets) return [];
if (!widgets) return [];
// map easy to write YAML objects into easy to consume JS arrays
const widgetsArray = widgets.map((group, index) => ({
type: Object.keys(group)[0],
options: {
index,
...group[Object.keys(group)[0]]
},
}));
return widgetsArray;
// map easy to write YAML objects into easy to consume JS arrays
const widgetsArray = widgets.map((group, index) => ({
type: Object.keys(group)[0],
options: {
index,
...group[Object.keys(group)[0]],
},
}));
return widgetsArray;
}
export async function cleanWidgetGroups(widgets) {
return widgets.map((widget, index) => {
const sanitizedOptions = widget.options;
const optionKeys = Object.keys(sanitizedOptions);
// delete private options from the sanitized options
["username", "password", "key"].forEach((pO) => {
if (optionKeys.includes(pO)) {
delete sanitizedOptions[pO];
}
});
// delete url from the sanitized options if the widget is not a search or glances widgeth
if (widget.type !== "search" && widget.type !== "glances" && optionKeys.includes("url")) {
delete sanitizedOptions.url;
}
return widgets.map((widget, index) => {
const sanitizedOptions = widget.options;
const optionKeys = Object.keys(sanitizedOptions);
return {
type: widget.type,
options: {
index,
...sanitizedOptions
},
}
// delete private options from the sanitized options
["username", "password", "key"].forEach((pO) => {
if (optionKeys.includes(pO)) {
delete sanitizedOptions[pO];
}
});
// delete url from the sanitized options if the widget is not a search or glances widgeth
if (widget.type !== "search" && widget.type !== "glances" && optionKeys.includes("url")) {
delete sanitizedOptions.url;
}
return {
type: widget.type,
options: {
index,
...sanitizedOptions,
},
};
});
}
export async function getPrivateWidgetOptions(type, widgetIndex) {
const widgets = await widgetsFromConfig();
const privateOptions = widgets.map((widget) => {
const {
index,
url,
username,
password,
key
} = widget.options;
const widgets = await widgetsFromConfig();
return {
type: widget.type,
options: {
index,
url,
username,
password,
key
},
}
});
return (type !== undefined && widgetIndex !== undefined) ? privateOptions.find(o => o.type === type && o.options.index === parseInt(widgetIndex, 10))?.options : privateOptions;
const privateOptions = widgets.map((widget) => {
const { index, url, username, password, key } = widget.options;
return {
type: widget.type,
options: {
index,
url,
username,
password,
key,
},
};
});
return type !== undefined && widgetIndex !== undefined
? privateOptions.find((o) => o.type === type && o.options.index === parseInt(widgetIndex, 10))?.options
: privateOptions;
}

View file

@ -4,11 +4,11 @@ export function parseCpu(cpuStr) {
const units = cpuStr.substring(cpuStr.length - unitLength);
if (Number.isNaN(Number(units))) {
switch (units) {
case 'n':
case "n":
return base / 1000000000;
case 'u':
case "u":
return base / 1000000;
case 'm':
case "m":
return base / 1000;
default:
return base;
@ -19,22 +19,22 @@ export function parseCpu(cpuStr) {
}
export function parseMemory(memStr) {
const unitLength = (memStr.substring(memStr.length - 1) === 'i' ? 2 : 1);
const unitLength = memStr.substring(memStr.length - 1) === "i" ? 2 : 1;
const base = Number.parseInt(memStr, 10);
const units = memStr.substring(memStr.length - unitLength);
if (Number.isNaN(Number(units))) {
switch (units) {
case 'Ki':
case "Ki":
return base * 1000;
case 'K':
case "K":
return base * 1024;
case 'Mi':
case "Mi":
return base * 1000000;
case 'M':
case "M":
return base * 1024 * 1024;
case 'Gi':
case "Gi":
return base * 1000000000;
case 'G':
case "G":
return base * 1024 * 1024 * 1024;
default:
return base;

View file

@ -1,12 +1,12 @@
// eslint-disable-next-line import/prefer-default-export
export const columnMap = [
"grid-cols-1 md:grid-cols-1 lg:grid-cols-1",
"grid-cols-1 md:grid-cols-1 lg:grid-cols-1",
"grid-cols-1 md:grid-cols-2 lg:grid-cols-2",
"grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
"grid-cols-1 md:grid-cols-2 lg:grid-cols-4",
"grid-cols-1 md:grid-cols-2 lg:grid-cols-5",
"grid-cols-1 md:grid-cols-2 lg:grid-cols-6",
"grid-cols-1 md:grid-cols-2 lg:grid-cols-7",
"grid-cols-1 md:grid-cols-2 lg:grid-cols-8",
];
"grid-cols-1 md:grid-cols-1 lg:grid-cols-1",
"grid-cols-1 md:grid-cols-1 lg:grid-cols-1",
"grid-cols-1 md:grid-cols-2 lg:grid-cols-2",
"grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
"grid-cols-1 md:grid-cols-2 lg:grid-cols-4",
"grid-cols-1 md:grid-cols-2 lg:grid-cols-5",
"grid-cols-1 md:grid-cols-2 lg:grid-cols-6",
"grid-cols-1 md:grid-cols-2 lg:grid-cols-7",
"grid-cols-1 md:grid-cols-2 lg:grid-cols-8",
];

View file

@ -5,7 +5,6 @@ import winston from "winston";
import checkAndCopyConfig, { getSettings, CONF_DIR } from "utils/config/config";
let winstonLogger;
function init() {
@ -48,7 +47,7 @@ function init() {
combineMessageAndSplat(),
winston.format.timestamp(),
winston.format.colorize(),
winston.format.printf(messageFormatter)
winston.format.printf(messageFormatter),
),
handleExceptions: true,
handleRejections: true,
@ -59,7 +58,7 @@ function init() {
winston.format.errors({ stack: true }),
combineMessageAndSplat(),
winston.format.timestamp(),
winston.format.printf(messageFormatter)
winston.format.printf(messageFormatter),
),
filename: `${logpath}/logs/homepage.log`,
handleExceptions: true,

View file

@ -5,7 +5,7 @@ export function formatApiCall(url, args) {
return args[key] || "";
};
return url.replace(/\/+$/, "").replace(find, replace).replace(find,replace);
return url.replace(/\/+$/, "").replace(find, replace).replace(find, replace);
}
function getURLSearchParams(widget, endpoint) {
@ -57,8 +57,8 @@ export function jsonArrayFilter(data, filter) {
export function sanitizeErrorURL(errorURL) {
// Dont display sensitive params on frontend
const url = new URL(errorURL);
["apikey", "api_key", "token", "t"].forEach(key => {
if (url.searchParams.has(key)) url.searchParams.set(key, "***")
["apikey", "api_key", "token", "t"].forEach((key) => {
if (url.searchParams.has(key)) url.searchParams.set(key, "***");
});
return url.toString();
}
}

View file

@ -28,17 +28,12 @@ export default async function credentialedProxyHandler(req, res, map) {
headers["X-CMC_PRO_API_KEY"] = `${widget.key}`;
} else if (widget.type === "gotify") {
headers["X-gotify-Key"] = `${widget.key}`;
} else if ([
"authentik",
"cloudflared",
"ghostfolio",
"mealie",
"tailscale",
"truenas",
"pterodactyl",
].includes(widget.type))
{
headers.Authorization = `Bearer ${widget.key}`;
} else if (
["authentik", "cloudflared", "ghostfolio", "mealie", "tailscale", "truenas", "pterodactyl"].includes(
widget.type,
)
) {
headers.Authorization = `Bearer ${widget.key}`;
} else if (widget.type === "proxmox") {
headers.Authorization = `PVEAPIToken=${widget.username}=${widget.password}`;
} else if (widget.type === "proxmoxbackupserver") {
@ -62,8 +57,7 @@ export default async function credentialedProxyHandler(req, res, map) {
} else {
headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`;
}
}
else if (widget.type === "azuredevops") {
} else if (widget.type === "azuredevops") {
headers.Authorization = `Basic ${Buffer.from(`$:${widget.key}`).toString("base64")}`;
} else if (widget.type === "glances") {
headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`;
@ -91,10 +85,12 @@ export default async function credentialedProxyHandler(req, res, map) {
if (status >= 400) {
logger.error("HTTP Error %d calling %s", status, url.toString());
}
if (status === 200) {
if (!validateWidgetData(widget, endpoint, resultData)) {
return res.status(500).json({error: {message: "Invalid data", url: sanitizeErrorURL(url), data: resultData}});
return res
.status(500)
.json({ error: { message: "Invalid data", url: sanitizeErrorURL(url), data: resultData } });
}
if (map) resultData = map(resultData);
}

View file

@ -19,10 +19,12 @@ export default async function genericProxyHandler(req, res, map) {
if (widget) {
// if there are more than one question marks, replace others to &
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }).replace(/(?<=\?.*)\?/g, '&'));
const url = new URL(
formatApiCall(widgets[widget.type].api, { endpoint, ...widget }).replace(/(?<=\?.*)\?/g, "&"),
);
const headers = req.extraHeaders ?? widget.headers ?? {};
if (widget.username && widget.password) {
headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`;
}
@ -30,7 +32,7 @@ export default async function genericProxyHandler(req, res, map) {
const params = {
method: widget.method ?? req.method,
headers,
}
};
if (req.body) {
params.body = req.body;
}
@ -38,14 +40,16 @@ export default async function genericProxyHandler(req, res, map) {
const [status, contentType, data] = await httpProxy(url, params);
let resultData = data;
if (resultData.error?.url) {
resultData.error.url = sanitizeErrorURL(url);
}
if (status === 200) {
if (!validateWidgetData(widget, endpoint, resultData)) {
return res.status(status).json({error: {message: "Invalid data", url: sanitizeErrorURL(url), data: resultData}});
return res
.status(status)
.json({ error: { message: "Invalid data", url: sanitizeErrorURL(url), data: resultData } });
}
if (map) resultData = map(resultData);
}
@ -62,10 +66,10 @@ export default async function genericProxyHandler(req, res, map) {
status,
url.protocol,
url.hostname,
url.port ? `:${url.port}` : '',
url.pathname
url.port ? `:${url.port}` : "",
url.pathname,
);
return res.status(status).json({error: {message: "HTTP Error", url: sanitizeErrorURL(url), resultData}});
return res.status(status).json({ error: { message: "HTTP Error", url: sanitizeErrorURL(url), resultData } });
}
return res.status(status).send(resultData);

View file

@ -11,8 +11,8 @@ const logger = createLogger("jsonrpcProxyHandler");
export async function sendJsonRpcRequest(url, method, params, username, password) {
const headers = {
"content-type": "application/json",
"accept": "application/json"
}
accept: "application/json",
};
if (username && password) {
headers.authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
@ -23,7 +23,7 @@ export async function sendJsonRpcRequest(url, method, params, username, password
const httpRequestParams = {
method: "POST",
headers,
body
body,
};
// eslint-disable-next-line no-unused-vars
@ -33,7 +33,7 @@ export async function sendJsonRpcRequest(url, method, params, username, password
// in order to get access to the underlying error object in the JSON response
// you must set `result` equal to undefined
if (json.error && (json.result === null)) {
if (json.error && json.result === null) {
json.result = undefined;
}
return client.receive(json);
@ -45,15 +45,14 @@ export async function sendJsonRpcRequest(url, method, params, username, password
try {
const response = await client.request(method, params);
return [200, "application/json", JSON.stringify(response)];
}
catch (e) {
} catch (e) {
if (e instanceof JSONRPCErrorException) {
logger.debug("Error calling JSONPRC endpoint: %s. %s", url, e.message);
return [200, "application/json", JSON.stringify({result: null, error: {code: e.code, message: e.message}})];
return [200, "application/json", JSON.stringify({ result: null, error: { code: e.code, message: e.message } })];
}
logger.warn("Error calling JSONPRC endpoint: %s. %s", url, e);
return [500, "application/json", JSON.stringify({result: null, error: {code: 2, message: e.toString()}})];
return [500, "application/json", JSON.stringify({ result: null, error: { code: 2, message: e.toString() } })];
}
}

View file

@ -7,7 +7,8 @@ import createLogger from "utils/logger";
import widgets from "widgets/widgets";
const INFO_ENDPOINT = "{url}/webapi/query.cgi?api=SYNO.API.Info&version=1&method=query";
const AUTH_ENDPOINT = "{url}/webapi/{path}?api=SYNO.API.Auth&version={maxVersion}&method=login&account={username}&passwd={password}&session=DownloadStation&format=cookie";
const AUTH_ENDPOINT =
"{url}/webapi/{path}?api=SYNO.API.Auth&version={maxVersion}&method=login&account={username}&passwd={password}&session=DownloadStation&format=cookie";
const AUTH_API_NAME = "SYNO.API.Auth";
const proxyName = "synologyProxyHandler";
@ -40,7 +41,7 @@ async function login(loginUrl) {
}
async function getApiInfo(serviceWidget, apiName, serviceName) {
const cacheKey = `${proxyName}__${apiName}__${serviceName}`
const cacheKey = `${proxyName}__${apiName}__${serviceName}`;
let { cgiPath, maxVersion } = cache.get(cacheKey) ?? {};
if (cgiPath && maxVersion) {
return [cgiPath, maxVersion];
@ -56,12 +57,13 @@ async function getApiInfo(serviceWidget, apiName, serviceName) {
if (json?.data?.[apiName]) {
cgiPath = json.data[apiName].path;
maxVersion = json.data[apiName].maxVersion;
logger.debug(`Detected ${serviceWidget.type}: apiName '${apiName}', cgiPath '${cgiPath}', and maxVersion ${maxVersion}`);
logger.debug(
`Detected ${serviceWidget.type}: apiName '${apiName}', cgiPath '${cgiPath}', and maxVersion ${maxVersion}`,
);
cache.put(cacheKey, { cgiPath, maxVersion });
return [cgiPath, maxVersion];
}
}
catch {
} catch {
logger.warn(`Error ${status} obtaining ${apiName} info`);
}
}
@ -124,7 +126,7 @@ function toError(url, synologyError) {
error.error = synologyError.message ?? "Unknown error.";
break;
}
logger.warn(`Unable to call ${url}. code: ${code}, error: ${error.error}.`)
logger.warn(`Unable to call ${url}. code: ${code}, error: ${error.error}.`);
return error;
}
@ -144,7 +146,7 @@ export default async function synologyProxyHandler(req, res) {
const [cgiPath, maxVersion] = await getApiInfo(serviceWidget, mapping.apiName, service);
if (!cgiPath || !maxVersion) {
return res.status(400).json({ error: `Unrecognized API name: ${mapping.apiName}`})
return res.status(400).json({ error: `Unrecognized API name: ${mapping.apiName}` });
}
const url = formatApiCall(widget.api, {
@ -152,7 +154,7 @@ export default async function synologyProxyHandler(req, res) {
apiMethod: mapping.apiMethod,
cgiPath,
maxVersion,
...serviceWidget
...serviceWidget,
});
let [status, contentType, data] = await httpProxy(url);
if (status !== 200) {

View file

@ -25,21 +25,21 @@ function handleRequest(requestor, url, params) {
addCookieHandler(url, params);
if (params?.body) {
params.headers = params.headers ?? {};
params.headers['content-length'] = Buffer.byteLength(params.body);
params.headers["content-length"] = Buffer.byteLength(params.body);
}
const request = requestor.request(url, params, (response) => {
const data = [];
const contentEncoding = response.headers['content-encoding']?.trim().toLowerCase();
const contentEncoding = response.headers["content-encoding"]?.trim().toLowerCase();
let responseContent = response;
if (contentEncoding === 'gzip' || contentEncoding === 'deflate') {
if (contentEncoding === "gzip" || contentEncoding === "deflate") {
// https://github.com/request/request/blob/3c0cddc7c8eb60b470e9519da85896ed7ee0081e/request.js#L1018-L1025
// Be more lenient with decoding compressed responses, in case of invalid gzip responses that are still accepted
// by common browsers.
responseContent = createUnzip({
flush: zlibConstants.Z_SYNC_FLUSH,
finishFlush: zlibConstants.Z_SYNC_FLUSH
finishFlush: zlibConstants.Z_SYNC_FLUSH,
});
// zlib errors
@ -100,14 +100,13 @@ export async function httpProxy(url, params = {}) {
try {
const [status, contentType, data, responseHeaders] = await request;
return [status, contentType, data, responseHeaders];
}
catch (err) {
} catch (err) {
logger.error(
"Error calling %s//%s%s%s...",
constructedUrl.protocol,
constructedUrl.hostname,
constructedUrl.port ? `:${constructedUrl.port}` : '',
constructedUrl.pathname
constructedUrl.port ? `:${constructedUrl.port}` : "",
constructedUrl.pathname,
);
logger.error(err);
return [500, "application/json", { error: { message: err?.message ?? "Unknown error", url, rawError: err } }, null];

View file

@ -7,11 +7,11 @@ export default function useWidgetAPI(widget, ...options) {
if (options && options[1]?.refreshInterval) {
config.refreshInterval = options[1].refreshInterval;
}
let url = formatProxyUrl(widget, ...options)
let url = formatProxyUrl(widget, ...options);
if (options[0] === "") {
url = null
url = null;
}
const { data, error, mutate } = useSWR(url, config);
// make the data error the top-level error
return { data, error: data?.error ?? error, mutate }
return { data, error: data?.error ?? error, mutate };
}

View file

@ -2,34 +2,38 @@
import widgets from "widgets/widgets";
export default function validateWidgetData(widget, endpoint, data) {
let valid = true;
let dataParsed = data;
let error;
let mapping;
if (Buffer.isBuffer(data)) {
try {
dataParsed = JSON.parse(data);
} catch (e) {
error = e;
valid = false;
}
let valid = true;
let dataParsed = data;
let error;
let mapping;
if (Buffer.isBuffer(data)) {
try {
dataParsed = JSON.parse(data);
} catch (e) {
error = e;
valid = false;
}
}
if (dataParsed && Object.entries(dataParsed).length) {
const mappings = widgets[widget.type]?.mappings;
if (mappings) {
mapping = Object.values(mappings).find(m => m.endpoint === endpoint);
mapping?.validate?.forEach(key => {
if (dataParsed[key] === undefined) {
valid = false;
}
});
if (dataParsed && Object.entries(dataParsed).length) {
const mappings = widgets[widget.type]?.mappings;
if (mappings) {
mapping = Object.values(mappings).find((m) => m.endpoint === endpoint);
mapping?.validate?.forEach((key) => {
if (dataParsed[key] === undefined) {
valid = false;
}
});
}
}
if (!valid) {
console.warn(`Invalid data for widget '${widget.type}' endpoint '${endpoint}':\nExpected:${mapping?.validate}\nParse error: ${error ?? "none"}\nData: ${JSON.stringify(data)}`);
}
return valid;
if (!valid) {
console.warn(
`Invalid data for widget '${widget.type}' endpoint '${endpoint}':\nExpected:${mapping?.validate}\nParse error: ${
error ?? "none"
}\nData: ${JSON.stringify(data)}`,
);
}
return valid;
}

View file

@ -14,7 +14,7 @@ export default function Component({ service }) {
if (adguardError) {
return <Container service={service} error={adguardError} />;
}
if (!adguardData) {
return (
<Container service={service}>

View file

@ -6,9 +6,9 @@ const widget = {
mappings: {
info: {
endpoint: "info"
}
endpoint: "info",
},
},
};
export default widget;
export default widget;

View file

@ -10,11 +10,10 @@ export default function Component({ service }) {
const { widget } = service;
const { data: librariesData, error: librariesError } = useWidgetAPI(widget, "libraries");
if (librariesError) {
return <Container service={service} error={librariesError} />;
}
if (!librariesData) {
return (
<Container service={service}>
@ -25,9 +24,9 @@ export default function Component({ service }) {
</Container>
);
}
const podcastLibraries = librariesData.filter(l => l.mediaType === "podcast");
const bookLibraries = librariesData.filter(l => l.mediaType === "book");
const podcastLibraries = librariesData.filter((l) => l.mediaType === "podcast");
const bookLibraries = librariesData.filter((l) => l.mediaType === "book");
const totalPodcasts = podcastLibraries.reduce((total, pL) => parseInt(pL.stats?.totalItems, 10) + total, 0);
const totalBooks = bookLibraries.reduce((total, bL) => parseInt(bL.stats?.totalItems, 10) + total, 0);
@ -38,9 +37,25 @@ export default function Component({ service }) {
return (
<Container service={service}>
<Block label="audiobookshelf.podcasts" value={t("common.number", { value: totalPodcasts })} />
<Block label="audiobookshelf.podcastsDuration" value={t("common.number", { value: totalPodcastsDuration / 60, maximumFractionDigits: 0, style: "unit", unit: "minute" })} />
<Block
label="audiobookshelf.podcastsDuration"
value={t("common.number", {
value: totalPodcastsDuration / 60,
maximumFractionDigits: 0,
style: "unit",
unit: "minute",
})}
/>
<Block label="audiobookshelf.books" value={t("common.number", { value: totalBooks })} />
<Block label="audiobookshelf.booksDuration" value={t("common.number", { value: totalBooksDuration / 60, maximumFractionDigits: 0, style: "unit", unit: "minute" })} />
<Block
label="audiobookshelf.booksDuration"
value={t("common.number", {
value: totalBooksDuration / 60,
maximumFractionDigits: 0,
style: "unit",
unit: "minute",
})}
/>
</Container>
);
}

View file

@ -10,7 +10,7 @@ const logger = createLogger(proxyName);
async function retrieveFromAPI(url, key) {
const headers = {
"content-type": "application/json",
"Authorization": `Bearer ${key}`
Authorization: `Bearer ${key}`,
};
const [status, , data] = await httpProxy(url, { headers });
@ -48,17 +48,22 @@ export default async function audiobookshelfProxyHandler(req, res) {
const url = new URL(formatApiCall(apiURL, { endpoint, ...widget }));
const libraryData = await retrieveFromAPI(url, widget.key);
const libraryStats = await Promise.all(libraryData.libraries.map(async l => {
const stats = await retrieveFromAPI(new URL(formatApiCall(apiURL, { endpoint: `libraries/${l.id}/stats`, ...widget })), widget.key);
return {
...l,
stats
};
}));
const libraryStats = await Promise.all(
libraryData.libraries.map(async (l) => {
const stats = await retrieveFromAPI(
new URL(formatApiCall(apiURL, { endpoint: `libraries/${l.id}/stats`, ...widget })),
widget.key,
);
return {
...l,
stats,
};
}),
);
return res.status(200).send(libraryStats);
} catch (e) {
logger.error(e.message);
return res.status(500).send({error: {message: e.message}});
return res.status(500).send({ error: { message: e.message } });
}
}

View file

@ -11,4 +11,4 @@ const widget = {
},
};
export default widget;
export default widget;

View file

@ -31,11 +31,11 @@ export default function Component({ service }) {
const yesterday = new Date(Date.now()).setHours(-24);
const loginsLast24H = loginsData.reduce(
(total, current) => (current.x_cord >= yesterday ? total + current.y_cord : total),
0
0,
);
const failedLoginsLast24H = failedLoginsData.reduce(
(total, current) => (current.x_cord >= yesterday ? total + current.y_cord : total),
0
0,
);
return (

View file

@ -7,10 +7,7 @@ const widget = {
mappings: {
stats: {
endpoint: "release/stats",
validate: [
"push_approved_count",
"push_rejected_count"
]
validate: ["push_approved_count", "push_rejected_count"],
},
filters: {
endpoint: "filters",

View file

@ -12,14 +12,11 @@ export default function Component({ service }) {
const { data: prData, error: prError } = useWidgetAPI(widget, includePR ? "pr" : null);
const { data: pipelineData, error: pipelineError } = useWidgetAPI(widget, "pipeline");
if (
pipelineError ||
(includePR && (prError || prData?.errorCode !== undefined))
) {
if (pipelineError || (includePR && (prError || prData?.errorCode !== undefined))) {
let finalError = pipelineError ?? prError;
if (includePR && prData?.errorCode !== null) {
// pr call failed possibly with more specific message
finalError = { message: prData?.message ?? 'Error communicating with Azure API' }
finalError = { message: prData?.message ?? "Error communicating with Azure API" };
}
return <Container service={service} error={finalError} />;
}
@ -42,24 +39,27 @@ export default function Component({ service }) {
) : (
<Block label="azuredevops.status" value={t(`azuredevops.${pipelineData.value[0].status.toString()}`)} />
)}
{includePR && <Block label="azuredevops.totalPrs" value={t("common.number", { value: prData.count })} />}
{includePR && <Block
{includePR && (
<Block
label="azuredevops.myPrs"
value={t("common.number", {
value: prData.value?.filter((item) => item.createdBy.uniqueName.toLowerCase() === userEmail.toLowerCase())
.length,
})}
/>}
{includePR && <Block
label="azuredevops.approved"
value={t("common.number", {
value: prData.value
?.filter((item) => item.createdBy.uniqueName.toLowerCase() === userEmail.toLowerCase())
.filter((item) => item.reviewers.some((reviewer) => [5,10].includes(reviewer.vote))).length
})}
/>}
/>
)}
{includePR && (
<Block
label="azuredevops.approved"
value={t("common.number", {
value: prData.value
?.filter((item) => item.createdBy.uniqueName.toLowerCase() === userEmail.toLowerCase())
.filter((item) => item.reviewers.some((reviewer) => [5, 10].includes(reviewer.vote))).length,
})}
/>
)}
</Container>
);
}

View file

@ -6,11 +6,11 @@ const widget = {
mappings: {
pr: {
endpoint: "git/repositories/{repositoryId}/pullrequests"
endpoint: "git/repositories/{repositoryId}/pullrequests",
},
pipeline: {
endpoint: "build/Builds?branchName={branchName}&definitions={definitionId}"
endpoint: "build/Builds?branchName={branchName}&definitions={definitionId}",
},
},
};

View file

@ -10,14 +10,14 @@ export default function Component({ service }) {
const { widget } = service;
const { data: resultData, error: resultError } = useWidgetAPI(widget, "result");
if (resultError) {
return <Container service={service} error={resultError} />;
}
if (!resultData) {
return (
<Container service={service}>,
<Container service={service}>
,
<Block label="caddy.upstreams" />
<Block label="caddy.requests" />
<Block label="caddy.requests_failed" />

View file

@ -18,30 +18,42 @@ export default function Component({ service }) {
}
return {
start: showDate.minus({months: 3}).toFormat("yyyy-MM-dd"),
end: showDate.plus({months: 3}).toFormat("yyyy-MM-dd"),
unmonitored: 'false',
start: showDate.minus({ months: 3 }).toFormat("yyyy-MM-dd"),
end: showDate.plus({ months: 3 }).toFormat("yyyy-MM-dd"),
unmonitored: "false",
};
}, [showDate]);
// Load active integrations
const integrations = useMemo(() => widget.integrations?.map(integration => ({
service: dynamic(() => import(`./integrations/${integration?.type}`)),
widget: integration,
})) ?? [], [widget.integrations]);
const integrations = useMemo(
() =>
widget.integrations?.map((integration) => ({
service: dynamic(() => import(`./integrations/${integration?.type}`)),
widget: integration,
})) ?? [],
[widget.integrations],
);
return <Container service={service}>
<div className="flex flex-col w-full">
<div className="sticky top-0">
{integrations.map(integration => {
const Integration = integration.service;
const key = integration.widget.type + integration.widget.service_name + integration.widget.service_group;
return (
<Container service={service}>
<div className="flex flex-col w-full">
<div className="sticky top-0">
{integrations.map((integration) => {
const Integration = integration.service;
const key = integration.widget.type + integration.widget.service_name + integration.widget.service_group;
return <Integration key={key} config={integration.widget} params={params}
className="fixed bottom-0 left-0 bg-red-500 w-screen h-12" />
})}
return (
<Integration
key={key}
config={integration.widget}
params={params}
className="fixed bottom-0 left-0 bg-red-500 w-screen h-12"
/>
);
})}
</div>
<MonthlyView service={service} className="flex" />
</div>
<MonthlyView service={service} className="flex"/>
</div>
</Container>;
</Container>
);
}

View file

@ -7,9 +7,11 @@ import Error from "../../../components/services/widget/error";
export default function Integration({ config, params }) {
const { setEvents } = useContext(EventContext);
const { data: lidarrData, error: lidarrError } = useWidgetAPI(config, "calendar",
{ ...params, includeArtist: 'false', ...config?.params ?? {} }
);
const { data: lidarrData, error: lidarrError } = useWidgetAPI(config, "calendar", {
...params,
includeArtist: "false",
...(config?.params ?? {}),
});
useEffect(() => {
if (!lidarrData || lidarrError) {
@ -18,19 +20,19 @@ export default function Integration({ config, params }) {
const eventsToAdd = {};
lidarrData?.forEach(event => {
lidarrData?.forEach((event) => {
const title = `${event.artist.artistName} - ${event.title}`;
eventsToAdd[title] = {
title,
date: DateTime.fromISO(event.releaseDate),
color: config?.color ?? 'green'
color: config?.color ?? "green",
};
})
});
setEvents((prevEvents) => ({ ...prevEvents, ...eventsToAdd }));
}, [lidarrData, lidarrError, config, setEvents]);
const error = lidarrError ?? lidarrData?.error;
return error && <Error error={{ message: `${config.type}: ${error.message ?? error}`}} />
return error && <Error error={{ message: `${config.type}: ${error.message ?? error}` }} />;
}

View file

@ -9,9 +9,10 @@ import Error from "../../../components/services/widget/error";
export default function Integration({ config, params }) {
const { t } = useTranslation();
const { setEvents } = useContext(EventContext);
const { data: radarrData, error: radarrError } = useWidgetAPI(config, "calendar",
{ ...params, ...config?.params ?? {} }
);
const { data: radarrData, error: radarrError } = useWidgetAPI(config, "calendar", {
...params,
...(config?.params ?? {}),
});
useEffect(() => {
if (!radarrData || radarrError) {
return;
@ -19,7 +20,7 @@ export default function Integration({ config, params }) {
const eventsToAdd = {};
radarrData?.forEach(event => {
radarrData?.forEach((event) => {
const cinemaTitle = `${event.title} - ${t("calendar.inCinemas")}`;
const physicalTitle = `${event.title} - ${t("calendar.physicalRelease")}`;
const digitalTitle = `${event.title} - ${t("calendar.digitalRelease")}`;
@ -27,23 +28,23 @@ export default function Integration({ config, params }) {
eventsToAdd[cinemaTitle] = {
title: cinemaTitle,
date: DateTime.fromISO(event.inCinemas),
color: config?.color ?? 'amber'
color: config?.color ?? "amber",
};
eventsToAdd[physicalTitle] = {
title: physicalTitle,
date: DateTime.fromISO(event.physicalRelease),
color: config?.color ?? 'cyan'
color: config?.color ?? "cyan",
};
eventsToAdd[digitalTitle] = {
title: digitalTitle,
date: DateTime.fromISO(event.digitalRelease),
color: config?.color ?? 'emerald'
color: config?.color ?? "emerald",
};
})
});
setEvents((prevEvents) => ({ ...prevEvents, ...eventsToAdd }));
}, [radarrData, radarrError, config, setEvents, t]);
const error = radarrError ?? radarrData?.error;
return error && <Error error={{ message: `${config.type}: ${error.message ?? error}`}} />
return error && <Error error={{ message: `${config.type}: ${error.message ?? error}` }} />;
}

View file

@ -7,9 +7,11 @@ import Error from "../../../components/services/widget/error";
export default function Integration({ config, params }) {
const { setEvents } = useContext(EventContext);
const { data: readarrData, error: readarrError } = useWidgetAPI(config, "calendar",
{ ...params, includeAuthor: 'true', ...config?.params ?? {} },
);
const { data: readarrData, error: readarrError } = useWidgetAPI(config, "calendar", {
...params,
includeAuthor: "true",
...(config?.params ?? {}),
});
useEffect(() => {
if (!readarrData || readarrError) {
@ -18,20 +20,20 @@ export default function Integration({ config, params }) {
const eventsToAdd = {};
readarrData?.forEach(event => {
const authorName = event.author?.authorName ?? event.authorTitle.replace(event.title, '');
const title = `${authorName} - ${event.title} ${event?.seriesTitle ? `(${event.seriesTitle})` : ''} `;
readarrData?.forEach((event) => {
const authorName = event.author?.authorName ?? event.authorTitle.replace(event.title, "");
const title = `${authorName} - ${event.title} ${event?.seriesTitle ? `(${event.seriesTitle})` : ""} `;
eventsToAdd[title] = {
title,
date: DateTime.fromISO(event.releaseDate),
color: config?.color ?? 'rose'
color: config?.color ?? "rose",
};
})
});
setEvents((prevEvents) => ({ ...prevEvents, ...eventsToAdd }));
}, [readarrData, readarrError, config, setEvents]);
const error = readarrError ?? readarrData?.error;
return error && <Error error={{ message: `${config.type}: ${error.message ?? error}`}} />
return error && <Error error={{ message: `${config.type}: ${error.message ?? error}` }} />;
}

View file

@ -7,9 +7,13 @@ import Error from "../../../components/services/widget/error";
export default function Integration({ config, params }) {
const { setEvents } = useContext(EventContext);
const { data: sonarrData, error: sonarrError } = useWidgetAPI(config, "calendar",
{ ...params, includeSeries: 'true', includeEpisodeFile: 'false', includeEpisodeImages: 'false', ...config?.params ?? {} }
);
const { data: sonarrData, error: sonarrError } = useWidgetAPI(config, "calendar", {
...params,
includeSeries: "true",
includeEpisodeFile: "false",
includeEpisodeImages: "false",
...(config?.params ?? {}),
});
useEffect(() => {
if (!sonarrData || sonarrError) {
@ -18,19 +22,19 @@ export default function Integration({ config, params }) {
const eventsToAdd = {};
sonarrData?.forEach(event => {
sonarrData?.forEach((event) => {
const title = `${event.series.title ?? event.title} - S${event.seasonNumber}E${event.episodeNumber}`;
eventsToAdd[title] = {
title,
date: DateTime.fromISO(event.airDateUtc),
color: config?.color ?? 'teal'
color: config?.color ?? "teal",
};
})
});
setEvents((prevEvents) => ({ ...prevEvents, ...eventsToAdd }));
}, [sonarrData, sonarrError, config, setEvents]);
const error = sonarrError ?? sonarrData?.error;
return error && <Error error={{ message: `${config.type}: ${error.message ?? error}`}} />
return error && <Error error={{ message: `${config.type}: ${error.message ?? error}` }} />;
}

Some files were not shown because too many files have changed in this diff Show more