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;