mirror of https://github.com/portainer/portainer
feat(sidebar): update menu structure [EE-5666] (#10418)
parent
b468070945
commit
a0dbabcc5f
|
@ -359,17 +359,17 @@ input:-webkit-autofill {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .tippy-box[data-placement^='right'] > .tippy-arrow {
|
.sidebar.tippy-box[data-placement^='right'] > .tippy-arrow {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .tippy-box[data-placement^='right'] > .tippy-arrow:before {
|
.sidebar.tippy-box[data-placement^='right'] > .tippy-arrow:before {
|
||||||
border-right: 8px solid var(--ui-gray-9);
|
border-right: 8px solid var(--ui-blue-9);
|
||||||
border-width: 6px 8px 6px 0;
|
border-width: 6px 8px 6px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[theme='dark'] .sidebar .tippy-box[data-placement^='right'] > .tippy-arrow:before {
|
[theme='dark'] .sidebar.tippy-box[data-placement^='right'] > .tippy-arrow:before {
|
||||||
border-right: 8px solid var(--ui-gray-true-9);
|
border-right: 8px solid var(--ui-gray-true-9);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { useApiVersion } from '@/react/docker/proxy/queries/useVersion';
|
||||||
import { SidebarItem } from './SidebarItem';
|
import { SidebarItem } from './SidebarItem';
|
||||||
import { DashboardLink } from './items/DashboardLink';
|
import { DashboardLink } from './items/DashboardLink';
|
||||||
import { VolumesLink } from './items/VolumesLink';
|
import { VolumesLink } from './items/VolumesLink';
|
||||||
|
import { SidebarParent } from './SidebarItem/SidebarParent';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
environmentId: EnvironmentId;
|
environmentId: EnvironmentId;
|
||||||
|
@ -72,21 +73,29 @@ export function DockerSidebar({ environmentId, environment }: Props) {
|
||||||
platformPath="docker"
|
platformPath="docker"
|
||||||
data-cy="dockerSidebar-dashboard"
|
data-cy="dockerSidebar-dashboard"
|
||||||
/>
|
/>
|
||||||
|
<SidebarParent
|
||||||
<SidebarItem
|
|
||||||
label="App Templates"
|
|
||||||
icon={Edit}
|
icon={Edit}
|
||||||
|
label="Templates"
|
||||||
to="docker.templates"
|
to="docker.templates"
|
||||||
params={{ endpointId: environmentId }}
|
params={{ endpointId: environmentId }}
|
||||||
data-cy="portainerSidebar-appTemplates"
|
data-cy="portainerSidebar-templates"
|
||||||
>
|
>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label="Custom Templates"
|
label="Application"
|
||||||
|
to="docker.templates"
|
||||||
|
ignorePaths={['docker.templates.custom']}
|
||||||
|
params={{ endpointId: environmentId }}
|
||||||
|
isSubMenu
|
||||||
|
data-cy="portainerSidebar-appTemplates"
|
||||||
|
/>
|
||||||
|
<SidebarItem
|
||||||
|
label="Custom"
|
||||||
to="docker.templates.custom"
|
to="docker.templates.custom"
|
||||||
params={{ endpointId: environmentId }}
|
params={{ endpointId: environmentId }}
|
||||||
|
isSubMenu
|
||||||
data-cy="dockerSidebar-customTemplates"
|
data-cy="dockerSidebar-customTemplates"
|
||||||
/>
|
/>
|
||||||
</SidebarItem>
|
</SidebarParent>
|
||||||
|
|
||||||
{areStacksVisible && (
|
{areStacksVisible && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
|
@ -168,31 +177,42 @@ export function DockerSidebar({ environmentId, environment }: Props) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SidebarItem
|
<SidebarParent
|
||||||
label={setupSubMenuProps.label}
|
label={setupSubMenuProps.label}
|
||||||
icon={setupSubMenuProps.icon}
|
icon={setupSubMenuProps.icon}
|
||||||
to={setupSubMenuProps.to}
|
to={setupSubMenuProps.to}
|
||||||
params={{ endpointId: environmentId }}
|
params={{ endpointId: environmentId }}
|
||||||
data-cy={setupSubMenuProps.dataCy}
|
data-cy="portainerSidebar-host"
|
||||||
>
|
>
|
||||||
|
<SidebarItem
|
||||||
|
label="Details"
|
||||||
|
isSubMenu
|
||||||
|
to={setupSubMenuProps.to}
|
||||||
|
params={{ endpointId: environmentId }}
|
||||||
|
ignorePaths={[featSubMenuTo, registrySubMenuTo]}
|
||||||
|
data-cy={setupSubMenuProps.dataCy}
|
||||||
|
/>
|
||||||
|
|
||||||
<Authorized
|
<Authorized
|
||||||
authorizations="PortainerEndpointUpdateSettings"
|
authorizations="PortainerEndpointUpdateSettings"
|
||||||
adminOnlyCE
|
adminOnlyCE
|
||||||
environmentId={environmentId}
|
environmentId={environmentId}
|
||||||
>
|
>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
|
label="Setup"
|
||||||
|
isSubMenu
|
||||||
to={featSubMenuTo}
|
to={featSubMenuTo}
|
||||||
params={{ endpointId: environmentId }}
|
params={{ endpointId: environmentId }}
|
||||||
label="Setup"
|
|
||||||
/>
|
/>
|
||||||
</Authorized>
|
</Authorized>
|
||||||
|
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
|
label="Registries"
|
||||||
|
isSubMenu
|
||||||
to={registrySubMenuTo}
|
to={registrySubMenuTo}
|
||||||
params={{ endpointId: environmentId }}
|
params={{ endpointId: environmentId }}
|
||||||
label="Registries"
|
|
||||||
/>
|
/>
|
||||||
</SidebarItem>
|
</SidebarParent>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
.root {
|
|
||||||
display: block;
|
|
||||||
margin: -5px auto -8px auto;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
|
|
@ -1,37 +1,53 @@
|
||||||
import clsx from 'clsx';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { Terminal } from 'lucide-react';
|
import { Terminal } from 'lucide-react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
import { useAnalytics } from '@/react/hooks/useAnalytics';
|
import { useAnalytics } from '@/react/hooks/useAnalytics';
|
||||||
|
|
||||||
import { Button } from '@@/buttons';
|
import { Button } from '@@/buttons';
|
||||||
import { Icon } from '@@/Icon';
|
|
||||||
|
import { useSidebarState } from '../../useSidebarState';
|
||||||
|
import { SidebarTooltip } from '../../SidebarItem/SidebarTooltip';
|
||||||
|
|
||||||
import { KubeCtlShell } from './KubectlShell';
|
import { KubeCtlShell } from './KubectlShell';
|
||||||
import styles from './KubectlShellButton.module.css';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
environmentId: EnvironmentId;
|
environmentId: EnvironmentId;
|
||||||
}
|
}
|
||||||
export function KubectlShellButton({ environmentId }: Props) {
|
export function KubectlShellButton({ environmentId }: Props) {
|
||||||
|
const { isOpen: isSidebarOpen } = useSidebarState();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { trackEvent } = useAnalytics();
|
const { trackEvent } = useAnalytics();
|
||||||
|
|
||||||
|
const button = (
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
disabled={open}
|
||||||
|
data-cy="k8sSidebar-shellButton"
|
||||||
|
onClick={() => handleOpen()}
|
||||||
|
className={clsx('sidebar', !isSidebarOpen && '!p-1')}
|
||||||
|
icon={Terminal}
|
||||||
|
>
|
||||||
|
{isSidebarOpen ? 'kubectl shell' : ''}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
{!isSidebarOpen && (
|
||||||
color="primary"
|
<SidebarTooltip
|
||||||
size="small"
|
content={
|
||||||
disabled={open}
|
<span className="text-sm whitespace-nowrap">Kubectl Shell</span>
|
||||||
data-cy="k8sSidebar-shellButton"
|
}
|
||||||
onClick={() => handleOpen()}
|
>
|
||||||
className={clsx(styles.root, '!flex')}
|
<span className="flex w-full justify-center">{button}</span>
|
||||||
>
|
</SidebarTooltip>
|
||||||
<Icon icon={Terminal} className="vertical-center" size="md" /> kubectl
|
)}
|
||||||
shell
|
{isSidebarOpen && button}
|
||||||
</Button>
|
|
||||||
|
|
||||||
{open &&
|
{open &&
|
||||||
createPortal(
|
createPortal(
|
||||||
<KubeCtlShell
|
<KubeCtlShell
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { Box, Edit, Layers, Lock, Server, Shuffle } from 'lucide-react';
|
import { Box, Edit, Layers, Lock, Network, Server } from 'lucide-react';
|
||||||
|
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
import { Authorized } from '@/react/hooks/useUser';
|
import { Authorized } from '@/react/hooks/useUser';
|
||||||
import Route from '@/assets/ico/route.svg?c';
|
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||||
|
|
||||||
import { DashboardLink } from '../items/DashboardLink';
|
import { DashboardLink } from '../items/DashboardLink';
|
||||||
import { SidebarItem } from '../SidebarItem';
|
import { SidebarItem } from '../SidebarItem';
|
||||||
import { VolumesLink } from '../items/VolumesLink';
|
import { VolumesLink } from '../items/VolumesLink';
|
||||||
import { useSidebarState } from '../useSidebarState';
|
import { SidebarParent } from '../SidebarItem/SidebarParent';
|
||||||
|
|
||||||
import { KubectlShellButton } from './KubectlShell';
|
import { KubectlShellButton } from './KubectlShell';
|
||||||
|
|
||||||
|
@ -16,15 +16,11 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KubernetesSidebar({ environmentId }: Props) {
|
export function KubernetesSidebar({ environmentId }: Props) {
|
||||||
const { isOpen } = useSidebarState();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isOpen && (
|
<div className="w-full flex mb-2 justify-center -mt-2">
|
||||||
<div className="mb-3">
|
<KubectlShellButton environmentId={environmentId} />
|
||||||
<KubectlShellButton environmentId={environmentId} />
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DashboardLink
|
<DashboardLink
|
||||||
environmentId={environmentId}
|
environmentId={environmentId}
|
||||||
|
@ -56,21 +52,30 @@ export function KubernetesSidebar({ environmentId }: Props) {
|
||||||
data-cy="k8sSidebar-applications"
|
data-cy="k8sSidebar-applications"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SidebarItem
|
<SidebarParent
|
||||||
|
label="Networking"
|
||||||
|
icon={Network}
|
||||||
to="kubernetes.services"
|
to="kubernetes.services"
|
||||||
params={{ endpointId: environmentId }}
|
params={{ endpointId: environmentId }}
|
||||||
label="Services"
|
pathOptions={{ includePaths: ['kubernetes.ingresses'] }}
|
||||||
data-cy="k8sSidebar-services"
|
data-cy="k8sSidebar-networking"
|
||||||
icon={Shuffle}
|
>
|
||||||
/>
|
<SidebarItem
|
||||||
|
to="kubernetes.services"
|
||||||
|
params={{ endpointId: environmentId }}
|
||||||
|
label="Services"
|
||||||
|
isSubMenu
|
||||||
|
data-cy="k8sSidebar-services"
|
||||||
|
/>
|
||||||
|
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="kubernetes.ingresses"
|
to="kubernetes.ingresses"
|
||||||
params={{ endpointId: environmentId }}
|
params={{ endpointId: environmentId }}
|
||||||
label="Ingresses"
|
label="Ingresses"
|
||||||
data-cy="k8sSidebar-ingresses"
|
isSubMenu
|
||||||
icon={Route}
|
data-cy="k8sSidebar-ingresses"
|
||||||
/>
|
/>
|
||||||
|
</SidebarParent>
|
||||||
|
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="kubernetes.configurations"
|
to="kubernetes.configurations"
|
||||||
|
@ -86,13 +91,25 @@ export function KubernetesSidebar({ environmentId }: Props) {
|
||||||
data-cy="k8sSidebar-volumes"
|
data-cy="k8sSidebar-volumes"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SidebarItem
|
<SidebarParent
|
||||||
label="Cluster"
|
label="Cluster"
|
||||||
to="kubernetes.cluster"
|
|
||||||
icon={Server}
|
icon={Server}
|
||||||
|
to="kubernetes.cluster"
|
||||||
params={{ endpointId: environmentId }}
|
params={{ endpointId: environmentId }}
|
||||||
|
pathOptions={{ includePaths: ['kubernetes.registries'] }}
|
||||||
data-cy="k8sSidebar-cluster"
|
data-cy="k8sSidebar-cluster"
|
||||||
>
|
>
|
||||||
|
<SidebarItem
|
||||||
|
label="Details"
|
||||||
|
to="kubernetes.cluster"
|
||||||
|
ignorePaths={[
|
||||||
|
'kubernetes.cluster.setup',
|
||||||
|
'kubernetes.cluster.securityConstraint',
|
||||||
|
]}
|
||||||
|
params={{ endpointId: environmentId }}
|
||||||
|
isSubMenu
|
||||||
|
data-cy="k8sSidebar-clusterDetails"
|
||||||
|
/>
|
||||||
<Authorized
|
<Authorized
|
||||||
authorizations="K8sClusterSetupRW"
|
authorizations="K8sClusterSetupRW"
|
||||||
adminOnlyCE
|
adminOnlyCE
|
||||||
|
@ -102,6 +119,7 @@ export function KubernetesSidebar({ environmentId }: Props) {
|
||||||
to="kubernetes.cluster.setup"
|
to="kubernetes.cluster.setup"
|
||||||
params={{ endpointId: environmentId }}
|
params={{ endpointId: environmentId }}
|
||||||
label="Setup"
|
label="Setup"
|
||||||
|
isSubMenu
|
||||||
data-cy="k8sSidebar-setup"
|
data-cy="k8sSidebar-setup"
|
||||||
/>
|
/>
|
||||||
</Authorized>
|
</Authorized>
|
||||||
|
@ -115,17 +133,35 @@ export function KubernetesSidebar({ environmentId }: Props) {
|
||||||
to="kubernetes.cluster.securityConstraint"
|
to="kubernetes.cluster.securityConstraint"
|
||||||
params={{ endpointId: environmentId }}
|
params={{ endpointId: environmentId }}
|
||||||
label="Security constraints"
|
label="Security constraints"
|
||||||
|
isSubMenu
|
||||||
data-cy="k8sSidebar-securityConstraints"
|
data-cy="k8sSidebar-securityConstraints"
|
||||||
/>
|
/>
|
||||||
</Authorized>
|
</Authorized>
|
||||||
|
|
||||||
|
{isBE && (
|
||||||
|
<Authorized
|
||||||
|
authorizations="K8sClusterSetupRW"
|
||||||
|
adminOnlyCE
|
||||||
|
environmentId={environmentId}
|
||||||
|
>
|
||||||
|
<SidebarItem
|
||||||
|
to="kubernetes.cluster.securityConstraint"
|
||||||
|
params={{ endpointId: environmentId }}
|
||||||
|
label="Security Constraints"
|
||||||
|
isSubMenu
|
||||||
|
data-cy="k8sSidebar-securityConstraints"
|
||||||
|
/>
|
||||||
|
</Authorized>
|
||||||
|
)}
|
||||||
|
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="kubernetes.registries"
|
to="kubernetes.registries"
|
||||||
params={{ endpointId: environmentId }}
|
params={{ endpointId: environmentId }}
|
||||||
label="Registries"
|
label="Registries"
|
||||||
|
isSubMenu
|
||||||
data-cy="k8sSidebar-registries"
|
data-cy="k8sSidebar-registries"
|
||||||
/>
|
/>
|
||||||
</SidebarItem>
|
</SidebarParent>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||||
|
|
||||||
import { SidebarItem } from './SidebarItem';
|
import { SidebarItem } from './SidebarItem';
|
||||||
import { SidebarSection } from './SidebarSection';
|
import { SidebarSection } from './SidebarSection';
|
||||||
|
import { SidebarParent } from './SidebarItem/SidebarParent';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
|
@ -30,15 +31,23 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
|
||||||
return (
|
return (
|
||||||
<SidebarSection title="Settings">
|
<SidebarSection title="Settings">
|
||||||
{showUsersSection && (
|
{showUsersSection && (
|
||||||
<SidebarItem
|
<SidebarParent
|
||||||
to="portainer.users"
|
label="User-related"
|
||||||
label="Users"
|
|
||||||
icon={Users}
|
icon={Users}
|
||||||
data-cy="portainerSidebar-users"
|
to="portainer.users"
|
||||||
|
pathOptions={{ includePaths: ['portainer.teams', 'portainer.roles'] }}
|
||||||
|
data-cy="portainerSidebar-userRelated"
|
||||||
>
|
>
|
||||||
|
<SidebarItem
|
||||||
|
to="portainer.users"
|
||||||
|
label="Users"
|
||||||
|
isSubMenu
|
||||||
|
data-cy="portainerSidebar-users"
|
||||||
|
/>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="portainer.teams"
|
to="portainer.teams"
|
||||||
label="Teams"
|
label="Teams"
|
||||||
|
isSubMenu
|
||||||
data-cy="portainerSidebar-teams"
|
data-cy="portainerSidebar-teams"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -46,38 +55,56 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="portainer.roles"
|
to="portainer.roles"
|
||||||
label="Roles"
|
label="Roles"
|
||||||
|
isSubMenu
|
||||||
data-cy="portainerSidebar-roles"
|
data-cy="portainerSidebar-roles"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SidebarItem>
|
</SidebarParent>
|
||||||
)}
|
)}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<>
|
<>
|
||||||
<SidebarItem
|
<SidebarParent
|
||||||
label="Environments"
|
label="Environment-related"
|
||||||
to="portainer.endpoints"
|
|
||||||
icon={HardDrive}
|
icon={HardDrive}
|
||||||
openOnPaths={['portainer.wizard.endpoints']}
|
to="portainer.endpoints"
|
||||||
data-cy="portainerSidebar-environments"
|
pathOptions={{
|
||||||
|
includePaths: [
|
||||||
|
'portainer.wizard.endpoints',
|
||||||
|
'portainer.groups',
|
||||||
|
'portainer.tags',
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
data-cy="k8sSidebar-networking"
|
||||||
>
|
>
|
||||||
|
<SidebarItem
|
||||||
|
label="Environments"
|
||||||
|
to="portainer.endpoints"
|
||||||
|
ignorePaths={['portainer.endpoints.updateSchedules']}
|
||||||
|
includePaths={['portainer.wizard.endpoints']}
|
||||||
|
isSubMenu
|
||||||
|
data-cy="portainerSidebar-environments"
|
||||||
|
/>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="portainer.groups"
|
to="portainer.groups"
|
||||||
label="Groups"
|
label="Groups"
|
||||||
|
isSubMenu
|
||||||
data-cy="portainerSidebar-environmentGroups"
|
data-cy="portainerSidebar-environmentGroups"
|
||||||
/>
|
/>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="portainer.tags"
|
to="portainer.tags"
|
||||||
label="Tags"
|
label="Tags"
|
||||||
|
isSubMenu
|
||||||
data-cy="portainerSidebar-environmentTags"
|
data-cy="portainerSidebar-environmentTags"
|
||||||
/>
|
/>
|
||||||
{isBE && (
|
{isBE && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="portainer.endpoints.updateSchedules"
|
to="portainer.endpoints.updateSchedules"
|
||||||
label="Update & Rollback"
|
label="Update & Rollback"
|
||||||
|
isSubMenu
|
||||||
data-cy="portainerSidebar-updateSchedules"
|
data-cy="portainerSidebar-updateSchedules"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SidebarItem>
|
</SidebarParent>
|
||||||
|
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label="Registries"
|
label="Registries"
|
||||||
|
@ -95,18 +122,28 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SidebarItem
|
<SidebarParent
|
||||||
label="Authentication logs"
|
label="Logs"
|
||||||
to="portainer.authLogs"
|
to="portainer.authLogs"
|
||||||
icon={FileText}
|
icon={FileText}
|
||||||
data-cy="portainerSidebar-authLogs"
|
pathOptions={{
|
||||||
|
includePaths: ['portainer.activityLogs'],
|
||||||
|
}}
|
||||||
|
data-cy="k8sSidebar-logs"
|
||||||
>
|
>
|
||||||
|
<SidebarItem
|
||||||
|
label="Authentication"
|
||||||
|
to="portainer.authLogs"
|
||||||
|
isSubMenu
|
||||||
|
data-cy="portainerSidebar-authLogs"
|
||||||
|
/>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="portainer.activityLogs"
|
to="portainer.activityLogs"
|
||||||
label="Activity Logs"
|
label="Activity"
|
||||||
|
isSubMenu
|
||||||
data-cy="portainerSidebar-activityLogs"
|
data-cy="portainerSidebar-activityLogs"
|
||||||
/>
|
/>
|
||||||
</SidebarItem>
|
</SidebarParent>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
|
@ -116,29 +153,42 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
|
||||||
data-cy="portainerSidebar-notifications"
|
data-cy="portainerSidebar-notifications"
|
||||||
/>
|
/>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<SidebarItem
|
<SidebarParent
|
||||||
to="portainer.settings"
|
to="portainer.settings"
|
||||||
label="Settings"
|
label="Settings"
|
||||||
icon={Settings}
|
icon={Settings}
|
||||||
data-cy="portainerSidebar-settings"
|
data-cy="portainerSidebar-settings"
|
||||||
>
|
>
|
||||||
|
<SidebarItem
|
||||||
|
to="portainer.settings"
|
||||||
|
label="General"
|
||||||
|
isSubMenu
|
||||||
|
ignorePaths={[
|
||||||
|
'portainer.settings.authentication',
|
||||||
|
'portainer.settings.edgeCompute',
|
||||||
|
]}
|
||||||
|
data-cy="portainerSidebar-generalSettings"
|
||||||
|
/>
|
||||||
{!window.ddExtension && (
|
{!window.ddExtension && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="portainer.settings.authentication"
|
to="portainer.settings.authentication"
|
||||||
label="Authentication"
|
label="Authentication"
|
||||||
|
isSubMenu
|
||||||
data-cy="portainerSidebar-authentication"
|
data-cy="portainerSidebar-authentication"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isBE && (
|
{isBE && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="portainer.settings.cloud"
|
to="portainer.settings.sharedcredentials"
|
||||||
label="Cloud"
|
label="Shared Credentials"
|
||||||
|
isSubMenu
|
||||||
data-cy="portainerSidebar-cloud"
|
data-cy="portainerSidebar-cloud"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="portainer.settings.edgeCompute"
|
to="portainer.settings.edgeCompute"
|
||||||
label="Edge Compute"
|
label="Edge Compute"
|
||||||
|
isSubMenu
|
||||||
data-cy="portainerSidebar-edgeCompute"
|
data-cy="portainerSidebar-edgeCompute"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -151,12 +201,12 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
|
||||||
}
|
}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="flex h-8 items-center rounded px-3"
|
className="hover:!underline focus:no-underline text-sm flex h-8 w-full items-center rounded px-3 transition-colors duration-200 hover:bg-blue-5/20 be:hover:bg-gray-5/20 th-dark:hover:bg-gray-true-5/20"
|
||||||
>
|
>
|
||||||
Help / About
|
Help / About
|
||||||
</a>
|
</a>
|
||||||
</SidebarItem.Wrapper>
|
</SidebarItem.Wrapper>
|
||||||
</SidebarItem>
|
</SidebarParent>
|
||||||
)}
|
)}
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
);
|
);
|
||||||
|
|
|
@ -16,7 +16,8 @@
|
||||||
|
|
||||||
:global(#page-wrapper) {
|
:global(#page-wrapper) {
|
||||||
--sidebar-width: 300px;
|
--sidebar-width: 300px;
|
||||||
--sidebar-closed-width: 75px;
|
/* 32px for collapsed items + 20px padding on each side */
|
||||||
|
--sidebar-closed-width: 72px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(#page-wrapper.open) .root {
|
:global(#page-wrapper.open) .root {
|
||||||
|
@ -45,3 +46,19 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* make the scrollbar track background transparent */
|
||||||
|
.nav-list-container::-webkit-scrollbar {
|
||||||
|
@apply !w-4 !h-4 bg-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list-container::-webkit-scrollbar-thumb {
|
||||||
|
@apply !bg-gray-3/40 hover:!bg-gray-3/50 th-dark:!bg-gray-3/40 th-dark:hover:!bg-gray-3/50 !rounded-full;
|
||||||
|
/* adding this border gives some gap between the right edge of the box and the scrollbar thumb for webkit browsers,
|
||||||
|
while giving right no right gap from the scrollbar to the box edge for non-webkit browsers (firefox) */
|
||||||
|
@apply !border-4 !border-solid !border-transparent bg-clip-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list-container::-webkit-scrollbar-track {
|
||||||
|
@apply !bg-transparent;
|
||||||
|
}
|
||||||
|
|
|
@ -12,12 +12,22 @@ import { SettingsSidebar } from './SettingsSidebar';
|
||||||
import { SidebarItem } from './SidebarItem';
|
import { SidebarItem } from './SidebarItem';
|
||||||
import { Footer } from './Footer';
|
import { Footer } from './Footer';
|
||||||
import { Header } from './Header';
|
import { Header } from './Header';
|
||||||
import { SidebarProvider } from './useSidebarState';
|
import { SidebarProvider, useSidebarState } from './useSidebarState';
|
||||||
import { UpgradeBEBannerWrapper } from './UpgradeBEBanner';
|
import { UpgradeBEBannerWrapper } from './UpgradeBEBanner';
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
|
return (
|
||||||
|
/* in the future (when we remove r2a) this should wrap the whole app - to change root styles */
|
||||||
|
<SidebarProvider>
|
||||||
|
<InnerSidebar />
|
||||||
|
</SidebarProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InnerSidebar() {
|
||||||
const { isAdmin, user } = useUser();
|
const { isAdmin, user } = useUser();
|
||||||
const isTeamLeader = useIsTeamLeader(user) as boolean;
|
const isTeamLeader = useIsTeamLeader(user) as boolean;
|
||||||
|
const { isOpen } = useSidebarState();
|
||||||
|
|
||||||
const settingsQuery = usePublicSettings();
|
const settingsQuery = usePublicSettings();
|
||||||
|
|
||||||
|
@ -28,37 +38,41 @@ export function Sidebar() {
|
||||||
const { LogoURL } = settingsQuery.data;
|
const { LogoURL } = settingsQuery.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
/* in the future (when we remove r2a) this should wrap the whole app - to change root styles */
|
<div className={clsx(styles.root, 'sidebar flex flex-col')}>
|
||||||
<SidebarProvider>
|
<UpgradeBEBannerWrapper />
|
||||||
<div className={clsx(styles.root, 'sidebar flex flex-col')}>
|
<nav
|
||||||
<UpgradeBEBannerWrapper />
|
className={clsx(
|
||||||
<nav
|
styles.nav,
|
||||||
|
'flex flex-1 flex-col overflow-y-auto py-5 pl-5',
|
||||||
|
{ 'pr-5': isOpen }
|
||||||
|
)}
|
||||||
|
aria-label="Main"
|
||||||
|
>
|
||||||
|
<Header logo={LogoURL} />
|
||||||
|
{/* negative margin + padding -> scrollbar won't hide the content */}
|
||||||
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
styles.nav,
|
styles.navListContainer,
|
||||||
'flex flex-1 flex-col overflow-y-auto p-5'
|
'mt-6 flex-1 overflow-y-auto [color-scheme:light] be:[color-scheme:dark] th-dark:[color-scheme:dark] th-highcontrast:[color-scheme:dark]',
|
||||||
|
{ 'pr-5 -mr-5': isOpen }
|
||||||
)}
|
)}
|
||||||
aria-label="Main"
|
|
||||||
>
|
>
|
||||||
<Header logo={LogoURL} />
|
<ul className={clsx('space-y-5', { 'w-[32px]': !isOpen })}>
|
||||||
{/* negative margin + padding -> scrollbar won't hide the content */}
|
<SidebarItem
|
||||||
<div className="-mr-4 mt-6 flex-1 overflow-y-auto pr-4">
|
to="portainer.home"
|
||||||
<ul className="space-y-9">
|
icon={Home}
|
||||||
<SidebarItem
|
label="Home"
|
||||||
to="portainer.home"
|
data-cy="portainerSidebar-home"
|
||||||
icon={Home}
|
/>
|
||||||
label="Home"
|
<EnvironmentSidebar />
|
||||||
data-cy="portainerSidebar-home"
|
{isAdmin && <EdgeComputeSidebar />}
|
||||||
/>
|
<SettingsSidebar isAdmin={isAdmin} isTeamLeader={isTeamLeader} />
|
||||||
<EnvironmentSidebar />
|
</ul>
|
||||||
{isAdmin && <EdgeComputeSidebar />}
|
</div>
|
||||||
<SettingsSidebar isAdmin={isAdmin} isTeamLeader={isTeamLeader} />
|
<div className="mt-auto pt-8">
|
||||||
</ul>
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-auto pt-8">
|
</nav>
|
||||||
<Footer />
|
</div>
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</SidebarProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,52 +1,71 @@
|
||||||
import { ReactNode } from 'react';
|
import { Icon as IconTest } from 'lucide-react';
|
||||||
import { Icon } from 'lucide-react';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { AutomationTestingProps } from '@/types';
|
import { AutomationTestingProps } from '@/types';
|
||||||
|
|
||||||
|
import { Icon } from '@@/Icon';
|
||||||
|
|
||||||
|
import { useSidebarState } from '../useSidebarState';
|
||||||
|
|
||||||
import { Wrapper } from './Wrapper';
|
import { Wrapper } from './Wrapper';
|
||||||
import { Menu } from './Menu';
|
import { SidebarTooltip } from './SidebarTooltip';
|
||||||
import { Head } from './Head';
|
import { useSidebarSrefActive } from './useSidebarSrefActive';
|
||||||
import { getPathsForChildren } from './utils';
|
|
||||||
|
|
||||||
interface Props extends AutomationTestingProps {
|
interface Props extends AutomationTestingProps {
|
||||||
icon?: Icon;
|
icon?: IconTest;
|
||||||
to: string;
|
to: string;
|
||||||
params?: object;
|
params?: object;
|
||||||
label: string;
|
label: string;
|
||||||
children?: ReactNode;
|
isSubMenu?: boolean;
|
||||||
openOnPaths?: string[];
|
ignorePaths?: string[];
|
||||||
|
includePaths?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SidebarItem({
|
export function SidebarItem({
|
||||||
children,
|
|
||||||
icon,
|
icon,
|
||||||
to,
|
to,
|
||||||
params,
|
params,
|
||||||
label,
|
label,
|
||||||
openOnPaths = [],
|
isSubMenu = false,
|
||||||
|
ignorePaths = [],
|
||||||
|
includePaths = [],
|
||||||
'data-cy': dataCy,
|
'data-cy': dataCy,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const childrenPath = getPathsForChildren(children);
|
const { isOpen } = useSidebarState();
|
||||||
const head = (
|
const anchorProps = useSidebarSrefActive(to, undefined, params, undefined, {
|
||||||
<Head
|
ignorePaths,
|
||||||
icon={icon}
|
includePaths,
|
||||||
to={to}
|
});
|
||||||
params={params}
|
|
||||||
label={label}
|
|
||||||
ignorePaths={childrenPath}
|
|
||||||
data-cy={dataCy}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
const anchor = (
|
||||||
<Wrapper label={label} className="sidebar">
|
<Wrapper label={label}>
|
||||||
{children ? (
|
<a
|
||||||
<Menu head={head} openOnPaths={[...openOnPaths, ...childrenPath]}>
|
href={anchorProps.href}
|
||||||
{children}
|
onClick={anchorProps.onClick}
|
||||||
</Menu>
|
className={clsx(
|
||||||
) : (
|
anchorProps.className,
|
||||||
head
|
'text-inherit no-underline hover:text-inherit hover:no-underline focus:text-inherit focus:no-underline',
|
||||||
)}
|
'flex h-8 w-full flex-1 items-center space-x-4 rounded-md text-sm',
|
||||||
|
'transition-colors duration-200 hover:bg-blue-5/20 be:hover:bg-gray-5/20 th-dark:hover:bg-gray-true-5/20',
|
||||||
|
{
|
||||||
|
// submenu items are always expanded (in a tooltip or in the sidebar)
|
||||||
|
'w-full justify-start px-3': isOpen || isSubMenu,
|
||||||
|
'w-8 justify-center': !isOpen && !isSubMenu,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
data-cy={dataCy}
|
||||||
|
>
|
||||||
|
{!!icon && <Icon icon={icon} className={clsx('flex [&>svg]:w-4')} />}
|
||||||
|
{(isOpen || isSubMenu) && <span>{label}</span>}
|
||||||
|
</a>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isOpen || isSubMenu) return anchor;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarTooltip content={<span className="text-sm">{label}</span>}>
|
||||||
|
<span className="w-full">{anchor}</span>
|
||||||
|
</SidebarTooltip>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { ChevronDown } from 'lucide-react';
|
||||||
|
import { PropsWithChildren, useState } from 'react';
|
||||||
|
|
||||||
|
import { Icon } from '@@/Icon';
|
||||||
|
import { Link } from '@@/Link';
|
||||||
|
|
||||||
|
import { useSidebarState } from '../useSidebarState';
|
||||||
|
|
||||||
|
import { Wrapper } from './Wrapper';
|
||||||
|
import { PathOptions, useSidebarSrefActive } from './useSidebarSrefActive';
|
||||||
|
import { SidebarTooltip } from './SidebarTooltip';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
to: string;
|
||||||
|
'data-cy': string;
|
||||||
|
pathOptions?: PathOptions;
|
||||||
|
params?: object;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SidebarParent({
|
||||||
|
children,
|
||||||
|
icon,
|
||||||
|
label: title,
|
||||||
|
to,
|
||||||
|
params,
|
||||||
|
pathOptions,
|
||||||
|
'data-cy': dataCy,
|
||||||
|
}: PropsWithChildren<Props>) {
|
||||||
|
const anchorProps = useSidebarSrefActive(
|
||||||
|
to,
|
||||||
|
undefined,
|
||||||
|
params,
|
||||||
|
{},
|
||||||
|
pathOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasActiveChild = !!anchorProps.className;
|
||||||
|
|
||||||
|
const { isOpen: isSidebarOpen } = useSidebarState();
|
||||||
|
|
||||||
|
const [isExpanded, setIsExpanded] = useState(hasActiveChild);
|
||||||
|
|
||||||
|
const parentItem = (
|
||||||
|
<Wrapper className="flex flex-col">
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'w-full h-8 items-center ease-in-out transition-colors flex duration-200 hover:bg-blue-5/20 be:hover:bg-gray-5/20 th-dark:hover:bg-gray-true-5/20 rounded-md',
|
||||||
|
isSidebarOpen && 'pl-3',
|
||||||
|
// only highlight the parent when the sidebar is closed/contracted and a child item is selected
|
||||||
|
(!isSidebarOpen || !isExpanded) && anchorProps.className
|
||||||
|
)}
|
||||||
|
data-cy={dataCy}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex-1 h-full cursor-pointer flex items-center border-none bg-transparent"
|
||||||
|
onClick={() => setIsExpanded(true)}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
to={to}
|
||||||
|
params={params}
|
||||||
|
className={clsx(
|
||||||
|
'w-full h-full font-medium items-center flex list-none border-none text-gray-5 hover:text-gray-5 hover:no-underline focus:text-gray-5 focus:no-underline',
|
||||||
|
{
|
||||||
|
'justify-start': isSidebarOpen,
|
||||||
|
'justify-center': !isSidebarOpen,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon icon={icon} />
|
||||||
|
{isSidebarOpen && <span className="ml-4">{title}</span>}
|
||||||
|
</Link>
|
||||||
|
</button>
|
||||||
|
{isSidebarOpen && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex-none border-none bg-transparent flex items-center group p-0 px-3 h-8"
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center group-hover:bg-blue-5 be:group-hover:bg-gray-5 group-hover:th-dark:bg-gray-true-7 group-hover:bg-opacity-10 be:group-hover:bg-opacity-10 rounded-full p-[3px] transition ease-in-out">
|
||||||
|
<Icon
|
||||||
|
icon={ChevronDown}
|
||||||
|
size="md"
|
||||||
|
className={clsx('transition ease-in-out', {
|
||||||
|
'rotate-180': isExpanded,
|
||||||
|
'rotate-0': !isExpanded,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
const childList = (
|
||||||
|
<ul
|
||||||
|
// pl-11 must be important because it needs to avoid the padding from '.root ul' in sidebar.module.css
|
||||||
|
className={clsx('text-white !pl-11', {
|
||||||
|
hidden: !isExpanded,
|
||||||
|
block: isExpanded,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isSidebarOpen)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{parentItem}
|
||||||
|
{childList}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarTooltip
|
||||||
|
content={
|
||||||
|
<ul>
|
||||||
|
<li className="flex items-center space-x-2 text-sm mb-1">
|
||||||
|
<span>{title}</span>
|
||||||
|
</li>
|
||||||
|
<div className="bg-blue-8/50 be:bg-gray-8 th-dark:bg-gray-true-8 rounded">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>{parentItem}</span>
|
||||||
|
</SidebarTooltip>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
import Tippy, { TippyProps } from '@tippyjs/react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
content: React.ReactNode;
|
||||||
|
children?: TippyProps['children'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SidebarTooltip({ children, content }: Props) {
|
||||||
|
return (
|
||||||
|
<Tippy
|
||||||
|
className="sidebar !rounded-md bg-blue-9 p-3 !opacity-100 be:bg-gray-9 th-dark:bg-gray-true-9"
|
||||||
|
content={content}
|
||||||
|
delay={[0, 0]}
|
||||||
|
duration={[0, 0]}
|
||||||
|
zIndex={1000}
|
||||||
|
placement="right"
|
||||||
|
arrow
|
||||||
|
allowHTML
|
||||||
|
interactive
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Tippy>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
import {
|
||||||
|
TransitionOptions,
|
||||||
|
useCurrentStateAndParams,
|
||||||
|
useSrefActive,
|
||||||
|
} from '@uirouter/react';
|
||||||
|
|
||||||
|
export type PathOptions = {
|
||||||
|
ignorePaths?: string[];
|
||||||
|
includePaths?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends useSrefActive by ignoring or including paths and updating the classNames field returned when a child route is active.
|
||||||
|
* @param to The route to match
|
||||||
|
* @param activeClassName The active class names to return
|
||||||
|
* @param params The route params
|
||||||
|
* @param options The transition options
|
||||||
|
* @param pathOptions The paths to ignore/include
|
||||||
|
*/
|
||||||
|
export function useSidebarSrefActive(
|
||||||
|
to: string,
|
||||||
|
// default values are the classes used in the sidebar for an active item
|
||||||
|
activeClassName: string = 'bg-blue-5/25 be:bg-gray-5/25 th-dark:bg-gray-true-5/25',
|
||||||
|
params: Partial<Record<string, string>> = {},
|
||||||
|
options: TransitionOptions = {},
|
||||||
|
pathOptions: PathOptions = {
|
||||||
|
ignorePaths: [],
|
||||||
|
includePaths: [],
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { state: { name: stateName = '' } = {} } = useCurrentStateAndParams();
|
||||||
|
const anchorProps = useSrefActive(to, params || {}, activeClassName, options);
|
||||||
|
|
||||||
|
// overwrite the className to '' if the the current route is in ignorePaths
|
||||||
|
const isIgnorePathInRoute = pathOptions.ignorePaths?.some((path) =>
|
||||||
|
stateName.includes(path)
|
||||||
|
);
|
||||||
|
if (isIgnorePathInRoute) {
|
||||||
|
return { ...anchorProps, className: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// overwrite the className to activeClassName if the the current route is in includePaths
|
||||||
|
const isIncludePathInRoute = pathOptions.includePaths?.some((path) =>
|
||||||
|
stateName.includes(path)
|
||||||
|
);
|
||||||
|
if (isIncludePathInRoute) {
|
||||||
|
return { ...anchorProps, className: activeClassName };
|
||||||
|
}
|
||||||
|
|
||||||
|
return anchorProps;
|
||||||
|
}
|
|
@ -9,10 +9,6 @@ function isReactElement(element: ReactNode): element is ReactElement {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPathsForChildren(children: ReactNode): string[] {
|
|
||||||
return Children.map(children, (child) => getPaths(child, []))?.flat() || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPaths(element: ReactNode, paths: string[]): string[] {
|
export function getPaths(element: ReactNode, paths: string[]): string[] {
|
||||||
if (!isReactElement(element)) {
|
if (!isReactElement(element)) {
|
||||||
return paths;
|
return paths;
|
||||||
|
|
Loading…
Reference in New Issue