Custom JS and CSS (#1950)

* First commit for custom styles and JS

* Adjusted classes

* Added ids and classes for services and bookmarks

* Apply suggestions from code review

* Remove mime dependency

* Update settings.json

* Detect custom css / js changes, no refresh

* Added preload to custom scripts and styles so they can load earlier

* Added data attribute name for bookmarks too

* Update [path].js

* code style, revert some pointer changes

---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
pull/1970/head
TheRolf 8 months ago committed by GitHub
parent 0741ef0427
commit b39c79bea1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -13,6 +13,7 @@ export default function BookmarksGroup({ bookmarks, layout, disableCollapse }) {
<div
key={bookmarks.name}
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"
)}
@ -23,11 +24,11 @@ export default function BookmarksGroup({ bookmarks, layout, disableCollapse }) {
{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">
<div className="flex-shrink-0 mr-2 w-7 h-7 bookmark-group-icon">
<ResolvedIcon icon={layout.icon} />
</div>
)}
<h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium">{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" : "",

@ -9,7 +9,7 @@ export default function Item({ bookmark }) {
const { settings } = useContext(SettingsContext);
return (
<li key={bookmark.name}>
<li key={bookmark.name} id={bookmark.id} className="bookmark" data-name={bookmark.name}>
<a
href={bookmark.href}
title={bookmark.name}
@ -20,7 +20,7 @@ export default function Item({ bookmark }) {
)}
>
<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">
<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 &&
<div className="flex-shrink-0 w-5 h-5">
<ResolvedIcon icon={bookmark.icon} alt={bookmark.abbr} />
@ -28,9 +28,9 @@ export default function Item({ bookmark }) {
}
{!bookmark.icon && bookmark.abbr}
</div>
<div className="flex-1 flex items-center justify-between rounded-r-md">
<div className="flex-1 grow pl-3 py-2 text-xs">{bookmark.name}</div>
<div className="px-2 py-2 truncate text-theme-500 dark:text-theme-300 text-xs">{hostname}</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-hostname">{hostname}</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"
"mt-3 bookmark-list"
)}
>
{bookmarks.map((bookmark) => (

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

@ -14,6 +14,7 @@ export default function ServicesGroup({ group, services, layout, fiveColumns, di
<div
key={services.name}
className={classNames(
"services-group",
layout?.style === "row" ? "basis-full" : "basis-full md:basis-1/2 lg:basis-1/3 xl:basis-1/4",
layout?.style !== "row" && fiveColumns ? "3xl:basis-1/5" : "",
layout?.header === false ? "flex-1 px-1 -my-1" : "flex-1 p-1",
@ -25,11 +26,11 @@ export default function ServicesGroup({ group, services, layout, fiveColumns, di
{ 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">
<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">{services.name}</h2>
<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',

@ -29,28 +29,30 @@ export default function Item({ service, group }) {
}
};
return (
<li key={service.name}>
<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}`,
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'
"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">
<div className="flex select-none z-0 service-title">
{service.icon &&
(hasLink ? (
<a
href={service.href}
target={service.target ?? settings.target ?? "_blank"}
rel="noreferrer"
className="flex-shrink-0 flex items-center justify-center w-12 "
className="flex-shrink-0 flex items-center justify-center w-12 service-icon"
>
<ResolvedIcon icon={service.icon} />
</a>
) : (
<div className="flex-shrink-0 flex items-center justify-center w-12 ">
<div className="flex-shrink-0 flex items-center justify-center w-12 service-icon">
<ResolvedIcon icon={service.icon} />
</div>
))}
@ -60,25 +62,25 @@ export default function Item({ service, group }) {
href={service.href}
target={service.target ?? settings.target ?? "_blank"}
rel="noreferrer"
className="flex-1 flex items-center justify-between rounded-r-md "
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">
<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}</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 ">
<div className="flex-1 px-2 py-2 text-sm text-left z-10">
<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}</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 w-1/2 flex flex-row justify-end gap-2 mr-2 z-30 pointer-events-none">
<div className="absolute top-0 right-0 flex flex-row justify-end gap-2 mr-2 z-30 pointer-events-none service-tags">
{service.ping && (
<div className="flex-shrink-0 flex items-center justify-center cursor-pointer">
<div className="flex-shrink-0 flex items-center justify-center service-tag service-ping">
<Ping group={group} service={service.name} />
<span className="sr-only">Ping status</span>
</div>
@ -88,7 +90,7 @@ export default function Item({ service, group }) {
<button
type="button"
onClick={() => (statsOpen ? closeStats() : setStatsOpen(true))}
className="flex-shrink-0 flex items-center justify-center cursor-pointer"
className="flex-shrink-0 flex items-center justify-center cursor-pointer service-tag service-container-stats"
>
<Status service={service} />
<span className="sr-only">View container stats</span>
@ -98,7 +100,7 @@ export default function Item({ service, group }) {
<button
type="button"
onClick={() => (statsOpen ? closeStats() : setStatsOpen(true))}
className="flex-shrink-0 flex items-center justify-center cursor-pointer"
className="flex-shrink-0 flex items-center justify-center cursor-pointer service-tag service-app"
>
<KubernetesStatus service={service} />
<span className="sr-only">View container stats</span>
@ -111,7 +113,7 @@ 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"
"w-full overflow-hidden transition-all duration-300 ease-in-out service-stats"
)}
>
{(showStats || statsOpen) && <Docker service={{ widget: { container: service.container, server: service.server } }} />}
@ -121,7 +123,7 @@ export default function Item({ service, group }) {
<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"
"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 } }} />}

@ -6,14 +6,14 @@ export default function KubernetesStatus({ service }) {
const { data, error } = useSWR(`/api/kubernetes/status/${service.namespace}/${service.app}?${podSelectorString}`);
if (error) {
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={t("docker.error")}>
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden k8s-status-error" title={t("docker.error")}>
<div className="text-[8px] font-bold text-rose-500/80 uppercase">{t("docker.error")}</div>
</div>
}
if (data && data.status === "running") {
return (
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={data.health ?? data.status}>
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden k8s-status" title={data.health ?? data.status}>
<div className="text-[8px] font-bold text-emerald-500/80 uppercase">{data.health ?? data.status}</div>
</div>
);
@ -21,14 +21,14 @@ export default function KubernetesStatus({ service }) {
if (data && (data.status === "not found" || data.status === "down" || data.status === "partial")) {
return (
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={data.status}>
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden k8s-status-warning" title={data.status}>
<div className="text-[8px] font-bold text-orange-400/50 dark:text-orange-400/80 uppercase">{data.status}</div>
</div>
);
}
return (
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden">
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden k8s-status-unknown">
<div className="text-[8px] font-bold text-black/20 dark:text-white/40 uppercase">{t("docker.unknown")}</div>
</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"
"mt-3 services-list"
)}
>
{services.map((service) => (

@ -9,7 +9,7 @@ export default function Ping({ group, service }) {
if (error) {
return (
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden">
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden ping-error">
<div className="text-[8px] font-bold text-rose-500 uppercase">{t("ping.error")}</div>
</div>
);
@ -17,7 +17,7 @@ export default function Ping({ group, service }) {
if (!data) {
return (
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden">
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden ping-ping">
<div className="text-[8px] font-bold text-black/20 dark:text-white/40 uppercase">{t("ping.ping")}</div>
</div>
);
@ -27,14 +27,14 @@ export default function Ping({ group, service }) {
if (data.status > 403) {
return (
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={statusText}>
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden ping-status-invalid" title={statusText}>
<div className="text-[8px] font-bold text-rose-500/80">{data.status}</div>
</div>
);
}
return (
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={statusText}>
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden ping-status-valid" title={statusText}>
<div className="text-[8px] font-bold text-emerald-500/80">{t("common.ms", { value: data.latency, style: "unit", unit: "millisecond", maximumFractionDigits: 0 })}</div>
</div>
);

@ -7,7 +7,7 @@ export default function Status({ service }) {
const { data, error } = useSWR(`/api/docker/status/${service.container}/${service.server || ""}`);
if (error) {
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={t("docker.error")}>
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden docker-error" title={t("docker.error")}>
<div className="text-[8px] font-bold text-rose-500/80 uppercase">{t("docker.error")}</div>
</div>
}
@ -18,7 +18,7 @@ export default function Status({ service }) {
if (data.status?.includes("running")) {
if (data.health === "starting") {
return (
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={t("docker.starting")}>
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden docker-starting" title={t("docker.starting")}>
<div className="text-[8px] font-bold text-blue-500/80 uppercase">{t("docker.starting")}</div>
</div>
);
@ -26,7 +26,7 @@ export default function Status({ service }) {
if (data.health === "unhealthy") {
return (
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={t("docker.unhealthy")}>
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden docker-unhealthy" title={t("docker.unhealthy")}>
<div className="text-[8px] font-bold text-orange-400/50 dark:text-orange-400/80 uppercase">{t("docker.unhealthy")}</div>
</div>
);
@ -39,7 +39,7 @@ export default function Status({ service }) {
}
return (
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={statusLabel}>
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden docker-status" title={statusLabel}>
<div className="text-[8px] font-bold text-emerald-500/80 uppercase">{statusLabel}</div>
</div>
);
@ -50,7 +50,7 @@ export default function Status({ service }) {
else if (data.status === "exited") statusLabel = t("docker.exited")
else statusLabel = data.status.replace("partial", t("docker.partial"))
return (
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={statusLabel}>
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden docker-status-warning" title={statusLabel}>
<div className="text-[8px] font-bold text-orange-400/50 dark:text-orange-400/80 uppercase">{statusLabel}</div>
</div>
);
@ -58,7 +58,7 @@ export default function Status({ service }) {
}
return (
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden">
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden docker-status-unknown">
<div className="text-[8px] font-bold text-black/20 dark:text-white/40 uppercase">{t("docker.unknown")}</div>
</div>
);

@ -17,7 +17,7 @@ export default function Widget({ service }) {
}
return (
<div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1">
<div className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-col items-center justify-center p-1 service-missing">
<div className="font-thin text-sm">{t("widget.missing_type", { type: service.widget.type })}</div>
</div>
);

@ -8,7 +8,8 @@ export default function Block({ value, label }) {
<div
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" : ""
value === undefined ? "animate-pulse" : "",
"service-block"
)}
>
<div className="font-thin text-sm">{value === undefined || value === null ? "-" : value}</div>

@ -36,5 +36,5 @@ export default function Container({ error = false, children, service }) {
}));
}
return <div className="relative flex flex-row w-full">{visibleChildren}</div>;
return <div className="relative flex flex-row w-full service-container">{visibleChildren}</div>;
}

@ -38,7 +38,7 @@ export default function ColorToggle() {
}
return (
<div className="w-full self-center">
<div id="color" className="w-full self-center">
<Popover className="relative flex items-center">
<Popover.Button className="outline-none">
<IoColorPalette

@ -10,7 +10,7 @@ export default function Revalidate() {
};
return (
<div className="rounded-full flex align-middle self-center mr-3">
<div id="revalidate" className="rounded-full flex align-middle self-center mr-3">
<MdRefresh onClick={() => revalidate()} className="text-theme-800 dark:text-theme-200 w-6 h-6 cursor-pointer" />
</div>
);

@ -11,7 +11,7 @@ export default function ThemeToggle() {
}
return (
<div className="rounded-full flex self-end">
<div id="theme" className="rounded-full flex self-end">
<MdLightMode className="text-theme-800 dark:text-theme-200 w-5 h-5 m-1.5" />
{theme === "dark" ? (
<MdToggleOn

@ -25,7 +25,7 @@ export default function Version() {
const latestRelease = releaseData?.[0];
return (
<div className="flex flex-row items-center">
<div id="version" className="flex flex-row items-center">
<span className="text-xs text-theme-500 dark:text-theme-400">
{version === "main" || version === "dev" || version === "nightly" ? (
<>

@ -30,7 +30,7 @@ export default function DateTime({ options }) {
}, [date, setDate, dateLocale, format]);
return (
<Container options={options}>
<Container options={options} additionalClassNames="information-widget-datetime">
<Raw>
<div className="flex flex-row items-center grow justify-end">
<span className={`text-theme-800 dark:text-theme-200 tabular-nums ${textSizes[textSize || "lg"]}`}>

@ -3,6 +3,7 @@ import { useContext } from "react";
import { FaMemory, FaRegClock, FaThermometerHalf } from "react-icons/fa";
import { FiCpu, FiHardDrive } from "react-icons/fi";
import { useTranslation } from "next-i18next";
import classNames from "classnames";
import Error from "../widget/error";
import Resource from "../widget/resource";
@ -32,7 +33,7 @@ export default function Widget({ options }) {
}
if (!data) {
return <Resources options={options}>
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" /> }
@ -69,8 +70,10 @@ 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 })
return (
<Resources options={options} target={settings.target ?? "_blank"}>
<Resources options={options} target={settings.target ?? "_blank"} additionalClassNames={addedClasses}>
{options.cpu !== false && <Resource
icon={FiCpu}
value={t("common.number", {

@ -14,7 +14,7 @@ const textSizes = {
export default function Greeting({ options }) {
if (options.text) {
return <Container options={options}>
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}

@ -36,7 +36,7 @@ export default function Widget({ options }) {
}
if (!data) {
return <Container options={options}>
return <Container options={options} additionalClassNames="information-widget-kubernetes">
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between">
{cluster.show &&
@ -50,7 +50,7 @@ export default function Widget({ options }) {
</Container>;
}
return <Container options={options}>
return <Container options={options} additionalClassNames="information-widget-kubernetes">
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between">
{cluster.show &&

@ -5,14 +5,14 @@ import ResolvedIcon from "components/resolvedicon"
export default function Logo({ options }) {
return (
<Container options={options}>
<Container options={options} additionalClassNames={`information-widget-logo ${ options.icon ? 'resolved' : 'fallback'}`}>
<Raw>
{options.icon ?
<div className="mr-3">
<div className="resolved mr-3">
<ResolvedIcon icon={options.icon} width={48} height={48} />
</div> :
// fallback to homepage logo
<div className="w-12 h-12">
<div className="fallback w-12 h-12">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1024 1024"

@ -17,14 +17,14 @@ export default function Longhorn({ options }) {
}
if (!data) {
return <Container options={options}>
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}>
return <Container options={options} additionalClassNames="infomation-widget-longhorn">
<Raw>
<div className="flex flex-row self-center flex-wrap justify-between">
{data.nodes

@ -8,6 +8,7 @@ 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")}

@ -24,7 +24,7 @@ function Widget({ options }) {
}
if (!data) {
return <Container options={options}>
return <Container options={options} additionalClassNames="information-widget-openmeteo">
<PrimaryText>{t("weather.updating")}</PrimaryText>
<SecondaryText>{t("weather.wait")}</SecondaryText>
<WidgetIcon icon={WiCloudDown} size="l" />
@ -35,7 +35,7 @@ function Widget({ options }) {
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";
return <Container options={options}>
return <Container options={options} additionalClassNames="information-widget-openmeteo">
<PrimaryText>
{options.label && `${options.label}, `}
{t("common.number", {
@ -81,7 +81,7 @@ export default function OpenMeteo({ options }) {
// if (!requesting && !location) requestLocation();
if (!location) {
return <ContainerButton options={options} callback={requestLocation} >
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 />

@ -24,7 +24,7 @@ function Widget({ options }) {
}
if (!data) {
return <Container options={options}>
return <Container options={options} additionalClassNames="information-widget-openweathermap">
<PrimaryText>{t("weather.updating")}</PrimaryText>
<SecondaryText>{t("weather.wait")}</SecondaryText>
<WidgetIcon icon={WiCloudDown} size="l" />
@ -36,7 +36,7 @@ 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}>
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" />

@ -1,6 +1,6 @@
export default function UsageBar({ percent }) {
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">
<div className={`mt-0.5 w-full bg-theme-800/30 rounded-full h-1 dark:bg-theme-200/20 ${additionalClassNames}`}>
<div
className="bg-theme-800/70 h-1 rounded-full dark:bg-theme-200/50 transition-all duration-1000"
style={{

@ -103,7 +103,7 @@ export default function Search({ options }) {
localStorage.setItem(localStorageKey, provider.name);
}
return <ContainerForm options={options} callback={submitCallback} additionalClassNames="grow" >
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" />

@ -25,7 +25,7 @@ export default function Widget({ options }) {
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}>
return <Container options={options} additionalClassNames="information-widget-unifi-console">
<PrimaryText>{t("unifi.wait")}</PrimaryText>
<WidgetIcon icon={SiUbiquiti} />
</Container>;
@ -43,7 +43,7 @@ export default function Widget({ options }) {
const dataEmpty = !(wan.show || lan.show || wlan.show || uptime);
return <Container options={options}>
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">

@ -24,7 +24,7 @@ function Widget({ options }) {
}
if (!data) {
return <Container options={options}>
return <Container options={options} additionalClassNames="information-widget-weather">
<PrimaryText>{t("weather.updating")}</PrimaryText>
<SecondaryText>{t("weather.wait")}</SecondaryText>
<WidgetIcon icon={WiCloudDown} size="l" />
@ -35,7 +35,7 @@ function Widget({ options }) {
const condition = data.current.condition.code;
const timeOfDay = data.current.is_day ? "day" : "night";
return <Container options={options}>
return <Container options={options} additionalClassNames="information-widget-weather">
<PrimaryText>
{options.label && `${options.label}, `}
{t("common.number", {

@ -35,9 +35,9 @@ export function getAllClasses(options, 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">
<div className="flex flex-col items-center">{children.find(child => child.type === WidgetIcon)}</div>
<div className="flex flex-col ml-3 text-left">
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>
@ -54,7 +54,7 @@ export function getBottomBlock(children) {
export default function Container({ children = [], options, additionalClassNames = '' }) {
return (
<div className={getAllClasses(options, additionalClassNames)}>
<div className={getAllClasses(options, `${ additionalClassNames } widget-container`)}>
{getInnerBlock(children)}
{getBottomBlock(children)}
</div>

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

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

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

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

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

@ -1,11 +1,11 @@
import UsageBar from "../resources/usage-bar";
export default function Resource({ children, icon, value, label, expandedValue = "", expandedLabel = "", percentage, expanded = false }) {
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">
<Icon className="text-theme-800 dark:text-theme-200 w-5 h-5"/>
<div className="flex flex-col ml-3 text-left min-w-[85px]">
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>
@ -15,7 +15,7 @@ export default function Resource({ children, icon, value, label, expandedValue =
<div className="pr-1">{expandedLabel}</div>
</div>
}
{ percentage >= 0 && <UsageBar percent={percentage} /> }
{ percentage >= 0 && <UsageBar percent={percentage} additionalClassNames="resource-usage" /> }
{ children }
</div>
</div>;

@ -1,12 +1,15 @@
import classNames from "classnames";
import ContainerLink from "./container_link";
import Resource from "./resource";
import Raw from "./raw";
import WidgetLabel from "./widget_label";
export default function Resources({ options, children, target }) {
export default function Resources({ options, children, target, additionalClassNames }) {
const widgetParts = [].concat(...children);
const addedClassNames = classNames('information-widget-resources', additionalClassNames);
return <ContainerLink options={options} target={target}>
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) }

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

@ -1,6 +1,6 @@
export default function WidgetIcon({ icon, size = "s", pulse = false }) {
const Icon = icon;
let additionalClasses = "text-theme-800 dark:text-theme-200 ";
let additionalClasses = "information-widget-icon text-theme-800 dark:text-theme-200 ";
switch (size) {
case "m": additionalClasses += "w-6 h-6 "; break;

@ -1,3 +1,3 @@
export default function WidgetLabel({ label = "" }) {
return <div className="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>
}

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

@ -4,7 +4,7 @@ import { readFileSync } from "fs";
import checkAndCopyConfig, { CONF_DIR } from "utils/config/config";
const configs = ["docker.yaml", "settings.yaml", "services.yaml", "bookmarks.yaml", "widgets.yaml"];
const configs = ["docker.yaml", "settings.yaml", "services.yaml", "bookmarks.yaml", "widgets.yaml", "custom.css", "custom.js"];
function hash(buffer) {
const hashSum = createHash("sha256");

@ -8,6 +8,7 @@ import { useEffect, useContext, useState, useMemo } from "react";
import { BiError } from "react-icons/bi";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import FileContent from "components/filecontent";
import ServicesGroup from "components/services/group";
import BookmarksGroup from "components/bookmarks/group";
import Widget from "components/widgets/widget";
@ -239,7 +240,7 @@ function Home({ initialSettings }) {
const bookmarkGroups = bookmarks.filter(group => settings.layout?.[group.name] === undefined);
return <>
{layoutGroups.length > 0 && <div key="layoutGroups" className="flex flex-wrap p-4 sm:p-8 sm:pt-4 items-start pb-2">
{layoutGroups.length > 0 && <div key="layoutGroups" id="layout-groups" className="flex flex-wrap p-4 sm:p-8 sm:pt-4 items-start pb-2">
{layoutGroups.map((group) => (
group.services ?
(<ServicesGroup
@ -259,7 +260,7 @@ function Home({ initialSettings }) {
)
)}
</div>}
{serviceGroups?.length > 0 && <div key="services" className="flex flex-wrap p-4 sm:p-8 sm:pt-4 items-start pb-2">
{serviceGroups?.length > 0 && <div key="services" id="services" className="flex flex-wrap p-4 sm:p-8 sm:pt-4 items-start pb-2">
{serviceGroups.map((group) => (
<ServicesGroup
key={group.name}
@ -271,7 +272,7 @@ function Home({ initialSettings }) {
/>
))}
</div>}
{bookmarkGroups?.length > 0 && <div key="bookmarks" className="flex flex-wrap p-4 sm:p-8 sm:pt-4 items-start pb-2">
{bookmarkGroups?.length > 0 && <div key="bookmarks" id="bookmarks" className="flex flex-wrap p-4 sm:p-8 sm:pt-4 items-start pb-2">
{bookmarkGroups.map((group) => (
<BookmarksGroup
key={group.name}
@ -311,6 +312,24 @@ function Home({ initialSettings }) {
<meta name="msapplication-TileColor" content={themes[settings.color || "slate"][settings.theme || "dark"]} />
<meta name="theme-color" content={themes[settings.color || "slate"][settings.theme || "dark"]} />
</Head>
<link rel="preload" href="/api/config/custom.css" as="fetch" crossorigin="anonymous" />
<style data-name="custom.css">
<FileContent path="custom.css"
loadingValue="/* Loading custom CSS... */"
errorValue="/* Failed to load custom CSS... */"
emptyValue="/* No custom CSS */"
/>
</style>
<link rel="preload" href="/api/config/custom.js" as="fetch" crossorigin="anonymous" />
<script data-name="custom.js">
<FileContent path="custom.js"
loadingValue="/* Loading custom JS... */"
errorValue="/* Failed to load custom JS... */"
emptyValue="/* No custom JS */"
/>
</script>
<div className="relative container m-auto flex flex-col justify-start z-10 h-full">
<QuickLaunch
servicesAndBookmarks={servicesAndBookmarks}
@ -321,6 +340,7 @@ function Home({ initialSettings }) {
searchProvider={settings.quicklaunch?.hideInternetSearch ? null : searchProvider}
/>
<div
id="information-widgets"
className={classNames(
"flex flex-row flex-wrap justify-between",
headerStyles[headerStyle],
@ -335,7 +355,7 @@ function Home({ initialSettings }) {
<Widget key={i} widget={widget} style={{ header: headerStyle, isRightAligned: false, cardBlur: settings.cardBlur }} />
))}
<div className={classNames(
<div id="information-widgets-right" className={classNames(
"m-auto flex flex-wrap grow sm:basis-auto justify-between md:justify-end",
headerStyle === "boxedWidgets" ? "sm:ml-4" : "sm:ml-2"
)}>
@ -351,14 +371,14 @@ function Home({ initialSettings }) {
{servicesAndBookmarksGroups}
<div className="flex flex-col mt-auto p-8 w-full">
<div className="flex w-full justify-end">
<div id="footer" className="flex flex-col mt-auto p-8 w-full">
<div id="style" className="flex w-full justify-end">
{!settings?.color && <ColorToggle />}
<Revalidate />
{!settings.theme && <ThemeToggle />}
</div>
<div className="flex mt-4 w-full justify-end">
<div id="version" className="flex mt-4 w-full justify-end">
{!settings.hideVersion && <Version />}
</div>
</div>

Loading…
Cancel
Save