@seamapi/react 1.63.1 → 1.65.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/README.md +61 -4
- package/dist/elements.js +6133 -6020
- package/dist/elements.js.map +1 -1
- package/dist/index.css +2 -4
- package/dist/index.css.map +1 -1
- package/dist/index.min.css +1 -1
- package/dist/index.min.css.map +1 -1
- package/lib/seam/SeamProvider.d.ts +2 -1
- package/lib/seam/SeamProvider.js +10 -4
- package/lib/seam/SeamProvider.js.map +1 -1
- package/lib/seam/components/AccessCodeDetails/AccessCodeDetails.d.ts +1 -1
- package/lib/seam/components/AccessCodeDetails/AccessCodeDetails.js +11 -13
- package/lib/seam/components/AccessCodeDetails/AccessCodeDetails.js.map +1 -1
- package/lib/seam/components/AccessCodeTable/AccessCodeHealthBar.d.ts +4 -1
- package/lib/seam/components/AccessCodeTable/AccessCodeHealthBar.js +3 -2
- package/lib/seam/components/AccessCodeTable/AccessCodeHealthBar.js.map +1 -1
- package/lib/seam/components/AccessCodeTable/AccessCodeTable.d.ts +1 -1
- package/lib/seam/components/AccessCodeTable/AccessCodeTable.js +15 -11
- package/lib/seam/components/AccessCodeTable/AccessCodeTable.js.map +1 -1
- package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.d.ts +1 -1
- package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.js +2 -2
- package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.js.map +1 -1
- package/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleTable.d.ts +1 -1
- package/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleTable.js +12 -7
- package/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleTable.js.map +1 -1
- package/lib/seam/components/DeviceDetails/LockDeviceDetails.js +11 -4
- package/lib/seam/components/DeviceDetails/LockDeviceDetails.js.map +1 -1
- package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.d.ts +1 -1
- package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js +2 -2
- package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js.map +1 -1
- package/lib/seam/components/DeviceTable/DeviceHealthBar.d.ts +3 -1
- package/lib/seam/components/DeviceTable/DeviceHealthBar.js +4 -3
- package/lib/seam/components/DeviceTable/DeviceHealthBar.js.map +1 -1
- package/lib/seam/components/DeviceTable/DeviceTable.d.ts +1 -1
- package/lib/seam/components/DeviceTable/DeviceTable.js +14 -9
- package/lib/seam/components/DeviceTable/DeviceTable.js.map +1 -1
- package/lib/seam/components/SupportedDeviceTable/use-device-model.d.ts +7 -0
- package/lib/seam/components/SupportedDeviceTable/use-device-model.js +18 -0
- package/lib/seam/components/SupportedDeviceTable/use-device-model.js.map +1 -0
- package/lib/seam/components/SupportedDeviceTable/use-device-models.d.ts +7 -0
- package/lib/seam/components/SupportedDeviceTable/use-device-models.js +17 -0
- package/lib/seam/components/SupportedDeviceTable/use-device-models.js.map +1 -0
- package/lib/seam/components/SupportedDeviceTable/use-manufacturer.d.ts +7 -0
- package/lib/seam/components/SupportedDeviceTable/use-manufacturer.js +18 -0
- package/lib/seam/components/SupportedDeviceTable/use-manufacturer.js.map +1 -0
- package/lib/seam/components/SupportedDeviceTable/use-manufacturers.d.ts +7 -0
- package/lib/seam/components/SupportedDeviceTable/use-manufacturers.js +17 -0
- package/lib/seam/components/SupportedDeviceTable/use-manufacturers.js.map +1 -0
- package/lib/seam/components/common-props.d.ts +3 -0
- package/lib/seam/components/common-props.js.map +1 -1
- package/lib/seam/filters.d.ts +8 -0
- package/lib/seam/filters.js +16 -0
- package/lib/seam/filters.js.map +1 -0
- package/lib/telemetry/client.js.map +1 -1
- package/lib/ui/Snackbar/Snackbar.d.ts +1 -1
- package/lib/ui/Snackbar/Snackbar.js +1 -1
- package/lib/ui/Snackbar/Snackbar.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +15 -5
- package/src/lib/element.tsx +6 -1
- package/src/lib/seam/SeamProvider.tsx +17 -2
- package/src/lib/seam/components/AccessCodeDetails/AccessCodeDetails.tsx +24 -30
- package/src/lib/seam/components/AccessCodeTable/AccessCodeHealthBar.tsx +9 -1
- package/src/lib/seam/components/AccessCodeTable/AccessCodeTable.tsx +32 -5
- package/src/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.tsx +4 -0
- package/src/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleTable.tsx +21 -5
- package/src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx +19 -8
- package/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx +4 -0
- package/src/lib/seam/components/DeviceTable/DeviceHealthBar.tsx +10 -2
- package/src/lib/seam/components/DeviceTable/DeviceTable.tsx +29 -7
- package/src/lib/seam/components/SupportedDeviceTable/use-device-model.ts +45 -0
- package/src/lib/seam/components/SupportedDeviceTable/use-device-models.ts +43 -0
- package/src/lib/seam/components/SupportedDeviceTable/use-manufacturer.ts +45 -0
- package/src/lib/seam/components/SupportedDeviceTable/use-manufacturers.ts +43 -0
- package/src/lib/seam/components/common-props.tsx +10 -0
- package/src/lib/seam/filters.ts +32 -0
- package/src/lib/telemetry/client.ts +3 -3
- package/src/lib/ui/Snackbar/Snackbar.tsx +2 -2
- package/src/lib/version.ts +1 -1
- package/src/styles/_device-details.scss +1 -2
- package/src/styles/_thermostat.scss +1 -2
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { AccessCode } from 'seamapi'
|
|
2
|
+
|
|
1
3
|
import { CheckIcon } from 'lib/icons/Check.js'
|
|
2
4
|
import { ExclamationCircleOutlineIcon } from 'lib/icons/ExclamationCircleOutline.js'
|
|
3
5
|
import type { UseAccessCodesData } from 'lib/seam/access-codes/use-access-codes.js'
|
|
@@ -8,17 +10,23 @@ export type AccessCodeFilter = 'access_code_issues'
|
|
|
8
10
|
|
|
9
11
|
interface AccessCodeHealthBarProps {
|
|
10
12
|
accessCodes: Array<UseAccessCodesData[number]>
|
|
13
|
+
errorFilter: (error: AccessCode['errors'][number]) => boolean
|
|
14
|
+
warningFilter: (warning: AccessCode['warnings'][number]) => boolean
|
|
11
15
|
filter: AccessCodeFilter | null
|
|
12
16
|
onFilterSelect: (filter: AccessCodeFilter | null) => void
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
export function AccessCodeHealthBar({
|
|
16
20
|
accessCodes,
|
|
21
|
+
errorFilter,
|
|
22
|
+
warningFilter,
|
|
17
23
|
filter,
|
|
18
24
|
onFilterSelect,
|
|
19
25
|
}: AccessCodeHealthBarProps): JSX.Element {
|
|
20
26
|
const codesWithIssues = accessCodes.filter(
|
|
21
|
-
({ errors, warnings }) =>
|
|
27
|
+
({ errors, warnings }) =>
|
|
28
|
+
errors.filter(errorFilter).length > 0 ||
|
|
29
|
+
warnings.filter(warningFilter).length > 0
|
|
22
30
|
)
|
|
23
31
|
const issueCount = codesWithIssues.length
|
|
24
32
|
|
|
@@ -79,6 +79,8 @@ export function AccessCodeTable({
|
|
|
79
79
|
onBack,
|
|
80
80
|
accessCodeFilter = defaultAccessCodeFilter,
|
|
81
81
|
accessCodeComparator = compareByCreatedAtDesc,
|
|
82
|
+
errorFilter = () => true,
|
|
83
|
+
warningFilter = () => true,
|
|
82
84
|
heading = t.accessCodes,
|
|
83
85
|
title = t.accessCodes,
|
|
84
86
|
className,
|
|
@@ -90,7 +92,7 @@ export function AccessCodeTable({
|
|
|
90
92
|
}: AccessCodeTableProps): JSX.Element {
|
|
91
93
|
useComponentTelemetry('AccessCodeTable')
|
|
92
94
|
|
|
93
|
-
const { accessCodes, isInitialLoading, isError,
|
|
95
|
+
const { accessCodes, isInitialLoading, isError, refetch } = useAccessCodes({
|
|
94
96
|
device_id: deviceId,
|
|
95
97
|
})
|
|
96
98
|
|
|
@@ -143,6 +145,8 @@ export function AccessCodeTable({
|
|
|
143
145
|
return (
|
|
144
146
|
<NestedEditAccessCodeForm
|
|
145
147
|
accessCodeId={selectedEditAccessCodeId}
|
|
148
|
+
errorFilter={errorFilter}
|
|
149
|
+
warningFilter={warningFilter}
|
|
146
150
|
disableLockUnlock={disableLockUnlock}
|
|
147
151
|
disableCreateAccessCode={disableCreateAccessCode}
|
|
148
152
|
disableEditAccessCode={disableEditAccessCode}
|
|
@@ -176,6 +180,8 @@ export function AccessCodeTable({
|
|
|
176
180
|
onEdit={() => {
|
|
177
181
|
setSelectedEditAccessCodeId(selectedViewAccessCodeId)
|
|
178
182
|
}}
|
|
183
|
+
errorFilter={errorFilter}
|
|
184
|
+
warningFilter={warningFilter}
|
|
179
185
|
disableLockUnlock={disableLockUnlock}
|
|
180
186
|
disableCreateAccessCode={disableCreateAccessCode}
|
|
181
187
|
disableEditAccessCode={disableEditAccessCode}
|
|
@@ -194,6 +200,8 @@ export function AccessCodeTable({
|
|
|
194
200
|
return (
|
|
195
201
|
<NestedCreateAccessCodeForm
|
|
196
202
|
deviceId={deviceId}
|
|
203
|
+
errorFilter={errorFilter}
|
|
204
|
+
warningFilter={warningFilter}
|
|
197
205
|
disableLockUnlock={disableLockUnlock}
|
|
198
206
|
disableCreateAccessCode={disableCreateAccessCode}
|
|
199
207
|
disableEditAccessCode={disableEditAccessCode}
|
|
@@ -208,10 +216,6 @@ export function AccessCodeTable({
|
|
|
208
216
|
)
|
|
209
217
|
}
|
|
210
218
|
|
|
211
|
-
if (isError) {
|
|
212
|
-
return <p className={className}>{error?.message}</p>
|
|
213
|
-
}
|
|
214
|
-
|
|
215
219
|
return (
|
|
216
220
|
<>
|
|
217
221
|
<Snackbar
|
|
@@ -264,10 +268,25 @@ export function AccessCodeTable({
|
|
|
264
268
|
accessCodes={filteredAccessCodes}
|
|
265
269
|
onAccessCodeClick={handleAccessCodeClick}
|
|
266
270
|
onAccessCodeEdit={handleAccessCodeEdit}
|
|
271
|
+
errorFilter={errorFilter}
|
|
272
|
+
warningFilter={warningFilter}
|
|
267
273
|
disableEditAccessCode={disableEditAccessCode}
|
|
268
274
|
disableDeleteAccessCode={disableDeleteAccessCode}
|
|
269
275
|
/>
|
|
270
276
|
</TableBody>
|
|
277
|
+
|
|
278
|
+
<Snackbar
|
|
279
|
+
variant='error'
|
|
280
|
+
visible={isError}
|
|
281
|
+
message={t.fallbackErrorMessage}
|
|
282
|
+
action={{
|
|
283
|
+
label: t.tryAgain,
|
|
284
|
+
onClick: () => {
|
|
285
|
+
void refetch()
|
|
286
|
+
},
|
|
287
|
+
}}
|
|
288
|
+
disableCloseButton
|
|
289
|
+
/>
|
|
271
290
|
</div>
|
|
272
291
|
</>
|
|
273
292
|
)
|
|
@@ -277,6 +296,8 @@ function Content(props: {
|
|
|
277
296
|
accessCodes: Array<UseAccessCodesData[number]>
|
|
278
297
|
onAccessCodeClick: (accessCodeId: string) => void
|
|
279
298
|
onAccessCodeEdit: (accessCodeId: string) => void
|
|
299
|
+
errorFilter: (error: AccessCode['errors'][number]) => boolean
|
|
300
|
+
warningFilter: (warning: AccessCode['warnings'][number]) => boolean
|
|
280
301
|
disableEditAccessCode: boolean
|
|
281
302
|
disableDeleteAccessCode: boolean
|
|
282
303
|
}): JSX.Element {
|
|
@@ -284,6 +305,8 @@ function Content(props: {
|
|
|
284
305
|
accessCodes,
|
|
285
306
|
onAccessCodeClick,
|
|
286
307
|
onAccessCodeEdit,
|
|
308
|
+
errorFilter,
|
|
309
|
+
warningFilter,
|
|
287
310
|
disableEditAccessCode,
|
|
288
311
|
disableDeleteAccessCode,
|
|
289
312
|
} = props
|
|
@@ -313,6 +336,8 @@ function Content(props: {
|
|
|
313
336
|
accessCodes={accessCodes}
|
|
314
337
|
filter={filter}
|
|
315
338
|
onFilterSelect={setFilter}
|
|
339
|
+
errorFilter={errorFilter}
|
|
340
|
+
warningFilter={warningFilter}
|
|
316
341
|
/>
|
|
317
342
|
{filteredAccessCodes.map((accessCode) => (
|
|
318
343
|
<AccessCodeRow
|
|
@@ -338,4 +363,6 @@ const t = {
|
|
|
338
363
|
loading: 'Loading access codes',
|
|
339
364
|
accesCodeUpdated: 'Access code updated',
|
|
340
365
|
accesCodeCreated: 'Access code created',
|
|
366
|
+
tryAgain: 'Try again',
|
|
367
|
+
fallbackErrorMessage: 'Access codes could not be loaded',
|
|
341
368
|
}
|
package/src/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.tsx
CHANGED
|
@@ -33,6 +33,8 @@ export function ClimateSettingScheduleDetails({
|
|
|
33
33
|
disableDeleteAccessCode = false,
|
|
34
34
|
onBack,
|
|
35
35
|
className,
|
|
36
|
+
errorFilter = () => true,
|
|
37
|
+
warningFilter = () => true,
|
|
36
38
|
disableCreateAccessCode,
|
|
37
39
|
disableEditAccessCode,
|
|
38
40
|
disableResourceIds = false,
|
|
@@ -56,6 +58,8 @@ export function ClimateSettingScheduleDetails({
|
|
|
56
58
|
return (
|
|
57
59
|
<NestedDeviceDetails
|
|
58
60
|
deviceId={selectedDeviceId}
|
|
61
|
+
errorFilter={errorFilter}
|
|
62
|
+
warningFilter={warningFilter}
|
|
59
63
|
disableLockUnlock={disableLockUnlock}
|
|
60
64
|
disableCreateAccessCode={disableCreateAccessCode}
|
|
61
65
|
disableEditAccessCode={disableEditAccessCode}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from 'lib/seam/thermostats/climate-setting-schedules/use-climate-setting-schedules.js'
|
|
18
18
|
import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
|
|
19
19
|
import { LoadingToast } from 'lib/ui/LoadingToast/LoadingToast.js'
|
|
20
|
+
import { Snackbar } from 'lib/ui/Snackbar/Snackbar.js'
|
|
20
21
|
import { EmptyPlaceholder } from 'lib/ui/Table/EmptyPlaceholder.js'
|
|
21
22
|
import { TableBody } from 'lib/ui/Table/TableBody.js'
|
|
22
23
|
import { TableHeader } from 'lib/ui/Table/TableHeader.js'
|
|
@@ -66,13 +67,15 @@ export function ClimateSettingScheduleTable({
|
|
|
66
67
|
disableDeleteAccessCode = false,
|
|
67
68
|
onBack,
|
|
68
69
|
className,
|
|
70
|
+
errorFilter = () => true,
|
|
71
|
+
warningFilter = () => true,
|
|
69
72
|
disableCreateAccessCode,
|
|
70
73
|
disableEditAccessCode,
|
|
71
74
|
disableResourceIds = false,
|
|
72
75
|
}: ClimateSettingScheduleTableProps): JSX.Element {
|
|
73
76
|
useComponentTelemetry('ClimateSettingScheduleTable')
|
|
74
77
|
|
|
75
|
-
const { climateSettingSchedules, isInitialLoading, isError,
|
|
78
|
+
const { climateSettingSchedules, isInitialLoading, isError, refetch } =
|
|
76
79
|
useClimateSettingSchedules({
|
|
77
80
|
device_id: deviceId,
|
|
78
81
|
})
|
|
@@ -115,6 +118,8 @@ export function ClimateSettingScheduleTable({
|
|
|
115
118
|
return (
|
|
116
119
|
<NestedClimateSettingScheduleDetails
|
|
117
120
|
climateSettingScheduleId={selectedViewClimateSettingScheduleId}
|
|
121
|
+
errorFilter={errorFilter}
|
|
122
|
+
warningFilter={warningFilter}
|
|
118
123
|
disableLockUnlock={disableLockUnlock}
|
|
119
124
|
disableCreateAccessCode={disableCreateAccessCode}
|
|
120
125
|
disableEditAccessCode={disableEditAccessCode}
|
|
@@ -128,10 +133,6 @@ export function ClimateSettingScheduleTable({
|
|
|
128
133
|
)
|
|
129
134
|
}
|
|
130
135
|
|
|
131
|
-
if (isError) {
|
|
132
|
-
return <p className={className}>{error?.message}</p>
|
|
133
|
-
}
|
|
134
|
-
|
|
135
136
|
return (
|
|
136
137
|
<div className={classNames('seam-table', className)}>
|
|
137
138
|
<ContentHeader onBack={onBack} />
|
|
@@ -165,6 +166,19 @@ export function ClimateSettingScheduleTable({
|
|
|
165
166
|
onClimateSettingScheduleClick={handleClimateSettingScheduleClick}
|
|
166
167
|
/>
|
|
167
168
|
</TableBody>
|
|
169
|
+
|
|
170
|
+
<Snackbar
|
|
171
|
+
variant='error'
|
|
172
|
+
visible={isError}
|
|
173
|
+
message={t.fallbackErrorMessage}
|
|
174
|
+
action={{
|
|
175
|
+
label: t.tryAgain,
|
|
176
|
+
onClick: () => {
|
|
177
|
+
void refetch()
|
|
178
|
+
},
|
|
179
|
+
}}
|
|
180
|
+
disableCloseButton
|
|
181
|
+
/>
|
|
168
182
|
</div>
|
|
169
183
|
)
|
|
170
184
|
}
|
|
@@ -201,4 +215,6 @@ const t = {
|
|
|
201
215
|
noClimateSettingSchedulesMessage:
|
|
202
216
|
'Sorry, no climate setting schedules were found',
|
|
203
217
|
loading: 'Loading schedules',
|
|
218
|
+
tryAgain: 'Try again',
|
|
219
|
+
fallbackErrorMessage: 'Climate settings could not be loaded',
|
|
204
220
|
}
|
|
@@ -7,6 +7,7 @@ import { NestedAccessCodeTable } from 'lib/seam/components/AccessCodeTable/Acces
|
|
|
7
7
|
import type { CommonProps } from 'lib/seam/components/common-props.js'
|
|
8
8
|
import { DeviceModel } from 'lib/seam/components/DeviceDetails/DeviceModel.js'
|
|
9
9
|
import { useToggleLock } from 'lib/seam/devices/use-toggle-lock.js'
|
|
10
|
+
import { deviceErrorFilter, deviceWarningFilter } from 'lib/seam/filters.js'
|
|
10
11
|
import { Alerts } from 'lib/ui/Alert/Alerts.js'
|
|
11
12
|
import { Button } from 'lib/ui/Button.js'
|
|
12
13
|
import { BatteryStatus } from 'lib/ui/device/BatteryStatus.js'
|
|
@@ -24,6 +25,8 @@ export function LockDeviceDetails(
|
|
|
24
25
|
): JSX.Element | null {
|
|
25
26
|
const {
|
|
26
27
|
device,
|
|
28
|
+
errorFilter = () => true,
|
|
29
|
+
warningFilter = () => true,
|
|
27
30
|
disableLockUnlock,
|
|
28
31
|
disableCreateAccessCode,
|
|
29
32
|
disableEditAccessCode,
|
|
@@ -52,6 +55,8 @@ export function LockDeviceDetails(
|
|
|
52
55
|
return (
|
|
53
56
|
<NestedAccessCodeTable
|
|
54
57
|
deviceId={device.device_id}
|
|
58
|
+
errorFilter={errorFilter}
|
|
59
|
+
warningFilter={warningFilter}
|
|
55
60
|
disableLockUnlock={disableLockUnlock}
|
|
56
61
|
disableCreateAccessCode={disableCreateAccessCode}
|
|
57
62
|
disableEditAccessCode={disableEditAccessCode}
|
|
@@ -64,14 +69,20 @@ export function LockDeviceDetails(
|
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
const alerts = [
|
|
67
|
-
...device.errors
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
...device.errors
|
|
73
|
+
.filter(deviceErrorFilter)
|
|
74
|
+
.filter(errorFilter)
|
|
75
|
+
.map((error) => ({
|
|
76
|
+
variant: 'error' as const,
|
|
77
|
+
message: error.message,
|
|
78
|
+
})),
|
|
79
|
+
...device.warnings
|
|
80
|
+
.filter(deviceWarningFilter)
|
|
81
|
+
.filter(warningFilter)
|
|
82
|
+
.map((warning) => ({
|
|
83
|
+
variant: 'warning' as const,
|
|
84
|
+
message: warning.message,
|
|
85
|
+
})),
|
|
75
86
|
]
|
|
76
87
|
|
|
77
88
|
return (
|
|
@@ -23,6 +23,8 @@ export function ThermostatDeviceDetails({
|
|
|
23
23
|
device,
|
|
24
24
|
onBack,
|
|
25
25
|
className,
|
|
26
|
+
errorFilter = () => true,
|
|
27
|
+
warningFilter = () => true,
|
|
26
28
|
disableLockUnlock,
|
|
27
29
|
disableCreateAccessCode,
|
|
28
30
|
disableEditAccessCode,
|
|
@@ -41,6 +43,8 @@ export function ThermostatDeviceDetails({
|
|
|
41
43
|
return (
|
|
42
44
|
<NestedClimateSettingScheduleTable
|
|
43
45
|
deviceId={device.device_id}
|
|
46
|
+
errorFilter={errorFilter}
|
|
47
|
+
warningFilter={warningFilter}
|
|
44
48
|
disableLockUnlock={disableLockUnlock}
|
|
45
49
|
disableCreateAccessCode={disableCreateAccessCode}
|
|
46
50
|
disableEditAccessCode={disableEditAccessCode}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import type { ConnectedAccountError, DeviceError } from 'seamapi'
|
|
2
|
+
|
|
1
3
|
import { CheckIcon } from 'lib/icons/Check.js'
|
|
2
4
|
import { ExclamationCircleOutlineIcon } from 'lib/icons/ExclamationCircleOutline.js'
|
|
3
5
|
import { OnlineStatusAccountOfflineIcon } from 'lib/icons/OnlineStatusAccountOffline.js'
|
|
4
6
|
import type { UseDevicesData } from 'lib/seam/devices/use-devices.js'
|
|
7
|
+
import {
|
|
8
|
+
connectedAccountErrorFilter,
|
|
9
|
+
deviceErrorFilter,
|
|
10
|
+
} from 'lib/seam/filters.js'
|
|
5
11
|
import { TableFilterBar } from 'lib/ui/Table/TableFilterBar/TableFilterBar.js'
|
|
6
12
|
import { TableFilterItem } from 'lib/ui/Table/TableFilterBar/TableFilterItem.js'
|
|
7
13
|
|
|
@@ -12,22 +18,24 @@ interface DeviceHealthBarProps {
|
|
|
12
18
|
devices: Array<UseDevicesData[number]>
|
|
13
19
|
filter: AccountFilter | DeviceFilter | null
|
|
14
20
|
onFilterSelect: (filter: AccountFilter | DeviceFilter | null) => void
|
|
21
|
+
errorFilter: (error: DeviceError | ConnectedAccountError) => boolean
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
export function DeviceHealthBar({
|
|
18
25
|
devices,
|
|
19
26
|
filter,
|
|
20
27
|
onFilterSelect,
|
|
28
|
+
errorFilter,
|
|
21
29
|
}: DeviceHealthBarProps): JSX.Element {
|
|
22
30
|
const erroredAccounts = devices.filter((device) => {
|
|
23
31
|
return (
|
|
24
|
-
device.errors.filter((
|
|
32
|
+
device.errors.filter(errorFilter).filter(connectedAccountErrorFilter)
|
|
25
33
|
.length > 0
|
|
26
34
|
)
|
|
27
35
|
})
|
|
28
36
|
const erroredDevices = devices.filter((device) => {
|
|
29
37
|
return (
|
|
30
|
-
device.errors.filter((
|
|
38
|
+
device.errors.filter(errorFilter).filter(deviceErrorFilter).length > 0
|
|
31
39
|
)
|
|
32
40
|
})
|
|
33
41
|
const accountIssueCount = erroredAccounts.length
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from 'lib/seam/devices/use-devices.js'
|
|
23
23
|
import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
|
|
24
24
|
import { LoadingToast } from 'lib/ui/LoadingToast/LoadingToast.js'
|
|
25
|
+
import { Snackbar } from 'lib/ui/Snackbar/Snackbar.js'
|
|
25
26
|
import { EmptyPlaceholder } from 'lib/ui/Table/EmptyPlaceholder.js'
|
|
26
27
|
import { TableBody } from 'lib/ui/Table/TableBody.js'
|
|
27
28
|
import { TableHeader } from 'lib/ui/Table/TableHeader.js'
|
|
@@ -67,6 +68,8 @@ export function DeviceTable({
|
|
|
67
68
|
deviceComparator = compareByCreatedAtDesc,
|
|
68
69
|
heading = t.devices,
|
|
69
70
|
title = t.devices,
|
|
71
|
+
errorFilter = () => true,
|
|
72
|
+
warningFilter = () => true,
|
|
70
73
|
disableLockUnlock = false,
|
|
71
74
|
disableCreateAccessCode = false,
|
|
72
75
|
disableEditAccessCode = false,
|
|
@@ -77,7 +80,7 @@ export function DeviceTable({
|
|
|
77
80
|
}: DeviceTableProps = {}): JSX.Element {
|
|
78
81
|
useComponentTelemetry('DeviceTable')
|
|
79
82
|
|
|
80
|
-
const { devices, isInitialLoading, isError,
|
|
83
|
+
const { devices, isInitialLoading, isError, refetch } = useDevices({
|
|
81
84
|
device_ids: deviceIds,
|
|
82
85
|
connected_account_ids: connectedAccountIds,
|
|
83
86
|
})
|
|
@@ -107,6 +110,8 @@ export function DeviceTable({
|
|
|
107
110
|
return (
|
|
108
111
|
<NestedDeviceDetails
|
|
109
112
|
deviceId={selectedDeviceId}
|
|
113
|
+
errorFilter={errorFilter}
|
|
114
|
+
warningFilter={warningFilter}
|
|
110
115
|
disableLockUnlock={disableLockUnlock}
|
|
111
116
|
disableCreateAccessCode={disableCreateAccessCode}
|
|
112
117
|
disableEditAccessCode={disableEditAccessCode}
|
|
@@ -120,10 +125,6 @@ export function DeviceTable({
|
|
|
120
125
|
)
|
|
121
126
|
}
|
|
122
127
|
|
|
123
|
-
if (isError) {
|
|
124
|
-
return <p className={className}>{error?.message}</p>
|
|
125
|
-
}
|
|
126
|
-
|
|
127
128
|
return (
|
|
128
129
|
<div className={classNames('seam-device-table', className)}>
|
|
129
130
|
<ContentHeader onBack={onBack} />
|
|
@@ -152,8 +153,25 @@ export function DeviceTable({
|
|
|
152
153
|
)}
|
|
153
154
|
</TableHeader>
|
|
154
155
|
<TableBody>
|
|
155
|
-
<Content
|
|
156
|
+
<Content
|
|
157
|
+
devices={filteredDevices}
|
|
158
|
+
onDeviceClick={handleDeviceClick}
|
|
159
|
+
errorFilter={errorFilter}
|
|
160
|
+
/>
|
|
156
161
|
</TableBody>
|
|
162
|
+
|
|
163
|
+
<Snackbar
|
|
164
|
+
variant='error'
|
|
165
|
+
visible={isError}
|
|
166
|
+
message={t.fallbackErrorMessage}
|
|
167
|
+
action={{
|
|
168
|
+
label: t.tryAgain,
|
|
169
|
+
onClick: () => {
|
|
170
|
+
void refetch()
|
|
171
|
+
},
|
|
172
|
+
}}
|
|
173
|
+
disableCloseButton
|
|
174
|
+
/>
|
|
157
175
|
</div>
|
|
158
176
|
)
|
|
159
177
|
}
|
|
@@ -161,8 +179,9 @@ export function DeviceTable({
|
|
|
161
179
|
function Content(props: {
|
|
162
180
|
devices: Array<UseDevicesData[number]>
|
|
163
181
|
onDeviceClick: (deviceId: string) => void
|
|
182
|
+
errorFilter: (error: any) => boolean
|
|
164
183
|
}): JSX.Element {
|
|
165
|
-
const { devices, onDeviceClick } = props
|
|
184
|
+
const { devices, onDeviceClick, errorFilter } = props
|
|
166
185
|
const [filter, setFilter] = useState<AccountFilter | DeviceFilter | null>(
|
|
167
186
|
null
|
|
168
187
|
)
|
|
@@ -198,6 +217,7 @@ function Content(props: {
|
|
|
198
217
|
devices={devices}
|
|
199
218
|
filter={filter}
|
|
200
219
|
onFilterSelect={setFilter}
|
|
220
|
+
errorFilter={errorFilter}
|
|
201
221
|
/>
|
|
202
222
|
{filteredDevices.map((device) => (
|
|
203
223
|
<DeviceRow
|
|
@@ -216,4 +236,6 @@ const t = {
|
|
|
216
236
|
devices: 'Devices',
|
|
217
237
|
noDevicesMessage: 'Sorry, no devices were found',
|
|
218
238
|
loading: 'Loading devices',
|
|
239
|
+
tryAgain: 'Try again',
|
|
240
|
+
fallbackErrorMessage: 'Devices could not be loaded',
|
|
219
241
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DeviceModelV1,
|
|
3
|
+
RouteRequestParams,
|
|
4
|
+
RouteResponse,
|
|
5
|
+
} from '@seamapi/types/devicedb'
|
|
6
|
+
import { useQuery } from '@tanstack/react-query'
|
|
7
|
+
import type { SeamError } from 'seamapi'
|
|
8
|
+
|
|
9
|
+
import { useSeamClient } from 'lib/seam/use-seam-client.js'
|
|
10
|
+
import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js'
|
|
11
|
+
|
|
12
|
+
export type UseDeviceModelParams = DeviceModelsGetParams | string
|
|
13
|
+
export type UseDeviceModelData = DeviceModelV1 | null
|
|
14
|
+
|
|
15
|
+
export function useDeviceModel(
|
|
16
|
+
params?: UseDeviceModelParams
|
|
17
|
+
): UseSeamQueryResult<'deviceModel', UseDeviceModelData> {
|
|
18
|
+
const normalizedParams =
|
|
19
|
+
typeof params === 'string' ? { device_model_id: params } : params
|
|
20
|
+
|
|
21
|
+
const { client: seam } = useSeamClient()
|
|
22
|
+
const { data, ...rest } = useQuery<
|
|
23
|
+
DeviceModelsGetResponse['device_model'] | null,
|
|
24
|
+
SeamError
|
|
25
|
+
>({
|
|
26
|
+
enabled: seam != null,
|
|
27
|
+
queryKey: ['internal', 'device_models', 'get', normalizedParams],
|
|
28
|
+
queryFn: async () => {
|
|
29
|
+
if (seam == null) return null
|
|
30
|
+
const {
|
|
31
|
+
data: { device_model: deviceModel },
|
|
32
|
+
} = await seam.client.get<DeviceModelsGetResponse>(
|
|
33
|
+
'/internal/devicedb/v1/device_models/get',
|
|
34
|
+
{ params: normalizedParams }
|
|
35
|
+
)
|
|
36
|
+
return deviceModel
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
return { ...rest, deviceModel: data }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type DeviceModelsGetParams = RouteRequestParams<'/v1/device_models/get'>
|
|
44
|
+
|
|
45
|
+
type DeviceModelsGetResponse = RouteResponse<'/v1/device_models/get'>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DeviceModelV1,
|
|
3
|
+
RouteRequestParams,
|
|
4
|
+
RouteResponse,
|
|
5
|
+
} from '@seamapi/types/devicedb'
|
|
6
|
+
import { useQuery } from '@tanstack/react-query'
|
|
7
|
+
import type { SeamError } from 'seamapi'
|
|
8
|
+
|
|
9
|
+
import { useSeamClient } from 'lib/seam/use-seam-client.js'
|
|
10
|
+
import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js'
|
|
11
|
+
|
|
12
|
+
export type UseDeviceModelsParams = DeviceModelsListParams
|
|
13
|
+
export type UseDeviceModelsData = DeviceModelV1[]
|
|
14
|
+
|
|
15
|
+
export function useDeviceModels(
|
|
16
|
+
params?: UseDeviceModelsParams
|
|
17
|
+
): UseSeamQueryResult<'deviceModels', UseDeviceModelsData> {
|
|
18
|
+
const { client: seam } = useSeamClient()
|
|
19
|
+
|
|
20
|
+
const { data, ...rest } = useQuery<
|
|
21
|
+
DeviceModelsListResponse['device_models'],
|
|
22
|
+
SeamError
|
|
23
|
+
>({
|
|
24
|
+
enabled: seam != null,
|
|
25
|
+
queryKey: ['internal', 'device_models', 'list', params],
|
|
26
|
+
queryFn: async () => {
|
|
27
|
+
if (seam == null) return []
|
|
28
|
+
const {
|
|
29
|
+
data: { device_models: deviceModels },
|
|
30
|
+
} = await seam.client.get<DeviceModelsListResponse>(
|
|
31
|
+
'/internal/devicedb/v1/device_models/list',
|
|
32
|
+
{ params }
|
|
33
|
+
)
|
|
34
|
+
return deviceModels
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
return { ...rest, deviceModels: data }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type DeviceModelsListParams = RouteRequestParams<'/v1/device_models/list'>
|
|
42
|
+
|
|
43
|
+
type DeviceModelsListResponse = RouteResponse<'/v1/device_models/list'>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Manufacturer,
|
|
3
|
+
RouteRequestParams,
|
|
4
|
+
RouteResponse,
|
|
5
|
+
} from '@seamapi/types/devicedb'
|
|
6
|
+
import { useQuery } from '@tanstack/react-query'
|
|
7
|
+
import type { SeamError } from 'seamapi'
|
|
8
|
+
|
|
9
|
+
import { useSeamClient } from 'lib/seam/use-seam-client.js'
|
|
10
|
+
import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js'
|
|
11
|
+
|
|
12
|
+
export type UseManufacturerParams = ManufacturersGetParams | string
|
|
13
|
+
export type UseManufacturerData = Manufacturer | null
|
|
14
|
+
|
|
15
|
+
export function useManufacturer(
|
|
16
|
+
params?: UseManufacturerParams
|
|
17
|
+
): UseSeamQueryResult<'manufacturer', UseManufacturerData> {
|
|
18
|
+
const normalizedParams =
|
|
19
|
+
typeof params === 'string' ? { manufacturer_id: params } : params
|
|
20
|
+
|
|
21
|
+
const { client: seam } = useSeamClient()
|
|
22
|
+
const { data, ...rest } = useQuery<
|
|
23
|
+
ManufacturersGetResponse['manufacturer'] | null,
|
|
24
|
+
SeamError
|
|
25
|
+
>({
|
|
26
|
+
enabled: seam != null,
|
|
27
|
+
queryKey: ['internal', 'manufacturers', 'get', normalizedParams],
|
|
28
|
+
queryFn: async () => {
|
|
29
|
+
if (seam == null) return null
|
|
30
|
+
const {
|
|
31
|
+
data: { manufacturer },
|
|
32
|
+
} = await seam.client.get<ManufacturersGetResponse>(
|
|
33
|
+
'/internal/devicedb/v1/manufacturers/get',
|
|
34
|
+
{ params: normalizedParams }
|
|
35
|
+
)
|
|
36
|
+
return manufacturer
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
return { ...rest, manufacturer: data }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type ManufacturersGetParams = RouteRequestParams<'/v1/manufacturers/get'>
|
|
44
|
+
|
|
45
|
+
type ManufacturersGetResponse = RouteResponse<'/v1/manufacturers/get'>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Manufacturer,
|
|
3
|
+
RouteRequestParams,
|
|
4
|
+
RouteResponse,
|
|
5
|
+
} from '@seamapi/types/devicedb'
|
|
6
|
+
import { useQuery } from '@tanstack/react-query'
|
|
7
|
+
import type { SeamError } from 'seamapi'
|
|
8
|
+
|
|
9
|
+
import { useSeamClient } from 'lib/seam/use-seam-client.js'
|
|
10
|
+
import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js'
|
|
11
|
+
|
|
12
|
+
export type UseManufacturersParams = ManufacturersListParams
|
|
13
|
+
export type UseManufacturersData = Manufacturer[]
|
|
14
|
+
|
|
15
|
+
export function useManufacturers(
|
|
16
|
+
params?: UseManufacturersParams
|
|
17
|
+
): UseSeamQueryResult<'manufacturers', UseManufacturersData> {
|
|
18
|
+
const { client: seam } = useSeamClient()
|
|
19
|
+
|
|
20
|
+
const { data, ...rest } = useQuery<
|
|
21
|
+
ManufacturersListResponse['manufacturers'],
|
|
22
|
+
SeamError
|
|
23
|
+
>({
|
|
24
|
+
enabled: seam != null,
|
|
25
|
+
queryKey: ['internal', 'manufacturers', 'list', params],
|
|
26
|
+
queryFn: async () => {
|
|
27
|
+
if (seam == null) return []
|
|
28
|
+
const {
|
|
29
|
+
data: { manufacturers },
|
|
30
|
+
} = await seam.client.get<ManufacturersListResponse>(
|
|
31
|
+
'/internal/devicedb/v1/manufacturers/list',
|
|
32
|
+
{ params }
|
|
33
|
+
)
|
|
34
|
+
return manufacturers
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
return { ...rest, manufacturers: data }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type ManufacturersListParams = RouteRequestParams<'/v1/manufacturers/list'>
|
|
42
|
+
|
|
43
|
+
type ManufacturersListResponse = RouteResponse<'/v1/manufacturers/list'>
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import type { ComponentType } from 'react'
|
|
2
|
+
import type {
|
|
3
|
+
AccessCodeError,
|
|
4
|
+
ConnectedAccountError,
|
|
5
|
+
DeviceError,
|
|
6
|
+
SeamWarning,
|
|
7
|
+
} from 'seamapi'
|
|
2
8
|
|
|
3
9
|
export interface RequiredCommonProps {
|
|
4
10
|
className: string | undefined
|
|
5
11
|
onBack: (() => void) | undefined
|
|
12
|
+
errorFilter: (
|
|
13
|
+
error: ConnectedAccountError | DeviceError | AccessCodeError
|
|
14
|
+
) => boolean
|
|
15
|
+
warningFilter: (warning: SeamWarning) => boolean
|
|
6
16
|
disableDeleteAccessCode: boolean | undefined
|
|
7
17
|
disableCreateAccessCode: boolean | undefined
|
|
8
18
|
disableEditAccessCode: boolean | undefined
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AccessCodeError,
|
|
3
|
+
ConnectedAccountError,
|
|
4
|
+
DeviceError,
|
|
5
|
+
SeamWarning,
|
|
6
|
+
} from 'seamapi'
|
|
7
|
+
|
|
8
|
+
type SeamCompositeError = ConnectedAccountError | DeviceError | AccessCodeError
|
|
9
|
+
|
|
10
|
+
export const accessCodeErrorFilter = (error: SeamCompositeError): boolean => {
|
|
11
|
+
return 'is_access_code_error' in error && error.is_access_code_error
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const accessCodeWarningFilter = (_: SeamWarning): boolean => {
|
|
15
|
+
return true
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const deviceErrorFilter = (error: SeamCompositeError): boolean => {
|
|
19
|
+
return 'is_device_error' in error && error.is_device_error
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const deviceWarningFilter = (_: SeamWarning): boolean => {
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const connectedAccountErrorFilter = (
|
|
27
|
+
error: SeamCompositeError
|
|
28
|
+
): boolean => {
|
|
29
|
+
return (
|
|
30
|
+
'is_connected_account_error' in error && error.is_connected_account_error
|
|
31
|
+
)
|
|
32
|
+
}
|