Skip to content

Commit 9058743

Browse files
xplatoseambot
andauthored
feat: Add noise sensor activity list (#633)
* 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 <[email protected]>
1 parent 1221de8 commit 9058743

15 files changed

+526
-26
lines changed

.storybook/seed-fake.js

+20
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,26 @@ export const seedFake = (db) => {
513513
name: 'Active Hours',
514514
})
515515

516+
db.addEvent({
517+
device_id: device7.device_id,
518+
workspace_id: ws2.workspace_id,
519+
created_at: '2024-05-16T00:16:12.000',
520+
event_type: 'noise_sensor.noise_threshold_triggered',
521+
noise_level_decibels: 75,
522+
noise_threshold_id: 2,
523+
noise_threshold_name: 'Active Hours',
524+
})
525+
526+
db.addEvent({
527+
device_id: device7.device_id,
528+
workspace_id: ws2.workspace_id,
529+
created_at: '2024-05-16T00:16:12.000',
530+
event_type: 'noise_sensor.noise_threshold_triggered',
531+
noise_level_decibels: 75,
532+
noise_threshold_id: 2,
533+
noise_threshold_name: 'Active Hours',
534+
})
535+
516536
// add climate setting schedules
517537
db.addClimateSettingSchedule({
518538
device_id: device5.device_id,

assets/icons/clock.svg

+8
Loading

src/lib/icons/Clock.tsx

+36
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/seam/components/DeviceDetails/DeviceDetails.stories.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const meta: Meta<typeof DeviceDetails> = {
1818
type: 'figma',
1919
url: 'https://www.figma.com/file/Su3VO6yupz4yxe88fv0Uqa/Seam-Components?type=design&node-id=358-39439&mode=design&t=4OQwfRB8Mw8kT1rw-4',
2020
},
21+
layout: 'fullscreen',
2122
},
2223
}
2324

Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import classNames from 'classnames'
2+
import { useState } from 'react'
23
import type { NoiseSensorDevice } from 'seamapi'
34

45
import type { NestedSpecificDeviceDetailsProps } from 'lib/seam/components/DeviceDetails/DeviceDetails.js'
@@ -8,7 +9,11 @@ import { DeviceImage } from 'lib/ui/device/DeviceImage.js'
89
import { NoiseLevelStatus } from 'lib/ui/device/NoiseLevelStatus.js'
910
import { OnlineStatus } from 'lib/ui/device/OnlineStatus.js'
1011
import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
12+
import { NoiseSensorActivityList } from 'lib/ui/noise-sensor/NoiseSensorActivityList.js'
1113
import { NoiseThresholdsList } from 'lib/ui/noise-sensor/NoiseThresholdsList.js'
14+
import { TabSet } from 'lib/ui/TabSet.js'
15+
16+
type TabType = 'details' | 'activity'
1217

1318
interface NoiseSensorDeviceDetailsProps
1419
extends NestedSpecificDeviceDetailsProps {
@@ -22,38 +27,63 @@ export function NoiseSensorDeviceDetails({
2227
onBack,
2328
className,
2429
}: NoiseSensorDeviceDetailsProps): JSX.Element | null {
30+
const [tab, setTab] = useState<TabType>('details')
31+
2532
return (
2633
<div className={classNames('seam-device-details', className)}>
27-
<ContentHeader title={t.noiseSensor} onBack={onBack} />
28-
29-
<div className='seam-body'>
30-
<div className='seam-summary'>
31-
<div className='seam-content'>
32-
<div className='seam-image'>
33-
<DeviceImage device={device} />
34-
</div>
35-
<div className='seam-info'>
36-
<span className='seam-label'>{t.noiseSensor}</span>
37-
<h4 className='seam-device-name'>{device.properties.name}</h4>
38-
<div className='seam-properties'>
39-
<span className='seam-label'>{t.status}:</span>{' '}
40-
<OnlineStatus device={device} />
41-
<NoiseLevelStatus device={device} />
42-
<DeviceModel device={device} />
34+
<div className='seam-body seam-body-no-margin'>
35+
<div className='seam-contained-summary'>
36+
<ContentHeader
37+
title={t.noiseSensor}
38+
onBack={onBack}
39+
className='seam-content-header-contained'
40+
/>
41+
<div className='seam-summary'>
42+
<div className='seam-content'>
43+
<div className='seam-image'>
44+
<DeviceImage device={device} />
45+
</div>
46+
<div className='seam-info'>
47+
<span className='seam-label'>{t.noiseSensor}</span>
48+
<h4 className='seam-device-name'>{device.properties.name}</h4>
49+
<div className='seam-properties'>
50+
<span className='seam-label'>{t.status}:</span>{' '}
51+
<OnlineStatus device={device} />
52+
<NoiseLevelStatus device={device} />
53+
<DeviceModel device={device} />
54+
</div>
4355
</div>
4456
</div>
4557
</div>
58+
59+
<TabSet<TabType>
60+
tabs={['details', 'activity']}
61+
tabTitles={{
62+
details: t.details,
63+
activity: t.activity,
64+
}}
65+
activeTab={tab}
66+
onTabChange={(tab) => {
67+
setTab(tab)
68+
}}
69+
/>
4670
</div>
4771

48-
<NoiseThresholdsList device={device} />
72+
{tab === 'details' && (
73+
<div className='seam-padded-container'>
74+
<NoiseThresholdsList device={device} />
75+
76+
<DeviceInfo
77+
device={device}
78+
disableConnectedAccountInformation={
79+
disableConnectedAccountInformation
80+
}
81+
disableResourceIds={disableResourceIds}
82+
/>
83+
</div>
84+
)}
4985

50-
<DeviceInfo
51-
device={device}
52-
disableConnectedAccountInformation={
53-
disableConnectedAccountInformation
54-
}
55-
disableResourceIds={disableResourceIds}
56-
/>
86+
{tab === 'activity' && <NoiseSensorActivityList device={device} />}
5787
</div>
5888
</div>
5989
)
@@ -63,4 +93,6 @@ const t = {
6393
noiseSensor: 'Noise Sensor',
6494
status: 'Status',
6595
noiseLevel: 'Noise level',
96+
details: 'Details',
97+
activity: 'Activity',
6698
}

src/lib/seam/events/use-events.ts

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { useQuery, useQueryClient } from '@tanstack/react-query'
2+
import type {
3+
Event,
4+
EventsListRequest,
5+
EventsListResponse,
6+
SeamError,
7+
} 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 UseEventsParams = EventsListRequest
13+
export type UseEventsData = Event[]
14+
export interface UseEventsOptions {
15+
refetchInterval?: number
16+
}
17+
18+
export function useEvents(
19+
params?: UseEventsParams,
20+
options?: UseEventsOptions
21+
): UseSeamQueryResult<'events', UseEventsData> {
22+
const { client } = useSeamClient()
23+
const queryClient = useQueryClient()
24+
25+
const { data, ...rest } = useQuery<EventsListResponse['events'], SeamError>({
26+
enabled: client != null,
27+
queryKey: ['events', 'list', params],
28+
queryFn: async () => {
29+
if (client == null) return []
30+
return await client.events.list(params)
31+
},
32+
onSuccess: (events) => {
33+
for (const event of events) {
34+
queryClient.setQueryData(
35+
['events', 'get', { event_id: event.event_id }],
36+
event
37+
)
38+
}
39+
},
40+
refetchInterval: options?.refetchInterval ?? 30_000,
41+
})
42+
43+
return { ...rest, events: data }
44+
}

src/lib/ui/TabSet.tsx

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import classNames from 'classnames'
2+
import {
3+
type MouseEventHandler,
4+
useCallback,
5+
useEffect,
6+
useLayoutEffect,
7+
useState,
8+
} from 'react'
9+
10+
interface TabSetProps<TabType extends string> {
11+
tabs: TabType[]
12+
tabTitles: Record<TabType, string>
13+
activeTab: TabType
14+
onTabChange: (tab: TabType) => void
15+
}
16+
17+
interface HighlightStyle {
18+
left: number
19+
width: number
20+
}
21+
22+
export function TabSet<TabType extends string>({
23+
tabs,
24+
tabTitles,
25+
activeTab,
26+
onTabChange,
27+
}: TabSetProps<TabType>): JSX.Element {
28+
const [highlightStyle, setHighlightStyle] = useState<HighlightStyle>({
29+
left: 0,
30+
width: 140,
31+
})
32+
33+
const calculateHighlightStyle = useCallback(() => {
34+
const tabButton: HTMLButtonElement | null =
35+
globalThis.document?.querySelector(
36+
`.seam-tab-button:nth-of-type(${tabs.indexOf(activeTab) + 1})`
37+
)
38+
39+
setHighlightStyle({
40+
left: tabButton?.offsetLeft ?? 0,
41+
width: tabButton?.offsetWidth ?? 140,
42+
})
43+
}, [activeTab, tabs])
44+
45+
useLayoutEffect(() => {
46+
calculateHighlightStyle()
47+
}, [activeTab, calculateHighlightStyle])
48+
49+
useEffect(() => {
50+
globalThis.addEventListener?.('resize', calculateHighlightStyle)
51+
return () => {
52+
globalThis.removeEventListener?.('resize', calculateHighlightStyle)
53+
}
54+
}, [calculateHighlightStyle])
55+
56+
return (
57+
<div className='seam-tab-set'>
58+
<div className='seam-tab-set-buttons'>
59+
<div className='seam-tab-set-highlight' style={highlightStyle} />
60+
61+
{tabs.map((tab) => (
62+
<TabButton<TabType>
63+
key={tab}
64+
tab={tab}
65+
title={tabTitles[tab]}
66+
isActive={activeTab === tab}
67+
onTabChange={onTabChange}
68+
setHighlightStyle={setHighlightStyle}
69+
/>
70+
))}
71+
</div>
72+
</div>
73+
)
74+
}
75+
76+
interface TabButtonProps<TabType> {
77+
tab: TabType
78+
title: string
79+
isActive: boolean
80+
onTabChange: (tab: TabType) => void
81+
setHighlightStyle: (style: HighlightStyle) => void
82+
}
83+
84+
function TabButton<TabType extends string>({
85+
tab,
86+
title,
87+
isActive,
88+
onTabChange,
89+
setHighlightStyle,
90+
}: TabButtonProps<TabType>): JSX.Element {
91+
const handleClick: MouseEventHandler<HTMLButtonElement> = (ev) => {
92+
onTabChange(tab)
93+
setHighlightStyle({
94+
left: ev.currentTarget.offsetLeft,
95+
width: ev.currentTarget.offsetWidth,
96+
})
97+
}
98+
99+
return (
100+
<button
101+
className={classNames(
102+
'seam-tab-button',
103+
isActive && 'seam-tab-button-active'
104+
)}
105+
onClick={handleClick}
106+
>
107+
<p className='seam-tab-button-label'>{title}</p>
108+
</button>
109+
)
110+
}

src/lib/ui/layout/ContentHeader.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
1+
import classNames from 'classnames'
2+
13
import { ArrowBackIcon } from 'lib/icons/ArrowBack.js'
24

35
interface ContentHeaderProps {
46
onBack: (() => void) | undefined
57
title?: string
68
subheading?: string
9+
className?: string
710
}
811

912
export function ContentHeader(props: ContentHeaderProps): JSX.Element | null {
10-
const { title, onBack, subheading } = props
13+
const { title, onBack, subheading, className } = props
1114
if (title == null && onBack == null) {
1215
return null
1316
}
1417

1518
return (
16-
<div className='seam-content-header'>
19+
<div className={classNames('seam-content-header', className)}>
1720
<BackIcon onClick={onBack} />
1821
<div>
1922
<span className='seam-title'>{title}</span>

0 commit comments

Comments
 (0)