mirror of
https://github.com/karl0ss/homepage.git
synced 2025-05-01 13:09:31 +01:00
Feature: nested groups (#4346)
This commit is contained in:
parent
907abee1aa
commit
be8363cc35
@ -3,12 +3,13 @@ import classNames from "classnames";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import { MdKeyboardArrowDown } from "react-icons/md";
|
||||
|
||||
import { columnMap } from "../../utils/layout/columns";
|
||||
|
||||
import List from "components/services/list";
|
||||
import ResolvedIcon from "components/resolvedicon";
|
||||
|
||||
export default function ServicesGroup({
|
||||
group,
|
||||
services,
|
||||
layout,
|
||||
fiveColumns,
|
||||
disableCollapse,
|
||||
@ -23,7 +24,7 @@ export default function ServicesGroup({
|
||||
|
||||
return (
|
||||
<div
|
||||
key={services.name}
|
||||
key={group.name}
|
||||
className={classNames(
|
||||
"services-group",
|
||||
layout?.style === "row" ? "basis-full" : "basis-full md:basis-1/2 lg:basis-1/3 xl:basis-1/4",
|
||||
@ -42,7 +43,7 @@ export default function ServicesGroup({
|
||||
</div>
|
||||
)}
|
||||
<h2 className="flex text-theme-800 dark:text-theme-300 text-xl font-medium service-group-name">
|
||||
{services.name}
|
||||
{group.name}
|
||||
</h2>
|
||||
<MdKeyboardArrowDown
|
||||
className={classNames(
|
||||
@ -74,7 +75,31 @@ export default function ServicesGroup({
|
||||
}}
|
||||
>
|
||||
<Disclosure.Panel className="transition-all overflow-hidden duration-300 ease-out" ref={panel} static>
|
||||
<List group={group} services={services.services} layout={layout} useEqualHeights={useEqualHeights} />
|
||||
<List
|
||||
groupName={group.name}
|
||||
services={group.services}
|
||||
layout={layout}
|
||||
useEqualHeights={useEqualHeights}
|
||||
/>
|
||||
{group.groups?.length > 0 && (
|
||||
<div
|
||||
className={`grid ${
|
||||
layout?.style === "row" ? `grid ${columnMap[layout?.columns]} gap-x-2` : "flex flex-col"
|
||||
} gap-2`}
|
||||
>
|
||||
{group.groups.map((subgroup) => (
|
||||
<ServicesGroup
|
||||
key={subgroup.name}
|
||||
group={subgroup}
|
||||
layout={layout?.[subgroup.name]}
|
||||
fiveColumns={fiveColumns}
|
||||
disableCollapse={disableCollapse}
|
||||
useEqualHeights={useEqualHeights}
|
||||
groupsInitiallyCollapsed={groupsInitiallyCollapsed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
|
@ -12,7 +12,7 @@ import Kubernetes from "widgets/kubernetes/component";
|
||||
import { SettingsContext } from "utils/contexts/settings";
|
||||
import ResolvedIcon from "components/resolvedicon";
|
||||
|
||||
export default function Item({ service, group, useEqualHeights }) {
|
||||
export default function Item({ service, groupName, useEqualHeights }) {
|
||||
const hasLink = service.href && service.href !== "#";
|
||||
const { settings } = useContext(SettingsContext);
|
||||
const showStats = service.showStats === false ? false : settings.showStats;
|
||||
@ -90,14 +90,14 @@ export default function Item({ service, group, useEqualHeights }) {
|
||||
>
|
||||
{service.ping && (
|
||||
<div className="flex-shrink-0 flex items-center justify-center service-tag service-ping">
|
||||
<Ping group={group} service={service.name} style={statusStyle} />
|
||||
<Ping groupName={groupName} serviceName={service.name} style={statusStyle} />
|
||||
<span className="sr-only">Ping status</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{service.siteMonitor && (
|
||||
<div className="flex-shrink-0 flex items-center justify-center service-tag service-site-monitor">
|
||||
<SiteMonitor group={group} service={service.name} style={statusStyle} />
|
||||
<SiteMonitor groupName={groupName} serviceName={service.name} style={statusStyle} />
|
||||
<span className="sr-only">Site monitor status</span>
|
||||
</div>
|
||||
)}
|
||||
|
@ -4,7 +4,7 @@ import { columnMap } from "../../utils/layout/columns";
|
||||
|
||||
import Item from "components/services/item";
|
||||
|
||||
export default function List({ group, services, layout, useEqualHeights }) {
|
||||
export default function List({ groupName, services, layout, useEqualHeights }) {
|
||||
return (
|
||||
<ul
|
||||
className={classNames(
|
||||
@ -16,7 +16,7 @@ export default function List({ group, services, layout, useEqualHeights }) {
|
||||
<Item
|
||||
key={[service.container, service.app, service.name].filter((s) => s).join("-")}
|
||||
service={service}
|
||||
group={group}
|
||||
groupName={groupName}
|
||||
useEqualHeights={layout?.useEqualHeights ?? useEqualHeights}
|
||||
/>
|
||||
))}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function Ping({ group, service, style }) {
|
||||
export default function Ping({ groupName, serviceName, style }) {
|
||||
const { t } = useTranslation();
|
||||
const { data, error } = useSWR(`/api/ping?${new URLSearchParams({ group, service }).toString()}`, {
|
||||
const { data, error } = useSWR(`/api/ping?${new URLSearchParams({ groupName, serviceName }).toString()}`, {
|
||||
refreshInterval: 30000,
|
||||
});
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function SiteMonitor({ group, service, style }) {
|
||||
export default function SiteMonitor({ groupName, serviceName, style }) {
|
||||
const { t } = useTranslation();
|
||||
const { data, error } = useSWR(`/api/siteMonitor?${new URLSearchParams({ group, service }).toString()}`, {
|
||||
const { data, error } = useSWR(`/api/siteMonitor?${new URLSearchParams({ groupName, serviceName }).toString()}`, {
|
||||
refreshInterval: 30000,
|
||||
});
|
||||
|
||||
|
@ -6,10 +6,10 @@ import createLogger from "utils/logger";
|
||||
const logger = createLogger("ping");
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const serviceItem = await getServiceItem(group, service);
|
||||
const { groupName, serviceName } = req.query;
|
||||
const serviceItem = await getServiceItem(groupName, serviceName);
|
||||
if (!serviceItem) {
|
||||
logger.debug(`No service item found for group ${group} named ${service}`);
|
||||
logger.debug(`No service item found for group ${groupName} named ${serviceName}`);
|
||||
return res.status(400).send({
|
||||
error: "Unable to find service, see log for details.",
|
||||
});
|
||||
|
@ -7,10 +7,10 @@ import { httpProxy } from "utils/proxy/http";
|
||||
const logger = createLogger("siteMonitor");
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { group, service } = req.query;
|
||||
const serviceItem = await getServiceItem(group, service);
|
||||
const { groupName, serviceName } = req.query;
|
||||
const serviceItem = await getServiceItem(groupName, serviceName);
|
||||
if (!serviceItem) {
|
||||
logger.debug(`No service item found for group ${group} named ${service}`);
|
||||
logger.debug(`No service item found for group ${groupName} named ${serviceName}`);
|
||||
return res.status(400).send({
|
||||
error: "Unable to find service, see log for details.",
|
||||
});
|
||||
|
@ -291,8 +291,7 @@ function Home({ initialSettings }) {
|
||||
group.services ? (
|
||||
<ServicesGroup
|
||||
key={group.name}
|
||||
group={group.name}
|
||||
services={group}
|
||||
group={group}
|
||||
layout={settings.layout?.[group.name]}
|
||||
fiveColumns={settings.fiveColumns}
|
||||
disableCollapse={settings.disableCollapse}
|
||||
@ -316,8 +315,7 @@ function Home({ initialSettings }) {
|
||||
{serviceGroups.map((group) => (
|
||||
<ServicesGroup
|
||||
key={group.name}
|
||||
group={group.name}
|
||||
services={group}
|
||||
group={group}
|
||||
layout={settings.layout?.[group.name]}
|
||||
fiveColumns={settings.fiveColumns}
|
||||
disableCollapse={settings.disableCollapse}
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
servicesFromDocker,
|
||||
cleanServiceGroups,
|
||||
servicesFromKubernetes,
|
||||
findGroupByName,
|
||||
} from "utils/config/service-helpers";
|
||||
import { cleanWidgetGroups, widgetsFromConfig } from "utils/config/widget-helpers";
|
||||
|
||||
@ -84,6 +85,17 @@ export async function widgetsResponse() {
|
||||
return configuredWidgets;
|
||||
}
|
||||
|
||||
function mergeSubgroups(configuredGroups, mergedGroup) {
|
||||
configuredGroups.forEach((group) => {
|
||||
if (group.name === mergedGroup.name) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
group.services = mergedGroup.services;
|
||||
} else if (group.groups) {
|
||||
mergeSubgroups(group.groups, mergedGroup);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function servicesResponse() {
|
||||
let discoveredDockerServices;
|
||||
let discoveredKubernetesServices;
|
||||
@ -140,25 +152,29 @@ export async function servicesResponse() {
|
||||
const definedLayouts = initialSettings.layout ? Object.keys(initialSettings.layout) : null;
|
||||
|
||||
mergedGroupsNames.forEach((groupName) => {
|
||||
const discoveredDockerGroup = discoveredDockerServices.find((group) => group.name === groupName) || {
|
||||
const discoveredDockerGroup = findGroupByName(discoveredDockerServices, groupName) || {
|
||||
services: [],
|
||||
};
|
||||
const discoveredKubernetesGroup = discoveredKubernetesServices.find((group) => group.name === groupName) || {
|
||||
const discoveredKubernetesGroup = findGroupByName(discoveredKubernetesServices, groupName) || {
|
||||
services: [],
|
||||
};
|
||||
const configuredGroup = configuredServices.find((group) => group.name === groupName) || { services: [] };
|
||||
const configuredGroup = findGroupByName(configuredServices, groupName) || { services: [] };
|
||||
|
||||
const mergedGroup = {
|
||||
name: groupName,
|
||||
services: [...discoveredDockerGroup.services, ...discoveredKubernetesGroup.services, ...configuredGroup.services]
|
||||
.filter((service) => service)
|
||||
.sort(compareServices),
|
||||
groups: [...configuredGroup.groups],
|
||||
};
|
||||
|
||||
if (definedLayouts) {
|
||||
const layoutIndex = definedLayouts.findIndex((layout) => layout === mergedGroup.name);
|
||||
if (layoutIndex > -1) sortedGroups[layoutIndex] = mergedGroup;
|
||||
else unsortedGroups.push(mergedGroup);
|
||||
else if (configuredGroup.name) {
|
||||
// this is a nested group, so find the parent group and merge the services
|
||||
mergeSubgroups(configuredServices, mergedGroup);
|
||||
} else unsortedGroups.push(mergedGroup);
|
||||
} else {
|
||||
unsortedGroups.push(mergedGroup);
|
||||
}
|
||||
|
@ -13,6 +13,38 @@ import * as shvl from "utils/config/shvl";
|
||||
|
||||
const logger = createLogger("service-helpers");
|
||||
|
||||
function parseServicesToGroups(services) {
|
||||
if (!services) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// map easy to write YAML objects into easy to consume JS arrays
|
||||
return services.map((serviceGroup) => {
|
||||
const name = Object.keys(serviceGroup)[0];
|
||||
let groups = [];
|
||||
const serviceGroupServices = [];
|
||||
serviceGroup[name].forEach((entries) => {
|
||||
const entryName = Object.keys(entries)[0];
|
||||
if (Array.isArray(entries[entryName])) {
|
||||
groups = groups.concat(parseServicesToGroups([{ [entryName]: entries[entryName] }]));
|
||||
} else {
|
||||
serviceGroupServices.push({
|
||||
name: entryName,
|
||||
...entries[entryName],
|
||||
weight: entries[entryName].weight || serviceGroupServices.length * 100, // default weight
|
||||
type: "service",
|
||||
});
|
||||
}
|
||||
});
|
||||
return {
|
||||
name,
|
||||
type: "group",
|
||||
services: serviceGroupServices,
|
||||
groups,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function servicesFromConfig() {
|
||||
checkAndCopyConfig("services.yaml");
|
||||
|
||||
@ -20,31 +52,7 @@ export async function servicesFromConfig() {
|
||||
const rawFileContents = await fs.readFile(servicesYaml, "utf8");
|
||||
const fileContents = substituteEnvironmentVars(rawFileContents);
|
||||
const services = yaml.load(fileContents);
|
||||
|
||||
if (!services) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// map easy to write YAML objects into easy to consume JS arrays
|
||||
const servicesArray = services.map((servicesGroup) => ({
|
||||
name: Object.keys(servicesGroup)[0],
|
||||
services: servicesGroup[Object.keys(servicesGroup)[0]].map((entries) => ({
|
||||
name: Object.keys(entries)[0],
|
||||
...entries[Object.keys(entries)[0]],
|
||||
type: "service",
|
||||
})),
|
||||
}));
|
||||
|
||||
// add default weight to services based on their position in the configuration
|
||||
servicesArray.forEach((group, groupIndex) => {
|
||||
group.services.forEach((service, serviceIndex) => {
|
||||
if (service.weight === undefined) {
|
||||
servicesArray[groupIndex].services[serviceIndex].weight = (serviceIndex + 1) * 100;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return servicesArray;
|
||||
return parseServicesToGroups(services);
|
||||
}
|
||||
|
||||
export async function servicesFromDocker() {
|
||||
@ -667,13 +675,30 @@ export function cleanServiceGroups(groups) {
|
||||
});
|
||||
return cleanedService;
|
||||
}),
|
||||
type: serviceGroup.type || "group",
|
||||
groups: serviceGroup.groups ? cleanServiceGroups(serviceGroup.groups) : [],
|
||||
}));
|
||||
}
|
||||
|
||||
export function findGroupByName(groups, name) {
|
||||
for (let i = 0; i < groups.length; i += 1) {
|
||||
const group = groups[i];
|
||||
if (group.name === name) {
|
||||
return group;
|
||||
} else if (group.groups) {
|
||||
const foundGroup = findGroupByName(group.groups, name);
|
||||
if (foundGroup) {
|
||||
return foundGroup;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getServiceItem(group, service) {
|
||||
const configuredServices = await servicesFromConfig();
|
||||
|
||||
const serviceGroup = configuredServices.find((g) => g.name === group);
|
||||
const serviceGroup = findGroupByName(configuredServices, group);
|
||||
if (serviceGroup) {
|
||||
const serviceEntry = serviceGroup.services.find((s) => s.name === service);
|
||||
if (serviceEntry) return serviceEntry;
|
||||
@ -681,14 +706,14 @@ export async function getServiceItem(group, service) {
|
||||
|
||||
const discoveredServices = await servicesFromDocker();
|
||||
|
||||
const dockerServiceGroup = discoveredServices.find((g) => g.name === group);
|
||||
const dockerServiceGroup = findGroupByName(discoveredServices, group);
|
||||
if (dockerServiceGroup) {
|
||||
const dockerServiceEntry = dockerServiceGroup.services.find((s) => s.name === service);
|
||||
if (dockerServiceEntry) return dockerServiceEntry;
|
||||
}
|
||||
|
||||
const kubernetesServices = await servicesFromKubernetes();
|
||||
const kubernetesServiceGroup = kubernetesServices.find((g) => g.name === group);
|
||||
const kubernetesServiceGroup = findGroupByName(kubernetesServices, group);
|
||||
if (kubernetesServiceGroup) {
|
||||
const kubernetesServiceEntry = kubernetesServiceGroup.services.find((s) => s.name === service);
|
||||
if (kubernetesServiceEntry) return kubernetesServiceEntry;
|
||||
|
Loading…
x
Reference in New Issue
Block a user