Merge branch 'main' into kubernetes

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

View File

@@ -94,7 +94,8 @@ export async function servicesResponse() {
].flat()),
];
const mergedGroups = [];
const sortedGroups = [];
const unsortedGroups = [];
const definedLayouts = initialSettings.layout ? Object.keys(initialSettings.layout) : null;
mergedGroupsNames.forEach((groupName) => {
@@ -113,12 +114,12 @@ export async function servicesResponse() {
if (definedLayouts) {
const layoutIndex = definedLayouts.findIndex(layout => layout === mergedGroup.name);
if (layoutIndex > -1) mergedGroups.splice(layoutIndex, 0, mergedGroup);
else mergedGroups.push(mergedGroup);
if (layoutIndex > -1) sortedGroups[layoutIndex] = mergedGroup;
else unsortedGroups.push(mergedGroup);
} else {
mergedGroups.push(mergedGroup);
unsortedGroups.push(mergedGroup);
}
});
return mergedGroups;
return [...sortedGroups.filter(g => g), ...unsortedGroups];
}

View File

@@ -32,5 +32,5 @@ export function getSettings() {
const settingsYaml = join(process.cwd(), "config", "settings.yaml");
const fileContents = readFileSync(settingsYaml, "utf8");
return yaml.load(fileContents);
return yaml.load(fileContents) ?? {};
}

View File

@@ -202,6 +202,7 @@ export function cleanServiceGroups(groups) {
container,
currency, // coinmarketcap widget
symbols,
defaultinterval
namespace, // kubernetes widget
app
} = cleanedService.widget;
@@ -215,6 +216,7 @@ export function cleanServiceGroups(groups) {
if (currency) cleanedService.widget.currency = currency;
if (symbols) cleanedService.widget.symbols = symbols;
if (defaultinterval) cleanedService.widget.defaultinterval = defaultinterval;
if (type === "docker") {
if (server) cleanedService.widget.server = server;
@@ -265,4 +267,4 @@ export default async function getServiceWidget(group, service) {
}
return false;
}
}

View File

@@ -4,10 +4,15 @@ import { format as utilFormat } from "node:util";
import winston from "winston";
import checkAndCopyConfig, { getSettings } from "utils/config/config";
let winstonLogger;
function init() {
const configPath = join(process.cwd(), "config");
checkAndCopyConfig("settings.yaml");
const settings = getSettings();
const logpath = settings.logpath || configPath;
function combineMessageAndSplat() {
return {
@@ -57,7 +62,7 @@ function init() {
winston.format.timestamp(),
winston.format.printf(messageFormatter)
),
filename: `${configPath}/logs/homepage.log`,
filename: `${logpath}/logs/homepage.log`,
handleExceptions: true,
handleRejections: true,
}),

View File

@@ -0,0 +1,82 @@
import { JSONRPCClient, JSONRPCErrorException } from "json-rpc-2.0";
import { formatApiCall } from "utils/proxy/api-helpers";
import { httpProxy } from "utils/proxy/http";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
import widgets from "widgets/widgets";
const logger = createLogger("jsonrpcProxyHandler");
export async function sendJsonRpcRequest(url, method, params, username, password) {
const headers = {
"content-type": "application/json",
"accept": "application/json"
}
if (username && password) {
headers.authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
}
const client = new JSONRPCClient(async (rpcRequest) => {
const body = JSON.stringify(rpcRequest);
const httpRequestParams = {
method: "POST",
headers,
body
};
// eslint-disable-next-line no-unused-vars
const [status, contentType, data] = await httpProxy(url, httpRequestParams);
if (status === 200) {
const json = JSON.parse(data.toString());
// in order to get access to the underlying error object in the JSON response
// you must set `result` equal to undefined
if (json.error && (json.result === null)) {
json.result = undefined;
}
return client.receive(json);
}
return Promise.reject(data?.error ? data : new Error(data.toString()));
});
try {
const response = await client.request(method, params);
return [200, "application/json", JSON.stringify(response)];
}
catch (e) {
if (e instanceof JSONRPCErrorException) {
logger.debug("Error calling JSONPRC endpoint: %s. %s", url, e.message);
return [200, "application/json", JSON.stringify({result: null, error: {code: e.code, message: e.message}})];
}
logger.warn("Error calling JSONPRC endpoint: %s. %s", url, e);
return [500, "application/json", JSON.stringify({result: null, error: {code: 2, message: e.toString()}})];
}
}
export default async function jsonrpcProxyHandler(req, res) {
const { group, service, endpoint: method } = req.query;
if (group && service) {
const widget = await getServiceWidget(group, service);
const api = widgets?.[widget.type]?.api;
if (!api) {
return res.status(403).json({ error: "Service does not support API calls" });
}
if (widget) {
const url = formatApiCall(api, { ...widget });
// eslint-disable-next-line no-unused-vars
const [status, contentType, data] = await sendJsonRpcRequest(url, method, null, widget.username, widget.password);
return res.status(status).end(data);
}
}
logger.debug("Invalid or missing proxy service type '%s' in group '%s'", service, group);
return res.status(400).json({ error: "Invalid proxy service type" });
}

View File

@@ -18,10 +18,15 @@ function addCookieHandler(url, params) {
};
}
export function httpsRequest(url, params) {
function handleRequest(requestor, url, params) {
return new Promise((resolve, reject) => {
addCookieHandler(url, params);
const request = https.request(url, params, (response) => {
if (params?.body) {
params.headers = params.headers ?? {};
params.headers['content-length'] = Buffer.byteLength(params.body);
}
const request = requestor.request(url, params, (response) => {
const data = [];
response.on("data", (chunk) => {
@@ -38,7 +43,7 @@ export function httpsRequest(url, params) {
reject([500, error]);
});
if (params.body) {
if (params?.body) {
request.write(params.body);
}
@@ -46,32 +51,12 @@ export function httpsRequest(url, params) {
});
}
export function httpsRequest(url, params) {
return handleRequest(https, url, params);
}
export function httpRequest(url, params) {
return new Promise((resolve, reject) => {
addCookieHandler(url, params);
const request = http.request(url, params, (response) => {
const data = [];
response.on("data", (chunk) => {
data.push(chunk);
});
response.on("end", () => {
addCookieToJar(url, response.headers);
resolve([response.statusCode, response.headers["content-type"], Buffer.concat(data), response.headers]);
});
});
request.on("error", (error) => {
reject([500, error]);
});
if (params.body) {
request.write(params.body);
}
request.end();
});
return handleRequest(http, url, params);
}
export async function httpProxy(url, params = {}) {
@@ -96,7 +81,7 @@ export async function httpProxy(url, params = {}) {
return [status, contentType, data, responseHeaders];
}
catch (err) {
logger.error("Error calling %s//%s%s...", url.protocol, url.hostname, url.pathname);
logger.error("Error calling %s//%s%s...", constructedUrl.protocol, constructedUrl.hostname, constructedUrl.pathname);
logger.error(err);
return [500, "application/json", { error: {message: err?.message ?? "Unknown error", url, rawError: err} }, null];
}

View File

@@ -0,0 +1,211 @@
import * as Icons from "react-icons/wi";
// see https://open-meteo.com/en/docs
const conditions = [
{
code: 1,
icon: {
day: Icons.WiDayCloudy,
night: Icons.WiNightAltCloudy,
},
},
{
code: 2,
icon: {
day: Icons.WiDayCloudy,
night: Icons.WiNightAltCloudy,
},
},
{
code: 3,
icon: {
day: Icons.WiDayCloudy,
night: Icons.WiNightAltCloudy,
},
},
{
code: 45,
icon: {
day: Icons.WiDayFog,
night: Icons.WiNightFog,
},
},
{
code: 48,
icon: {
day: Icons.WiDayFog,
night: Icons.WiNightFog,
},
},
{
code: 51,
icon: {
day: Icons.WiDaySprinkle,
night: Icons.WiNightAltSprinkle,
},
},
{
code: 53,
icon: {
day: Icons.WiDaySprinkle,
night: Icons.WiNightAltSprinkle,
},
},
{
code: 55,
icon: {
day: Icons.WiDaySprinkle,
night: Icons.WiNightAltSprinkle,
},
},
{
code: 56,
icon: {
day: Icons.WiDaySleet,
night: Icons.WiNightAltSleet,
},
},
{
code: 57,
icon: {
day: Icons.WiDaySleet,
night: Icons.WiNightAltSleet,
},
},
{
code: 61,
icon: {
day: Icons.WiDayShowers,
night: Icons.WiNightAltShowers,
},
},
{
code: 63,
icon: {
day: Icons.WiDayShowers,
night: Icons.WiNightAltShowers,
},
},
{
code: 65,
icon: {
day: Icons.WiDayShowers,
night: Icons.WiNightAltShowers,
},
},
{
code: 66,
icon: {
day: Icons.WiDaySleet,
night: Icons.WiNightAltSleet,
},
},
{
code: 67,
icon: {
day: Icons.WiDaySleet,
night: Icons.WiNightAltSleet,
},
},
{
code: 71,
icon: {
day: Icons.WiDaySnow,
night: Icons.WiNightAltSnow,
},
},
{
code: 73,
icon: {
day: Icons.WiDaySnow,
night: Icons.WiNightAltSnow,
},
},
{
code: 75,
icon: {
day: Icons.WiDaySnow,
night: Icons.WiNightAltSnow,
},
},
{
code: 77,
icon: {
day: Icons.WiDaySnow,
night: Icons.WiNightAltSnow,
},
},
{
code: 80,
icon: {
day: Icons.WiDaySnow,
night: Icons.WiNightAltSnow,
},
},
{
code: 81,
icon: {
day: Icons.WiDaySnow,
night: Icons.WiNightAltSnow,
},
},
{
code: 82,
icon: {
day: Icons.WiDaySnow,
night: Icons.WiNightAltSnow,
},
},
{
code: 85,
icon: {
day: Icons.WiDaySnow,
night: Icons.WiNightAltSnow,
},
},
{
code: 86,
icon: {
day: Icons.WiDaySnow,
night: Icons.WiNightAltSnow,
},
},
{
code: 95,
icon: {
day: Icons.WiDayThunderstorm,
night: Icons.WiNightAltThunderstorm,
},
},
{
code: 96,
icon: {
day: Icons.WiDayThunderstorm,
night: Icons.WiNightAltThunderstorm,
},
},
{
code: 99,
icon: {
day: Icons.WiDayThunderstorm,
night: Icons.WiNightAltThunderstorm,
},
},
];
export default function mapIcon(weatherStatusCode, timeOfDay) {
const mapping = conditions.find((condition) => condition.code === weatherStatusCode);
if (mapping) {
if (timeOfDay === "day") {
return mapping.icon.day;
}
if (timeOfDay === "night") {
return mapping.icon.night;
}
}
return Icons.WiDaySunny;
}