feat: add app ping url (#2380)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
@@ -56,7 +56,11 @@ export const appRouter = createTRPCRouter({
|
||||
}),
|
||||
selectable: protectedProcedure
|
||||
.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({
|
||||
openapi: {
|
||||
method: "GET",
|
||||
@@ -73,6 +77,7 @@ export const appRouter = createTRPCRouter({
|
||||
iconUrl: true,
|
||||
description: true,
|
||||
href: true,
|
||||
pingUrl: true,
|
||||
},
|
||||
orderBy: asc(apps.name),
|
||||
});
|
||||
@@ -121,6 +126,7 @@ export const appRouter = createTRPCRouter({
|
||||
description: input.description,
|
||||
iconUrl: input.iconUrl,
|
||||
href: input.href,
|
||||
pingUrl: input.pingUrl === "" ? null : input.pingUrl,
|
||||
});
|
||||
|
||||
return { appId: id };
|
||||
@@ -164,6 +170,7 @@ export const appRouter = createTRPCRouter({
|
||||
description: input.description,
|
||||
iconUrl: input.iconUrl,
|
||||
href: input.href,
|
||||
pingUrl: input.pingUrl === "" ? null : input.pingUrl,
|
||||
})
|
||||
.where(eq(apps.id, input.id));
|
||||
}),
|
||||
|
||||
@@ -158,6 +158,7 @@ describe("create should create a new app with all arguments", () => {
|
||||
description: "React components and hooks library",
|
||||
iconUrl: "https://mantine.dev/favicon.svg",
|
||||
href: "https://mantine.dev",
|
||||
pingUrl: "https://mantine.dev/a",
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -170,6 +171,7 @@ describe("create should create a new app with all arguments", () => {
|
||||
expect(dbApp!.description).toBe(input.description);
|
||||
expect(dbApp!.iconUrl).toBe(input.iconUrl);
|
||||
expect(dbApp!.href).toBe(input.href);
|
||||
expect(dbApp!.pingUrl).toBe(input.pingUrl);
|
||||
});
|
||||
|
||||
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,
|
||||
iconUrl: "https://mantine.dev/favicon.svg",
|
||||
href: null,
|
||||
pingUrl: "",
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -197,6 +200,7 @@ describe("create should create a new app with all arguments", () => {
|
||||
expect(dbApp!.description).toBe(input.description);
|
||||
expect(dbApp!.iconUrl).toBe(input.iconUrl);
|
||||
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",
|
||||
iconUrl: "https://mantine.dev/favicon.svg2",
|
||||
href: "https://mantine.dev",
|
||||
pingUrl: "https://mantine.dev/a",
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -257,6 +262,7 @@ describe("update should update an app", () => {
|
||||
iconUrl: "https://mantine.dev/favicon.svg",
|
||||
description: null,
|
||||
href: null,
|
||||
pingUrl: "",
|
||||
});
|
||||
|
||||
// Assert
|
||||
|
||||
1
packages/db/migrations/mysql/0028_add_app_ping_url.sql
Normal file
1
packages/db/migrations/mysql/0028_add_app_ping_url.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE `app` ADD `ping_url` text;
|
||||
1833
packages/db/migrations/mysql/meta/0028_snapshot.json
Normal file
1833
packages/db/migrations/mysql/meta/0028_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -197,6 +197,13 @@
|
||||
"when": 1739915526818,
|
||||
"tag": "0027_acoustic_karma",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 28,
|
||||
"version": "5",
|
||||
"when": 1740086765989,
|
||||
"tag": "0028_add_app_ping_url",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
1
packages/db/migrations/sqlite/0028_add_app_ping_url.sql
Normal file
1
packages/db/migrations/sqlite/0028_add_app_ping_url.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE `app` ADD `ping_url` text;
|
||||
1758
packages/db/migrations/sqlite/meta/0028_snapshot.json
Normal file
1758
packages/db/migrations/sqlite/meta/0028_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -197,6 +197,13 @@
|
||||
"when": 1739915486467,
|
||||
"tag": "0027_wooden_blizzard",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 28,
|
||||
"version": "6",
|
||||
"when": 1740086746417,
|
||||
"tag": "0028_add_app_ping_url",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -376,6 +376,7 @@ export const apps = mysqlTable("app", {
|
||||
description: text(),
|
||||
iconUrl: text().notNull(),
|
||||
href: text(),
|
||||
pingUrl: text(),
|
||||
});
|
||||
|
||||
export const integrationItems = mysqlTable(
|
||||
|
||||
@@ -361,6 +361,7 @@ export const apps = sqliteTable("app", {
|
||||
description: text(),
|
||||
iconUrl: text().notNull(),
|
||||
href: text(),
|
||||
pingUrl: text(),
|
||||
});
|
||||
|
||||
export const integrationItems = sqliteTable(
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import type { ChangeEventHandler } from "react";
|
||||
import { useRef } from "react";
|
||||
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 { useZodForm } from "@homarr/form";
|
||||
@@ -39,6 +41,7 @@ export const AppForm = ({
|
||||
description: initialValues?.description ?? "",
|
||||
iconUrl: initialValues?.iconUrl ?? "",
|
||||
href: initialValues?.href ?? "",
|
||||
pingUrl: initialValues?.pingUrl ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -54,6 +57,17 @@ export const AppForm = ({
|
||||
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 (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
@@ -62,6 +76,18 @@ export const AppForm = ({
|
||||
<Textarea {...form.getInputProps("description")} label={t("app.field.description.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">
|
||||
{showBackToOverview && (
|
||||
<Button variant="default" component={Link} href="/manage/apps">
|
||||
|
||||
@@ -52,6 +52,7 @@ export const AddDockerAppToHomarrModal = createModal<AddDockerAppToHomarrProps>(
|
||||
iconUrl: container.iconUrl,
|
||||
description: null,
|
||||
href: form.values.containerUrls[index] ?? null,
|
||||
pingUrl: null,
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -120,6 +120,7 @@ const addMappingFor = <TApp extends OldmarrApp | BookmarkApp>(
|
||||
href: existing.href,
|
||||
iconUrl: existing.iconUrl,
|
||||
description: existing.description,
|
||||
pingUrl: existing.pingUrl,
|
||||
exists: true,
|
||||
});
|
||||
continue;
|
||||
@@ -144,6 +145,7 @@ const convertApp = (app: OldmarrApp): DbAppWithoutId => ({
|
||||
href: app.behaviour.externalUrl === "" ? app.url : app.behaviour.externalUrl,
|
||||
iconUrl: app.appearance.iconUrl,
|
||||
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 => ({
|
||||
...app,
|
||||
description: null,
|
||||
pingUrl: null,
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export const mapOldmarrApp = (app: OldmarrApp): InferSelectModel<typeof apps> =>
|
||||
iconUrl: app.appearance.iconUrl,
|
||||
description: app.behaviour.tooltipDescription ?? null,
|
||||
href: app.behaviour.externalUrl || app.url,
|
||||
pingUrl: app.url.length > 0 ? app.url : null,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -23,5 +24,6 @@ export const mapOldmarrBookmarkApp = (
|
||||
iconUrl: app.iconUrl,
|
||||
description: null,
|
||||
href: app.href,
|
||||
pingUrl: null,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -600,6 +600,12 @@
|
||||
},
|
||||
"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": {
|
||||
|
||||
@@ -17,6 +17,14 @@ const manageAppSchema = z.object({
|
||||
.or(z.literal(""))
|
||||
.transform((value) => (value.length === 0 ? null : value))
|
||||
.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() }));
|
||||
|
||||
@@ -97,7 +97,7 @@ export default function AppWidget({ options, isEditMode }: WidgetComponentProps<
|
||||
</Tooltip.Floating>
|
||||
{options.pingEnabled && !settings.forceDisableStatus && !board.disableStatus && app.href ? (
|
||||
<Suspense fallback={<PingDot icon={IconLoader} color="blue" tooltip={`${t("common.action.loading")}…`} />}>
|
||||
<PingIndicator href={app.href} />
|
||||
<PingIndicator href={app.pingUrl ?? app.href} />
|
||||
</Suspense>
|
||||
) : null}
|
||||
</AppLink>
|
||||
|
||||
@@ -9,7 +9,14 @@ import type { SortableItemListInput } from "../options";
|
||||
import { AppSelectModal } from "./app-select-modal";
|
||||
|
||||
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
|
||||
>["AddButton"] = ({ addItem, values }) => {
|
||||
const { openModal } = useModalAction(AppSelectModal);
|
||||
|
||||
Reference in New Issue
Block a user