diff --git a/web/packages/design/src/Alert/Alert.jsx b/web/packages/design/src/Alert/Alert.jsx index 8d777bf005144..bf4824c5d4474 100644 --- a/web/packages/design/src/Alert/Alert.jsx +++ b/web/packages/design/src/Alert/Alert.jsx @@ -21,6 +21,7 @@ import styled from 'styled-components'; import PropTypes from 'prop-types'; import { space, color, width } from 'design/system'; +import { fade } from 'design/theme/utils/colorManipulator'; const kind = props => { const { kind, theme } = props; @@ -45,6 +46,14 @@ const kind = props => { background: theme.colors.success.main, color: theme.colors.text.primaryInverse, }; + case 'outline-info': + return { + background: fade(theme.colors.link, 0.1), + border: `${theme.radii[1]}px solid ${theme.colors.link}`, + borderRadius: `${theme.radii[3]}px`, + boxShadow: 'none', + justifyContent: 'normal', + }; default: return { background: theme.colors.error.main, @@ -57,7 +66,7 @@ const Alert = styled.div` display: flex; align-items: center; justify-content: center; - border-radius: 2px; + border-radius: ${p => p.theme.radii[1]}px; box-sizing: border-box; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.24); margin: 0 0 24px 0; @@ -76,7 +85,13 @@ const Alert = styled.div` `; Alert.propTypes = { - kind: PropTypes.oneOf(['danger', 'info', 'warning', 'success']), + kind: PropTypes.oneOf([ + 'danger', + 'info', + 'warning', + 'success', + 'outline-info', + ]), ...color.propTypes, ...space.propTypes, ...width.propTypes, @@ -93,3 +108,4 @@ export const Danger = props => ; export const Info = props => ; export const Warning = props => ; export const Success = props => ; +export const OutlineInfo = props => ; diff --git a/web/packages/design/src/Alert/Alert.story.js b/web/packages/design/src/Alert/Alert.story.js index 9f487a0298d76..a62d8ff16e9bc 100644 --- a/web/packages/design/src/Alert/Alert.story.js +++ b/web/packages/design/src/Alert/Alert.story.js @@ -32,5 +32,6 @@ export const Alerts = () => ( Some warning message Some informational message This is success + Text align it yourself ); diff --git a/web/packages/teleport/src/Discover/Discover.tsx b/web/packages/teleport/src/Discover/Discover.tsx index 3af2e4036be00..a17ca20a8a11b 100644 --- a/web/packages/teleport/src/Discover/Discover.tsx +++ b/web/packages/teleport/src/Discover/Discover.tsx @@ -21,16 +21,16 @@ import React from 'react'; import { Prompt } from 'react-router-dom'; import { Box } from 'design'; +import { Navigation } from 'teleport/components/Wizard/Navigation'; import { FeatureBox } from 'teleport/components/Layout'; - -import { Navigation } from 'teleport/Discover/Navigation/Navigation'; import { SelectResource } from 'teleport/Discover/SelectResource/SelectResource'; import cfg from 'teleport/config'; +import { findViewAtIndex } from 'teleport/components/Wizard/flow'; import { EViewConfigs } from './types'; -import { findViewAtIndex } from './flow'; import { DiscoverProvider, useDiscover } from './useDiscover'; +import { DiscoverIcon } from './SelectResource/icons'; function DiscoverContent() { const { @@ -63,11 +63,16 @@ function DiscoverContent() { <> {hasSelectedResource && ( - + + , + }} + /> + )} {content} diff --git a/web/packages/teleport/src/Discover/Navigation/Navigation.tsx b/web/packages/teleport/src/Discover/Navigation/Navigation.tsx deleted file mode 100644 index ba7e453dac229..0000000000000 --- a/web/packages/teleport/src/Discover/Navigation/Navigation.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import React from 'react'; -import styled from 'styled-components'; - -import { Flex } from 'design'; - -import { View } from '../flow'; - -import { StepList } from './StepList'; -import { StepItem } from './StepItem'; - -import type { ResourceSpec } from '../SelectResource'; - -interface NavigationProps { - currentStep: number; - selectedResource: ResourceSpec; - views: View[]; -} - -const StyledNav = styled.div` - display: flex; -`; - -export function Navigation(props: NavigationProps) { - let content; - if (props.views) { - content = ( - - {/* - This initial StepItem is to render the first "bullet" - in this nav, which is the selected resource's icon - and name. - */} - - - - ); - } - - return {content}; -} diff --git a/web/packages/teleport/src/Discover/Navigation/StepItem.tsx b/web/packages/teleport/src/Discover/Navigation/StepItem.tsx deleted file mode 100644 index f5dcd59d13e7f..0000000000000 --- a/web/packages/teleport/src/Discover/Navigation/StepItem.tsx +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import React from 'react'; -import Flex from 'design/Flex'; - -import { DiscoverIcon } from 'teleport/Discover/SelectResource/icons'; -import { StepTitle, StepsContainer } from 'teleport/components/StepNavigation'; -import { - Bullet, - Props as BulletProps, -} from 'teleport/components/StepNavigation/Bullet'; - -import { StepList } from './StepList'; - -import type { View } from 'teleport/Discover/flow'; -import type { ResourceSpec } from '../SelectResource'; - -// FirstStepItemProps are the required -// props to render the first step item -// in the step navigation. -type FirstStepItemProps = { - view?: never; - currentStep?: never; - index?: never; - selectedResource: ResourceSpec; -}; - -// RestOfStepItemProps are the required -// props to render the rest of the step item's -// after the `FirstStepItemProps`. -type RestOfStepItemProps = { - view: View; - currentStep: number; - index: number; - selectedResource?: never; -}; - -export type StepItemProps = FirstStepItemProps | RestOfStepItemProps; - -export function StepItem(props: StepItemProps) { - if (props.selectedResource) { - return ( - - - } - /> - {props.selectedResource.name} - - - ); - } - - if (props.view.hide) { - return null; - } - - let isActive = props.currentStep === props.view.index; - // Make items for nested views. - // Nested views is possible when a view has it's - // own set of sub-steps. - if (props.view.views) { - return ( - - ); - } - - const isDone = props.currentStep > props.view.index; - - return ( - - - - {props.view.title} - - - ); -} - -function BulletIcon({ - isDone, - isActive, - Icon, - stepNumber, -}: BulletProps & { - Icon?: JSX.Element; -}) { - if (Icon) { - return {Icon}; - } - - return ; -} diff --git a/web/packages/teleport/src/Discover/flow.test.tsx b/web/packages/teleport/src/Discover/flow.test.tsx deleted file mode 100644 index d2e4aeecd81a7..0000000000000 --- a/web/packages/teleport/src/Discover/flow.test.tsx +++ /dev/null @@ -1,185 +0,0 @@ -/** - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -/* - Discover is a complicated wizard that has different steps depending on what - input has been given - - To be able to support this, we have the flow configured in an object, allowing - infinitely deep states. - - To start, you define an array of `Resource`s - - const resources: Resource[] = [ - { - kind: ResourceKind.Name, - icon: , - shouldPrompt(currentStep) { - return true; - }, - views: [], - } - ]; - - `shouldPrompt` allows for the resource type to decide when to prompt the user - if they try and navigate away. It receives `currentStep: number` which points - to the active view in the `views` array. It should return a `boolean`, where - `true` would prompt the user if they navigated away, and `false` would not. - - All the different views the resource can have go into the `views` property. - - const resources: Resource[] = [ - { - kind: ResourceKind.Name, - icon: , - shouldPrompt(currentStep) { - return true; - }, - views: [ - { - title: 'Select Resource Type', - component: SomeComponent, - }, - { - title: 'Configure Resource', - component: SomeOtherComponent, - }, - ], - } - ]; - - To add child views to a view, specify `views` again with the same schema - - const resources: Resource[] = [ - { - kind: ResourceKind.Name, - shouldPrompt(currentStep) { - return true; - }, - icon: , - views: [ - { - title: 'Select Resource Type', - component: SomeComponent, - }, - { - title: 'Configure Resource', - views: [ - { - title: 'Deploy Database Agent', - component: DatabaseAgentComponent, - }, - { - title: 'Register a Database', - component: RegisterDatabaseComponent, - }, - ], - }, - ], - } - ]; - - To keep track of what view is active, we track the currentStep index. - - Once a view has children, the first child's index is the same as the parent's index. - - This means we can just increment the `currentStep` by 1 each time to land on the next step, - regardless of how deep inside the configuration object it is. - - Take this view configuration - - - const views: View[] = [ - { - title: 'Select Resource Type', - component: SomeComponent, - }, - { - title: 'Configure Resource', - views: [ - { - title: 'Deploy Database Agent', - component: DatabaseAgentComponent, - }, - { - title: 'Register a Database', - component: RegisterDatabaseComponent, - }, - ], - }, - { - title: 'Test Connection', - component: TestConnectionComponent, - }, - ]; - - `Select Resource Type` is index 0 - `Configure Resource` is index 1 - `Deploy Database Agent` is also index 1 - - This is because when you're on step 1, you don't want to view "Configure Resource" - - there's no component for that stage, as it consists only of child views - `Register a Database` is index 2 - `Test Connection` is index 3 - - By tracking the step like this, we can increment the value from 0 and end up with - - index === 0 - show "Select Resource Type" - - index === 1 - show "Deploy Database Agent" - - index === 2 - show "Register a Database" - - index === 3 - show "Test Connection" - - The index of each stage is calculated via the `addParentAndIndexToViews` method. - */ - -import { ResourceKind } from 'teleport/Discover/Shared'; - -import { computeViewChildrenSize, ResourceViewConfig, View } from './flow'; - -describe('discover flow', () => { - describe('computeViewChildrenSize', () => { - it('should calculate the children size correctly', () => { - const resource: ResourceViewConfig = { - kind: ResourceKind.Server, - shouldPrompt: () => null, - views: [ - { - title: 'Select Resource Type', - views: [ - { - title: 'Ridiculous', - views: [ - { - title: 'Nesting', - views: [ - { - title: 'Here', - }, - { - title: 'Again', - }, - ], - }, - ], - }, - ], - }, - ], - }; - - expect(computeViewChildrenSize(resource.views as View[])).toBe(2); - }); - }); -}); diff --git a/web/packages/teleport/src/Discover/flow.tsx b/web/packages/teleport/src/Discover/flow.tsx index 6bee54f53d843..8840f59306faa 100644 --- a/web/packages/teleport/src/Discover/flow.tsx +++ b/web/packages/teleport/src/Discover/flow.tsx @@ -21,6 +21,7 @@ import React from 'react'; import { ResourceKind } from 'teleport/Discover/Shared'; import { AgentStepComponent } from 'teleport/Discover/types'; import { DiscoverEvent } from 'teleport/services/userEvent'; +import { BaseView } from 'teleport/components/Wizard/flow'; import { ResourceSpec } from './SelectResource'; @@ -52,12 +53,8 @@ export interface ResourceViewConfig { shouldPrompt?: (currentStep: number, resourceSpec: ResourceSpec) => boolean; } -export interface View { - title: string; +export type View = BaseView<{ component?: AgentStepComponent; - hide?: boolean; - index?: number; - views?: View[]; eventName?: DiscoverEvent; /** * manuallyEmitSuccessEvent is a flag that when true @@ -66,93 +63,4 @@ export interface View { * which is sent by the parent context. */ manuallyEmitSuccessEvent?: boolean; -} - -/** - * computeViewChildrenSize calculates how many children a view has, without counting the first - * child. This is because the first child shares the same index with its parent, so we don't - * need to count it as it's not taking up a new index - */ -export function computeViewChildrenSize(views: View[]) { - let size = 0; - for (const view of views) { - if (view.views) { - size += computeViewChildrenSize(view.views); - } else { - size += 1; - } - } - - return size; -} - -/** - * addIndexToViews will recursively loop over the given views, adding an index value to each one - * The first child shares its index with the parent, as we effectively ignore the fact the parent - * exists when trying to find the active view by the current step index. - */ -export function addIndexToViews(views: View[], index = 0): View[] { - const result: View[] = []; - - for (const view of views) { - const copy = { - ...view, - index, - parent, - }; - - if (view.views) { - copy.views = addIndexToViews(view.views, index); - - index += computeViewChildrenSize(view.views); - } else { - index += 1; - } - - result.push(copy); - } - - return result; -} - -/** - * findViewAtIndex will recursively loop views and their children in order to find the deepest - * match at that index. - */ -export function findViewAtIndex( - views: View[], - currentStep: number -): View | null { - for (const view of views) { - if (view.views) { - const result = findViewAtIndex(view.views, currentStep); - - if (result) { - return result; - } - } - - if (currentStep === view.index) { - return view; - } - } -} - -/** - * hasActiveChildren will recursively loop through views and their children in order to find - * out if there is a view with a matching index to the given `currentStep` value - * This is because a parent is active as long as its children are active - */ -export function hasActiveChildren(views: View[], currentStep: number) { - for (const view of views) { - if (view.index === currentStep) { - return true; - } - - if (view.views && hasActiveChildren(view.views, currentStep)) { - return true; - } - } - - return false; -} +}>; diff --git a/web/packages/teleport/src/Discover/useDiscover.tsx b/web/packages/teleport/src/Discover/useDiscover.tsx index 8916a62a51959..fcc0a72ee5b92 100644 --- a/web/packages/teleport/src/Discover/useDiscover.tsx +++ b/web/packages/teleport/src/Discover/useDiscover.tsx @@ -31,13 +31,12 @@ import { } from 'teleport/services/userEvent'; import cfg from 'teleport/config'; import { DiscoveryConfig } from 'teleport/services/discovery'; - import { addIndexToViews, findViewAtIndex, - ResourceViewConfig, - View, -} from './flow'; +} from 'teleport/components/Wizard/flow'; + +import { ResourceViewConfig, View } from './flow'; import { viewConfigs } from './resourceViewConfigs'; import { EViewConfigs } from './types'; import { ServiceDeployMethod } from './Database/common'; @@ -295,7 +294,7 @@ export function DiscoverProvider({ const currCfg = [...viewConfigs, ...eViewConfigs].find( r => r.kind === resource.kind ); - let indexedViews = []; + let indexedViews: View[] = []; if (typeof currCfg.views === 'function') { indexedViews = addIndexToViews(currCfg.views(resource)); } else { diff --git a/web/packages/teleport/src/components/StepNavigation/StepNavigation.story.tsx b/web/packages/teleport/src/components/StepNavigation/StepNavigation.story.tsx deleted file mode 100644 index a832cca1dbbcf..0000000000000 --- a/web/packages/teleport/src/components/StepNavigation/StepNavigation.story.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import React from 'react'; -import { Box } from 'design'; - -import { StepNavigation } from './StepNavigation'; - -export default { - title: 'Teleport/StepNavigation', -}; - -const steps = [ - { title: 'first title' }, - { title: 'second title' }, - { title: 'third title' }, - { title: 'fourth title' }, - { title: 'fifth title' }, - { title: 'sixth title' }, - { title: 'seventh title' }, - { title: 'eighth title' }, -]; - -export const Examples = () => { - return ( - <> - - - - - - - - - - - - - - ); -}; diff --git a/web/packages/teleport/src/components/StepNavigation/StepNavigation.tsx b/web/packages/teleport/src/components/StepNavigation/StepNavigation.tsx deleted file mode 100644 index c2f4fb6246878..0000000000000 --- a/web/packages/teleport/src/components/StepNavigation/StepNavigation.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Teleport - * Copyright (C) 2023 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import React from 'react'; - -import { Flex } from 'design'; - -import { StepTitle, StepsContainer } from './Shared'; -import { Bullet } from './Bullet'; - -export type StepItem = { - title: string; -}; - -interface NavigationProps { - currentStep: number; - steps: StepItem[]; -} - -export function StepNavigation({ currentStep, steps }: NavigationProps) { - const items: JSX.Element[] = []; - - steps.forEach((step, index) => { - const isDone = currentStep > index; - let isActive = currentStep === index; - - items.push( - - - - {step.title} - - - ); - }); - - return {items}; -} diff --git a/web/packages/teleport/src/components/StepNavigation/Bullet.tsx b/web/packages/teleport/src/components/Wizard/Navigation/Bullet.tsx similarity index 91% rename from web/packages/teleport/src/components/StepNavigation/Bullet.tsx rename to web/packages/teleport/src/components/Wizard/Navigation/Bullet.tsx index 10b980ed7b147..31f2166456b67 100644 --- a/web/packages/teleport/src/components/StepNavigation/Bullet.tsx +++ b/web/packages/teleport/src/components/Wizard/Navigation/Bullet.tsx @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import Flex from 'design/Flex'; import React from 'react'; import styled from 'styled-components'; @@ -23,9 +24,14 @@ export type Props = { isDone?: boolean; isActive?: boolean; stepNumber?: number; + Icon?: JSX.Element; }; -export function Bullet({ isDone, isActive, stepNumber }: Props) { +export function Bullet({ isDone, isActive, stepNumber, Icon }: Props) { + if (Icon) { + return {Icon}; + } + if (isActive) { return ; } diff --git a/web/packages/teleport/src/components/Wizard/Navigation/Navigation.story.tsx b/web/packages/teleport/src/components/Wizard/Navigation/Navigation.story.tsx new file mode 100644 index 0000000000000..be358abfa6dc8 --- /dev/null +++ b/web/packages/teleport/src/components/Wizard/Navigation/Navigation.story.tsx @@ -0,0 +1,112 @@ +/** + * 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 { Box } from 'design'; + +import { addIndexToViews } from '../flow'; + +import { Navigation } from './Navigation'; + +export default { + title: 'Teleport/StepNavigation', +}; + +const steps = [ + { title: 'first title' }, + { title: 'second title' }, + { title: 'third title' }, + { title: 'fourth title' }, + { title: 'fifth title' }, + { title: 'sixth title' }, + { title: 'seventh title' }, + { title: 'eighth title' }, +]; + +export const WithoutNesting = () => { + return ( + <> + + + + + + + + + + + + + + ); +}; + +export const WithNesting = () => { + const nestedViews = [ + { + title: 'First Step', + }, + { + title: 'Nesting', + views: [ + { + title: 'Nesting Again', + views: [ + { + title: 'Second Step', + }, + { + title: 'Third Step', + }, + ], + }, + { + title: 'Fourth Step', + }, + ], + }, + { + title: 'Fifth Step', + }, + ]; + + const indexedViews = addIndexToViews(nestedViews); + return ( + <> + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/web/packages/teleport/src/components/StepNavigation/StepNavigation.test.tsx b/web/packages/teleport/src/components/Wizard/Navigation/Navigation.test.tsx similarity index 92% rename from web/packages/teleport/src/components/StepNavigation/StepNavigation.test.tsx rename to web/packages/teleport/src/components/Wizard/Navigation/Navigation.test.tsx index 9c1f11c6f8f1c..cc9c307e041c8 100644 --- a/web/packages/teleport/src/components/StepNavigation/StepNavigation.test.tsx +++ b/web/packages/teleport/src/components/Wizard/Navigation/Navigation.test.tsx @@ -19,12 +19,12 @@ import React from 'react'; import { render, screen } from 'design/utils/testing'; -import { StepNavigation } from './StepNavigation'; +import { Navigation } from './Navigation'; const steps = [{ title: 'first' }, { title: 'second' }, { title: 'third' }]; test('step 1/3', async () => { - render(); + render(); const firstBullet = screen.getByTestId('bullet-active'); expect(firstBullet).toHaveTextContent(''); @@ -43,7 +43,7 @@ test('step 1/3', async () => { }); test('step 2/3', async () => { - render(); + render(); const firstBullet = screen.getByTestId('bullet-checked'); expect(firstBullet).toHaveTextContent(''); @@ -59,7 +59,7 @@ test('step 2/3', async () => { }); test('step 3/3', async () => { - render(); + render(); const checkedBullets = screen.getAllByTestId('bullet-checked'); expect(checkedBullets).toHaveLength(2); diff --git a/web/packages/teleport/src/components/Wizard/Navigation/Navigation.tsx b/web/packages/teleport/src/components/Wizard/Navigation/Navigation.tsx new file mode 100644 index 0000000000000..01fad8c7aec9c --- /dev/null +++ b/web/packages/teleport/src/components/Wizard/Navigation/Navigation.tsx @@ -0,0 +1,183 @@ +/** + * 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 { Flex } from 'design'; + +import { BaseView } from '../flow'; + +import { StepTitle, StepsContainer } from './Shared'; +import { Bullet } from './Bullet'; +import { StepList } from './StepList'; + +export type StepIcon = { + component: JSX.Element; + title: string; +}; + +interface NavigationProps { + currentStep: number; + views: BaseView[]; + startWithIcon?: StepIcon; +} + +/** + * Renders horizontal steps for each view. + * + * @param views can be simple (non-nested) or nested for + * more complex configurations (see below for an example). + * + * For nested views, it is required to apply + * function `addIndexToViews(views: BaseView[])` + * before passing the views to Navigation so it can correctly + * increment the steps. + * + * For simple views, defining indexes is not required. + * + * + * @example + * How nesting views are used for Discover wizards: + * + * Discover is a complicated wizard that has different steps depending on what + * input has been given. + * + * To be able to support this, we have the flow configured in an object, allowing + * infinitely deep states. + * + * All the different views the resource can have go into the `views` property eg: + * + * const resources: Resource[] = [ + * { + * kind: ResourceKind.Name, + * icon: , + * shouldPrompt(currentStep) { + * return true; + * }, + * views: [ + * { + * title: 'Select Resource Type', + * component: SomeComponent, + * }, + * { + * title: 'Configure Resource', + * component: SomeOtherComponent, + * }, + * ], + * } + * ]; + * + * To add child views to a view, specify `views` again with the same schema + * + * const resources: Resource[] = [ + * { + * kind: ResourceKind.Name, + * shouldPrompt(currentStep) { + * return true; + * }, + * icon: , + * views: [ + * { + * title: 'Select Resource Type', + * component: SomeComponent, + * }, + * { + * title: 'Configure Resource', + * views: [ + * { + * title: 'Deploy Database Agent', + * component: DatabaseAgentComponent, + * }, + * { + * title: 'Register a Database', + * component: RegisterDatabaseComponent, + * }, + * ], + * }, + * ], + * } + * ]; + * + * To keep track of what view is active, we track the currentStep index. + * + * Once a view has children, the first child's index is the same as the parent's index. + * + * This means we can just increment the `currentStep` by 1 each time to land on the next step, + * regardless of how deep inside the configuration object it is. + * + * Take this view configuration - + * + * const views: View[] = [ + * { + * title: 'Select Resource Type', + * component: SomeComponent, + * }, + * { + * title: 'Configure Resource', + * views: [ + * { + * title: 'Deploy Database Agent', + * component: DatabaseAgentComponent, + * }, + * { + * title: 'Register a Database', + * component: RegisterDatabaseComponent, + * }, + * ], + * }, + * { + * title: 'Test Connection', + * component: TestConnectionComponent, + * }, + * ]; + * + * `Select Resource Type` is index 0 + * `Configure Resource` is index 1 + * `Deploy Database Agent` is also index 1 + * - This is because when you're on step 1, you don't want to view "Configure Resource" - + * there's no component for that stage, as it consists only of child views + * `Register a Database` is index 2 + * `Test Connection` is index 3 + * + * By tracking the step like this, we can increment the value from 0 and end up with + * - index === 0 - show "Select Resource Type" + * - index === 1 - show "Deploy Database Agent" + * - index === 2 - show "Register a Database" + * - index === 3 - show "Test Connection" + * + * The index of each stage is calculated via the `addParentAndIndexToViews` method. + */ +export function Navigation({ + currentStep, + views, + startWithIcon, +}: NavigationProps) { + return ( + + {startWithIcon && ( + + + + {startWithIcon.title} + + + )} + + + ); +} diff --git a/web/packages/teleport/src/components/StepNavigation/Shared.tsx b/web/packages/teleport/src/components/Wizard/Navigation/Shared.tsx similarity index 100% rename from web/packages/teleport/src/components/StepNavigation/Shared.tsx rename to web/packages/teleport/src/components/Wizard/Navigation/Shared.tsx diff --git a/web/packages/teleport/src/components/Wizard/Navigation/StepItem.tsx b/web/packages/teleport/src/components/Wizard/Navigation/StepItem.tsx new file mode 100644 index 0000000000000..4236d85471209 --- /dev/null +++ b/web/packages/teleport/src/components/Wizard/Navigation/StepItem.tsx @@ -0,0 +1,65 @@ +/** + * 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 { + StepTitle, + StepsContainer, +} from 'teleport/components/Wizard/Navigation'; +import { Bullet } from 'teleport/components/Wizard/Navigation/Bullet'; + +import { BaseView } from '../flow'; + +import { StepList } from './StepList'; + +export function StepItem(props: { + view: BaseView; + currentStep: number; + index: number; +}) { + if (props.view.hide) { + return null; + } + + // Make items for nested views. + // Nested views is possible when a view has it's + // own set of sub-steps. + if (props.view.views) { + return ( + + ); + } + + const index = props.view.index ?? props.index; + const isActive = props.currentStep === index; + const isDone = props.currentStep > index; + + return ( + + + + {props.view.title} + + + ); +} diff --git a/web/packages/teleport/src/Discover/Navigation/StepList.tsx b/web/packages/teleport/src/components/Wizard/Navigation/StepList.tsx similarity index 87% rename from web/packages/teleport/src/Discover/Navigation/StepList.tsx rename to web/packages/teleport/src/components/Wizard/Navigation/StepList.tsx index 7807949348f22..e4dec58e714d8 100644 --- a/web/packages/teleport/src/Discover/Navigation/StepList.tsx +++ b/web/packages/teleport/src/components/Wizard/Navigation/StepList.tsx @@ -18,23 +18,23 @@ import React from 'react'; -import { StepItem } from './StepItem'; +import { BaseView } from '../flow'; -import type { View } from 'teleport/Discover/flow'; +import { StepItem } from './StepItem'; -interface StepListProps { - views: View[]; +interface StepListProps { + views: BaseView[]; currentStep: number; index?: number; } -export function StepList(props: StepListProps) { +export function StepList(props: StepListProps) { const items = []; let startIndex = props.index || 0; for (const view of props.views) { items.push( - key={startIndex} view={view} currentStep={props.currentStep} diff --git a/web/packages/teleport/src/components/StepNavigation/index.ts b/web/packages/teleport/src/components/Wizard/Navigation/index.ts similarity index 94% rename from web/packages/teleport/src/components/StepNavigation/index.ts rename to web/packages/teleport/src/components/Wizard/Navigation/index.ts index 9dbfafc16308b..8591a53352d25 100644 --- a/web/packages/teleport/src/components/StepNavigation/index.ts +++ b/web/packages/teleport/src/components/Wizard/Navigation/index.ts @@ -16,6 +16,6 @@ * along with this program. If not, see . */ -export { StepNavigation } from './StepNavigation'; +export { Navigation } from './Navigation'; export { StepTitle, StepsContainer } from './Shared'; export { Bullet } from './Bullet'; diff --git a/web/packages/teleport/src/components/Wizard/flow.test.tsx b/web/packages/teleport/src/components/Wizard/flow.test.tsx new file mode 100644 index 0000000000000..b89b884007d71 --- /dev/null +++ b/web/packages/teleport/src/components/Wizard/flow.test.tsx @@ -0,0 +1,114 @@ +/** + * 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 { render, screen } from 'design/utils/testing'; + +import { Navigation } from './Navigation'; +import { addIndexToViews, computeViewChildrenSize } from './flow'; + +test('computeViewChildrenSize', async () => { + const nestedViews = [ + { + title: 'Ridiculous', + views: [ + { + title: 'Nesting', + views: [ + { + title: 'Here', + }, + { + title: 'Again', + }, + ], + }, + ], + }, + { + title: 'Banana', + }, + ]; + expect(computeViewChildrenSize(nestedViews)).toBe(3); + + const notNestedViews = [ + { + title: 'Apple', + }, + { + title: 'Banana', + }, + ]; + expect(computeViewChildrenSize(notNestedViews)).toBe(2); +}); + +test('addIndexToViews and rendering correct steps', async () => { + const nestedViews = [ + { + title: 'First Step', + }, + { + title: 'Nesting', + views: [ + { + title: 'Nesting Again', + views: [ + { + title: 'Second Step', + }, + { + title: 'Third Step', + }, + ], + }, + { + title: 'Fourth Step', + }, + ], + }, + { + title: 'Fifth Step', + }, + ]; + + const indexedViews = addIndexToViews(nestedViews); + + // Should render 5 bullets. + render(); + + // First bullets always active. + const firstBullet = screen.getByTestId('bullet-active'); + expect(firstBullet).toHaveTextContent(''); + expect(firstBullet.parentElement).toHaveTextContent(/first step/i); + + // Rest should be not active. + const uncheckedBullets = screen.getAllByTestId('bullet-default'); + expect(uncheckedBullets).toHaveLength(4); + + expect(uncheckedBullets[0]).toHaveTextContent(/2/i); + expect(uncheckedBullets[0].parentElement).toHaveTextContent(/second/i); + + expect(uncheckedBullets[1]).toHaveTextContent(/3/i); + expect(uncheckedBullets[1].parentElement).toHaveTextContent(/third/i); + + expect(uncheckedBullets[2]).toHaveTextContent(/4/i); + expect(uncheckedBullets[2].parentElement).toHaveTextContent(/fourth/i); + + expect(uncheckedBullets[3]).toHaveTextContent(/5/i); + expect(uncheckedBullets[3].parentElement).toHaveTextContent(/fifth/i); +}); diff --git a/web/packages/teleport/src/components/Wizard/flow.tsx b/web/packages/teleport/src/components/Wizard/flow.tsx new file mode 100644 index 0000000000000..c00f4d16ba7d4 --- /dev/null +++ b/web/packages/teleport/src/components/Wizard/flow.tsx @@ -0,0 +1,97 @@ +/** + * 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 type BaseView = T & { + hide?: boolean; + index?: number; + views?: BaseView[]; + title: string; +}; + +/** + * computeViewChildrenSize calculates how many children a view has, without counting the first + * child. This is because the first child shares the same index with its parent, so we don't + * need to count it as it's not taking up a new index + */ +export function computeViewChildrenSize(views: BaseView[]) { + let size = 0; + for (const view of views) { + if (view.views) { + size += computeViewChildrenSize(view.views); + } else { + size += 1; + } + } + + return size; +} + +/** + * addIndexToViews will recursively loop over the given views, adding an index value to each one + * The first child shares its index with the parent, as we effectively ignore the fact the parent + * exists when trying to find the active view by the current step index. + */ +export function addIndexToViews( + views: BaseView[], + index = 0 +): BaseView[] { + const result: BaseView[] = []; + + for (const view of views) { + const copy = { + ...view, + index, + parent, + }; + + if (view.views) { + copy.views = addIndexToViews(view.views, index); + + index += computeViewChildrenSize(view.views); + } else { + index += 1; + } + + result.push(copy); + } + + return result; +} + +/** + * findViewAtIndex will recursively loop views and their children in order to find the deepest + * match at that index. + */ +export function findViewAtIndex( + views: BaseView[], + currentStep: number +): BaseView | undefined { + for (const view of views) { + if (view.views) { + const result = findViewAtIndex(view.views, currentStep); + + if (result) { + return result; + } + } + + if (currentStep === view.index) { + return view; + } + } +} diff --git a/web/packages/teleport/src/services/integrations/types.ts b/web/packages/teleport/src/services/integrations/types.ts index 8094c0072e3d1..fe8681fb5ef7e 100644 --- a/web/packages/teleport/src/services/integrations/types.ts +++ b/web/packages/teleport/src/services/integrations/types.ts @@ -115,8 +115,8 @@ export type ExternalAuditStorageIntegration = Integration< ExternalAuditStorage >; -export type Plugin = Integration<'plugin', PluginKind, PluginSpec>; -export type PluginSpec = Record; // currently no 'spec' fields exposed to the frontend +export type Plugin = Integration<'plugin', PluginKind, T>; +export type PluginSpec = PluginOktaSpec | any; // currently only okta has a plugin spec // PluginKind represents the type of the plugin // and should be the same value as defined in the backend (check master branch for the latest): // https://github.com/gravitational/teleport/blob/a410acef01e0023d41c18ca6b0a7b384d738bb32/api/types/plugin.go#L27 @@ -134,6 +134,25 @@ export type PluginKind = | 'servicenow' | 'jamf'; +export type PluginOktaSpec = { + // scimBearerToken is the plain text of the bearer token that Okta will use + // to authenticate SCIM requests + scimBearerToken: string; + // oktaAppID is the Okta ID of the SAML App created during the Okta plugin + // installation + oktaAppId: string; + // oktaAppName is the human readable name of the Okta SAML app created + // during the Okta plugin installation + oktaAppName: string; + // teleportSSOConnector is the name of the Teleport SAML SSO connector + // created by the plugin during installation + teleportSsoConnector: string; + // error contains a description of any failures during plugin installation + // that were deemed not serious enough to fail the plugin installation, but + // may effect the operation of advanced features like User Sync or SCIM. + error: string; +}; + export type IntegrationCreateRequest = { name: string; subKind: IntegrationKind;