@seamapi/react 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.css +140 -0
- package/index.css.map +1 -1
- package/index.min.css +1 -1
- package/index.min.css.map +1 -1
- package/lib/icons/ArrowRestart.d.ts +2 -0
- package/lib/icons/ArrowRestart.js +5 -0
- package/lib/icons/ArrowRestart.js.map +1 -0
- package/lib/icons/Check.d.ts +2 -0
- package/lib/icons/Check.js +3 -0
- package/lib/icons/Check.js.map +1 -0
- package/lib/icons/ExclamationCircle.d.ts +2 -0
- package/lib/icons/ExclamationCircle.js +5 -0
- package/lib/icons/ExclamationCircle.js.map +1 -0
- package/lib/icons/ExclamationCircleOutline.d.ts +2 -0
- package/lib/icons/ExclamationCircleOutline.js +5 -0
- package/lib/icons/ExclamationCircleOutline.js.map +1 -0
- package/lib/icons/TriangleWarning.d.ts +2 -0
- package/lib/icons/TriangleWarning.js +5 -0
- package/lib/icons/TriangleWarning.js.map +1 -0
- package/lib/ui/AccessCodeTable/AccessCodeTable.js +24 -11
- package/lib/ui/AccessCodeTable/AccessCodeTable.js.map +1 -1
- package/lib/ui/Alert/Alert.d.ts +11 -0
- package/lib/ui/Alert/Alert.js +12 -0
- package/lib/ui/Alert/Alert.js.map +1 -0
- package/lib/ui/Alert/Alerts.d.ts +8 -0
- package/lib/ui/Alert/Alerts.js +8 -0
- package/lib/ui/Alert/Alerts.js.map +1 -0
- package/lib/ui/DeviceDetails/DeviceDetails.js +23 -1
- package/lib/ui/DeviceDetails/DeviceDetails.js.map +1 -1
- package/lib/ui/DeviceTable/DeviceHealthBar.d.ts +9 -0
- package/lib/ui/DeviceTable/DeviceHealthBar.js +29 -0
- package/lib/ui/DeviceTable/DeviceHealthBar.js.map +1 -0
- package/lib/ui/DeviceTable/DeviceRow.d.ts +11 -0
- package/lib/ui/DeviceTable/DeviceRow.js +24 -0
- package/lib/ui/DeviceTable/DeviceRow.js.map +1 -0
- package/lib/ui/DeviceTable/DeviceTable.js +23 -19
- package/lib/ui/DeviceTable/DeviceTable.js.map +1 -1
- package/lib/ui/IconButton.d.ts +1 -1
- package/lib/ui/IconButton.js +3 -2
- package/lib/ui/IconButton.js.map +1 -1
- package/lib/ui/SupportedDevices/SupportedDevices.d.ts +1 -1
- package/lib/ui/SupportedDevices/SupportedDevices.js +1 -1
- package/lib/ui/SupportedDevices/SupportedDevices.js.map +1 -1
- package/lib/ui/Table/EmptyPlaceholder.d.ts +2 -0
- package/lib/ui/Table/EmptyPlaceholder.js +5 -0
- package/lib/ui/Table/EmptyPlaceholder.js.map +1 -0
- package/lib/ui/Table/TableFilterBar/TableFilterBar.d.ts +6 -0
- package/lib/ui/Table/TableFilterBar/TableFilterBar.js +11 -0
- package/lib/ui/Table/TableFilterBar/TableFilterBar.js.map +1 -0
- package/lib/ui/Table/TableFilterBar/TableFilterItem.d.ts +5 -0
- package/lib/ui/Table/TableFilterBar/TableFilterItem.js +9 -0
- package/lib/ui/Table/TableFilterBar/TableFilterItem.js.map +1 -0
- package/lib/ui/index.d.ts +1 -0
- package/lib/ui/index.js +1 -0
- package/lib/ui/index.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/icons/ArrowRestart.tsx +32 -0
- package/src/lib/icons/Check.tsx +15 -0
- package/src/lib/icons/ExclamationCircle.tsx +32 -0
- package/src/lib/icons/ExclamationCircleOutline.tsx +32 -0
- package/src/lib/icons/TriangleWarning.tsx +32 -0
- package/src/lib/ui/AccessCodeTable/AccessCodeTable.tsx +73 -40
- package/src/lib/ui/Alert/Alert.tsx +52 -0
- package/src/lib/ui/Alert/Alerts.tsx +22 -0
- package/src/lib/ui/DeviceDetails/DeviceDetails.tsx +37 -0
- package/src/lib/ui/DeviceTable/DeviceHealthBar.tsx +66 -0
- package/src/lib/ui/DeviceTable/DeviceRow.tsx +60 -0
- package/src/lib/ui/DeviceTable/DeviceTable.tsx +43 -42
- package/src/lib/ui/IconButton.tsx +6 -2
- package/src/lib/ui/SupportedDevices/SupportedDevices.tsx +1 -1
- package/src/lib/ui/Table/EmptyPlaceholder.tsx +12 -0
- package/src/lib/ui/Table/TableFilterBar/TableFilterBar.tsx +31 -0
- package/src/lib/ui/Table/TableFilterBar/TableFilterItem.tsx +24 -0
- package/src/lib/ui/index.ts +1 -0
- package/src/styles/_alert.scss +115 -0
- package/src/styles/_main.scss +2 -0
- package/src/styles/_tables.scss +65 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
import type { MouseEventHandler } from 'react'
|
|
3
|
+
|
|
4
|
+
import { ExclamationCircleIcon } from 'lib/icons/ExclamationCircle.js'
|
|
5
|
+
import { TriangleWarningIcon } from 'lib/icons/TriangleWarning.js'
|
|
6
|
+
|
|
7
|
+
export interface AlertProps {
|
|
8
|
+
variant: 'warning' | 'error'
|
|
9
|
+
message: string
|
|
10
|
+
action?: {
|
|
11
|
+
label: string
|
|
12
|
+
onClick: MouseEventHandler
|
|
13
|
+
}
|
|
14
|
+
className?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function Alert(props: AlertProps): JSX.Element {
|
|
18
|
+
const { variant, message, action, className, ...rest } = props
|
|
19
|
+
|
|
20
|
+
const handleClick: MouseEventHandler<HTMLButtonElement> = (ev) => {
|
|
21
|
+
action?.onClick(ev)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
className={classNames('seam-alert', `seam-${variant}-alert`, className)}
|
|
27
|
+
{...rest}
|
|
28
|
+
>
|
|
29
|
+
<div className='seam-alert-content'>
|
|
30
|
+
<div className='seam-alert-icon'>
|
|
31
|
+
{variant === 'warning' ? (
|
|
32
|
+
<TriangleWarningIcon />
|
|
33
|
+
) : (
|
|
34
|
+
<ExclamationCircleIcon />
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div className='seam-alert-message-wrap'>
|
|
39
|
+
<p className='seam-alert-message'>{message}</p>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{action != null && (
|
|
44
|
+
<div className='seam-alert-action-wrap'>
|
|
45
|
+
<button onClick={handleClick} className='seam-alert-action'>
|
|
46
|
+
{action.label}
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
|
|
3
|
+
import { Alert, type AlertProps } from 'lib/ui/Alert/Alert.js'
|
|
4
|
+
|
|
5
|
+
export interface AlertsProps {
|
|
6
|
+
alerts?: AlertProps[]
|
|
7
|
+
children?: React.ReactNode
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Alerts(props: AlertsProps): JSX.Element {
|
|
12
|
+
const { alerts, children, className, ...rest } = props
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className={classNames('seam-alerts', className)} {...rest}>
|
|
16
|
+
{alerts?.map((alert, index) => (
|
|
17
|
+
<Alert key={`${index}:${alert.message}`} {...alert} />
|
|
18
|
+
))}
|
|
19
|
+
{children}
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -7,6 +7,8 @@ import { useAccessCodes } from 'lib/seam/access-codes/use-access-codes.js'
|
|
|
7
7
|
import { isLockDevice } from 'lib/seam/devices/types.js'
|
|
8
8
|
import { useToggleLock } from 'lib/seam/devices/use-toggle-lock.js'
|
|
9
9
|
import { AccessCodeTable } from 'lib/ui/AccessCodeTable/AccessCodeTable.js'
|
|
10
|
+
import { type AlertProps } from 'lib/ui/Alert/Alert.js'
|
|
11
|
+
import { Alerts } from 'lib/ui/Alert/Alerts.js'
|
|
10
12
|
import { Button } from 'lib/ui/Button.js'
|
|
11
13
|
import { BatteryStatus } from 'lib/ui/device/BatteryStatus.js'
|
|
12
14
|
import { DeviceImage } from 'lib/ui/device/DeviceImage.js'
|
|
@@ -40,6 +42,7 @@ export function DeviceDetails(props: DeviceDetailsProps): JSX.Element | null {
|
|
|
40
42
|
|
|
41
43
|
function LockDeviceDetails(props: { device: LockDevice; onBack?: () => void }) {
|
|
42
44
|
const { device, onBack } = props
|
|
45
|
+
|
|
43
46
|
const [accessCodesOpen, toggleAccessCodesOpen] = useToggle()
|
|
44
47
|
const toggleLock = useToggleLock(device)
|
|
45
48
|
const { accessCodes } = useAccessCodes({
|
|
@@ -54,6 +57,36 @@ function LockDeviceDetails(props: { device: LockDevice; onBack?: () => void }) {
|
|
|
54
57
|
const accessCodeLength =
|
|
55
58
|
device.properties?.schlage_metadata?.access_code_length
|
|
56
59
|
|
|
60
|
+
function generateDeviceAlerts() {
|
|
61
|
+
if (device.errors.length === 0 && device.warnings.length === 0) {
|
|
62
|
+
return []
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const alerts: AlertProps[] = []
|
|
66
|
+
|
|
67
|
+
if (device.errors.length > 0) {
|
|
68
|
+
alerts.push(
|
|
69
|
+
...device.errors.map((error) => ({
|
|
70
|
+
variant: 'error' as const,
|
|
71
|
+
message: error.message,
|
|
72
|
+
}))
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (device.warnings.length > 0) {
|
|
77
|
+
alerts.push(
|
|
78
|
+
...device.warnings.map((warning) => ({
|
|
79
|
+
variant: 'warning' as const,
|
|
80
|
+
message: warning.message,
|
|
81
|
+
}))
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return alerts
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const alerts = generateDeviceAlerts()
|
|
89
|
+
|
|
57
90
|
if (accessCodes == null) {
|
|
58
91
|
return null
|
|
59
92
|
}
|
|
@@ -88,6 +121,10 @@ function LockDeviceDetails(props: { device: LockDevice; onBack?: () => void }) {
|
|
|
88
121
|
</div>
|
|
89
122
|
</div>
|
|
90
123
|
</div>
|
|
124
|
+
|
|
125
|
+
{alerts.length > 0 && (
|
|
126
|
+
<Alerts alerts={alerts} className='seam-alerts-space-top' />
|
|
127
|
+
)}
|
|
91
128
|
</div>
|
|
92
129
|
<div className='seam-box'>
|
|
93
130
|
<div
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { CheckIcon } from 'lib/icons/Check.js'
|
|
2
|
+
import { ExclamationCircleOutlineIcon } from 'lib/icons/ExclamationCircleOutline.js'
|
|
3
|
+
import type { UseDevicesData } from 'lib/seam/devices/use-devices.js'
|
|
4
|
+
import { TableFilterBar } from 'lib/ui/Table/TableFilterBar/TableFilterBar.js'
|
|
5
|
+
import { TableFilterItem } from 'lib/ui/Table/TableFilterBar/TableFilterItem.js'
|
|
6
|
+
|
|
7
|
+
export type DeviceFilter = 'device_issues'
|
|
8
|
+
|
|
9
|
+
interface DeviceHealthBarProps {
|
|
10
|
+
devices: Array<UseDevicesData[number]>
|
|
11
|
+
filter: DeviceFilter | null
|
|
12
|
+
onFilterSelect: (filter: DeviceFilter | null) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function DeviceHealthBar({
|
|
16
|
+
devices,
|
|
17
|
+
filter,
|
|
18
|
+
onFilterSelect,
|
|
19
|
+
}: DeviceHealthBarProps) {
|
|
20
|
+
const erroredDevices = devices.filter((device) => device.errors.length > 0)
|
|
21
|
+
const issueCount = erroredDevices.length
|
|
22
|
+
|
|
23
|
+
const toggle = (target: DeviceFilter) => () => {
|
|
24
|
+
const newFilter = target === filter ? null : target
|
|
25
|
+
onFilterSelect(newFilter)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const isPlural = issueCount === 0 || issueCount > 1
|
|
29
|
+
const label = isPlural
|
|
30
|
+
? `${issueCount} ${t.deviceIssues}`
|
|
31
|
+
: `${issueCount} ${t.deviceIssue}`
|
|
32
|
+
|
|
33
|
+
if (issueCount === 0) {
|
|
34
|
+
return (
|
|
35
|
+
<TableFilterBar>
|
|
36
|
+
<TableFilterItem>
|
|
37
|
+
<CheckIcon />
|
|
38
|
+
{t.devicesOk}
|
|
39
|
+
</TableFilterItem>
|
|
40
|
+
</TableFilterBar>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<TableFilterBar
|
|
46
|
+
isFilterCleared={filter == null}
|
|
47
|
+
onFilterClear={() => {
|
|
48
|
+
onFilterSelect(null)
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<TableFilterItem
|
|
52
|
+
onClick={toggle('device_issues')}
|
|
53
|
+
selected={filter === 'device_issues'}
|
|
54
|
+
>
|
|
55
|
+
<ExclamationCircleOutlineIcon />
|
|
56
|
+
{label}
|
|
57
|
+
</TableFilterItem>
|
|
58
|
+
</TableFilterBar>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const t = {
|
|
63
|
+
devicesOk: 'Devices OK',
|
|
64
|
+
deviceIssue: 'device issue',
|
|
65
|
+
deviceIssues: 'device issues',
|
|
66
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { isLockDevice } from 'lib/seam/devices/types.js'
|
|
2
|
+
import {
|
|
3
|
+
type UseDevicesData,
|
|
4
|
+
type UseDevicesParams,
|
|
5
|
+
} from 'lib/seam/devices/use-devices.js'
|
|
6
|
+
import { BatteryStatus } from 'lib/ui/device/BatteryStatus.js'
|
|
7
|
+
import { DeviceImage } from 'lib/ui/device/DeviceImage.js'
|
|
8
|
+
import { LockStatus } from 'lib/ui/device/LockStatus.js'
|
|
9
|
+
import { OnlineStatus } from 'lib/ui/device/OnlineStatus.js'
|
|
10
|
+
import { getDeviceModel } from 'lib/ui/DeviceDetails/DeviceModel.js'
|
|
11
|
+
import { TableCell } from 'lib/ui/Table/TableCell.js'
|
|
12
|
+
import { TableRow } from 'lib/ui/Table/TableRow.js'
|
|
13
|
+
import { Title } from 'lib/ui/typography/Title.js'
|
|
14
|
+
|
|
15
|
+
export type DeviceTableProps = Props & UseDevicesParams
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
onBack?: () => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DeviceRowProps {
|
|
22
|
+
device: UseDevicesData[number]
|
|
23
|
+
onClick: () => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function DeviceRow({
|
|
27
|
+
device,
|
|
28
|
+
onClick,
|
|
29
|
+
}: DeviceRowProps): JSX.Element | null {
|
|
30
|
+
if (!isLockDevice(device)) {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const deviceModel = getDeviceModel(device) ?? t.unknownLock
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<TableRow key={device.device_id} onClick={onClick}>
|
|
38
|
+
<TableCell className='seam-image-cell'>
|
|
39
|
+
<DeviceImage device={device} />
|
|
40
|
+
</TableCell>
|
|
41
|
+
<TableCell className='seam-body-cell'>
|
|
42
|
+
<Title>{device.properties.name}</Title>
|
|
43
|
+
<div className='seam-bottom'>
|
|
44
|
+
<span className='seam-device-model'>{deviceModel}</span>
|
|
45
|
+
<div className='seam-device-statuses'>
|
|
46
|
+
<OnlineStatus device={device} />
|
|
47
|
+
<BatteryStatus device={device} />
|
|
48
|
+
<LockStatus device={device} />
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</TableCell>
|
|
52
|
+
</TableRow>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const t = {
|
|
57
|
+
devices: 'Devices',
|
|
58
|
+
unknownLock: 'Unknown Lock',
|
|
59
|
+
noDevicesMessage: 'Sorry, no devices were found',
|
|
60
|
+
}
|
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
|
|
3
|
-
import { isLockDevice } from 'lib/seam/devices/types.js'
|
|
4
3
|
import {
|
|
5
4
|
useDevices,
|
|
6
5
|
type UseDevicesData,
|
|
7
6
|
type UseDevicesParams,
|
|
8
7
|
} from 'lib/seam/devices/use-devices.js'
|
|
9
|
-
import { BatteryStatus } from 'lib/ui/device/BatteryStatus.js'
|
|
10
|
-
import { DeviceImage } from 'lib/ui/device/DeviceImage.js'
|
|
11
|
-
import { LockStatus } from 'lib/ui/device/LockStatus.js'
|
|
12
|
-
import { OnlineStatus } from 'lib/ui/device/OnlineStatus.js'
|
|
13
8
|
import { DeviceDetails } from 'lib/ui/DeviceDetails/DeviceDetails.js'
|
|
14
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
type DeviceFilter,
|
|
11
|
+
DeviceHealthBar,
|
|
12
|
+
} from 'lib/ui/DeviceTable/DeviceHealthBar.js'
|
|
13
|
+
import { DeviceRow } from 'lib/ui/DeviceTable/DeviceRow.js'
|
|
15
14
|
import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
|
|
15
|
+
import { EmptyPlaceholder } from 'lib/ui/Table/EmptyPlaceholder.js'
|
|
16
16
|
import { TableBody } from 'lib/ui/Table/TableBody.js'
|
|
17
|
-
import { TableCell } from 'lib/ui/Table/TableCell.js'
|
|
18
17
|
import { TableHeader } from 'lib/ui/Table/TableHeader.js'
|
|
19
|
-
import { TableRow } from 'lib/ui/Table/TableRow.js'
|
|
20
18
|
import { TableTitle } from 'lib/ui/Table/TableTitle.js'
|
|
21
19
|
import { Caption } from 'lib/ui/typography/Caption.js'
|
|
22
|
-
import { Title } from 'lib/ui/typography/Title.js'
|
|
23
20
|
|
|
24
21
|
export type DeviceTableProps = Props & UseDevicesParams
|
|
25
22
|
|
|
@@ -69,53 +66,57 @@ export function DeviceTable({
|
|
|
69
66
|
</TableTitle>
|
|
70
67
|
</TableHeader>
|
|
71
68
|
<TableBody>
|
|
72
|
-
{devices
|
|
73
|
-
<DeviceRow
|
|
74
|
-
device={device}
|
|
75
|
-
key={device.device_id}
|
|
76
|
-
onClick={() => {
|
|
77
|
-
selectDevice(device.device_id)
|
|
78
|
-
}}
|
|
79
|
-
/>
|
|
80
|
-
))}
|
|
69
|
+
<Body devices={devices} selectDevice={selectDevice} />
|
|
81
70
|
</TableBody>
|
|
82
71
|
</div>
|
|
83
72
|
)
|
|
84
73
|
}
|
|
85
74
|
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
})
|
|
90
|
-
const {
|
|
75
|
+
function Body(props: {
|
|
76
|
+
devices: Array<UseDevicesData[number]>
|
|
77
|
+
selectDevice: (id: string) => void
|
|
78
|
+
}) {
|
|
79
|
+
const { devices, selectDevice } = props
|
|
80
|
+
const [filter, setFilter] = useState<DeviceFilter | null>(null)
|
|
91
81
|
|
|
92
|
-
if (
|
|
93
|
-
return
|
|
82
|
+
if (devices.length === 0) {
|
|
83
|
+
return <EmptyPlaceholder>{t.noDevicesMessage}</EmptyPlaceholder>
|
|
94
84
|
}
|
|
95
85
|
|
|
96
|
-
const
|
|
86
|
+
const filteredDevices = devices.filter((device) => {
|
|
87
|
+
if (filter === null) {
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (filter === 'device_issues') {
|
|
92
|
+
return device.errors.length > 0
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return true
|
|
96
|
+
})
|
|
97
97
|
|
|
98
98
|
return (
|
|
99
|
-
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
99
|
+
<>
|
|
100
|
+
<DeviceHealthBar
|
|
101
|
+
devices={devices}
|
|
102
|
+
filter={filter}
|
|
103
|
+
onFilterSelect={setFilter}
|
|
104
|
+
/>
|
|
105
|
+
{filteredDevices.map((device) => (
|
|
106
|
+
<DeviceRow
|
|
107
|
+
device={device}
|
|
108
|
+
key={device.device_id}
|
|
109
|
+
onClick={() => {
|
|
110
|
+
selectDevice(device.device_id)
|
|
111
|
+
}}
|
|
112
|
+
/>
|
|
113
|
+
))}
|
|
114
|
+
</>
|
|
115
115
|
)
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
const t = {
|
|
119
119
|
devices: 'Devices',
|
|
120
120
|
unknownLock: 'Unknown Lock',
|
|
121
|
+
noDevicesMessage: 'Sorry, no devices were found',
|
|
121
122
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
|
|
1
3
|
import type { ButtonProps } from 'lib/ui/types.js'
|
|
2
4
|
|
|
3
|
-
export function IconButton(props: ButtonProps): JSX.Element {
|
|
4
|
-
return
|
|
5
|
+
export function IconButton({ className, ...props }: ButtonProps): JSX.Element {
|
|
6
|
+
return (
|
|
7
|
+
<button {...props} className={classNames('seam-icon-btn', className)} />
|
|
8
|
+
)
|
|
5
9
|
}
|
|
@@ -7,7 +7,7 @@ import type { DeviceModel } from 'lib/ui/SupportedDevices/types.js'
|
|
|
7
7
|
import SupportedDeviceRow from './SupportedDeviceRow.js'
|
|
8
8
|
import SupportedDevicesHeader from './SupportedDevicesHeader.js'
|
|
9
9
|
|
|
10
|
-
export
|
|
10
|
+
export function SupportedDevices() {
|
|
11
11
|
const { data, isLoading, isError, refetch } = useQuery<{
|
|
12
12
|
data: {
|
|
13
13
|
device_models?: DeviceModel[]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ArrowRestartIcon } from 'lib/icons/ArrowRestart.js'
|
|
2
|
+
import { IconButton } from 'lib/ui/IconButton.js'
|
|
3
|
+
import type { ButtonProps, DivProps } from 'lib/ui/types.js'
|
|
4
|
+
|
|
5
|
+
export interface TableFiltersProps extends DivProps {
|
|
6
|
+
isFilterCleared?: boolean
|
|
7
|
+
onFilterClear?: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function TableFilterBar({
|
|
11
|
+
isFilterCleared,
|
|
12
|
+
onFilterClear,
|
|
13
|
+
children,
|
|
14
|
+
...props
|
|
15
|
+
}: TableFiltersProps) {
|
|
16
|
+
const showClearFilterButton = isFilterCleared === false
|
|
17
|
+
return (
|
|
18
|
+
<div className='seam-table-filter-bar' {...props}>
|
|
19
|
+
{children}{' '}
|
|
20
|
+
{showClearFilterButton && <ClearFiltersButton onClick={onFilterClear} />}
|
|
21
|
+
</div>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ClearFiltersButton(props: ButtonProps) {
|
|
26
|
+
return (
|
|
27
|
+
<IconButton {...props} className='seam-clear-filters-button'>
|
|
28
|
+
<ArrowRestartIcon />
|
|
29
|
+
</IconButton>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
|
|
3
|
+
import type { DivProps } from 'lib/ui/types.js'
|
|
4
|
+
|
|
5
|
+
export interface TableFilterItemProps extends DivProps {
|
|
6
|
+
selected?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function TableFilterItem({
|
|
10
|
+
onClick,
|
|
11
|
+
selected,
|
|
12
|
+
...props
|
|
13
|
+
}: TableFilterItemProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
onClick={onClick}
|
|
17
|
+
className={classNames('seam-table-filter-item', {
|
|
18
|
+
'seam-clickable': onClick !== undefined,
|
|
19
|
+
'seam-selected': selected,
|
|
20
|
+
})}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
}
|
package/src/lib/ui/index.ts
CHANGED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
@use './colors';
|
|
2
|
+
|
|
3
|
+
@mixin all {
|
|
4
|
+
// Alerts
|
|
5
|
+
.seam-alerts {
|
|
6
|
+
width: 100%;
|
|
7
|
+
display: flex;
|
|
8
|
+
justify-content: flex-start;
|
|
9
|
+
align-items: flex-start;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
gap: 8px;
|
|
12
|
+
|
|
13
|
+
&.seam-alerts-space-top {
|
|
14
|
+
margin-top: 16px;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Alert
|
|
19
|
+
.seam-alert {
|
|
20
|
+
width: 100%;
|
|
21
|
+
min-height: 70px;
|
|
22
|
+
padding: 8px 16px;
|
|
23
|
+
border-radius: 8px;
|
|
24
|
+
display: flex;
|
|
25
|
+
justify-content: space-between;
|
|
26
|
+
align-items: center;
|
|
27
|
+
flex-direction: row;
|
|
28
|
+
gap: 16px;
|
|
29
|
+
|
|
30
|
+
.seam-alert-action {
|
|
31
|
+
appearance: none;
|
|
32
|
+
height: 32px;
|
|
33
|
+
padding: 0 10px;
|
|
34
|
+
border-radius: 6px;
|
|
35
|
+
background-color: transparent;
|
|
36
|
+
font-size: 14px;
|
|
37
|
+
line-height: 1;
|
|
38
|
+
font-weight: 600;
|
|
39
|
+
white-space: nowrap;
|
|
40
|
+
cursor: pointer;
|
|
41
|
+
|
|
42
|
+
&:hover {
|
|
43
|
+
background-color: colors.$item-hover-bg;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
&.seam-warning-alert {
|
|
48
|
+
background-color: colors.$alert-warn-fill;
|
|
49
|
+
border: 1px solid colors.$alert-warn-line;
|
|
50
|
+
|
|
51
|
+
.seam-alert-action {
|
|
52
|
+
border: 1px solid colors.$alert-warn-dark;
|
|
53
|
+
color: colors.$alert-warn-dark;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
&.seam-error-alert {
|
|
58
|
+
background-color: colors.$alert-error-fill;
|
|
59
|
+
border: 1px solid colors.$alert-error-line;
|
|
60
|
+
|
|
61
|
+
.seam-alert-action {
|
|
62
|
+
border: 1px solid colors.$alert-error-dark;
|
|
63
|
+
color: colors.$alert-error-dark;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
&.seam-alert-space-top {
|
|
68
|
+
margin-top: 16px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.seam-alert-icon {
|
|
72
|
+
width: 24px;
|
|
73
|
+
height: 24px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.seam-alert-content {
|
|
77
|
+
display: flex;
|
|
78
|
+
justify-content: flex-start;
|
|
79
|
+
align-items: center;
|
|
80
|
+
flex-direction: row;
|
|
81
|
+
gap: 8px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.seam-alert-message-wrap {
|
|
85
|
+
.seam-alert-message {
|
|
86
|
+
font-size: 14px;
|
|
87
|
+
font-weight: 400;
|
|
88
|
+
line-height: 134%;
|
|
89
|
+
color: colors.$text-default;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@media only screen and (width <= 500px) {
|
|
95
|
+
.seam-alert {
|
|
96
|
+
justify-content: flex-start;
|
|
97
|
+
flex-direction: column;
|
|
98
|
+
|
|
99
|
+
.seam-alert-content {
|
|
100
|
+
width: 100%;
|
|
101
|
+
align-items: flex-start;
|
|
102
|
+
flex-direction: column;
|
|
103
|
+
gap: 4px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.seam-alert-action-wrap {
|
|
107
|
+
width: 100%;
|
|
108
|
+
|
|
109
|
+
.seam-alert-action {
|
|
110
|
+
width: 100%;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
package/src/styles/_main.scss
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
@use './typography';
|
|
3
3
|
@use './tables';
|
|
4
4
|
@use './inputs';
|
|
5
|
+
@use './alert';
|
|
5
6
|
@use './buttons';
|
|
6
7
|
@use './layout';
|
|
7
8
|
@use './access-code-table';
|
|
@@ -31,5 +32,6 @@
|
|
|
31
32
|
@include device-table.all;
|
|
32
33
|
@include access-code-table.all;
|
|
33
34
|
@include access-code-details.all;
|
|
35
|
+
@include alert.all;
|
|
34
36
|
@include supported-devices.all;
|
|
35
37
|
}
|
package/src/styles/_tables.scss
CHANGED
|
@@ -47,8 +47,73 @@
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
@mixin empty-placeholder {
|
|
51
|
+
.seam-table-empty-placeholder {
|
|
52
|
+
height: 300px;
|
|
53
|
+
font-size: 18px;
|
|
54
|
+
line-height: 132%;
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: center;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@mixin table-filters {
|
|
62
|
+
.seam-table-filter-bar {
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
padding: 6px 16px;
|
|
66
|
+
background: colors.$bg-a;
|
|
67
|
+
|
|
68
|
+
.seam-clear-filters-button {
|
|
69
|
+
margin-left: 6px;
|
|
70
|
+
border: 0;
|
|
71
|
+
background: transparent !important;
|
|
72
|
+
padding: 0;
|
|
73
|
+
|
|
74
|
+
svg {
|
|
75
|
+
scale: 0.8333;
|
|
76
|
+
|
|
77
|
+
* {
|
|
78
|
+
fill: colors.$text-gray-1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.seam-table-filter-item {
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
padding: 2px 8px 2px 5px;
|
|
88
|
+
border-radius: 5px;
|
|
89
|
+
border: 1px solid colors.$bg-a;
|
|
90
|
+
font-size: 14px;
|
|
91
|
+
line-height: 134%;
|
|
92
|
+
|
|
93
|
+
svg {
|
|
94
|
+
scale: 0.75;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
&.seam-clickable {
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
|
|
100
|
+
&:hover {
|
|
101
|
+
background: rgb(255 255 255 / 65%);
|
|
102
|
+
border: 1px solid colors.$text-gray-3;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
&.seam-selected {
|
|
107
|
+
background: #fff;
|
|
108
|
+
border: 1px solid colors.$text-gray-3;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
50
113
|
@mixin all {
|
|
51
114
|
@include header;
|
|
52
115
|
@include row;
|
|
53
116
|
@include cell;
|
|
117
|
+
@include empty-placeholder;
|
|
118
|
+
@include table-filters;
|
|
54
119
|
}
|