From 7400a8d0a51d763255c0e6215d26cfc200aa89d8 Mon Sep 17 00:00:00 2001 From: Yassine Bounekhla Date: Mon, 20 Jan 2025 10:51:10 -0500 Subject: [PATCH 1/4] update dashboard view to use sidenav --- web/packages/teleport/src/Main/Main.tsx | 8 +- .../Navigation/SideNavigation/Navigation.tsx | 93 +++++++++++++------ .../src/Navigation/SideNavigation/Section.tsx | 36 ++++++- web/packages/teleport/src/config.ts | 2 +- web/packages/teleport/src/features.tsx | 10 +- web/packages/teleport/src/types.ts | 3 + 6 files changed, 114 insertions(+), 38 deletions(-) diff --git a/web/packages/teleport/src/Main/Main.tsx b/web/packages/teleport/src/Main/Main.tsx index 4f2ed3f4f3449..c108317e34638 100644 --- a/web/packages/teleport/src/Main/Main.tsx +++ b/web/packages/teleport/src/Main/Main.tsx @@ -78,12 +78,8 @@ export function Main(props: MainProps) { const { preferences } = useUser(); - const isTopBarView = storageService.getIsTopBarView(); - const TopBarComponent = - //TODO(rudream): Add sidenav dashboard view. - isTopBarView || cfg.isDashboard ? TopBar : TopBarSideNav; - const NavigationComponent = - isTopBarView || cfg.isDashboard ? Navigation : SideNavigation; + const TopBarComponent = TopBarSideNav; + const NavigationComponent = SideNavigation; useEffect(() => { if (ctx.storeUser.state) { diff --git a/web/packages/teleport/src/Navigation/SideNavigation/Navigation.tsx b/web/packages/teleport/src/Navigation/SideNavigation/Navigation.tsx index f98f008f5c559..92a15e282f217 100644 --- a/web/packages/teleport/src/Navigation/SideNavigation/Navigation.tsx +++ b/web/packages/teleport/src/Navigation/SideNavigation/Navigation.tsx @@ -26,6 +26,7 @@ import React, { useState, } from 'react'; import { matchPath, useHistory } from 'react-router'; +import { NavLink } from 'react-router-dom'; import styled from 'styled-components'; import { Box, Flex } from 'design'; @@ -44,7 +45,7 @@ import { } from './categories'; import { getResourcesSection, ResourcesSection } from './ResourcesSection'; import { SearchSection } from './Search'; -import { DefaultSection, rightPanelWidth } from './Section'; +import { DefaultSection, rightPanelWidth, StandaloneSection } from './Section'; import { zIndexMap } from './zIndexMap'; const SideNavContainer = styled(Flex).attrs({ @@ -75,10 +76,14 @@ const PanelBackground = styled.div` export type NavigationSection = { category?: SidenavCategory; subsections?: NavigationSubsection[]; + route?: string; /* standalone is whether this is a clickable nav section with no subsections/drawer. */ standalone?: boolean; + /* Icon is the custom icon to display for a standalone section. This should only for standalone sections, as icons for categories are derived automatically by CategoryIcon */ + Icon?: (props) => ReactNode; + /* title is the custom title of a standalone section */ + title?: string; }; - /** * NavigationSubsection is a subsection of a NavigationSection, these are the items listed in the drawer of a NavigationSection, or if isTopMenuItem is true, in the top menu (eg. Account Settings). */ @@ -118,6 +123,21 @@ function getNavigationSections( return navigationSections; } +function getDashboardNavigationSections( + features: TeleportFeature[] +): NavigationSection[] { + const navigationSections = features + .filter(feature => feature.showInDashboard) + .map(feature => ({ + standalone: true, + title: feature.navigationItem.title, + Icon: feature.navigationItem.icon, + route: feature.navigationItem.getLink(cfg.proxyCluster), + })); + + return navigationSections; +} + function getSubsectionsForCategory( category: SidenavCategory, features: TeleportFeature[] @@ -282,13 +302,14 @@ export function Navigation() { }); }; - const navSections = useMemo( - () => - getNavigationSections(features).filter( - section => section.subsections.length - ), - [features] - ); + const navSections = useMemo(() => { + if (cfg.isDashboard) { + return getDashboardNavigationSections(features); + } + return getNavigationSections(features).filter( + section => section.subsections.length + ); + }, [features]); const topMenuSection = useMemo(() => getTopMenuSection(features), [features]); @@ -407,26 +428,42 @@ export function Navigation() { > - - + {!cfg.isDashboard && ( + <> + + + + )} {navSections.map(section => { + if (section.standalone) { + return ( + + ); + } + const isExpanded = !!debouncedSection && !debouncedSection.standalone && diff --git a/web/packages/teleport/src/Navigation/SideNavigation/Section.tsx b/web/packages/teleport/src/Navigation/SideNavigation/Section.tsx index b1143fbc95efd..6f5608db9df6a 100644 --- a/web/packages/teleport/src/Navigation/SideNavigation/Section.tsx +++ b/web/packages/teleport/src/Navigation/SideNavigation/Section.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import React, { PropsWithChildren } from 'react'; +import React, { PropsWithChildren, ReactNode } from 'react'; import { NavLink } from 'react-router-dom'; import styled, { css, useTheme } from 'styled-components'; @@ -70,7 +70,11 @@ export function DefaultSection({ isExpanded={isExpanded} tabIndex={section.standalone ? 0 : -1} > - + {section.Icon ? ( + + ) : ( + + )} {section.category} @@ -148,6 +152,33 @@ export function CustomChildrenSection({ ); } +/** + * StandaloneSection is a section with no subsections, instead of expanding a drawer, the category button is clickable and takes you directly to a route. + */ +export function StandaloneSection({ + title, + route, + Icon, + $active, +}: { + title: string; + route: string; + Icon: (props) => ReactNode; + $active: boolean; +}) { + return ( + + + {title} + + ); +} + export const rightPanelWidth = 236; export const RightPanel = styled(Box).attrs({ px: '5px' })<{ @@ -265,6 +296,7 @@ export const CategoryButton = styled.button<{ font-weight: ${props => props.theme.typography.body4.fontWeight}; letter-spacing: ${props => props.theme.typography.body4.letterSpacing}; line-height: ${props => props.theme.typography.body4.lineHeight}; + text-decoration: none; ${props => getCategoryStyles(props.theme, props.$active, props.isExpanded)} `; diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts index a5d3661efee56..bae9367dce207 100644 --- a/web/packages/teleport/src/config.ts +++ b/web/packages/teleport/src/config.ts @@ -55,7 +55,7 @@ const cfg = { automaticUpgradesTargetVersion: '', // isDashboard is used generally when we want to hide features that can't be hidden by RBAC in // the case of a self-hosted license tenant dashboard. - isDashboard: false, + isDashboard: true, tunnelPublicAddress: '', recoveryCodesEnabled: false, // IsUsageBasedBilling determines if the user subscription is usage-based (pay-as-you-go). diff --git a/web/packages/teleport/src/features.tsx b/web/packages/teleport/src/features.tsx index 8fc4dde759296..45efcbacfea7c 100644 --- a/web/packages/teleport/src/features.tsx +++ b/web/packages/teleport/src/features.tsx @@ -233,6 +233,8 @@ export class FeatureUsers implements TeleportFeature { getRoute() { return this.route; } + + showInDashboard = true; } export class FeatureBots implements TeleportFeature { @@ -327,6 +329,8 @@ export class FeatureRoles implements TeleportFeature { }, searchableTags: ['roles', 'user roles'], }; + + showInDashboard = true; } export class FeatureAuthConnectors implements TeleportFeature { @@ -346,7 +350,9 @@ export class FeatureAuthConnectors implements TeleportFeature { } navigationItem = { - title: NavTitle.AuthConnectors, + title: cfg.isDashboard + ? NavTitle.AuthConnectorsShortened + : NavTitle.AuthConnectors, icon: PlugsConnected, exact: false, getLink() { @@ -354,6 +360,8 @@ export class FeatureAuthConnectors implements TeleportFeature { }, searchableTags: ['auth connectors', 'saml', 'okta', 'oidc', 'github'], }; + + showInDashboard = true; } export class FeatureLocks implements TeleportFeature { diff --git a/web/packages/teleport/src/types.ts b/web/packages/teleport/src/types.ts index 69e909cd5011c..66b4a6cf722e3 100644 --- a/web/packages/teleport/src/types.ts +++ b/web/packages/teleport/src/types.ts @@ -66,6 +66,7 @@ export enum NavTitle { Roles = 'Roles', JoinTokens = 'Join Tokens', AuthConnectors = 'Auth Connectors', + AuthConnectorsShortened = 'Auth Conn.', Integrations = 'Integrations', EnrollNewResource = 'Resource', EnrollNewIntegration = 'Integration', @@ -149,6 +150,8 @@ export interface TeleportFeature { // if highlightKey is specified, navigating to ?highlight= // will highlight the feature in the navigation, to draw a users attention to it highlightKey?: string; + /** showInDashboard is whether this page should be shown in the navigation for dashboard tenants. Any feature without this flag will not be shown for dashboards. */ + showInDashboard?: boolean; } export type StickyCluster = { From 69f607d85543b8199b246613950516e2d0c235d8 Mon Sep 17 00:00:00 2001 From: Yassine Bounekhla Date: Mon, 20 Jan 2025 11:24:07 -0500 Subject: [PATCH 2/4] delete old nav --- web/packages/teleport/src/Main/Main.tsx | 12 +- .../{SideNavigation => }/CategoryIcon.tsx | 0 .../teleport/src/Navigation/Navigation.tsx | 584 +++++++++++++----- .../NavigationCategoryContainer.tsx | 116 ---- .../src/Navigation/NavigationDropdown.tsx | 301 --------- .../src/Navigation/NavigationItem.test.tsx | 177 ------ .../src/Navigation/NavigationItem.tsx | 311 ---------- .../src/Navigation/NavigationSection.tsx | 83 --- .../teleport/src/Navigation/RecentHistory.tsx | 4 +- .../{SideNavigation => }/ResourcesSection.tsx | 0 .../{SideNavigation => }/Search.tsx | 2 +- .../{SideNavigation => }/Section.tsx | 0 .../Navigation/SideNavigation/Navigation.tsx | 512 --------------- .../Navigation/SideNavigation/categories.ts | 51 -- .../teleport/src/Navigation/categories.ts | 42 +- .../teleport/src/Navigation/common.tsx | 82 --- web/packages/teleport/src/Navigation/index.ts | 4 +- .../teleport/src/Navigation/utils.tsx | 34 - .../{SideNavigation => }/zIndexMap.ts | 0 .../ClusterSelector/ClusterSelector.story.tsx | 155 ----- .../ClusterSelector/ClusterSelector.tsx | 187 ------ .../src/TopBar/ClusterSelector/index.ts | 21 - .../teleport/src/TopBar/TopBar.story.tsx | 115 ---- .../teleport/src/TopBar/TopBar.test.tsx | 193 ------ web/packages/teleport/src/TopBar/TopBar.tsx | 276 +-------- .../teleport/src/TopBar/TopBarSideNav.tsx | 164 ----- web/packages/teleport/src/TopBar/index.ts | 1 + web/packages/teleport/src/config.ts | 2 +- web/packages/teleport/src/features.tsx | 69 +-- web/packages/teleport/src/teleportContext.tsx | 27 - web/packages/teleport/src/types.ts | 12 +- 31 files changed, 506 insertions(+), 3031 deletions(-) rename web/packages/teleport/src/Navigation/{SideNavigation => }/CategoryIcon.tsx (100%) delete mode 100644 web/packages/teleport/src/Navigation/NavigationCategoryContainer.tsx delete mode 100644 web/packages/teleport/src/Navigation/NavigationDropdown.tsx delete mode 100644 web/packages/teleport/src/Navigation/NavigationItem.test.tsx delete mode 100644 web/packages/teleport/src/Navigation/NavigationItem.tsx delete mode 100644 web/packages/teleport/src/Navigation/NavigationSection.tsx rename web/packages/teleport/src/Navigation/{SideNavigation => }/ResourcesSection.tsx (100%) rename web/packages/teleport/src/Navigation/{SideNavigation => }/Search.tsx (99%) rename web/packages/teleport/src/Navigation/{SideNavigation => }/Section.tsx (100%) delete mode 100644 web/packages/teleport/src/Navigation/SideNavigation/Navigation.tsx delete mode 100644 web/packages/teleport/src/Navigation/SideNavigation/categories.ts delete mode 100644 web/packages/teleport/src/Navigation/common.tsx delete mode 100644 web/packages/teleport/src/Navigation/utils.tsx rename web/packages/teleport/src/Navigation/{SideNavigation => }/zIndexMap.ts (100%) delete mode 100644 web/packages/teleport/src/TopBar/ClusterSelector/ClusterSelector.story.tsx delete mode 100644 web/packages/teleport/src/TopBar/ClusterSelector/ClusterSelector.tsx delete mode 100644 web/packages/teleport/src/TopBar/ClusterSelector/index.ts delete mode 100644 web/packages/teleport/src/TopBar/TopBar.story.tsx delete mode 100644 web/packages/teleport/src/TopBar/TopBar.test.tsx delete mode 100644 web/packages/teleport/src/TopBar/TopBarSideNav.tsx diff --git a/web/packages/teleport/src/Main/Main.tsx b/web/packages/teleport/src/Main/Main.tsx index c108317e34638..390c063c6d2ca 100644 --- a/web/packages/teleport/src/Main/Main.tsx +++ b/web/packages/teleport/src/Main/Main.tsx @@ -42,16 +42,13 @@ import { Redirect, Route, Switch } from 'teleport/components/Router'; import cfg from 'teleport/config'; import { FeaturesContextProvider, useFeatures } from 'teleport/FeaturesContext'; import { Navigation } from 'teleport/Navigation'; -import { Navigation as SideNavigation } from 'teleport/Navigation/SideNavigation/Navigation'; import { ClusterAlert, LINK_DESTINATION_LABEL, LINK_TEXT_LABEL, } from 'teleport/services/alerts/alerts'; import { storageService } from 'teleport/services/storageService'; -import { TopBar } from 'teleport/TopBar'; -import { TopBarProps } from 'teleport/TopBar/TopBar'; -import { TopBar as TopBarSideNav } from 'teleport/TopBar/TopBarSideNav'; +import { TopBar, TopBarProps } from 'teleport/TopBar'; import type { LockedFeatures, TeleportFeature } from 'teleport/types'; import { useUser } from 'teleport/User/UserContext'; import useTeleport from 'teleport/useTeleport'; @@ -78,9 +75,6 @@ export function Main(props: MainProps) { const { preferences } = useUser(); - const TopBarComponent = TopBarSideNav; - const NavigationComponent = SideNavigation; - useEffect(() => { if (ctx.storeUser.state) { setAttempt({ status: 'success' }); @@ -191,7 +185,7 @@ export function Main(props: MainProps) { return ( - - + p.theme.colors.levels.surface}; - width: var(--sidebar-width); - position: relative; - display: flex; - flex-direction: column; + position: absolute; + top: 0; + z-index: ${zIndexMap.sideNavContainer}; border-right: 1px solid ${p => p.theme.colors.spotBackground[1]}; `; -const CategoriesContainer = styled.div` - position: relative; - width: inherit; - flex: 1; -`; +/* NavigationSection is a section in the navbar, this can either be a standalone section (clickable button with no drawer), or a category with subsections shown in a drawer that expands. */ +export type NavigationSection = { + category?: SidenavCategory; + subsections?: NavigationSubsection[]; + route?: string; + /* standalone is whether this is a clickable nav section with no subsections/drawer. */ + standalone?: boolean; + /* Icon is the custom icon to display for a standalone section. This should only for standalone sections, as icons for categories are derived automatically by CategoryIcon */ + Icon?: (props) => ReactNode; + /* title is the custom title of a standalone section */ + title?: string; +}; +/** + * NavigationSubsection is a subsection of a NavigationSection, these are the items listed in the drawer of a NavigationSection, or if isTopMenuItem is true, in the top menu (eg. Account Settings). + */ +export type NavigationSubsection = { + category?: SidenavCategory; + isTopMenuItem?: boolean; + title: string; + route: string; + exact: boolean; + icon: (props) => ReactNode; + 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; +}; -export function getFirstRouteForCategory( - features: TeleportFeature[], - category: NavigationCategory -) { - const firstRoute = features - .filter(feature => feature.category === category) - .filter(feature => Boolean(feature.route))[0]; +function getNavigationSections( + features: TeleportFeature[] +): NavigationSection[] { + const navigationSections = NAVIGATION_CATEGORIES.map(category => ({ + category, + subsections: getSubsectionsForCategory(category, features), + })); - return ( - firstRoute?.navigationItem?.getLink(cfg.proxyCluster) || cfg.routes.support - ); + return navigationSections; } -function getFeatureForRoute( - features: TeleportFeature[], - route: history.Location | Location -): TeleportFeature | undefined { - return features.find( +function getDashboardNavigationSections( + features: TeleportFeature[] +): NavigationSection[] { + const navigationSections = features + .filter(feature => feature.showInDashboard) + .map(feature => ({ + standalone: true, + title: feature.navigationItem.title, + Icon: feature.navigationItem.icon, + route: feature.navigationItem.getLink(cfg.proxyCluster), + })); + + return navigationSections; +} + +function getSubsectionsForCategory( + category: SidenavCategory, + features: TeleportFeature[] +): NavigationSubsection[] { + const filteredFeatures = features.filter( feature => - feature.route && - matchPath(route.pathname, { - path: feature.route.path, - exact: feature.route.exact, - }) + feature.category === category && + !!feature.navigationItem && + !feature.parent + ); + + return filteredFeatures.map(feature => { + return { + category, + title: feature.navigationItem.title, + route: feature.navigationItem.getLink(cfg.proxyCluster), + exact: feature.navigationItem.exact, + icon: feature.navigationItem.icon, + searchableTags: feature.navigationItem.searchableTags, + }; + }); +} + +// 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). +/** + * getTopMenuSection returns a NavigationSection with the top menu items. This is not used in the sidenav, but will be used to make the top menu items searchable. + */ +function getTopMenuSection(features: TeleportFeature[]): NavigationSection { + const topMenuItems = features.filter( + feature => !!feature.topMenuItem && !feature.category ); + + return { + subsections: topMenuItems.map(feature => ({ + isTopMenuItem: true, + title: feature.topMenuItem.title, + route: feature.topMenuItem.getLink(cfg.proxyCluster), + exact: feature?.route?.exact, + icon: feature.topMenuItem.icon, + searchableTags: feature.topMenuItem.searchableTags, + })), + }; } -function getCategoryForRoute( +function getNavSubsectionForRoute( features: TeleportFeature[], route: history.Location | Location -) { +): NavigationSubsection { const feature = features .filter(feature => Boolean(feature.route)) .find(feature => matchPath(route.pathname, { path: feature.route.path, - exact: false, + exact: feature.route.exact, }) ); - if (!feature) { + if (!feature || (!feature.category && !feature.topMenuItem)) { return; } - return feature.category; + if (feature.topMenuItem) { + return { + isTopMenuItem: true, + exact: feature.route.exact, + title: feature.topMenuItem.title, + route: feature.topMenuItem.getLink(cfg.proxyCluster), + icon: feature.topMenuItem.icon, + searchableTags: feature.topMenuItem.searchableTags, + category: feature?.category, + }; + } + + return { + category: feature.category, + title: feature.navigationItem.title, + route: feature.navigationItem.getLink(cfg.proxyCluster), + exact: feature.navigationItem.exact, + icon: feature.navigationItem.icon, + searchableTags: feature.navigationItem.searchableTags, + }; +} + +/** + * 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 drawer 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 location = useLocation(); - - const view = - getCategoryForRoute(features, history.location) || - NavigationCategory.Resources; - - const categories = NAVIGATION_CATEGORIES.map((category, index) => ( - - )); - - const feature = getFeatureForRoute(features, location); - - if ( - feature?.hideNavigation || - feature?.category !== NavigationCategory.Management - ) { - return null; - } + const { clusterId } = useStickyClusterId(); + const { preferences, updatePreferences } = useUser(); + const [targetSection, setTargetSection] = useState( + null + ); + const [isClosing, setIsClosing] = useState(false); + const debouncedSection = useDebounceClose(targetSection, 200, isClosing); + const [previousExpandedSection, setPreviousExpandedSection] = + useState(); + const navigationTimeoutRef = useRef(); - return ( - - {categories} - {cfg.edition === 'oss' && } - {cfg.edition === 'community' && } - + // Clear navigation timeout on unmount. + useEffect(() => { + return () => { + if (navigationTimeoutRef.current) { + clearTimeout(navigationTimeoutRef.current); + } + }; + }, []); + const currentView = useMemo( + () => getNavSubsectionForRoute(features, history.location), + [features, history.location] ); -} -function AGPLFooter() { - const theme = useTheme(); - return ( - - {/* This is an independently compiled AGPL-3.0 version of Teleport. You */} - {/* can find the official release on{' '} */} - This is an independently compiled AGPL-3.0 version of Teleport. -
- Visit{' '} - - the Downloads page - {' '} - for the official release. - + + const stickyMode = preferences.sideNavDrawerMode === SideNavDrawerMode.STICKY; + + const toggleStickyMode = () => { + // Close the drawer right away if they're disabling sticky mode. + if (stickyMode) { + setIsClosing(false); + setPreviousExpandedSection(null); + setTargetSection(null); + } + updatePreferences({ + sideNavDrawerMode: stickyMode + ? SideNavDrawerMode.COLLAPSED + : SideNavDrawerMode.STICKY, + }); + }; + + const navSections = useMemo(() => { + if (cfg.isDashboard) { + return getDashboardNavigationSections(features); + } + return getNavigationSections(features).filter( + section => section.subsections.length + ); + }, [features]); + + const topMenuSection = useMemo(() => getTopMenuSection(features), [features]); + + const resourcesSection = useMemo(() => { + const searchParams = new URLSearchParams(location.search); + return getResourcesSection({ + clusterId, + preferences, + updatePreferences, + searchParams, + }); + }, [clusterId, preferences, updatePreferences]); + + const handleSetExpandedSection = useCallback( + (section: NavigationSection) => { + setIsClosing(false); + if (!section.standalone) { + setPreviousExpandedSection(debouncedSection); + setTargetSection(section); + } else { + setPreviousExpandedSection(null); + setTargetSection(null); } - /> + }, + [debouncedSection] ); -} -function CommunityFooter() { - const theme = useTheme(); - return ( - - - Upgrade to Teleport Enterprise - {' '} - for SSO, just-in-time access requests, Access Graph, and much more! - + const combinedSideNavSections = useMemo( + () => [resourcesSection, ...navSections], + [resourcesSection, navSections] + ); + const currentPageSection = useMemo(() => { + return combinedSideNavSections.find( + section => section.category === currentView?.category + ); + }, [combinedSideNavSections, currentView]); + + const collapseDrawer = useCallback( + (closeAfterDelay = true) => { + if (stickyMode && currentPageSection) { + setPreviousExpandedSection(debouncedSection); + setTargetSection(currentPageSection); + } else { + setIsClosing(closeAfterDelay); + setPreviousExpandedSection(null); + setTargetSection(null); } - /> + }, + [currentPageSection, stickyMode, debouncedSection] ); -} -function LicenseFooter({ - title, - subText, - infoContent, -}: { - title: string; - subText: string; - infoContent: JSX.Element; -}) { + useEffect(() => { + // Whenever the user changes page, if stickyMode is enabled and the page is part of the sidenav, the drawer should be expanded with the current page section. + if (!stickyMode) { + return; + } + + // If the page is not part of the sidenav, such as Account Settings, curentPageSection will be undefined, and the drawer should be collapsed. + if (currentPageSection) { + // If there is already an expanded section set, don't change it. + if (debouncedSection) { + return; + } + handleSetExpandedSection(currentPageSection); + } else { + collapseDrawer(false); + } + }, [currentPageSection]); + + // Handler for clicking nav items. + const onNavigationItemClick = useCallback(() => { + // Clear any existing timeout + if (navigationTimeoutRef.current) { + clearTimeout(navigationTimeoutRef.current); + } + + if (!stickyMode) { + // 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(() => { + collapseDrawer(false); + }, 150); + } + }, [collapseDrawer]); + + // 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 ( - - - {title} - - {infoContent} - - - {subText} - + collapseDrawer()} + onKeyUp={e => e.key === 'Escape' && collapseDrawer(false)} + onBlur={(event: React.FocusEvent) => { + if (!event.currentTarget.contains(event.relatedTarget)) { + collapseDrawer(); + } + }} + className={ + stickyMode && + currentPageSection && + !!debouncedSection && + !debouncedSection.standalone + ? 'sticky-mode' + : '' + } + > + + + {!cfg.isDashboard && ( + <> + + + + )} + {navSections.map(section => { + if (section.standalone) { + return ( + + ); + } + + const isExpanded = + !!debouncedSection && + !debouncedSection.standalone && + section.category === debouncedSection?.category; + + return ( + + {section.category === 'Add New' && } + handleSetExpandedSection(section)} + currentPageSection={currentPageSection} + stickyMode={stickyMode} + toggleStickyMode={toggleStickyMode} + $active={section.category === currentView?.category} + aria-controls={`panel-${debouncedSection?.category}`} + onNavigationItemClick={onNavigationItemClick} + isExpanded={isExpanded} + /> + + ); + })} + + ); } -const StyledFooterBox = styled(Box)` - line-height: 20px; - border-top: ${props => props.theme.borders[1]} - ${props => props.theme.colors.spotBackground[0]}; +const Container = styled(Box)` + position: relative; + width: var(--sidenav-width); + z-index: ${zIndexMap.sideNavContainer}; + + &.sticky-mode { + margin-right: ${rightPanelWidth}px; + } `; -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/NavigationCategoryContainer.tsx b/web/packages/teleport/src/Navigation/NavigationCategoryContainer.tsx deleted file mode 100644 index e8c66e055b497..0000000000000 --- a/web/packages/teleport/src/Navigation/NavigationCategoryContainer.tsx +++ /dev/null @@ -1,116 +0,0 @@ -/** - * 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 . - */ - -import styled from 'styled-components'; - -import { useFeatures } from 'teleport/FeaturesContext'; -import { - MANAGEMENT_NAVIGATION_SECTIONS, - NavigationCategory, -} from 'teleport/Navigation/categories'; -import { NavigationItemSize } from 'teleport/Navigation/common'; -import { NavigationItem } from 'teleport/Navigation/NavigationItem'; -import { NavigationSection } from 'teleport/Navigation/NavigationSection'; - -interface NavigationCategoryProps { - category: NavigationCategory; - visible: boolean; -} - -interface SelectedProps { - visible: boolean; -} - -const Container = styled.div` - position: absolute; - width: inherit; - pointer-events: ${p => (p.visible ? 'auto' : 'none')}; - opacity: ${p => (p.visible ? 1 : 0)}; - transition: opacity 0.15s linear; - bottom: 0; - top: 0; - overflow-y: auto; -`; - -const TRANSITION_DELAY_OFFSET = 160; -const TRANSITION_TOTAL_TIME = 140; - -export function NavigationCategoryContainer(props: NavigationCategoryProps) { - const features = useFeatures(); - - let items = features - .filter(feature => feature.category === props.category) - .filter(feature => !feature.parent); - - if (props.category === NavigationCategory.Resources) { - const transitionDelayPerItem = TRANSITION_TOTAL_TIME / items.length; - - const children = []; - - let transitionDelay = TRANSITION_DELAY_OFFSET; - - for (const [index, item] of items.entries()) { - children.push( - - ); - - transitionDelay += transitionDelayPerItem; - } - - return {children}; - } - - const managementSectionsWithItems = MANAGEMENT_NAVIGATION_SECTIONS.filter( - section => items.filter(feature => feature.section === section).length - ); - - const transitionDelayPerItem = - TRANSITION_TOTAL_TIME / (items.length + managementSectionsWithItems.length); - - const sections = []; - - let transitionDelay = TRANSITION_DELAY_OFFSET; - for (const [index, section] of managementSectionsWithItems.entries()) { - const sectionItems = items.filter(feature => feature.section === section); - - if (!sectionItems.length) { - continue; - } - - sections.push( - - ); - - transitionDelay += (sectionItems.length + 1) * transitionDelayPerItem; - } - - return {sections}; -} diff --git a/web/packages/teleport/src/Navigation/NavigationDropdown.tsx b/web/packages/teleport/src/Navigation/NavigationDropdown.tsx deleted file mode 100644 index 1fc7aab8df544..0000000000000 --- a/web/packages/teleport/src/Navigation/NavigationDropdown.tsx +++ /dev/null @@ -1,301 +0,0 @@ -/** - * 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 . - */ - -import type { Location } from 'history'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { matchPath, useHistory } from 'react-router'; -import { NavLink } from 'react-router-dom'; -import styled from 'styled-components'; - -import { ChevronRight } from 'design/Icon'; - -import { useTeleport } from 'teleport'; -import { useFeatures } from 'teleport/FeaturesContext'; -import { - commonNavigationItemStyles, - LinkContent, - NavigationItemSize, -} from 'teleport/Navigation/common'; -import { getIcon } from 'teleport/Navigation/utils'; -import type { TeleportFeature } from 'teleport/types'; - -interface NavigationDropdownProps { - feature: TeleportFeature; - size: NavigationItemSize; - transitionDelay: number; - visible: boolean; -} - -interface OpenProps { - open: boolean; -} - -const Container = styled.div` - ${commonNavigationItemStyles}; - - cursor: pointer; - - &:focus { - outline: none; - background: ${props => props.theme.colors.spotBackground[0]}; - } - - ${LinkContent} { - opacity: ${p => (p.open ? 0.9 : 0.6)}; - } -`; - -const DropdownArrow = styled.div` - position: absolute; - top: 50%; - right: 18px; - line-height: 0; - transform: translate(0, -50%); - - svg { - transform: ${p => (p.open ? 'rotate(90deg)' : 'none')}; - transition: 0.1s linear transform; - } -`; - -const DropdownLinks = styled.div` - overflow: hidden; - max-height: ${p => (p.open ? 500 : 0)}px; - transition: ${p => - p.open - ? 'max-height 0.3s ease-in-out' - : 'max-height 0.3s cubic-bezier(0, 1, 0, 1)'}; - transform: translate3d(0, 0, 0); -`; - -const DropdownLink = styled(NavLink)` - ${commonNavigationItemStyles}; - - &:focus { - background: ${props => props.theme.colors.spotBackground[0]}; - } - - &.active { - background: ${props => props.theme.colors.spotBackground[1]}; - - ${LinkContent} { - font-weight: 700; - opacity: 1; - } - - &:before { - height: 8px; - width: 8px; - position: absolute; - top: 50%; - transform: translate(0, -50%); - left: 37px; - border-radius: 2px; - background: ${props => props.theme.colors.brand}; - content: ''; - } - } -`; - -function hasActiveChild(features: TeleportFeature[], route: Location) { - const feature = features - .filter(feature => Boolean(feature.route)) - .find(feature => - matchPath(route.pathname, { - path: feature.route.path, - exact: false, - }) - ); - - return Boolean(feature); -} - -export function NavigationDropdown(props: NavigationDropdownProps) { - const features = useFeatures(); - const ctx = useTeleport(); - const history = useHistory(); - - const ref = useRef(); - const firstLinkRef = useRef(); - - const clusterId = ctx.storeUser.getClusterId(); - - const childFeatures = features - .filter(feature => Boolean(feature.parent)) - .filter(feature => props.feature instanceof feature.parent); - - const [open, setOpen] = useState( - hasActiveChild(childFeatures, history.location) - ); - - useEffect(() => { - return history.listen(next => { - setOpen(hasActiveChild(childFeatures, next)); - }); - }, []); - - useEffect(() => { - if (!props.visible) { - setOpen(false); - } - }, [props.visible]); - - const handleKeyDown = useCallback( - (event: React.KeyboardEvent) => { - switch (event.key) { - case 'ArrowRight': - setOpen(true); - - break; - - case 'ArrowLeft': - setOpen(false); - - break; - - case 'Enter': - setOpen(open => !open); - - break; - - case 'ArrowUp': - const previousSibling = event.currentTarget - .previousSibling as HTMLAnchorElement; - if (previousSibling) { - previousSibling.focus(); - } - - break; - - case 'ArrowDown': - if (open) { - firstLinkRef.current.focus(); - - return; - } - - // nextSibling is `DropdownLinks`, so we need to go one further to get the next - // navigation item - const nextSibling = event.currentTarget.nextSibling - .nextSibling as HTMLDivElement; - if (nextSibling) { - nextSibling.focus(); - } - - break; - } - }, - [firstLinkRef, open] - ); - - const handleKeyDownLink = useCallback( - (event: React.KeyboardEvent) => { - switch (event.key) { - case 'ArrowDown': - const nextSibling = event.currentTarget.nextSibling as HTMLDivElement; - - if (nextSibling) { - nextSibling.focus(); - - return; - } - - const nextParentSibling = event.currentTarget.parentElement - .nextSibling as HTMLDivElement; - if (nextParentSibling) { - nextParentSibling.focus(); - } - - break; - - case 'ArrowUp': - const previousSibling = event.currentTarget - .previousSibling as HTMLDivElement; - if (previousSibling) { - previousSibling.focus(); - - return; - } - - ref.current.focus(); - - break; - - case 'ArrowLeft': - setOpen(false); - ref.current.focus(); - - break; - } - }, - [ref] - ); - - const items = childFeatures.map((feature, index) => ( - - - {getIcon(feature, NavigationItemSize.Small)} - {feature.navigationItem.title} - - - )); - - return ( - <> - setOpen(!open)} - onKeyDown={handleKeyDown} - open={open} - style={{ - transitionDelay: `${props.transitionDelay}ms,0s`, - transform: `translate3d(${ - props.visible ? 0 : 'calc(var(--sidebar-width) * -1)' - }, 0, 0)`, - }} - > - - {getIcon(props.feature, props.size)} - - {props.feature.navigationItem.title} - - - - - - - - - {items} - - - ); -} diff --git a/web/packages/teleport/src/Navigation/NavigationItem.test.tsx b/web/packages/teleport/src/Navigation/NavigationItem.test.tsx deleted file mode 100644 index 9cb96204fec45..0000000000000 --- a/web/packages/teleport/src/Navigation/NavigationItem.test.tsx +++ /dev/null @@ -1,177 +0,0 @@ -/** - * 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 . - */ - -import { act } from '@testing-library/react'; -import { createMemoryHistory, MemoryHistory } from 'history'; -import { generatePath, Router } from 'react-router'; - -import { Server } from 'design/Icon'; -import { render, screen } from 'design/utils/testing'; - -import { NavigationCategory } from 'teleport/Navigation/categories'; -import { NavigationItemSize } from 'teleport/Navigation/common'; -import { NavigationItem } from 'teleport/Navigation/NavigationItem'; -import { LocalNotificationKind } from 'teleport/services/notifications'; -import { makeUserContext } from 'teleport/services/user'; -import TeleportContext from 'teleport/teleportContext'; -import TeleportContextProvider from 'teleport/TeleportContextProvider'; -import { NavTitle, TeleportFeature } from 'teleport/types'; - -class MockUserFeature implements TeleportFeature { - category = NavigationCategory.Resources; - - route = { - title: 'Users', - path: '/web/cluster/:clusterId/feature', - exact: true, - component: () =>
Test!
, - }; - - hasAccess() { - return true; - } - - navigationItem = { - title: NavTitle.Users, - icon: Server, - exact: true, - getLink(clusterId: string) { - return generatePath('/web/cluster/:clusterId/feature', { clusterId }); - }, - }; -} - -class MockAccessListFeature implements TeleportFeature { - category = NavigationCategory.Resources; - - route = { - title: 'Users', - path: '/web/cluster/:clusterId/feature', - exact: true, - component: () =>
Test!
, - }; - - hasAccess() { - return true; - } - - navigationItem = { - title: NavTitle.AccessLists, - icon: Server, - exact: true, - getLink(clusterId: string) { - return generatePath('/web/cluster/:clusterId/feature', { clusterId }); - }, - }; -} - -describe('navigation items', () => { - let ctx: TeleportContext; - let history: MemoryHistory; - - beforeEach(() => { - history = createMemoryHistory({ - initialEntries: ['/web/cluster/root/feature'], - }); - - ctx = new TeleportContext(); - ctx.storeUser.state = makeUserContext({ - cluster: { - name: 'test-cluster', - lastConnected: Date.now(), - }, - }); - }); - - it('should render the feature link correctly', () => { - render(getNavigationItem({ ctx, history })); - - expect(screen.getByRole('link', { name: 'Users' })).toHaveAttribute( - 'href', - '/web/cluster/root/feature' - ); - }); - - it('should change the feature link to the leaf cluster when navigating to a leaf cluster', () => { - render(getNavigationItem({ ctx, history })); - - expect(screen.getByRole('link', { name: 'Users' })).toHaveAttribute( - 'href', - '/web/cluster/root/feature' - ); - - act(() => history.push('/web/cluster/leaf/feature')); - - expect(screen.getByRole('link', { name: 'Users' })).toHaveAttribute( - 'href', - '/web/cluster/leaf/feature' - ); - }); - - it('rendeirng of attention dot for access list', () => { - const { rerender } = render( - getNavigationItem({ ctx, history, feature: new MockAccessListFeature() }) - ); - - expect( - screen.queryByTestId('nav-item-attention-dot') - ).not.toBeInTheDocument(); - - // Add in some notifications - ctx.storeNotifications.setNotifications([ - { - item: { - kind: LocalNotificationKind.AccessList, - resourceName: 'banana', - route: '', - }, - id: 'abc', - date: new Date(), - }, - ]); - - rerender( - getNavigationItem({ ctx, history, feature: new MockAccessListFeature() }) - ); - - expect(screen.getByTestId('nav-item-attention-dot')).toBeInTheDocument(); - }); -}); - -function getNavigationItem({ - ctx, - history, - feature = new MockUserFeature(), -}: { - ctx: TeleportContext; - history: MemoryHistory; - feature?: TeleportFeature; -}) { - return ( - - - - - - ); -} diff --git a/web/packages/teleport/src/Navigation/NavigationItem.tsx b/web/packages/teleport/src/Navigation/NavigationItem.tsx deleted file mode 100644 index 68873f4b54162..0000000000000 --- a/web/packages/teleport/src/Navigation/NavigationItem.tsx +++ /dev/null @@ -1,311 +0,0 @@ -/** - * 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 . - */ - -import React, { useCallback, useMemo } from 'react'; -import { NavLink, useLocation } from 'react-router-dom'; -import styled, { css, keyframes } from 'styled-components'; - -import { ArrowSquareOut } from 'design/Icon'; - -import { useTeleport } from 'teleport'; -import { - commonNavigationItemStyles, - LinkContent, - NavigationItemSize, -} from 'teleport/Navigation/common'; -import { NavigationDropdown } from 'teleport/Navigation/NavigationDropdown'; -import { getIcon } from 'teleport/Navigation/utils'; -import { LocalNotificationKind } from 'teleport/services/notifications'; -import { storageService } from 'teleport/services/storageService'; -import { - NavTitle, - RecommendationStatus, - type TeleportFeature, - type TeleportFeatureNavigationItem, -} from 'teleport/types'; -import useStickyClusterId from 'teleport/useStickyClusterId'; - -interface NavigationItemProps { - feature: TeleportFeature; - size: NavigationItemSize; - transitionDelay: number; - visible: boolean; -} - -const ExternalLink = styled.a` - ${commonNavigationItemStyles}; - - &:focus { - background: ${props => props.theme.colors.spotBackground[0]}; - } -`; - -const highlight = keyframes` - to { - background: none; - } -`; - -const Link = styled(NavLink)<{ isHighlighted?: boolean }>` - ${commonNavigationItemStyles}; - color: ${props => props.theme.colors.text.main}; - z-index: 1; - background: ${p => - p.isHighlighted ? p.theme.colors.highlightedNavigationItem : 'none'}; - animation: ${p => - p.isHighlighted - ? css` - ${highlight} 10s forwards linear - ` - : 'none'}; - animation-delay: 2s; - - &:focus { - background: ${props => props.theme.colors.spotBackground[0]}; - } - - &.active { - background: ${props => props.theme.colors.spotBackground[0]}; - border-left-color: ${props => props.theme.colors.brand}; - - ${LinkContent} { - font-weight: 700; - opacity: 1; - } - } -`; - -const ExternalLinkIndicator = styled.div` - position: absolute; - top: 50%; - right: 18px; - line-height: 0; - transform: translate(0, -50%); -`; - -export function NavigationItem(props: NavigationItemProps) { - const ctx = useTeleport(); - const { clusterId } = useStickyClusterId(); - - const { - navigationItem, - route, - isLocked, - lockedNavigationItem, - lockedRoute, - hideFromNavigation, - } = props.feature; - - const { search } = useLocation(); - - const params = useMemo(() => new URLSearchParams(search), [search]); - - const highlighted = - props.feature.highlightKey && - params.get('highlight') === props.feature.highlightKey; - - const handleKeyDown = useCallback( - (event: React.KeyboardEvent) => { - switch (event.key) { - case 'ArrowDown': - let nextSibling = event.currentTarget.nextSibling as HTMLDivElement; - if (!nextSibling) { - return; - } - - if (nextSibling.nodeName !== 'A' && nextSibling.nodeName !== 'DIV') { - nextSibling = nextSibling.nextSibling as HTMLDivElement; - } - - if (nextSibling) { - nextSibling.focus(); - } - - break; - - case 'ArrowUp': - let previousSibling = event.currentTarget - .previousSibling as HTMLDivElement; - if (!previousSibling) { - return; - } - - // navigating up to a dropdown - if (previousSibling.hasAttribute('data-open')) { - const isOpen = previousSibling.getAttribute('data-open') === 'true'; - - if (isOpen) { - // focus on the last item in the open dropdown - const lastLinkInDropdown = - previousSibling.lastElementChild as HTMLAnchorElement; - - lastLinkInDropdown.focus(); - - return; - } - - // go to the previous sibling of the dropdown links to focus on the dropdown - // container - const dropdownContainer = - previousSibling.previousSibling as HTMLDivElement; - - dropdownContainer.focus(); - - return; - } - - if ( - previousSibling.nodeName !== 'A' && - previousSibling.nodeName !== 'DIV' - ) { - previousSibling = previousSibling.previousSibling as HTMLDivElement; - } - - if (previousSibling) { - previousSibling.focus(); - } - - break; - } - }, - [] - ); - - if (hideFromNavigation) { - return null; - } - - // renderHighlightFeature returns red dot component if the feature recommendation state is 'NOTIFY' - function renderHighlightFeature(featureName: NavTitle): JSX.Element { - if (featureName === NavTitle.AccessLists) { - const hasNotifications = ctx.storeNotifications.hasNotificationsByKind( - LocalNotificationKind.AccessList - ); - - if (hasNotifications) { - return ; - } - - return null; - } - - // Get onboarding status. We'll only recommend features once user completes - // initial onboarding (i.e. connect resources to Teleport cluster). - const onboard = storageService.getOnboardDiscover(); - if (!onboard?.hasResource) { - return null; - } - - const recommendFeatureStatus = - storageService.getFeatureRecommendationStatus(); - if ( - featureName === NavTitle.TrustedDevices && - recommendFeatureStatus?.TrustedDevices === RecommendationStatus.Notify - ) { - return ; - } - return null; - } - - if (navigationItem) { - const linkProps = { - style: { - transitionDelay: `${props.transitionDelay}ms,0s`, - transform: `translate3d(${ - props.visible ? 0 : 'calc(var(--sidebar-width) * -1)' - }, 0, 0)`, - }, - }; - - if (navigationItem.isExternalLink) { - return ( - - - {getIcon(props.feature, props.size)} - {navigationItem.title} - - - - - - - ); - } - - let navigationItemVersion: TeleportFeatureNavigationItem; - if (route) { - navigationItemVersion = navigationItem; - } - - // use locked item version if feature is locked - if (lockedRoute && isLocked?.(ctx.lockedFeatures)) { - if (!lockedNavigationItem) { - throw new Error( - 'locked feature without an alternative navigation item' - ); - } - navigationItemVersion = lockedNavigationItem; - } - - if (navigationItemVersion) { - return ( - - - {getIcon(props.feature, props.size)} - {navigationItemVersion.title} - {renderHighlightFeature(props.feature.navigationItem.title)} - - - ); - } - } - - return ( - - ); -} - -const AttentionDot = styled.div.attrs<{ 'data-testid'?: string }>(() => ({ - 'data-testid': 'nav-item-attention-dot', -}))` - margin-left: 15px; - margin-top: 2px; - width: 7px; - height: 7px; - border-radius: 50%; - background-color: ${props => props.theme.colors.error.main}; -`; diff --git a/web/packages/teleport/src/Navigation/NavigationSection.tsx b/web/packages/teleport/src/Navigation/NavigationSection.tsx deleted file mode 100644 index 670e80f643095..0000000000000 --- a/web/packages/teleport/src/Navigation/NavigationSection.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/** - * 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 . - */ - -import styled from 'styled-components'; - -import { NavigationItemSize } from 'teleport/Navigation/common'; -import { NavigationItem } from 'teleport/Navigation/NavigationItem'; -import type { TeleportFeature } from 'teleport/types'; - -interface NavigationSectionProps { - title: string; - items: TeleportFeature[]; - transitionDelay: number; - transitionDelayPerItem: number; - visible: boolean; -} - -const Title = styled.h3` - font-size: 13px; - line-height: 14px; - color: ${props => props.theme.colors.text.slightlyMuted}; - margin-left: 32px; - transition: - transform 0.3s cubic-bezier(0.19, 1, 0.22, 1), - opacity 0.15s ease-in; - will-change: transform; - margin-top: 33px; - - &:first-of-type { - margin-top: 13px; - } -`; - -export function NavigationSection(props: NavigationSectionProps) { - const items = []; - - let transitionDelay = props.transitionDelay; - for (const [index, item] of props.items.entries()) { - transitionDelay += props.transitionDelayPerItem; - - items.push( - - ); - } - - return ( - <> - - {props.title} - - - {items} - - ); -} diff --git a/web/packages/teleport/src/Navigation/RecentHistory.tsx b/web/packages/teleport/src/Navigation/RecentHistory.tsx index e3a4fdc929efc..b67429f6cdca9 100644 --- a/web/packages/teleport/src/Navigation/RecentHistory.tsx +++ b/web/packages/teleport/src/Navigation/RecentHistory.tsx @@ -27,8 +27,8 @@ import { Cross } from 'design/Icon'; import { useFeatures } from 'teleport/FeaturesContext'; import { TeleportFeature } from 'teleport/types'; -import { SidenavCategory } from './SideNavigation/categories'; -import { getSubsectionStyles } from './SideNavigation/Section'; +import { SidenavCategory } from './categories'; +import { getSubsectionStyles } from './Section'; export type RecentHistoryItem = { category?: SidenavCategory; diff --git a/web/packages/teleport/src/Navigation/SideNavigation/ResourcesSection.tsx b/web/packages/teleport/src/Navigation/ResourcesSection.tsx similarity index 100% rename from web/packages/teleport/src/Navigation/SideNavigation/ResourcesSection.tsx rename to web/packages/teleport/src/Navigation/ResourcesSection.tsx diff --git a/web/packages/teleport/src/Navigation/SideNavigation/Search.tsx b/web/packages/teleport/src/Navigation/Search.tsx similarity index 99% rename from web/packages/teleport/src/Navigation/SideNavigation/Search.tsx rename to web/packages/teleport/src/Navigation/Search.tsx index 78d4f677f8c63..73729e9c6ac18 100644 --- a/web/packages/teleport/src/Navigation/SideNavigation/Search.tsx +++ b/web/packages/teleport/src/Navigation/Search.tsx @@ -25,9 +25,9 @@ import { color, height, space } from 'design/system'; import { storageService } from 'teleport/services/storageService'; -import { RecentHistory, RecentHistoryItem } from '../RecentHistory'; import { CustomNavigationCategory } from './categories'; import { NavigationSection, NavigationSubsection } from './Navigation'; +import { RecentHistory, RecentHistoryItem } from './RecentHistory'; import { CustomChildrenSection, getSubsectionStyles, diff --git a/web/packages/teleport/src/Navigation/SideNavigation/Section.tsx b/web/packages/teleport/src/Navigation/Section.tsx similarity index 100% rename from web/packages/teleport/src/Navigation/SideNavigation/Section.tsx rename to web/packages/teleport/src/Navigation/Section.tsx diff --git a/web/packages/teleport/src/Navigation/SideNavigation/Navigation.tsx b/web/packages/teleport/src/Navigation/SideNavigation/Navigation.tsx deleted file mode 100644 index 92a15e282f217..0000000000000 --- a/web/packages/teleport/src/Navigation/SideNavigation/Navigation.tsx +++ /dev/null @@ -1,512 +0,0 @@ -/** - * 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 type * as history from 'history'; -import React, { - ReactNode, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; -import { matchPath, useHistory } from 'react-router'; -import { NavLink } from 'react-router-dom'; -import styled from 'styled-components'; - -import { Box, Flex } from 'design'; -import { SideNavDrawerMode } from 'gen-proto-ts/teleport/userpreferences/v1/sidenav_preferences_pb'; - -import cfg from 'teleport/config'; -import { useFeatures } from 'teleport/FeaturesContext'; -import type { TeleportFeature } from 'teleport/types'; -import { useUser } from 'teleport/User/UserContext'; -import useStickyClusterId from 'teleport/useStickyClusterId'; - -import { - CustomNavigationSubcategory, - NAVIGATION_CATEGORIES, - SidenavCategory, -} from './categories'; -import { getResourcesSection, ResourcesSection } from './ResourcesSection'; -import { SearchSection } from './Search'; -import { DefaultSection, rightPanelWidth, StandaloneSection } from './Section'; -import { zIndexMap } from './zIndexMap'; - -const SideNavContainer = styled(Flex).attrs({ - gap: 2, - pt: 2, - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'start', - bg: 'levels.surface', -})` - height: 100vh; - width: var(--sidenav-width); - position: fixed; - overflow: visible; -`; - -const PanelBackground = styled.div` - width: 100%; - height: 100%; - background: ${p => p.theme.colors.levels.surface}; - position: absolute; - top: 0; - z-index: ${zIndexMap.sideNavContainer}; - border-right: 1px solid ${p => p.theme.colors.spotBackground[1]}; -`; - -/* NavigationSection is a section in the navbar, this can either be a standalone section (clickable button with no drawer), or a category with subsections shown in a drawer that expands. */ -export type NavigationSection = { - category?: SidenavCategory; - subsections?: NavigationSubsection[]; - route?: string; - /* standalone is whether this is a clickable nav section with no subsections/drawer. */ - standalone?: boolean; - /* Icon is the custom icon to display for a standalone section. This should only for standalone sections, as icons for categories are derived automatically by CategoryIcon */ - Icon?: (props) => ReactNode; - /* title is the custom title of a standalone section */ - title?: string; -}; -/** - * NavigationSubsection is a subsection of a NavigationSection, these are the items listed in the drawer of a NavigationSection, or if isTopMenuItem is true, in the top menu (eg. Account Settings). - */ -export type NavigationSubsection = { - category?: SidenavCategory; - isTopMenuItem?: boolean; - title: string; - route: string; - exact: boolean; - icon: (props) => ReactNode; - 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( - features: TeleportFeature[] -): NavigationSection[] { - const navigationSections = NAVIGATION_CATEGORIES.map(category => ({ - category, - subsections: getSubsectionsForCategory(category, features), - })); - - return navigationSections; -} - -function getDashboardNavigationSections( - features: TeleportFeature[] -): NavigationSection[] { - const navigationSections = features - .filter(feature => feature.showInDashboard) - .map(feature => ({ - standalone: true, - title: feature.navigationItem.title, - Icon: feature.navigationItem.icon, - route: feature.navigationItem.getLink(cfg.proxyCluster), - })); - - return navigationSections; -} - -function getSubsectionsForCategory( - category: SidenavCategory, - features: TeleportFeature[] -): NavigationSubsection[] { - const filteredFeatures = features.filter( - feature => - feature.sideNavCategory === category && - !!feature.navigationItem && - !feature.parent - ); - - return filteredFeatures.map(feature => { - return { - category, - title: feature.navigationItem.title, - route: feature.navigationItem.getLink(cfg.proxyCluster), - exact: feature.navigationItem.exact, - icon: feature.navigationItem.icon, - searchableTags: feature.navigationItem.searchableTags, - }; - }); -} - -// 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). -/** - * getTopMenuSection returns a NavigationSection with the top menu items. This is not used in the sidenav, but will be used to make the top menu items searchable. - */ -function getTopMenuSection(features: TeleportFeature[]): NavigationSection { - const topMenuItems = features.filter( - feature => !!feature.topMenuItem && !feature.sideNavCategory - ); - - return { - subsections: topMenuItems.map(feature => ({ - isTopMenuItem: true, - title: feature.topMenuItem.title, - route: feature.topMenuItem.getLink(cfg.proxyCluster), - exact: feature?.route?.exact, - icon: feature.topMenuItem.icon, - searchableTags: feature.topMenuItem.searchableTags, - })), - }; -} - -function getNavSubsectionForRoute( - features: TeleportFeature[], - route: history.Location | Location -): NavigationSubsection { - const feature = features - .filter(feature => Boolean(feature.route)) - .find(feature => - matchPath(route.pathname, { - path: feature.route.path, - exact: feature.route.exact, - }) - ); - - if (!feature || (!feature.sideNavCategory && !feature.topMenuItem)) { - return; - } - - if (feature.topMenuItem) { - return { - isTopMenuItem: true, - exact: feature.route.exact, - title: feature.topMenuItem.title, - route: feature.topMenuItem.getLink(cfg.proxyCluster), - icon: feature.topMenuItem.icon, - searchableTags: feature.topMenuItem.searchableTags, - category: feature?.sideNavCategory, - }; - } - - return { - category: feature.sideNavCategory, - title: feature.navigationItem.title, - route: feature.navigationItem.getLink(cfg.proxyCluster), - exact: feature.navigationItem.exact, - icon: feature.navigationItem.icon, - searchableTags: feature.navigationItem.searchableTags, - }; -} - -/** - * 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 drawer 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 { clusterId } = useStickyClusterId(); - const { preferences, updatePreferences } = useUser(); - 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 = useMemo( - () => getNavSubsectionForRoute(features, history.location), - [features, history.location] - ); - - const stickyMode = preferences.sideNavDrawerMode === SideNavDrawerMode.STICKY; - - const toggleStickyMode = () => { - // Close the drawer right away if they're disabling sticky mode. - if (stickyMode) { - setIsClosing(false); - setPreviousExpandedSection(null); - setTargetSection(null); - } - updatePreferences({ - sideNavDrawerMode: stickyMode - ? SideNavDrawerMode.COLLAPSED - : SideNavDrawerMode.STICKY, - }); - }; - - const navSections = useMemo(() => { - if (cfg.isDashboard) { - return getDashboardNavigationSections(features); - } - return getNavigationSections(features).filter( - section => section.subsections.length - ); - }, [features]); - - const topMenuSection = useMemo(() => getTopMenuSection(features), [features]); - - const resourcesSection = useMemo(() => { - const searchParams = new URLSearchParams(location.search); - return getResourcesSection({ - clusterId, - preferences, - updatePreferences, - searchParams, - }); - }, [clusterId, preferences, updatePreferences]); - - const handleSetExpandedSection = useCallback( - (section: NavigationSection) => { - setIsClosing(false); - if (!section.standalone) { - setPreviousExpandedSection(debouncedSection); - setTargetSection(section); - } else { - setPreviousExpandedSection(null); - setTargetSection(null); - } - }, - [debouncedSection] - ); - - const combinedSideNavSections = useMemo( - () => [resourcesSection, ...navSections], - [resourcesSection, navSections] - ); - const currentPageSection = useMemo(() => { - return combinedSideNavSections.find( - section => section.category === currentView?.category - ); - }, [combinedSideNavSections, currentView]); - - const collapseDrawer = useCallback( - (closeAfterDelay = true) => { - if (stickyMode && currentPageSection) { - setPreviousExpandedSection(debouncedSection); - setTargetSection(currentPageSection); - } else { - setIsClosing(closeAfterDelay); - setPreviousExpandedSection(null); - setTargetSection(null); - } - }, - [currentPageSection, stickyMode, debouncedSection] - ); - - useEffect(() => { - // Whenever the user changes page, if stickyMode is enabled and the page is part of the sidenav, the drawer should be expanded with the current page section. - if (!stickyMode) { - return; - } - - // If the page is not part of the sidenav, such as Account Settings, curentPageSection will be undefined, and the drawer should be collapsed. - if (currentPageSection) { - // If there is already an expanded section set, don't change it. - if (debouncedSection) { - return; - } - handleSetExpandedSection(currentPageSection); - } else { - collapseDrawer(false); - } - }, [currentPageSection]); - - // Handler for clicking nav items. - const onNavigationItemClick = useCallback(() => { - // Clear any existing timeout - if (navigationTimeoutRef.current) { - clearTimeout(navigationTimeoutRef.current); - } - - if (!stickyMode) { - // 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(() => { - collapseDrawer(false); - }, 150); - } - }, [collapseDrawer]); - - // 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 ( - collapseDrawer()} - onKeyUp={e => e.key === 'Escape' && collapseDrawer(false)} - onBlur={(event: React.FocusEvent) => { - if (!event.currentTarget.contains(event.relatedTarget)) { - collapseDrawer(); - } - }} - className={ - stickyMode && - currentPageSection && - !!debouncedSection && - !debouncedSection.standalone - ? 'sticky-mode' - : '' - } - > - - - {!cfg.isDashboard && ( - <> - - - - )} - {navSections.map(section => { - if (section.standalone) { - return ( - - ); - } - - const isExpanded = - !!debouncedSection && - !debouncedSection.standalone && - section.category === debouncedSection?.category; - - return ( - - {section.category === 'Add New' && } - handleSetExpandedSection(section)} - currentPageSection={currentPageSection} - stickyMode={stickyMode} - toggleStickyMode={toggleStickyMode} - $active={section.category === currentView?.category} - aria-controls={`panel-${debouncedSection?.category}`} - onNavigationItemClick={onNavigationItemClick} - isExpanded={isExpanded} - /> - - ); - })} - - - ); -} - -const Container = styled(Box)` - position: relative; - width: var(--sidenav-width); - z-index: ${zIndexMap.sideNavContainer}; - - &.sticky-mode { - margin-right: ${rightPanelWidth}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/categories.ts b/web/packages/teleport/src/Navigation/SideNavigation/categories.ts deleted file mode 100644 index 6bc60bd20f818..0000000000000 --- a/web/packages/teleport/src/Navigation/SideNavigation/categories.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * 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 . - */ - -export enum NavigationCategory { - Resources = 'Resources', - Access = 'Access', - Identity = 'Identity', - Policy = 'Policy', - Audit = 'Audit', - AddNew = 'Add New', -} - -/** - * 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.Access, - NavigationCategory.Identity, - NavigationCategory.Policy, - NavigationCategory.Audit, - NavigationCategory.AddNew, -]; diff --git a/web/packages/teleport/src/Navigation/categories.ts b/web/packages/teleport/src/Navigation/categories.ts index 6640fb46f0437..6bc60bd20f818 100644 --- a/web/packages/teleport/src/Navigation/categories.ts +++ b/web/packages/teleport/src/Navigation/categories.ts @@ -18,28 +18,34 @@ export enum NavigationCategory { Resources = 'Resources', - Management = 'Management', + Access = 'Access', + Identity = 'Identity', + Policy = 'Policy', + Audit = 'Audit', + AddNew = 'Add New', } -export enum ManagementSection { - Access = 'Access Management', - Identity = 'Identity', - Activity = 'Activity', - Billing = 'Usage & Billing', - Clusters = 'Clusters', - Permissions = 'Permissions Management', +/** + * CustomNavigationCategory are pseudo-categories which exist only in the nav menu, eg. Search. + */ +export enum CustomNavigationCategory { + Search = 'Search', } -export const MANAGEMENT_NAVIGATION_SECTIONS = [ - ManagementSection.Access, - ManagementSection.Permissions, - ManagementSection.Identity, - ManagementSection.Activity, - ManagementSection.Billing, - ManagementSection.Clusters, -]; +/** + * 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.Management, + NavigationCategory.Access, + NavigationCategory.Identity, + NavigationCategory.Policy, + NavigationCategory.Audit, + NavigationCategory.AddNew, ]; diff --git a/web/packages/teleport/src/Navigation/common.tsx b/web/packages/teleport/src/Navigation/common.tsx deleted file mode 100644 index bd762b0ec9bf0..0000000000000 --- a/web/packages/teleport/src/Navigation/common.tsx +++ /dev/null @@ -1,82 +0,0 @@ -/** - * 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 . - */ - -import styled, { css } from 'styled-components'; - -export enum NavigationItemSize { - Small, - Large, - Indented, -} - -export const Icon = styled.div` - flex: 0 0 24px; - margin-right: 16px; - display: flex; - align-items: center; - justify-content: center; - svg { - height: 24px; - width: 24px; - } -`; - -export const SmallIcon = styled(Icon)` - flex: 0 0 14px; - margin-right: 10px; - svg { - height: 18px; - width: 18px; - } -`; - -interface LinkContentProps { - size: NavigationItemSize; -} - -const padding = { - [NavigationItemSize.Small]: '7px 0px 7px 30px', - [NavigationItemSize.Indented]: '7px 30px 7px 67px', - [NavigationItemSize.Large]: '16px 30px', -}; - -export const LinkContent = styled.div` - display: flex; - padding: ${p => padding[p.size]}; - align-items: center; - width: 100%; - opacity: 0.7; - transition: opacity 0.15s ease-in; -`; - -export const commonNavigationItemStyles = css` - display: flex; - position: relative; - color: ${props => props.theme.colors.text.main}; - text-decoration: none; - user-select: none; - font-size: 14px; - font-weight: 300; - border-left: 4px solid transparent; - transition: transform 0.3s cubic-bezier(0.19, 1, 0.22, 1); - will-change: transform; - - &:hover { - background: ${props => props.theme.colors.spotBackground[0]}; - } -`; diff --git a/web/packages/teleport/src/Navigation/index.ts b/web/packages/teleport/src/Navigation/index.ts index 320bdcf70c529..d7c33431a13fd 100644 --- a/web/packages/teleport/src/Navigation/index.ts +++ b/web/packages/teleport/src/Navigation/index.ts @@ -1,6 +1,6 @@ /** * Teleport - * Copyright (C) 2023 Gravitational, Inc. + * Copyright (C) 2025 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 @@ -16,5 +16,5 @@ * along with this program. If not, see . */ -export { Navigation as SideNavigation } from './SideNavigation/Navigation'; export { Navigation } from './Navigation'; +export { NavigationCategory } from './categories'; diff --git a/web/packages/teleport/src/Navigation/utils.tsx b/web/packages/teleport/src/Navigation/utils.tsx deleted file mode 100644 index f5c9643b125a2..0000000000000 --- a/web/packages/teleport/src/Navigation/utils.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/** - * 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 . - */ - -import { - Icon, - NavigationItemSize, - SmallIcon, -} from 'teleport/Navigation/common'; -import type { TeleportFeature } from 'teleport/types'; - -export function getIcon(feature: TeleportFeature, size: NavigationItemSize) { - switch (size) { - case NavigationItemSize.Large: - return {}; - - case NavigationItemSize.Small: - return {}; - } -} diff --git a/web/packages/teleport/src/Navigation/SideNavigation/zIndexMap.ts b/web/packages/teleport/src/Navigation/zIndexMap.ts similarity index 100% rename from web/packages/teleport/src/Navigation/SideNavigation/zIndexMap.ts rename to web/packages/teleport/src/Navigation/zIndexMap.ts diff --git a/web/packages/teleport/src/TopBar/ClusterSelector/ClusterSelector.story.tsx b/web/packages/teleport/src/TopBar/ClusterSelector/ClusterSelector.story.tsx deleted file mode 100644 index 134406a72d79f..0000000000000 --- a/web/packages/teleport/src/TopBar/ClusterSelector/ClusterSelector.story.tsx +++ /dev/null @@ -1,155 +0,0 @@ -/** - * 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 . - */ - -import { Flex } from 'design'; - -import ClusterSelector from './ClusterSelector'; - -export default { - title: 'Teleport/TopBar/ClusterSelector', -}; - -export const Component = () => { - return renderlusterSelector({ - defaultMenuIsOpen: true, - onLoad: () => Promise.resolve(clusters), - }); -}; - -export const Loading = () => { - return renderlusterSelector({ - defaultMenuIsOpen: true, - onLoad: () => new Promise(() => null), - }); -}; - -export const Failed = () => { - return renderlusterSelector({ - defaultMenuIsOpen: true, - onLoad: () => Promise.reject(new Error('server error')), - }); -}; - -function renderlusterSelector(props) { - return ( - - null} - {...props} - /> - - ); -} - -const clusters = [ - { - clusterId: 'cluster-Cordelia-Lynch', - }, - { - clusterId: 'cluster-Cameron-Smith', - }, - { - clusterId: 'cluster-Agnes-Lee', - }, - { - clusterId: 'cluster-Victor-Nguyen', - }, - { - clusterId: 'cluster-Catherine-Dennis', - }, - { - clusterId: 'cluster-Bertha-Maldonado', - }, - { - clusterId: 'cluster-Hulda-Mullins', - }, - { - clusterId: 'cluster-Mary-250-Andrews', - }, - { - clusterId: 'cluster-Mary-158-Andrews', - }, - { - clusterId: 'cluster-Mary-4-Andrews', - }, - { - clusterId: 'cluster-80-Mary-228-Andrews', - }, - { - clusterId: 'cluster-189-Mary-228-Andrews', - }, - { - clusterId: 'cluster-145-Mary-228-Andrews', - }, - { - clusterId: 'cluster-163-Mary-228-Andrews', - }, - { - clusterId: 'cluster-132-Mary-228-Andrews', - }, - { - clusterId: 'cluster-218-Mary-228-Andrews', - }, - { - clusterId: 'cluster-58-Mary-228-Andrews', - }, - { - clusterId: 'cluster-227-Mary-228-Andrews', - }, - { - clusterId: 'cluster-67-Mary-228-Andrews', - }, - { - clusterId: 'cluster-77-Mary-228-Andrews', - }, - { - clusterId: 'cluster-221-Mary-228-Andrews', - }, - { - clusterId: 'cluster-103-Mary-228-Andrews', - }, - { - clusterId: 'cluster-146-Mary-228-Andrews', - }, - { - clusterId: 'cluster-187-Mary-228-Andrews', - }, - { - clusterId: 'cluster-202-Mary-228-Andrews', - }, - { - clusterId: 'cluster-210-Mary-228-Andrews', - }, - { - clusterId: 'cluster-5-Mary-228-Andrews', - }, - { - clusterId: 'cluster-188-Mary-228-Andrews', - }, - { - clusterId: 'cluster-197-Mary-228-Andrews', - }, - { - clusterId: 'cluster-122-Mary-228-Andrews', - }, -]; diff --git a/web/packages/teleport/src/TopBar/ClusterSelector/ClusterSelector.tsx b/web/packages/teleport/src/TopBar/ClusterSelector/ClusterSelector.tsx deleted file mode 100644 index 6509b0496c392..0000000000000 --- a/web/packages/teleport/src/TopBar/ClusterSelector/ClusterSelector.tsx +++ /dev/null @@ -1,187 +0,0 @@ -/** - * 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 . - */ - -import { useState } from 'react'; -import { components, ValueContainerProps } from 'react-select'; -import styled from 'styled-components'; - -import { Box, Flex, Text } from 'design'; -import { SelectAsync } from 'shared/components/Select'; - -const ValueContainer = ({ - children, - ...props -}: ValueContainerProps