Skip to content

Commit

Permalink
console: Add live data split view tutorial
Browse files Browse the repository at this point in the history
  • Loading branch information
mjamescompton committed Jan 22, 2025
1 parent c21aaf5 commit c83a306
Show file tree
Hide file tree
Showing 12 changed files with 213 additions and 18 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ For details about compatibility between different releases, see the **Commitment

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

### Changed

Expand Down
10 changes: 10 additions & 0 deletions cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,14 @@ Cypress.Commands.add('getAccessToken', callback => {
callback(accessToken)
})

Cypress.Commands.add('setAllTutorialSeen', user => {
const tutorialNames = ['TUTORIAL_LIVE_DATA_SPLIT_VIEW']
cy.task(
'execSql',
`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}';`,
)
})

// Helper function to create a new user programmatically.
Cypress.Commands.add('createUser', user => {
const baseUrl = Cypress.config('baseUrl')
Expand All @@ -147,6 +155,8 @@ Cypress.Commands.add('createUser', user => {
})
})

// Set all tutorials as seen.
cy.setAllTutorialSeen(user)
// Reset cookies and local storage to avoid csrf and session state inconsistencies within tests.
cy.clearCookies()
cy.clearLocalStorage()
Expand Down
Binary file added pkg/webui/assets/misc/split-view-illustration.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
69 changes: 69 additions & 0 deletions pkg/webui/console/components/live-data-tutorial/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright © 2025 The Things Network Foundation, The Things Industries B.V.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import React, { useCallback } from 'react'
import { defineMessages } from 'react-intl'
import PropTypes from 'prop-types'

import splitViewIllustration from '@assets/misc/split-view-illustration.png'

import Button from '@ttn-lw/components/button'

import Message from '@ttn-lw/lib/components/message'

import style from './live-data-tutorial.styl'

const m = defineMessages({
liveDataSplitView: 'Live data split view',
liveDataSplitViewDescription:
'Debug, make changes while keeping an eye on live data from everywhere with split view.',
gotIt: 'Got it',
tryIt: 'Try it',
})

const LiveDataTutorial = props => {
const { setIsOpen, seen, setTutorialSeen } = props

const handleTryIt = useCallback(() => {
setIsOpen(true)
setTutorialSeen()
}, [setIsOpen, setTutorialSeen])

return (
!seen && (
<div className={style.container}>
<Message component="h3" content={m.liveDataSplitView} className={style.title} />
<Message
component="p"
content={m.liveDataSplitViewDescription}
className={style.subtitle}
/>
<img className={style.image} src={splitViewIllustration} alt="live-data-split-view" />
<div className={style.buttonGroup}>
<Button message={m.gotIt} secondary className={style.button} onClick={setTutorialSeen} />
<Button message={m.tryIt} primary onClick={handleTryIt} className={style.button} />
</div>
</div>
)
)
}

LiveDataTutorial.propTypes = {
seen: PropTypes.bool.isRequired,
setIsOpen: PropTypes.func.isRequired,
setTutorialSeen: PropTypes.func.isRequired,
}
LiveDataTutorial.defaultProps = {}

export default LiveDataTutorial
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright © 2025 The Things Network Foundation, The Things Industries B.V.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

.container
background-color: var(--c-bg-neutral-heavy)
color: var(--c-text-neutral-min)
padding: $cs.m $cs.l $cs.l $cs.l
border-radius: $br.xxl
margin: 0 0 $cs.s 0
width: 21rem
box-shadow: var(--shadow-box-modal-normal)

.title
margin: 0
font-size: $fs.m

.subtitle
font-size: $fs.s
margin: $cs.xs 0 $cs.m 0

.image
width: 100%

.buttonGroup
display: flex
justify-content: space-between
gap: $cs.m
margin-top: $cs.m

.button
flex: 1

.closeButton
position: absolute
top: $cs.m
right: $cs.m
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,31 @@

.open-button
position: fixed
right: 0
bottom: 0
right: $cs.s
bottom: $cs.s
padding: $cs.xxs
width: fit-content
display: flex
flex-direction: column
align-items: flex-end

.live-data-button
position: relative
display: inline-flex
transition: 80ms background ease-in-out, 80ms color ease-in-out, 80ms border-color ease-in-out, 80ms box-shadow ease-in-out
outline: 0
cursor: pointer
justify-content: center
align-items: center
gap: $cs.xxs
height: $ls.m
text-decoration: none
padding: 0 $cs.l 0 $cs.m
border-radius: $br.xl3
color: var(--c-text-neutral-min)
background-color: var(--c-bg-neutral-heavy)
border: 1px solid transparent
box-shadow: var(--shadow-box-button-bold)

&:hover
background-color: var(--c-bg-neutral-heavy-hover)
60 changes: 47 additions & 13 deletions pkg/webui/console/containers/event-split-frame/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,58 @@

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

import Button from '@ttn-lw/components/button'
import { IconChevronUp } from '@ttn-lw/components/icon'

import RequireRequest from '@ttn-lw/lib/components/require-request'

import LiveDataTutorial from '@console/components/live-data-tutorial'

import PropTypes from '@ttn-lw/lib/prop-types'
import attachPromise from '@ttn-lw/lib/store/actions/attach-promise'
import sharedMessages from '@ttn-lw/lib/shared-messages'

import { getUser } from '@console/store/actions/users'
import { updateUser } from '@console/store/actions/user'

import { selectUserId } from '@console/store/selectors/logout'
import { selectUserById } from '@console/store/selectors/users'
import { selectConsolePreferences } from '@console/store/selectors/user-preferences'

import EventSplitFrameContext from './context'

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

const m = defineMessages({
expandEventPanel: 'Expand live data overlay',
})

const EventSplitFrameInner = ({ children }) => {
const { isOpen, height, isActive, setHeight, setIsMounted, setIsOpen } =
useContext(EventSplitFrameContext)
const ref = useRef()
const dispatch = useDispatch()
const userId = useSelector(selectUserId)
const user = useSelector(state => selectUserById(state, userId))
const consolePreferences = useSelector(state => selectConsolePreferences(state))
const tutorialsSeen = consolePreferences.tutorials?.seen || []
const seen = tutorialsSeen.includes('TUTORIAL_LIVE_DATA_SPLIT_VIEW')

useEffect(() => {
setIsMounted(true)
return () => setIsMounted(false)
}, [setIsMounted])

const setTutorialSeen = useCallback(async () => {
const patch = {
console_preferences: {
tutorials: {
seen: [...tutorialsSeen, 'TUTORIAL_LIVE_DATA_SPLIT_VIEW'],
},
},
}

await dispatch(attachPromise(updateUser({ id: user.ids.user_id, patch })))
}, [dispatch, tutorialsSeen, user.ids.user_id])

// Handle the dragging of the handler to resize the frame.
const handleDragStart = useCallback(
e => {
Expand Down Expand Up @@ -79,13 +106,12 @@ const EventSplitFrameInner = ({ children }) => {
)}
{isActive && !isOpen && (
<div className={style.openButton}>
<LiveDataTutorial setIsOpen={setIsOpen} setTutorialSeen={setTutorialSeen} seen={seen} />
<Button
icon={IconLayoutBottombarExpand}
tooltip={m.expandEventPanel}
tooltipPlacement="left"
icon={IconChevronUp}
className={style.liveDataButton}
onClick={() => setIsOpen(true)}
secondary
small
message={sharedMessages.liveData}
/>
</div>
)}
Expand All @@ -97,7 +123,15 @@ EventSplitFrameInner.propTypes = {
children: PropTypes.node.isRequired,
}

const EventSplitFrame = props =>
DOM.createPortal(<EventSplitFrameInner {...props} />, document.getElementById('split-frame'))
const EventSplitFrame = props => {
const userId = useSelector(selectUserId)

return DOM.createPortal(
<RequireRequest requestAction={getUser(userId, ['console_preferences'])}>
<EventSplitFrameInner {...props} />
</RequireRequest>,
document.getElementById('split-frame'),
)
}

export default EventSplitFrame
3 changes: 3 additions & 0 deletions pkg/webui/console/store/reducers/user-preferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
} from '@console/store/actions/user-preferences'
import { APPLY_PERSISTED_STATE_SUCCESS, GET_USER_ME_SUCCESS } from '@console/store/actions/user'

import { UPDATE_USER_SUCCESS } from '../actions/users'

const initialState = {
bookmarks: {
bookmarks: [],
Expand Down Expand Up @@ -109,6 +111,7 @@ const userPreferences = (state = initialState, { type, payload }) => {
},
}
case GET_USER_ME_SUCCESS:
case UPDATE_USER_SUCCESS:
return {
...state,
consolePreferences: {
Expand Down
2 changes: 1 addition & 1 deletion pkg/webui/console/views/gateway-api-key-edit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const GatewayApiKeyEditInner = () => {
)

return (
<div className="container container--xl grid">
<div className="container container--xl grid mb-ls-xs">
<PageTitle title={sharedMessages.keyEdit} />
<div className="item-12">
<ApiKeyEditForm entity={GATEWAY} entityId={gtwId} />
Expand Down
5 changes: 4 additions & 1 deletion pkg/webui/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@
"console.components.gateway-api-keys-modal.index.downloadLns": "Download LNS key",
"console.components.gateway-api-keys-modal.index.downloadCups": "Download CUPS key",
"console.components.gateway-visibility-form.index.saveDefaultGatewayVisibility": "Save default gateway visibility",
"console.components.live-data-tutorial.index.liveDataSplitView": "Live data split view",
"console.components.live-data-tutorial.index.liveDataSplitViewDescription": "Debug, make changes while keeping an eye on live data from everywhere with split view.",
"console.components.live-data-tutorial.index.gotIt": "Got it",
"console.components.live-data-tutorial.index.tryIt": "Try it",
"console.components.location-form.index.deleteAllLocations": "Delete all location data",
"console.components.location-form.index.deleteFailure": "An error occurred and the location could not be deleted",
"console.components.location-form.index.deleteLocation": "Remove location data",
Expand Down Expand Up @@ -539,7 +543,6 @@
"console.containers.email-notifications-form.index.unsubscribeDescription": "You will continue to receive notifications in the console.",
"console.containers.email-notifications-form.index.discardChanges": "Discard changes",
"console.containers.email-notifications-form.index.updateEmailPreferences": "Updated email preferences",
"console.containers.event-split-frame.index.expandEventPanel": "Expand live data overlay",
"console.containers.freq-plans-select.utils.warning": "Frequency plans unavailable",
"console.containers.freq-plans-select.utils.none": "Do not set a frequency plan",
"console.containers.freq-plans-select.utils.selectFrequencyPlan": "Select a frequency plan...",
Expand Down
5 changes: 4 additions & 1 deletion pkg/webui/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@
"console.components.gateway-api-keys-modal.index.downloadLns": "LNSキーをダウンロード",
"console.components.gateway-api-keys-modal.index.downloadCups": "CUPSキーをダウンロード",
"console.components.gateway-visibility-form.index.saveDefaultGatewayVisibility": "デフォルトゲートウェイの可視性を保存",
"console.components.live-data-tutorial.index.liveDataSplitView": "",
"console.components.live-data-tutorial.index.liveDataSplitViewDescription": "",
"console.components.live-data-tutorial.index.gotIt": "",
"console.components.live-data-tutorial.index.tryIt": "",
"console.components.location-form.index.deleteAllLocations": "",
"console.components.location-form.index.deleteFailure": "",
"console.components.location-form.index.deleteLocation": "",
Expand Down Expand Up @@ -539,7 +543,6 @@
"console.containers.email-notifications-form.index.unsubscribeDescription": "",
"console.containers.email-notifications-form.index.discardChanges": "",
"console.containers.email-notifications-form.index.updateEmailPreferences": "",
"console.containers.event-split-frame.index.expandEventPanel": "",
"console.containers.freq-plans-select.utils.warning": "",
"console.containers.freq-plans-select.utils.none": "",
"console.containers.freq-plans-select.utils.selectFrequencyPlan": "",
Expand Down
2 changes: 2 additions & 0 deletions pkg/webui/styles/variables/tokens.styl
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ $tokens = {
'bg-neutral-semibold': $c.neutral-700,
'bg-neutral-bold': $c.neutral-800,
'bg-neutral-heavy': $c.neutral-900, // Background for navigation element items when active.
'bg-neutral-heavy-hover': $c.neutral-1050, // Background for navigation element items when active on hover.

// Brand

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

Expand Down

0 comments on commit c83a306

Please sign in to comment.