diff --git a/app/react/sidebar/EnvironmentSidebar.tsx b/app/react/sidebar/EnvironmentSidebar.tsx index 47eb5aa6c..2ad4bf5fb 100644 --- a/app/react/sidebar/EnvironmentSidebar.tsx +++ b/app/react/sidebar/EnvironmentSidebar.tsx @@ -1,6 +1,6 @@ import { useCurrentStateAndParams, useRouter } from '@uirouter/react'; import { useEffect, useState } from 'react'; -import { X } from 'react-feather'; +import { X, Slash } from 'react-feather'; import { PlatformType, @@ -15,35 +15,56 @@ import { getPlatformIcon } from '../portainer/environments/utils/get-platform-ic import { AzureSidebar } from './AzureSidebar'; import { DockerSidebar } from './DockerSidebar'; import { KubernetesSidebar } from './KubernetesSidebar'; -import { SidebarSection } from './SidebarSection'; +import { SidebarSection, SidebarSectionTitle } from './SidebarSection'; import { useSidebarState } from './useSidebarState'; export function EnvironmentSidebar() { const { query: currentEnvironmentQuery, clearEnvironment } = useCurrentEnvironment(); const environment = currentEnvironmentQuery.data; - if (!environment) { + + const { isOpen } = useSidebarState(); + + if (!isOpen && !environment) { return null; } + return ( +
+ {environment ? ( + + ) : ( + +
+ Environment: + + None selected +
+
+ )} +
+ ); +} + +interface ContentProps { + environment: Environment; + onClear: () => void; +} + +function Content({ environment, onClear }: ContentProps) { const platform = getPlatformType(environment.Type); const Sidebar = getSidebar(platform); return ( -
- ( - - )} - > + <SidebarSection + title={<Title environment={environment} onClear={onClear} />} + aria-label={environment.Name} + showTitleWhenOpen + > + <div className="mt-2"> <Sidebar environmentId={environment.Id} environment={environment} /> - </SidebarSection> - </div> + </div> + </SidebarSection> ); function getSidebar(platform: PlatformType) { @@ -86,38 +107,38 @@ function useCurrentEnvironment() { } interface TitleProps { - className: string; environment: Environment; onClear(): void; } -function Title({ className, environment, onClear }: TitleProps) { +function Title({ environment, onClear }: TitleProps) { const { isOpen } = useSidebarState(); + const EnvironmentIcon = getPlatformIcon(environment.Type); if (!isOpen) { return ( - <li className="w-full flex justify-center" title={environment.Name}> + <div className="w-8 flex justify-center -ml-3" title={environment.Name}> <EnvironmentIcon className="text-2xl" /> - </li> + </div> ); } return ( - <li className={className}> - <div className="flex items-center gap-2"> - <span>Environment</span> - <EnvironmentIcon className="text-2xl" /> - <span className="text-white">{environment.Name}</span> + <div className="flex items-center"> + <EnvironmentIcon className="text-2xl mr-3" /> + <span className="text-white text-ellipsis overflow-hidden whitespace-nowrap"> + {environment.Name} + </span> - <button - type="button" - onClick={onClear} - className="flex items-center justify-center be:bg-gray-9 bg-blue-10 rounded border-0 text-sm h-5 w-5 p-1 ml-auto mr-2 text-gray-5 be:text-gray-6 hover:text-white" - > - <X /> - </button> - </div> - </li> + <button + title="Clear environment" + type="button" + onClick={onClear} + className="flex items-center justify-center be:bg-gray-9 bg-blue-10 hover:bg-blue-9 be:hover:bg-gray-7 transition-colors duration-200 rounded border-0 text-sm h-5 w-5 p-1 ml-auto mr-2 text-gray-5 be:text-gray-6 hover:text-white be:hover:text-white" + > + <X /> + </button> + </div> ); } diff --git a/app/react/sidebar/Header.tsx b/app/react/sidebar/Header.tsx index 24f48d1f8..3040e293e 100644 --- a/app/react/sidebar/Header.tsx +++ b/app/react/sidebar/Header.tsx @@ -18,7 +18,7 @@ export function Header({ logo }: Props) { <Link to="portainer.home" data-cy="portainerSidebar-homeImage" - className="text-2xl text-white no-underline hover:no-underline hover:text-white" + className="text-2xl text-white no-underline hover:no-underline hover:text-white focus:no-underline focus:text-white focus:outline-none" > <img src={logo || defaultLogo} @@ -31,7 +31,7 @@ export function Header({ logo }: Props) { <button type="button" onClick={() => toggle()} - className="w-6 h-6 flex justify-center items-center text-gray-4 be:text-gray-5 border-0 rounded text-sm be:bg-gray-10 bg-blue-11 hover:text-white be:hover:text-white" + className="w-6 h-6 flex justify-center items-center text-gray-4 be:text-gray-5 border-0 rounded text-sm bg-blue-11 hover:bg-blue-10 be:bg-gray-10 be:hover:bg-gray-8 transition-colors duration-200 hover:text-white be:hover:text-white" aria-label="Toggle Sidebar" title="Toggle Sidebar" > diff --git a/app/react/sidebar/KubernetesSidebar/KubernetesSidebar.tsx b/app/react/sidebar/KubernetesSidebar/KubernetesSidebar.tsx index 92838e4c2..0c85a82fd 100644 --- a/app/react/sidebar/KubernetesSidebar/KubernetesSidebar.tsx +++ b/app/react/sidebar/KubernetesSidebar/KubernetesSidebar.tsx @@ -19,7 +19,11 @@ export function KubernetesSidebar({ environmentId }: Props) { return ( <> - {isOpen && <KubectlShellButton environmentId={environmentId} />} + {isOpen && ( + <div className="mb-3"> + <KubectlShellButton environmentId={environmentId} /> + </div> + )} <DashboardLink environmentId={environmentId} diff --git a/app/react/sidebar/SidebarItem/Head.tsx b/app/react/sidebar/SidebarItem/Head.tsx index bf2a2ee08..c8524be7c 100644 --- a/app/react/sidebar/SidebarItem/Head.tsx +++ b/app/react/sidebar/SidebarItem/Head.tsx @@ -5,6 +5,7 @@ import { } from '@uirouter/react'; import clsx from 'clsx'; import { ComponentProps } from 'react'; +import ReactTooltip from 'react-tooltip'; import { AutomationTestingProps } from '@/types'; @@ -43,25 +44,32 @@ export function Head({ <a href={anchorProps.href} onClick={anchorProps.onClick} - title={label} className={clsx( anchorProps.className, - 'text-inherit no-underline hover:no-underline hover:text-inherit focus:no-underline focus:text-inherit w-full flex-1 rounded-md', - { 'px-3': isOpen } + 'text-inherit no-underline hover:no-underline hover:text-inherit focus:no-underline focus:text-inherit', + 'w-full flex-1 rounded-md flex items-center h-8 space-x-4 text-sm', + 'hover:bg-blue-9 be:hover:bg-gray-9 transition-colors duration-200', + { + 'px-3 justify-start w-full': isOpen, + 'justify-center w-8': !isOpen, + } )} + data-tip={label} data-cy={dataCy} > - <div - className={clsx('flex items-center h-8 space-x-4 text-sm', { - 'justify-start w-full': isOpen, - 'justify-center w-8': !isOpen, - })} - > - {!!icon && ( - <Icon icon={icon} feather className={clsx('flex [&>svg]:w-4')} /> - )} - {isOpen && <span>{label}</span>} - </div> + {!!icon && ( + <Icon icon={icon} feather className={clsx('flex [&>svg]:w-4')} /> + )} + {isOpen && <span>{label}</span>} + + <ReactTooltip + type="info" + place="right" + effect="solid" + className="!opacity-100 bg-blue-9 be:bg-gray-9 !rounded-md !py-1 !px-2" + arrowColor="transparent" + disable={isOpen} + /> </a> ); } @@ -73,7 +81,7 @@ function useSrefActive( options: TransitionOptions = {}, ignorePaths: string[] = [] ) { - const { state } = useCurrentStateAndParams(); + const { state: { name: stateName = '' } = {} } = useCurrentStateAndParams(); const anchorProps = useUiRouterSrefActive( to, params || {}, @@ -81,7 +89,7 @@ function useSrefActive( options ); - const className = ignorePaths.includes(state.name || '') + const className = ignorePaths.some((path) => stateName.includes(path)) ? '' : anchorProps.className; diff --git a/app/react/sidebar/SidebarItem/Wrapper.tsx b/app/react/sidebar/SidebarItem/Wrapper.tsx index 56c2c59fa..af852ca6f 100644 --- a/app/react/sidebar/SidebarItem/Wrapper.tsx +++ b/app/react/sidebar/SidebarItem/Wrapper.tsx @@ -19,7 +19,6 @@ export function Wrapper({ className, 'text-gray-3 min-h-8 [&>a]:text-inherit [&>a]:hover:text-inherit [&>a]:hover:no-underline' )} - title={label} aria-label={label} // eslint-disable-next-line react/jsx-props-no-spreading {...ariaProps} diff --git a/app/react/sidebar/SidebarSection.tsx b/app/react/sidebar/SidebarSection.tsx index 77750588f..9de673618 100644 --- a/app/react/sidebar/SidebarSection.tsx +++ b/app/react/sidebar/SidebarSection.tsx @@ -3,28 +3,50 @@ import { PropsWithChildren, ReactNode } from 'react'; import { useSidebarState } from './useSidebarState'; interface Props { - title: string; - renderTitle?: (className: string) => ReactNode; + title: ReactNode; + showTitleWhenOpen?: boolean; + 'aria-label'?: string; } export function SidebarSection({ title, - renderTitle, children, + showTitleWhenOpen, + 'aria-label': ariaLabel, }: PropsWithChildren<Props>) { - const { isOpen } = useSidebarState(); - const titleClassName = - 'ml-3 text-sm text-gray-3 be:text-gray-6 transition-all duration-500 ease-in-out'; - return ( <div> - {renderTitle - ? renderTitle(titleClassName) - : isOpen && <li className={titleClassName}>{title}</li>} + <SidebarSectionTitle showWhenOpen={showTitleWhenOpen}> + {title} + </SidebarSectionTitle> - <nav aria-label={title} className="mt-4"> + <nav + aria-label={typeof title === 'string' ? title : ariaLabel} + className="mt-4" + > <ul>{children}</ul> </nav> </div> ); } + +interface TitleProps { + showWhenOpen?: boolean; +} + +export function SidebarSectionTitle({ + showWhenOpen, + children, +}: PropsWithChildren<TitleProps>) { + const { isOpen } = useSidebarState(); + + if (!isOpen && !showWhenOpen) { + return null; + } + + return ( + <li className="ml-3 text-sm text-gray-3 be:text-gray-6 transition-all duration-500 ease-in-out"> + {children} + </li> + ); +}