Files
homarr/packages/widgets/src/smart-home/execute-automation/component.tsx
T

172 lines
6.0 KiB
TypeScript

"use client";
import React from "react";
import {
Box,
Center,
LoadingOverlay,
Overlay,
Stack,
Text,
ThemeIcon,
UnstyledButton,
VisuallyHidden,
} from "@mantine/core";
import { useDisclosure, useTimeout } from "@mantine/hooks";
import { IconAutomation, IconCheck } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useIntegrationsWithInteractAccess } from "@homarr/auth/client";
import { useRegisterSpotlightContextActions } from "@homarr/spotlight";
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../../definition";
import { isSmartHomeTiny } from "../layout";
export default function SmartHomeTriggerAutomationWidget({
options,
integrationIds,
isEditMode,
width,
height,
displayMode,
}: WidgetComponentProps<"smartHome-executeAutomation">) {
const integrationId = integrationIds[0];
const canInteract = useIntegrationsWithInteractAccess().some(({ id }) => id === integrationId);
const [lastExecutedAt, setLastExecutedAt] = React.useState<Date | null>(null);
const [isShowSuccess, { open: showSuccess, close: closeSuccess }] = useDisclosure();
const { start } = useTimeout(() => {
closeSuccess();
}, 1000);
const { mutateAsync, isPending, error } = clientApi.widget.smartHome.executeAutomation.useMutation({
onSuccess: () => {
setLastExecutedAt(new Date());
showSuccess();
start();
},
});
const handleClick = React.useCallback(async () => {
if (isEditMode || isPending || !integrationId || !canInteract) {
return;
}
try {
await mutateAsync({
automationId: options.automationId,
integrationId,
});
} catch {
// The mutation exposes the error below and remains retryable.
}
}, [canInteract, integrationId, isEditMode, isPending, mutateAsync, options.automationId]);
const t = useI18n();
const locale = useCurrentIntlLocale();
useRegisterSpotlightContextActions(
`smartHome-automation-${options.automationId}`,
[
{
id: options.automationId,
name: t("widget.smartHome-executeAutomation.spotlightAction.run", { name: options.displayName }),
icon: IconAutomation,
interaction() {
return {
type: "javaScript",
// eslint-disable-next-line no-restricted-syntax
async onSelect() {
await handleClick();
},
};
},
disabled: !integrationId || !canInteract || isPending,
},
],
[canInteract, handleClick, integrationId, isPending, options.automationId, options.displayName],
);
const isTiny = isSmartHomeTiny(width, height);
return (
<Box pos="relative" w="100%" h="100%">
<VisuallyHidden component="output">
{isPending
? t("widget.smartHome-executeAutomation.status.running")
: isShowSuccess
? t("widget.smartHome-executeAutomation.status.success")
: ""}
</VisuallyHidden>
{error && (
<VisuallyHidden role="alert">{t("widget.smartHome-executeAutomation.error.executeFailed")}</VisuallyHidden>
)}
<UnstyledButton
onClick={handleClick}
disabled={!integrationId || !canInteract || isPending}
aria-label={t("widget.smartHome-executeAutomation.spotlightAction.run", { name: options.displayName })}
style={{
cursor: !isEditMode && integrationId && canInteract ? "pointer" : "initial",
pointerEvents: isEditMode ? "none" : undefined,
}}
aria-description={error ? t("widget.smartHome-executeAutomation.error.executeFailed") : undefined}
w="100%"
h="100%"
>
{isShowSuccess && (
<Overlay>
<Center w="100%" h="100%">
<ThemeIcon variant="filled" color="green" size="xl" radius="xl">
<IconCheck style={{ width: "70%", height: "70%" }} stroke={1.5} />
</ThemeIcon>
</Center>
</Overlay>
)}
<LoadingOverlay visible={isPending} />
{error && displayMode !== "advanced" && (
<Overlay>
<Center w="100%" h="100%" p="xs">
<Text size="xs" c="red" ta="center" lineClamp={3}>
{t("widget.smartHome-executeAutomation.error.executeFailed")}
</Text>
</Center>
</Overlay>
)}
<Center w="100%" h="100%">
<Stack align="center" gap={isTiny ? 6 : "md"} p="xs" maw="100%">
<ThemeIcon variant="light" size={isTiny ? "md" : "xl"} radius="xl">
<IconAutomation size={isTiny ? 14 : 24} />
</ThemeIcon>
<Text ta="center" fw={600} fz={isTiny ? "xs" : "sm"} lineClamp={2} maw="100%">
{options.displayName}
</Text>
{displayMode === "advanced" && (
<Stack gap={2} align="center">
<Text ta="center" size="xs" c="dimmed">
{t("widget.smartHome-executeAutomation.advanced.automationId", { id: options.automationId })}
</Text>
<Text ta="center" size="xs" c={canInteract ? "dimmed" : "orange"}>
{t(
canInteract
? "widget.smartHome-executeAutomation.advanced.ready"
: "widget.smartHome-executeAutomation.advanced.noPermission",
)}
</Text>
{lastExecutedAt && (
<Text ta="center" size="xs" c="dimmed">
{t("widget.smartHome-executeAutomation.advanced.lastExecuted", {
time: lastExecutedAt.toLocaleTimeString(locale),
})}
</Text>
)}
{error && (
<Text ta="center" size="xs" c="red">
{t("widget.smartHome-executeAutomation.error.executeFailed")}
</Text>
)}
</Stack>
)}
</Stack>
</Center>
</UnstyledButton>
</Box>
);
}