parent
c4333fd2dc
commit
8887fcc3ee
@ -0,0 +1,57 @@
|
||||
import useSWR from "swr";
|
||||
import { BiError } from "react-icons/bi";
|
||||
import { i18n, useTranslation } from "next-i18next";
|
||||
|
||||
import Node from "./node";
|
||||
|
||||
export default function Longhorn({ options }) {
|
||||
const { expanded, total, labels, include, nodes } = options;
|
||||
const { t } = useTranslation();
|
||||
const { data, error } = useSWR(`/api/widgets/longhorn`, {
|
||||
refreshInterval: 1500
|
||||
});
|
||||
|
||||
if (error || data?.error) {
|
||||
return (
|
||||
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
|
||||
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<div className="flex flex-col ml-3 text-left">
|
||||
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
|
||||
<div className="flex flex-row self-center flex-wrap justify-between" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
|
||||
<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;
|
||||
})
|
||||
.map((node) =>
|
||||
<div key={node.id}>
|
||||
<Node data={{ node }} expanded={expanded} labels={labels} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import { FiHardDrive } from "react-icons/fi";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import UsageBar from "../resources/usage-bar";
|
||||
|
||||
export default function Node({ data, expanded, labels }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
|
||||
<FiHardDrive className="text-theme-800 dark:text-theme-200 w-5 h-5" />
|
||||
<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.node.available })}</div>
|
||||
<div className="pr-1">{t("resources.free")}</div>
|
||||
</span>
|
||||
{expanded && (
|
||||
<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.node.maximum })}</div>
|
||||
<div className="pr-1">{t("resources.total")}</div>
|
||||
</span>
|
||||
)}
|
||||
<UsageBar percent={Math.round((data.node.available / data.node.maximum) * 100)} />
|
||||
</div>
|
||||
</div>
|
||||
{labels && (
|
||||
<div className="ml-6 pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{data.node.id}</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
import { httpProxy } from "../../../utils/proxy/http";
|
||||
import createLogger from "../../../utils/logger";
|
||||
import { getSettings } from "../../../utils/config/config";
|
||||
|
||||
const logger = createLogger("longhorn");
|
||||
|
||||
function parseLonghornData(data) {
|
||||
const json = JSON.parse(data);
|
||||
|
||||
if (!json) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nodes = json.data.map((node) => {
|
||||
let available = 0;
|
||||
let maximum = 0;
|
||||
let reserved = 0;
|
||||
let scheduled = 0;
|
||||
Object.keys(node.disks).forEach((diskKey) => {
|
||||
const disk = node.disks[diskKey];
|
||||
available += disk.storageAvailable;
|
||||
maximum += disk.storageMaximum;
|
||||
reserved += disk.storageReserved;
|
||||
scheduled += disk.storageScheduled;
|
||||
});
|
||||
return {
|
||||
id: node.id,
|
||||
available,
|
||||
maximum,
|
||||
reserved,
|
||||
scheduled,
|
||||
};
|
||||
});
|
||||
const total = nodes.reduce((summary, node) => ({
|
||||
available: summary.available + node.available,
|
||||
maximum: summary.maximum + node.maximum,
|
||||
reserved: summary.reserved + node.reserved,
|
||||
scheduled: summary.scheduled + node.scheduled,
|
||||
}));
|
||||
total.id = "total";
|
||||
nodes.push(total);
|
||||
return nodes;
|
||||
}
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const settings = getSettings();
|
||||
const longhornSettings = settings?.providers?.longhorn;
|
||||
const {url, username, password} = longhornSettings;
|
||||
|
||||
if (!url) {
|
||||
const errorMessage = "Missing Longhorn URL";
|
||||
logger.error(errorMessage);
|
||||
return res.status(400).json({ error: errorMessage });
|
||||
}
|
||||
|
||||
const apiUrl = `${url}/v1/nodes`;
|
||||
const headers = {
|
||||
"Accept-Encoding": "application/json"
|
||||
};
|
||||
if (username && password) {
|
||||
headers.Authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
|
||||
}
|
||||
const params = { method: "GET", headers };
|
||||
|
||||
const [status, contentType, data] = await httpProxy(apiUrl, params);
|
||||
|
||||
if (status === 401) {
|
||||
logger.error("Authorization failure getting data from Longhorn API. Data: %s", data);
|
||||
}
|
||||
|
||||
if (status !== 200) {
|
||||
logger.error("HTTP %d getting data from Longhorn API. Data: %s", status, data);
|
||||
}
|
||||
|
||||
if (contentType) res.setHeader("Content-Type", contentType);
|
||||
|
||||
const nodes = parseLonghornData(data);
|
||||
|
||||
return res.status(200).json({
|
||||
nodes,
|
||||
});
|
||||
}
|
Loading…
Reference in new issue