Skip to content

Commit

Permalink
Merge pull request #247 from seamapi/dh-code-issues
Browse files Browse the repository at this point in the history
Add Access Code filter; show warning icon on access codes table
  • Loading branch information
dawnho authored Jun 28, 2023
2 parents 681bbc8 + aa5235e commit 9f69384
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 6 deletions.
41 changes: 41 additions & 0 deletions .storybook/seed-fake.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,47 @@ export const seedFake = (db) => {
is_managed: true,
})

db.addAccessCode({
device_id: device1.device_id,
workspace_id: ws2.workspace_id,
created_at: '2023-05-19T03:11:10.000',
name: "Bob's Front Door Code",
code: '3333',
common_code_key: null,
type: 'ongoing',
status: 'set',
errors: [
{
error_code: 'failed_to_set_on_device',
is_device_error: true,
message:
'An error occurred when we tried to set the access code on the device. We will continue to try and set the code on the device in case the error was temporary.',
},
],
warnings: [],
is_managed: true,
})

db.addAccessCode({
device_id: device1.device_id,
workspace_id: ws2.workspace_id,
created_at: '2023-05-19T03:11:10.000',
name: "Kai's Front Door Code",
code: '3334',
common_code_key: null,
type: 'ongoing',
status: 'set',
errors: [],
warnings: [
{
warning_code: 'delay_in_setting_on_device',
message:
'There was an unusually long delay in programming the code onto the device. For time bound codes, this is sent when the code enters its active time. Note that this is a temporary warning and might be removed if the code is successfully set.',
},
],
is_managed: true,
})

const device2 = db.addDevice({
connected_account_id: ca.connected_account_id,
device_type: 'august_lock',
Expand Down
5 changes: 5 additions & 0 deletions assets/icons/triangle-warning-outline.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions src/lib/icons/TriangleWarningOutline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { SVGProps } from 'react'
export const TriangleWarningOutlineIcon = (
props: SVGProps<SVGSVGElement>
): JSX.Element => (
<svg
xmlns='http://www.w3.org/2000/svg'
width={24}
height={24}
fill='none'
{...props}
>
<path
fill='#FF9800'
d='m12 6.49 7.53 13.01H4.47L12 6.49Zm0-3.99-11 19h22l-11-19Z'
/>
<path fill='#FF9800' d='M13 16.5h-2v2h2v-2ZM13 10.5h-2v5h2v-5Z' />
</svg>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Box } from '@mui/material'
import type { Meta, StoryObj } from '@storybook/react'
import { useState } from 'react'

import {
type AccessCodeFilter,
AccessCodeHealthBar,
} from 'lib/seam/components/AccessCodeTable/AccessCodeHealthBar.js'

const meta: Meta<typeof AccessCodeHealthBar> = {
title: 'Library/AccessCodeHealthBar',
component: AccessCodeHealthBar,
tags: ['autodocs'],
}

export default meta

type Story = StoryObj<typeof AccessCodeHealthBar>

export const Content: Story = {
render: () => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [filter, setFilter] = useState<AccessCodeFilter | null>(null)
return (
<Box display='grid' gap={3} gridTemplateColumns='1fr'>
<AccessCodeHealthBar
filter={null}
onFilterSelect={() => {}}
accessCodes={[]}
/>
<AccessCodeHealthBar
filter={filter}
onFilterSelect={setFilter}
accessCodes={[
{
device_id: 'device_1',
access_code_id: 'code_1',
created_at: '2023-05-08T22:38:30.963Z',
type: 'ongoing',
code: '1234',
status: 'setting',
is_backup_access_code_available: false,
errors: [
{
error_code: 'account_disconnected',
is_connected_account_error: true,
message:
'Account Disconnected, you may need to reconnect the account with a new webview.',
},
],
warnings: [
{
warning_code: 'salto_office_mode',
message:
'Salto office mode is enabled. Access codes will not unlock doors. You can disable office mode in the Salto dashboard.',
},
],
},

{
device_id: 'device_1',
access_code_id: 'code_2',
created_at: '2023-05-08T22:38:30.963Z',
type: 'ongoing',
code: '1234',
status: 'setting',
is_backup_access_code_available: false,
errors: [
{
error_code: 'account_disconnected',
is_connected_account_error: true,
message:
'Account Disconnected, you may need to reconnect the account with a new webview.',
},
],
warnings: [
{
warning_code: 'salto_office_mode',
message:
'Salto office mode is enabled. Access codes will not unlock doors. You can disable office mode in the Salto dashboard.',
},
],
},
]}
/>
</Box>
)
},
}
68 changes: 68 additions & 0 deletions src/lib/seam/components/AccessCodeTable/AccessCodeHealthBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { CheckIcon } from 'lib/icons/Check.js'
import { ExclamationCircleOutlineIcon } from 'lib/icons/ExclamationCircleOutline.js'
import type { UseAccessCodesData } from 'lib/seam/access-codes/use-access-codes.js'
import { TableFilterBar } from 'lib/ui/Table/TableFilterBar/TableFilterBar.js'
import { TableFilterItem } from 'lib/ui/Table/TableFilterBar/TableFilterItem.js'

export type AccessCodeFilter = 'access_code_issues'

interface AccessCodeHealthBarProps {
accessCodes: Array<UseAccessCodesData[number]>
filter: AccessCodeFilter | null
onFilterSelect: (filter: AccessCodeFilter | null) => void
}

export function AccessCodeHealthBar({
accessCodes,
filter,
onFilterSelect,
}: AccessCodeHealthBarProps): JSX.Element {
const codesWithIssues = accessCodes.filter(
({ errors, warnings }) => errors.length > 0 || warnings.length > 0
)
const issueCount = codesWithIssues.length

const toggle = (target: AccessCodeFilter) => () => {
const newFilter = target === filter ? null : target
onFilterSelect(newFilter)
}

const isPlural = issueCount === 0 || issueCount > 1
const label = isPlural
? `${issueCount} ${t.accessCodeIssues}`
: `${issueCount} ${t.accessCodeIssue}`

if (issueCount === 0) {
return (
<TableFilterBar filterCleared>
<TableFilterItem>
<CheckIcon />
{t.accessCodesOk}
</TableFilterItem>
</TableFilterBar>
)
}

return (
<TableFilterBar
filterCleared={filter == null}
onFilterClear={() => {
onFilterSelect(null)
}}
>
<TableFilterItem
onClick={toggle('access_code_issues')}
selected={filter === 'access_code_issues'}
>
<ExclamationCircleOutlineIcon />
{label}
</TableFilterItem>
</TableFilterBar>
)
}

const t = {
accessCodesOk: 'Access codes OK',
accessCodeIssue: 'access code issue',
accessCodeIssues: 'access code issues',
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ export const Content: Story = {
),
}

export const Issue: Story = {
render: (props, { globals }) => (
<AccessCodeTable
{...props}
deviceId={props.deviceId ?? globals['deviceId']}
accessCodeFilter={(accessCode) => accessCode.errors.length > 0}
/>
),
}

export const InsideModal: Story = {
render: (props, { globals }) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
Expand Down
50 changes: 44 additions & 6 deletions src/lib/seam/components/AccessCodeTable/AccessCodeTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ import { compareByCreatedAtDesc } from 'lib/dates.js'
import { AccessCodeKeyIcon } from 'lib/icons/AccessCodeKey.js'
import { CopyIcon } from 'lib/icons/Copy.js'
import { ExclamationCircleOutlineIcon } from 'lib/icons/ExclamationCircleOutline.js'
import { TriangleWarningOutlineIcon } from 'lib/icons/TriangleWarningOutline.js'
import {
useAccessCodes,
type UseAccessCodesData,
} from 'lib/seam/access-codes/use-access-codes.js'
import { AccessCodeDetails } from 'lib/seam/components/AccessCodeDetails/AccessCodeDetails.js'
import {
type AccessCodeFilter,
AccessCodeHealthBar,
} from 'lib/seam/components/AccessCodeTable/AccessCodeHealthBar.js'
import { CodeDetails } from 'lib/seam/components/AccessCodeTable/CodeDetails.js'
import { copyToClipboard } from 'lib/ui/clipboard.js'
import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
Expand Down Expand Up @@ -49,7 +54,11 @@ const defaultAccessCodeFilter = (
): boolean => {
const value = searchInputValue.trim()
if (value === '') return true
return new RegExp(value, 'i').test(accessCode.name ?? '')
const searchRegex = new RegExp(value, 'i')
return (
searchRegex.test(accessCode?.name ?? '') ||
searchRegex.test(accessCode?.code ?? '')
)
}

export function AccessCodeTable({
Expand Down Expand Up @@ -132,14 +141,34 @@ function Content(props: {
onAccessCodeClick: (accessCodeId: string) => void
}): JSX.Element {
const { accessCodes, onAccessCodeClick } = props
const [filter, setFilter] = useState<AccessCodeFilter | null>(null)

const filteredAccessCodes = useMemo(() => {
if (filter === null) {
return accessCodes
}

if (filter === 'access_code_issues') {
return accessCodes.filter((accessCode) => {
return accessCode.errors.length > 0 || accessCode.warnings.length > 0
})
}

return accessCodes
}, [accessCodes, filter])

if (accessCodes.length === 0) {
return <EmptyPlaceholder>{t.noAccessCodesMessage}</EmptyPlaceholder>
}

return (
<>
{accessCodes.map((accessCode) => (
<AccessCodeHealthBar
accessCodes={accessCodes}
filter={filter}
onFilterSelect={setFilter}
/>
{filteredAccessCodes.map((accessCode) => (
<AccessCodeRow
key={accessCode.access_code_id}
accessCode={accessCode}
Expand All @@ -158,14 +187,18 @@ function AccessCodeRow(props: {
}): JSX.Element {
const { onClick, accessCode } = props

const errorCount = accessCode.errors?.length ?? 0
const errorCount = accessCode.errors.length
const warningCount = accessCode.warnings.length
const isPlural = errorCount === 0 || errorCount > 1
const issueIconTitle = isPlural
const errorIconTitle = isPlural
? `${errorCount} ${t.codeIssues}`
: `${errorCount} ${t.codeIssue}`
const warningIconTitle = isPlural
? `${warningCount} ${t.codeIssues}`
: `${warningCount} ${t.codeIssue}`

return (
<TableRow key={accessCode.access_code_id} onClick={onClick}>
<TableRow onClick={onClick}>
<TableCell className='seam-icon-cell'>
<div>
<AccessCodeKeyIcon />
Expand All @@ -177,10 +210,15 @@ function AccessCodeRow(props: {
</TableCell>
<TableCell className='seam-action-cell'>
{errorCount > 0 && (
<div className='seam-code-issue-icon-wrap' title={issueIconTitle}>
<div className='seam-code-issue-icon-wrap' title={errorIconTitle}>
<ExclamationCircleOutlineIcon />
</div>
)}
{errorCount === 0 && warningCount > 0 && (
<div className='seam-code-issue-icon-wrap' title={warningIconTitle}>
<TriangleWarningOutlineIcon />
</div>
)}
<MoreActionsMenu
menuProps={{
backgroundProps: {
Expand Down

0 comments on commit 9f69384

Please sign in to comment.