Merge branch 'main' into kubernetes

This commit is contained in:
James Wynn 2022-12-08 09:57:51 -06:00
commit 174cb651b4
81 changed files with 2717 additions and 342 deletions

View file

@ -107,18 +107,19 @@ export default function QuickLaunch({servicesAndBookmarks, searchString, setSear
function highlightText(text) {
const parts = text.split(new RegExp(`(${searchString})`, 'gi'));
return <span>{parts.map(part => part.toLowerCase() === searchString.toLowerCase() ? <span className="bg-theme-300/10">{part}</span> : part)}</span>;
// 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>;
}
return (
<div className={classNames(
"relative z-10 ease-in-out duration-300 transition-opacity",
"relative z-20 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-10 overflow-y-auto">
<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(
@ -147,7 +148,7 @@ export default function QuickLaunch({servicesAndBookmarks, searchString, setSear
}
</div>
</div>
<div className="text-xs text-theme-600 font-bold pointer-events-none">{r.abbr ? t("quicklaunch.bookmark") : t("quicklaunch.service")}</div>
<div className="text-xs text-theme-600 font-bold pointer-events-none">{r.type === 'service' ? t("quicklaunch.service") : t("quicklaunch.bookmark")}</div>
</button>
</li>
))}

View file

@ -3,6 +3,7 @@ import { useContext, useState } from "react";
import Status from "./status";
import Widget from "./widget";
import Ping from "./ping";
import KubernetesStatus from "./kubernetes-status";
import Docker from "widgets/docker/component";
@ -32,7 +33,7 @@ export default function Item({ service }) {
<div
className={`${
hasLink ? "cursor-pointer " : " "
}transition-all h-15 mb-3 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`}
}transition-all h-15 mb-3 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`}
>
<div className="flex select-none">
{service.icon &&
@ -72,26 +73,35 @@ export default function Item({ service }) {
</div>
)}
{service.container && (
<button
type="button"
onClick={() => (statsOpen ? closeStats() : setStatsOpen(true))}
className="flex-shrink-0 flex items-center justify-center w-12 cursor-pointer"
>
<Status service={service} />
<span className="sr-only">View container stats</span>
</button>
)}
{service.app && (
<button
type="button"
onClick={() => (statsOpen ? closeStats() : setStatsOpen(true))}
className="flex-shrink-0 flex items-center justify-center w-12 cursor-pointer"
>
<KubernetesStatus service={service} />
<span className="sr-only">View container stats</span>
</button>
)}
<div className="absolute top-0 right-0 w-1/2 flex flex-row justify-end gap-2 mr-2">
{service.ping && (
<div className="flex-shrink-0 flex items-center justify-center cursor-pointer">
<Ping service={service} />
<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"
>
<Status service={service} />
<span className="sr-only">View container stats</span>
</button>
)}
{service.app && (
<button
type="button"
onClick={() => (statsOpen ? closeStats() : setStatsOpen(true))}
className="flex-shrink-0 flex items-center justify-center w-12 cursor-pointer"
>
<KubernetesStatus service={service} />
<span className="sr-only">View container stats</span>
</button>
)}
</div>
</div>
{service.container && service.server && (

View file

@ -0,0 +1,44 @@
import { useTranslation } from "react-i18next";
import useSWR from "swr";
export default function Ping({ service }) {
const { t } = useTranslation();
const { data, error } = useSWR(`/api/ping?${new URLSearchParams({ping: service.ping}).toString()}`, {
refreshInterval: 30000
});
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="text-[8px] font-bold text-rose-500 uppercase">{t("ping.error")}</div>
</div>
);
}
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="text-[8px] font-bold text-black/20 dark:text-white/40 uppercase">{t("ping.ping")}</div>
</div>
);
}
const statusText = `${service.ping}: HTTP status ${data.status}`;
if (data && data.status !== 200) {
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="text-[8px] font-bold text-rose-500/80">{data.status}</div>
</div>
);
}
if (data && data.status === 200) {
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="text-[8px] font-bold text-emerald-500/80">{t("common.ms", { value: data.latency, style: "unit", unit: "millisecond", unitDisplay: "narrow", maximumFractionDigits: 0 })}</div>
</div>
);
}
}

View file

@ -1,19 +1,52 @@
import { useTranslation } from "react-i18next";
import useSWR from "swr";
export default function Status({ service }) {
const { t } = useTranslation();
const { data, error } = useSWR(`/api/docker/status/${service.container}/${service.server || ""}`);
if (error) {
return <div className="w-3 h-3 bg-rose-300 dark:bg-rose-500 rounded-full" />;
<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="text-[8px] font-bold text-rose-500/80 uppercase">{t("docker.error")}</div>
</div>
}
if (data && data.status === "running") {
return <div className="w-3 h-3 bg-emerald-300 dark:bg-emerald-500 rounded-full" />;
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={data.health}>
<div className="text-[8px] font-bold text-blue-500/80 uppercase">{data.health}</div>
</div>
);
}
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={data.health}>
<div className="text-[8px] font-bold text-orange-400/50 dark:text-orange-400/80 uppercase">{data.health}</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={data.health ?? data.status}>
<div className="text-[8px] font-bold text-emerald-500/80 uppercase">{data.health ?? data.status}</div>
</div>
);
}
if (data && data.status === "not found") {
return <div className="h-2.5 w-2.5 bg-orange-400/50 dark:bg-yellow-200/40 -rotate-45" />;
if (data && (data.status === "not found" || data.status === "exited")) {
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="text-[8px] font-bold text-orange-400/50 dark:text-orange-400/80 uppercase">{data.status}</div>
</div>
);
}
return <div className="w-3 h-3 bg-black/20 dark:bg-white/40 rounded-full" />;
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="text-[8px] font-bold text-black/20 dark:text-white/40 uppercase">{t("docker.unknown")}</div>
</div>
);
}

View file

@ -8,9 +8,9 @@ import cachedFetch from "utils/proxy/cached-fetch";
export default function Version() {
const { t, i18n } = useTranslation();
const buildTime = process.env.NEXT_PUBLIC_BUILDTIME ?? new Date().toISOString();
const revision = process.env.NEXT_PUBLIC_REVISION ?? "dev";
const version = process.env.NEXT_PUBLIC_VERSION ?? "dev";
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 cachedFetcher = (resource) => cachedFetch(resource, 5).then((res) => res.json());
@ -36,17 +36,14 @@ export default function Version() {
{version} ({revision.substring(0, 7)}, {formatDate(buildTime)})
</>
) : (
releaseData &&
compareVersions(latestRelease.tag_name, version) > 0 && (
<a
href={`https://github.com/benphelps/homepage/releases/tag/${version}`}
target="_blank"
rel="noopener noreferrer"
className="ml-2 text-xs text-theme-500 dark:text-theme-400 flex flex-row items-center"
>
{version} ({revision.substring(0, 7)}, {formatDate(buildTime)})
</a>
)
<a
href={`https://github.com/benphelps/homepage/releases/tag/${version}`}
target="_blank"
rel="noopener noreferrer"
className="ml-2 text-xs text-theme-500 dark:text-theme-400 flex flex-row items-center"
>
{version} ({revision.substring(0, 7)}, {formatDate(buildTime)})
</a>
)}
</span>
{version === "main" || version === "dev" || version === "nightly"

View file

@ -15,22 +15,21 @@ const textSizes = {
export default function DateTime({ options }) {
const { text_size: textSize, format } = options;
const { i18n } = useTranslation();
const [date, setDate] = useState(new Date());
const [date, setDate] = useState("");
useEffect(() => {
const dateFormat = new Intl.DateTimeFormat(i18n.language, { ...format });
const interval = setInterval(() => {
setDate(new Date());
setDate(dateFormat.format(new Date()));
}, 1000);
return () => clearInterval(interval);
}, [setDate]);
const dateFormat = new Intl.DateTimeFormat(i18n.language, { ...format });
}, [date, setDate, i18n.language, format]);
return (
<div className="flex flex-col justify-center first:ml-0 ml-4">
<div className="flex flex-row items-center grow justify-end">
<span className={`text-theme-800 dark:text-theme-200 ${textSizes[textSize || "lg"]}`}>
{dateFormat.format(date)}
{date}
</span>
</div>
</div>

View file

@ -1,4 +1,4 @@
import mapIcon from "utils/weather/owm-condition-map";
import mapIcon from "utils/weather/openmeteo-condition-map";
export default function Icon({ condition, timeOfDay }) {
const IconComponent = mapIcon(condition, timeOfDay);

View file

@ -52,7 +52,7 @@ export default function Memory({ expanded }) {
<div className="flex flex-col ml-3 text-left min-w-[85px]">
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5">
{t("common.bytes", { value: data.memory.freeMemMb * 1024 * 1024, maximumFractionDigits: 0, binary: true })}
{t("common.bytes", { value: data.memory.freeMemMb * 1024 * 1024, maximumFractionDigits: 1, binary: true })}
</div>
<div className="pr-1">{t("resources.free")}</div>
</span>
@ -61,7 +61,7 @@ export default function Memory({ expanded }) {
<div className="pl-0.5">
{t("common.bytes", {
value: data.memory.totalMemMb * 1024 * 1024,
maximumFractionDigits: 0,
maximumFractionDigits: 1,
binary: true,
})}
</div>