diff --git a/web/packages/design/src/Alert/Alert.jsx b/web/packages/design/src/Alert/Alert.jsx index bf4824c5d4474..8d777bf005144 100644 --- a/web/packages/design/src/Alert/Alert.jsx +++ b/web/packages/design/src/Alert/Alert.jsx @@ -21,7 +21,6 @@ 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; @@ -46,14 +45,6 @@ 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, @@ -66,7 +57,7 @@ const Alert = styled.div` display: flex; align-items: center; justify-content: center; - border-radius: ${p => p.theme.radii[1]}px; + border-radius: 2px; box-sizing: border-box; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.24); margin: 0 0 24px 0; @@ -85,13 +76,7 @@ const Alert = styled.div` `; Alert.propTypes = { - kind: PropTypes.oneOf([ - 'danger', - 'info', - 'warning', - 'success', - 'outline-info', - ]), + kind: PropTypes.oneOf(['danger', 'info', 'warning', 'success']), ...color.propTypes, ...space.propTypes, ...width.propTypes, @@ -108,4 +93,3 @@ 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 a62d8ff16e9bc..9f487a0298d76 100644 --- a/web/packages/design/src/Alert/Alert.story.js +++ b/web/packages/design/src/Alert/Alert.story.js @@ -32,6 +32,5 @@ 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 a17ca20a8a11b..3af2e4036be00 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,16 +63,11 @@ function DiscoverContent() { <> {hasSelectedResource && ( - - , - }} - /> - + )} {content} diff --git a/web/packages/teleport/src/Discover/Navigation/Navigation.tsx b/web/packages/teleport/src/Discover/Navigation/Navigation.tsx new file mode 100644 index 0000000000000..ba7e453dac229 --- /dev/null +++ b/web/packages/teleport/src/Discover/Navigation/Navigation.tsx @@ -0,0 +1,58 @@ +/** + * 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 new file mode 100644 index 0000000000000..f5dcd59d13e7f --- /dev/null +++ b/web/packages/teleport/src/Discover/Navigation/StepItem.tsx @@ -0,0 +1,117 @@ +/** + * 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/components/Wizard/Navigation/StepList.tsx b/web/packages/teleport/src/Discover/Navigation/StepList.tsx similarity index 87% rename from web/packages/teleport/src/components/Wizard/Navigation/StepList.tsx rename to web/packages/teleport/src/Discover/Navigation/StepList.tsx index e4dec58e714d8..7807949348f22 100644 --- a/web/packages/teleport/src/components/Wizard/Navigation/StepList.tsx +++ b/web/packages/teleport/src/Discover/Navigation/StepList.tsx @@ -18,23 +18,23 @@ import React from 'react'; -import { BaseView } from '../flow'; - import { StepItem } from './StepItem'; -interface StepListProps { - views: BaseView[]; +import type { View } from 'teleport/Discover/flow'; + +interface StepListProps { + views: View[]; 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( - + . + */ + +/* + 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 8840f59306faa..6bee54f53d843 100644 --- a/web/packages/teleport/src/Discover/flow.tsx +++ b/web/packages/teleport/src/Discover/flow.tsx @@ -21,7 +21,6 @@ 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'; @@ -53,8 +52,12 @@ export interface ResourceViewConfig { shouldPrompt?: (currentStep: number, resourceSpec: ResourceSpec) => boolean; } -export type View = BaseView<{ +export interface View { + title: string; component?: AgentStepComponent; + hide?: boolean; + index?: number; + views?: View[]; eventName?: DiscoverEvent; /** * manuallyEmitSuccessEvent is a flag that when true @@ -63,4 +66,93 @@ export type View = BaseView<{ * 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 fcc0a72ee5b92..8916a62a51959 100644 --- a/web/packages/teleport/src/Discover/useDiscover.tsx +++ b/web/packages/teleport/src/Discover/useDiscover.tsx @@ -31,12 +31,13 @@ import { } from 'teleport/services/userEvent'; import cfg from 'teleport/config'; import { DiscoveryConfig } from 'teleport/services/discovery'; + import { addIndexToViews, findViewAtIndex, -} from 'teleport/components/Wizard/flow'; - -import { ResourceViewConfig, View } from './flow'; + ResourceViewConfig, + View, +} from './flow'; import { viewConfigs } from './resourceViewConfigs'; import { EViewConfigs } from './types'; import { ServiceDeployMethod } from './Database/common'; @@ -294,7 +295,7 @@ export function DiscoverProvider({ const currCfg = [...viewConfigs, ...eViewConfigs].find( r => r.kind === resource.kind ); - let indexedViews: View[] = []; + let indexedViews = []; if (typeof currCfg.views === 'function') { indexedViews = addIndexToViews(currCfg.views(resource)); } else { diff --git a/web/packages/teleport/src/components/Wizard/Navigation/Bullet.tsx b/web/packages/teleport/src/components/StepNavigation/Bullet.tsx similarity index 91% rename from web/packages/teleport/src/components/Wizard/Navigation/Bullet.tsx rename to web/packages/teleport/src/components/StepNavigation/Bullet.tsx index 31f2166456b67..10b980ed7b147 100644 --- a/web/packages/teleport/src/components/Wizard/Navigation/Bullet.tsx +++ b/web/packages/teleport/src/components/StepNavigation/Bullet.tsx @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import Flex from 'design/Flex'; import React from 'react'; import styled from 'styled-components'; @@ -24,14 +23,9 @@ export type Props = { isDone?: boolean; isActive?: boolean; stepNumber?: number; - Icon?: JSX.Element; }; -export function Bullet({ isDone, isActive, stepNumber, Icon }: Props) { - if (Icon) { - return {Icon}; - } - +export function Bullet({ isDone, isActive, stepNumber }: Props) { if (isActive) { return ; } diff --git a/web/packages/teleport/src/components/Wizard/Navigation/Shared.tsx b/web/packages/teleport/src/components/StepNavigation/Shared.tsx similarity index 100% rename from web/packages/teleport/src/components/Wizard/Navigation/Shared.tsx rename to web/packages/teleport/src/components/StepNavigation/Shared.tsx diff --git a/web/packages/teleport/src/components/StepNavigation/StepNavigation.story.tsx b/web/packages/teleport/src/components/StepNavigation/StepNavigation.story.tsx new file mode 100644 index 0000000000000..a832cca1dbbcf --- /dev/null +++ b/web/packages/teleport/src/components/StepNavigation/StepNavigation.story.tsx @@ -0,0 +1,56 @@ +/** + * 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/Wizard/Navigation/Navigation.test.tsx b/web/packages/teleport/src/components/StepNavigation/StepNavigation.test.tsx similarity index 92% rename from web/packages/teleport/src/components/Wizard/Navigation/Navigation.test.tsx rename to web/packages/teleport/src/components/StepNavigation/StepNavigation.test.tsx index cc9c307e041c8..9c1f11c6f8f1c 100644 --- a/web/packages/teleport/src/components/Wizard/Navigation/Navigation.test.tsx +++ b/web/packages/teleport/src/components/StepNavigation/StepNavigation.test.tsx @@ -19,12 +19,12 @@ import React from 'react'; import { render, screen } from 'design/utils/testing'; -import { Navigation } from './Navigation'; +import { StepNavigation } from './StepNavigation'; 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/StepNavigation/StepNavigation.tsx b/web/packages/teleport/src/components/StepNavigation/StepNavigation.tsx new file mode 100644 index 0000000000000..c2f4fb6246878 --- /dev/null +++ b/web/packages/teleport/src/components/StepNavigation/StepNavigation.tsx @@ -0,0 +1,53 @@ +/** + * 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/Wizard/Navigation/index.ts b/web/packages/teleport/src/components/StepNavigation/index.ts similarity index 94% rename from web/packages/teleport/src/components/Wizard/Navigation/index.ts rename to web/packages/teleport/src/components/StepNavigation/index.ts index 8591a53352d25..9dbfafc16308b 100644 --- a/web/packages/teleport/src/components/Wizard/Navigation/index.ts +++ b/web/packages/teleport/src/components/StepNavigation/index.ts @@ -16,6 +16,6 @@ * along with this program. If not, see . */ -export { Navigation } from './Navigation'; +export { StepNavigation } from './StepNavigation'; export { StepTitle, StepsContainer } from './Shared'; export { Bullet } from './Bullet'; diff --git a/web/packages/teleport/src/components/Wizard/Navigation/Navigation.story.tsx b/web/packages/teleport/src/components/Wizard/Navigation/Navigation.story.tsx deleted file mode 100644 index be358abfa6dc8..0000000000000 --- a/web/packages/teleport/src/components/Wizard/Navigation/Navigation.story.tsx +++ /dev/null @@ -1,112 +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 { 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/Wizard/Navigation/Navigation.tsx b/web/packages/teleport/src/components/Wizard/Navigation/Navigation.tsx deleted file mode 100644 index 01fad8c7aec9c..0000000000000 --- a/web/packages/teleport/src/components/Wizard/Navigation/Navigation.tsx +++ /dev/null @@ -1,183 +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 { 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/Wizard/Navigation/StepItem.tsx b/web/packages/teleport/src/components/Wizard/Navigation/StepItem.tsx deleted file mode 100644 index 4236d85471209..0000000000000 --- a/web/packages/teleport/src/components/Wizard/Navigation/StepItem.tsx +++ /dev/null @@ -1,65 +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 { - 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/components/Wizard/flow.test.tsx b/web/packages/teleport/src/components/Wizard/flow.test.tsx deleted file mode 100644 index b89b884007d71..0000000000000 --- a/web/packages/teleport/src/components/Wizard/flow.test.tsx +++ /dev/null @@ -1,114 +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 { 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 deleted file mode 100644 index c00f4d16ba7d4..0000000000000 --- a/web/packages/teleport/src/components/Wizard/flow.tsx +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Teleport - * Copyright (C) 2024 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -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 fe8681fb5ef7e..8094c0072e3d1 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, T>; -export type PluginSpec = PluginOktaSpec | any; // currently only okta has a plugin spec +export type Plugin = Integration<'plugin', PluginKind, PluginSpec>; +export type PluginSpec = Record; // currently no 'spec' fields exposed to the frontend // 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,25 +134,6 @@ 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;