diff --git a/package.json b/package.json index b61b174e..462c9973 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,9 @@ "MLRun", "mlrun" ], + "dependencies": { + "@reduxjs/toolkit": "^2.9.0" + }, "peerDependencies": { "@reduxjs/toolkit": "*", "classnames": "*", @@ -59,7 +62,6 @@ }, "devDependencies": { "@eslint/js": "^9.19.0", - "@reduxjs/toolkit": "^2.8.2", "@storybook/addon-actions": "^8.0.0", "@storybook/addon-essentials": "^8.0.0", "@storybook/addon-interactions": "^8.0.0", diff --git a/src/lib/components/Navbar/Navbar.jsx b/src/lib/components/Navbar/Navbar.jsx new file mode 100644 index 00000000..bae8c086 --- /dev/null +++ b/src/lib/components/Navbar/Navbar.jsx @@ -0,0 +1,98 @@ +/* +Copyright 2019 Iguazio Systems Ltd. + +Licensed under the Apache License, Version 2.0 (the "License") with +an addition restriction as set forth herein. 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. + +In addition, you may not use the software for any purposes that are +illegal under applicable law, and the grant of the foregoing license +under the Apache 2.0 license is conditioned upon your compliance with +such restriction. +*/ +import React, { useState, useRef, useEffect } from 'react' +import PropTypes from 'prop-types' +import classNames from 'classnames' + +import Tooltip from '../Tooltip/Tooltip' +import TextTooltipTemplate from '../TooltipTemplate/TextTooltipTemplate' + +import { NAVBAR_WIDTH_OPENED } from '../../constants' +import { localStorageService } from '../../utils' + +import NavbarClosedIcon from '../../images/navbar/navbar-closed-icon.svg?react' +import NavbarOpenedIcon from '../../images/navbar/navbar-opened-icon.svg?react' + +import './Navbar.scss' + +const Navbar = ({ children, id = 'navbar', setNavbarIsPinned }) => { + const [isPinned, setIsPinned] = useState( + localStorageService.getStorageValue('isNavbarStatic', false) === 'true' + ) + const navbarRef = useRef(null) + + const navbarClasses = classNames('navbar', isPinned && 'navbar_pinned') + const navbarStyles = { + maxWidth: isPinned && NAVBAR_WIDTH_OPENED + } + + const handlePinClick = () => { + setIsPinned(prevIsPinned => { + localStorageService.setStorageValue('isNavbarStatic', !prevIsPinned) + return !prevIsPinned + }) + } + + useEffect(() => { + setNavbarIsPinned(isPinned) + }, [isPinned, setNavbarIsPinned]) + + return ( + + ) +} + +Navbar.Body = ({ children }) => ( +
+ {children} +
+) + +Navbar.Body.displayName = 'Navbar.Body' + +Navbar.Divider = () =>
+ +Navbar.Divider.displayName = 'Navbar.Divider' + +Navbar.Body.propTypes = { + children: PropTypes.node.isRequired +} + +Navbar.propTypes = { + children: PropTypes.oneOfType([ + PropTypes.element, + PropTypes.object, + PropTypes.node, + PropTypes.string + ]).isRequired, + id: PropTypes.string, + setNavbarIsPinned: PropTypes.func +} + +export default Navbar diff --git a/src/lib/components/Navbar/Navbar.scss b/src/lib/components/Navbar/Navbar.scss new file mode 100644 index 00000000..fe43f5c4 --- /dev/null +++ b/src/lib/components/Navbar/Navbar.scss @@ -0,0 +1,135 @@ +@use '../../scss/variables'; +@use '../../scss/colors'; +@use '../../scss/borders'; + +.navbar { + display: flex; + flex-flow: column nowrap; + flex-grow: 1; + flex-shrink: 0; + flex-basis: 245px; + position: absolute; + top: 0; + left: 0; + z-index: 11; + height: 100%; + width: 245px; + max-width: 57px; + border-right: borders.$tertiaryBorder; + background-color: colors.$cultured; + transition: max-width 0.3s ease-in-out; + + &:hover { + max-width: 245px; + + .navbar__pin-icon { + opacity: 1; + visibility: visible; + } + } + + &:hover, + &.navbar_pinned { + .navbar__body { + overflow-y: auto; + } + + .nav-link__button { + padding: 0 8px 0 24px; + } + + .nav-link__button.expanded { + & + .navbar-links_nested { + max-height: 30rem; + transition: max-height 0.3s ease-in-out; + } + } + } + + .nav-link { + &__icon { + display: flex; + opacity: 1; + flex: 0 0 auto; + transition: opacity 0.3s ease-in-out; + + svg { + flex: 0 0 20px; + width: 20px; + height: 20px; + + & * { + fill: colors.$topaz; + } + } + } + } + + &__divider { + border-top: 1px solid colors.$mischka; + margin: 0 1rem; + } + + &__body { + position: relative; + z-index: 2; + display: flex; + flex-flow: column nowrap; + justify-content: flex-start; + width: 100%; + height: 100%; + overflow: hidden; + transition: padding 0.3s ease-in-out; + } + + &-links { + display: flex; + flex: 0 0 auto; + flex-flow: column nowrap; + max-width: 238px; + margin: 0.5rem 0; + padding: 0 10px 0 0; + width: 100%; + list-style-type: none; + + &.navbar-links_nested { + max-height: 0; + padding: 0; + margin: 0; + overflow: hidden; + transition: max-height 0.3s ease-in-out; + + .nav-link__button { + padding-left: 44px; + } + + & > *:first-child { + margin-top: 5px; + } + } + } + + &__pin { + &-icon { + position: absolute; + display: flex; + justify-content: flex-end; + padding: 5px 10px; + opacity: 0; + top: 10px; + right: -45px; + background-color: colors.$cultured; + border-radius: 0 8px 8px 0; + border-width: 1px 1px 1px 0; + border-color: colors.$mischka; + border-style: solid; + visibility: hidden; + transition: all 0.3s ease-in-out; + + &:hover { + cursor: pointer; + background-color: colors.$crystalBell; + } + } + } +} diff --git a/src/lib/components/index.js b/src/lib/components/index.js index e6cf9ef1..45634658 100644 --- a/src/lib/components/index.js +++ b/src/lib/components/index.js @@ -37,6 +37,7 @@ import LoadButton from './LoadButton/LoadButton' import Loader from './Loader/Loader' import LoaderForSuspenseFallback from './Loader/LoaderForSuspenseFallback' import Modal from './Modal/Modal' +import Navbar from './Navbar/Navbar' import PopUpDialog from './PopUpDialog/PopUpDialog' import RoundedIcon from './RoundedIcon/RoundedIcon' import TableCell from './TableCell/TableCell' @@ -69,6 +70,7 @@ export { Loader, LoaderForSuspenseFallback, Modal, + Navbar, PopUpDialog, RoundedIcon, TableCell, diff --git a/src/lib/constants.js b/src/lib/constants.js index 7a393caf..ce28788c 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -83,3 +83,8 @@ export const MODAL_MAX = 'max' export const MAIN_TABLE_ID = 'main-table' export const MAIN_TABLE_BODY_ID = 'main-table-body' + +/*=========== NAVBAR =============*/ + +export const NAVBAR_WIDTH_CLOSED = 57 +export const NAVBAR_WIDTH_OPENED = 245 diff --git a/src/lib/elements/NavbarLink/NavbarLink.jsx b/src/lib/elements/NavbarLink/NavbarLink.jsx new file mode 100644 index 00000000..8b94593d --- /dev/null +++ b/src/lib/elements/NavbarLink/NavbarLink.jsx @@ -0,0 +1,98 @@ +/* +Copyright 2019 Iguazio Systems Ltd. + +Licensed under the Apache License, Version 2.0 (the "License") with +an addition restriction as set forth herein. 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. + +In addition, you may not use the software for any purposes that are +illegal under applicable law, and the grant of the foregoing license +under the Apache 2.0 license is conditioned upon your compliance with +such restriction. +*/ +import React from 'react' +import { NavLink, useLocation } from 'react-router-dom' +import classNames from 'classnames' +import PropTypes from 'prop-types' + +import ArrowIcon from '../../images/arrow.svg?react' + +import './NavbarLink.scss' + +const NavbarLink = ({ + externalLink = false, + icon = null, + index = null, + label, + link = '', + selectedIndex = null, + setSelectedIndex, + ...props +}) => { + const { pathname } = useLocation() + const [, , page] = pathname.split('/').slice(1, 4) + + const parentLinkClasses = classNames( + 'nav-link__button btn nav-link__parent', + props.screens && props.screens.includes(page) && 'active', + index === selectedIndex && 'expanded' + ) + + const handleExpanded = () => { + if (setSelectedIndex) { + if (index !== selectedIndex) { + setSelectedIndex(index) + } else { + setSelectedIndex(null) + } + } + } + + return externalLink ? ( + + {icon} + {label} + + ) : props.nestedLinks ? ( +
+ {icon} + {label} + + + + +
+ ) : ( + + {icon} + {label} + + ) +} + +NavbarLink.propTypes = { + externalLink: PropTypes.bool, + icon: PropTypes.object, + id: PropTypes.string, + index: PropTypes.number, + label: PropTypes.string.isRequired, + link: PropTypes.string, + nestedLinks: PropTypes.array, + screens: PropTypes.array, + selectedIndex: PropTypes.number, + setSelectedIndex: PropTypes.func +} + +export default React.memo(NavbarLink) diff --git a/src/lib/elements/NavbarLink/NavbarLink.scss b/src/lib/elements/NavbarLink/NavbarLink.scss new file mode 100644 index 00000000..83f80076 --- /dev/null +++ b/src/lib/elements/NavbarLink/NavbarLink.scss @@ -0,0 +1,121 @@ +@use '../../scss/colors'; + +.nav-link { + margin-bottom: 5px; + + &:last-child { + margin: 0; + } + + &__arrow { + flex: 1; + display: flex; + justify-content: end; + + svg { + transition: transform 0.2s ease-in-out; + } + } + + &__button { + position: relative; + flex-flow: row nowrap; + justify-content: flex-start; + width: 100%; + min-height: 48px; + min-width: 48px; + padding: 0 8px 0 18px; + color: colors.$primary; + font-weight: normal; + background-color: transparent; + border: none; + border-radius: 0 8px 8px 0; + white-space: nowrap; + cursor: pointer; + transition: all 0.3s ease-in-out; + + &.active, + &.active:hover { + &:not(:disabled):not(.nav-link__parent) { + background-color: colors.$crystalBell; + cursor: default; + + &::before { + content: ''; + border: 2px solid colors.$malibu; + height: 100%; + position: absolute; + left: 0; + } + } + + &.nav-link__parent { + background-color: colors.$crystalBell; + font-weight: bold; + cursor: pointer; + + &::before { + content: ''; + border: 2px solid colors.$malibu; + height: 100%; + position: absolute; + left: 0; + transition: border-color 0.3s ease-in-out; + + .navbar:hover &, + .navbar.navbar_hovered &, + .navbar.navbar_pinned & { + border-color: transparent; + } + } + } + } + + &.expanded { + &:not(:disabled):not(.active) { + background-color: inherit; + } + + .nav-link__arrow { + svg { + transform: rotate(90deg); + } + } + + &.nav-link__parent { + font-weight: bold; + cursor: pointer; + } + } + + &:hover, + &:focus-visible, + &.expanded { + background-color: colors.$crystalBell; + outline: none; + transition: all 0.3s ease-in-out; + + svg { + & * { + fill: colors.$topaz; + } + } + } + + &:focus, + &:active { + border-color: transparent; + } + } + + &__label { + opacity: 0; + transition: opacity 0.3s ease-in-out; + + .navbar:hover &, + .navbar.navbar_hovered &, + .navbar.navbar_pinned & { + opacity: 1; + } + } +} diff --git a/src/lib/elements/index.js b/src/lib/elements/index.js index 0dd0b67a..3883fbab 100644 --- a/src/lib/elements/index.js +++ b/src/lib/elements/index.js @@ -18,6 +18,7 @@ such restriction. import ActionsMenuItem from './ActionsMenuItem/ActionsMenuItem' import FormActionButton from './FormActionButton/FormActionButton' import FormRowActions from './FormRowActions/FormRowActions' +import NavbarLink from './NavbarLink/NavbarLink' import OptionsMenu from './OptionsMenu/OptionsMenu' import SelectOption from './SelectOption/SelectOption' import TableHead from './TableHead/TableHead' @@ -29,6 +30,7 @@ export { ActionsMenuItem, FormActionButton, FormRowActions, + NavbarLink, OptionsMenu, SelectOption, TableHead, diff --git a/src/lib/images/ig4-header-projects-icon.svg b/src/lib/images/ig4-header-projects-icon.svg new file mode 100644 index 00000000..e9531fee --- /dev/null +++ b/src/lib/images/ig4-header-projects-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/lib/images/navbar/group-navbar-icon.svg b/src/lib/images/navbar/group-navbar-icon.svg new file mode 100644 index 00000000..63e76087 --- /dev/null +++ b/src/lib/images/navbar/group-navbar-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/lib/images/navbar/navbar-closed-icon.svg b/src/lib/images/navbar/navbar-closed-icon.svg new file mode 100644 index 00000000..d5b69ed2 --- /dev/null +++ b/src/lib/images/navbar/navbar-closed-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/lib/images/navbar/navbar-opened-icon.svg b/src/lib/images/navbar/navbar-opened-icon.svg new file mode 100644 index 00000000..068bd8cd --- /dev/null +++ b/src/lib/images/navbar/navbar-opened-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/lib/images/navbar/users-navbar-icon.svg b/src/lib/images/navbar/users-navbar-icon.svg new file mode 100644 index 00000000..d9a849bd --- /dev/null +++ b/src/lib/images/navbar/users-navbar-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/lib/images/users-delete.svg b/src/lib/images/users-delete.svg new file mode 100644 index 00000000..f47a4eb0 --- /dev/null +++ b/src/lib/images/users-delete.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/lib/images/users.svg b/src/lib/images/users.svg index f47a4eb0..25d972e6 100644 --- a/src/lib/images/users.svg +++ b/src/lib/images/users.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/lib/scss/colors.scss b/src/lib/scss/colors.scss index cb2f1a98..550623fa 100644 --- a/src/lib/scss/colors.scss +++ b/src/lib/scss/colors.scss @@ -17,6 +17,8 @@ $cerulean: #00b6ed; $chateauGreen: #49af53; $cornflowerBlue: #6279e7; $cornflowerBlueTwo: #5871f4; +$cultured: #f9f8f8; +$crystalBell: #efefef; $darkPurple: #2f2b46; $doveGray: #666; $doveGrayTwo: #6e6e6e; diff --git a/src/lib/utils/index.js b/src/lib/utils/index.js index f6d0c353..79d94591 100644 --- a/src/lib/utils/index.js +++ b/src/lib/utils/index.js @@ -22,6 +22,7 @@ export * as filter from './filter.util' export * as form from './form.util' export * as generateChipsList from './generateChipsList.util' export * as getFirstScrollableParent from './getFirstScrollableParent.util' +export * as localStorageService from './localStorageService.util' export * as math from './math.util' export * as notification from './notification.util' export * as string from './string.util' diff --git a/src/lib/utils/localStorageService.util.js b/src/lib/utils/localStorageService.util.js new file mode 100644 index 00000000..af9d1e1d --- /dev/null +++ b/src/lib/utils/localStorageService.util.js @@ -0,0 +1,35 @@ +/* +Copyright 2019 Iguazio Systems Ltd. + +Licensed under the Apache License, Version 2.0 (the "License") with +an addition restriction as set forth herein. 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. + +In addition, you may not use the software for any purposes that are +illegal under applicable law, and the grant of the foregoing license +under the Apache 2.0 license is conditioned upon your compliance with +such restriction. +*/ +export const getStorageValue = (key, defaultValue) => { + if (typeof window !== 'undefined') { + try { + const saved = localStorage.getItem(key) + + return saved !== null ? saved : defaultValue + } catch (err) { + /* eslint-disable-next-line no-console */ + console.log(err) + } + } +} + +export const setStorageValue = (key, defaultValue) => { + localStorage.setItem(key, defaultValue) +}