fix(onboarding): align detected app setup

This commit is contained in:
Thomas Camlong authored and ajnart committed 2026-08-14 01:20:28 +02:00
1 parent ad0bb41fe4
commit def3a3a6fa
5 files changed
+57 -53

No files matched your search

@@ -69,7 +69,8 @@ Homepage metadata: once `homarr.name` is present, use `homarr.*` labels for the
## What onboarding creates
In **Connect**, label-discovered apps and integrations appear before image-based suggestions. You can import multiple
services in one pass and edit every suggested address.
services in one pass and edit every suggested address. For image-matched apps, Homarr shows the detected service type
and uses it when building host, subdomain, or reverse-proxy path suggestions.
When setup completes, Homarr rechecks the selected container sources and then:
@@ -88,6 +88,7 @@ interface DiscoveredApp {
suggestedUrl: string;
publishedPort: number | null;
iconUrl: string | null;
detectedType?: string;
source: "label" | "docker";
group?: string;
boardName?: string;
@@ -207,8 +208,8 @@ export const onboardRouter = createTRPCRouter({
const normalized = imageName.toLowerCase().trim();
if (normalized.length < 3) return null;
return (
dbIcons.find((icon) => icon.name.toLowerCase() === normalized)?.url ??
dbIcons.find((icon) => icon.name.toLowerCase().startsWith(normalized))?.url ??
dbIcons.find((icon) => icon.name.toLowerCase() === normalized) ??
dbIcons.find((icon) => icon.name.toLowerCase().startsWith(normalized)) ??
null
);
};
@@ -257,7 +258,7 @@ export const onboardRouter = createTRPCRouter({
const imageName = extractContainerImageName(container.Image);
const containerName = container.Names[0]?.split("/")[1] ?? "Unknown";
const dbIcon = strictIconMatch(imageName);
const iconUrl = dbIcon ?? cdnIconUrl(imageName.toLowerCase());
const iconUrl = dbIcon?.url ?? cdnIconUrl(imageName.toLowerCase());
const { url: suggestedUrl, publishedPort } = buildSuggestedUrl(container.Ports, container.host);
const kind = matchIntegrationKindFromContainer({ image: container.Image, name: containerName });
const sourceId = createDockerSourceId(container.host, container.Id);
@@ -271,6 +272,7 @@ export const onboardRouter = createTRPCRouter({
suggestedUrl,
publishedPort,
iconUrl,
detectedType: dbIcon.name,
source: "docker",
host: container.host,
});
@@ -181,6 +181,11 @@
border: 1px solid color-mix(in srgb, var(--mantine-color-default-border) 76%, transparent);
}
.discoveryChoice {
width: 100%;
min-height: 4.25rem;
}
.discoveryChoice[data-checked] {
border-color: var(--mantine-primary-color-filled);
background: color-mix(in srgb, var(--mantine-primary-color-light) 62%, var(--mantine-color-body));
+43 -45
View File
@@ -269,7 +269,9 @@ export const SetupStudio = ({ environment, assistantConfiguration }: OnboardingS
resolveDiscoveredAppUrl(
appUrlOverrides[app.sourceId],
app.suggestedUrl,
serverOrigin ? buildAppUrl(app.containerName, serverOrigin, urlMode, app.publishedPort ?? undefined) : null,
serverOrigin
? buildAppUrl(app.detectedType ?? app.containerName, serverOrigin, urlMode, app.publishedPort ?? undefined)
: null,
),
]),
),
@@ -1148,7 +1150,7 @@ const Connections = (props: StudioSectionProps) => {
{integration.containerName}
</Text>
<Text size="xs" c="dimmed" truncate>
{getIntegrationName(integration.kind)} · {integration.suggestedUrl || t("addressNeeded")}
{getIntegrationName(integration.kind)}
</Text>
</Stack>
</Group>
@@ -1165,7 +1167,7 @@ const Connections = (props: StudioSectionProps) => {
{props.discoveredApps.map((app) => {
const selected = props.selectedAppIds.includes(app.sourceId);
return (
<Stack key={app.sourceId} gap="xs">
<Stack key={app.sourceId} gap="xs" h="100%">
<Checkbox.Card
className={classes.discoveryChoice}
value={app.sourceId}
@@ -1181,29 +1183,24 @@ const Connections = (props: StudioSectionProps) => {
p="sm"
radius="md"
>
<Group wrap="nowrap">
<Group wrap="nowrap" w="100%">
<Checkbox.Indicator />
<Avatar src={app.iconUrl} size="sm" radius="sm">
{app.containerName.at(0)}
</Avatar>
<Stack gap={0} miw={0}>
<Group gap="xs" wrap="nowrap">
<Text size="sm" fw={600} truncate>
{app.containerName}
</Text>
<Badge size="xs" variant="light" color={app.source === "label" ? "blue" : "gray"}>
{app.source === "label" ? t("dockerLabelSource") : t("dockerImageSource")}
</Badge>
</Group>
<Stack gap={0} miw={0} style={{ flex: 1 }}>
<Text size="sm" fw={600} truncate>
{app.containerName}
</Text>
<Text size="xs" c="dimmed" truncate>
{props.discoveredAppUrls[app.sourceId] || t("addressNeeded")}
{app.detectedType ?? t("appType")}
</Text>
</Stack>
</Group>
</Checkbox.Card>
{selected ? (
<TextInput
label={t("appUrl", { name: app.containerName })}
label={t("url")}
value={props.discoveredAppUrls[app.sourceId] ?? ""}
onChange={(event) => props.setDiscoveredAppUrl(app.sourceId, event.currentTarget.value)}
error={props.appErrorSourceId === app.sourceId ? props.appError : undefined}
@@ -1218,6 +1215,12 @@ const Connections = (props: StudioSectionProps) => {
</Fieldset>
) : null}
{props.appError ? (
<Alert color="red" role="alert">
{props.appError}
</Alert>
) : null}
<Fieldset legend={t("otherIntegrations")}>
<IntegrationMultiSelectGrid
selectedKinds={props.selectedKinds}
@@ -1227,36 +1230,31 @@ const Connections = (props: StudioSectionProps) => {
/>
</Fieldset>
{props.appError ? (
<Alert color="red" role="alert">
{props.appError}
</Alert>
) : null}
{props.drafts.length > 0 ? (
<Accordion
className={classes.integrationAccordion}
variant="separated"
multiple
transitionDuration={200}
order={3}
mb="md"
defaultValue={props.drafts.slice(0, 1).map((draft) => draft.id)}
>
{props.drafts.map((draft) => (
<IntegrationEditor
key={draft.id}
draft={draft}
update={(patch) => props.updateDraft(draft.id, patch)}
selectSecretOption={(option) => props.selectSecretOption(draft, option)}
/>
))}
</Accordion>
) : (
<Alert variant="light" icon={<IconPlugConnected size={18} />} my="xs">
{t("empty")}
</Alert>
)}
<Fieldset legend={t("configureIntegrations")}>
{props.drafts.length > 0 ? (
<Accordion
className={classes.integrationAccordion}
variant="separated"
multiple
transitionDuration={200}
order={3}
defaultValue={props.drafts.slice(0, 1).map((draft) => draft.id)}
>
{props.drafts.map((draft) => (
<IntegrationEditor
key={draft.id}
draft={draft}
update={(patch) => props.updateDraft(draft.id, patch)}
selectSecretOption={(option) => props.selectSecretOption(draft, option)}
/>
))}
</Accordion>
) : (
<Alert variant="light" icon={<IconPlugConnected size={18} />}>
{t("empty")}
</Alert>
)}
</Fieldset>
</Stack>
);
};
+2 -4
View File
@@ -112,11 +112,9 @@
"detectedIntegrationLabel": "Import {name}, detected as {kind}",
"detectedAppLabel": "Import detected app {name}",
"otherIntegrations": "Add another integration",
"configureIntegrations": "Configure integrations",
"discoveredApps": "Detected apps ({count})",
"dockerLabelSource": "Docker label",
"dockerImageSource": "Docker image",
"addressNeeded": "Address needed",
"appUrl": "Address for {name}",
"appType": "App",
"appAddressRequired": "Add an HTTP or HTTPS address for {name}, or remove it from the selection.",
"empty": "No integrations selected. Homarr will still create a useful starter board with general widgets.",
"dockerSource": "Docker",