From 73312ea1a79a1e630ed0b820b48765d0ecb9d516 Mon Sep 17 00:00:00 2001 From: Kevin Schiffer Date: Thu, 28 Sep 2023 16:29:17 +0200 Subject: [PATCH] console,account: Convert class components to functional components --- pkg/webui/components/navigation/side/index.js | 344 +++++------ pkg/webui/components/safe-inspector/index.js | 536 +++++++++--------- pkg/webui/lib/components/init.js | 71 +-- pkg/webui/lib/components/intl-helmet.js | 91 ++- 4 files changed, 474 insertions(+), 568 deletions(-) diff --git a/pkg/webui/components/navigation/side/index.js b/pkg/webui/components/navigation/side/index.js index 8647cb76b53..ef456675d75 100644 --- a/pkg/webui/components/navigation/side/index.js +++ b/pkg/webui/components/navigation/side/index.js @@ -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,220 +40,181 @@ 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 ( - <> - + - - )} - {!noTransform && !hidden && isBytes && ( - - )} - {!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 ( +
+
+ {display} +
+ {renderButtonContainer && ( +
+ {!hidden && !byteStyle && isBytes && ( + + {representation} - )} - {hideable && ( - + )} + {!noCopy && ( + - )} -
- )} -
- ) - } + )} + + )} + {hideable && ( + + )} + + )} + + ) +} + +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 diff --git a/pkg/webui/lib/components/init.js b/pkg/webui/lib/components/init.js index 30e9a420e67..3b02568b1b1 100644 --- a/pkg/webui/lib/components/init.js +++ b/pkg/webui/lib/components/init.js @@ -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 ( +
+ + + +
+ ) + } - if (!initialized) { - return ( -
- - - -
- ) - } + return children +} - return this.props.children - } +Init.propTypes = { + children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired, } + +export default Init diff --git a/pkg/webui/lib/components/intl-helmet.js b/pkg/webui/lib/components/intl-helmet.js index e7bc130430e..f4f71b7d98a 100644 --- a/pkg/webui/lib/components/intl-helmet.js +++ b/pkg/webui/lib/components/intl-helmet.js @@ -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) { - warn(`Children of will not be translated. If you tried to -translate head elements with , use props with message objects -instead.`) + useEffect(() => { + if (children) { + warn( + `Children of will not be translated. If you tried to translate head elements with , 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 {children} + translatedRest = { + ...translatedRest, + [key]: prop, + } } + + return {children} +} + +IntlHelmet.propTypes = { + children: PropTypes.node, + values: PropTypes.shape({}), } + +IntlHelmet.defaultProps = { + children: undefined, + values: undefined, +} + +export default IntlHelmet