Merge branch 'gethomepage:dev' into integration
This commit is contained in:
commit
01dc8e1531
24
docs/configs/info-widgets.md
Normal file
24
docs/configs/info-widgets.md
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
title: Information Widgets
|
||||
description: Homepage info widgets.
|
||||
---
|
||||
|
||||
Information widgets are widgets that provide information about your system or environment and are displayed at the top of the homepage. You can find a list of all available info widgets under the [Info Widgets](../widgets/info/index.md) section.
|
||||
|
||||
Info widgets are defined in the widgets.yaml
|
||||
|
||||
Each widget has its own configuration options, which are detailed in the widget's documentation.
|
||||
|
||||
## Layout
|
||||
|
||||
Info widgets are displayed in the order they are defined in the `widgets.yaml` file. You can change the order by moving the widgets around in the file. However, some widgets (weather, search and datetime) are aligned to the right side of the screen which can affect the layout of the widgets.
|
||||
|
||||
## Adding A Link
|
||||
|
||||
You can add a link to an info widget such as the logo or text widgets by adding an `href` option, for example:
|
||||
|
||||
```yaml
|
||||
logo:
|
||||
href: https://example.com
|
||||
target: _blank # Optional, can be set in settings
|
||||
```
|
||||
@ -1,58 +0,0 @@
|
||||
---
|
||||
title: Service Widgets
|
||||
description: Service Widget Configuration
|
||||
---
|
||||
|
||||
Unless otherwise noted, URLs should not end with a `/` or other API path. Each widget will handle the path on its own.
|
||||
|
||||
Each service can have widgets attached to it (often matching the service type, but that's not forced).
|
||||
|
||||
In addition to the href of the service, you can also specify the target location in which to open that link. See [Link Target](settings.md#link-target) for more details.
|
||||
|
||||
Using Emby as an example, this is how you would attach the Emby service widget.
|
||||
|
||||
```yaml
|
||||
- Emby:
|
||||
icon: emby.png
|
||||
href: http://emby.host.or.ip/
|
||||
description: Movies & TV Shows
|
||||
widget:
|
||||
type: emby
|
||||
url: http://emby.host.or.ip
|
||||
key: apikeyapikeyapikeyapikeyapikey
|
||||
```
|
||||
|
||||
## Multiple Widgets
|
||||
|
||||
Each service can have multiple widgets attached to it, for example:
|
||||
|
||||
```yaml
|
||||
- Emby:
|
||||
icon: emby.png
|
||||
href: http://emby.host.or.ip/
|
||||
description: Movies & TV Shows
|
||||
widgets:
|
||||
- type: emby
|
||||
url: http://emby.host.or.ip
|
||||
key: apikeyapikeyapikeyapikeyapikey
|
||||
- type: uptimekuma
|
||||
url: http://uptimekuma.host.or.ip:port
|
||||
slug: statuspageslug
|
||||
```
|
||||
|
||||
## Field Visibility
|
||||
|
||||
Each widget can optionally provide a list of which fields should be visible via the `fields` widget property. If no fields are specified, then all fields will be displayed. The `fields` property must be a valid YAML array of strings. As an example, here is the entry for Sonarr showing only a couple of fields.
|
||||
|
||||
**In all cases a widget will work and display all fields without specifying the `fields` property.**
|
||||
|
||||
```yaml
|
||||
- Sonarr:
|
||||
icon: sonarr.png
|
||||
href: http://sonarr.host.or.ip
|
||||
widget:
|
||||
type: sonarr
|
||||
fields: ["wanted", "queued"]
|
||||
url: http://sonarr.host.or.ip
|
||||
key: apikeyapikeyapikeyapikeyapikey
|
||||
```
|
||||
@ -21,6 +21,23 @@ Groups are defined as top-level array entries.
|
||||
|
||||
<img width="1038" alt="Service Groups" src="https://user-images.githubusercontent.com/82196/187040754-28065242-4534-4409-881c-93d1921c6141.png">
|
||||
|
||||
### Nested Groups
|
||||
|
||||
Groups can be nested by using the same format as the top-level groups.
|
||||
|
||||
```yaml
|
||||
- Group A:
|
||||
- Service A:
|
||||
href: http://localhost/
|
||||
|
||||
- Group B:
|
||||
- Service B:
|
||||
href: http://localhost/
|
||||
|
||||
- Service C:
|
||||
href: http://localhost/
|
||||
```
|
||||
|
||||
## Services
|
||||
|
||||
Services are defined as array entries on groups,
|
||||
@ -43,6 +60,60 @@ Services are defined as array entries on groups,
|
||||
|
||||
<img width="1038" alt="Service Services" src="https://user-images.githubusercontent.com/82196/187040763-038023a2-8bee-4d87-b5cc-13447e7365a4.png">
|
||||
|
||||
### Service Widgets
|
||||
|
||||
Each service can have widgets attached to it (often matching the service type, but that's not forced).
|
||||
|
||||
In addition to the href of the service, you can also specify the target location in which to open that link. See [Link Target](settings.md#link-target) for more details.
|
||||
|
||||
Using Emby as an example, this is how you would attach the Emby service widget.
|
||||
|
||||
```yaml
|
||||
- Emby:
|
||||
icon: emby.png
|
||||
href: http://emby.host.or.ip/
|
||||
description: Movies & TV Shows
|
||||
widget:
|
||||
type: emby
|
||||
url: http://emby.host.or.ip
|
||||
key: apikeyapikeyapikeyapikeyapikey
|
||||
```
|
||||
|
||||
#### Multiple Widgets
|
||||
|
||||
Each service can have multiple widgets attached to it, for example:
|
||||
|
||||
```yaml
|
||||
- Emby:
|
||||
icon: emby.png
|
||||
href: http://emby.host.or.ip/
|
||||
description: Movies & TV Shows
|
||||
widgets:
|
||||
- type: emby
|
||||
url: http://emby.host.or.ip
|
||||
key: apikeyapikeyapikeyapikeyapikey
|
||||
- type: uptimekuma
|
||||
url: http://uptimekuma.host.or.ip:port
|
||||
slug: statuspageslug
|
||||
```
|
||||
|
||||
#### Field Visibility
|
||||
|
||||
Each widget can optionally provide a list of which fields should be visible via the `fields` widget property. If no fields are specified, then all fields will be displayed. The `fields` property must be a valid YAML array of strings. As an example, here is the entry for Sonarr showing only a couple of fields.
|
||||
|
||||
**In all cases a widget will work and display all fields without specifying the `fields` property.**
|
||||
|
||||
```yaml
|
||||
- Sonarr:
|
||||
icon: sonarr.png
|
||||
href: http://sonarr.host.or.ip
|
||||
widget:
|
||||
type: sonarr
|
||||
fields: ["wanted", "queued"]
|
||||
url: http://sonarr.host.or.ip
|
||||
key: apikeyapikeyapikeyapikeyapikey
|
||||
```
|
||||
|
||||
## Descriptions
|
||||
|
||||
Services may have descriptions,
|
||||
|
||||
@ -137,6 +137,27 @@ layout:
|
||||
columns: 3
|
||||
```
|
||||
|
||||
### Nested Groups
|
||||
|
||||
If your services config has nested groups, you can apply settings to these groups by nesting them in the layout block
|
||||
and using the same settings. For example
|
||||
|
||||
```yaml
|
||||
layout:
|
||||
Group A:
|
||||
style: row
|
||||
columns: 4
|
||||
Group C:
|
||||
style: row
|
||||
columns: 2
|
||||
Nested Group A:
|
||||
style: row
|
||||
columns: 2
|
||||
Nested Group B:
|
||||
style: row
|
||||
columns: 2
|
||||
```
|
||||
|
||||
### Headers
|
||||
|
||||
You can hide headers for each section in the layout as well by passing `header` as false, like so:
|
||||
@ -348,12 +369,12 @@ This can also be set for individual services. Note setting this at the service l
|
||||
|
||||
## Providers
|
||||
|
||||
The `providers` section allows you to define shared API provider options and secrets. Currently this allows you to define your weather API keys in secret and is also the location of the Longhorn URL and credentials.
|
||||
The `providers` section allows you to define shared API provider options and secrets.
|
||||
|
||||
```yaml
|
||||
providers:
|
||||
openweathermap: openweathermapapikey
|
||||
weatherapi: weatherapiapikey
|
||||
finnhub: yourfinnhubapikeyhere
|
||||
longhorn:
|
||||
url: https://longhorn.example.com
|
||||
username: admin
|
||||
@ -363,10 +384,10 @@ providers:
|
||||
You can then pass `provider` instead of `apiKey` in your widget configuration.
|
||||
|
||||
```yaml
|
||||
- weatherapi:
|
||||
- openweathermap:
|
||||
latitude: 50.449684
|
||||
longitude: 30.525026
|
||||
provider: weatherapi
|
||||
provider: openweathermap
|
||||
```
|
||||
|
||||
## Quick Launch
|
||||
|
||||
@ -17,7 +17,7 @@ hide:
|
||||
|
||||
All service widgets work essentially the same, that is, homepage makes a proxied call to an API made available by that service. The majority of the time widgets don't work it is a configuration issue. Of course, sometimes things do break. Some basic steps to try:
|
||||
|
||||
1. Ensure that you follow the rule mentioned on https://gethomepage.dev/configs/service-widgets/. **Unless otherwise noted, URLs should not end with a / or other API path. Each widget will handle the path on its own.**. This is very important as including a trailing slash can result in an error.
|
||||
1. **URLs should not end with a / or other API path. Each widget will handle the path on its own.**. Including a trailing slash can result in an error.
|
||||
|
||||
2. Verify the homepage installation can connect to the IP address or host you are using for the widget `url`. This is most simply achieved by pinging the server from the homepage machine, in Docker this means _from inside the container_ itself, e.g.:
|
||||
|
||||
|
||||
@ -50,7 +50,7 @@ You can also pass API keys from the widget configuration to the proxy handler, f
|
||||
|
||||
### `credentialedProxyHandler`
|
||||
|
||||
A proxy handler that makes authenticated by setting request headers. Credentials are pulled from the widgets configuration.
|
||||
A proxy handler that makes authenticated requests by setting request headers. Credentials are pulled from the widgets configuration.
|
||||
|
||||
By default the key is passed as an `X-API-Key` header. If you need to pass the key as something else, either add a case to the credentialedProxyHandler or create a new proxy handler.
|
||||
|
||||
|
||||
@ -28,6 +28,8 @@ Service widgets are used to display the status of a service, often a web service
|
||||
slug: aaaaaaabbbbb
|
||||
```
|
||||
|
||||
More detail on configuring service widgets can be found in the [Service Widgets Config](../configs/services.md) section.
|
||||
|
||||
## Info Widgets
|
||||
|
||||
Info widgets are used to display information in the header, often about your system or environment. Info widgets are defined your `widgets.yaml` file. Here's an example:
|
||||
@ -39,3 +41,5 @@ Info widgets are used to display information in the header, often about your sys
|
||||
longitude: -117.51
|
||||
cache: 5
|
||||
```
|
||||
|
||||
More detail on configuring info widgets can be found in the [Info Widgets Config](../configs/info-widgets.md) section.
|
||||
|
||||
@ -3,7 +3,7 @@ title: Open-Meteo
|
||||
description: Open-Meteo Information Widget Configuration
|
||||
---
|
||||
|
||||
No registration is required at all! See [https://open-meteo.com/en/docs](https://open-meteo.com/en/docs)
|
||||
Homepage's recommended weather widget. No registration is required at all! See [https://open-meteo.com/en/docs](https://open-meteo.com/en/docs)
|
||||
|
||||
```yaml
|
||||
- openmeteo:
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
---
|
||||
title: Weather API
|
||||
description: Weather API Information Widget Configuration
|
||||
---
|
||||
|
||||
**Note: this widget is considered 'deprecated' since there is no longer a free Weather API tier for new members. See the openmeteo or openweathermap widgets for alternatives.**
|
||||
|
||||
The free tier is all that's required, you will need to [register](https://www.weatherapi.com/signup.aspx) and grab your API key.
|
||||
|
||||
```yaml
|
||||
- weatherapi:
|
||||
label: Kyiv # optional
|
||||
latitude: 50.449684
|
||||
longitude: 30.525026
|
||||
units: metric # or imperial
|
||||
apiKey: yourweatherapikey
|
||||
cache: 5 # Time in minutes to cache API responses, to stay within limits
|
||||
format: # optional, Intl.NumberFormat options
|
||||
maximumFractionDigits: 1
|
||||
```
|
||||
|
||||
You can optionally not pass a `latitude` and `longitude` and the widget will use your current location (requires a secure context, eg. HTTPS).
|
||||
@ -21,8 +21,8 @@ nav:
|
||||
- configs/index.md
|
||||
- configs/settings.md
|
||||
- configs/bookmarks.md
|
||||
- configs/info-widgets.md
|
||||
- configs/services.md
|
||||
- configs/service-widgets.md
|
||||
- configs/kubernetes.md
|
||||
- configs/docker.md
|
||||
- configs/custom-css-js.md
|
||||
@ -142,6 +142,7 @@ nav:
|
||||
- widgets/services/spoolman.md
|
||||
- widgets/services/stash.md
|
||||
- widgets/services/stocks.md
|
||||
- widgets/services/suwayomi.md
|
||||
- widgets/services/swagdashboard.md
|
||||
- widgets/services/syncthing-relay-server.md
|
||||
- widgets/services/tailscale.md
|
||||
@ -177,7 +178,6 @@ nav:
|
||||
- widgets/info/search.md
|
||||
- widgets/info/stocks.md
|
||||
- widgets/info/unifi_controller.md
|
||||
- widgets/info/weather.md
|
||||
- "Learn":
|
||||
- widgets/authoring/index.md
|
||||
- "Getting Started": widgets/authoring/getting-started.md
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import classNames from "classnames";
|
||||
import { useContext } from "react";
|
||||
|
||||
import WidgetIcon from "./widget_icon";
|
||||
import PrimaryText from "./primary_text";
|
||||
import SecondaryText from "./secondary_text";
|
||||
import Raw from "./raw";
|
||||
|
||||
import { SettingsContext } from "utils/contexts/settings";
|
||||
|
||||
export function getAllClasses(options, additionalClassNames = "") {
|
||||
if (options?.style?.header === "boxedWidgets") {
|
||||
if (options?.style?.cardBlur !== undefined) {
|
||||
@ -56,7 +59,17 @@ export function getBottomBlock(children) {
|
||||
}
|
||||
|
||||
export default function Container({ children = [], options, additionalClassNames = "" }) {
|
||||
return (
|
||||
const { settings } = useContext(SettingsContext);
|
||||
return options.href ? (
|
||||
<a
|
||||
href={options.href}
|
||||
target={options.target ?? settings.target ?? "_blank"}
|
||||
className={getAllClasses(options, `${additionalClassNames} widget-container`)}
|
||||
>
|
||||
{getInnerBlock(children)}
|
||||
{getBottomBlock(children)}
|
||||
</a>
|
||||
) : (
|
||||
<div className={getAllClasses(options, `${additionalClassNames} widget-container`)}>
|
||||
{getInnerBlock(children)}
|
||||
{getBottomBlock(children)}
|
||||
|
||||
@ -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}
|
||||
@ -454,6 +452,7 @@ function Home({ initialSettings }) {
|
||||
}
|
||||
|
||||
export default function Wrapper({ initialSettings, fallback }) {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const wrappedStyle = {};
|
||||
let backgroundBlur = false;
|
||||
let backgroundSaturate = false;
|
||||
@ -484,8 +483,9 @@ export default function Wrapper({ initialSettings, fallback }) {
|
||||
id="page_wrapper"
|
||||
className={classNames(
|
||||
"relative",
|
||||
initialSettings.theme && initialSettings.theme,
|
||||
theme && theme,
|
||||
initialSettings.color && `theme-${initialSettings.color}`,
|
||||
theme === "dark" ? "scheme-dark" : "scheme-light",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
# For configuration options and examples, please see:
|
||||
# https://gethomepage.dev/configs/services
|
||||
# https://gethomepage.dev/configs/services/
|
||||
|
||||
- My First Group:
|
||||
- My First Service:
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
# For configuration options and examples, please see:
|
||||
# https://gethomepage.dev/configs/settings
|
||||
# https://gethomepage.dev/configs/settings/
|
||||
|
||||
providers:
|
||||
openweathermap: openweathermapapikey
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
# For configuration options and examples, please see:
|
||||
# https://gethomepage.dev/configs/service-widgets
|
||||
# https://gethomepage.dev/configs/info-widgets/
|
||||
|
||||
- resources:
|
||||
cpu: true
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -12,6 +12,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");
|
||||
|
||||
@ -19,31 +51,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() {
|
||||
@ -581,13 +589,31 @@ export function cleanServiceGroups(groups) {
|
||||
});
|
||||
return cleanedService;
|
||||
}),
|
||||
type: serviceGroup.type || "group",
|
||||
groups: serviceGroup.groups ? cleanServiceGroups(serviceGroup.groups) : [],
|
||||
}));
|
||||
}
|
||||
|
||||
export function findGroupByName(groups, name) {
|
||||
// Deep search for a group by name. Using for loop allows for early return
|
||||
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;
|
||||
@ -595,14 +621,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…
Reference in New Issue
Block a user