@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.
Files changed (82) hide show
  1. package/README.md +61 -4
  2. package/dist/elements.js +6133 -6020
  3. package/dist/elements.js.map +1 -1
  4. package/dist/index.css +2 -4
  5. package/dist/index.css.map +1 -1
  6. package/dist/index.min.css +1 -1
  7. package/dist/index.min.css.map +1 -1
  8. package/lib/seam/SeamProvider.d.ts +2 -1
  9. package/lib/seam/SeamProvider.js +10 -4
  10. package/lib/seam/SeamProvider.js.map +1 -1
  11. package/lib/seam/components/AccessCodeDetails/AccessCodeDetails.d.ts +1 -1
  12. package/lib/seam/components/AccessCodeDetails/AccessCodeDetails.js +11 -13
  13. package/lib/seam/components/AccessCodeDetails/AccessCodeDetails.js.map +1 -1
  14. package/lib/seam/components/AccessCodeTable/AccessCodeHealthBar.d.ts +4 -1
  15. package/lib/seam/components/AccessCodeTable/AccessCodeHealthBar.js +3 -2
  16. package/lib/seam/components/AccessCodeTable/AccessCodeHealthBar.js.map +1 -1
  17. package/lib/seam/components/AccessCodeTable/AccessCodeTable.d.ts +1 -1
  18. package/lib/seam/components/AccessCodeTable/AccessCodeTable.js +15 -11
  19. package/lib/seam/components/AccessCodeTable/AccessCodeTable.js.map +1 -1
  20. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.d.ts +1 -1
  21. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.js +2 -2
  22. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.js.map +1 -1
  23. package/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleTable.d.ts +1 -1
  24. package/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleTable.js +12 -7
  25. package/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleTable.js.map +1 -1
  26. package/lib/seam/components/DeviceDetails/LockDeviceDetails.js +11 -4
  27. package/lib/seam/components/DeviceDetails/LockDeviceDetails.js.map +1 -1
  28. package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.d.ts +1 -1
  29. package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js +2 -2
  30. package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js.map +1 -1
  31. package/lib/seam/components/DeviceTable/DeviceHealthBar.d.ts +3 -1
  32. package/lib/seam/components/DeviceTable/DeviceHealthBar.js +4 -3
  33. package/lib/seam/components/DeviceTable/DeviceHealthBar.js.map +1 -1
  34. package/lib/seam/components/DeviceTable/DeviceTable.d.ts +1 -1
  35. package/lib/seam/components/DeviceTable/DeviceTable.js +14 -9
  36. package/lib/seam/components/DeviceTable/DeviceTable.js.map +1 -1
  37. package/lib/seam/components/SupportedDeviceTable/use-device-model.d.ts +7 -0
  38. package/lib/seam/components/SupportedDeviceTable/use-device-model.js +18 -0
  39. package/lib/seam/components/SupportedDeviceTable/use-device-model.js.map +1 -0
  40. package/lib/seam/components/SupportedDeviceTable/use-device-models.d.ts +7 -0
  41. package/lib/seam/components/SupportedDeviceTable/use-device-models.js +17 -0
  42. package/lib/seam/components/SupportedDeviceTable/use-device-models.js.map +1 -0
  43. package/lib/seam/components/SupportedDeviceTable/use-manufacturer.d.ts +7 -0
  44. package/lib/seam/components/SupportedDeviceTable/use-manufacturer.js +18 -0
  45. package/lib/seam/components/SupportedDeviceTable/use-manufacturer.js.map +1 -0
  46. package/lib/seam/components/SupportedDeviceTable/use-manufacturers.d.ts +7 -0
  47. package/lib/seam/components/SupportedDeviceTable/use-manufacturers.js +17 -0
  48. package/lib/seam/components/SupportedDeviceTable/use-manufacturers.js.map +1 -0
  49. package/lib/seam/components/common-props.d.ts +3 -0
  50. package/lib/seam/components/common-props.js.map +1 -1
  51. package/lib/seam/filters.d.ts +8 -0
  52. package/lib/seam/filters.js +16 -0
  53. package/lib/seam/filters.js.map +1 -0
  54. package/lib/telemetry/client.js.map +1 -1
  55. package/lib/ui/Snackbar/Snackbar.d.ts +1 -1
  56. package/lib/ui/Snackbar/Snackbar.js +1 -1
  57. package/lib/ui/Snackbar/Snackbar.js.map +1 -1
  58. package/lib/version.d.ts +1 -1
  59. package/lib/version.js +1 -1
  60. package/package.json +15 -5
  61. package/src/lib/element.tsx +6 -1
  62. package/src/lib/seam/SeamProvider.tsx +17 -2
  63. package/src/lib/seam/components/AccessCodeDetails/AccessCodeDetails.tsx +24 -30
  64. package/src/lib/seam/components/AccessCodeTable/AccessCodeHealthBar.tsx +9 -1
  65. package/src/lib/seam/components/AccessCodeTable/AccessCodeTable.tsx +32 -5
  66. package/src/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.tsx +4 -0
  67. package/src/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleTable.tsx +21 -5
  68. package/src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx +19 -8
  69. package/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx +4 -0
  70. package/src/lib/seam/components/DeviceTable/DeviceHealthBar.tsx +10 -2
  71. package/src/lib/seam/components/DeviceTable/DeviceTable.tsx +29 -7
  72. package/src/lib/seam/components/SupportedDeviceTable/use-device-model.ts +45 -0
  73. package/src/lib/seam/components/SupportedDeviceTable/use-device-models.ts +43 -0
  74. package/src/lib/seam/components/SupportedDeviceTable/use-manufacturer.ts +45 -0
  75. package/src/lib/seam/components/SupportedDeviceTable/use-manufacturers.ts +43 -0
  76. package/src/lib/seam/components/common-props.tsx +10 -0
  77. package/src/lib/seam/filters.ts +32 -0
  78. package/src/lib/telemetry/client.ts +3 -3
  79. package/src/lib/ui/Snackbar/Snackbar.tsx +2 -2
  80. package/src/lib/version.ts +1 -1
  81. package/src/styles/_device-details.scss +1 -2
  82. 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 }) => errors.length > 0 || warnings.length > 0
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, error } = useAccessCodes({
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
  }
@@ -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, error } =
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.map((error) => ({
68
- variant: 'error' as const,
69
- message: error.message,
70
- })),
71
- ...device.warnings.map((warning) => ({
72
- variant: 'warning' as const,
73
- message: warning.message,
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((error) => 'is_connected_account_error' in error)
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((error) => 'is_device_error' in error).length > 0
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, error } = useDevices({
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 devices={filteredDevices} onDeviceClick={handleDeviceClick} />
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
+ }