Skip to content

Commit d360059

Browse files
committed
console: Add live data split view tutorial
1 parent 3a1ee1e commit d360059

File tree

12 files changed

+213
-18
lines changed

12 files changed

+213
-18
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ For details about compatibility between different releases, see the **Commitment
1313

1414
- Add recvTime field to the decodeUplink input in payload formatters
1515
- Add the latest battery percentage of the end device in the `ApplicationUplink` message.
16+
- Add live data split view tutorial to the Console.
1617

1718
### Changed
1819

cypress/support/commands.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,14 @@ Cypress.Commands.add('getAccessToken', callback => {
126126
callback(accessToken)
127127
})
128128

129+
Cypress.Commands.add('setAllTutorialSeen', user => {
130+
const tutorialNames = ['TUTORIAL_LIVE_DATA_SPLIT_VIEW']
131+
cy.task(
132+
'execSql',
133+
`UPDATE users SET console_preferences = '{"dashboard_layouts":{},"sort_by":{},"tutorials":{"seen":[${tutorialNames.map(name => `"${name}"`).join(',')}]}}'::jsonb::text::bytea WHERE primary_email_address = '${user.primary_email_address}';`,
134+
)
135+
})
136+
129137
// Helper function to create a new user programmatically.
130138
Cypress.Commands.add('createUser', user => {
131139
const baseUrl = Cypress.config('baseUrl')
@@ -147,6 +155,8 @@ Cypress.Commands.add('createUser', user => {
147155
})
148156
})
149157

158+
// Set all tutorials as seen.
159+
cy.setAllTutorialSeen(user)
150160
// Reset cookies and local storage to avoid csrf and session state inconsistencies within tests.
151161
cy.clearCookies()
152162
cy.clearLocalStorage()
Loading
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright © 2025 The Things Network Foundation, The Things Industries B.V.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import React, { useCallback } from 'react'
16+
import { defineMessages } from 'react-intl'
17+
import PropTypes from 'prop-types'
18+
19+
import splitViewIllustration from '@assets/misc/split-view-illustration.png'
20+
21+
import Button from '@ttn-lw/components/button'
22+
23+
import Message from '@ttn-lw/lib/components/message'
24+
25+
import style from './live-data-tutorial.styl'
26+
27+
const m = defineMessages({
28+
liveDataSplitView: 'Live data split view',
29+
liveDataSplitViewDescription:
30+
'Debug, make changes while keeping an eye on live data from everywhere with split view.',
31+
gotIt: 'Got it',
32+
tryIt: 'Try it',
33+
})
34+
35+
const LiveDataTutorial = props => {
36+
const { setIsOpen, seen, setTutorialSeen } = props
37+
38+
const handleTryIt = useCallback(() => {
39+
setIsOpen(true)
40+
setTutorialSeen()
41+
}, [setIsOpen, setTutorialSeen])
42+
43+
return (
44+
!seen && (
45+
<div className={style.container}>
46+
<Message component="h3" content={m.liveDataSplitView} className={style.title} />
47+
<Message
48+
component="p"
49+
content={m.liveDataSplitViewDescription}
50+
className={style.subtitle}
51+
/>
52+
<img className={style.image} src={splitViewIllustration} alt="live-data-split-view" />
53+
<div className={style.buttonGroup}>
54+
<Button message={m.gotIt} secondary className={style.button} onClick={setTutorialSeen} />
55+
<Button message={m.tryIt} primary onClick={handleTryIt} className={style.button} />
56+
</div>
57+
</div>
58+
)
59+
)
60+
}
61+
62+
LiveDataTutorial.propTypes = {
63+
seen: PropTypes.bool.isRequired,
64+
setIsOpen: PropTypes.func.isRequired,
65+
setTutorialSeen: PropTypes.func.isRequired,
66+
}
67+
LiveDataTutorial.defaultProps = {}
68+
69+
export default LiveDataTutorial
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright © 2025 The Things Network Foundation, The Things Industries B.V.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
.container
16+
background-color: var(--c-bg-neutral-heavy)
17+
color: var(--c-text-neutral-min)
18+
padding: $cs.m $cs.l $cs.l $cs.l
19+
border-radius: $br.xxl
20+
margin: 0 0 $cs.s 0
21+
width: 21rem
22+
box-shadow: var(--shadow-box-modal-normal)
23+
24+
.title
25+
margin: 0
26+
font-size: $fs.m
27+
28+
.subtitle
29+
font-size: $fs.s
30+
margin: $cs.xs 0 $cs.m 0
31+
32+
.image
33+
width: 100%
34+
35+
.buttonGroup
36+
display: flex
37+
justify-content: space-between
38+
gap: $cs.m
39+
margin-top: $cs.m
40+
41+
.button
42+
flex: 1
43+
44+
.closeButton
45+
position: absolute
46+
top: $cs.m
47+
right: $cs.m

pkg/webui/console/containers/event-split-frame/event-split-frame.styl

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,31 @@
5151

5252
.open-button
5353
position: fixed
54-
right: 0
55-
bottom: 0
54+
right: $cs.s
55+
bottom: $cs.s
5656
padding: $cs.xxs
5757
width: fit-content
58+
display: flex
59+
flex-direction: column
60+
align-items: flex-end
61+
62+
.live-data-button
63+
position: relative
64+
display: inline-flex
65+
transition: 80ms background ease-in-out, 80ms color ease-in-out, 80ms border-color ease-in-out, 80ms box-shadow ease-in-out
66+
outline: 0
67+
cursor: pointer
68+
justify-content: center
69+
align-items: center
70+
gap: $cs.xxs
71+
height: $ls.m
72+
text-decoration: none
73+
padding: 0 $cs.l 0 $cs.m
74+
border-radius: $br.xl3
75+
color: var(--c-text-neutral-min)
76+
background-color: var(--c-bg-neutral-heavy)
77+
border: 1px solid transparent
78+
box-shadow: var(--shadow-box-button-bold)
5879

80+
&:hover
81+
background-color: var(--c-bg-neutral-heavy-hover)

pkg/webui/console/containers/event-split-frame/index.js

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,58 @@
1414

1515
import React, { useContext, useCallback, useRef, useEffect } from 'react'
1616
import DOM from 'react-dom'
17-
import { IconLayoutBottombarExpand } from '@tabler/icons-react'
18-
import { defineMessages } from 'react-intl'
17+
import { useDispatch, useSelector } from 'react-redux'
1918

2019
import Button from '@ttn-lw/components/button'
20+
import { IconChevronUp } from '@ttn-lw/components/icon'
21+
22+
import RequireRequest from '@ttn-lw/lib/components/require-request'
23+
24+
import LiveDataTutorial from '@console/components/live-data-tutorial'
2125

2226
import PropTypes from '@ttn-lw/lib/prop-types'
27+
import attachPromise from '@ttn-lw/lib/store/actions/attach-promise'
28+
import sharedMessages from '@ttn-lw/lib/shared-messages'
29+
30+
import { getUser } from '@console/store/actions/users'
31+
import { updateUser } from '@console/store/actions/user'
32+
33+
import { selectUserId } from '@console/store/selectors/logout'
34+
import { selectUserById } from '@console/store/selectors/users'
35+
import { selectConsolePreferences } from '@console/store/selectors/user-preferences'
2336

2437
import EventSplitFrameContext from './context'
2538

2639
import style from './event-split-frame.styl'
2740

28-
const m = defineMessages({
29-
expandEventPanel: 'Expand live data overlay',
30-
})
31-
3241
const EventSplitFrameInner = ({ children }) => {
3342
const { isOpen, height, isActive, setHeight, setIsMounted, setIsOpen } =
3443
useContext(EventSplitFrameContext)
3544
const ref = useRef()
45+
const dispatch = useDispatch()
46+
const userId = useSelector(selectUserId)
47+
const user = useSelector(state => selectUserById(state, userId))
48+
const consolePreferences = useSelector(state => selectConsolePreferences(state))
49+
const tutorialsSeen = consolePreferences.tutorials?.seen || []
50+
const seen = tutorialsSeen.includes('TUTORIAL_LIVE_DATA_SPLIT_VIEW')
3651

3752
useEffect(() => {
3853
setIsMounted(true)
3954
return () => setIsMounted(false)
4055
}, [setIsMounted])
4156

57+
const setTutorialSeen = useCallback(async () => {
58+
const patch = {
59+
console_preferences: {
60+
tutorials: {
61+
seen: [...tutorialsSeen, 'TUTORIAL_LIVE_DATA_SPLIT_VIEW'],
62+
},
63+
},
64+
}
65+
66+
await dispatch(attachPromise(updateUser({ id: user.ids.user_id, patch })))
67+
}, [dispatch, tutorialsSeen, user.ids.user_id])
68+
4269
// Handle the dragging of the handler to resize the frame.
4370
const handleDragStart = useCallback(
4471
e => {
@@ -79,13 +106,12 @@ const EventSplitFrameInner = ({ children }) => {
79106
)}
80107
{isActive && !isOpen && (
81108
<div className={style.openButton}>
109+
<LiveDataTutorial setIsOpen={setIsOpen} setTutorialSeen={setTutorialSeen} seen={seen} />
82110
<Button
83-
icon={IconLayoutBottombarExpand}
84-
tooltip={m.expandEventPanel}
85-
tooltipPlacement="left"
111+
icon={IconChevronUp}
112+
className={style.liveDataButton}
86113
onClick={() => setIsOpen(true)}
87-
secondary
88-
small
114+
message={sharedMessages.liveData}
89115
/>
90116
</div>
91117
)}
@@ -97,7 +123,15 @@ EventSplitFrameInner.propTypes = {
97123
children: PropTypes.node.isRequired,
98124
}
99125

100-
const EventSplitFrame = props =>
101-
DOM.createPortal(<EventSplitFrameInner {...props} />, document.getElementById('split-frame'))
126+
const EventSplitFrame = props => {
127+
const userId = useSelector(selectUserId)
128+
129+
return DOM.createPortal(
130+
<RequireRequest requestAction={getUser(userId, ['console_preferences'])}>
131+
<EventSplitFrameInner {...props} />
132+
</RequireRequest>,
133+
document.getElementById('split-frame'),
134+
)
135+
}
102136

103137
export default EventSplitFrame

pkg/webui/console/store/reducers/user-preferences.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {
2222
} from '@console/store/actions/user-preferences'
2323
import { APPLY_PERSISTED_STATE_SUCCESS, GET_USER_ME_SUCCESS } from '@console/store/actions/user'
2424

25+
import { UPDATE_USER_SUCCESS } from '../actions/users'
26+
2527
const initialState = {
2628
bookmarks: {
2729
bookmarks: [],
@@ -109,6 +111,7 @@ const userPreferences = (state = initialState, { type, payload }) => {
109111
},
110112
}
111113
case GET_USER_ME_SUCCESS:
114+
case UPDATE_USER_SUCCESS:
112115
return {
113116
...state,
114117
consolePreferences: {

pkg/webui/console/views/gateway-api-key-edit/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const GatewayApiKeyEditInner = () => {
4141
)
4242

4343
return (
44-
<div className="container container--xl grid">
44+
<div className="container container--xl grid mb-ls-xs">
4545
<PageTitle title={sharedMessages.keyEdit} />
4646
<div className="item-12">
4747
<ApiKeyEditForm entity={GATEWAY} entityId={gtwId} />

pkg/webui/locales/en.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@
163163
"console.components.gateway-api-keys-modal.index.downloadLns": "Download LNS key",
164164
"console.components.gateway-api-keys-modal.index.downloadCups": "Download CUPS key",
165165
"console.components.gateway-visibility-form.index.saveDefaultGatewayVisibility": "Save default gateway visibility",
166+
"console.components.live-data-tutorial.index.liveDataSplitView": "Live data split view",
167+
"console.components.live-data-tutorial.index.liveDataSplitViewDescription": "Debug, make changes while keeping an eye on live data from everywhere with split view.",
168+
"console.components.live-data-tutorial.index.gotIt": "Got it",
169+
"console.components.live-data-tutorial.index.tryIt": "Try it",
166170
"console.components.location-form.index.deleteAllLocations": "Delete all location data",
167171
"console.components.location-form.index.deleteFailure": "An error occurred and the location could not be deleted",
168172
"console.components.location-form.index.deleteLocation": "Remove location data",
@@ -539,7 +543,6 @@
539543
"console.containers.email-notifications-form.index.unsubscribeDescription": "You will continue to receive notifications in the console.",
540544
"console.containers.email-notifications-form.index.discardChanges": "Discard changes",
541545
"console.containers.email-notifications-form.index.updateEmailPreferences": "Updated email preferences",
542-
"console.containers.event-split-frame.index.expandEventPanel": "Expand live data overlay",
543546
"console.containers.freq-plans-select.utils.warning": "Frequency plans unavailable",
544547
"console.containers.freq-plans-select.utils.none": "Do not set a frequency plan",
545548
"console.containers.freq-plans-select.utils.selectFrequencyPlan": "Select a frequency plan...",

pkg/webui/locales/ja.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@
163163
"console.components.gateway-api-keys-modal.index.downloadLns": "LNSキーをダウンロード",
164164
"console.components.gateway-api-keys-modal.index.downloadCups": "CUPSキーをダウンロード",
165165
"console.components.gateway-visibility-form.index.saveDefaultGatewayVisibility": "デフォルトゲートウェイの可視性を保存",
166+
"console.components.live-data-tutorial.index.liveDataSplitView": "",
167+
"console.components.live-data-tutorial.index.liveDataSplitViewDescription": "",
168+
"console.components.live-data-tutorial.index.gotIt": "",
169+
"console.components.live-data-tutorial.index.tryIt": "",
166170
"console.components.location-form.index.deleteAllLocations": "",
167171
"console.components.location-form.index.deleteFailure": "",
168172
"console.components.location-form.index.deleteLocation": "",
@@ -539,7 +543,6 @@
539543
"console.containers.email-notifications-form.index.unsubscribeDescription": "",
540544
"console.containers.email-notifications-form.index.discardChanges": "",
541545
"console.containers.email-notifications-form.index.updateEmailPreferences": "",
542-
"console.containers.event-split-frame.index.expandEventPanel": "",
543546
"console.containers.freq-plans-select.utils.warning": "",
544547
"console.containers.freq-plans-select.utils.none": "",
545548
"console.containers.freq-plans-select.utils.selectFrequencyPlan": "",

pkg/webui/styles/variables/tokens.styl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ $tokens = {
4040
'bg-neutral-semibold': $c.neutral-700,
4141
'bg-neutral-bold': $c.neutral-800,
4242
'bg-neutral-heavy': $c.neutral-900, // Background for navigation element items when active.
43+
'bg-neutral-heavy-hover': $c.neutral-1050, // Background for navigation element items when active on hover.
4344

4445
// Brand
4546

@@ -181,6 +182,7 @@ $tokens = {
181182
'box-warning-normal': 0 0 3px 2px rgba(219, 118, 0,.2), // Shadow for focused inputs and other elements that have errors.
182183
'box-panel-normal': 0px 1px 5px 0px rgba(0, 0, 0, .09),
183184
'box-button-normal': 0 1px 2px 0 rgba(0, 0, 0, .05),
185+
'box-button-bold': 0px 5px 10px 0px rgba(0, 0, 0, .2),
184186
'box-modal-normal': 0px 4px 35px 0px rgba(0, 0, 0, .25),
185187
},
186188

0 commit comments

Comments
 (0)