Skip to content

Commit

Permalink
feat: Add noise sensor activity list (#633)
Browse files Browse the repository at this point in the history
* Add `TabSet`

* ci: Format code

* ci: Format code

* Recalculate on window resize

* ci: Format code

* Format

* ci: Format code

* Move ContentHeader, add on-fly adjustment

* Add noise activity list (empty)

* ci: Format code

* Add useEvents

* ci: Format code

* Begin style improvements

* Fix merge issues

* ci: Format code

* More style fixes

* ci: Format code

* Update item style

* Annotate return type on `TabSet`

* Remove console.log

* Update effect deps with `calculateHighlightStyle`

* Lint fixes

* ci: Format code

* Update types and row layout style

* ci: Format code

* Update layout style

* Lint fixes

* Remove string template

* Remove string typeof checks

* Use `globalThis`

* Use `globalThis` (again)

* Use `tabTitles`

* Remove dates global func

* ci: Format code

* `useNow`

* ci: Format code

* Remove `noise_detection.detected_noise`

* Replace with Luxon constants

* Add expect err comment

* Add return type

* Format fixes

* Change var names

* Remove TS comment

* Add refetchInterval

* Update comment

* Add issue link

* ci: Format code

---------

Co-authored-by: Seam Bot <devops@getseam.com>
  • Loading branch information
xplato and seambot authored May 23, 2024
1 parent 1221de8 commit 9058743
Show file tree
Hide file tree
Showing 15 changed files with 526 additions and 26 deletions.
20 changes: 20 additions & 0 deletions .storybook/seed-fake.js
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,26 @@ export const seedFake = (db) => {
name: 'Active Hours',
})

db.addEvent({
device_id: device7.device_id,
workspace_id: ws2.workspace_id,
created_at: '2024-05-16T00:16:12.000',
event_type: 'noise_sensor.noise_threshold_triggered',
noise_level_decibels: 75,
noise_threshold_id: 2,
noise_threshold_name: 'Active Hours',
})

db.addEvent({
device_id: device7.device_id,
workspace_id: ws2.workspace_id,
created_at: '2024-05-16T00:16:12.000',
event_type: 'noise_sensor.noise_threshold_triggered',
noise_level_decibels: 75,
noise_threshold_id: 2,
noise_threshold_name: 'Active Hours',
})

// add climate setting schedules
db.addClimateSettingSchedule({
device_id: device5.device_id,
Expand Down
8 changes: 8 additions & 0 deletions assets/icons/clock.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions src/lib/icons/Clock.tsx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const meta: Meta<typeof DeviceDetails> = {
type: 'figma',
url: 'https://www.figma.com/file/Su3VO6yupz4yxe88fv0Uqa/Seam-Components?type=design&node-id=358-39439&mode=design&t=4OQwfRB8Mw8kT1rw-4',
},
layout: 'fullscreen',
},
}

Expand Down
80 changes: 56 additions & 24 deletions src/lib/seam/components/DeviceDetails/NoiseSensorDeviceDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import classNames from 'classnames'
import { useState } from 'react'
import type { NoiseSensorDevice } from 'seamapi'

import type { NestedSpecificDeviceDetailsProps } from 'lib/seam/components/DeviceDetails/DeviceDetails.js'
Expand All @@ -8,7 +9,11 @@ import { DeviceImage } from 'lib/ui/device/DeviceImage.js'
import { NoiseLevelStatus } from 'lib/ui/device/NoiseLevelStatus.js'
import { OnlineStatus } from 'lib/ui/device/OnlineStatus.js'
import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
import { NoiseSensorActivityList } from 'lib/ui/noise-sensor/NoiseSensorActivityList.js'
import { NoiseThresholdsList } from 'lib/ui/noise-sensor/NoiseThresholdsList.js'
import { TabSet } from 'lib/ui/TabSet.js'

type TabType = 'details' | 'activity'

interface NoiseSensorDeviceDetailsProps
extends NestedSpecificDeviceDetailsProps {
Expand All @@ -22,38 +27,63 @@ export function NoiseSensorDeviceDetails({
onBack,
className,
}: NoiseSensorDeviceDetailsProps): JSX.Element | null {
const [tab, setTab] = useState<TabType>('details')

return (
<div className={classNames('seam-device-details', className)}>
<ContentHeader title={t.noiseSensor} onBack={onBack} />

<div className='seam-body'>
<div className='seam-summary'>
<div className='seam-content'>
<div className='seam-image'>
<DeviceImage device={device} />
</div>
<div className='seam-info'>
<span className='seam-label'>{t.noiseSensor}</span>
<h4 className='seam-device-name'>{device.properties.name}</h4>
<div className='seam-properties'>
<span className='seam-label'>{t.status}:</span>{' '}
<OnlineStatus device={device} />
<NoiseLevelStatus device={device} />
<DeviceModel device={device} />
<div className='seam-body seam-body-no-margin'>
<div className='seam-contained-summary'>
<ContentHeader
title={t.noiseSensor}
onBack={onBack}
className='seam-content-header-contained'
/>
<div className='seam-summary'>
<div className='seam-content'>
<div className='seam-image'>
<DeviceImage device={device} />
</div>
<div className='seam-info'>
<span className='seam-label'>{t.noiseSensor}</span>
<h4 className='seam-device-name'>{device.properties.name}</h4>
<div className='seam-properties'>
<span className='seam-label'>{t.status}:</span>{' '}
<OnlineStatus device={device} />
<NoiseLevelStatus device={device} />
<DeviceModel device={device} />
</div>
</div>
</div>
</div>

<TabSet<TabType>
tabs={['details', 'activity']}
tabTitles={{
details: t.details,
activity: t.activity,
}}
activeTab={tab}
onTabChange={(tab) => {
setTab(tab)
}}
/>
</div>

<NoiseThresholdsList device={device} />
{tab === 'details' && (
<div className='seam-padded-container'>
<NoiseThresholdsList device={device} />

<DeviceInfo
device={device}
disableConnectedAccountInformation={
disableConnectedAccountInformation
}
disableResourceIds={disableResourceIds}
/>
</div>
)}

<DeviceInfo
device={device}
disableConnectedAccountInformation={
disableConnectedAccountInformation
}
disableResourceIds={disableResourceIds}
/>
{tab === 'activity' && <NoiseSensorActivityList device={device} />}
</div>
</div>
)
Expand All @@ -63,4 +93,6 @@ const t = {
noiseSensor: 'Noise Sensor',
status: 'Status',
noiseLevel: 'Noise level',
details: 'Details',
activity: 'Activity',
}
44 changes: 44 additions & 0 deletions src/lib/seam/events/use-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useQuery, useQueryClient } from '@tanstack/react-query'
import type {
Event,
EventsListRequest,
EventsListResponse,
SeamError,
} from 'seamapi'

import { useSeamClient } from 'lib/seam/use-seam-client.js'
import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js'

export type UseEventsParams = EventsListRequest
export type UseEventsData = Event[]
export interface UseEventsOptions {
refetchInterval?: number
}

export function useEvents(
params?: UseEventsParams,
options?: UseEventsOptions
): UseSeamQueryResult<'events', UseEventsData> {
const { client } = useSeamClient()
const queryClient = useQueryClient()

const { data, ...rest } = useQuery<EventsListResponse['events'], SeamError>({
enabled: client != null,
queryKey: ['events', 'list', params],
queryFn: async () => {
if (client == null) return []
return await client.events.list(params)
},
onSuccess: (events) => {
for (const event of events) {
queryClient.setQueryData(
['events', 'get', { event_id: event.event_id }],
event
)
}
},
refetchInterval: options?.refetchInterval ?? 30_000,
})

return { ...rest, events: data }
}
110 changes: 110 additions & 0 deletions src/lib/ui/TabSet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import classNames from 'classnames'
import {
type MouseEventHandler,
useCallback,
useEffect,
useLayoutEffect,
useState,
} from 'react'

interface TabSetProps<TabType extends string> {
tabs: TabType[]
tabTitles: Record<TabType, string>
activeTab: TabType
onTabChange: (tab: TabType) => void
}

interface HighlightStyle {
left: number
width: number
}

export function TabSet<TabType extends string>({
tabs,
tabTitles,
activeTab,
onTabChange,
}: TabSetProps<TabType>): JSX.Element {
const [highlightStyle, setHighlightStyle] = useState<HighlightStyle>({
left: 0,
width: 140,
})

const calculateHighlightStyle = useCallback(() => {
const tabButton: HTMLButtonElement | null =
globalThis.document?.querySelector(
`.seam-tab-button:nth-of-type(${tabs.indexOf(activeTab) + 1})`
)

setHighlightStyle({
left: tabButton?.offsetLeft ?? 0,
width: tabButton?.offsetWidth ?? 140,
})
}, [activeTab, tabs])

useLayoutEffect(() => {
calculateHighlightStyle()
}, [activeTab, calculateHighlightStyle])

useEffect(() => {
globalThis.addEventListener?.('resize', calculateHighlightStyle)
return () => {
globalThis.removeEventListener?.('resize', calculateHighlightStyle)
}
}, [calculateHighlightStyle])

return (
<div className='seam-tab-set'>
<div className='seam-tab-set-buttons'>
<div className='seam-tab-set-highlight' style={highlightStyle} />

{tabs.map((tab) => (
<TabButton<TabType>
key={tab}
tab={tab}
title={tabTitles[tab]}
isActive={activeTab === tab}
onTabChange={onTabChange}
setHighlightStyle={setHighlightStyle}
/>
))}
</div>
</div>
)
}

interface TabButtonProps<TabType> {
tab: TabType
title: string
isActive: boolean
onTabChange: (tab: TabType) => void
setHighlightStyle: (style: HighlightStyle) => void
}

function TabButton<TabType extends string>({
tab,
title,
isActive,
onTabChange,
setHighlightStyle,
}: TabButtonProps<TabType>): JSX.Element {
const handleClick: MouseEventHandler<HTMLButtonElement> = (ev) => {
onTabChange(tab)
setHighlightStyle({
left: ev.currentTarget.offsetLeft,
width: ev.currentTarget.offsetWidth,
})
}

return (
<button
className={classNames(
'seam-tab-button',
isActive && 'seam-tab-button-active'
)}
onClick={handleClick}
>
<p className='seam-tab-button-label'>{title}</p>
</button>
)
}
7 changes: 5 additions & 2 deletions src/lib/ui/layout/ContentHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import classNames from 'classnames'

import { ArrowBackIcon } from 'lib/icons/ArrowBack.js'

interface ContentHeaderProps {
onBack: (() => void) | undefined
title?: string
subheading?: string
className?: string
}

export function ContentHeader(props: ContentHeaderProps): JSX.Element | null {
const { title, onBack, subheading } = props
const { title, onBack, subheading, className } = props
if (title == null && onBack == null) {
return null
}

return (
<div className='seam-content-header'>
<div className={classNames('seam-content-header', className)}>
<BackIcon onClick={onBack} />
<div>
<span className='seam-title'>{title}</span>
Expand Down
Loading

0 comments on commit 9058743

Please sign in to comment.