diff --git a/config/eslintrc.yaml b/config/eslintrc.yaml index 1c338b90b7..c10c940903 100644 --- a/config/eslintrc.yaml +++ b/config/eslintrc.yaml @@ -141,6 +141,8 @@ rules: react/forbid-foreign-prop-types: off # Prevent undefined components. react/jsx-no-undef: warn + # Allow arrow functions as props + react/jsx-no-bind: off # Prevent vague prop types. react/forbid-prop-types: - warn diff --git a/config/storybook/webpack.config.js b/config/storybook/webpack.config.js index 5566e14daa..6def1b0a81 100644 --- a/config/storybook/webpack.config.js +++ b/config/storybook/webpack.config.js @@ -14,6 +14,9 @@ /* eslint-env node */ /* eslint-disable import/no-commonjs */ +import fs from 'fs' +import path from 'path' + const MiniCssExtractPlugin = require('mini-css-extract-plugin') require('@babel/register') @@ -22,9 +25,16 @@ const { default: bundleConfig, styleConfig } = require('../webpack.config.babel' // List of allowed plugins. const allow = [MiniCssExtractPlugin] +const { CONTEXT = '.' } = process.env +const context = path.resolve(CONTEXT) +const supportedLocales = fs + .readdirSync(path.resolve(context, 'pkg/webui/locales')) + .filter(fn => fn.endsWith('.json')) + .map(fn => fn.split('.')[0]) + module.exports = async ({ config, mode }) => { + const webpack = require('webpack') if (mode === 'PRODUCTION') { - const webpack = require('webpack') allow.push(webpack.DllReferencePlugin) } @@ -50,7 +60,13 @@ module.exports = async ({ config, mode }) => { module: { rules: [...config.module.rules, styleConfig], }, - plugins: [...config.plugins, ...filteredPlugins], + plugins: [ + ...config.plugins, + ...filteredPlugins, + new webpack.DefinePlugin({ + 'process.predefined.SUPPORTED_LOCALES': JSON.stringify(supportedLocales), + }), + ], } return cfg diff --git a/pkg/webui/account/containers/clients-table/index.js b/pkg/webui/account/containers/clients-table/index.js index 36d2c5f33c..d09437f5b8 100644 --- a/pkg/webui/account/containers/clients-table/index.js +++ b/pkg/webui/account/containers/clients-table/index.js @@ -151,7 +151,7 @@ const ClientsTable = () => { }), render: details => ( -   -   - - - ) - } - - @bind - toggle() { - this.setState(state => ({ - busy: !state.busy, - })) - } - - @bind - disable() { - this.setState(state => ({ - disabled: !state.disabled, - })) - } - - @bind - error() { - this.setState({ - error: true, - }) - setTimeout( - function () { - this.setState({ - error: false, - }) - }.bind(this), - 1200, - ) - } -} - export default { - title: 'Button', + title: 'Button V2', + parameters: { + design: { + type: 'figma', + url: 'https://www.figma.com/file/7pBLWK4tsjoAbyJq2viMAQ/2023-console-redesign?type=design&node-id=590%3A51411&mode=design&t=Hbk2Qngeg1xqg4V3-1', + }, + }, } -export const Default = () => ( -
-
-) - -export const Warning = () => ( -
-
-) - -export const Danger = () => ( -
-
+const dropdownItems = ( + + + + ) export const Primary = () => ( -
+
) export const WithIcon = () => ( -
-
) -export const Naked = () => ( -
-
) -export const NakedWithIcon = () => ( -
-
+ ) +} + +export const PrimayOnlyIconDropdown = () => { + const ref = useRef() + + return ( +
+
+ ) +} + +export const Secondary = () => ( +
+
) -export const OnlyIcon = () => ( -
-
) -export const CustomContent = () => ( -
- -
-
- -
-
- +export const SecondaryOnlyIcon = () => ( +
+
) -CustomContent.story = { - name: 'Custom content', +export const SecondaryDropdown = () => { + const ref = useRef() + + return ( +
+
+ ) } -export const Toggle = () => +export const SecondaryOnlyIconDropdown = () => { + const ref = useRef() + + return ( +
+
+ ) +} diff --git a/pkg/webui/components/collapse/index.js b/pkg/webui/components/collapse/index.js index 20f8f4da06..ad5a3f8efa 100644 --- a/pkg/webui/components/collapse/index.js +++ b/pkg/webui/components/collapse/index.js @@ -50,6 +50,7 @@ const Collapse = props => {
- ) : external ? ( - - {Boolean(iconElement) ? iconElement : null} - - ) : ( - - {iconElement} + + {Boolean(iconElement) ? iconElement : null} - + + ) + + const submenu = Boolean(submenuItems) && ( + <> + + + {submenuItems} + + ) + return ( -
  • - {ItemElement} +
  • + {submenu || ItemElement}
  • ) } @@ -105,6 +171,7 @@ DropdownItem.propTypes = { icon: PropTypes.string, path: PropTypes.string, showActive: PropTypes.bool, + submenuItems: PropTypes.arrayOf(PropTypes.node), tabIndex: PropTypes.string, title: PropTypes.message.isRequired, } @@ -118,6 +185,7 @@ DropdownItem.defaultProps = { path: undefined, showActive: true, tabIndex: '0', + submenuItems: undefined, } const DropdownHeaderItem = ({ title }) => ( @@ -134,5 +202,6 @@ DropdownHeaderItem.propTypes = { Dropdown.Item = DropdownItem Dropdown.HeaderItem = DropdownHeaderItem +Dropdown.Attached = AttachedDropdown export default Dropdown diff --git a/pkg/webui/components/dropdown/story.js b/pkg/webui/components/dropdown/story.js new file mode 100644 index 0000000000..67f236fd87 --- /dev/null +++ b/pkg/webui/components/dropdown/story.js @@ -0,0 +1,34 @@ +// 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. +// 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 from 'react' + +import Dropdown from '.' + +export default { + title: 'Dropdown V2', + component: Dropdown, +} + +export const Default = () => ( +
    + + + + +
    + +
    +
    +) diff --git a/pkg/webui/components/header/header.styl b/pkg/webui/components/header/header.styl index 9433fef1fb..ea29eeb183 100644 --- a/pkg/webui/components/header/header.styl +++ b/pkg/webui/components/header/header.styl @@ -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,128 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -mobile-styles($breakpoint) - +media-query($breakpoint) - &.mobile-menu-open - position: fixed - z-index: $zi.mobile-menu - top: 0 - left: 0 - height: 100vh - background-color: white - - .hamburger img - width: 1.5rem - - .mobile-menu-button - display: block - - .nav-list - display: none - - .profile-dropdown - display: none - - .mobile-menu - display: flex - - +media-query-between($bp.s, $breakpoint) - .mobile-menu - height: 'calc(100vh - %s)' % $header-height - - +media-query($bp.s) - z-index: $zi.mobile-menu - position: fixed - top: 0 - - & + main - padding-top: $header-height - +media-query($bp.s) - padding-top: $header-height-mobile - - .bar - height: $header-height-mobile - background-color: white - padding: 0 $ls.xxs - - .mobile-menu - height: 'calc(100vh - %s)' % $header-height-mobile - top: $header-height-mobile - - .mobile-menu-button - width: $header-height-mobile - margin-right: $ls.xxs * -1 - .container - mobile-styles($bp.m) - background-color: white + --dropdown-offset: $cs.s + padding: 0 $cs.m + background: $c['tts-primary-050'] z-index: $zi.nav - width: 100% - -.bar - border-normal('bottom') - background: linear-gradient(bottom left, 0% #F8F8F8, 60% white) // @stylint ignore - height: $header-height - box-sizing: border-box - position: relative + border-bottom: 1px solid $c-divider display: flex - padding: 0 $ls.s justify-content: space-between - flex: none - -.logo - display: inline-flex align-items: center - justify-content: flex-start + height: 4rem + +media-query($bp.xs) + padding: 0 $cs.xs - img - nudge('up') - -.nav-list - display: flex - align-items: stretch - justify-content: flex-start - padding: 0 $ls.m - flex-grow: 2 - height: 100% - max-width: 56rem - flex-shrink: 0 - -.profile-dropdown - margin-right: -2rem - -.mobile-menu-button - reset-button() - display: none - width: $header-height - margin-right: $ls.s * -1 - height: 100% - position: relative - - .hamburger - center-absolute() - display: block - width: 2rem - height: 100% - - img - center-absolute() - width: 2rem - -.left - display: flex - align-items: center - max-width: 42rem - flex-grow: 1 - -.right - padding-left: $cs.l - display: flex - align-items: center - justify-content: flex-end - max-width: 25rem - flex-grow: 2 - - & > div:first-child - max-width: 15rem - border-color: $c-divider-dark +.logo + height: $cs.l + +media-query($bp.xs) + width: 3rem diff --git a/pkg/webui/components/header/index.js b/pkg/webui/components/header/index.js index d7af0b5d8a..6fb396579b 100644 --- a/pkg/webui/components/header/index.js +++ b/pkg/webui/components/header/index.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,107 +12,73 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { useState, useCallback } from 'react' +import React from 'react' import classnames from 'classnames' -import hamburgerMenuNormal from '@assets/misc/hamburger-menu-normal.svg' -import hamburgerMenuClose from '@assets/misc/hamburger-menu-close.svg' - -import NavigationBar from '@ttn-lw/components/navigation/bar' +import Button from '@ttn-lw/components/button' import ProfileDropdown from '@ttn-lw/components/profile-dropdown' -import MobileMenu from '@ttn-lw/components/mobile-menu' -import Input from '@ttn-lw/components/input' import PropTypes from '@ttn-lw/lib/prop-types' import style from './header.styl' const Header = ({ + brandLogo, + Logo, className, - dropdownItems, - navigationEntries, + addDropdownItems, + starDropdownItems, + profileDropdownItems, user, - searchable, - logo, - mobileDropdownItems, - onLogout, - onSearchRequest, + onMenuClick, ...rest -}) => { - const isGuest = !Boolean(user) - - const [mobileMenuOpen, setMobileMenuOpen] = useState(false) - const handleMobileMenuClick = useCallback(() => { - setMobileMenuOpen(!mobileMenuOpen) - }, [mobileMenuOpen]) - - const handleMobileMenuItemsClick = useCallback(() => { - setMobileMenuOpen(false) - }, []) +}) => ( +
    +
    +
    +
    - const classNames = classnames(className, style.container, { - [style.mobileMenuOpen]: mobileMenuOpen, - }) +
    +
    +
    +) - const hamburgerGraphic = mobileMenuOpen ? hamburgerMenuClose : hamburgerMenuNormal - - return ( -
    -
    -
    - {logo} - {!isGuest && {navigationEntries}} -
    - {!isGuest && ( -
    - {searchable && } - - {dropdownItems} - - -
    - )} -
    - {mobileMenuOpen && ( - - {mobileDropdownItems} - - )} -
    - ) -} +const imgPropType = PropTypes.shape({ + src: PropTypes.string.isRequired, + alt: PropTypes.string.isRequired, +}) Header.propTypes = { + Logo: PropTypes.elementType.isRequired, + /** The dropdown items when the add button is clicked. */ + addDropdownItems: PropTypes.node.isRequired, + brandLogo: imgPropType, /** The classname applied to the component. */ className: PropTypes.string, - /** The child node of the dropdown component. */ - dropdownItems: PropTypes.node, - /** The logo component. */ - logo: PropTypes.node.isRequired, - /** The child node of the mobile dropdown. */ - mobileDropdownItems: PropTypes.node, - /** The Child node of the navigation bar. */ - navigationEntries: PropTypes.node, - /** A handler for when the user used the search input. */ - onLogout: PropTypes.func, - /** Handler of the search function. */ - onSearchRequest: PropTypes.func, - /* A flag indicating whether the header has a search input. */ - searchable: PropTypes.bool, + /** A handler for when the menu button is clicked. */ + onMenuClick: PropTypes.func.isRequired, + /** The dropdown items when the profile button is clicked. */ + profileDropdownItems: PropTypes.node.isRequired, + /** The dropdown items when the star button is clicked. */ + starDropdownItems: PropTypes.node.isRequired, /** * The User object, retrieved from the API. If it is `undefined`, then the * guest header is rendered. @@ -122,13 +88,8 @@ Header.propTypes = { Header.defaultProps = { className: undefined, - dropdownItems: undefined, - navigationEntries: undefined, - mobileDropdownItems: null, - onSearchRequest: () => null, - onLogout: () => null, - searchable: false, user: undefined, + brandLogo: undefined, } export default Header diff --git a/pkg/webui/components/header/story.js b/pkg/webui/components/header/story.js index 3d1610df6c..4c48bbb031 100644 --- a/pkg/webui/components/header/story.js +++ b/pkg/webui/components/header/story.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. @@ -13,70 +13,83 @@ // limitations under the License. import React from 'react' -import { action } from '@storybook/addon-actions' -import TtsLogo from '@assets/static/logo.svg' +import TtsLogo from '@assets/static/tts-logo.svg' import Dropdown from '@ttn-lw/components/dropdown' -import NavigationBar from '@ttn-lw/components/navigation/bar' -import Logo from '@ttn-lw/components/logo' -import ExampleLogo from '@ttn-lw/components/logo/story-logo.svg' +import ExampleLogo from '@ttn-lw/components/logo/story-logo-new.svg' +import Breadcrumb from '@ttn-lw/components/breadcrumbs/breadcrumb' import Header from '.' const user = { - name: 'kschiffer', + name: 'johndoe', ids: { - user_id: 'ksc300', + user_id: 'jdoe300', }, } -const singleLogo = -const doubleLogo = ( - -) +export default { + title: 'Header V2', +} -const navigationEntries = ( - - - - - - +const plusDropdownItems = ( + <> + + + + + ) -const items = ( - - - - +const starDropdownItems = ( + <> + + + + + + + ) -export default { - title: 'Header', -} - -export const SingleLogo = () => ( -
    +const profileDropdownItems = ( + <> + + + + +
    + + + +
    + + ) -export const DoubleLogo = () => ( -
    -) +export const Default = () => { + const breadcrumbs = [ + , + , + , + ] + + return ( +
    +
    +
    + ) +} diff --git a/pkg/webui/components/icon/index.js b/pkg/webui/components/icon/index.js index a433e7765c..33709c9493 100644 --- a/pkg/webui/components/icon/index.js +++ b/pkg/webui/components/icon/index.js @@ -23,12 +23,12 @@ import style from './icon.styl' const hardcoded = { access: 'lock', api_keys: 'key', - application: 'web_asset', + application: 'display_settings', cluster: 'language', collaborators: 'people', data: 'poll', develop: 'code', - device: 'device_hub', + device: 'settings_remote', devices: 'device_hub', downlink: 'arrow_downward', event: 'info', @@ -56,7 +56,7 @@ const hardcoded = { join: 'link', link: 'link', location: 'place', - logout: 'power_settings_new', + logout: 'logout', organization: 'people', overview: 'dashboard', packet_broker: 'camera', diff --git a/pkg/webui/components/input/input.styl b/pkg/webui/components/input/input.styl index dc1da341cb..7dd975485e 100644 --- a/pkg/webui/components/input/input.styl +++ b/pkg/webui/components/input/input.styl @@ -20,7 +20,7 @@ input-width-classes() box-sizing: border-box background: white - border-radius: $br.xs + border-radius: $br.m padding: 0 position: relative transition: border-color $ad.s diff --git a/pkg/webui/components/key-value-map/entry.js b/pkg/webui/components/key-value-map/entry.js index 91c1a778de..d7d1552ffe 100644 --- a/pkg/webui/components/key-value-map/entry.js +++ b/pkg/webui/components/key-value-map/entry.js @@ -143,7 +143,8 @@ const Entry = ({ title={m.deleteEntry} message={removeMessage} disabled={readOnly} - danger={!Boolean(removeMessage)} + danger + naked /> )}
    diff --git a/pkg/webui/components/key-value-map/index.js b/pkg/webui/components/key-value-map/index.js index 9ae7679f93..9de98697bb 100644 --- a/pkg/webui/components/key-value-map/index.js +++ b/pkg/webui/components/key-value-map/index.js @@ -107,6 +107,7 @@ const KeyValueMap = ({ onClick={addEmptyEntry} disabled={disabled} icon="add" + secondary />
    diff --git a/pkg/webui/components/link/link.styl b/pkg/webui/components/link/link.styl index b834b165fc..a901397505 100644 --- a/pkg/webui/components/link/link.styl +++ b/pkg/webui/components/link/link.styl @@ -31,7 +31,7 @@ color: $c-active-blue-active &.secondary - color: $tc-subtle-gray + color: $c['grey-700'] &:active, &:hover color: $tc-deep-gray diff --git a/pkg/webui/components/logo/index.js b/pkg/webui/components/logo/index.js index 570da164e3..5cec3dbd3c 100644 --- a/pkg/webui/components/logo/index.js +++ b/pkg/webui/components/logo/index.js @@ -15,48 +15,18 @@ import React from 'react' import classnames from 'classnames' -import Link from '@ttn-lw/components/link' - import PropTypes from '@ttn-lw/lib/prop-types' import style from './logo.styl' -const LogoLink = ({ safe, to, ...rest }) => { - if (safe) { - return - } - - return -} - -LogoLink.propTypes = { - safe: PropTypes.bool, - to: PropTypes.string.isRequired, -} - -LogoLink.defaultProps = { - safe: false, -} - -const Logo = ({ className, logo, brandLogo, vertical, safe }) => { - const classname = classnames(style.container, className, { - [style.vertical]: vertical, - [style.customBranding]: Boolean(brandLogo), - }) +const Logo = ({ className, logo, miniLogo }) => { + const classname = classnames(style.container, className) return (
    - {Boolean(brandLogo) && ( -
    - -
    - )}
    - - - + +
    ) @@ -71,6 +41,7 @@ Logo.propTypes = { brandLogo: imgPropType, className: PropTypes.string, logo: imgPropType.isRequired, + miniLogo: imgPropType.isRequired, safe: PropTypes.bool, vertical: PropTypes.bool, } diff --git a/pkg/webui/components/logo/logo.styl b/pkg/webui/components/logo/logo.styl index f24babdd7e..4d69f34d1d 100644 --- a/pkg/webui/components/logo/logo.styl +++ b/pkg/webui/components/logo/logo.styl @@ -26,104 +26,22 @@ display: flex align-items: center line-height: 0 - height: 2.3rem - padding: $cs.m 0 img width: 100% - +media-query($bp.s) - width: 9rem - height: 2.1rem - .logo display: flex align-items: center - .brand-logo - display: flex - align-items: center - justify-content: center - flex-direction: column - max-width: 9rem - - +media-query-min($bp.s) - height: 3rem - - +media-query($bp.s) - height: 2rem - - &-container - display: flex - align-items: center - height: 100% - width: 100% - - &:not(.vertical) - &:not(.custom-branding) - .logo-container - width: 13rem - +media-query($bp.s) - width: 12rem - - &.custom-branding .logo - display: flex - align-items: center - padding-left: 'calc(%s - .15rem)' % $ls.xs - +media-query($bp.s) - padding-left: $cs.s - - .logo-container - nudge('down', .15rem) - width: 11rem - +media-query($bp.s) - width: 8rem - - .brand-logo - border-dark('right') - align-items: center - padding-right: $ls.xs - +media-query($bp.s) - padding-right: $cs.s - - &-container - +media-query($bp.s) - max-width: 6rem - height: 2.5rem - - img - width: 100% - - img - object-position: left - - &.vertical - width: 15rem - flex-direction: column - margin-bottom: $ls.s - - &.custom-branding .logo - border-dark('top') - padding-top: $ls.xs - margin-top: $ls.xs - height: auto - width: 100% - - &-container - width: 100% - height: auto - - .brand-logo - margin-left: auto - margin-right: auto - width: 100% - - &-container + +media-query-min($bp.xs) + img:first-child display: block - max-height: 3rem - max-width: 10rem - width: 100% + img:last-child + display: none - img - object-position: center - width: 100% + +media-query($bp.xs) + img:first-child + display: none + img:last-child + display: block diff --git a/pkg/webui/components/logo/story-logo-new.svg b/pkg/webui/components/logo/story-logo-new.svg new file mode 100644 index 0000000000..4327977521 --- /dev/null +++ b/pkg/webui/components/logo/story-logo-new.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pkg/webui/components/navigation/side/index.js b/pkg/webui/components/navigation/side/index.js deleted file mode 100644 index 77bafa2edc..0000000000 --- a/pkg/webui/components/navigation/side/index.js +++ /dev/null @@ -1,225 +0,0 @@ -// 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. -// 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 ReactDom from 'react-dom' -import React, { useState, useEffect, useCallback, useRef } from 'react' -import classnames from 'classnames' -import { defineMessages, useIntl } from 'react-intl' - -import LAYOUT from '@ttn-lw/constants/layout' - -import Button from '@ttn-lw/components/button' -import Icon from '@ttn-lw/components/icon' -import Link from '@ttn-lw/components/link' - -import Message from '@ttn-lw/lib/components/message' - -import PropTypes from '@ttn-lw/lib/prop-types' - -import SideNavigationList from './list' -import SideNavigationItem from './item' -import SideNavigationContext from './context' - -import style from './side.styl' - -const getViewportWidth = () => - Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0) - -const m = defineMessages({ - hideSidebar: 'Hide sidebar', -}) - -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]) - - const closeDrawer = useCallback(() => { - setIsDrawerOpen(false) - document.body.classList.remove(style.scrollLock) - }, []) - - const openDrawer = useCallback(() => { - setIsDrawerOpen(true) - document.body.classList.add(style.scrollLock) - }, []) - - 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) - ) { - setIsMinimized(getViewportWidth() <= LAYOUT.BREAKPOINTS.M || preferMinimized) - updateAppContainerClasses() - } - }, [isMinimized, preferMinimized, updateAppContainerClasses]) - - useEffect(() => { - window.addEventListener('resize', setMinimizedState) - updateAppContainerClasses(true) - return () => { - window.removeEventListener('resize', setMinimizedState) - removeAppContainerClasses() - } - }, [removeAppContainerClasses, setMinimizedState, updateAppContainerClasses]) - - const onToggle = useCallback(async () => { - setIsMinimized(prev => !prev) - setPreferMinimized(prev => !prev) - updateAppContainerClasses() - }, [updateAppContainerClasses]) - - const onDrawerExpandClick = useCallback(() => { - if (!isDrawerOpen) { - openDrawer() - } else { - closeDrawer() - } - }, [isDrawerOpen, openDrawer, closeDrawer]) - - const onLeafItemClick = useCallback(() => { - if (isDrawerOpen) { - onDrawerExpandClick() - } - }, [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 ( - <> - - ) } ProfileDropdown.propTypes = { + brandLogo: PropTypes.shape({ + src: PropTypes.string.isRequired, + alt: PropTypes.string.isRequired, + }), /** * A list of items for the dropdown component. See ``'s `items` * proptypes for details. @@ -69,11 +56,10 @@ ProfileDropdown.propTypes = { className: PropTypes.string, /** The profile picture of the current user. */ profilePicture: PropTypes.profilePicture, - /** The name/id of the current user. */ - userName: PropTypes.string.isRequired, } ProfileDropdown.defaultProps = { + brandLogo: undefined, className: undefined, profilePicture: undefined, } diff --git a/pkg/webui/components/profile-dropdown/profile-dropdown.styl b/pkg/webui/components/profile-dropdown/profile-dropdown.styl index af14a2d1e5..eff28cb81d 100644 --- a/pkg/webui/components/profile-dropdown/profile-dropdown.styl +++ b/pkg/webui/components/profile-dropdown/profile-dropdown.styl @@ -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. @@ -13,37 +13,21 @@ // limitations under the License. .container - display: inline-flex - align-items: center + --dropdown-offset: $cs.s position: relative - cursor: pointer - transition: background-color $ad.s - height: 100% - padding: 0 $cs.xl - &:hover - area-hover() +.profile-dropdown + gap: 0 - +focus-visible() - background-color: $c-backdrop-lighter + button + padding: 0 $cs.s !important - &:active, &:focus - area-active() +.brand-logo + height: 1.14rem -.id - one-liner() - margin-right: $cs.xs - - +media-query($bp.l) + +media-query($bp.s) display: none -.dropdown - top: $header-height - - &-icon - margin-right: -.4rem - .profile-picture - height: 3rem - width: 3rem - margin-right: $cs.m + height: 1.5rem + width: 1.5rem diff --git a/pkg/webui/components/profile-dropdown/story.js b/pkg/webui/components/profile-dropdown/story.js index 1d3c30be8e..a08ba25b67 100644 --- a/pkg/webui/components/profile-dropdown/story.js +++ b/pkg/webui/components/profile-dropdown/story.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. @@ -15,6 +15,7 @@ import React from 'react' import Dropdown from '@ttn-lw/components/dropdown' +import ExampleLogo from '@ttn-lw/components/logo/story-logo-new.svg' import ProfileDropdown from '.' @@ -24,15 +25,27 @@ const handleLogout = () => { } export default { - title: 'Profile Dropdown', + title: 'Profile Dropdown V2', component: ProfileDropdown, } export const Default = () => ( -
    - - - +
    + + + + + +
    + + + +
    +
    ) diff --git a/pkg/webui/components/profile-picture/index.js b/pkg/webui/components/profile-picture/index.js index 295431f614..36e96bd725 100644 --- a/pkg/webui/components/profile-picture/index.js +++ b/pkg/webui/components/profile-picture/index.js @@ -15,7 +15,7 @@ import React, { useRef, useCallback } from 'react' import classnames from 'classnames' -import missingProfilePicture from '@assets/img/placeholder/missing-profile-picture.png' +import missingProfilePicture from '@assets/img/placeholder/missing-profile-picture.svg' import PropTypes from '@ttn-lw/lib/prop-types' import { diff --git a/pkg/webui/components/sidebar/dedicated-entity/dedicated-entity.styl b/pkg/webui/components/sidebar/dedicated-entity/dedicated-entity.styl new file mode 100644 index 0000000000..00c448ea7e --- /dev/null +++ b/pkg/webui/components/sidebar/dedicated-entity/dedicated-entity.styl @@ -0,0 +1,87 @@ +// 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. +// 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. + +.dedicated-entity + text-decoration: none + line-height: 1.6rem + color: $c.grey-900 + position: relative + height: 2.5rem + + &-curtain + position: absolute + overflow: hidden + border-radius: $br.l + width: 2.5rem + transition: 0.3s 0s all cubic-bezier(0.770, 0.000, 0.175, 1.000) + top: 0 + left: 0 + z-index: $zi.slight + + &:hover + transition: 0.3s 0.2s all cubic-bezier(0.770, 0.000, 0.175, 1.000) + width: 100% + + // Reveal the button label. + button span:last-child + opacity: 1 + + &-item + display: flex + color: $c.grey-900 + opacity: 1 + position: absolute + left: 2.5rem + height: 100% + text-decoration: none + + &-label + font-size: $fs.m + font-weight: $fw.normal + margin: 0 0 0 $cs.xs + + &-divider + display: list-item + height: 100% + margin: 0 $cs.xs 0 $cs.s + border-left: 1px solid $c.grey-200 + background-color: $c.grey-200 + + &-button + border-radius: $br.l + padding: 0 $cs.xs 0 0.73rem + background-color: $c.grey-900 + position: relative + display: inline-flex + border-radius: $br.l + outline: 0 + cursor: pointer + justify-content: center + align-items: center + gap: $cs.xxs + height: 2.5rem + text-decoration: none + padding: 0 .75rem + white-space: nowrap + color: white + width: 100% + justify-content: flex-start + + span:last-child + transition: 0.2s 0.2s all ease-in-out + opacity: 0 + + &:not(:disabled) + +focus-visible() + background-color: hoverize($c.grey-500) diff --git a/pkg/webui/components/sidebar/dedicated-entity/index.js b/pkg/webui/components/sidebar/dedicated-entity/index.js new file mode 100644 index 0000000000..662e279b20 --- /dev/null +++ b/pkg/webui/components/sidebar/dedicated-entity/index.js @@ -0,0 +1,55 @@ +// 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. +// 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 from 'react' +import { Link } from 'react-router-dom' +import classnames from 'classnames' + +import Button from '@ttn-lw/components/button' + +import Message from '@ttn-lw/lib/components/message' + +import PropTypes from '@ttn-lw/lib/prop-types' + +import style from './dedicated-entity.styl' + +const DedicatedEntity = ({ label, className, buttonMessage, path, backPath }) => ( +
    + +
    +) + +DedicatedEntity.propTypes = { + backPath: PropTypes.string.isRequired, + buttonMessage: PropTypes.message.isRequired, + className: PropTypes.string, + label: PropTypes.string.isRequired, + path: PropTypes.string.isRequired, +} + +DedicatedEntity.defaultProps = { + className: undefined, +} + +export default DedicatedEntity diff --git a/pkg/webui/components/sidebar/dedicated-entity/story.js b/pkg/webui/components/sidebar/dedicated-entity/story.js new file mode 100644 index 0000000000..87c931fbed --- /dev/null +++ b/pkg/webui/components/sidebar/dedicated-entity/story.js @@ -0,0 +1,29 @@ +// 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. +// 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 from 'react' + +import DedicatedEntity from '.' + +export default { + title: 'Sidebar/DedicatedEntity', + parameters: { + design: { + type: 'figma', + url: 'https://www.figma.com/file/7pBLWK4tsjoAbyJq2viMAQ/2023-console-redesign?type=design&node-id=1345%3A11699&mode=design&t=Hbk2Qngeg1xqg4V3-1', + }, + }, +} + +export const Default = () => diff --git a/pkg/webui/components/sidebar/search-button/index.js b/pkg/webui/components/sidebar/search-button/index.js new file mode 100644 index 0000000000..fe3ac30782 --- /dev/null +++ b/pkg/webui/components/sidebar/search-button/index.js @@ -0,0 +1,70 @@ +// 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. +// 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, useContext, useRef } from 'react' +import classnames from 'classnames' + +import Button from '@ttn-lw/components/button' +import Icon from '@ttn-lw/components/icon' +import Dropdown from '@ttn-lw/components/dropdown' + +import Message from '@ttn-lw/lib/components/message' + +import SidebarContext from '@console/containers/side-bar/context' + +import sharedMessages from '@ttn-lw/lib/shared-messages' +import PropTypes from '@ttn-lw/lib/prop-types' + +import style from './search-button.styl' + +const SearchButton = ({ onClick, className }) => { + const ref = useRef(null) + const { isMinimized } = useContext(SidebarContext) + + const handleClick = useCallback(() => { + onClick() + }, [onClick]) + + return ( + + ) +} + +SearchButton.propTypes = { + className: PropTypes.string, + onClick: PropTypes.func.isRequired, +} + +SearchButton.defaultProps = { + className: undefined, +} + +export default SearchButton diff --git a/pkg/webui/components/sidebar/search-button/search-button.styl b/pkg/webui/components/sidebar/search-button/search-button.styl new file mode 100644 index 0000000000..a85444aa24 --- /dev/null +++ b/pkg/webui/components/sidebar/search-button/search-button.styl @@ -0,0 +1,71 @@ +// 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. +// 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. + +.search-button + font-size: $fs.m + color: $c.grey-500 + width: 100% + position: relative + display: flex + justify-content: space-between + align-items: center + padding: $cs.xxs $cs.xxs + gap: $cs.xxs + box-shadow: 0px 2px 7px 1px rgba(0, 0, 0, 0.06) + background: white + border: 1px solid $c.grey-200 + border-radius: $br.l + transition: border-color 0.08s linear, box-shadow 0.08s linear, color 0.08s linear + + + &:hover + border: 1px solid $c.grey-200 + color: $c.grey-700 + box-shadow: 0px 2px 7px 0px rgba(0, 0, 0, 0.08) + + &.is-minimized + justify-content: center + padding: $cs.xs $cs.xxs + + & > div p + display: none + + .backslash + display: none + + p + line-height: 1.4 + + .icon + font-size: $fs.xl + position: relative + top: 1px + + .backslash + margin: 0 + height: 1.65rem // 24px + width: 1.65rem // 24px + text-align: center + border-radius: $br.m + border: 1px solid $c.grey-200 + font-size: $fs.m + display: flex + justify-content: center + align-items: center + padding-bottom: 1px + + .fly-out-list + --dropdown-offset: calc(.75rem - 2px) + top: - $cs.xs + color: $c.grey-900 diff --git a/pkg/webui/components/sidebar/search-button/story.js b/pkg/webui/components/sidebar/search-button/story.js new file mode 100644 index 0000000000..fa123d050a --- /dev/null +++ b/pkg/webui/components/sidebar/search-button/story.js @@ -0,0 +1,40 @@ +// 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. +// 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 from 'react' + +import SidebarContext from '@console/containers/side-bar/context' + +import SearchButton from '.' + +export default { + title: 'Sidebar/SearchButton', + decorators: [ + storyFn => ( + {storyFn()} + ), + ], + parameters: { + design: { + type: 'figma', + url: 'https://www.figma.com/file/7pBLWK4tsjoAbyJq2viMAQ/2023-console-redesign?type=design&node-id=1345%3A11586&mode=design&t=Hbk2Qngeg1xqg4V3-1', + }, + }, +} + +export const Default = () => ( +
    + +
    +) diff --git a/pkg/webui/components/sidebar/section-label/index.js b/pkg/webui/components/sidebar/section-label/index.js new file mode 100644 index 0000000000..0fe80aa179 --- /dev/null +++ b/pkg/webui/components/sidebar/section-label/index.js @@ -0,0 +1,64 @@ +// 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. +// 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 from 'react' +import classnames from 'classnames' + +import Button from '@ttn-lw/components/button' + +import Message from '@ttn-lw/lib/components/message' + +import PropTypes from '@ttn-lw/lib/prop-types' + +const SectionLabel = ({ + label, + icon, + className, + onClick, + buttonDisabled, + 'data-test-id': dataTestId, +}) => ( +
    + +
    +) + +SectionLabel.propTypes = { + buttonDisabled: PropTypes.bool, + className: PropTypes.string, + 'data-test-id': PropTypes.string, + icon: PropTypes.string.isRequired, + label: PropTypes.oneOfType([PropTypes.node, PropTypes.message]).isRequired, + onClick: PropTypes.func.isRequired, +} + +SectionLabel.defaultProps = { + buttonDisabled: false, + className: undefined, + 'data-test-id': 'section-label', +} + +export default SectionLabel diff --git a/pkg/webui/components/sidebar/section-label/story.js b/pkg/webui/components/sidebar/section-label/story.js new file mode 100644 index 0000000000..689edaa8e8 --- /dev/null +++ b/pkg/webui/components/sidebar/section-label/story.js @@ -0,0 +1,34 @@ +// 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. +// 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 from 'react' + +import SectionLabel from '.' + +export default { + title: 'Sidebar/SectionLabel', + component: SectionLabel, + parameters: { + design: { + type: 'figma', + url: 'https://www.figma.com/file/7pBLWK4tsjoAbyJq2viMAQ/2023-console-redesign?type=design&node-id=1312%3A27341&mode=design&t=Hbk2Qngeg1xqg4V3-1', + }, + }, +} + +export const Default = () => ( +
    + +
    +) diff --git a/pkg/webui/components/sidebar/side-footer/index.js b/pkg/webui/components/sidebar/side-footer/index.js new file mode 100644 index 0000000000..76be31873a --- /dev/null +++ b/pkg/webui/components/sidebar/side-footer/index.js @@ -0,0 +1,153 @@ +// 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. +// 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, useContext, useRef } from 'react' +import classnames from 'classnames' + +import Button from '@ttn-lw/components/button' +import Dropdown from '@ttn-lw/components/dropdown' + +import { LanguageContext } from '@ttn-lw/lib/components/with-locale' + +import SidebarContext from '@console/containers/side-bar/context' + +import sharedMessages from '@ttn-lw/lib/shared-messages' +import PropTypes from '@ttn-lw/lib/prop-types' +import { + selectDocumentationUrlConfig, + selectPageStatusBaseUrlConfig, + selectSupportLinkConfig, +} from '@ttn-lw/lib/selectors/env' + +import style from './side-footer.styl' + +const supportLink = selectSupportLinkConfig() +const documentationBaseUrl = selectDocumentationUrlConfig() +const statusPageBaseUrl = selectPageStatusBaseUrlConfig() + +const LanguageOption = ({ locale, title, currentLocale, onSetLocale }) => { + const handleSetLocale = useCallback(() => { + onSetLocale(locale) + }, [locale, onSetLocale]) + + return +} + +LanguageOption.propTypes = { + currentLocale: PropTypes.string.isRequired, + locale: PropTypes.string.isRequired, + onSetLocale: PropTypes.func.isRequired, + title: PropTypes.string.isRequired, +} + +const SideFooter = () => { + const { isMinimized } = useContext(SidebarContext) + const supportButtonRef = useRef(null) + const clusterButtonRef = useRef(null) + + const clusterDropdownItems = ( + + ) + + const languageContext = useContext(LanguageContext) + const { locale, supportedLocales, setLocale } = languageContext || {} + + const handleSetLocale = useCallback( + locale => { + setLocale(locale) + }, + [setLocale], + ) + + const languageItems = supportedLocales + ? Object.keys(supportedLocales).map(l => ( + + )) + : null + + const supportDropdownItems = ( + <> + + + + {Boolean(languageContext) && ( + + )} + + ) + + const sideFooterClassnames = classnames( + style.sideFooter, + 'd-flex', + 'j-center', + 'al-center', + 'gap-cs-xs', + 'fs-s', + { [style.isMinimized]: isMinimized }, + ) + + return ( +
    + +
    + ) +} + +export default SideFooter diff --git a/pkg/webui/components/sidebar/side-footer/side-footer.styl b/pkg/webui/components/sidebar/side-footer/side-footer.styl new file mode 100644 index 0000000000..ca0eaa62f1 --- /dev/null +++ b/pkg/webui/components/sidebar/side-footer/side-footer.styl @@ -0,0 +1,54 @@ +// 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. +// 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. + +.side-footer + margin-top: auto + + .support-button + width: 100% + + button + width: 100% + + &-button + justify-content: space-between + + &.is-minimized + .cluster-button + display: none + + .side-footer-version + display: none + + .support-button > button + justify-content: center + + span:first-child + margin: 0 + + .side-footer-dropdown + width: calc(19rem - 2px) + min-width: initial + max-width: initial + + .side-footer-version + margin-left: $cs.xxs + + .support-button > button + width: 100% !important + justify-content: flex-start + + ul.side-footer-cluster-dropdown + left: initial + right: 0 diff --git a/pkg/webui/components/sidebar/side-footer/story.js b/pkg/webui/components/sidebar/side-footer/story.js new file mode 100644 index 0000000000..aa8bcb2695 --- /dev/null +++ b/pkg/webui/components/sidebar/side-footer/story.js @@ -0,0 +1,44 @@ +// 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. +// 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 from 'react' + +import SidebarContext from '@console/containers/side-bar/context' + +import Footer from '.' + +export default { + title: 'Sidebar/Footer', + component: Footer, + decorators: [ + storyFn => ( + {storyFn()} + ), + ], + parameters: { + design: { + type: 'figma', + url: 'https://www.figma.com/file/7pBLWK4tsjoAbyJq2viMAQ/2023-console-redesign?type=design&node-id=1345%3A11247&mode=design&t=Hbk2Qngeg1xqg4V3-1', + }, + }, +} + +export const Default = () => ( +
    +
    +
    +) diff --git a/pkg/webui/components/sidebar/side-header/index.js b/pkg/webui/components/sidebar/side-header/index.js new file mode 100644 index 0000000000..b3c225e11f --- /dev/null +++ b/pkg/webui/components/sidebar/side-header/index.js @@ -0,0 +1,68 @@ +// 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. +// 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, { useContext } from 'react' +import classnames from 'classnames' +import { Link } from 'react-router-dom' + +import Button from '@ttn-lw/components/button' + +import SidebarContext from '@console/containers/side-bar/context' + +import PropTypes from '@ttn-lw/lib/prop-types' + +import style from './side-header.styl' + +const SideHeader = ({ logo, miniLogo }) => { + const { onMinimizeToggle, isMinimized, onDrawerCloseClick } = useContext(SidebarContext) + + return ( +
    + {/* Render two logos to prevent layout flashes when switching between minimized and maximized states. */} + + + + +
    + ) +} + +SideHeader.propTypes = { + logo: PropTypes.shape({ + src: PropTypes.string.isRequired, + alt: PropTypes.string.isRequired, + }).isRequired, + miniLogo: PropTypes.shape({ + src: PropTypes.string.isRequired, + alt: PropTypes.string.isRequired, + }).isRequired, +} + +export default SideHeader diff --git a/pkg/webui/components/sidebar/side-header/side-header.styl b/pkg/webui/components/sidebar/side-header/side-header.styl new file mode 100644 index 0000000000..08ac433d96 --- /dev/null +++ b/pkg/webui/components/sidebar/side-header/side-header.styl @@ -0,0 +1,45 @@ +// 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. +// 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. + +.header-container + display: flex + justify-content: space-between + align-items: center + gap: $cs.m + + &.is-minimized + flex-direction: column + gap: $cs.xs + + .logo + display: none + + .mini-logo + display: flex + + .minimize-button + transition: none + + &:not(.is-minimized) + .minimize-button + margin-top: - $cs.xxs + margin-right: - $cs.s + + .logo + width: 9.57rem + + .mini-logo + display: none + width: 2.8rem + padding: $cs.s 0 diff --git a/pkg/webui/components/sidebar/side-menu/index.js b/pkg/webui/components/sidebar/side-menu/index.js new file mode 100644 index 0000000000..4b895e3948 --- /dev/null +++ b/pkg/webui/components/sidebar/side-menu/index.js @@ -0,0 +1,48 @@ +// 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. +// 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, { useRef } from 'react' +import classnames from 'classnames' + +import PropTypes from '@ttn-lw/lib/prop-types' + +import SideNavigationList from './list' +import SideNavigationItem from './item' + +import style from './side.styl' + +const SideNavigation = ({ className, children }) => { + const node = useRef() + + const navigationClassNames = classnames(className, style.navigation) + + return ( + + ) +} + +SideNavigation.propTypes = { + children: PropTypes.node.isRequired, + className: PropTypes.string, +} + +SideNavigation.defaultProps = { + className: undefined, +} + +SideNavigation.Item = SideNavigationItem + +export default SideNavigation diff --git a/pkg/webui/components/sidebar/side-menu/item/collapsible.js b/pkg/webui/components/sidebar/side-menu/item/collapsible.js new file mode 100644 index 0000000000..33d192e770 --- /dev/null +++ b/pkg/webui/components/sidebar/side-menu/item/collapsible.js @@ -0,0 +1,108 @@ +// 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. +// 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, { useRef } from 'react' +import classnames from 'classnames' + +import Dropdown from '@ttn-lw/components/dropdown' +import Button from '@ttn-lw/components/button' +import Icon from '@ttn-lw/components/icon' + +import Message from '@ttn-lw/lib/components/message' + +import PropTypes from '@ttn-lw/lib/prop-types' + +import SideNavigationList from '../list' + +import style from './item.styl' + +const CollapsibleItem = ({ + children, + onClick, + isExpanded, + isMinimized, + title, + icon, + depth, + onDropdownItemsClick, + currentPathName, +}) => { + const ref = useRef() + const subItems = React.Children.toArray(children) + .filter(item => React.isValidElement(item) && item.props) + .map(item => ({ + title: item.props.title, + path: item.props.path, + icon: item.props.icon, + })) + + const subItemActive = subItems.some(item => currentPathName.includes(item.path)) + + return ( +
    + + {isMinimized && ( + + + {subItems.map(item => ( + + ))} + + )} + + {children} + +
    + ) +} + +CollapsibleItem.propTypes = { + children: PropTypes.node, + currentPathName: PropTypes.string.isRequired, + depth: PropTypes.number.isRequired, + icon: PropTypes.string, + isExpanded: PropTypes.bool.isRequired, + isMinimized: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, + onDropdownItemsClick: PropTypes.func, + title: PropTypes.message.isRequired, +} + +CollapsibleItem.defaultProps = { + children: undefined, + icon: undefined, + onDropdownItemsClick: () => null, +} + +export default CollapsibleItem diff --git a/pkg/webui/components/navigation/side/item/index.js b/pkg/webui/components/sidebar/side-menu/item/index.js similarity index 50% rename from pkg/webui/components/navigation/side/item/index.js rename to pkg/webui/components/sidebar/side-menu/item/index.js index 82b9bda801..b9b7768230 100644 --- a/pkg/webui/components/navigation/side/item/index.js +++ b/pkg/webui/components/sidebar/side-menu/item/index.js @@ -12,19 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { useCallback, useContext, useEffect, useState } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import classnames from 'classnames' -import Dropdown from '@ttn-lw/components/dropdown' -import Icon from '@ttn-lw/components/icon' - -import Message from '@ttn-lw/lib/components/message' - import PropTypes from '@ttn-lw/lib/prop-types' -import SideNavigationList from '../list' -import NavigationLink from '../../link' -import SideNavigationContext from '../context' +import MenuLink from './link' +import CollapsibleItem from './collapsible' import style from './item.styl' @@ -35,8 +29,7 @@ const handleItemClick = event => { } const SideNavigationItem = props => { - const { className, children, title, depth, icon, path, exact, isActive } = props - const { isMinimized, onLeafItemClick } = useContext(SideNavigationContext) + const { className, children, title, depth, icon, path, exact, isActive, isMinimized } = props const [isExpanded, setIsExpanded] = useState(false) const handleExpandCollapsableItem = useCallback(() => { @@ -53,7 +46,7 @@ const SideNavigationItem = props => { [], ) for (const path of paths) { - if (location.pathname.startsWith(path)) { + if (location.pathname.includes(path)) { setIsExpanded(true) return } @@ -62,13 +55,9 @@ const SideNavigationItem = props => { }, [children]) return ( -
  • +
  • {Boolean(children) ? ( - { /> ) : ( { - const subItems = children - .filter(item => Boolean(item) && 'props' in item) - .map(item => ({ - title: item.props.title, - path: item.props.path, - icon: item.props.icon, - })) - - const subItemActive = subItems.some(item => item.path === currentPathName) - - return ( - <> - - - - {subItems.map(item => ( - - ))} - - - {children} - - - ) -} - -CollapsableItem.propTypes = { - children: PropTypes.node, - currentPathName: PropTypes.string.isRequired, - depth: PropTypes.number.isRequired, - icon: PropTypes.string, - isActive: PropTypes.bool.isRequired, - isExpanded: PropTypes.bool.isRequired, - onClick: PropTypes.func.isRequired, - onDropdownItemsClick: PropTypes.func, - title: PropTypes.message.isRequired, -} - -CollapsableItem.defaultProps = { - children: undefined, - icon: undefined, - onDropdownItemsClick: () => null, -} - -const LinkItem = ({ onClick, title, icon, exact, path, onDropdownItemsClick }) => { - const handleLinkItemClick = React.useCallback( +const LinkItem = ({ onClick, title, icon, exact, path }) => { + const handleLinkItemClick = useCallback( event => { document.activeElement.blur() onClick(event) @@ -202,21 +121,7 @@ const LinkItem = ({ onClick, title, icon, exact, path, onDropdownItemsClick }) = ) return ( - <> - - {icon && } - - - - - - + ) } @@ -224,7 +129,6 @@ LinkItem.propTypes = { exact: PropTypes.bool.isRequired, icon: PropTypes.string, onClick: PropTypes.func, - onDropdownItemsClick: PropTypes.func, path: PropTypes.string, title: PropTypes.message.isRequired, } @@ -233,7 +137,6 @@ LinkItem.defaultProps = { icon: undefined, path: undefined, onClick: () => null, - onDropdownItemsClick: () => null, } export default SideNavigationItem diff --git a/pkg/webui/components/sidebar/side-menu/item/item.styl b/pkg/webui/components/sidebar/side-menu/item/item.styl new file mode 100644 index 0000000000..01ca8f5eb6 --- /dev/null +++ b/pkg/webui/components/sidebar/side-menu/item/item.styl @@ -0,0 +1,83 @@ +// Copyright © 2024 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 + position: relative + + &.is-minimized, &.is-minimized .link + justify-content: center + + .title + display: none + + .expand-icon + display: none + + .icon + transition: color 50ms linear + color: $c.grey-500 + + .sub-item-list + display: none + +.message + vertical-align: middle + +.expand-icon + position: absolute + right: $cs.xs + top: 50% + transform: translateY(-50%) + transition: transform $ad.m ease-in-out + + &-open + transform: translateY(-50%) rotate(180deg) + +.link + transition: color 50ms linear + border-radius: $br.m + text-decoration: none + color: $c.grey-700 + display: flex + align-items: center + gap: $cs.xs + padding: $cs.xs + position: relative + box-sizing: border-box + width: 100% + padding-left: $cs.xs + + &.active + background: $c.tts-primary-150 + color: $c.grey-900 + + .icon + color: inherit + + &.disabled + color: $c.grey-400 + + .icon + color: inherit + + &:hover + color: $c.grey-900 + + .icon + color: inherit + +.fly-out-list + --dropdown-offset: $cs.xs + top: - $cs.xs + z-index: $zi.dropdown diff --git a/pkg/webui/components/sidebar/side-menu/item/link.js b/pkg/webui/components/sidebar/side-menu/item/link.js new file mode 100644 index 0000000000..4723dc8348 --- /dev/null +++ b/pkg/webui/components/sidebar/side-menu/item/link.js @@ -0,0 +1,72 @@ +// 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. +// 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, useContext, useRef } from 'react' +import { NavLink } from 'react-router-dom' +import classnames from 'classnames' + +import Icon from '@ttn-lw/components/icon' +import Dropdown from '@ttn-lw/components/dropdown' + +import Message from '@ttn-lw/lib/components/message' + +import SidebarContext from '@console/containers/side-bar/context' + +import PropTypes from '@ttn-lw/lib/prop-types' + +import style from './item.styl' + +const MenuLink = ({ icon, title, path, onClick, exact, disabled }) => { + const ref = useRef() + const { isMinimized } = useContext(SidebarContext) + + const className = useCallback( + ({ isActive }) => + classnames(style.link, style.container, { + [style.active]: isActive, + [style.disabled]: disabled, + [style.isMinimized]: isMinimized, + }), + [disabled, isMinimized], + ) + + return ( + + {icon && }{' '} + + {isMinimized && ( + + + + )} + + ) +} + +MenuLink.propTypes = { + disabled: PropTypes.bool, + exact: PropTypes.bool.isRequired, + icon: PropTypes.string, + onClick: PropTypes.func, + path: PropTypes.string.isRequired, + title: PropTypes.message.isRequired, +} + +MenuLink.defaultProps = { + icon: undefined, + onClick: () => null, + disabled: false, +} + +export default MenuLink diff --git a/pkg/webui/components/sidebar/side-menu/item/story.js b/pkg/webui/components/sidebar/side-menu/item/story.js new file mode 100644 index 0000000000..32c0b185b3 --- /dev/null +++ b/pkg/webui/components/sidebar/side-menu/item/story.js @@ -0,0 +1,40 @@ +// 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. +// 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 from 'react' + +import SidebarContext from '@console/containers/side-bar/context' + +import Link from './link' + +export default { + title: 'Side Menu Link', + decorators: [ + storyFn => ( + {storyFn()} + ), + ], + parameters: { + design: { + type: 'figma', + url: 'https://www.figma.com/file/7pBLWK4tsjoAbyJq2viMAQ/2023-console-redesign?type=design&node-id=1293%3A7435&mode=design&t=Hbk2Qngeg1xqg4V3-1', + }, + }, +} + +export const Default = () => ( +
    + +
    +) diff --git a/pkg/webui/components/navigation/side/list/index.js b/pkg/webui/components/sidebar/side-menu/list/index.js similarity index 99% rename from pkg/webui/components/navigation/side/list/index.js rename to pkg/webui/components/sidebar/side-menu/list/index.js index 4640991c93..4561c58e4c 100644 --- a/pkg/webui/components/navigation/side/list/index.js +++ b/pkg/webui/components/sidebar/side-menu/list/index.js @@ -25,6 +25,7 @@ const SideNavigationList = ({ children, className, depth, isExpanded }) => { [style.listNested]: !isRoot, [style.listExpanded]: isExpanded, }) + return
      {children}
    } diff --git a/pkg/webui/components/navigation/side/list/list.styl b/pkg/webui/components/sidebar/side-menu/list/list.styl similarity index 70% rename from pkg/webui/components/navigation/side/list/list.styl rename to pkg/webui/components/sidebar/side-menu/list/list.styl index 8b850437bb..c4af86598d 100644 --- a/pkg/webui/components/navigation/side/list/list.styl +++ b/pkg/webui/components/sidebar/side-menu/list/list.styl @@ -16,30 +16,29 @@ list-style-type: none padding: 0 margin: 0 - overflow: hidden + overflow: visible + font-size: $fs.m & > li:not(:last-child) margin-bottom: 0 - // We need to rely on the child list items for the reveal animation, the - // height of the list is variable, making proper slide up transitions - // impossible. - & > li + &-nested > li + height: 0px transition: height $ad.m ease-in-out - &-nested > li - height: 0 - transition: visibility $ad.m, height $ad.m ease-in-out + &-expanded > li + height: 2.4rem - &-nested:not(.list-expanded) > li - visibility: hidden + &-nested + transition: opacity $ad.m ease-in-out - &-expanded > li - height: 3.1rem + &:not(.list-expanded) + opacity: 0 &-expanded, &-nested + opacity: 1 & li a - padding-left: $cs.l + $cs.l + padding-left: $cs.l + $cs.xs .icon margin-right: $cs.xs diff --git a/pkg/webui/components/navigation/side/side.styl b/pkg/webui/components/sidebar/side-menu/side.styl similarity index 55% rename from pkg/webui/components/navigation/side/side.styl rename to pkg/webui/components/sidebar/side-menu/side.styl index 5d1e60a491..05670b0362 100644 --- a/pkg/webui/components/navigation/side/side.styl +++ b/pkg/webui/components/sidebar/side-menu/side.styl @@ -12,14 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -$minimize-button-height = 5rem - -// This style is to be applied to the portalled container. .container border-normal(right) display: flex flex-direction: column - background-color: white z-index: $zi.nav +media-query-min($bp.s) left: 0 @@ -34,50 +30,10 @@ $minimize-button-height = 5rem +media-query-height(500px) position: static - .minimize-button - reset-button() - transition-color() - border-normal(top) - box-sizing: border-box - color: $tc-subtle-gray - background-color: white - position: sticky - top: "calc(100vh - %s)" % $minimize-button-height - width: 100% - height: $minimize-button-height - margin-bottom: 1px - flex-shrink: 0 - &:not(.minimize-button-minimized) - padding-left: $cs.l - text-align: left - &-minimized - padding-left: 0 - text-align: center - +media-query-min($bp.s) - display: block - +media-query($bp.s) - display: none - &:hover - color: $tc-deep-gray - span:first-child - nudge('up') - - .navigation - sidebar-transition(width) - position: sticky - top: 0 - background-color: white box-sizing: border-box max-height: 100% - +media-query-min($bp.s) - padding-bottom: $minimize-button-height - width: $sidebar-width - overflow-y: auto - overflow-x: hidden - +media-query($bp.s) - height: $sidebar-mobile-menu-height - width: 100% + width: 100% &-minimized overflow: visible @@ -107,36 +63,6 @@ $minimize-button-height = 5rem .drawer overflow: visible -.header - border-normal(bottom) - sidebar-transition(padding) - height: 6rem - color: $tc-deep-gray - box-sizing: border-box - display: flex - align-items: center - padding: $cs.xl $cs.l - font-weight: $fw.bold - -.mobile-header - align-items: center - +media-query-min($bp.s) - display: none - +media-query($bp.s) - border-normal(bottom) - display: flex - padding: 0 $cs.s - height: $sidebar-mobile-menu-height - - .expand-icon - margin-right: $cs.s - font-size: 2rem - color: $tc-subtle-gray - -.icon - width: 2rem - margin-right: $cs.m - .drawer background-color: white z-index: $zi.nav diff --git a/pkg/webui/components/sidebar/side-menu/story.js b/pkg/webui/components/sidebar/side-menu/story.js new file mode 100644 index 0000000000..baf5acf7cd --- /dev/null +++ b/pkg/webui/components/sidebar/side-menu/story.js @@ -0,0 +1,57 @@ +// Copyright © 2019 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 from 'react' + +import SidebarContext from '@console/containers/side-bar/context' + +import SideNavigationItem from './item' + +import SideNavigation from '.' + +export default { + title: 'Navigation v2', + component: SideNavigation, + decorators: [ + storyFn => ( + {storyFn()} + ), + ], +} + +export const _SideNavigation = () => ( +
    + + + + + + + + + + + + + + + + + +
    +) diff --git a/pkg/webui/components/sidebar/switcher/index.js b/pkg/webui/components/sidebar/switcher/index.js new file mode 100644 index 0000000000..95c6ff6bf7 --- /dev/null +++ b/pkg/webui/components/sidebar/switcher/index.js @@ -0,0 +1,107 @@ +// 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. +// 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, useRef } from 'react' +import { NavLink, useLocation } from 'react-router-dom' +import classnames from 'classnames' + +import Icon from '@ttn-lw/components/icon' +import Dropdown from '@ttn-lw/components/dropdown' + +import Message from '@ttn-lw/lib/components/message' + +import sharedMessages from '@ttn-lw/lib/shared-messages' +import PropTypes from '@ttn-lw/lib/prop-types' + +import style from './switcher.styl' + +const Switcher = ({ isMinimized }) => { + const overviewRef = useRef(null) + const applicationsRef = useRef(null) + const gatewaysRef = useRef(null) + const { pathname } = useLocation() + + const getNavLinkClass = useCallback( + ({ isActive }) => + classnames(style.link, { + [style.active]: isActive, + }), + [], + ) + + const getOverviewNavLinkClass = classnames(style.link, { + [style.active]: !pathname.startsWith('/applications') && !pathname.startsWith('/gateways'), + }) + + return ( +
    + + + + {isMinimized && ( + + + + )} + + + + + {isMinimized && ( + + + + )} + + + + + {isMinimized && ( + + + + )} + +
    + ) +} + +Switcher.propTypes = { + isMinimized: PropTypes.bool, +} + +Switcher.defaultProps = { + isMinimized: false, +} + +export default Switcher diff --git a/pkg/webui/components/sidebar/switcher/story.js b/pkg/webui/components/sidebar/switcher/story.js new file mode 100644 index 0000000000..3507c2f87d --- /dev/null +++ b/pkg/webui/components/sidebar/switcher/story.js @@ -0,0 +1,34 @@ +// 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. +// 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 from 'react' + +import Switcher from '.' + +export default { + title: 'Sidebar/Switcher', + component: Switcher, + parameters: { + design: { + type: 'figma', + url: 'https://www.figma.com/file/7pBLWK4tsjoAbyJq2viMAQ/2023-console-redesign?type=design&node-id=1312%3A7620&mode=design&t=Hbk2Qngeg1xqg4V3-1', + }, + }, +} + +export const Default = () => ( +
    + +
    +) diff --git a/pkg/webui/components/sidebar/switcher/switcher.styl b/pkg/webui/components/sidebar/switcher/switcher.styl new file mode 100644 index 0000000000..6631298bb0 --- /dev/null +++ b/pkg/webui/components/sidebar/switcher/switcher.styl @@ -0,0 +1,71 @@ +// 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. +// 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. + +.switcher-container + border-radius: $br.l + border: 1px solid $c.grey-200 + background: white + box-shadow: 0px 2px 7px 1px rgba(0, 0, 0, 0.06) + display: flex + gap: $cs.xxs + justify-content: center + padding: $cs.xxs + + .link + border-radius: $br.m + text-decoration: none + color: $c.grey-700 + display: flex + justify-content: center + width: 100% + transition: background 80ms ease-in-out + position: relative + padding: $cs.s $cs.xxs + + &.active + background: $c.grey-900 + color: $c.white + font-weight: $fw.bold + + &:visited + text-decoration: none + &:not(.active) + color: $c.grey-700 + + &:hover + text-decoration: none + &:not(.active) + background: $c.grey-100 + + &.is-minimized + flex-direction: column + + .link + padding: $cs.xs 0 + + .caption + display: none + + &:not(.is-minimized) + .icon + display: none + + .icon + font-size: $fs.xl + font-weight: 300 + + .fly-out-list + --dropdown-offset: calc(1rem - 2px) + top: - $cs.xs + color: $c.grey-900 diff --git a/pkg/webui/console/components/gateway-api-keys-modal/index.js b/pkg/webui/console/components/gateway-api-keys-modal/index.js index df1ebfc799..e941a43576 100644 --- a/pkg/webui/console/components/gateway-api-keys-modal/index.js +++ b/pkg/webui/console/components/gateway-api-keys-modal/index.js @@ -50,7 +50,13 @@ const GatewayApiKeysModal = ({ > {lnsKey && ( -
    + icon={action.icon} + message={action.message} + secondary + /> )} {hasSupportLink && !isNotFound && ( <> - - - - + className={style.actionButton} + icon="contact_support" + message={sharedMessages.getSupport} + secondary + /> {hasErrorDetails && ( )} @@ -276,19 +277,17 @@ const FullViewErrorInner = ({ error, safe, action, unexpected }) => {
    {errorDetails}
    - + icon={copied ? 'done' : 'file_copy'} + message={ + copied ? sharedMessages.copiedToClipboard : sharedMessages.copyToClipboard + } + secondary + /> )} diff --git a/pkg/webui/lib/shared-messages.js b/pkg/webui/lib/shared-messages.js index b8578d3e27..58b2fb48da 100644 --- a/pkg/webui/lib/shared-messages.js +++ b/pkg/webui/lib/shared-messages.js @@ -54,6 +54,7 @@ export default defineMessages({ apiKeyNamePlaceholder: 'My new API key', apiKeys: 'API keys', appData: 'Application data', + appOverview: 'Application overview', appEUI: 'AppEUI', appEUIDescription: 'The AppEUI uniquely identifies the owner of the end device. If no AppEUI is provided by the device manufacturer (usually for development), it can be filled with zeros.', @@ -149,6 +150,7 @@ export default defineMessages({ createdAt: 'Created at', currentCollaborators: 'Current collaborators', currentUserIndicator: '(This is you)', + dashboard: 'Dashboard', data: 'Data', defineRights: 'Define rights', delayWarning: @@ -197,6 +199,7 @@ export default defineMessages({ downlinkReplace: 'Downlink replace', downlinkSent: 'Downlink sent', downlinksScheduled: 'Downlinks (re)scheduled', + endDeviceOverview: 'End device overview', edit: 'Edit', editWebhook: 'Edit webhook', email: 'Email', @@ -248,6 +251,7 @@ export default defineMessages({ 'Without choosing a frequency plan, packets from the gateway will not be correctly processed', furtherResources: 'Further resources', gateway: 'Gateway', + gatewayOverview: 'Gateway overview', gatewayDescDescription: 'Optional gateway description; can also be used to save notes about the gateway', gatewayDescPlaceholder: 'Description for my new gateway', @@ -294,6 +298,7 @@ export default defineMessages({ key: 'key', keyEdit: 'Edit API key', keyId: 'Key ID', + language: 'Language', lastSeen: 'Last activity', latitude: 'Latitude', latitudeDesc: 'The north-south position in degrees, where 0 is the equator', @@ -302,6 +307,7 @@ export default defineMessages({ 'The Authentication Key for Lora Basics Station LNS connections. This field is ignored for other gateways.', link: 'Link', linked: 'Linked', + list: 'List', liveData: 'Live data', location: 'Location', locationDescription: @@ -365,6 +371,7 @@ export default defineMessages({ normalizedPayloadWind: 'Wind', notAvailable: 'n/a', notLinked: 'Not linked', + notifications: 'Notifications', notSet: 'Not set', nsEmptyDefault: 'Leave empty to link to the Network Server in the same cluster', nsServerKekLabel: 'Network Server KEK label', @@ -480,6 +487,7 @@ export default defineMessages({ statusUnknown: 'Status unknown', success: 'Success', suggestions: 'Suggestions', + support: 'Support', supportsClassB: 'Supports class B', supportsClassC: 'Supports class C', takeMeBack: 'Take me back', @@ -490,6 +498,9 @@ export default defineMessages({ token: 'Token', tokenDelete: 'Token delete', tokenDeleted: 'Token deleted', + topApplications: 'Top applications', + topGateways: 'Top gateways', + topEntities: 'Top entities', tokenSet: 'Set token', tokenUpdated: 'Token updated', traffic: 'Traffic', diff --git a/pkg/webui/locales/en.json b/pkg/webui/locales/en.json index 8f4e19e56c..07963e3ffa 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -145,8 +145,6 @@ "components.key-value-map.index.addEntry": "Add entry", "components.link.index.glossaryTitle": "See \"{term}\" in the glossary", "components.link.index.defaultGlossaryTitle": "See in the glossary", - "components.mobile-menu.index.loggedInAs": "Logged in as {userId}", - "components.navigation.side.index.hideSidebar": "Hide sidebar", "components.notification.details.index.showDetails": "Show details", "components.notification.details.index.details": "Details", "components.notification.details.index.errorDetails": "Error details", @@ -588,6 +586,8 @@ "console.containers.packet-broker-networks-table.index.homeNetworkPolicy": "Our routing policy for them", "console.containers.pubsub-formats-select.index.warning": "Pub/Sub formats unavailable", "console.containers.pubsubs-table.index.host": "Server host", + "console.containers.side-bar.navigation.app-side-navigation.buttonMessage": "Back to Applications list", + "console.containers.side-bar.navigation.gtw-side-navigation.buttonMessage": "Back to Gateways list", "console.containers.user-data-form.edit.deleteWarning": "This will PERMANENTLY DELETE THIS ACCOUNT and LOCK THE USER ID AND EMAIL FOR RE-REGISTRATION. Associated entities (e.g. gateways, applications and end devices) owned by this user that do not have any other collaborators will become UNACCESSIBLE and it will NOT BE POSSIBLE TO REGISTER ENTITIES WITH THE SAME ID OR EUI's AGAIN. Make sure you assign new collaborators to such entities if you plan to continue using them.", "console.containers.user-data-form.edit.purgeWarning": "This will PERMANENTLY DELETE THIS ACCOUNT. Associated entities (e.g. gateways, applications and end devices) owned by this user that do not have any other collaborators will become UNACCESSIBLE and it will NOT BE POSSIBLE TO REGISTER ENTITIES WITH THE SAME ID OR EUI's AGAIN. Make sure you assign new collaborators to such entities if you plan to continue using them.", "console.containers.user-data-form.edit.deleteConfirmMessage": "Please type in this user's user ID to confirm.", @@ -1013,6 +1013,7 @@ "lib.shared-messages.apiKeyNamePlaceholder": "My new API key", "lib.shared-messages.apiKeys": "API keys", "lib.shared-messages.appData": "Application data", + "lib.shared-messages.appOverview": "Application overview", "lib.shared-messages.appEUI": "AppEUI", "lib.shared-messages.appEUIDescription": "The AppEUI uniquely identifies the owner of the end device. If no AppEUI is provided by the device manufacturer (usually for development), it can be filled with zeros.", "lib.shared-messages.appEUIJoinEUI": "AppEUI/JoinEUI", @@ -1099,6 +1100,7 @@ "lib.shared-messages.createdAt": "Created at", "lib.shared-messages.currentCollaborators": "Current collaborators", "lib.shared-messages.currentUserIndicator": "(This is you)", + "lib.shared-messages.dashboard": "Dashboard", "lib.shared-messages.data": "Data", "lib.shared-messages.defineRights": "Define rights", "lib.shared-messages.delayWarning": "Delay too short. The lower bound ({minimumValue}ms) will be used by the Gateway Server.", @@ -1141,6 +1143,7 @@ "lib.shared-messages.downlinkReplace": "Downlink replace", "lib.shared-messages.downlinkSent": "Downlink sent", "lib.shared-messages.downlinksScheduled": "Downlinks (re)scheduled", + "lib.shared-messages.endDeviceOverview": "End device overview", "lib.shared-messages.edit": "Edit", "lib.shared-messages.editWebhook": "Edit webhook", "lib.shared-messages.email": "Email", @@ -1187,6 +1190,7 @@ "lib.shared-messages.frequencyPlanWarning": "Without choosing a frequency plan, packets from the gateway will not be correctly processed", "lib.shared-messages.furtherResources": "Further resources", "lib.shared-messages.gateway": "Gateway", + "lib.shared-messages.gatewayOverview": "Gateway overview", "lib.shared-messages.gatewayDescDescription": "Optional gateway description; can also be used to save notes about the gateway", "lib.shared-messages.gatewayDescPlaceholder": "Description for my new gateway", "lib.shared-messages.gatewayDescription": "Gateway description", @@ -1232,6 +1236,7 @@ "lib.shared-messages.key": "key", "lib.shared-messages.keyEdit": "Edit API key", "lib.shared-messages.keyId": "Key ID", + "lib.shared-messages.language": "Language", "lib.shared-messages.lastSeen": "Last activity", "lib.shared-messages.latitude": "Latitude", "lib.shared-messages.latitudeDesc": "The north-south position in degrees, where 0 is the equator", @@ -1239,6 +1244,7 @@ "lib.shared-messages.lbsLNSSecretDescription": "The Authentication Key for Lora Basics Station LNS connections. This field is ignored for other gateways.", "lib.shared-messages.link": "Link", "lib.shared-messages.linked": "Linked", + "lib.shared-messages.list": "List", "lib.shared-messages.liveData": "Live data", "lib.shared-messages.location": "Location", "lib.shared-messages.locationDescription": "When set to public, the gateway location may be visible to other users of the network", @@ -1297,6 +1303,7 @@ "lib.shared-messages.normalizedPayloadWind": "Wind", "lib.shared-messages.notAvailable": "n/a", "lib.shared-messages.notLinked": "Not linked", + "lib.shared-messages.notifications": "Notifications", "lib.shared-messages.notSet": "Not set", "lib.shared-messages.nsEmptyDefault": "Leave empty to link to the Network Server in the same cluster", "lib.shared-messages.nsServerKekLabel": "Network Server KEK label", @@ -1407,6 +1414,7 @@ "lib.shared-messages.statusUnknown": "Status unknown", "lib.shared-messages.success": "Success", "lib.shared-messages.suggestions": "Suggestions", + "lib.shared-messages.support": "Support", "lib.shared-messages.supportsClassB": "Supports class B", "lib.shared-messages.supportsClassC": "Supports class C", "lib.shared-messages.takeMeBack": "Take me back", @@ -1417,6 +1425,9 @@ "lib.shared-messages.token": "Token", "lib.shared-messages.tokenDelete": "Token delete", "lib.shared-messages.tokenDeleted": "Token deleted", + "lib.shared-messages.topApplications": "Top applications", + "lib.shared-messages.topGateways": "Top gateways", + "lib.shared-messages.topEntities": "Top entities", "lib.shared-messages.tokenSet": "Set token", "lib.shared-messages.tokenUpdated": "Token updated", "lib.shared-messages.traffic": "Traffic", diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index 2f9f10b27d..1ca2b73a32 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -145,8 +145,6 @@ "components.key-value-map.index.addEntry": "エントリー追加", "components.link.index.glossaryTitle": "用語集「{term}」を参照してください", "components.link.index.defaultGlossaryTitle": "用語集を見る", - "components.mobile-menu.index.loggedInAs": "{userId}でログインしました", - "components.navigation.side.index.hideSidebar": "サイドバーを隠す", "components.notification.details.index.showDetails": "詳細を表示", "components.notification.details.index.details": "詳細", "components.notification.details.index.errorDetails": "エラー詳細", @@ -588,6 +586,8 @@ "console.containers.packet-broker-networks-table.index.homeNetworkPolicy": "それらに対する当社のルーティングポリシー", "console.containers.pubsub-formats-select.index.warning": "Pub/Subフォーマットが無効です", "console.containers.pubsubs-table.index.host": "サーバーホスト", + "console.containers.side-bar.navigation.app-side-navigation.buttonMessage": "", + "console.containers.side-bar.navigation.gtw-side-navigation.buttonMessage": "", "console.containers.user-data-form.edit.deleteWarning": "", "console.containers.user-data-form.edit.purgeWarning": "", "console.containers.user-data-form.edit.deleteConfirmMessage": "", @@ -1013,6 +1013,7 @@ "lib.shared-messages.apiKeyNamePlaceholder": "私の新しいAPIキー", "lib.shared-messages.apiKeys": "APIキー", "lib.shared-messages.appData": "", + "lib.shared-messages.appOverview": "", "lib.shared-messages.appEUI": "AppEUI", "lib.shared-messages.appEUIDescription": "AppEUI はエンドデバイスの所有者を一意に識別します。デバイスメーカー(通常は開発用)がAppEUIを提供していない場合は、ゼロで埋めることができます", "lib.shared-messages.appEUIJoinEUI": "AppEUI/JoinEUI", @@ -1099,6 +1100,7 @@ "lib.shared-messages.createdAt": "で作成しました。", "lib.shared-messages.currentCollaborators": "現在のコラボレーター", "lib.shared-messages.currentUserIndicator": "(これはあなたです)", + "lib.shared-messages.dashboard": "", "lib.shared-messages.data": "データ", "lib.shared-messages.defineRights": "権利の定義", "lib.shared-messages.delayWarning": "遅延が短すぎます。ゲートウェイサーバーは下限値 ({minimumValue}ms) を使用します", @@ -1141,6 +1143,7 @@ "lib.shared-messages.downlinkReplace": "ダウンリンク交換", "lib.shared-messages.downlinkSent": "ダウンリンク送信", "lib.shared-messages.downlinksScheduled": "ダウンリンク(再)予定", + "lib.shared-messages.endDeviceOverview": "", "lib.shared-messages.edit": "編集", "lib.shared-messages.editWebhook": "Webhookの編集", "lib.shared-messages.email": "Email", @@ -1187,6 +1190,7 @@ "lib.shared-messages.frequencyPlanWarning": "周波数プランを選択しないと、ゲートウェイからのパケットは正しく処理されません", "lib.shared-messages.furtherResources": "その他のリソース", "lib.shared-messages.gateway": "ゲートウェイ", + "lib.shared-messages.gatewayOverview": "", "lib.shared-messages.gatewayDescDescription": "オプションのゲートウェイの説明;ゲートウェイについてのメモを保存するために使用することもできます", "lib.shared-messages.gatewayDescPlaceholder": "新しいゲートウェイの説明", "lib.shared-messages.gatewayDescription": "ゲートウェイの説明", @@ -1232,6 +1236,7 @@ "lib.shared-messages.key": "キー", "lib.shared-messages.keyEdit": "APIキーの編集", "lib.shared-messages.keyId": "キーID", + "lib.shared-messages.language": "", "lib.shared-messages.lastSeen": "最後に閲覧", "lib.shared-messages.latitude": "緯度", "lib.shared-messages.latitudeDesc": "南北の位置を度数で表したもので、0は赤道です", @@ -1239,6 +1244,7 @@ "lib.shared-messages.lbsLNSSecretDescription": "LoRa Basics Station LNS 接続のための認証キーです。このフィールドは、他のゲートウェイでは無視されます", "lib.shared-messages.link": "リンク", "lib.shared-messages.linked": "リンク済", + "lib.shared-messages.list": "", "lib.shared-messages.liveData": "ライブデータ", "lib.shared-messages.location": "場所", "lib.shared-messages.locationDescription": "公開に設定すると、ゲートウェイの位置がネットワークの他のユーザーから見えることがあります", @@ -1297,6 +1303,7 @@ "lib.shared-messages.normalizedPayloadWind": "風", "lib.shared-messages.notAvailable": "n/a", "lib.shared-messages.notLinked": "リンクされていません", + "lib.shared-messages.notifications": "", "lib.shared-messages.notSet": "未設定", "lib.shared-messages.nsEmptyDefault": "同じクラスタ内のネットワークサーバーにリンクするには空のままにします", "lib.shared-messages.nsServerKekLabel": "ネットワークサーバーKEKラベル", @@ -1407,6 +1414,7 @@ "lib.shared-messages.statusUnknown": "ステータス不明", "lib.shared-messages.success": "成功", "lib.shared-messages.suggestions": "", + "lib.shared-messages.support": "", "lib.shared-messages.supportsClassB": "クラスBをサポート", "lib.shared-messages.supportsClassC": "クラスCをサポート", "lib.shared-messages.takeMeBack": "私を連れ戻して", @@ -1417,6 +1425,9 @@ "lib.shared-messages.token": "トークン", "lib.shared-messages.tokenDelete": "トークン削除", "lib.shared-messages.tokenDeleted": "削除されたトークン", + "lib.shared-messages.topApplications": "", + "lib.shared-messages.topGateways": "", + "lib.shared-messages.topEntities": "", "lib.shared-messages.tokenSet": "トークンの設定", "lib.shared-messages.tokenUpdated": "トークンの更新", "lib.shared-messages.traffic": "トラフィック", diff --git a/pkg/webui/styles/mixins.styl b/pkg/webui/styles/mixins.styl index 1116d440f2..bb136da961 100644 --- a/pkg/webui/styles/mixins.styl +++ b/pkg/webui/styles/mixins.styl @@ -361,7 +361,7 @@ transition-color() sidebar-transition($properties) transition-timing-function: cubic-bezier(.18,.71,.3,.99) - transition-duration: 330ms + transition-duration: 0ms transition-property: $properties // Global classes attached by the sidebar component. diff --git a/pkg/webui/styles/utilities/general.styl b/pkg/webui/styles/utilities/general.styl index a29660c299..3a3f1c6f2d 100644 --- a/pkg/webui/styles/utilities/general.styl +++ b/pkg/webui/styles/utilities/general.styl @@ -120,6 +120,9 @@ .h-auto height: auto + .h-vh + height: 100vh + for $num in (1..9) $percentage = percentage($num / 10) .w-{$num}0 diff --git a/pkg/webui/styles/utilities/spacing.styl b/pkg/webui/styles/utilities/spacing.styl index 6e7a8cacd0..9abe949aab 100644 --- a/pkg/webui/styles/utilities/spacing.styl +++ b/pkg/webui/styles/utilities/spacing.styl @@ -29,7 +29,7 @@ $scalesuffix = '-0' else // Compose property using CSS variables. - $property = convert('$' + $scale + '.' + $size) + $property = convert('$' + $scale + '.' + $size + ' !important') $scalesuffix = '-' + $scale + '-' + $size // Special case for gap if $type == 'gap' diff --git a/pkg/webui/styles/variables/generic.styl b/pkg/webui/styles/variables/generic.styl index be9ce99a4c..6ca958bebb 100644 --- a/pkg/webui/styles/variables/generic.styl +++ b/pkg/webui/styles/variables/generic.styl @@ -38,7 +38,9 @@ $c = { 'bone': #F7F6F1, 'white': #FFFFFF, // Theme colors + 'tts-primary-050': #FBFCFF, 'tts-primary-100': #D2E3FF, + 'tts-primary-150': #EEF3FF, 'tts-primary-200': #A4C6FF, 'tts-primary-300': #78A5FF, 'tts-primary-400': #568AFF, @@ -59,6 +61,7 @@ $c-subtle-fill = $c.grey-500 $c-backdrop = $c.grey-200 $c-backdrop-lighter = $c.grey-100 $c-text-backdrop = #ECECEC +$c-focus = #FAFAFA // ## Text