Merge remote-tracking branch 'origin/benphelpsMain' into LocalMain

This commit is contained in:
Karl Hudgell 2024-01-17 16:24:58 +00:00
commit b4094b316e
16 changed files with 99 additions and 38 deletions

View File

@ -164,10 +164,10 @@ labels:
- homepage.description=Media server
- homepage.widget.type=customapi
- homepage.widget.url=http://argus.service/api/v1/service/summary/emby
- homepage.widget.field[0].label=Deployed Version
- homepage.widget.field[0].field.status=deployed_version
- homepage.widget.field[1].label=Latest Version
- homepage.widget.field[1].field.status=latest_version
- homepage.widget.mappings[0].label=Deployed Version
- homepage.widget.mappings[0].field.status=deployed_version
- homepage.widget.mappings[1].label=Latest Version
- homepage.widget.mappings[1].field.status=latest_version
```
## Docker Swarm

View File

@ -27,6 +27,7 @@ widget:
url: https://domain.url/with/link/to.ics # URL with calendar events
name: My Events # required - name for these calendar events
color: zinc # optional - defaults to pre-defined color for the service (zinc for ical)
timezone: America/Los_Angeles # optional - force timezone for events (if it's the same - no change, if missing or different in ical - will be converted to this timezone)
params: # optional - additional params for the service
showName: true # optional - show name before event title in event line - defaults to false
```

View File

@ -3,18 +3,21 @@ title: Health checks
description: Health checks Widget Configuration
---
To use the Health Checks widget, you first need to generate an API key. To do this, follow these steps:
Specify a single check by including the `uuid` field or show the total 'up' and 'down' for all
checks by leaving off the `uuid` field.
1. Go to Settings in your check dashboard.
To use the Health Checks widget, you first need to generate an API key.
1. In your project, go to project Settings on the navigation bar.
2. Click on API key (read-only) and then click _Create_.
3. Copy the API key that is generated for you.
Allowed fields: `["status", "last_ping"]`.
Allowed fields: `["status", "last_ping"]` for single checks, `["up", "down"]` for total stats.
```yaml
widget:
type: healthchecks
url: http://healthchecks.host.or.ip:port
key: <YOUR_API_KEY>
uuid: <YOUR_CHECK_UUID>
uuid: <CHECK_UUID> # optional, if not included total statistics for all checks is shown
```

View File

@ -491,9 +491,9 @@
},
"healthchecks": {
"new": "New",
"up": "Online",
"up": "Up",
"grace": "In Grace Period",
"down": "Offline",
"down": "Down",
"paused": "Paused",
"status": "Status",
"last_ping": "Last Ping",

View File

@ -199,7 +199,7 @@ export default function QuickLaunch({
{results.length > 0 && (
<ul className="max-h-[60vh] overflow-y-auto m-2">
{results.map((r, i) => (
<li key={r.container ?? r.app ?? `${r.name}-${r.href}`}>
<li key={[r.name, r.container, r.app, r.href].filter((s) => s).join("-")}>
<button
type="button"
data-index={i}

View File

@ -49,6 +49,7 @@ export default function Item({ service, group, useEqualHeights }) {
target={service.target ?? settings.target ?? "_blank"}
rel="noreferrer"
className="flex-shrink-0 flex items-center justify-center w-12 service-icon"
aria-label={service.icon}
>
<ResolvedIcon icon={service.icon} />
</a>

View File

@ -14,7 +14,7 @@ export default function List({ group, services, layout, useEqualHeights }) {
>
{services.map((service) => (
<Item
key={service.container ?? service.app ?? service.name}
key={[service.container, service.app, service.name].filter((s) => s).join("-")}
service={service}
group={group}
useEqualHeights={layout?.useEqualHeights ?? useEqualHeights}

View File

@ -1,10 +1,11 @@
export default function UsageBar({ percent, additionalClassNames = "" }) {
const normalized = Math.min(100, Math.max(0, percent));
return (
<div className={`mt-0.5 w-full bg-theme-800/30 rounded-full h-1 dark:bg-theme-200/20 ${additionalClassNames}`}>
<div
className="bg-theme-800/70 h-1 rounded-full dark:bg-theme-200/50 transition-all duration-1000"
style={{
width: `${percent}%`,
width: `${normalized}%`,
}}
/>
</div>

View File

@ -38,7 +38,7 @@ export async function servicesFromConfig() {
// add default weight to services based on their position in the configuration
servicesArray.forEach((group, groupIndex) => {
group.services.forEach((service, serviceIndex) => {
if (!service.weight) {
if (service.weight === undefined) {
servicesArray[groupIndex].services[serviceIndex].weight = (serviceIndex + 1) * 100;
}
});
@ -102,6 +102,16 @@ export async function servicesFromDocker() {
}
});
if (!constructedService.name || !constructedService.group) {
logger.error(
`Error constructing service using homepage labels for container '${containerName.replace(
/^\//,
"",
)}'. Ensure required labels are present.`,
);
return null;
}
return constructedService;
});
@ -387,6 +397,9 @@ export function cleanServiceGroups(groups) {
// glances, customapi, iframe
refreshInterval,
// healthchecks
uuid,
// iframe
allowFullscreen,
allowPolicy,
@ -526,6 +539,9 @@ export function cleanServiceGroups(groups) {
if (previousDays) cleanedService.widget.previousDays = previousDays;
if (showTime) cleanedService.widget.showTime = showTime;
}
if (type === "healthchecks") {
if (uuid !== undefined) cleanedService.widget.uuid = uuid;
}
}
return cleanedService;

View File

@ -2,7 +2,7 @@ import { DateTime } from "luxon";
import classNames from "classnames";
import { useTranslation } from "next-i18next";
import Event from "./event";
import Event, { compareDateTimezoneAware } from "./event";
export default function Agenda({ service, colorVariants, events, showDate }) {
const { widget } = service;
@ -15,8 +15,10 @@ export default function Agenda({ service, colorVariants, events, showDate }) {
const eventsArray = Object.keys(events)
.filter(
(eventKey) =>
showDate.minus({ days: widget?.previousDays ?? 0 }).startOf("day").ts <=
events[eventKey].date?.startOf("day").ts,
showDate
.setZone(events[eventKey].date.zoneName)
.minus({ days: widget?.previousDays ?? 0 })
.startOf("day").ts <= events[eventKey].date?.startOf("day").ts,
)
.map((eventKey) => events[eventKey])
.sort((a, b) => a.date - b.date)
@ -56,7 +58,7 @@ export default function Agenda({ service, colorVariants, events, showDate }) {
event={event}
colorVariants={colorVariants}
showDate={j === 0}
showTime={widget?.showTime && event.date.startOf("day").ts === showDate.startOf("day").ts}
showTime={widget?.showTime && compareDateTimezoneAware(showDate, event)}
/>
))}
</div>

View File

@ -39,3 +39,7 @@ export default function Event({ event, colorVariants, showDate = false, showTime
</div>
);
}
export function compareDateTimezoneAware(date, event) {
return date.setZone(event.date.zoneName).startOf("day").valueOf() === event.date.startOf("day").valueOf();
}

View File

@ -23,8 +23,9 @@ export default function Integration({ config, params, setEvents, hideErrors }) {
}
}
const startDate = DateTime.fromISO(params.start);
const endDate = DateTime.fromISO(params.end);
const zone = config?.timezone || null;
const startDate = DateTime.fromISO(params.start, { zone });
const endDate = DateTime.fromISO(params.end, { zone });
if (icalError || !parsedIcal || !startDate.isValid || !endDate.isValid) {
return;
@ -43,20 +44,24 @@ export default function Integration({ config, params, setEvents, hideErrors }) {
const duration = event.dtend.value - event.dtstart.value;
const days = duration / (1000 * 60 * 60 * 24);
const now = DateTime.now().setZone(zone);
const eventDate = DateTime.fromJSDate(date, { zone });
for (let j = 0; j < days; j += 1) {
eventsToAdd[`${event?.uid?.value}${i}${j}${type}`] = {
title,
date: DateTime.fromJSDate(date).plus({ days: j }),
date: eventDate.plus({ days: j }),
color: config?.color ?? "zinc",
isCompleted: DateTime.fromJSDate(date) < DateTime.now(),
isCompleted: eventDate < now,
additional: event.location?.value,
type: "ical",
};
}
};
if (event?.recurrenceRule?.options) {
const rule = new RRule(event.recurrenceRule.options);
const recurrenceOptions = event?.recurrenceRule?.origOptions;
if (recurrenceOptions && Object.keys(recurrenceOptions).length !== 0) {
const rule = new RRule(recurrenceOptions);
const recurringEvents = rule.between(startDate.toJSDate(), endDate.toJSDate());
recurringEvents.forEach((date, i) => eventToAdd(date, i, "recurring"));

View File

@ -3,7 +3,7 @@ import { DateTime, Info } from "luxon";
import classNames from "classnames";
import { useTranslation } from "next-i18next";
import Event from "./event";
import Event, { compareDateTimezoneAware } from "./event";
const cellStyle = "relative w-10 flex items-center justify-center flex-col";
const monthButton = "pl-6 pr-6 ml-2 mr-2 hover:bg-theme-100/20 dark:hover:bg-white/5 rounded-md cursor-pointer";
@ -12,9 +12,7 @@ export function Day({ weekNumber, weekday, events, colorVariants, showDate, setS
const currentDate = DateTime.now();
const cellDate = showDate.set({ weekday, weekNumber }).startOf("day");
const filteredEvents = events?.filter(
(event) => event.date?.startOf("day").toUnixInteger() === cellDate.toUnixInteger(),
);
const filteredEvents = events?.filter((event) => compareDateTimezoneAware(cellDate, event));
const dayStyles = (displayDate) => {
let style = "h-9 ";
@ -111,6 +109,7 @@ export default function Monthly({ service, colorVariants, events, showDate, setS
}
const eventsArray = Object.keys(events).map((eventKey) => events[eventKey]);
eventsArray.sort((a, b) => a.date - b.date);
return (
<div className="w-full text-center">
@ -172,7 +171,7 @@ export default function Monthly({ service, colorVariants, events, showDate, setS
<div className="flex flex-col">
{eventsArray
?.filter((event) => showDate.startOf("day").ts === event.date?.startOf("day").ts)
?.filter((event) => compareDateTimezoneAware(showDate, event))
.slice(0, widget?.maxEvents ?? 10)
.map((event) => (
<Event
@ -180,7 +179,7 @@ export default function Monthly({ service, colorVariants, events, showDate, setS
event={event}
colorVariants={colorVariants}
showDateColumn={widget?.showTime ?? false}
showTime={widget?.showTime && event.date.startOf("day").ts === showDate.startOf("day").ts}
showTime={widget?.showTime && compareDateTimezoneAware(showDate, event)}
/>
))}
</div>

View File

@ -27,6 +27,23 @@ function formatDate(dateString) {
return new Intl.DateTimeFormat(i18n.language, dateOptions).format(date);
}
function countStatus(data) {
let upCount = 0;
let downCount = 0;
if (data.checks) {
data.checks.forEach((check) => {
if (check.status === "up") {
upCount += 1;
} else if (check.status === "down") {
downCount += 1;
}
});
}
return { upCount, downCount };
}
export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
@ -46,13 +63,26 @@ export default function Component({ service }) {
);
}
const hasUuid = widget?.uuid;
const { upCount, downCount } = countStatus(data);
return (
<Container service={service}>
<Block label="healthchecks.status" value={t(`healthchecks.${data.status}`)} />
<Block
label="healthchecks.last_ping"
value={data.last_ping ? formatDate(data.last_ping) : t("healthchecks.never")}
/>
{hasUuid ? (
<>
<Block label="healthchecks.status" value={t(`healthchecks.${data.status}`)} />
<Block
label="healthchecks.last_ping"
value={data.last_ping ? formatDate(data.last_ping) : t("healthchecks.never")}
/>
</>
) : (
<>
<Block label="healthchecks.up" value={upCount} />
<Block label="healthchecks.down" value={downCount} />
</>
)}
</Container>
);
}

View File

@ -1,13 +1,12 @@
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const widget = {
api: "{url}/api/v2/{endpoint}/{uuid}",
api: "{url}/api/v3/{endpoint}/{uuid}",
proxyHandler: credentialedProxyHandler,
mappings: {
checks: {
endpoint: "checks",
validate: ["status", "last_ping"],
},
},
};

View File

@ -64,7 +64,7 @@ async function tryLogin(widget) {
const resp = await rpc(url, {
method: "login",
service: "session",
params: { username, password },
params: { username: username.toString(), password: password.toString() },
});
if (resp.status !== 200) {