diff --git a/web/packages/teleport/src/Main/Main.tsx b/web/packages/teleport/src/Main/Main.tsx index 2ed373312d8d..3aa34cda1753 100644 --- a/web/packages/teleport/src/Main/Main.tsx +++ b/web/packages/teleport/src/Main/Main.tsx @@ -27,7 +27,7 @@ import React, { useState, } from 'react'; import styled from 'styled-components'; -import { Box, Indicator } from 'design'; +import { Box, Flex, Indicator } from 'design'; import { Failed } from 'design/CardError'; import useAttempt from 'shared/hooks/useAttemptNext'; @@ -35,13 +35,13 @@ import useAttempt from 'shared/hooks/useAttemptNext'; import { matchPath, useHistory } from 'react-router'; import Dialog from 'design/Dialog'; -import { sharedStyles } from 'design/theme/themes/sharedStyles'; import { Redirect, Route, Switch } from 'teleport/components/Router'; import { CatchError } from 'teleport/components/CatchError'; import cfg from 'teleport/config'; import useTeleport from 'teleport/useTeleport'; import { TopBar } from 'teleport/TopBar'; +import { TopBar as TopBarSideNav } from 'teleport/TopBar/TopBarSideNav'; import { BannerList } from 'teleport/components/BannerList'; import { storageService } from 'teleport/services/storageService'; import { @@ -51,11 +51,9 @@ import { } from 'teleport/services/alerts/alerts'; import { useAlerts } from 'teleport/components/BannerList/useAlerts'; import { FeaturesContextProvider, useFeatures } from 'teleport/FeaturesContext'; -import { - getFirstRouteForCategory, - Navigation, -} from 'teleport/Navigation/Navigation'; -import { NavigationCategory } from 'teleport/Navigation/categories'; + +import { Navigation as SideNavigation } from 'teleport/Navigation/SideNavigation/Navigation'; +import { Navigation } from 'teleport/Navigation'; import { TopBarProps } from 'teleport/TopBar/TopBar'; import { useUser } from 'teleport/User/UserContext'; import { QuestionnaireProps } from 'teleport/Welcome/NewCredentials'; @@ -84,6 +82,10 @@ export function Main(props: MainProps) { const { preferences } = useUser(); + const isTopBarView = storageService.getIsTopBarView(); + const TopBarComponent = isTopBarView ? TopBar : TopBarSideNav; + const NavigationComponent = isTopBarView ? Navigation : SideNavigation; + useEffect(() => { if (ctx.storeUser.state) { setAttempt({ status: 'success' }); @@ -99,14 +101,6 @@ export function Main(props: MainProps) { () => props.features.filter(feature => feature.hasAccess(featureFlags)), [featureFlags, props.features] ); - const feature = features - .filter(feature => Boolean(feature.route)) - .find(f => - matchPath(history.location.pathname, { - path: f.route.path, - exact: f.route.exact ?? false, - }) - ); const { alerts, dismissAlert } = useAlerts(props.initialAlerts); @@ -168,7 +162,7 @@ export function Main(props: MainProps) { const indexRoute = cfg.isDashboard ? cfg.routes.downloadCenter - : getFirstRouteForCategory(features, NavigationCategory.Resources); + : cfg.getUnifiedResourcesRoute(cfg.proxyCluster); return ; } @@ -197,13 +191,10 @@ export function Main(props: MainProps) { const requiresOnboarding = onboard && !onboard.hasResource && !onboard.notified; const displayOnboardDiscover = requiresOnboarding && showOnboardDiscover; - const hasSidebar = - feature?.category === NavigationCategory.Management && - !feature?.hideNavigation; return ( - - - + + - + {displayOnboardDiscover && ( @@ -351,23 +342,15 @@ export const ContentMinWidth = ({ children }: { children: ReactNode }) => { ); }; -function getWidth(hasSidebar?: boolean) { - const { sidebarWidth } = sharedStyles; - if (hasSidebar) { - return `max-width: calc(100% - ${sidebarWidth}px);`; - } - return 'max-width: 100%;'; -} - -export const HorizontalSplit = styled.div<{ hasSidebar?: boolean }>` +export const ContentWrapper = styled.div` display: flex; flex-direction: column; flex: 1; - ${props => getWidth(props.hasSidebar)} overflow-x: auto; + max-width: 100%; `; -export const StyledIndicator = styled(HorizontalSplit)` +export const StyledIndicator = styled(Flex)` align-items: center; justify-content: center; position: absolute; @@ -380,5 +363,5 @@ const Wrapper = styled(Box)` display: flex; height: 100vh; flex-direction: column; - width: 100vw; + max-width: 100vw; `; diff --git a/web/packages/teleport/src/Main/MainContainer.tsx b/web/packages/teleport/src/Main/MainContainer.tsx index 6d3997601601..4f7910b80e5d 100644 --- a/web/packages/teleport/src/Main/MainContainer.tsx +++ b/web/packages/teleport/src/Main/MainContainer.tsx @@ -26,8 +26,9 @@ import styled from 'styled-components'; export const MainContainer = styled.div` display: flex; flex: 1; - min-height: 0; --sidebar-width: 256px; + --sidenav-width: 76px; + --sidenav-panel-width: 224px; 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; diff --git a/web/packages/teleport/src/Main/index.ts b/web/packages/teleport/src/Main/index.ts index 52815afd5f5f..ca454d1ce48a 100644 --- a/web/packages/teleport/src/Main/index.ts +++ b/web/packages/teleport/src/Main/index.ts @@ -16,11 +16,11 @@ * along with this program. If not, see . */ +export { zIndexMap } from './zIndexMap'; export { Main, useContentMinWidthContext, useNoMinWidth, - HorizontalSplit, StyledIndicator, } from './Main'; export { MainContainer } from './MainContainer'; diff --git a/web/packages/teleport/src/Main/zIndexMap.ts b/web/packages/teleport/src/Main/zIndexMap.ts new file mode 100644 index 000000000000..75d1ecdc2f82 --- /dev/null +++ b/web/packages/teleport/src/Main/zIndexMap.ts @@ -0,0 +1,23 @@ +/** + * 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 . + */ + +export const zIndexMap = { + topBar: 22, + sideNavContainer: 21, + sideNavExpandedPanel: 20, +}; diff --git a/web/packages/teleport/src/Navigation/SideNavigation/CategoryIcon.tsx b/web/packages/teleport/src/Navigation/SideNavigation/CategoryIcon.tsx new file mode 100644 index 000000000000..0ac5f0bf8be8 --- /dev/null +++ b/web/packages/teleport/src/Navigation/SideNavigation/CategoryIcon.tsx @@ -0,0 +1,42 @@ +/** + * 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 * as Icons from 'design/Icon'; + +import { NavigationCategory } from './categories'; + +export function CategoryIcon({ category }: { category: NavigationCategory }) { + switch (category) { + case NavigationCategory.Resources: + return ; + case NavigationCategory.Access: + return ; + case NavigationCategory.Identity: + return ; + case NavigationCategory.Policy: + return ; + case NavigationCategory.Audit: + return ; + case NavigationCategory.AddNew: + return ; + default: + return null; + } +} diff --git a/web/packages/teleport/src/Navigation/SideNavigation/Navigation.tsx b/web/packages/teleport/src/Navigation/SideNavigation/Navigation.tsx new file mode 100644 index 000000000000..bc65d0824743 --- /dev/null +++ b/web/packages/teleport/src/Navigation/SideNavigation/Navigation.tsx @@ -0,0 +1,536 @@ +/** + * 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, { useState, useRef, useCallback, useEffect } from 'react'; +import styled, { css } from 'styled-components'; +import { matchPath, useHistory } from 'react-router'; +import { NavLink } from 'react-router-dom'; +import { Text, Flex, Box } from 'design'; +import { Theme } from 'design/theme/themes/types'; + +import cfg from 'teleport/config'; + +import { useFeatures } from 'teleport/FeaturesContext'; +import { zIndexMap } from 'teleport/Main'; + +import { + NavigationCategory, + NAVIGATION_CATEGORIES, + STANDALONE_CATEGORIES, +} from './categories'; +import { CategoryIcon } from './CategoryIcon'; + +import type * as history from 'history'; + +import type { TeleportFeature } from 'teleport/types'; + +const SideNavContainer = styled(Flex).attrs({ + gap: 2, + pt: 2, + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'start', + bg: 'levels.surface', +})` + height: 100%; + width: var(--sidenav-width); + position: relative; + border-right: 1px solid ${p => p.theme.colors.spotBackground[1]}; + z-index: ${zIndexMap.sideNavContainer}; + overflow-y: auto; +`; + +const verticalPadding = '12px'; + +const rightPanelWidth = '224px'; + +const RightPanel = styled(Box).attrs({ pt: 2, pb: 4, px: 2 })<{ + isVisible: boolean; +}>` + position: absolute; + top: 0; + left: var(--sidenav-width); + height: 100%; + scrollbar-gutter: auto; + overflow-y: auto; + width: ${rightPanelWidth}; + background: ${p => p.theme.colors.levels.surface}; + z-index: ${zIndexMap.sideNavExpandedPanel}; + border-right: 1px solid ${p => p.theme.colors.spotBackground[1]}; + + transition: transform 0.3s ease-in-out; + ${props => + props.isVisible + ? ` + transition: transform .15s ease-out; + transform: translateX(0); + ` + : ` + transition: transform .15s ease-in; + transform: translateX(-100%); + `} + + padding-top: ${p => p.theme.topBarHeight[0]}px; + @media screen and (min-width: ${p => p.theme.breakpoints.small}px) { + padding-top: ${p => p.theme.topBarHeight[1]}px; + } + @media screen and (min-width: ${p => p.theme.breakpoints.large}px) { + padding-top: ${p => p.theme.topBarHeight[2]}px; + } +`; + +type NavigationSection = { + category: NavigationCategory; + subsections: NavigationSubsection[]; + standalone?: boolean; +}; + +type NavigationSubsection = { + category: NavigationCategory; + title: string; + route: string; + exact: boolean; + icon: (props) => JSX.Element; + parent?: TeleportFeature; +}; + +function getNavigationSections( + features: TeleportFeature[] +): NavigationSection[] { + const navigationSections = NAVIGATION_CATEGORIES.map(category => ({ + category, + subsections: getSubsectionsForCategory(category, features), + standalone: STANDALONE_CATEGORIES.indexOf(category) !== -1, + })); + + return navigationSections; +} + +function getSubsectionsForCategory( + category: NavigationCategory, + 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, + }; + }); +} + +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, + }) + ); + + if (!feature || !feature.sideNavCategory) { + return; + } + + return { + category: feature.sideNavCategory, + title: feature.navigationItem.title, + route: feature.navigationItem.getLink(cfg.proxyCluster), + exact: feature.navigationItem.exact, + icon: feature.navigationItem.icon, + }; +} + +export function Navigation() { + const features = useFeatures(); + const history = useHistory(); + const [expandedSection, setExpandedSection] = + useState(null); + const [expandedSectionIndex, setExpandedSectionIndex] = useState(-1); + + const currentView = getNavSubsectionForRoute(features, history.location); + + const navSections = getNavigationSections(features).filter( + section => section.subsections.length + ); + + const sectionRefs = useRef>([]); + const subsectionRefs = useRef>([]); + + const handleKeyDown = useCallback( + ( + event: React.KeyboardEvent, + index: number, + isSubsection: boolean, + section: NavigationSection + ) => { + if (event.key === 'Tab') { + if (!isSubsection) return; + + // When the user presses shift+tab on first subsection item of a section. + if (event.shiftKey && index === 0) { + event.preventDefault(); + const prevSectionIndex = + (expandedSectionIndex - 1 + navSections.length) % + navSections.length; + sectionRefs.current[prevSectionIndex]?.focus(); + } else if ( + !event.shiftKey && + index === expandedSection.subsections.length - 1 + ) { + // When the user presses tab on last subsection item of a section. + event.preventDefault(); + const nextSectionIndex = + (expandedSectionIndex + 1) % navSections.length; + sectionRefs.current[nextSectionIndex]?.focus(); + } + } else if (event.key === 'Enter' && !isSubsection) { + if (section?.standalone) { + event.preventDefault(); + history.push(section.subsections[0].route); + } else { + event.preventDefault(); + subsectionRefs.current[0]?.focus(); + } + } + }, + [expandedSection, expandedSectionIndex, navSections.length] + ); + + const handleSetExpandedSection = useCallback( + (section: NavigationSection, index: number) => { + setExpandedSection(section); + setExpandedSectionIndex(index); + }, + [] + ); + + // Reset subsectionRefs when expanded section changes + useEffect(() => { + subsectionRefs.current = subsectionRefs.current.slice( + 0, + expandedSection?.subsections.length || 0 + ); + }, [expandedSection]); + + return ( + setExpandedSection(null)} + onKeyUp={e => e.key === 'Escape' && setExpandedSection(null)} + css={'height: 100%;'} + > + + {navSections.map((section, index) => ( +
handleSetExpandedSection(section, index)} + aria-controls={`panel-${expandedSection?.category}`} + ref={el => (sectionRefs.current[index] = el)} + onKeyDown={e => handleKeyDown(e, index, false, section)} + onClick={() => { + if (section.standalone) { + history.push(section.subsections[0].route); + } + }} + /> + ))} + + + + + {expandedSection?.category} + + + {expandedSection?.subsections.map((section, idx) => ( + (subsectionRefs.current[idx] = el)} + active={currentView?.route === section.route} + to={section.route} + exact={section.exact} + key={section.title} + tabIndex={0} + role="button" + onKeyDown={e => handleKeyDown(e, idx, true, null)} + > + + {section.title} + + ))} + + + ); +} + +const SubsectionItem = React.forwardRef< + HTMLAnchorElement, + { + active: boolean; + to: string; + exact: boolean; + tabIndex: number; + role: string; + onKeyDown: (event: React.KeyboardEvent) => void; + children: React.ReactNode; + } +>((props, ref) => ); + +const StyledSubsectionItem = styled(NavLink)<{ active: boolean }>` + display: flex; + position: relative; + color: ${props => props.theme.colors.text.slightlyMuted}; + text-decoration: none; + user-select: none; + gap: ${props => props.theme.space[2]}px; + padding-top: ${verticalPadding}; + padding-bottom: ${verticalPadding}; + padding-left: ${props => props.theme.space[3]}px; + padding-right: ${props => props.theme.space[3]}px; + border-radius: ${props => props.theme.radii[2]}px; + cursor: pointer; + + ${props => getSubsectionStyles(props.theme, props.active)} +`; + +const Section = React.forwardRef< + HTMLButtonElement, + { + section: NavigationSection; + active: boolean; + setExpandedSection: () => void; + onKeyDown: (event: React.KeyboardEvent) => void; + onClick: (event: React.MouseEvent) => void; + } +>(({ section, active, setExpandedSection, onKeyDown, onClick }, ref) => { + return ( + + + {section.category} + + ); +}); + +const CategoryButton = styled.button<{ active: boolean }>` + height: 60px; + width: 60px; + cursor: pointer; + outline: hidden; + border: none; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + border-radius: ${props => props.theme.radii[2]}px; + + font-size: ${props => props.theme.typography.body4.fontSize}; + font-weight: ${props => props.theme.typography.body4.fontWeight}; + letter-spacing: ${props => props.theme.typography.body4.letterSpacing}; + line-height: ${props => props.theme.typography.body4.lineHeight}; + + ${props => getCategoryStyles(props.theme, props.active)} +`; + +function getCategoryStyles(theme: Theme, active: boolean) { + if (active) { + return css` + color: ${theme.colors.brand}; + background: ${theme.colors.interactive.tonal.primary[0].background}; + &:hover, + &:focus { + background: ${theme.colors.interactive.tonal.primary[1].background}; + color: ${theme.colors.interactive.tonal.primary[0].text}; + } + &:active { + background: ${theme.colors.interactive.tonal.primary[2].background}; + color: ${theme.colors.interactive.tonal.primary[1].text}; + } + `; + } + + return css` + background: transparent; + color: ${props => props.theme.colors.text.slightlyMuted}; + &:hover, + &:focus { + background: ${props => props.theme.colors.interactive.tonal.neutral[0]}; + color: ${props => props.theme.colors.text.main}; + } + &:active { + background: ${props => props.theme.colors.interactive.tonal.neutral[1]}; + color: ${props => props.theme.colors.text.main}; + } + `; +} + +function getSubsectionStyles(theme: Theme, active: boolean) { + if (active) { + return css` + color: ${theme.colors.brand}; + background: ${theme.colors.interactive.tonal.primary[0].background}; + &:focus { + outline: 2px solid + ${theme.colors.interactive.solid.primary.default.background}; + } + &:hover { + background: ${theme.colors.interactive.tonal.primary[1].background}; + color: ${theme.colors.interactive.tonal.primary[0].text}; + } + &:active { + background: ${theme.colors.interactive.tonal.primary[2].background}; + color: ${theme.colors.interactive.tonal.primary[1].text}; + } + `; + } + + return css` + color: ${props => props.theme.colors.text.slightlyMuted}; + &:focus { + outline: 2px solid ${theme.colors.text.muted}; + } + &:hover { + background: ${props => props.theme.colors.interactive.tonal.neutral[0]}; + color: ${props => props.theme.colors.text.main}; + } + &:active { + background: ${props => props.theme.colors.interactive.tonal.neutral[1]}; + color: ${props => props.theme.colors.text.main}; + } + `; +} + +// TODO(rudream): Figure out best place for for license footers. +// function AGPLFooter() { +// 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. +// +// } +// /> +// ); +// } + +// function CommunityFooter() { +// return ( +// +// +// Upgrade to Teleport Enterprise +// {' '} +// for SSO, just-in-time access requests, Access Graph, and much more! +// +// } +// /> +// ); +// } + +// function LicenseFooter({ +// title, +// subText, +// infoContent, +// }: { +// title: string; +// subText: string; +// infoContent: JSX.Element; +// }) { +// const [opened, setOpened] = useState(false); +// return ( +// setOpened(false)}> +// +// {title} +// setOpened(true)}> +// +// {opened && {infoContent}} +// +// +// {subText} +// +// ); +// } + +// const StyledFooterBox = styled(Box)` +// line-height: 20px; +// border-top: ${props => props.theme.borders[1]} +// ${props => props.theme.colors.spotBackground[0]}; +// `; + +// const SubText = styled(Text)` +// color: ${props => props.theme.colors.text.disabled}; +// font-size: ${props => props.theme.fontSizes[1]}px; +// `; + +// const TooltipContent = styled(Box)` +// width: max-content; +// position: absolute; +// bottom: 0; +// left: 24px; +// padding: 12px 16px 12px 16px; +// box-shadow: ${p => p.theme.boxShadow[1]}; +// background-color: ${props => props.theme.colors.tooltip.background}; +// z-index: 20; +// `; + +// const FooterContent = styled(Flex)` +// position: relative; +// `; diff --git a/web/packages/teleport/src/Navigation/SideNavigation/categories.ts b/web/packages/teleport/src/Navigation/SideNavigation/categories.ts new file mode 100644 index 000000000000..ba091059881c --- /dev/null +++ b/web/packages/teleport/src/Navigation/SideNavigation/categories.ts @@ -0,0 +1,37 @@ +/** + * 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', +} + +export const NAVIGATION_CATEGORIES = [ + NavigationCategory.Resources, + NavigationCategory.Access, + NavigationCategory.Identity, + NavigationCategory.Policy, + NavigationCategory.Audit, + NavigationCategory.AddNew, +]; + +export const STANDALONE_CATEGORIES = [NavigationCategory.AddNew]; diff --git a/web/packages/teleport/src/Navigation/index.ts b/web/packages/teleport/src/Navigation/index.ts index 45911a88936a..320bdcf70c52 100644 --- a/web/packages/teleport/src/Navigation/index.ts +++ b/web/packages/teleport/src/Navigation/index.ts @@ -16,4 +16,5 @@ * along with this program. If not, see . */ +export { Navigation as SideNavigation } from './SideNavigation/Navigation'; export { Navigation } from './Navigation'; diff --git a/web/packages/teleport/src/TopBar/TopBarSideNav.tsx b/web/packages/teleport/src/TopBar/TopBarSideNav.tsx new file mode 100644 index 000000000000..5860bcdc5ca8 --- /dev/null +++ b/web/packages/teleport/src/TopBar/TopBarSideNav.tsx @@ -0,0 +1,167 @@ +/** + * 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 from 'react'; +import styled, { useTheme } from 'styled-components'; +import { Link } from 'react-router-dom'; +import { Flex, Image, TopNav } from 'design'; +import { matchPath, useHistory } from 'react-router'; +import { Theme } from 'design/theme/themes/types'; +import { HoverTooltip } from 'shared/components/ToolTip'; + +import useTeleport from 'teleport/useTeleport'; +import { UserMenuNav } from 'teleport/components/UserMenuNav'; +import { useFeatures } from 'teleport/FeaturesContext'; +import cfg from 'teleport/config'; +import { useLayout } from 'teleport/Main/LayoutContext'; +import { logos } from 'teleport/components/LogoHero/LogoHero'; + +import { Notifications } from 'teleport/Notifications'; +import { zIndexMap } from 'teleport/Main'; + +export function TopBar({ CustomLogo }: TopBarProps) { + const ctx = useTeleport(); + const history = useHistory(); + const features = useFeatures(); + const { currentWidth } = useLayout(); + const theme: Theme = useTheme(); + + // find active feature + const feature = features.find( + f => + f.route && + matchPath(history.location.pathname, { + path: f.route.path, + exact: f.route.exact ?? false, + }) + ); + + const iconSize = + currentWidth >= theme.breakpoints.medium + ? navigationIconSizeMedium + : navigationIconSizeSmall; + + return ( + + {!feature?.hideNavigation && } + {!feature?.logoOnlyTopbar && ( + + + + + )} + + ); +} + +export const TopBarContainer = styled(TopNav)` + position: absolute; + width: 100%; + display: flex; + justify-content: space-between; + background: ${p => p.theme.colors.levels.surface}; + overflow-y: initial; + overflow-x: none; + flex-shrink: 0; + z-index: ${zIndexMap.topBar}; + border-bottom: 1px solid ${({ theme }) => theme.colors.spotBackground[1]}; + + height: ${p => p.theme.topBarHeight[0]}px; + @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) => { + const theme = useTheme(); + const src = logos[cfg.edition][theme.type]; + + return ( + p.theme.breakpoints.medium}px) { + margin-right: 76px; + } + @media screen and (min-width: ${p => p.theme.breakpoints.large}px) { + margin-right: 67px; + } + `} + > + + p.theme.colors.interactive.tonal.primary[0].background}; + } + align-items: center; + `} + to={cfg.routes.root} + > + {CustomLogo ? ( + + ) : ( + teleport logo props.theme.space[3]}px; + padding-right: ${props => props.theme.space[3]}px; + height: 18px; + @media screen and (min-width: ${p => + p.theme.breakpoints.small}px) { + height: 28px; + padding-left: ${props => props.theme.space[4]}px; + padding-right: ${props => props.theme.space[4]}px; + } + @media screen and (min-width: ${p => + p.theme.breakpoints.large}px) { + height: 30px; + } + `} + /> + )} + + + ); +}; + +export const navigationIconSizeSmall = 20; +export const navigationIconSizeMedium = 24; + +export type NavigationItem = { + title: string; + path: string; + Icon: JSX.Element; +}; + +export type TopBarProps = { + CustomLogo?: () => React.ReactElement; + showPoweredByLogo?: boolean; +}; diff --git a/web/packages/teleport/src/features.tsx b/web/packages/teleport/src/features.tsx index 61c6dffe7eee..4505b75761fe 100644 --- a/web/packages/teleport/src/features.tsx +++ b/web/packages/teleport/src/features.tsx @@ -42,9 +42,10 @@ import { import cfg from 'teleport/config'; import { - ManagementSection, NavigationCategory, + ManagementSection, } from 'teleport/Navigation/categories'; +import { NavigationCategory as SideNavigationCategory } from 'teleport/Navigation/SideNavigation/categories'; import { IntegrationEnroll } from '@gravitational/teleport/src/Integrations/Enroll'; import { NavTitle } from './types'; @@ -74,6 +75,7 @@ import type { FeatureFlags, TeleportFeature } from './types'; class AccessRequests implements TeleportFeature { category = NavigationCategory.Resources; + sideNavCategory = SideNavigationCategory.Resources; route = { title: 'Access Requests', @@ -101,6 +103,7 @@ class AccessRequests implements TeleportFeature { export class FeatureJoinTokens implements TeleportFeature { category = NavigationCategory.Management; section = ManagementSection.Access; + sideNavCategory = SideNavigationCategory.Access; navigationItem = { title: NavTitle.JoinTokens, icon: Key, @@ -123,6 +126,9 @@ export class FeatureJoinTokens implements TeleportFeature { } export class FeatureUnifiedResources implements TeleportFeature { + category = NavigationCategory.Resources; + sideNavCategory = SideNavigationCategory.Resources; + route = { title: 'Resources', path: cfg.routes.unifiedResources, @@ -139,8 +145,6 @@ export class FeatureUnifiedResources implements TeleportFeature { }, }; - category = NavigationCategory.Resources; - hasAccess() { return !cfg.isDashboard; } @@ -152,6 +156,7 @@ export class FeatureUnifiedResources implements TeleportFeature { export class FeatureSessions implements TeleportFeature { category = NavigationCategory.Resources; + sideNavCategory = SideNavigationCategory.Audit; route = { title: 'Active Sessions', @@ -184,6 +189,7 @@ export class FeatureSessions implements TeleportFeature { export class FeatureUsers implements TeleportFeature { category = NavigationCategory.Management; section = ManagementSection.Access; + sideNavCategory = SideNavigationCategory.Access; route = { title: 'Manage Users', @@ -213,6 +219,7 @@ export class FeatureUsers implements TeleportFeature { export class FeatureBots implements TeleportFeature { category = NavigationCategory.Management; section = ManagementSection.Access; + sideNavCategory = SideNavigationCategory.Access; route = { title: 'Manage Bots', @@ -242,6 +249,7 @@ export class FeatureBots implements TeleportFeature { export class FeatureAddBots implements TeleportFeature { category = NavigationCategory.Management; section = ManagementSection.Access; + sideNavCategory = SideNavigationCategory.Access; hideFromNavigation = true; route = { @@ -263,6 +271,7 @@ export class FeatureAddBots implements TeleportFeature { export class FeatureRoles implements TeleportFeature { category = NavigationCategory.Management; section = ManagementSection.Permissions; + sideNavCategory = SideNavigationCategory.Access; route = { title: 'Manage User Roles', @@ -288,6 +297,7 @@ export class FeatureRoles implements TeleportFeature { export class FeatureAuthConnectors implements TeleportFeature { category = NavigationCategory.Management; section = ManagementSection.Access; + sideNavCategory = SideNavigationCategory.Access; route = { title: 'Manage Auth Connectors', @@ -313,9 +323,10 @@ export class FeatureAuthConnectors implements TeleportFeature { export class FeatureLocks implements TeleportFeature { category = NavigationCategory.Management; section = ManagementSection.Identity; + sideNavCategory = SideNavigationCategory.Identity; route = { - title: 'Manage Session & Identity Locks', + title: 'Session & Identity Locks', path: cfg.routes.locks, exact: true, component: Locks, @@ -355,6 +366,11 @@ export class FeatureNewLock implements TeleportFeature { } export class FeatureDiscover implements TeleportFeature { + category = NavigationCategory.Management; + section = ManagementSection.Access; + sideNavCategory = SideNavigationCategory.AddNew; + standalone = true; + route = { title: 'Enroll New Resource', path: cfg.routes.discover, @@ -371,9 +387,6 @@ export class FeatureDiscover implements TeleportFeature { }, }; - category = NavigationCategory.Management; - section = ManagementSection.Access; - hasAccess(flags: FeatureFlags) { return flags.discover; } @@ -384,6 +397,7 @@ export class FeatureDiscover implements TeleportFeature { } export class FeatureIntegrations implements TeleportFeature { + sideNavCategory = SideNavigationCategory.Access; category = NavigationCategory.Management; section = ManagementSection.Access; @@ -415,6 +429,8 @@ export class FeatureIntegrations implements TeleportFeature { export class FeatureIntegrationEnroll implements TeleportFeature { category = NavigationCategory.Management; section = ManagementSection.Access; + sideNavCategory = SideNavigationCategory.Access; + parent = FeatureIntegrations; route = { title: 'Enroll New Integration', @@ -447,6 +463,7 @@ export class FeatureIntegrationEnroll implements TeleportFeature { export class FeatureRecordings implements TeleportFeature { category = NavigationCategory.Management; section = ManagementSection.Activity; + sideNavCategory = SideNavigationCategory.Audit; route = { title: 'Session Recordings', @@ -472,6 +489,7 @@ export class FeatureRecordings implements TeleportFeature { export class FeatureAudit implements TeleportFeature { category = NavigationCategory.Management; section = ManagementSection.Activity; + sideNavCategory = SideNavigationCategory.Audit; route = { title: 'Audit Log', @@ -497,6 +515,7 @@ export class FeatureAudit implements TeleportFeature { export class FeatureClusters implements TeleportFeature { category = NavigationCategory.Management; section = ManagementSection.Clusters; + sideNavCategory = SideNavigationCategory.Access; route = { title: 'Clusters', @@ -522,6 +541,7 @@ export class FeatureClusters implements TeleportFeature { export class FeatureTrust implements TeleportFeature { category = NavigationCategory.Management; section = ManagementSection.Clusters; + sideNavCategory = SideNavigationCategory.Access; route = { title: 'Trusted Root Clusters', @@ -545,8 +565,9 @@ export class FeatureTrust implements TeleportFeature { class FeatureDeviceTrust implements TeleportFeature { category = NavigationCategory.Management; section = ManagementSection.Identity; + sideNavCategory = SideNavigationCategory.Identity; route = { - title: 'Manage Trusted Devices', + title: 'Trusted Devices', path: cfg.routes.deviceTrust, exact: true, component: DeviceTrustLocked, @@ -615,37 +636,35 @@ export class FeatureHelpAndSupport implements TeleportFeature { export function getOSSFeatures(): TeleportFeature[] { return [ // Resources + // TODO(rudream): Implement shortcuts to pinned/nodes/apps/dbs/desktops/kubes. new FeatureUnifiedResources(), - new AccessRequests(), - new FeatureSessions(), // Management // - Access new FeatureUsers(), + new FeatureRoles(), new FeatureBots(), new FeatureAddBots(), + new FeatureJoinTokens(), new FeatureAuthConnectors(), new FeatureIntegrations(), - new FeatureJoinTokens(), - new FeatureDiscover(), new FeatureIntegrationEnroll(), - - // - Permissions - new FeatureRoles(), + new FeatureClusters(), + new FeatureTrust(), // - Identity + new AccessRequests(), new FeatureLocks(), new FeatureNewLock(), new FeatureDeviceTrust(), - // - Activity - new FeatureRecordings(), + // - Audit new FeatureAudit(), + new FeatureRecordings(), + new FeatureSessions(), - // - Clusters - new FeatureClusters(), - new FeatureTrust(), + new FeatureDiscover(), // Other new FeatureAccount(), diff --git a/web/packages/teleport/src/services/storageService/storageService.ts b/web/packages/teleport/src/services/storageService/storageService.ts index 644543046f59..f272b8943fb3 100644 --- a/web/packages/teleport/src/services/storageService/storageService.ts +++ b/web/packages/teleport/src/services/storageService/storageService.ts @@ -260,4 +260,8 @@ export const storageService = { JSON.stringify(true) ); }, + + getIsTopBarView(): boolean { + return this.getParsedJSONValue(KeysEnum.USE_TOP_BAR, false); + }, }; diff --git a/web/packages/teleport/src/services/storageService/types.ts b/web/packages/teleport/src/services/storageService/types.ts index 71ad6d5992ab..9eda32f0051f 100644 --- a/web/packages/teleport/src/services/storageService/types.ts +++ b/web/packages/teleport/src/services/storageService/types.ts @@ -36,6 +36,9 @@ export const KeysEnum = { USERS_NOT_EQUAL_TO_MAU_ACKNOWLEDGED: 'grv_users_not_equal_to_mau_acknowledged', LOCAL_NOTIFICATION_STATES: 'grv_teleport_notification_states', + + //TODO(rudream): Remove once sidenav implementation is complete. + USE_TOP_BAR: 'grv_teleport_use_topbar', }; // SurveyRequest is the request for sending data to the back end diff --git a/web/packages/teleport/src/types.ts b/web/packages/teleport/src/types.ts index d6f370bac769..4d75a4efaaea 100644 --- a/web/packages/teleport/src/types.ts +++ b/web/packages/teleport/src/types.ts @@ -25,6 +25,8 @@ import { NavigationCategory, } from 'teleport/Navigation/categories'; +import { NavigationCategory as SideNavigationCategory } from './Navigation/SideNavigation/categories'; + export type NavGroup = 'team' | 'activity' | 'clusters' | 'accessrequests'; export interface Context { @@ -106,6 +108,10 @@ export interface TeleportFeatureRoute { export interface TeleportFeature { parent?: new () => TeleportFeature | null; category?: NavigationCategory; + // TODO(rudream): Delete category field above and rename sideNavCategory field to category once old nav is removed. + sideNavCategory?: SideNavigationCategory; + /** standalone is whether this feature has no subsections */ + standalone?: boolean; section?: ManagementSection; hasAccess(flags: FeatureFlags): boolean; // logoOnlyTopbar is used to optionally hide the elements in the topbar from view except for the logo.