mirror of
https://github.com/DI0IK/homepage-plus.git
synced 2025-07-10 15:28:47 +00:00
Run pre-commit hooks over existing codebase
Co-Authored-By: Ben Phelps <ben@phelps.io>
This commit is contained in:
parent
fa50bbad9c
commit
19c25713c4
387 changed files with 4785 additions and 4109 deletions
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) => (
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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) => (
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 }} />;
|
||||
|
|
|
@ -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 }} />;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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")}`;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 }} />;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export default function Raw({ children }) {
|
||||
if (children.type === Raw) {
|
||||
return [children];
|
||||
return [children];
|
||||
}
|
||||
|
||||
return children;
|
||||
|
|
|
@ -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>;
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue