Feature: nested groups (#4346)

pull/4348/head
shamoon 2 months ago committed by GitHub
parent 907abee1aa
commit be8363cc35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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,38 +13,46 @@ import * as shvl from "utils/config/shvl";
const logger = createLogger("service-helpers");
export async function servicesFromConfig() {
checkAndCopyConfig("services.yaml");
const servicesYaml = path.join(CONF_DIR, "services.yaml");
const rawFileContents = await fs.readFile(servicesYaml, "utf8");
const fileContents = substituteEnvironmentVars(rawFileContents);
const services = yaml.load(fileContents);
function parseServicesToGroups(services) {
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 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,
};
});
}
return servicesArray;
export async function servicesFromConfig() {
checkAndCopyConfig("services.yaml");
const servicesYaml = path.join(CONF_DIR, "services.yaml");
const rawFileContents = await fs.readFile(servicesYaml, "utf8");
const fileContents = substituteEnvironmentVars(rawFileContents);
const services = yaml.load(fileContents);
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…
Cancel
Save