fix(init): improve integrations step UX
- Replace plain Base URL input with a prominent Alert callout so users don't overlook it - Add disableScroll prop to SelectGridLayout and use it in onboarding so integrations expand the page instead of scrolling - Fix Collapse `in` → `expanded` prop for Mantine v9 migration in both the URL helpers panel and the Docker discovery panel
This commit is contained in:
1 parent
1065e30c8d
commit
3ff3d41d7c
4 files changed
+61
-55
No files matched your search
@@ -142,7 +142,7 @@ export const DockerDiscoveryIndicator = ({
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
|
||||
<Collapse in={expanded}>
|
||||
<Collapse expanded={expanded}>
|
||||
<Stack gap="xs" mt="sm">
|
||||
{integrations.length > 0 && (
|
||||
<Stack gap={4}>
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
ActionIcon,
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Center,
|
||||
Collapse,
|
||||
Group,
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { IconArrowRight, IconSettings } from "@tabler/icons-react";
|
||||
import { IconArrowRight, IconInfoCircle, IconSettings } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type { IntegrationKind, UrlTemplateMode } from "@homarr/definitions";
|
||||
@@ -29,6 +29,7 @@ import { IntegrationAvatar } from "@homarr/ui";
|
||||
|
||||
import { NewIntegrationForm } from "~/app/[locale]/manage/integrations/new/_integration-new-form";
|
||||
import { IntegrationMultiSelectGrid } from "~/components/integration/integration-multi-select-grid";
|
||||
import { InitStepCard } from "../../_components/init-step-card";
|
||||
import { DockerDiscoveryIndicator } from "./docker-discovery-panel";
|
||||
|
||||
type Phase = "select" | "configure" | "done";
|
||||
@@ -41,8 +42,6 @@ interface IntegrationDraft {
|
||||
source: "manual" | "docker";
|
||||
}
|
||||
|
||||
const CARD_WIDTH = 64 * 16;
|
||||
|
||||
export const InitIntegrations = () => {
|
||||
const [phase, setPhase] = useState<Phase>("select");
|
||||
const [drafts, setDrafts] = useState<Map<IntegrationKind, IntegrationDraft>>(new Map());
|
||||
@@ -171,14 +170,14 @@ export const InitIntegrations = () => {
|
||||
|
||||
if (phase === "done") {
|
||||
return (
|
||||
<Card w={CARD_WIDTH} maw="90vw" withBorder>
|
||||
<InitStepCard>
|
||||
<Center py="xl">
|
||||
<Stack align="center" gap="md">
|
||||
<Loader size="lg" />
|
||||
<Text>{t("configure.finishing")}</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
</Card>
|
||||
</InitStepCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -200,7 +199,7 @@ export const InitIntegrations = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card w={CARD_WIDTH} maw="90vw" withBorder>
|
||||
<InitStepCard>
|
||||
<Stack>
|
||||
<Text>{t("description")}</Text>
|
||||
|
||||
@@ -212,32 +211,32 @@ export const InitIntegrations = () => {
|
||||
isLoading={isDockerLoading}
|
||||
/>
|
||||
|
||||
<Stack gap="xs">
|
||||
<Text size="sm" fw={500}>
|
||||
{t("urlTemplate.label")}
|
||||
</Text>
|
||||
<Group gap="sm" grow>
|
||||
<TextInput
|
||||
placeholder={t("urlTemplate.placeholder")}
|
||||
value={baseHost}
|
||||
onChange={(e) => setBaseHost(e.currentTarget.value)}
|
||||
/>
|
||||
<SegmentedControl
|
||||
value={urlMode}
|
||||
onChange={(value) => setUrlMode(value as UrlTemplateMode)}
|
||||
data={[
|
||||
{ label: t("urlTemplate.mode.subdomain"), value: "subdomain" },
|
||||
{ label: t("urlTemplate.mode.hostPort"), value: "hostPort" },
|
||||
]}
|
||||
style={{ flex: "0 0 auto" }}
|
||||
/>
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed">
|
||||
{baseHost
|
||||
? t("urlTemplate.preview", { example: buildIntegrationUrl("sonarr", baseHost, urlMode) })
|
||||
: t("urlTemplate.hint")}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Alert variant="light" color="blue" icon={<IconInfoCircle />} title={t("urlTemplate.label")}>
|
||||
<Stack gap="xs">
|
||||
<Text size="sm">{t("urlTemplate.hint")}</Text>
|
||||
<Group gap="sm" grow>
|
||||
<TextInput
|
||||
placeholder={t("urlTemplate.placeholder")}
|
||||
value={baseHost}
|
||||
onChange={(e) => setBaseHost(e.currentTarget.value)}
|
||||
/>
|
||||
<SegmentedControl
|
||||
value={urlMode}
|
||||
onChange={(value) => setUrlMode(value as UrlTemplateMode)}
|
||||
data={[
|
||||
{ label: t("urlTemplate.mode.subdomain"), value: "subdomain" },
|
||||
{ label: t("urlTemplate.mode.hostPort"), value: "hostPort" },
|
||||
]}
|
||||
style={{ flex: "0 0 auto" }}
|
||||
/>
|
||||
</Group>
|
||||
{baseHost && (
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("urlTemplate.preview", { example: buildIntegrationUrl("sonarr", baseHost, urlMode) })}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Alert>
|
||||
|
||||
<IntegrationMultiSelectGrid
|
||||
selectedKinds={selectedKinds}
|
||||
@@ -259,7 +258,7 @@ export const InitIntegrations = () => {
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
</InitStepCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -300,7 +299,7 @@ const ConfigurePhase = ({
|
||||
: draft?.initialUrl;
|
||||
|
||||
return (
|
||||
<Card w={CARD_WIDTH} maw="90vw" withBorder>
|
||||
<InitStepCard>
|
||||
<Stack>
|
||||
<Group justify="space-between" align="center">
|
||||
<Group gap="xs">
|
||||
@@ -321,7 +320,7 @@ const ConfigurePhase = ({
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Collapse in={helpersOpen}>
|
||||
<Collapse expanded={helpersOpen}>
|
||||
<Stack
|
||||
gap="xs"
|
||||
p="xs"
|
||||
@@ -369,6 +368,6 @@ const ConfigurePhase = ({
|
||||
isOnboarding
|
||||
/>
|
||||
</Stack>
|
||||
</Card>
|
||||
</InitStepCard>
|
||||
);
|
||||
};
|
||||
@@ -51,6 +51,7 @@ export const IntegrationMultiSelectGrid = ({
|
||||
search={search}
|
||||
onSearchChange={setSearch}
|
||||
placeholder={`${t("integration.page.list.search")}...`}
|
||||
disableScroll={onboarding}
|
||||
>
|
||||
{filtered.map((integration) => {
|
||||
const isSelected = selectedKinds.has(integration.kind);
|
||||
|
||||
@@ -13,6 +13,7 @@ interface SelectGridLayoutProps {
|
||||
onSearchChange: (value: string) => void;
|
||||
placeholder: string;
|
||||
onSearchKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;
|
||||
disableScroll?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
@@ -21,22 +22,27 @@ export const SelectGridLayout = ({
|
||||
onSearchChange,
|
||||
placeholder,
|
||||
onSearchKeyDown,
|
||||
disableScroll = false,
|
||||
children,
|
||||
}: SelectGridLayoutProps) => (
|
||||
<Stack>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(event) => onSearchChange(event.currentTarget.value)}
|
||||
leftSection={<IconSearch />}
|
||||
placeholder={placeholder}
|
||||
data-autofocus
|
||||
onKeyDown={onSearchKeyDown}
|
||||
/>
|
||||
}: SelectGridLayoutProps) => {
|
||||
const grid = (
|
||||
<SimpleGrid cols={selectGridCols} spacing="sm">
|
||||
{children}
|
||||
</SimpleGrid>
|
||||
);
|
||||
|
||||
<ScrollArea.Autosize mah={selectGridScrollMaxHeight}>
|
||||
<SimpleGrid cols={selectGridCols} spacing="sm">
|
||||
{children}
|
||||
</SimpleGrid>
|
||||
</ScrollArea.Autosize>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(event) => onSearchChange(event.currentTarget.value)}
|
||||
leftSection={<IconSearch />}
|
||||
placeholder={placeholder}
|
||||
data-autofocus
|
||||
onKeyDown={onSearchKeyDown}
|
||||
/>
|
||||
|
||||
{disableScroll ? grid : <ScrollArea.Autosize mah={selectGridScrollMaxHeight}>{grid}</ScrollArea.Autosize>}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user