diff --git a/web/packages/design/src/Alert/Alert.tsx b/web/packages/design/src/Alert/Alert.tsx index 5e471280ffe24..c674aff2b1b79 100644 --- a/web/packages/design/src/Alert/Alert.tsx +++ b/web/packages/design/src/Alert/Alert.tsx @@ -20,6 +20,8 @@ import React, { useState } from 'react'; import styled, { useTheme } from 'styled-components'; import { style, color, ColorProps } from 'styled-system'; +import { IconProps } from 'design/Icon/Icon'; + import { space, SpaceProps, width, WidthProps } from '../system'; import { Theme } from '../theme'; import * as Icon from '../Icon'; @@ -111,7 +113,7 @@ interface Props { /** Additional description to be displayed below the main content. */ details?: React.ReactNode; /** Overrides the icon specified by {@link AlertProps.kind}. */ - icon?: React.ComponentType; + icon?: React.ComponentType; /** If specified, causes the alert to display a primary action button. */ primaryAction?: Action; /** If specified, causes the alert to display a secondary action button. */ @@ -253,8 +255,8 @@ const AlertIcon = ({ ...otherProps }: { kind: AlertKind | BannerKind; - customIcon?: React.ComponentType; -} & Icon.IconProps) => { + customIcon?: React.ComponentType; +} & IconProps) => { const commonProps = { role: 'graphics-symbol', ...otherProps }; if (CustomIcon) { return ; diff --git a/web/packages/design/src/Icon/Icons.story.tsx b/web/packages/design/src/Icon/Icons.story.tsx index a511f5692b4ca..4a792aeec912c 100644 --- a/web/packages/design/src/Icon/Icons.story.tsx +++ b/web/packages/design/src/Icon/Icons.story.tsx @@ -135,6 +135,7 @@ export const Icons = () => ( + @@ -152,6 +153,7 @@ export const Icons = () => ( + @@ -170,6 +172,7 @@ export const Icons = () => ( + diff --git a/web/packages/design/src/Icon/Icons/KeyHole.tsx b/web/packages/design/src/Icon/Icons/KeyHole.tsx new file mode 100644 index 0000000000000..afc86a7543647 --- /dev/null +++ b/web/packages/design/src/Icon/Icons/KeyHole.tsx @@ -0,0 +1,73 @@ +/** + * Teleport + * Copyright (C) 2023 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/* MIT License + +Copyright (c) 2020 Phosphor Icons + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +import React from 'react'; + +import { Icon, IconProps } from '../Icon'; + +/* + +THIS FILE IS GENERATED. DO NOT EDIT. + +*/ + +export function KeyHole({ size = 24, color, ...otherProps }: IconProps) { + return ( + + + + + ); +} diff --git a/web/packages/design/src/Icon/Icons/LockKey.tsx b/web/packages/design/src/Icon/Icons/LockKey.tsx new file mode 100644 index 0000000000000..65a953566d71d --- /dev/null +++ b/web/packages/design/src/Icon/Icons/LockKey.tsx @@ -0,0 +1,73 @@ +/** + * Teleport + * Copyright (C) 2023 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/* MIT License + +Copyright (c) 2020 Phosphor Icons + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +import React from 'react'; + +import { Icon, IconProps } from '../Icon'; + +/* + +THIS FILE IS GENERATED. DO NOT EDIT. + +*/ + +export function LockKey({ size = 24, color, ...otherProps }: IconProps) { + return ( + + + + + ); +} diff --git a/web/packages/design/src/Icon/Icons/PlugsConnected.tsx b/web/packages/design/src/Icon/Icons/PlugsConnected.tsx new file mode 100644 index 0000000000000..ec991cd95b2ee --- /dev/null +++ b/web/packages/design/src/Icon/Icons/PlugsConnected.tsx @@ -0,0 +1,72 @@ +/** + * Teleport + * Copyright (C) 2023 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/* MIT License + +Copyright (c) 2020 Phosphor Icons + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +import React from 'react'; + +import { Icon, IconProps } from '../Icon'; + +/* + +THIS FILE IS GENERATED. DO NOT EDIT. + +*/ + +export function PlugsConnected({ size = 24, color, ...otherProps }: IconProps) { + return ( + + + + + + + + ); +} diff --git a/web/packages/design/src/Icon/assets/KeyHole.svg b/web/packages/design/src/Icon/assets/KeyHole.svg new file mode 100644 index 0000000000000..c4c69dfaf4c8e --- /dev/null +++ b/web/packages/design/src/Icon/assets/KeyHole.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/packages/design/src/Icon/assets/LockKey.svg b/web/packages/design/src/Icon/assets/LockKey.svg new file mode 100644 index 0000000000000..554f55b8d71d4 --- /dev/null +++ b/web/packages/design/src/Icon/assets/LockKey.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/packages/design/src/Icon/assets/PlugsConnected.svg b/web/packages/design/src/Icon/assets/PlugsConnected.svg new file mode 100644 index 0000000000000..2ce54755155bf --- /dev/null +++ b/web/packages/design/src/Icon/assets/PlugsConnected.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/web/packages/design/src/Icon/index.ts b/web/packages/design/src/Icon/index.ts index 57f2fa84d3a11..6e24c134497db 100644 --- a/web/packages/design/src/Icon/index.ts +++ b/web/packages/design/src/Icon/index.ts @@ -22,7 +22,7 @@ THIS FILE IS GENERATED. DO NOT EDIT. */ -export { Icon, type IconProps } from './Icon'; +export { Icon } from './Icon'; export { Add } from './Icons/Add'; export { AddCircle } from './Icons/AddCircle'; @@ -121,6 +121,7 @@ export { Info } from './Icons/Info'; export { Integrations } from './Icons/Integrations'; export { Invoices } from './Icons/Invoices'; export { Key } from './Icons/Key'; +export { KeyHole } from './Icons/KeyHole'; export { Keyboard } from './Icons/Keyboard'; export { Keypair } from './Icons/Keypair'; export { Kubernetes } from './Icons/Kubernetes'; @@ -138,6 +139,7 @@ export { ListMagnifyingGlass } from './Icons/ListMagnifyingGlass'; export { ListThin } from './Icons/ListThin'; export { ListView } from './Icons/ListView'; export { Lock } from './Icons/Lock'; +export { LockKey } from './Icons/LockKey'; export { Logout } from './Icons/Logout'; export { Magnifier } from './Icons/Magnifier'; export { MagnifyingMinus } from './Icons/MagnifyingMinus'; @@ -156,6 +158,7 @@ export { PaperPlane } from './Icons/PaperPlane'; export { Password } from './Icons/Password'; export { Pencil } from './Icons/Pencil'; export { Planet } from './Icons/Planet'; +export { PlugsConnected } from './Icons/PlugsConnected'; export { Plus } from './Icons/Plus'; export { PowerSwitch } from './Icons/PowerSwitch'; export { Printer } from './Icons/Printer'; diff --git a/web/packages/teleport/src/Main/MainContainer.tsx b/web/packages/teleport/src/Main/MainContainer.tsx index 4f7910b80e5dd..75a4109c32d15 100644 --- a/web/packages/teleport/src/Main/MainContainer.tsx +++ b/web/packages/teleport/src/Main/MainContainer.tsx @@ -29,11 +29,9 @@ export const MainContainer = styled.div` --sidebar-width: 256px; --sidenav-width: 76px; --sidenav-panel-width: 224px; + overflow: hidden; margin-top: ${p => p.theme.topBarHeight[0]}px; @media screen and (min-width: ${p => p.theme.breakpoints.small}px) { margin-top: ${p => p.theme.topBarHeight[1]}px; } - @media screen and (min-width: ${p => p.theme.breakpoints.large}px) { - margin-top: ${p => p.theme.topBarHeight[2]}px; - } `; diff --git a/web/packages/teleport/src/Navigation/SideNavigation/CategoryIcon.tsx b/web/packages/teleport/src/Navigation/SideNavigation/CategoryIcon.tsx index feb2135801050..944b53c218b2e 100644 --- a/web/packages/teleport/src/Navigation/SideNavigation/CategoryIcon.tsx +++ b/web/packages/teleport/src/Navigation/SideNavigation/CategoryIcon.tsx @@ -41,7 +41,7 @@ export function CategoryIcon({ Icon = Icons.Server; break; case NavigationCategory.Access: - Icon = Icons.Lock; + Icon = Icons.KeyHole; break; case NavigationCategory.Identity: Icon = Icons.FingerprintSimple; diff --git a/web/packages/teleport/src/Navigation/SideNavigation/Navigation.tsx b/web/packages/teleport/src/Navigation/SideNavigation/Navigation.tsx index 9d39688b52156..2099a09abd61f 100644 --- a/web/packages/teleport/src/Navigation/SideNavigation/Navigation.tsx +++ b/web/packages/teleport/src/Navigation/SideNavigation/Navigation.tsx @@ -16,10 +16,10 @@ * along with this program. If not, see . */ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useEffect, useRef } from 'react'; import styled, { useTheme } from 'styled-components'; import { matchPath, useHistory } from 'react-router'; -import { Text, Flex, Box } from 'design'; +import { Text, Flex, Box, P2 } from 'design'; import { ToolTipInfo } from 'shared/components/ToolTip'; @@ -36,11 +36,12 @@ import { import { zIndexMap } from './zIndexMap'; import { + CustomNavigationSubcategory, NAVIGATION_CATEGORIES, - STANDALONE_CATEGORIES, SidenavCategory, } from './categories'; import { SearchSection } from './Search'; +import { ResourcesSection } from './ResourcesSection'; import type * as history from 'history'; import type { TeleportFeature } from 'teleport/types'; @@ -86,6 +87,20 @@ export type NavigationSubsection = { icon: (props) => JSX.Element; parent?: TeleportFeature; searchableTags?: string[]; + /** + * customRouteMatchFn is a custom function for determining whether this subsection is currently active, + * this is useful in cases where a simple base route match isn't sufficient. + */ + customRouteMatchFn?: (currentViewRoute: string) => boolean; + /** + * subCategory is the subcategory (ie. subsection grouping) this subsection should be under, if applicable. + * */ + subCategory?: CustomNavigationSubcategory; + /** + * onClick is custom code that can be run when clicking on the subsection. + * Note that this is merely extra logic, and does not replace the default routing behaviour of a subsection which will navigate the user to the route. + */ + onClick?: () => void; }; function getNavigationSections( @@ -94,7 +109,6 @@ function getNavigationSections( const navigationSections = NAVIGATION_CATEGORIES.map(category => ({ category, subsections: getSubsectionsForCategory(category, features), - standalone: STANDALONE_CATEGORIES.indexOf(category) !== -1, })); return navigationSections; @@ -123,6 +137,8 @@ function getSubsectionsForCategory( }); } +// getNavSubsectionForRoute returns the sidenav subsection that the user is correctly on (based on route). +// Note that it is possible for this not to return anything, such as in the case where the user is on a page that isn't in the sidenav (eg. Account Settings). function getNavSubsectionForRoute( features: TeleportFeature[], route: history.Location | Location @@ -150,14 +166,64 @@ function getNavSubsectionForRoute( }; } +/** + * useDebounceClose adds a debounce to closing drawers, this is to prevent the drawer closing if the user overshoots it, giving them a slight delay to re-enter the drawer. + */ +function useDebounceClose( + value: T | null, + delay: number, + isClosing: boolean +): T | null { + const [debouncedValue, setDebouncedValue] = useState(value); + const timeoutRef = useRef(); + + useEffect(() => { + // Clear any existing timeout + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + // If we're closing the drarwer as opposed to switching to a different section (value is null and isClosing is true), apply debounce. + if (value === null && isClosing) { + timeoutRef.current = setTimeout(() => { + setDebouncedValue(null); + }, delay); + } else { + // For opening or any other change, update immediately. + setDebouncedValue(value); + } + + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, [value, delay, isClosing]); + + return debouncedValue; +} + export function Navigation() { const features = useFeatures(); const history = useHistory(); - const [expandedSection, setExpandedSection] = - useState(null); - const currentView = getNavSubsectionForRoute(features, history.location); + const [targetSection, setTargetSection] = useState( + null + ); + const [isClosing, setIsClosing] = useState(false); + const debouncedSection = useDebounceClose(targetSection, 200, isClosing); const [previousExpandedSection, setPreviousExpandedSection] = useState(); + const navigationTimeoutRef = useRef(); + + // Clear navigation timeout on unmount. + useEffect(() => { + return () => { + if (navigationTimeoutRef.current) { + clearTimeout(navigationTimeoutRef.current); + } + }; + }, []); + const currentView = getNavSubsectionForRoute(features, history.location); const navSections = getNavigationSections(features).filter( section => section.subsections.length @@ -165,24 +231,59 @@ export function Navigation() { const handleSetExpandedSection = useCallback( (section: NavigationSection) => { + setIsClosing(false); if (!section.standalone) { - setPreviousExpandedSection(expandedSection); - setExpandedSection(section); + setPreviousExpandedSection(debouncedSection); + setTargetSection(section); } else { setPreviousExpandedSection(null); - setExpandedSection(null); + setTargetSection(null); } }, - [expandedSection] + [debouncedSection] ); - const resetExpandedSection = useCallback(() => { + const resetExpandedSection = useCallback((closeAfterDelay = true) => { + setIsClosing(closeAfterDelay); setPreviousExpandedSection(null); - setExpandedSection(null); + setTargetSection(null); }, []); + // Handler for navigation actions + const handleNavigation = useCallback( + (route: string) => { + history.push(route); + + // Clear any existing timeout + if (navigationTimeoutRef.current) { + clearTimeout(navigationTimeoutRef.current); + } + + // Add a small delay to the close to allow the user to see some feedback (see the section they clicked become active). + navigationTimeoutRef.current = setTimeout(() => { + resetExpandedSection(false); + }, 150); + }, + [resetExpandedSection, history] + ); + + // Hide the nav if the current feature has hideNavigation set to true. + const hideNav = features.find( + f => + f.route && + matchPath(history.location.pathname, { + path: f.route.path, + exact: f.route.exact ?? false, + }) + )?.hideNavigation; + + if (hideNav) { + return null; + } + return ( resetExpandedSection()} onKeyUp={e => e.key === 'Escape' && resetExpandedSection()} onBlur={(event: React.FocusEvent) => { @@ -200,73 +301,87 @@ export function Navigation() { + {navSections.map(section => ( -
handleSetExpandedSection(section)} - aria-controls={`panel-${expandedSection?.category}`} - onClick={() => { - if (section.standalone) { - history.push(section.subsections[0].route); + + {section.category === 'Add New' && } +
handleSetExpandedSection(section)} + aria-controls={`panel-${debouncedSection?.category}`} + onClick={() => { + if (section.standalone) { + handleNavigation(section.subsections[0].route); + } + }} + isExpanded={ + !!debouncedSection && + !debouncedSection.standalone && + section.category === debouncedSection?.category } - }} - isExpanded={ - !!expandedSection && - !expandedSection.standalone && - section.category === expandedSection?.category - } - > - handleSetExpandedSection(section)} > - handleSetExpandedSection(section)} + onMouseEnter={() => handleSetExpandedSection(section)} > - - - - {section.category} - - - {!section.standalone && - section.subsections.map(section => ( - - - {section.title} - - ))} - - {cfg.edition === 'oss' && } - {cfg.edition === 'community' && } - - -
+ + + + {section.category} + + + {!section.standalone && + section.subsections.map(subsection => ( + { + e.preventDefault(); + handleNavigation(subsection.route); + }} + > + + {subsection.title} + + ))} + + {cfg.edition === 'oss' && } + {cfg.edition === 'community' && } + + +
+ ))}
@@ -356,3 +471,10 @@ const SubText = styled(Text)` color: ${props => props.theme.colors.text.disabled}; font-size: ${props => props.theme.fontSizes[1]}px; `; + +const Divider = styled.div` + z-index: ${zIndexMap.sideNavButtons}; + height: 1px; + background: ${props => props.theme.colors.interactive.tonal.neutral[1]}; + width: 60px; +`; diff --git a/web/packages/teleport/src/Navigation/SideNavigation/ResourcesSection.tsx b/web/packages/teleport/src/Navigation/SideNavigation/ResourcesSection.tsx new file mode 100644 index 0000000000000..dbcfbdc903195 --- /dev/null +++ b/web/packages/teleport/src/Navigation/SideNavigation/ResourcesSection.tsx @@ -0,0 +1,349 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React from 'react'; +import styled from 'styled-components'; +import { matchPath } from 'react-router'; + +import { Box, Flex, Text } from 'design'; +import * as Icons from 'design/Icon'; + +import { DefaultTab } from 'gen-proto-ts/teleport/userpreferences/v1/unified_resource_preferences_pb'; +import { UserPreferences } from 'gen-proto-ts/teleport/userpreferences/v1/userpreferences_pb'; + +import cfg from 'teleport/config'; +import useStickyClusterId from 'teleport/useStickyClusterId'; +import { encodeUrlQueryParams } from 'teleport/components/hooks/useUrlFiltering'; +import { EncodeUrlQueryParamsProps } from 'teleport/components/hooks/useUrlFiltering/encodeUrlQueryParams'; +import { ResourceIdKind } from 'teleport/services/agents'; +import { useUser } from 'teleport/User/UserContext'; + +import { NavigationSubsection, NavigationSection } from './Navigation'; +import { + Section, + RightPanel, + verticalPadding, + SubsectionItem, +} from './Section'; +import { CustomNavigationSubcategory, NavigationCategory } from './categories'; + +/** + * getResourcesSectionForSearch returns a NavigationSection for resources, + * this is only used for the sake of indexing these subsections in the sidenav search. + */ +export function getResourcesSectionForSearch( + subsectionProps: GetSubsectionProps +): NavigationSection { + return { + category: NavigationCategory.Resources, + subsections: getResourcesSubsections(subsectionProps), + }; +} + +type GetSubsectionProps = { + clusterId: string; + preferences: UserPreferences; + updatePreferences: (preferences: Partial) => Promise; + searchParams: URLSearchParams; +}; + +function encodeUrlQueryParamsWithTypedKinds( + params: Omit & { + kinds?: ResourceIdKind[]; + } +) { + return encodeUrlQueryParams(params); +} + +function getResourcesSubsections({ + clusterId, + preferences, + updatePreferences, + searchParams, +}: GetSubsectionProps): NavigationSubsection[] { + const baseRoute = cfg.getUnifiedResourcesRoute(clusterId); + + const setPinnedUserPreference = (pinnedOnly: boolean) => { + // Return early if the current user preference already matches the pinnedOnly param provided, since nothing needs to be done. + if ( + (pinnedOnly && + preferences?.unifiedResourcePreferences?.defaultTab === + DefaultTab.PINNED) || + (!pinnedOnly && + (preferences?.unifiedResourcePreferences?.defaultTab === + DefaultTab.ALL || + preferences?.unifiedResourcePreferences?.defaultTab === + DefaultTab.UNSPECIFIED)) + ) { + return; + } + + updatePreferences({ + ...preferences, + unifiedResourcePreferences: { + ...preferences?.unifiedResourcePreferences, + defaultTab: pinnedOnly ? DefaultTab.PINNED : DefaultTab.ALL, + }, + }); + }; + + const currentKinds = searchParams + .getAll('kinds') + .flatMap(k => k.split(',')) + .filter(Boolean); + const isPinnedOnly = + preferences?.unifiedResourcePreferences?.defaultTab === DefaultTab.PINNED; + + // isKindActive returns true if we are currently filtering for only the provided kind of resource. + const isKindActive = (kind: ResourceIdKind) => { + // This subsection for this kind should only be marked active when it is the only kind being filtered for, + // if there are multiple kinds then the "All Resources" button should be active. + return currentKinds.length === 1 && currentKinds[0] === kind; + }; + + const allResourcesRoute = encodeUrlQueryParamsWithTypedKinds({ + pathname: baseRoute, + pinnedOnly: false, + }); + const pinnedOnlyRoute = encodeUrlQueryParamsWithTypedKinds({ + pathname: baseRoute, + pinnedOnly: true, + }); + const applicationsOnlyRoute = encodeUrlQueryParamsWithTypedKinds({ + pathname: baseRoute, + kinds: ['app'], + pinnedOnly: false, + }); + const databasesOnlyRoute = encodeUrlQueryParamsWithTypedKinds({ + pathname: baseRoute, + kinds: ['db'], + pinnedOnly: false, + }); + const desktopsOnlyRoute = encodeUrlQueryParamsWithTypedKinds({ + pathname: baseRoute, + kinds: ['windows_desktop'], + pinnedOnly: false, + }); + const kubesOnlyRoute = encodeUrlQueryParamsWithTypedKinds({ + pathname: baseRoute, + kinds: ['kube_cluster'], + pinnedOnly: false, + }); + const nodesOnlyRoute = encodeUrlQueryParamsWithTypedKinds({ + pathname: baseRoute, + kinds: ['node'], + pinnedOnly: false, + }); + + return [ + { + title: 'All Resources', + icon: Icons.Server, + route: allResourcesRoute, + searchableTags: ['resources', 'resources', 'all resources'], + category: NavigationCategory.Resources, + exact: false, + customRouteMatchFn: currentViewRoute => + !!matchPath(currentViewRoute, { + path: cfg.routes.unifiedResources, + exact: false, + }) && + !isPinnedOnly && + currentKinds.length !== 1, + onClick: () => setPinnedUserPreference(false), + }, + { + title: 'Pinned Resources', + icon: Icons.PushPin, + route: pinnedOnlyRoute, + searchableTags: ['resources', 'resources', 'pinned resources'], + category: NavigationCategory.Resources, + exact: false, + customRouteMatchFn: () => isPinnedOnly && currentKinds.length !== 1, + onClick: () => setPinnedUserPreference(true), + }, + { + title: 'Applications', + icon: Icons.Application, + route: applicationsOnlyRoute, + searchableTags: ['resources', 'apps', 'applications'], + category: NavigationCategory.Resources, + exact: false, + customRouteMatchFn: () => isKindActive('app'), + onClick: () => setPinnedUserPreference(false), + subCategory: CustomNavigationSubcategory.FilteredViews, + }, + { + title: 'Databases', + icon: Icons.Database, + route: databasesOnlyRoute, + searchableTags: ['resources', 'dbs', 'databases'], + category: NavigationCategory.Resources, + exact: false, + customRouteMatchFn: () => isKindActive('db'), + onClick: () => setPinnedUserPreference(false), + subCategory: CustomNavigationSubcategory.FilteredViews, + }, + { + title: 'Desktops', + icon: Icons.Database, + route: desktopsOnlyRoute, + searchableTags: ['resources', 'desktops', 'rdp', 'windows'], + category: NavigationCategory.Resources, + exact: false, + customRouteMatchFn: () => isKindActive('windows_desktop'), + onClick: () => setPinnedUserPreference(false), + subCategory: CustomNavigationSubcategory.FilteredViews, + }, + { + title: 'Kubernetes', + icon: Icons.Kubernetes, + route: kubesOnlyRoute, + searchableTags: ['resources', 'k8s', 'kubes', 'kubernetes'], + category: NavigationCategory.Resources, + exact: false, + customRouteMatchFn: () => isKindActive('kube_cluster'), + onClick: () => setPinnedUserPreference(false), + subCategory: CustomNavigationSubcategory.FilteredViews, + }, + { + title: 'SSH Resources', + icon: Icons.Server, + route: nodesOnlyRoute, + searchableTags: ['resources', 'servers', 'nodes', 'ssh resources'], + category: NavigationCategory.Resources, + exact: false, + customRouteMatchFn: () => isKindActive('node'), + onClick: () => setPinnedUserPreference(false), + subCategory: CustomNavigationSubcategory.FilteredViews, + }, + ]; +} + +export function ResourcesSection({ + expandedSection, + previousExpandedSection, + handleSetExpandedSection, + currentView, +}: { + expandedSection: NavigationSection; + previousExpandedSection: NavigationSection; + currentView: NavigationSubsection; + handleSetExpandedSection: (section: NavigationSection) => void; +}) { + const { clusterId } = useStickyClusterId(); + const { preferences, updatePreferences } = useUser(); + const section: NavigationSection = { + category: NavigationCategory.Resources, + subsections: [], + }; + const baseRoute = cfg.getUnifiedResourcesRoute(clusterId); + + const searchParams = new URLSearchParams(location.search); + + const isExpanded = expandedSection?.category === NavigationCategory.Resources; + + const subsections = getResourcesSubsections({ + clusterId, + preferences, + updatePreferences, + searchParams, + }); + + const currentViewRoute = currentView?.route; + + return ( +
null} + setExpandedSection={() => handleSetExpandedSection(section)} + aria-controls={`panel-${expandedSection?.category}`} + isExpanded={isExpanded} + > + handleSetExpandedSection(section)} + > + + + + Resources + + + {subsections + .filter(section => !section.subCategory) + .map(section => ( + + + {section.title} + + ))} + + + + + Filtered Views + + + + {subsections + .filter( + section => + section.subCategory === + CustomNavigationSubcategory.FilteredViews + ) + .map(section => ( + + + {section.title} + + ))} + + +
+ ); +} + +export const Divider = styled.div` + height: 1px; + width: 100%; + background: ${props => props.theme.colors.interactive.tonal.neutral[1]}; + margin: ${props => props.theme.space[1]}px 0px + ${props => props.theme.space[1]}px 0px; +`; diff --git a/web/packages/teleport/src/Navigation/SideNavigation/Search.tsx b/web/packages/teleport/src/Navigation/SideNavigation/Search.tsx index 9ca0c39aab873..118fc905f7f79 100644 --- a/web/packages/teleport/src/Navigation/SideNavigation/Search.tsx +++ b/web/packages/teleport/src/Navigation/SideNavigation/Search.tsx @@ -23,6 +23,9 @@ import styled from 'styled-components'; import { Box, Flex, Text } from 'design'; import { height, space, color } from 'design/system'; +import useStickyClusterId from 'teleport/useStickyClusterId'; +import { useUser } from 'teleport/User/UserContext'; + import { NavigationSection, NavigationSubsection } from './Navigation'; import { Section, @@ -32,6 +35,7 @@ import { } from './Section'; import { CategoryIcon } from './CategoryIcon'; import { CustomNavigationCategory } from './categories'; +import { getResourcesSectionForSearch } from './ResourcesSection'; export function SearchSection({ navigationSections, @@ -50,6 +54,20 @@ export function SearchSection({ category: CustomNavigationCategory.Search, subsections: [], }; + const { clusterId } = useStickyClusterId(); + const { preferences, updatePreferences } = useUser(); + + const searchParams = new URLSearchParams(location.search); + + const searchableNavSections: NavigationSection[] = [ + getResourcesSectionForSearch({ + clusterId, + preferences, + updatePreferences, + searchParams, + }), + ...navigationSections, + ]; const isExpanded = expandedSection?.category === CustomNavigationCategory.Search; @@ -70,7 +88,7 @@ export function SearchSection({ onFocus={() => handleSetExpandedSection(section)} > @@ -123,7 +141,11 @@ function SearchContent({ ))} @@ -141,7 +163,12 @@ function SearchResult({ $active: boolean; }) { return ( - + ` position: fixed; left: var(--sidenav-width); height: 100%; - scrollbar-gutter: auto; scrollbar-color: ${p => p.theme.colors.spotBackground[2]} transparent; - overflow: visible; width: ${rightPanelWidth}; background: ${p => p.theme.colors.levels.surface}; z-index: ${zIndexMap.sideNavExpandedPanel}; @@ -94,18 +92,14 @@ export const RightPanel = styled(Box).attrs({ pt: 2, px: 2 })<{ top: ${p => p.theme.topBarHeight[1]}px; padding-bottom: ${p => p.theme.topBarHeight[1] + p.theme.space[2]}px; } - @media screen and (min-width: ${p => p.theme.breakpoints.large}px) { - top: ${p => p.theme.topBarHeight[2]}px; - padding-bottom: ${p => p.theme.topBarHeight[3] + p.theme.space[2]}px; - } `; export const CategoryButton = styled.button<{ $active: boolean; isExpanded: boolean; }>` - height: 60px; - width: 60px; + min-height: 60px; + min-width: 60px; cursor: pointer; outline: hidden; border: none; @@ -115,6 +109,11 @@ export const CategoryButton = styled.button<{ justify-content: center; border-radius: ${props => props.theme.radii[2]}px; z-index: ${zIndexMap.sideNavButtons}; + display: flex; + align-items: center; + justify-content: center; + gap: ${props => props.theme.space[1]}px; + font-family: ${props => props.theme.font}; font-size: ${props => props.theme.typography.body4.fontSize}; font-weight: ${props => props.theme.typography.body4.fontWeight}; @@ -164,7 +163,7 @@ export function getCategoryStyles( } ${isExpanded && ` - background: ${theme.colors.interactive.tonal.neutral[0]}; + background: ${theme.colors.interactive.tonal.neutral[0]}; color: ${theme.colors.text.main}; `} `; @@ -177,20 +176,28 @@ export function SubsectionItem({ to, exact, children, + onClick, }: { $active: boolean; to: string; exact: boolean; children: React.ReactNode; + onClick?: (event: React.MouseEvent) => void; }) { return ( - + {children} ); } -const StyledSubsectionItem = styled(NavLink)<{ +export const StyledSubsectionItem = styled(NavLink)<{ $active: boolean; }>` display: flex; @@ -214,6 +221,9 @@ export function getSubsectionStyles(theme: Theme, active: boolean) { return css` color: ${theme.colors.brand}; background: ${theme.colors.interactive.tonal.primary[0]}; + p { + font-weight: 500; + } &:focus-visible { outline: 2px solid ${theme.colors.interactive.solid.primary.default}; } diff --git a/web/packages/teleport/src/Navigation/SideNavigation/categories.ts b/web/packages/teleport/src/Navigation/SideNavigation/categories.ts index 33d6cf290e40a..6bc60bd20f818 100644 --- a/web/packages/teleport/src/Navigation/SideNavigation/categories.ts +++ b/web/packages/teleport/src/Navigation/SideNavigation/categories.ts @@ -25,24 +25,27 @@ export enum NavigationCategory { AddNew = 'Add New', } -/* CustomNavigationCategory are pseudo-categories which exist only in the nav menu, eg. Search. */ +/** + * CustomNavigationCategory are pseudo-categories which exist only in the nav menu, eg. Search. + */ export enum CustomNavigationCategory { Search = 'Search', } +/** + * CustomNavigationSubcategory are subcategories within a navigation category which can be used to + * create groupings of subsections, eg. Filtered Views. + */ +export enum CustomNavigationSubcategory { + FilteredViews = 'Filtered Views', +} + export type SidenavCategory = NavigationCategory | CustomNavigationCategory; export const NAVIGATION_CATEGORIES = [ - NavigationCategory.Resources, NavigationCategory.Access, NavigationCategory.Identity, NavigationCategory.Policy, NavigationCategory.Audit, NavigationCategory.AddNew, ]; - -export const STANDALONE_CATEGORIES = [ - NavigationCategory.AddNew, - // TODO(rudream): Remove this once shortcuts to pinned/nodes/apps/dbs/desktops/kubes are implemented. - NavigationCategory.Resources, -]; diff --git a/web/packages/teleport/src/Navigation/SideNavigation/zIndexMap.ts b/web/packages/teleport/src/Navigation/SideNavigation/zIndexMap.ts index a876bddbe8d37..745f119349d4c 100644 --- a/web/packages/teleport/src/Navigation/SideNavigation/zIndexMap.ts +++ b/web/packages/teleport/src/Navigation/SideNavigation/zIndexMap.ts @@ -17,8 +17,8 @@ */ export const zIndexMap = { - topBar: 23, - sideNavButtons: 22, - sideNavContainer: 21, - sideNavExpandedPanel: 20, + topBar: 9, + sideNavButtons: 8, + sideNavContainer: 7, + sideNavExpandedPanel: 6, }; diff --git a/web/packages/teleport/src/Notifications/Notification.story.tsx b/web/packages/teleport/src/Notifications/Notification.story.tsx index 05b76bfae920b..d8619f66a8227 100644 --- a/web/packages/teleport/src/Notifications/Notification.story.tsx +++ b/web/packages/teleport/src/Notifications/Notification.story.tsx @@ -220,7 +220,7 @@ const ListComponent = () => { css={` width: 100%; justify-content: center; - height: ${p => p.theme.topBarHeight[2]}px; + height: ${p => p.theme.topBarHeight[1]}px; `} > diff --git a/web/packages/teleport/src/TopBar/TopBarSideNav.tsx b/web/packages/teleport/src/TopBar/TopBarSideNav.tsx index a3a5d966ce2b6..c787f984fa763 100644 --- a/web/packages/teleport/src/TopBar/TopBarSideNav.tsx +++ b/web/packages/teleport/src/TopBar/TopBarSideNav.tsx @@ -85,9 +85,6 @@ export const TopBarContainer = styled(TopNav)` @media screen and (min-width: ${p => p.theme.breakpoints.small}px) { height: ${p => p.theme.topBarHeight[1]}px; } - @media screen and (min-width: ${p => p.theme.breakpoints.large}px) { - height: ${p => p.theme.topBarHeight[2]}px; - } `; const TeleportLogo = ({ CustomLogo }: TopBarProps) => { diff --git a/web/packages/teleport/src/components/Dropdown/Dropdown.tsx b/web/packages/teleport/src/components/Dropdown/Dropdown.tsx index 546dccd7c8dcc..36e47071dcfe7 100644 --- a/web/packages/teleport/src/components/Dropdown/Dropdown.tsx +++ b/web/packages/teleport/src/components/Dropdown/Dropdown.tsx @@ -50,9 +50,6 @@ export const Dropdown = styled.div` @media screen and (min-width: ${p => p.theme.breakpoints.small}px) { top: ${p => p.theme.topBarHeight[1]}px; } - @media screen and (min-width: ${p => p.theme.breakpoints.large}px) { - top: ${p => p.theme.topBarHeight[2]}px; - } `; export const DropdownItem = styled.div<{ diff --git a/web/packages/teleport/src/components/hooks/useUrlFiltering/encodeUrlQueryParams.test.ts b/web/packages/teleport/src/components/hooks/useUrlFiltering/encodeUrlQueryParams.test.ts index f68e5a0ca5733..56ac3f40c6358 100644 --- a/web/packages/teleport/src/components/hooks/useUrlFiltering/encodeUrlQueryParams.test.ts +++ b/web/packages/teleport/src/components/hooks/useUrlFiltering/encodeUrlQueryParams.test.ts @@ -29,17 +29,17 @@ const testCases: { { title: 'No query params', args: { pathname: '/foo' }, - expected: '/foo', + expected: '/foo?pinnedOnly=false', }, { title: 'Search string', args: { pathname: '/test', searchString: 'something' }, - expected: '/test?search=something', + expected: '/test?search=something&pinnedOnly=false', }, { title: 'Search string, encoded', args: { pathname: '/test', searchString: 'a$b$c' }, - expected: '/test?search=a%24b%24c', + expected: '/test?search=a%24b%24c&pinnedOnly=false', }, { title: 'Advanced search', @@ -48,7 +48,7 @@ const testCases: { searchString: 'foo=="bar"', isAdvancedSearch: true, }, - expected: '/test?query=foo%3D%3D%22bar%22', + expected: '/test?query=foo%3D%3D%22bar%22&pinnedOnly=false', }, { title: 'Search and sort', @@ -57,7 +57,7 @@ const testCases: { searchString: 'foobar', sort: { fieldName: 'name', dir: 'ASC' }, }, - expected: '/test?search=foobar&sort=name%3Aasc', + expected: '/test?search=foobar&sort=name%3Aasc&pinnedOnly=false', }, { title: 'Sort only', @@ -65,7 +65,7 @@ const testCases: { pathname: '/test', sort: { fieldName: 'name', dir: 'ASC' }, }, - expected: '/test?sort=name%3Aasc', + expected: '/test?sort=name%3Aasc&pinnedOnly=false', }, { title: 'Search, sort, and filter by kind', @@ -75,7 +75,8 @@ const testCases: { sort: { fieldName: 'name', dir: 'DESC' }, kinds: ['db', 'node'], }, - expected: '/test?search=foo&sort=name%3Adesc&kinds=db&kinds=node', + expected: + '/test?search=foo&sort=name%3Adesc&pinnedOnly=false&kinds=db&kinds=node', }, ]; diff --git a/web/packages/teleport/src/components/hooks/useUrlFiltering/encodeUrlQueryParams.ts b/web/packages/teleport/src/components/hooks/useUrlFiltering/encodeUrlQueryParams.ts index a00b57dc47695..fbcbf0217f917 100644 --- a/web/packages/teleport/src/components/hooks/useUrlFiltering/encodeUrlQueryParams.ts +++ b/web/packages/teleport/src/components/hooks/useUrlFiltering/encodeUrlQueryParams.ts @@ -45,7 +45,7 @@ export function encodeUrlQueryParams({ urlParams.append('sort', `${sort.fieldName}:${sort.dir.toLowerCase()}`); } - if (pinnedOnly) { + if (pinnedOnly !== undefined) { urlParams.append('pinnedOnly', `${pinnedOnly}`); } diff --git a/web/packages/teleport/src/components/hooks/useUrlFiltering/useUrlFiltering.ts b/web/packages/teleport/src/components/hooks/useUrlFiltering/useUrlFiltering.ts index 37652913fcb21..fa2bc1ce175a1 100644 --- a/web/packages/teleport/src/components/hooks/useUrlFiltering/useUrlFiltering.ts +++ b/web/packages/teleport/src/components/hooks/useUrlFiltering/useUrlFiltering.ts @@ -62,8 +62,16 @@ export function useUrlFiltering( const [initialParamsState] = useState(initialParams); const params = useMemo(() => { - return { ...initialParamsState, ...getResourceUrlQueryParams(search) }; - }, [initialParamsState, search]); + const urlParams = getResourceUrlQueryParams(search); + return { + ...initialParamsState, + ...urlParams, + pinnedOnly: + urlParams.pinnedOnly !== undefined + ? urlParams.pinnedOnly + : initialParamsState.pinnedOnly, + }; + }, [search]); function setParams(newParams: ResourceFilter) { replaceHistory( @@ -134,6 +142,7 @@ export default function getResourceUrlQueryParams( // Conditionally adds the sort field based on whether it exists or not ...(!!processedSortParam && { sort: processedSortParam }), // Conditionally adds the pinnedResources field based on whether its true or not - ...(pinnedOnly === 'true' && { pinnedOnly: true }), + pinnedOnly: + pinnedOnly === 'true' ? true : pinnedOnly === 'false' ? false : undefined, }; } diff --git a/web/packages/teleport/src/features.tsx b/web/packages/teleport/src/features.tsx index 0ea8bd7b9d3c1..f1aab44471ad5 100644 --- a/web/packages/teleport/src/features.tsx +++ b/web/packages/teleport/src/features.tsx @@ -19,7 +19,6 @@ import React from 'react'; import { - AddCircle, Bots as BotsIcon, CirclePlay, ClipboardUser, @@ -29,14 +28,14 @@ import { Laptop, ListAddCheck, ListThin, - Lock, + LockKey, + PlugsConnected, Question, Server, - ShieldCheck, SlidersVertical, Terminal, UserCircleGear, - Users as UsersIcon, + User as UserIcon, } from 'design/Icon'; import cfg from 'teleport/config'; @@ -221,7 +220,7 @@ export class FeatureUsers implements TeleportFeature { navigationItem = { title: NavTitle.Users, - icon: UsersIcon, + icon: UserIcon, exact: true, getLink() { return cfg.getUsersRoute(); @@ -268,13 +267,12 @@ export class FeatureBots implements TeleportFeature { export class FeatureAddBots implements TeleportFeature { category = NavigationCategory.Management; section = ManagementSection.Access; - sideNavCategory = SideNavigationCategory.Access; - hideFromNavigation = true; + sideNavCategory = SideNavigationCategory.AddNew; route = { - title: 'New Bot', + title: 'Bot', path: cfg.routes.botsNew, - exact: false, + exact: true, component: () => , }; @@ -285,6 +283,16 @@ export class FeatureAddBots implements TeleportFeature { getRoute() { return this.route; } + + navigationItem = { + title: NavTitle.NewBot, + icon: BotsIcon, + exact: true, + getLink() { + return cfg.getBotsNewRoute(); + }, + searchableTags: ['add bot', 'new bot', 'bots'], + }; } export class FeatureRoles implements TeleportFeature { @@ -332,7 +340,7 @@ export class FeatureAuthConnectors implements TeleportFeature { navigationItem = { title: NavTitle.AuthConnectors, - icon: ShieldCheck, + icon: PlugsConnected, exact: false, getLink() { return cfg.routes.sso; @@ -359,7 +367,7 @@ export class FeatureLocks implements TeleportFeature { navigationItem = { title: NavTitle.SessionAndIdentityLocks, - icon: Lock, + icon: LockKey, exact: false, getLink() { return cfg.getLocksRoute(); @@ -394,7 +402,7 @@ export class FeatureDiscover implements TeleportFeature { standalone = true; route = { - title: 'Enroll New Resource', + title: 'Resource', path: cfg.routes.discover, exact: true, component: Discover, @@ -402,12 +410,12 @@ export class FeatureDiscover implements TeleportFeature { navigationItem = { title: NavTitle.EnrollNewResource, - icon: AddCircle, + icon: Server, exact: true, getLink() { return cfg.routes.discover; }, - searchableTags: ['new resource', 'add'], + searchableTags: ['new', 'add', 'enroll', 'resources'], }; hasAccess(flags: FeatureFlags) { @@ -453,11 +461,10 @@ export class FeatureIntegrations implements TeleportFeature { export class FeatureIntegrationEnroll implements TeleportFeature { category = NavigationCategory.Management; section = ManagementSection.Access; - sideNavCategory = SideNavigationCategory.Access; - parent = FeatureIntegrations; + sideNavCategory = SideNavigationCategory.AddNew; route = { - title: 'Enroll New Integration', + title: 'Integration', path: cfg.routes.integrationEnroll, exact: false, component: () => , @@ -469,10 +476,11 @@ export class FeatureIntegrationEnroll implements TeleportFeature { navigationItem = { title: NavTitle.EnrollNewIntegration, - icon: AddCircle, + icon: IntegrationsIcon, getLink() { return cfg.getIntegrationEnrollRoute(null); }, + searchableTags: ['new', 'add', 'enroll', 'integration'], }; // getRoute allows child class extending this @@ -586,7 +594,7 @@ export class FeatureTrust implements TeleportFeature { getLink() { return cfg.routes.trustedClusters; }, - searchableTags: ['clusters', 'trusted clusters'], + searchableTags: ['clusters', 'trusted clusters', 'root clusters'], }; } @@ -668,17 +676,18 @@ export function getOSSFeatures(): TeleportFeature[] { // TODO(rudream): Implement shortcuts to pinned/nodes/apps/dbs/desktops/kubes. new FeatureUnifiedResources(), - // Management + // AddNew + new FeatureDiscover(), + new FeatureIntegrationEnroll(), + new FeatureAddBots(), // - Access new FeatureUsers(), - new FeatureRoles(), new FeatureBots(), - new FeatureAddBots(), new FeatureJoinTokens(), + new FeatureRoles(), new FeatureAuthConnectors(), new FeatureIntegrations(), - new FeatureIntegrationEnroll(), new FeatureClusters(), new FeatureTrust(), @@ -693,8 +702,6 @@ export function getOSSFeatures(): TeleportFeature[] { new FeatureRecordings(), new FeatureSessions(), - new FeatureDiscover(), - // Other new FeatureAccount(), new FeatureHelpAndSupport(), diff --git a/web/packages/teleport/src/types.ts b/web/packages/teleport/src/types.ts index 2644288375b82..2c3620e6d86b7 100644 --- a/web/packages/teleport/src/types.ts +++ b/web/packages/teleport/src/types.ts @@ -63,12 +63,14 @@ export enum NavTitle { // Access Management Users = 'Users', Bots = 'Bots', - Roles = 'User Roles', + Roles = 'Roles', JoinTokens = 'Join Tokens', AuthConnectors = 'Auth Connectors', Integrations = 'Integrations', - EnrollNewResource = 'Enroll New Resource', - EnrollNewIntegration = 'Enroll New Integration', + EnrollNewResource = 'Resource', + EnrollNewIntegration = 'Integration', + NewAccessList = 'Access List', + NewBot = 'Bot', // Identity Governance & Security AccessLists = 'Access Lists',