feat: add app ping url (#2380)

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
Manuel
2025-02-21 22:47:30 +01:00
committed by GitHub
parent 9d54e938c8
commit fb467ac165
18 changed files with 3679 additions and 4 deletions

View File

@@ -56,7 +56,11 @@ export const appRouter = createTRPCRouter({
}), }),
selectable: protectedProcedure selectable: protectedProcedure
.input(z.void()) .input(z.void())
.output(z.array(selectAppSchema.pick({ id: true, name: true, iconUrl: true, href: true, description: true }))) .output(
z.array(
selectAppSchema.pick({ id: true, name: true, iconUrl: true, href: true, pingUrl: true, description: true }),
),
)
.meta({ .meta({
openapi: { openapi: {
method: "GET", method: "GET",
@@ -73,6 +77,7 @@ export const appRouter = createTRPCRouter({
iconUrl: true, iconUrl: true,
description: true, description: true,
href: true, href: true,
pingUrl: true,
}, },
orderBy: asc(apps.name), orderBy: asc(apps.name),
}); });
@@ -121,6 +126,7 @@ export const appRouter = createTRPCRouter({
description: input.description, description: input.description,
iconUrl: input.iconUrl, iconUrl: input.iconUrl,
href: input.href, href: input.href,
pingUrl: input.pingUrl === "" ? null : input.pingUrl,
}); });
return { appId: id }; return { appId: id };
@@ -164,6 +170,7 @@ export const appRouter = createTRPCRouter({
description: input.description, description: input.description,
iconUrl: input.iconUrl, iconUrl: input.iconUrl,
href: input.href, href: input.href,
pingUrl: input.pingUrl === "" ? null : input.pingUrl,
}) })
.where(eq(apps.id, input.id)); .where(eq(apps.id, input.id));
}), }),

View File

@@ -158,6 +158,7 @@ describe("create should create a new app with all arguments", () => {
description: "React components and hooks library", description: "React components and hooks library",
iconUrl: "https://mantine.dev/favicon.svg", iconUrl: "https://mantine.dev/favicon.svg",
href: "https://mantine.dev", href: "https://mantine.dev",
pingUrl: "https://mantine.dev/a",
}; };
// Act // Act
@@ -170,6 +171,7 @@ describe("create should create a new app with all arguments", () => {
expect(dbApp!.description).toBe(input.description); expect(dbApp!.description).toBe(input.description);
expect(dbApp!.iconUrl).toBe(input.iconUrl); expect(dbApp!.iconUrl).toBe(input.iconUrl);
expect(dbApp!.href).toBe(input.href); expect(dbApp!.href).toBe(input.href);
expect(dbApp!.pingUrl).toBe(input.pingUrl);
}); });
test("should create a new app only with required arguments", async () => { test("should create a new app only with required arguments", async () => {
@@ -185,6 +187,7 @@ describe("create should create a new app with all arguments", () => {
description: null, description: null,
iconUrl: "https://mantine.dev/favicon.svg", iconUrl: "https://mantine.dev/favicon.svg",
href: null, href: null,
pingUrl: "",
}; };
// Act // Act
@@ -197,6 +200,7 @@ describe("create should create a new app with all arguments", () => {
expect(dbApp!.description).toBe(input.description); expect(dbApp!.description).toBe(input.description);
expect(dbApp!.iconUrl).toBe(input.iconUrl); expect(dbApp!.iconUrl).toBe(input.iconUrl);
expect(dbApp!.href).toBe(input.href); expect(dbApp!.href).toBe(input.href);
expect(dbApp!.pingUrl).toBe(null);
}); });
}); });
@@ -225,6 +229,7 @@ describe("update should update an app", () => {
description: "React components and hooks library", description: "React components and hooks library",
iconUrl: "https://mantine.dev/favicon.svg2", iconUrl: "https://mantine.dev/favicon.svg2",
href: "https://mantine.dev", href: "https://mantine.dev",
pingUrl: "https://mantine.dev/a",
}; };
// Act // Act
@@ -257,6 +262,7 @@ describe("update should update an app", () => {
iconUrl: "https://mantine.dev/favicon.svg", iconUrl: "https://mantine.dev/favicon.svg",
description: null, description: null,
href: null, href: null,
pingUrl: "",
}); });
// Assert // Assert

View File

@@ -0,0 +1 @@
ALTER TABLE `app` ADD `ping_url` text;

File diff suppressed because it is too large Load Diff

View File

@@ -197,6 +197,13 @@
"when": 1739915526818, "when": 1739915526818,
"tag": "0027_acoustic_karma", "tag": "0027_acoustic_karma",
"breakpoints": true "breakpoints": true
},
{
"idx": 28,
"version": "5",
"when": 1740086765989,
"tag": "0028_add_app_ping_url",
"breakpoints": true
} }
] ]
} }

View File

@@ -0,0 +1 @@
ALTER TABLE `app` ADD `ping_url` text;

File diff suppressed because it is too large Load Diff

View File

@@ -197,6 +197,13 @@
"when": 1739915486467, "when": 1739915486467,
"tag": "0027_wooden_blizzard", "tag": "0027_wooden_blizzard",
"breakpoints": true "breakpoints": true
},
{
"idx": 28,
"version": "6",
"when": 1740086746417,
"tag": "0028_add_app_ping_url",
"breakpoints": true
} }
] ]
} }

View File

@@ -376,6 +376,7 @@ export const apps = mysqlTable("app", {
description: text(), description: text(),
iconUrl: text().notNull(), iconUrl: text().notNull(),
href: text(), href: text(),
pingUrl: text(),
}); });
export const integrationItems = mysqlTable( export const integrationItems = mysqlTable(

View File

@@ -361,6 +361,7 @@ export const apps = sqliteTable("app", {
description: text(), description: text(),
iconUrl: text().notNull(), iconUrl: text().notNull(),
href: text(), href: text(),
pingUrl: text(),
}); });
export const integrationItems = sqliteTable( export const integrationItems = sqliteTable(

View File

@@ -1,8 +1,10 @@
"use client"; "use client";
import type { ChangeEventHandler } from "react";
import { useRef } from "react"; import { useRef } from "react";
import Link from "next/link"; import Link from "next/link";
import { Button, Group, Stack, Textarea, TextInput } from "@mantine/core"; import { Button, Checkbox, Collapse, Group, Stack, Textarea, TextInput } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import type { z } from "zod"; import type { z } from "zod";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
@@ -39,6 +41,7 @@ export const AppForm = ({
description: initialValues?.description ?? "", description: initialValues?.description ?? "",
iconUrl: initialValues?.iconUrl ?? "", iconUrl: initialValues?.iconUrl ?? "",
href: initialValues?.href ?? "", href: initialValues?.href ?? "",
pingUrl: initialValues?.pingUrl ?? "",
}, },
}); });
@@ -54,6 +57,17 @@ export const AppForm = ({
originalHandleSubmit(values, redirect, afterSuccess); originalHandleSubmit(values, redirect, afterSuccess);
}; };
const [opened, { open, close }] = useDisclosure((initialValues?.pingUrl?.length ?? 0) > 0);
const handleClickDifferentUrlPing: ChangeEventHandler<HTMLInputElement> = () => {
if (!opened) {
open();
} else {
close();
form.setFieldValue("pingUrl", "");
}
};
return ( return (
<form onSubmit={form.onSubmit(handleSubmit)}> <form onSubmit={form.onSubmit(handleSubmit)}>
<Stack> <Stack>
@@ -62,6 +76,18 @@ export const AppForm = ({
<Textarea {...form.getInputProps("description")} label={t("app.field.description.label")} /> <Textarea {...form.getInputProps("description")} label={t("app.field.description.label")} />
<TextInput {...form.getInputProps("href")} label={t("app.field.url.label")} /> <TextInput {...form.getInputProps("href")} label={t("app.field.url.label")} />
<Checkbox
checked={opened}
onChange={handleClickDifferentUrlPing}
label={t("app.field.useDifferentUrlForPing.checkbox.label")}
description={t("app.field.useDifferentUrlForPing.checkbox.description")}
mt="md"
/>
<Collapse in={opened}>
<TextInput {...form.getInputProps("pingUrl")} />
</Collapse>
<Group justify="end"> <Group justify="end">
{showBackToOverview && ( {showBackToOverview && (
<Button variant="default" component={Link} href="/manage/apps"> <Button variant="default" component={Link} href="/manage/apps">

View File

@@ -52,6 +52,7 @@ export const AddDockerAppToHomarrModal = createModal<AddDockerAppToHomarrProps>(
iconUrl: container.iconUrl, iconUrl: container.iconUrl,
description: null, description: null,
href: form.values.containerUrls[index] ?? null, href: form.values.containerUrls[index] ?? null,
pingUrl: null,
})), })),
); );
}; };

View File

@@ -120,6 +120,7 @@ const addMappingFor = <TApp extends OldmarrApp | BookmarkApp>(
href: existing.href, href: existing.href,
iconUrl: existing.iconUrl, iconUrl: existing.iconUrl,
description: existing.description, description: existing.description,
pingUrl: existing.pingUrl,
exists: true, exists: true,
}); });
continue; continue;
@@ -144,6 +145,7 @@ const convertApp = (app: OldmarrApp): DbAppWithoutId => ({
href: app.behaviour.externalUrl === "" ? app.url : app.behaviour.externalUrl, href: app.behaviour.externalUrl === "" ? app.url : app.behaviour.externalUrl,
iconUrl: app.appearance.iconUrl, iconUrl: app.appearance.iconUrl,
description: app.behaviour.tooltipDescription ?? null, description: app.behaviour.tooltipDescription ?? null,
pingUrl: app.url.length > 0 ? app.url : null,
}); });
/** /**
@@ -154,4 +156,5 @@ const convertApp = (app: OldmarrApp): DbAppWithoutId => ({
const convertBookmarkApp = (app: BookmarkApp): DbAppWithoutId => ({ const convertBookmarkApp = (app: BookmarkApp): DbAppWithoutId => ({
...app, ...app,
description: null, description: null,
pingUrl: null,
}); });

View File

@@ -11,6 +11,7 @@ export const mapOldmarrApp = (app: OldmarrApp): InferSelectModel<typeof apps> =>
iconUrl: app.appearance.iconUrl, iconUrl: app.appearance.iconUrl,
description: app.behaviour.tooltipDescription ?? null, description: app.behaviour.tooltipDescription ?? null,
href: app.behaviour.externalUrl || app.url, href: app.behaviour.externalUrl || app.url,
pingUrl: app.url.length > 0 ? app.url : null,
}; };
}; };
@@ -23,5 +24,6 @@ export const mapOldmarrBookmarkApp = (
iconUrl: app.iconUrl, iconUrl: app.iconUrl,
description: null, description: null,
href: app.href, href: app.href,
pingUrl: null,
}; };
}; };

View File

@@ -600,6 +600,12 @@
}, },
"url": { "url": {
"label": "Url" "label": "Url"
},
"useDifferentUrlForPing": {
"checkbox": {
"label": "Use different URL for ping",
"description": "Useful if Homarr can directly access using an internal hostname or network to avoid bandwidth usage of ISP"
}
} }
}, },
"action": { "action": {

View File

@@ -17,6 +17,14 @@ const manageAppSchema = z.object({
.or(z.literal("")) .or(z.literal(""))
.transform((value) => (value.length === 0 ? null : value)) .transform((value) => (value.length === 0 ? null : value))
.nullable(), .nullable(),
pingUrl: z
.string()
.trim()
.url()
.regex(/^https?:\/\//) // Only allow http and https for security reasons (javascript: is not allowed)
.or(z.literal(""))
.transform((value) => (value.length === 0 ? null : value))
.nullable(),
}); });
const editAppSchema = manageAppSchema.and(z.object({ id: z.string() })); const editAppSchema = manageAppSchema.and(z.object({ id: z.string() }));

View File

@@ -97,7 +97,7 @@ export default function AppWidget({ options, isEditMode }: WidgetComponentProps<
</Tooltip.Floating> </Tooltip.Floating>
{options.pingEnabled && !settings.forceDisableStatus && !board.disableStatus && app.href ? ( {options.pingEnabled && !settings.forceDisableStatus && !board.disableStatus && app.href ? (
<Suspense fallback={<PingDot icon={IconLoader} color="blue" tooltip={`${t("common.action.loading")}`} />}> <Suspense fallback={<PingDot icon={IconLoader} color="blue" tooltip={`${t("common.action.loading")}`} />}>
<PingIndicator href={app.href} /> <PingIndicator href={app.pingUrl ?? app.href} />
</Suspense> </Suspense>
) : null} ) : null}
</AppLink> </AppLink>

View File

@@ -9,7 +9,14 @@ import type { SortableItemListInput } from "../options";
import { AppSelectModal } from "./app-select-modal"; import { AppSelectModal } from "./app-select-modal";
export const BookmarkAddButton: SortableItemListInput< export const BookmarkAddButton: SortableItemListInput<
{ name: string; description: string | null; id: string; iconUrl: string; href: string | null }, {
name: string;
description: string | null;
id: string;
iconUrl: string;
href: string | null;
pingUrl: string | null;
},
string string
>["AddButton"] = ({ addItem, values }) => { >["AddButton"] = ({ addItem, values }) => {
const { openModal } = useModalAction(AppSelectModal); const { openModal } = useModalAction(AppSelectModal);