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 ? (
+