import type { Device } from '@seamapi/types/connect' import classNames from 'classnames' import { useCallback, useMemo, useState } from 'react' import { compareByCreatedAtDesc } from 'lib/dates.js' import { type CommonProps, withRequiredCommonProps, } from 'lib/seam/components/common-props.js' import { NestedDeviceDetails } from 'lib/seam/components/DeviceDetails/DeviceDetails.js' import { type AccountFilter, type DeviceFilter, DeviceHealthBar, } from 'lib/seam/components/DeviceTable/DeviceHealthBar.js' import { DeviceRow } from 'lib/seam/components/DeviceTable/DeviceRow.js' import { useDevices } from 'lib/seam/devices/use-devices.js' import { isLockDevice } from 'lib/seam/locks/lock-device.js' import { isNoiseSensorDevice } from 'lib/seam/noise-sensors/noise-sensor-device.js' import { isThermostatDevice } from 'lib/seam/thermostats/thermostat-device.js' import { useComponentTelemetry } from 'lib/telemetry/index.js' import { ContentHeader } from 'lib/ui/layout/ContentHeader.js' import { LoadingToast } from 'lib/ui/LoadingToast/LoadingToast.js' import { Snackbar } from 'lib/ui/Snackbar/Snackbar.js' import { EmptyPlaceholder } from 'lib/ui/Table/EmptyPlaceholder.js' import { TableBody } from 'lib/ui/Table/TableBody.js' import { TableHeader } from 'lib/ui/Table/TableHeader.js' import { TableTitle } from 'lib/ui/Table/TableTitle.js' import { SearchTextField } from 'lib/ui/TextField/SearchTextField.js' import { Caption } from 'lib/ui/typography/Caption.js' export interface DeviceTableProps extends CommonProps { deviceIds?: string[] connectedAccountIds?: string[] disableSearch?: boolean deviceFilter?: (device: Device, searchInputValue: string) => boolean deviceComparator?: (deviceA: Device, deviceB: Device) => number onDeviceClick?: (deviceId: string) => void preventDefaultOnDeviceClick?: boolean heading?: string | null } export const defaultDeviceFilter = ( device: Device, searchInputValue: string ): boolean => { const value = searchInputValue.trim() if (value === '') return true return device.properties.name.toLowerCase().includes(value) } export const NestedDeviceTable = withRequiredCommonProps(DeviceTable) export function DeviceTable({ deviceIds, connectedAccountIds, disableSearch = false, onDeviceClick = () => {}, preventDefaultOnDeviceClick = false, deviceFilter = defaultDeviceFilter, deviceComparator = compareByCreatedAtDesc, heading = t.devices, errorFilter = () => true, warningFilter = () => true, disableLockUnlock = false, disableCreateAccessCode = false, disableEditAccessCode = false, disableDeleteAccessCode = false, disableResourceIds = false, disableConnectedAccountInformation = false, onBack, className, }: DeviceTableProps = {}): JSX.Element { useComponentTelemetry('DeviceTable') const { devices, isInitialLoading, isError, refetch } = useDevices({ device_ids: deviceIds, connected_account_ids: connectedAccountIds, }) const [selectedDeviceId, setSelectedDeviceId] = useState(null) const [searchInputValue, setSearchInputValue] = useState('') const filteredDevices = useMemo( () => devices ?.filter( (device) => isLockDevice(device) || isThermostatDevice(device) || isNoiseSensorDevice(device) ) ?.filter((device) => deviceFilter(device, searchInputValue)) ?.sort(deviceComparator) ?? [], [devices, searchInputValue, deviceFilter, deviceComparator] ) const handleDeviceClick = useCallback( (deviceId: string): void => { onDeviceClick(deviceId) if (preventDefaultOnDeviceClick) return setSelectedDeviceId(deviceId) }, [onDeviceClick, preventDefaultOnDeviceClick, setSelectedDeviceId] ) if (selectedDeviceId != null) { return ( { setSelectedDeviceId(null) }} className={className} /> ) } return (
{heading != null ? ( {heading} ({filteredDevices.length}) ) : (
)}
{!disableSearch && ( )} { void refetch() }, }} disableCloseButton />
) } function Content(props: { devices: Device[] onDeviceClick: (deviceId: string) => void errorFilter: (error: any) => boolean }): JSX.Element { const { devices, onDeviceClick, errorFilter } = props const [filter, setFilter] = useState( null ) if (devices.length === 0) { return {t.noDevicesMessage} } const filteredDevices = devices.filter((device) => { if (filter === null) { return true } if (filter === 'account_issues') { return ( device.errors.filter((error) => 'is_connected_account_error' in error) .length > 0 ) } if (filter === 'device_issues') { return ( device.errors.filter((error) => 'is_device_error' in error).length > 0 ) } return true }) return ( <> {filteredDevices.map((device) => ( { onDeviceClick(device.device_id) }} /> ))} ) } const t = { devices: 'Devices', noDevicesMessage: 'Sorry, no devices were found', loading: 'Loading devices', tryAgain: 'Try again', fallbackErrorMessage: 'Devices could not be loaded', }