feat(onboarding): enhance integration setup with Docker discovery and URL templating (#5607)

This commit is contained in:
Thomas authored and GitHub committed 2026-06-05 16:50:35 +02:00
1 parent 0a6467979a
commit dd7a4908a3
68 files changed
+2922 -434

No files matched your search

@@ -1,11 +1,12 @@
import { useMemo, useState } from "react";
import { Button, Card, Center, Grid, Input, Stack, Text } from "@mantine/core";
import { IconPlus, IconSearch } from "@tabler/icons-react";
import { Box, Button, Card, Center, Group, Stack, Text, Tooltip } from "@mantine/core";
import { IconPlus } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { createModal, useModalAction } from "@homarr/modals";
import { createModal, modalSizeSelect, useModalAction } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client";
import { SelectGridLayout, selectGridCardHeight } from "@homarr/ui";
import { QuickAddAppModal } from "./quick-add-app/quick-add-app-modal";
@@ -29,96 +30,119 @@ export const AppSelectModal = createModal<AppSelectModalProps>(({ actions, inner
);
const handleSelect = (app: RouterOutputs["app"]["selectable"][number]) => {
if (innerProps.onSelect) {
innerProps.onSelect(app);
}
innerProps.onSelect?.(app);
actions.closeModal();
};
const handleAddNewApp = () => {
openQuickAddAppModal({
onClose(app) {
if (innerProps.onSelect) {
innerProps.onSelect(app);
}
innerProps.onSelect?.(app);
actions.closeModal();
},
});
};
return (
<Stack>
<Input
value={search}
onChange={(event) => setSearch(event.currentTarget.value)}
leftSection={<IconSearch />}
placeholder={`${t("app.action.select.search")}...`}
data-autofocus
onKeyDown={(event) => {
if (event.key === "Enter" && filteredApps.length === 1 && filteredApps[0]) {
handleSelect(filteredApps[0]);
}
}}
/>
<SelectGridLayout
search={search}
onSearchChange={setSearch}
placeholder={`${t("app.action.select.search")}...`}
onSearchKeyDown={(event) => {
if (event.key === "Enter" && filteredApps.length === 1 && filteredApps[0]) {
handleSelect(filteredApps[0]);
}
}}
>
{innerProps.withCreate && (
<Card
h={selectGridCardHeight}
withBorder
pos="relative"
style={{ overflow: "hidden", "--_hover-opacity": "0" }}
onMouseEnter={(e) => e.currentTarget.style.setProperty("--_hover-opacity", "1")}
onMouseLeave={(e) => e.currentTarget.style.setProperty("--_hover-opacity", "0")}
>
<Stack h="100%" gap="xs">
<Group gap="sm" wrap="nowrap" align="flex-start">
<IconPlus size={22} style={{ flexShrink: 0, marginTop: 2 }} />
<Text lh={1.2} style={{ whiteSpace: "normal" }} fw={500} size="sm" lineClamp={2}>
{t("app.action.create.title")}
</Text>
</Group>
<Text lh={1.2} style={{ whiteSpace: "normal" }} size="xs" c="dimmed" lineClamp={1}>
{t("app.action.create.description")}
</Text>
</Stack>
<Box
pos="absolute"
bottom={0}
left={0}
right={0}
p="xs"
style={{
opacity: "var(--_hover-opacity)",
transition: "opacity 150ms ease",
background: "linear-gradient(transparent, var(--mantine-color-body) 30%)",
}}
>
<Button onClick={handleAddNewApp} variant="light" size="xs" fullWidth>
{t("app.action.create.action")}
</Button>
</Box>
</Card>
)}
<Grid>
{innerProps.withCreate && (
<Grid.Col span={{ xs: 12, sm: 4, md: 3 }}>
<Card h="100%">
<Stack justify="space-between" h="100%">
<Stack gap="xs">
<Center>
<IconPlus size={24} />
</Center>
<Text lh={1.2} style={{ whiteSpace: "normal" }} ta="center">
{t("app.action.create.title")}
</Text>
<Text lh={1.2} style={{ whiteSpace: "normal" }} size="xs" ta="center" c="dimmed">
{t("app.action.create.description")}
</Text>
</Stack>
<Button onClick={handleAddNewApp} variant="light" size="xs" mt="auto" fullWidth>
{t("app.action.create.action")}
</Button>
</Stack>
</Card>
</Grid.Col>
)}
{filteredApps.map((app) => (
<Card
key={app.id}
h={selectGridCardHeight}
withBorder
pos="relative"
style={{ overflow: "hidden", "--_hover-opacity": "0" }}
onMouseEnter={(e) => e.currentTarget.style.setProperty("--_hover-opacity", "1")}
onMouseLeave={(e) => e.currentTarget.style.setProperty("--_hover-opacity", "0")}
>
<Stack h="100%" gap="xs">
<Group gap="sm" wrap="nowrap" align="flex-start">
<img src={app.iconUrl} alt={app.name} width={22} height={22} style={{ flexShrink: 0, marginTop: 2 }} />
<Text lh={1.2} style={{ whiteSpace: "normal" }} fw={500} size="sm" lineClamp={2}>
{app.name}
</Text>
</Group>
<Tooltip label={app.description} multiline w={250} disabled={!app.description}>
<Text lh={1.2} style={{ whiteSpace: "normal" }} size="xs" c="dimmed" lineClamp={1}>
{app.description ?? ""}
</Text>
</Tooltip>
</Stack>
<Box
pos="absolute"
bottom={0}
left={0}
right={0}
p="xs"
style={{
opacity: "var(--_hover-opacity)",
transition: "opacity 150ms ease",
background: "linear-gradient(transparent, var(--mantine-color-body) 30%)",
}}
>
<Button onClick={() => handleSelect(app)} variant="light" size="xs" fullWidth>
{t("app.action.select.action", { app: app.name })}
</Button>
</Box>
</Card>
))}
{filteredApps.map((app) => (
<Grid.Col key={app.id} span={{ xs: 12, sm: 4, md: 3 }}>
<Card h="100%">
<Stack justify="space-between" h="100%">
<Stack gap="xs">
<Center>
<img src={app.iconUrl} alt={app.name} width={24} height={24} />
</Center>
<Text lh={1.2} style={{ whiteSpace: "normal" }} ta="center">
{app.name}
</Text>
<Text lh={1.2} style={{ whiteSpace: "normal" }} size="xs" ta="center" c="dimmed">
{app.description ?? ""}
</Text>
</Stack>
<Button onClick={() => handleSelect(app)} variant="light" size="xs" mt="auto" fullWidth>
{t("app.action.select.action", { app: app.name })}
</Button>
</Stack>
</Card>
</Grid.Col>
))}
{filteredApps.length === 0 && !isPending && (
<Grid.Col span={12}>
<Center p="xl">
<Text c="dimmed">{t("app.action.select.noResults")}</Text>
</Center>
</Grid.Col>
)}
</Grid>
</Stack>
{filteredApps.length === 0 && !isPending && (
<Center p="xl">
<Text c="dimmed">{t("app.action.select.noResults")}</Text>
</Center>
)}
</SelectGridLayout>
);
}).withOptions({
defaultTitle: (t) => t("app.action.select.title"),
size: "xl",
size: modalSizeSelect,
});
@@ -3,7 +3,7 @@ import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { AppForm } from "@homarr/forms-collection";
import { createModal } from "@homarr/modals";
import { createModal, modalSizeForm } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import type { appManageSchema } from "@homarr/validation/app";
@@ -54,4 +54,5 @@ export const QuickAddAppModal = createModal<QuickAddAppModalProps>(({ actions, i
defaultTitle(t) {
return t("board.action.quickCreateApp.modal.title");
},
size: modalSizeForm,
});