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 ? (
+
+ ) : (
+ 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.