Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
console,account: Convert class components to functional components
Browse files Browse the repository at this point in the history
kschiffer committed Sep 28, 2023

Verified

This commit was signed with the committer’s verified signature.
1 parent 1daa35a commit 8b6e3ed
Showing 4 changed files with 471 additions and 565 deletions.
344 changes: 152 additions & 192 deletions pkg/webui/components/navigation/side/index.js
Original file line number Diff line number Diff line change
@@ -13,10 +13,9 @@
// limitations under the License.

import ReactDom from 'react-dom'
import React, { Component } from 'react'
import bind from 'autobind-decorator'
import React, { useState, useEffect, useCallback, useRef } from 'react'
import classnames from 'classnames'
import { defineMessages, injectIntl } from 'react-intl'
import { defineMessages, useIntl } from 'react-intl'

import LAYOUT from '@ttn-lw/constants/layout'

@@ -41,225 +40,186 @@ const m = defineMessages({
hideSidebar: 'Hide sidebar',
})

@injectIntl
export class SideNavigation extends Component {
static propTypes = {
appContainerId: PropTypes.string,
children: PropTypes.node.isRequired,
className: PropTypes.string,
/** The header for the side navigation. */
header: PropTypes.shape({
title: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
iconAlt: PropTypes.message.isRequired,
to: PropTypes.string.isRequired,
}).isRequired,
intl: PropTypes.shape({
formatMessage: PropTypes.func,
}).isRequired,
modifyAppContainerClasses: PropTypes.bool,
}

static defaultProps = {
appContainerId: 'app',
modifyAppContainerClasses: true,
className: undefined,
}

state = {
/** A flag specifying whether the side navigation is minimized or not. */
isMinimized: getViewportWidth() <= LAYOUT.BREAKPOINTS.M,
/** A flag specifying whether the drawer is currently open (in mobile
* screensizes).
*/
isDrawerOpen: false,
/** A flag indicating whether the user has last toggled the sidebar to
* minimized state. */
preferMinimized: false,
}

@bind
updateAppContainerClasses(initial = false) {
const { modifyAppContainerClasses, appContainerId } = this.props
if (!modifyAppContainerClasses) {
return
}
const { isMinimized } = this.state
const containerClasses = document.getElementById(appContainerId).classList
containerClasses.add('with-sidebar')
if (!initial) {
// The transitioned class is necessary to prevent unwanted width
// transitions during route changes.
containerClasses.add('sidebar-transitioned')
}
if (isMinimized) {
containerClasses.add('sidebar-minimized')
} else {
containerClasses.remove('sidebar-minimized')
}
}

@bind
removeAppContainerClasses() {
const { modifyAppContainerClasses, appContainerId } = this.props
const SideNavigation = ({
appContainerId,
modifyAppContainerClasses,
className,
header,
children,
}) => {
const [isMinimized, setIsMinimized] = useState(getViewportWidth() <= LAYOUT.BREAKPOINTS.M)
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
const [preferMinimized, setPreferMinimized] = useState(false)
const node = useRef()
const intl = useIntl()

const updateAppContainerClasses = useCallback(
(initial = false) => {
if (!modifyAppContainerClasses) {
return
}
const containerClasses = document.getElementById(appContainerId).classList
containerClasses.add('with-sidebar')
if (!initial) {
containerClasses.add('sidebar-transitioned')
}
if (isMinimized) {
containerClasses.add('sidebar-minimized')
} else {
containerClasses.remove('sidebar-minimized')
}
},
[modifyAppContainerClasses, appContainerId, isMinimized],
)

const removeAppContainerClasses = useCallback(() => {
if (!modifyAppContainerClasses) {
return
}
document
.getElementById(appContainerId)
.classList.remove('with-sidebar', 'sidebar-minimized', 'sidebar-transitioned')
}
}, [modifyAppContainerClasses, appContainerId])

componentDidMount() {
window.addEventListener('resize', this.setMinimizedState)
this.updateAppContainerClasses(true)
}
const closeDrawer = useCallback(() => {
setIsDrawerOpen(false)
document.body.classList.remove(style.scrollLock)
}, [])

componentWillUnmount() {
window.removeEventListener('resize', this.setMinimizedState)
this.removeAppContainerClasses()
}
const openDrawer = useCallback(() => {
setIsDrawerOpen(true)
document.body.classList.add(style.scrollLock)
}, [])

@bind
setMinimizedState() {
const { isMinimized, preferMinimized } = this.state
useEffect(() => {
const onClickOutside = e => {
if (isDrawerOpen && node.current && !node.current.contains(e.target)) {
closeDrawer()
}
}

if (isDrawerOpen) {
document.addEventListener('mousedown', onClickOutside)
return () => document.removeEventListener('mousedown', onClickOutside)
}
}, [isDrawerOpen, closeDrawer])

const setMinimizedState = useCallback(() => {
const viewportWidth = getViewportWidth()
if (
(!isMinimized && viewportWidth <= LAYOUT.BREAKPOINTS.M) ||
(isMinimized && viewportWidth > LAYOUT.BREAKPOINTS.M)
) {
this.setState({ isMinimized: getViewportWidth() <= LAYOUT.BREAKPOINTS.M || preferMinimized })
this.updateAppContainerClasses()
setIsMinimized(getViewportWidth() <= LAYOUT.BREAKPOINTS.M || preferMinimized)
updateAppContainerClasses()
}
}

@bind
async onToggle() {
await this.setState(prev => ({
isMinimized: !prev.isMinimized,
preferMinimized: !prev.isMinimized,
}))
this.updateAppContainerClasses()
}
}, [isMinimized, preferMinimized, updateAppContainerClasses])

useEffect(() => {
window.addEventListener('resize', setMinimizedState)
updateAppContainerClasses(true)
return () => {
window.removeEventListener('resize', setMinimizedState)
removeAppContainerClasses()
}
}, [removeAppContainerClasses, setMinimizedState, updateAppContainerClasses])

@bind
onDrawerExpandClick() {
const { isDrawerOpen } = this.state
const onToggle = useCallback(async () => {
setIsMinimized(prev => !prev)
setPreferMinimized(prev => !prev)
updateAppContainerClasses()
}, [updateAppContainerClasses])

const onDrawerExpandClick = useCallback(() => {
if (!isDrawerOpen) {
this.openDrawer()
openDrawer()
} else {
this.closeDrawer()
closeDrawer()
}
}
}, [isDrawerOpen, openDrawer, closeDrawer])

@bind
onClickOutside(e) {
const { isDrawerOpen } = this.state
if (isDrawerOpen && this.node && !this.node.contains(e.target)) {
this.closeDrawer()
}
}

@bind
closeDrawer() {
this.setState({ isDrawerOpen: false })

// Enable body scrolling.
document.body.classList.remove(style.scrollLock)
document.removeEventListener('mousedown', this.onClickOutside)
}

@bind
openDrawer() {
// Disable body scrolling.
document.body.classList.add(style.scrollLock)

document.addEventListener('mousedown', this.onClickOutside)
this.setState({ isDrawerOpen: true })
}

@bind
onLeafItemClick() {
const { isDrawerOpen } = this.state
const onLeafItemClick = useCallback(() => {
if (isDrawerOpen) {
this.onDrawerExpandClick()
onDrawerExpandClick()
}
}

@bind
ref(node) {
this.node = node
}

render() {
const { className, header, children, intl } = this.props
const { isMinimized, isDrawerOpen } = this.state

const navigationClassNames = classnames(className, style.navigation, {
[style.navigationMinimized]: isMinimized,
})
const minimizeButtonClassNames = classnames(style.minimizeButton, {
[style.minimizeButtonMinimized]: isMinimized,
})

const drawerClassNames = classnames(style.drawer, { [style.drawerOpen]: isDrawerOpen })

return (
<>
<nav className={navigationClassNames} ref={this.ref} data-test-id="navigation-sidebar">
<div className={style.mobileHeader} onClick={this.onDrawerExpandClick}>
<Icon className={style.expandIcon} icon="more_vert" />
<img
className={style.icon}
src={header.icon}
alt={intl.formatMessage(header.iconAlt)}
/>
<Message className={style.message} content={header.title} />
</div>
<div>
<div className={drawerClassNames}>
<Link to={header.to}>
<div className={style.header}>
<img
className={style.icon}
src={header.icon}
alt={intl.formatMessage(header.iconAlt)}
/>
<Message className={style.message} content={header.title} />
</div>
</Link>
<SideNavigationContext.Provider
value={{ isMinimized, onLeafItemClick: this.onLeafItemClick }}
}, [isDrawerOpen, onDrawerExpandClick])

const navigationClassNames = classnames(className, style.navigation, {
[style.navigationMinimized]: isMinimized,
})
const minimizeButtonClassNames = classnames(style.minimizeButton, {
[style.minimizeButtonMinimized]: isMinimized,
})

const drawerClassNames = classnames(style.drawer, { [style.drawerOpen]: isDrawerOpen })

return (
<>
<nav className={navigationClassNames} ref={node} data-test-id="navigation-sidebar">
<div className={style.mobileHeader} onClick={onDrawerExpandClick}>
<Icon className={style.expandIcon} icon="more_vert" />
<img className={style.icon} src={header.icon} alt={intl.formatMessage(header.iconAlt)} />
<Message className={style.message} content={header.title} />
</div>
<div>
<div className={drawerClassNames}>
<Link to={header.to}>
<div className={style.header}>
<img
className={style.icon}
src={header.icon}
alt={intl.formatMessage(header.iconAlt)}
/>
<Message className={style.message} content={header.title} />
</div>
</Link>
<SideNavigationContext.Provider value={{ isMinimized, onLeafItemClick }}>
<SideNavigationList
onListClick={onDrawerExpandClick}
isMinimized={isMinimized}
className={style.navigationList}
>
<SideNavigationList
onListClick={this.onDrawerExpandClick}
isMinimized={isMinimized}
className={style.navigationList}
>
{children}
</SideNavigationList>
</SideNavigationContext.Provider>
</div>
{children}
</SideNavigationList>
</SideNavigationContext.Provider>
</div>
</nav>
<Button
unstyled
className={minimizeButtonClassNames}
icon={isMinimized ? 'keyboard_arrow_right' : 'keyboard_arrow_left'}
message={isMinimized ? null : m.hideSidebar}
onClick={this.onToggle}
data-hook="side-nav-hide-button"
/>
</>
)
}
</div>
</nav>
<Button
unstyled
className={minimizeButtonClassNames}
icon={isMinimized ? 'keyboard_arrow_right' : 'keyboard_arrow_left'}
message={isMinimized ? null : m.hideSidebar}
onClick={onToggle}
data-hook="side-nav-hide-button"
/>
</>
)
}

SideNavigation.propTypes = {
appContainerId: PropTypes.string,
children: PropTypes.node.isRequired,
className: PropTypes.string,
/** The header for the side navigation. */
header: PropTypes.shape({
title: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
iconAlt: PropTypes.message.isRequired,
to: PropTypes.string.isRequired,
}).isRequired,
modifyAppContainerClasses: PropTypes.bool,
}

SideNavigation.defaultProps = {
appContainerId: 'app',
modifyAppContainerClasses: true,
className: undefined,
}

const PortalledSideNavigation = props =>
ReactDom.createPortal(<SideNavigation {...props} />, document.getElementById('sidebar'))

PortalledSideNavigation.Item = SideNavigationItem

export default PortalledSideNavigation
export { PortalledSideNavigation as default, SideNavigation }
536 changes: 253 additions & 283 deletions pkg/webui/components/safe-inspector/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2019 The Things Network Foundation, The Things Industries B.V.
// Copyright © 2023 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.
@@ -12,11 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import React, { Component } from 'react'
import bind from 'autobind-decorator'
import React, { useCallback, useState, useEffect, useRef } from 'react'
import classnames from 'classnames'
import clipboard from 'clipboard'
import { defineMessages, injectIntl } from 'react-intl'
import { defineMessages, useIntl } from 'react-intl'

import Icon from '@ttn-lw/components/icon'

@@ -61,321 +60,292 @@ const representationRotateMap = {
[UINT32_T]: MSB,
}

@injectIntl
export class SafeInspector extends Component {
static propTypes = {
/** The classname to be applied. */
className: PropTypes.string,
/** The data to be displayed. */
data: PropTypes.string.isRequired,
/** Whether the component should resize when its data is truncated. */
disableResize: PropTypes.bool,
/** Whether uint32_t notation should be enabled for byte representation. */
enableUint32: PropTypes.bool,
/** Whether the data can be hidden (like passwords). */
hideable: PropTypes.bool,
/** Whether the data is initially visible. */
initiallyVisible: PropTypes.bool,
/** Utility functions passed via react-intl HOC. */
intl: PropTypes.shape({
formatMessage: PropTypes.func,
}).isRequired,
/** Whether the data is in byte format. */
isBytes: PropTypes.bool,
/** Whether to hide the copy action. */
noCopy: PropTypes.bool,
/** Whether to hide the copy popup click and just display checkmark. */
noCopyPopup: PropTypes.bool,
/** Whether to hide the data transform action. */
noTransform: PropTypes.bool,
/**
* Whether a smaller style should be rendered (useful for display in
* tables).
*/
small: PropTypes.bool,
/** The input count (byte or characters, based on type) after which the
* display is truncated.
*/
truncateAfter: PropTypes.number,
}

static defaultProps = {
className: undefined,
noCopyPopup: false,
disableResize: false,
hideable: true,
initiallyVisible: false,
isBytes: true,
small: false,
noTransform: false,
noCopy: false,
enableUint32: false,
truncateAfter: Infinity,
}

_getNextRepresentation(current) {
const { enableUint32 } = this.props
const next = representationRotateMap[current]

return next === UINT32_T && !enableUint32 ? representationRotateMap[next] : next
}

constructor(props) {
super(props)

this._timer = null
const SafeInspector = ({
data,
hideable,
initiallyVisible,
enableUint32,
className,
isBytes,
small,
noCopyPopup,
noCopy,
noTransform,
truncateAfter,
disableResize,
}) => {
const _timer = useRef(null)

const [hidden, setHidden] = useState((hideable && !initiallyVisible) || false)
const [byteStyle, setByteStyle] = useState(true)
const [copied, setCopied] = useState(false)
const [copyIcon, setCopyIcon] = useState('file_copy')
const [representation, setRepresentation] = useState(MSB)
const [truncated, setTruncated] = useState(false)

const intl = useIntl()

const containerElem = useRef(null)
const displayElem = useRef(null)
const buttonsElem = useRef(null)
const copyElem = useRef(null)

const getNextRepresentation = useCallback(
current => {
const next = representationRotateMap[current]

return next === UINT32_T && !enableUint32 ? representationRotateMap[next] : next
},
[enableUint32],
)

this.state = {
hidden: (props.hideable && !props.initiallyVisible) || false,
byteStyle: true,
copied: false,
copyIcon: 'file_copy',
representation: MSB,
truncated: false,
const checkTruncateState = useCallback(() => {
if (!containerElem.current) {
return
}

this.containerElem = React.createRef()
this.displayElem = React.createRef()
this.buttonsElem = React.createRef()
this.copyElem = React.createRef()
}

@bind
handleVisibiltyToggle() {
this.setState(prev => ({
byteStyle: !prev.hidden ? true : prev.byteStyle,
hidden: !prev.hidden,
}))
this.checkTruncateState()
}

@bind
async handleTransformToggle() {
await this.setState(prev => ({ byteStyle: !prev.byteStyle }))
this.checkTruncateState()
}

@bind
handleSwapToggle() {
this.setState(({ representation }) => ({
representation: this._getNextRepresentation(representation),
}))
}
const containerWidth = containerElem.current.offsetWidth
const buttonsWidth = buttonsElem.current.offsetWidth
const displayWidth = displayElem.current.offsetWidth
const netContainerWidth = containerWidth - buttonsWidth - 14

@bind
handleDataClick() {
if (!this.state.hidden) {
selectText(this.displayElem.current)
if (netContainerWidth < displayWidth && !truncated) {
setTruncated(true)
} else if (netContainerWidth > displayWidth && truncated) {
setTruncated(false)
}
}

@bind
handleCopyClick() {
const { noCopyPopup } = this.props
const { copied } = this.state
}, [truncated])

const handleVisibiltyToggle = useCallback(() => {
setHidden(prev => !prev)
setByteStyle(prev => (!prev && !hidden ? true : prev))
checkTruncateState()
}, [checkTruncateState, hidden])

const handleTransformToggle = useCallback(async () => {
setByteStyle(prev => !prev)
checkTruncateState()
}, [checkTruncateState])

const handleSwapToggle = useCallback(() => {
setRepresentation(prev => getNextRepresentation(prev))
}, [getNextRepresentation])

const handleDataClick = useCallback(() => {
if (!hidden) {
selectText(displayElem.current)
}
}, [hidden])

const handleCopyClick = useCallback(() => {
if (copied) {
return
}

this.setState({ copied: true, copyIcon: 'done' })
setCopied(true)
setCopyIcon('done')
if (noCopyPopup) {
this._timer = setTimeout(() => {
this.setState({ copied: false, copyIcon: 'file_copy' })
_timer.current = setTimeout(() => {
setCopied(false)
setCopyIcon('file_copy')
}, 2000)
}
}
}, [copied, noCopyPopup])

@bind
handleCopyAnimationEnd() {
this.setState({ copied: false, copyIcon: 'file_copy' })
}
const handleCopyAnimationEnd = useCallback(() => {
setCopied(false)
setCopyIcon('file_copy')
}, [])

componentDidMount() {
const { disableResize } = this.props

if (this.copyElem && this.copyElem.current) {
new clipboard(this.copyElem.current, { container: this.containerElem.current })
useEffect(() => {
if (copyElem && copyElem.current) {
new clipboard(copyElem.current, { container: containerElem.current })
}

if (!disableResize) {
window.addEventListener('resize', this.handleWindowResize)
this.checkTruncateState()
}
}

componentWillUnmount() {
const { disableResize } = this.props
if (!disableResize) {
window.removeEventListener('resize', this.handleWindowResize)
}
clearTimeout(this._timer)
}
const handleWindowResize = () => {
// Your resize logic here
checkTruncateState()
}

@bind
handleWindowResize() {
this.checkTruncateState()
}
window.addEventListener('resize', handleWindowResize)
checkTruncateState()

checkTruncateState() {
if (!this.containerElem.current) {
return
return () => {
window.removeEventListener('resize', handleWindowResize)
}
}

const containerWidth = this.containerElem.current.offsetWidth
const buttonsWidth = this.buttonsElem.current.offsetWidth
const displayWidth = this.displayElem.current.offsetWidth
const netContainerWidth = containerWidth - buttonsWidth - 14
if (netContainerWidth < displayWidth && !this.state.truncated) {
this.setState({ truncated: true })
} else if (netContainerWidth > displayWidth && this.state.truncated) {
this.setState({ truncated: false })
return () => {
clearTimeout(_timer)
}
}
}, [_timer, checkTruncateState, disableResize])

handleContainerClick(e) {
// Prevent from opening links that the component might be wrapped in.
const handleContainerClick = useCallback(e => {
e.preventDefault()
e.stopPropagation()
}
}, [])

render() {
const { hidden, byteStyle, representation, copied, copyIcon } = this.state

const {
className,
data,
isBytes,
hideable,
small,
intl,
noCopyPopup,
noCopy,
noTransform,
truncateAfter,
} = this.props

let formattedData = isBytes ? data.toUpperCase() : data
let display = formattedData
let truncated = false

if (isBytes) {
let chunks = chunkArray(data.toUpperCase().split(''), 2)
if (chunks.length > truncateAfter) {
truncated = true
chunks = chunks.slice(0, truncateAfter)
}
if (!byteStyle) {
if (representation === UINT32_T) {
formattedData = display = `0x${data}`
} else {
const orderedChunks = representation === MSB ? chunks : chunks.reverse()
formattedData = display = orderedChunks.map(chunk => `0x${chunk.join('')}`).join(', ')
}
let formattedData = isBytes ? data.toUpperCase() : data
let display = formattedData

if (isBytes) {
let chunks = chunkArray(data.toUpperCase().split(''), 2)
if (chunks.length > truncateAfter) {
truncated = true
chunks = chunks.slice(0, truncateAfter)
}
if (!byteStyle) {
if (representation === UINT32_T) {
formattedData = display = `0x${data}`
} else {
display = chunks.map((chunk, index) => (
<span key={`${data}_chunk_${index}`}>{hidden ? '••' : chunk}</span>
))
const orderedChunks = representation === MSB ? chunks : chunks.reverse()
formattedData = display = orderedChunks.map(chunk => `0x${chunk.join('')}`).join(', ')
}
} else if (hidden) {
display = '•'.repeat(Math.min(formattedData.length, truncateAfter))
} else {
display = chunks.map((chunk, index) => (
<span key={`${data}_chunk_${index}`}>{hidden ? '••' : chunk}</span>
))
}
} else if (hidden) {
display = '•'.repeat(Math.min(formattedData.length, truncateAfter))
}

if (truncated) {
display = [...display, '…']
}
if (truncated) {
display = [...display, '…']
}

const containerStyle = classnames(className, style.container, {
[style.containerSmall]: small,
[style.containerHidden]: hidden,
})

const dataStyle = classnames(style.data, {
[style.dataHidden]: hidden,
[style.dataTruncated]: this.state.truncated,
})

const copyButtonStyle = classnames(style.buttonIcon, {
[style.buttonIconCopied]: copied,
})

const renderButtonContainer = hideable || !noCopy || !noTransform

return (
<div ref={this.containerElem} className={containerStyle} onClick={this.handleContainerClick}>
<div
ref={this.displayElem}
onClick={this.handleDataClick}
className={dataStyle}
title={truncated ? formattedData : undefined}
>
{display}
</div>
{renderButtonContainer && (
<div ref={this.buttonsElem} className={style.buttons}>
{!hidden && !byteStyle && isBytes && (
<React.Fragment>
<span>{representation}</span>
<button
title={intl.formatMessage(m.byteOrder)}
className={style.buttonSwap}
onClick={this.handleSwapToggle}
>
<Icon className={style.buttonIcon} small icon="swap_horiz" />
</button>
</React.Fragment>
)}
{!noTransform && !hidden && isBytes && (
<button
title={intl.formatMessage(m.arrayFormatting)}
className={style.buttonTransform}
onClick={this.handleTransformToggle}
>
<Icon className={style.buttonIcon} small icon="code" />
</button>
)}
{!noCopy && (
const containerStyle = classnames(className, style.container, {
[style.containerSmall]: small,
[style.containerHidden]: hidden,
})

const dataStyle = classnames(style.data, {
[style.dataHidden]: hidden,
[style.dataTruncated]: truncated,
})

const copyButtonStyle = classnames(style.buttonIcon, {
[style.buttonIconCopied]: copied,
})

const renderButtonContainer = hideable || !noCopy || !noTransform

return (
<div ref={containerElem} className={containerStyle} onClick={handleContainerClick}>
<div
ref={displayElem}
onClick={handleDataClick}
className={dataStyle}
title={truncated ? formattedData : undefined}
>
{display}
</div>
{renderButtonContainer && (
<div ref={buttonsElem} className={style.buttons}>
{!hidden && !byteStyle && isBytes && (
<React.Fragment>
<span>{representation}</span>
<button
title={intl.formatMessage(sharedMessages.copyToClipboard)}
className={style.buttonCopy}
onClick={this.handleCopyClick}
data-clipboard-text={formattedData}
ref={this.copyElem}
disabled={copied}
title={intl.formatMessage(m.byteOrder)}
className={style.buttonSwap}
onClick={handleSwapToggle}
>
<Icon
className={copyButtonStyle}
onClick={this.handleCopyClick}
small
icon={copyIcon}
/>
{copied && !noCopyPopup && (
<Message
content={sharedMessages.copiedToClipboard}
onAnimationEnd={this.handleCopyAnimationEnd}
className={style.copyConfirm}
/>
)}
<Icon className={style.buttonIcon} small icon="swap_horiz" />
</button>
)}
{hideable && (
<button
title={intl.formatMessage(m.toggleVisibility)}
className={style.buttonVisibility}
onClick={this.handleVisibiltyToggle}
>
<Icon
className={style.buttonIcon}
small
icon={hidden ? 'visibility' : 'visibility_off'}
</React.Fragment>
)}
{!noTransform && !hidden && isBytes && (
<button
title={intl.formatMessage(m.arrayFormatting)}
className={style.buttonTransform}
onClick={handleTransformToggle}
>
<Icon className={style.buttonIcon} small icon="code" />
</button>
)}
{!noCopy && (
<button
title={intl.formatMessage(sharedMessages.copyToClipboard)}
className={style.buttonCopy}
onClick={handleCopyClick}
data-clipboard-text={formattedData}
ref={copyElem}
disabled={copied}
>
<Icon className={copyButtonStyle} onClick={handleCopyClick} small icon={copyIcon} />
{copied && !noCopyPopup && (
<Message
content={sharedMessages.copiedToClipboard}
onAnimationEnd={handleCopyAnimationEnd}
className={style.copyConfirm}
/>
</button>
)}
</div>
)}
</div>
)
}
)}
</button>
)}
{hideable && (
<button
title={intl.formatMessage(m.toggleVisibility)}
className={style.buttonVisibility}
onClick={handleVisibiltyToggle}
>
<Icon
className={style.buttonIcon}
small
icon={hidden ? 'visibility' : 'visibility_off'}
/>
</button>
)}
</div>
)}
</div>
)
}

SafeInspector.propTypes = {
/** The classname to be applied. */
className: PropTypes.string,
/** The data to be displayed. */
data: PropTypes.string.isRequired,
/** Whether the component should resize when its data is truncated. */
disableResize: PropTypes.bool,
/** Whether uint32_t notation should be enabled for byte representation. */
enableUint32: PropTypes.bool,
/** Whether the data can be hidden (like passwords). */
hideable: PropTypes.bool,
/** Whether the data is initially visible. */
initiallyVisible: PropTypes.bool,
/** Whether the data is in byte format. */
isBytes: PropTypes.bool,
/** Whether to hide the copy action. */
noCopy: PropTypes.bool,
/** Whether to hide the copy popup click and just display checkmark. */
noCopyPopup: PropTypes.bool,
/** Whether to hide the data transform action. */
noTransform: PropTypes.bool,
/**
* Whether a smaller style should be rendered (useful for display in
* tables).
*/
small: PropTypes.bool,
/** The input count (byte or characters, based on type) after which the
* display is truncated.
*/
truncateAfter: PropTypes.number,
}

SafeInspector.defaultProps = {
className: undefined,
noCopyPopup: false,
disableResize: false,
hideable: true,
initiallyVisible: false,
isBytes: true,
small: false,
noTransform: false,
noCopy: false,
enableUint32: false,
truncateAfter: Infinity,
}

export default SafeInspector
71 changes: 27 additions & 44 deletions pkg/webui/lib/components/init.js
Original file line number Diff line number Diff line change
@@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import React from 'react'
import { connect } from 'react-redux'
import React, { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import 'focus-visible/dist/focus-visible'
import { setConfiguration } from 'react-grid-system'
import { defineMessages } from 'react-intl'
@@ -70,32 +70,13 @@ setConfiguration({
gutterWidth: LAYOUT.GUTTER_WIDTH,
})

@connect(
state => ({
initialized: !selectInitFetching(state) && selectIsInitialized(state),
error: selectInitError(state),
}),
dispatch => ({
initialize: () => dispatch(initialize()),
}),
)
export default class Init extends React.PureComponent {
static propTypes = {
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
error: PropTypes.error,
initialize: PropTypes.func.isRequired,
initialized: PropTypes.bool,
}

static defaultProps = {
initialized: false,
error: undefined,
}
const Init = ({ children }) => {
const initialized = useSelector(state => !selectInitFetching(state) && selectIsInitialized(state))
const error = useSelector(state => selectInitError(state))
const dispatch = useDispatch()

componentDidMount() {
const { initialize } = this.props

initialize()
useEffect(() => {
dispatch(initialize())

// Preload font files to avoid flashes of unstyled text.
for (const fontUrl of fontsToPreload) {
@@ -106,25 +87,27 @@ export default class Init extends React.PureComponent {
linkElem.setAttribute('crossorigin', 'anonymous')
document.getElementsByTagName('head')[0].appendChild(linkElem)
}
}
}, [dispatch])

render() {
const { initialized, error } = this.props
if (error) {
throw error
}

if (error) {
throw error
}
if (!initialized) {
return (
<div style={{ height: '100vh' }}>
<Spinner center>
<Message content={m.initializing} />
</Spinner>
</div>
)
}

if (!initialized) {
return (
<div style={{ height: '100vh' }}>
<Spinner center>
<Message content={m.initializing} />
</Spinner>
</div>
)
}
return children
}

return this.props.children
}
Init.propTypes = {
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
}

export default Init
85 changes: 39 additions & 46 deletions pkg/webui/lib/components/intl-helmet.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2019 The Things Network Foundation, The Things Industries B.V.
// Copyright © 2023 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.
@@ -12,67 +12,60 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import React from 'react'
import React, { useEffect } from 'react'
import { Helmet } from 'react-helmet'
import { injectIntl } from 'react-intl'
import { useIntl } from 'react-intl'

import PropTypes from '@ttn-lw/lib/prop-types'
import { warn } from '@ttn-lw/lib/log'

/**
* IntlHelmet is a HOC that enables usage of i18n message objects inside the
* props in react-helmet, which will be translated automatically.
*/
@injectIntl
export default class IntlHelmet extends React.Component {
static propTypes = {
children: PropTypes.node,
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
values: PropTypes.shape({}),
}

static defaultProps = {
children: undefined,
values: undefined,
}
const IntlHelmet = ({ children, values, ...rest }) => {
const intl = useIntl()

componentDidMount() {
if (this.props.children) {
useEffect(() => {
if (children) {
warn(`Children of <IntlHelmet /> will not be translated. If you tried to
translate head elements with <Message />, use props with message objects
instead.`)
}
}
}, [children])

render() {
const { intl, children, values, ...rest } = this.props
let translatedRest = {}
for (const key in rest) {
let prop = rest[key]
if (typeof prop === 'object' && prop.id && prop.defaultMessage) {
const messageValues = values || prop.values || {}
const translatedMessageValues = {}
let translatedRest = {}
for (const key in rest) {
let prop = rest[key]
if (typeof prop === 'object' && prop.id && prop.defaultMessage) {
const messageValues = values || prop.values || {}
const translatedMessageValues = {}

for (const entry in messageValues) {
const content = messageValues[entry]
if (typeof content === 'object' && prop.id && prop.defaultMessage) {
translatedMessageValues[entry] = intl.formatMessage(content)
} else {
translatedMessageValues[entry] = messageValues[entry]
}
for (const entry in messageValues) {
const content = messageValues[entry]
if (typeof content === 'object' && prop.id && prop.defaultMessage) {
translatedMessageValues[entry] = intl.formatMessage(content)
} else {
translatedMessageValues[entry] = messageValues[entry]
}

prop = intl.formatMessage(prop, translatedMessageValues)
}

translatedRest = {
...translatedRest,
[key]: prop,
}
prop = intl.formatMessage(prop, translatedMessageValues)
}

return <Helmet {...translatedRest}>{children}</Helmet>
translatedRest = {
...translatedRest,
[key]: prop,
}
}

return <Helmet {...translatedRest}>{children}</Helmet>
}

IntlHelmet.propTypes = {
children: PropTypes.node,
values: PropTypes.shape({}),
}

IntlHelmet.defaultProps = {
children: undefined,
values: undefined,
}

export default IntlHelmet

0 comments on commit 8b6e3ed

Please sign in to comment.