From 7e8be3bf7669d79907cce42f405e1a90bae36320 Mon Sep 17 00:00:00 2001 From: Lisa Kim Date: Mon, 10 Mar 2025 11:26:16 -0700 Subject: [PATCH] Web: Create a sliding info guide panel component Re-uses nav's sliding menu --- .../ManageCluster/ManageCluster.story.tsx | 13 +- .../ManageCluster/ManageCluster.test.tsx | 9 +- .../teleport/src/Main/InfoGuideContext.tsx | 76 +++++++++ web/packages/teleport/src/Main/Main.test.tsx | 77 ++++++++- web/packages/teleport/src/Main/Main.tsx | 41 +++-- .../teleport/src/Navigation/Section.tsx | 57 +++---- .../teleport/src/Navigation/zIndexMap.ts | 9 +- .../teleport/src/Support/Support.story.tsx | 9 +- .../InfoGuideSidePanel.story.tsx | 44 +++++ .../InfoGuideSidePanel/InfoGuideSidePanel.tsx | 158 ++++++++++++++++++ .../InfoGuideSidePanel/index.ts | 19 +++ .../SlidingSidePanel.story.tsx | 87 ++++++++++ .../SlidingSidePanel/SlidingSidePanel.tsx | 85 ++++++++++ .../src/components/SlidingSidePanel/index.ts | 20 +++ .../SlidingSidePanel/storyHelpers.tsx | 128 ++++++++++++++ 15 files changed, 771 insertions(+), 61 deletions(-) create mode 100644 web/packages/teleport/src/Main/InfoGuideContext.tsx create mode 100644 web/packages/teleport/src/components/SlidingSidePanel/InfoGuideSidePanel/InfoGuideSidePanel.story.tsx create mode 100644 web/packages/teleport/src/components/SlidingSidePanel/InfoGuideSidePanel/InfoGuideSidePanel.tsx create mode 100644 web/packages/teleport/src/components/SlidingSidePanel/InfoGuideSidePanel/index.ts create mode 100644 web/packages/teleport/src/components/SlidingSidePanel/SlidingSidePanel.story.tsx create mode 100644 web/packages/teleport/src/components/SlidingSidePanel/SlidingSidePanel.tsx create mode 100644 web/packages/teleport/src/components/SlidingSidePanel/index.ts create mode 100644 web/packages/teleport/src/components/SlidingSidePanel/storyHelpers.tsx diff --git a/web/packages/teleport/src/Clusters/ManageCluster/ManageCluster.story.tsx b/web/packages/teleport/src/Clusters/ManageCluster/ManageCluster.story.tsx index a5fa9a492b153..c3b92f1dde55f 100644 --- a/web/packages/teleport/src/Clusters/ManageCluster/ManageCluster.story.tsx +++ b/web/packages/teleport/src/Clusters/ManageCluster/ManageCluster.story.tsx @@ -21,6 +21,7 @@ import { MemoryRouter } from 'react-router'; import { Route } from 'teleport/components/Router'; import { ContextProvider } from 'teleport/index'; +import { InfoGuidePanelProvider } from 'teleport/Main/InfoGuideContext'; import { ContentMinWidth } from 'teleport/Main/Main'; import { createTeleportContext } from 'teleport/mocks/contexts'; @@ -38,11 +39,13 @@ function render(fetchClusterDetails: (clusterId: string) => Promise) { return ( - - - - - + + + + + + + ); diff --git a/web/packages/teleport/src/Clusters/ManageCluster/ManageCluster.test.tsx b/web/packages/teleport/src/Clusters/ManageCluster/ManageCluster.test.tsx index 52f5a59befc96..fe531d5f97887 100644 --- a/web/packages/teleport/src/Clusters/ManageCluster/ManageCluster.test.tsx +++ b/web/packages/teleport/src/Clusters/ManageCluster/ManageCluster.test.tsx @@ -25,6 +25,7 @@ import { render, screen, waitFor } from 'design/utils/testing'; import cfg from 'teleport/config'; import { ContextProvider } from 'teleport/index'; +import { InfoGuidePanelProvider } from 'teleport/Main/InfoGuideContext'; import { ContentMinWidth } from 'teleport/Main/Main'; import { createTeleportContext } from 'teleport/mocks/contexts'; @@ -35,9 +36,11 @@ function renderElement(element, ctx) { return render( - - {element} - + + + {element} + + ); diff --git a/web/packages/teleport/src/Main/InfoGuideContext.tsx b/web/packages/teleport/src/Main/InfoGuideContext.tsx new file mode 100644 index 0000000000000..ca7d78f05bca2 --- /dev/null +++ b/web/packages/teleport/src/Main/InfoGuideContext.tsx @@ -0,0 +1,76 @@ +/** + * Teleport + * 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 + * 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 { + createContext, + PropsWithChildren, + useContext, + useEffect, + useState, +} from 'react'; + +type InfoGuidePanelContextState = { + setInfoGuideElement: (element: JSX.Element | null) => void; + infoGuideElement: JSX.Element | null; +}; + +const InfoGuidePanelContext = createContext(null); + +export const InfoGuidePanelProvider: React.FC = ({ + children, +}) => { + const [infoGuideElement, setInfoGuideElement] = useState( + null + ); + + return ( + + {children} + + ); +}; + +/** + * hook that allows you to set the info guide element that + * will render in the InfoGuideSidePanel component. + * + * To close the InfoGuideSidePanel component, set infoGuideElement + * state back to null. + */ +export const useInfoGuide = () => { + const context = useContext(InfoGuidePanelContext); + + if (!context) { + throw new Error('useInfoGuide must be used within a InfoGuidePanelContext'); + } + + const { infoGuideElement, setInfoGuideElement } = context; + + useEffect(() => { + return () => { + setInfoGuideElement(null); + }; + }, []); + + return { + setInfoGuideElement, + infoGuideElement, + }; +}; diff --git a/web/packages/teleport/src/Main/Main.test.tsx b/web/packages/teleport/src/Main/Main.test.tsx index b19970fd6ae54..e979450b6b460 100644 --- a/web/packages/teleport/src/Main/Main.test.tsx +++ b/web/packages/teleport/src/Main/Main.test.tsx @@ -18,21 +18,26 @@ import { MemoryRouter } from 'react-router'; -import { render, screen } from 'design/utils/testing'; +import { ListThin } from 'design/Icon'; +import { fireEvent, render, screen } from 'design/utils/testing'; import { Context, ContextProvider } from 'teleport'; import { apps } from 'teleport/Apps/fixtures'; import { events } from 'teleport/Audit/fixtures'; import { clusters } from 'teleport/Clusters/fixtures'; +import { InfoGuideWrapper } from 'teleport/components/SlidingSidePanel/InfoGuideSidePanel/InfoGuideSidePanel'; import { databases } from 'teleport/Databases/fixtures'; import { desktops } from 'teleport/Desktops/fixtures'; import { getOSSFeatures } from 'teleport/features'; import { kubes } from 'teleport/Kubes/fixtures'; import { userContext } from 'teleport/Main/fixtures'; import { LayoutContextProvider } from 'teleport/Main/LayoutContext'; +import { createTeleportContext } from 'teleport/mocks/contexts'; +import { NavigationCategory } from 'teleport/Navigation'; import { nodes } from 'teleport/Nodes/fixtures'; import { sessions } from 'teleport/Sessions/fixtures'; import TeleportContext from 'teleport/teleportContext'; +import { TeleportFeature } from 'teleport/types'; import { makeTestUserContext } from 'teleport/User/testHelpers/makeTestUserContext'; import { mockUserContextProviderWith } from 'teleport/User/testHelpers/mockUserContextWith'; @@ -101,6 +106,43 @@ test('renders without questionnaire prop', () => { expect(screen.getByTestId('teleport-logo')).toBeInTheDocument(); }); +test('toggle rendering of info guide panel', async () => { + mockUserContextProviderWith(makeTestUserContext()); + const ctx = createTeleportContext(); + + const props: MainProps = { + features: [...getOSSFeatures(), new FeatureTestInfoGuide()], + }; + + render( + + + +
+ + + + ); + + expect(screen.getByTestId('teleport-logo')).toBeInTheDocument(); + + expect(screen.queryByText(/i am the guide/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/info guide title/i)).not.toBeInTheDocument(); + + // render the component that has the guide info button + fireEvent.click(screen.queryAllByText('Zero Trust Access')[0]); + fireEvent.click(screen.getByText(/test info guide/i)); + expect(screen.getByText(/info guide title/i)).toBeInTheDocument(); + + // test opening of panel + fireEvent.click(screen.getByTestId('info-guide-btn-open')); + expect(screen.getByText(/i am the guide/i)).toBeInTheDocument(); + + // test closing of panel + fireEvent.click(screen.getByTestId('info-guide-btn-close')); + expect(screen.queryByText(/i am the guide/i)).not.toBeInTheDocument(); +}); + test('displays invite collaborators feedback if present', () => { mockUserContextProviderWith(makeTestUserContext()); const ctx = setupContext(); @@ -144,3 +186,36 @@ test('renders without invite collaborators feedback enabled', () => { expect(screen.getByTestId('teleport-logo')).toBeInTheDocument(); }); + +const TestInfoGuide = () => { + return ( +
+ I am the guide
}> + Info Guide Title + + + ); +}; + +class FeatureTestInfoGuide implements TeleportFeature { + category = NavigationCategory.Audit; + + route = { + title: 'Test Info Guide', + path: '/web/testinfoguide', + component: TestInfoGuide, + }; + + navigationItem = { + title: 'Test Info Guide' as any, + icon: ListThin, + getLink() { + return '/web/testinfoguide'; + }, + searchableTags: ['test info guide'], + }; + + hasAccess() { + return true; + } +} diff --git a/web/packages/teleport/src/Main/Main.tsx b/web/packages/teleport/src/Main/Main.tsx index 390c063c6d2ca..14e9ad66a2f7c 100644 --- a/web/packages/teleport/src/Main/Main.tsx +++ b/web/packages/teleport/src/Main/Main.tsx @@ -39,6 +39,8 @@ import type { BannerType } from 'teleport/components/BannerList/BannerList'; import { useAlerts } from 'teleport/components/BannerList/useAlerts'; import { CatchError } from 'teleport/components/CatchError'; import { Redirect, Route, Switch } from 'teleport/components/Router'; +import { InfoGuideSidePanel } from 'teleport/components/SlidingSidePanel'; +import { infoGuidePanelWidth } from 'teleport/components/SlidingSidePanel/InfoGuideSidePanel/InfoGuideSidePanel'; import cfg from 'teleport/config'; import { FeaturesContextProvider, useFeatures } from 'teleport/FeaturesContext'; import { Navigation } from 'teleport/Navigation'; @@ -54,6 +56,7 @@ import { useUser } from 'teleport/User/UserContext'; import useTeleport from 'teleport/useTeleport'; import { QuestionnaireProps } from 'teleport/Welcome/NewCredentials'; +import { InfoGuidePanelProvider, useInfoGuide } from './InfoGuideContext'; import { MainContainer } from './MainContainer'; import { OnboardDiscover } from './OnboardDiscover'; @@ -195,19 +198,21 @@ export function Main(props: MainProps) { - - - - - - - - + + + + + + + + + + {displayOnboardDiscover && ( @@ -316,6 +321,8 @@ export const useNoMinWidth = () => { export const ContentMinWidth = ({ children }: { children: ReactNode }) => { const [enforceMinWidth, setEnforceMinWidth] = useState(true); + const { infoGuideElement } = useInfoGuide(); + const infoGuideSidePanelOpened = infoGuideElement != null; return ( @@ -326,10 +333,18 @@ export const ContentMinWidth = ({ children }: { children: ReactNode }) => { flex: 1; ${enforceMinWidth ? 'min-width: 1000px;' : ''} min-height: 0; + margin-right: ${infoGuideSidePanelOpened + ? infoGuidePanelWidth + : '0'}px; + transition: ${infoGuideSidePanelOpened + ? 'margin 150ms' + : 'margin 300ms'}; + overflow-y: auto; `} > {children} + ); }; diff --git a/web/packages/teleport/src/Navigation/Section.tsx b/web/packages/teleport/src/Navigation/Section.tsx index 0c4b5fd15aa41..bc840889ac2c8 100644 --- a/web/packages/teleport/src/Navigation/Section.tsx +++ b/web/packages/teleport/src/Navigation/Section.tsx @@ -25,6 +25,7 @@ import { ArrowLineLeft, ArrowSquareIn } from 'design/Icon'; import { Theme } from 'design/theme'; import { HoverTooltip, IconTooltip } from 'design/Tooltip'; +import { SlidingSidePanel } from 'teleport/components/SlidingSidePanel'; import cfg from 'teleport/config'; import { CategoryIcon } from './CategoryIcon'; @@ -177,38 +178,30 @@ export function StandaloneSection({ } export const rightPanelWidth = 274; - -export const RightPanel = styled(Box).attrs({ px: '5px' })<{ - isVisible: boolean; - skipAnimation: boolean; -}>` - position: fixed; - left: var(--sidenav-width); - height: 100%; - scrollbar-color: ${p => p.theme.colors.spotBackground[2]} transparent; - width: ${rightPanelWidth}px; - background: ${p => p.theme.colors.levels.surface}; - z-index: ${zIndexMap.sideNavExpandedPanel}; - border-right: 1px solid ${p => p.theme.colors.spotBackground[1]}; - - ${props => - props.isVisible - ? ` - ${props.skipAnimation ? '' : 'transition: transform .15s ease-out;'} - transform: translateX(0); - ` - : ` - ${props.skipAnimation ? '' : 'transition: transform .15s ease-in;'} - transform: translateX(-100%); - `} - - top: ${p => p.theme.topBarHeight[0]}px; - padding-bottom: ${p => p.theme.topBarHeight[0] + p.theme.space[2]}px; - @media screen and (min-width: ${p => p.theme.breakpoints.small}px) { - top: ${p => p.theme.topBarHeight[1]}px; - padding-bottom: ${p => p.theme.topBarHeight[1] + p.theme.space[2]}px; - } -`; +export const RightPanel: React.FC< + PropsWithChildren<{ + isVisible: boolean; + skipAnimation: boolean; + id: string; + onFocus(): void; + }> +> = ({ isVisible, skipAnimation, id, onFocus, children }) => { + return ( + + {children} + + ); +}; export function RightPanelHeader({ title, diff --git a/web/packages/teleport/src/Navigation/zIndexMap.ts b/web/packages/teleport/src/Navigation/zIndexMap.ts index 745f119349d4c..aa682cc2133bc 100644 --- a/web/packages/teleport/src/Navigation/zIndexMap.ts +++ b/web/packages/teleport/src/Navigation/zIndexMap.ts @@ -17,8 +17,9 @@ */ export const zIndexMap = { - topBar: 9, - sideNavButtons: 8, - sideNavContainer: 7, - sideNavExpandedPanel: 6, + topBar: 19, + sideNavButtons: 18, + sideNavContainer: 17, + sideNavExpandedPanel: 16, + infoGuideSidePanel: 15, }; diff --git a/web/packages/teleport/src/Support/Support.story.tsx b/web/packages/teleport/src/Support/Support.story.tsx index 771be9b3905d9..8b23991279bdd 100644 --- a/web/packages/teleport/src/Support/Support.story.tsx +++ b/web/packages/teleport/src/Support/Support.story.tsx @@ -19,6 +19,7 @@ import { MemoryRouter } from 'react-router'; import { ContextProvider } from 'teleport'; +import { InfoGuidePanelProvider } from 'teleport/Main/InfoGuideContext'; import { ContentMinWidth } from 'teleport/Main/Main'; import { createTeleportContext } from 'teleport/mocks/contexts'; @@ -30,9 +31,11 @@ export default { const Provider = ({ children }) => ( - - {children} - + + + {children} + + ); diff --git a/web/packages/teleport/src/components/SlidingSidePanel/InfoGuideSidePanel/InfoGuideSidePanel.story.tsx b/web/packages/teleport/src/components/SlidingSidePanel/InfoGuideSidePanel/InfoGuideSidePanel.story.tsx new file mode 100644 index 0000000000000..13a9adce918f3 --- /dev/null +++ b/web/packages/teleport/src/components/SlidingSidePanel/InfoGuideSidePanel/InfoGuideSidePanel.story.tsx @@ -0,0 +1,44 @@ +/** + * Teleport + * 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 + * 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 { Box } from 'design'; + +import { DevInfo, LongContent, TopBar } from '../storyHelpers'; +import { + InfoGuideSidePanel as Component, + InfoGuideWrapper, +} from './InfoGuideSidePanel'; + +export default { + title: 'Teleport/SlidingSidePanel', +}; + +export const InfoGuideSidePanel = () => { + return ( + + {/* this Box wrapper is just for demo purposes */} + + + }> + Click on the info icon + + + + + ); +}; diff --git a/web/packages/teleport/src/components/SlidingSidePanel/InfoGuideSidePanel/InfoGuideSidePanel.tsx b/web/packages/teleport/src/components/SlidingSidePanel/InfoGuideSidePanel/InfoGuideSidePanel.tsx new file mode 100644 index 0000000000000..d7b2c246329d2 --- /dev/null +++ b/web/packages/teleport/src/components/SlidingSidePanel/InfoGuideSidePanel/InfoGuideSidePanel.tsx @@ -0,0 +1,158 @@ +/** + * Teleport + * 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 + * 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, { PropsWithChildren } from 'react'; +import styled from 'styled-components'; + +import { Box, Button, ButtonIcon, Flex, H3, Link, Text } from 'design'; +import { Cross, Info } from 'design/Icon'; +import { P } from 'design/Text/Text'; + +import { useInfoGuide } from 'teleport/Main/InfoGuideContext'; +import { zIndexMap } from 'teleport/Navigation/zIndexMap'; + +import { SlidingSidePanel } from '..'; + +export const infoGuidePanelWidth = 300; + +/** + * An info panel that always slides from the right and supports closing + * from inside of panel (by clicking on x button from the sticky header). + */ +export const InfoGuideSidePanel = () => { + const { infoGuideElement, setInfoGuideElement } = useInfoGuide(); + const infoGuideSidePanelOpened = infoGuideElement != null; + + return ( + + + setInfoGuideElement(null)} /> + + {infoGuideElement} + + + + ); +}; + +const InfoGuideHeader = ({ onClose }: { onClose(): void }) => ( + p.theme.colors.levels.surface}; + border-bottom: 1px solid ${p => p.theme.colors.spotBackground[1]}; + `} + > + Page Info + + + + +); + +const FilledButtonIcon = styled(Button)` + width: 32px; + height: 32px; + padding: 0; +`; + +/** + * Renders a clickable info icon next to the children. + */ +export const InfoGuideWrapper: React.FC< + PropsWithChildren<{ + guide: JSX.Element; + spaceBetween?: boolean; + }> +> = ({ guide, children, spaceBetween = false }) => { + const { setInfoGuideElement } = useInfoGuide(); + + return ( + + {children} + setInfoGuideElement(guide)} + data-testid="info-guide-btn-open" + > + + + + ); +}; + +export const InfoTitle = styled(H3)` + margin-bottom: ${p => p.theme.space[2]}px; + margin-top: ${p => p.theme.space[3]}px; +`; + +export const InfoParagraph = styled(P)` + margin-top: ${p => p.theme.space[3]}px; +`; + +/** + * Links used within a paragraph. The color of link is same as main texts + * so it doesn't take so much focus away from the paragraph. + */ +export const InfoExternalTextLink = styled(Link).attrs({ target: '_blank' })<{ + href: string; +}>` + color: ${({ theme }) => theme.colors.text.main}; +`; + +export const InfoUl = styled.ul` + margin: 0; + padding-left: ${p => p.theme.space[4]}px; +`; + +const InfoLinkLi = styled.li` + color: ${({ theme }) => theme.colors.interactive.solid.accent.default}; +`; + +export type ReferenceLink = { title: string; href: string }; + +export const ReferenceLinks = ({ links }: { links: ReferenceLink[] }) => ( + <> + Reference Links + + {links.map(link => ( + + + {link.title} + + + ))} + + +); diff --git a/web/packages/teleport/src/components/SlidingSidePanel/InfoGuideSidePanel/index.ts b/web/packages/teleport/src/components/SlidingSidePanel/InfoGuideSidePanel/index.ts new file mode 100644 index 0000000000000..3ab6e3881b092 --- /dev/null +++ b/web/packages/teleport/src/components/SlidingSidePanel/InfoGuideSidePanel/index.ts @@ -0,0 +1,19 @@ +/** + * Teleport + * 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 + * 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 * from './InfoGuideSidePanel'; diff --git a/web/packages/teleport/src/components/SlidingSidePanel/SlidingSidePanel.story.tsx b/web/packages/teleport/src/components/SlidingSidePanel/SlidingSidePanel.story.tsx new file mode 100644 index 0000000000000..01bcebe0767d2 --- /dev/null +++ b/web/packages/teleport/src/components/SlidingSidePanel/SlidingSidePanel.story.tsx @@ -0,0 +1,87 @@ +/** + * Teleport + * 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 + * 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 { Meta } from '@storybook/react'; +import { useState } from 'react'; + +import { Box, ButtonPrimary } from 'design'; +import { Info } from 'design/Alert'; + +import { SlidingSidePanel } from './SlidingSidePanel'; +import { LongContent, TopBar } from './storyHelpers'; + +type StoryProps = { + slideFrom: 'left' | 'right'; + panelOffset: string; + skipAnimation: boolean; + panelWidth: number; +}; + +const meta: Meta = { + title: 'Teleport/SlidingSidePanel', + component: Controls, + argTypes: { + slideFrom: { + control: { type: 'select' }, + options: ['left', 'right'], + }, + panelOffset: { + control: { type: 'text' }, + }, + skipAnimation: { + control: { type: 'boolean' }, + }, + panelWidth: { + control: { type: 'number' }, + }, + }, + // default + args: { + slideFrom: 'right', + panelOffset: '0', + skipAnimation: false, + panelWidth: 300, + }, +}; +export default meta; + +export function Controls(props: StoryProps) { + const [show, setShow] = useState(false); + + return ( + + {/* this Box wrapper is just for demo purposes */} + + The top bar nav is only rendered for demo purposes + setShow(!show)}>Toggle Me + + + + + + + + ); +} diff --git a/web/packages/teleport/src/components/SlidingSidePanel/SlidingSidePanel.tsx b/web/packages/teleport/src/components/SlidingSidePanel/SlidingSidePanel.tsx new file mode 100644 index 0000000000000..cb1aacd4871e0 --- /dev/null +++ b/web/packages/teleport/src/components/SlidingSidePanel/SlidingSidePanel.tsx @@ -0,0 +1,85 @@ +/** + * Teleport + * 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 + * 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 { Box } from 'design'; + +type Base = { + isVisible: boolean; + skipAnimation: boolean; + panelWidth: number; + zIndex: number; + /** + * panelOffset is how much space to offset the panel + * from left or right position. + * + * the value is a number postfixed by `px` eg: "50px" or + * css variable eg: var(--some-var-name) + */ + panelOffset?: string; +}; + +type RightPanel = Base & { + slideFrom: 'right'; +}; + +type LeftPanel = Base & { + slideFrom: 'left'; +}; + +type Props = RightPanel | LeftPanel; + +/** + * Panel that slides from right or left underneath the web UI's + * top bar navigation. + */ +export const SlidingSidePanel = styled(Box)` + position: fixed; + height: 100%; + scrollbar-color: ${p => p.theme.colors.spotBackground[2]} transparent; + width: ${p => p.panelWidth}px; + background: ${p => p.theme.colors.levels.surface}; + z-index: ${p => p.zIndex}; + + ${props => + props.slideFrom === 'left' + ? `left: ${props.panelOffset || 0}; + border-right: 1px solid ${props.theme.colors.spotBackground[1]};` + : `right: ${props.panelOffset || 0}; + border-left: 1px solid ${props.theme.colors.spotBackground[1]};`} + + ${props => + props.isVisible + ? ` + ${props.skipAnimation ? '' : 'transition: transform .15s ease-out;'} + transform: translateX(0); + ` + : ` + ${props.skipAnimation ? '' : 'transition: transform .15s ease-in;'} + transform: translateX(${props.slideFrom === 'left' ? '-100%' : '100%'}); + `} + + + top: ${p => p.theme.topBarHeight[0]}px; + padding-bottom: ${p => p.theme.topBarHeight[0] + p.theme.space[2]}px; + @media screen and (min-width: ${p => p.theme.breakpoints.small}px) { + top: ${p => p.theme.topBarHeight[1]}px; + padding-bottom: ${p => p.theme.topBarHeight[1] + p.theme.space[2]}px; + } +`; diff --git a/web/packages/teleport/src/components/SlidingSidePanel/index.ts b/web/packages/teleport/src/components/SlidingSidePanel/index.ts new file mode 100644 index 0000000000000..c7d407b0aad0a --- /dev/null +++ b/web/packages/teleport/src/components/SlidingSidePanel/index.ts @@ -0,0 +1,20 @@ +/** + * Teleport + * 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 + * 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 { SlidingSidePanel } from './SlidingSidePanel'; +export { InfoGuideSidePanel } from './InfoGuideSidePanel'; diff --git a/web/packages/teleport/src/components/SlidingSidePanel/storyHelpers.tsx b/web/packages/teleport/src/components/SlidingSidePanel/storyHelpers.tsx new file mode 100644 index 0000000000000..d2794934ba3f7 --- /dev/null +++ b/web/packages/teleport/src/components/SlidingSidePanel/storyHelpers.tsx @@ -0,0 +1,128 @@ +/** + * Teleport + * 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 + * 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 { PropsWithChildren } from 'react'; +import { MemoryRouter } from 'react-router'; + +import { Box } from 'design'; +import { Info } from 'design/Alert'; + +import { getOSSFeatures } from 'teleport/features'; +import { FeaturesContextProvider } from 'teleport/FeaturesContext'; +import { ContextProvider } from 'teleport/index'; +import { InfoGuidePanelProvider } from 'teleport/Main/InfoGuideContext'; +import { LayoutContextProvider } from 'teleport/Main/LayoutContext'; +import { createTeleportContext } from 'teleport/mocks/contexts'; +import { makeDefaultUserPreferences } from 'teleport/services/userPreferences/userPreferences'; +import { TopBar as Component } from 'teleport/TopBar'; +import { UserContext } from 'teleport/User/UserContext'; + +import { InfoParagraph, InfoTitle, ReferenceLinks } from './InfoGuideSidePanel'; + +export const TopBar: React.FC = ({ children }) => { + const ctx = createTeleportContext(); + const updatePreferences = () => Promise.resolve(); + const getClusterPinnedResources = () => Promise.resolve([]); + const updateClusterPinnedResources = () => Promise.resolve(); + const updateDiscoverResourcePreferences = () => Promise.resolve(); + const preferences = makeDefaultUserPreferences(); + + return ( + + + + + + + + + {children} + + + + + + + + ); +}; + +export const LongContent = ({ withPadding = false }) => ( + + Each title is wrapped with InfoTitle component + + Each paragraphs are wrapped with InfoParagraph component. + + InfoTitle Two + + 2 Lorem ipsum dolor sit, amet consectetur adipisicing elit. Commodi + corrupti voluptates aliquam eligendi placeat harum rerum ipsam. Corrupti + architecto laudantium, libero perspiciatis officia doloremque est aliquam, + eius qui tenetur. + + InfoTitle Three + + 3 Lorem ipsum dolor sit, amet consectetur adipisicing elit. Commodi + corrupti voluptates aliquam eligendi placeat harum rerum ipsam. Corrupti + architecto laudantium, libero perspiciatis officia doloremque est aliquam, + eius qui tenetur. + + InfoTitle Four + + 4 Lorem ipsum dolor sit, amet consectetur adipisicing elit. Commodi + corrupti voluptates aliquam eligendi placeat harum rerum ipsam. Corrupti + architecto laudantium, libero perspiciatis officia doloremque est aliquam, + eius qui tenetur. + + InfoTitle Five + + 5 Lorem ipsum dolor sit, amet consectetur adipisicing elit. Commodi + corrupti voluptates aliquam eligendi placeat harum rerum ipsam. Corrupti + architecto laudantium, libero perspiciatis officia doloremque est aliquam, + eius qui tenetur. + + + +); + +export const DevInfo = () => ( + The top bar nav is only rendered for demo purposes +);