From f798c07fdecb39be11c00e0bce15c7645b76a009 Mon Sep 17 00:00:00 2001
From: Bartosz Leper
Date: Mon, 18 Nov 2024 20:13:25 +0100
Subject: [PATCH 01/14] Validation
Adds a model-level validation capability to our validation library.
---
.../FieldMultiInput/FieldMultiInput.story.tsx | 22 +-
.../FieldMultiInput/FieldMultiInput.test.tsx | 54 ++++-
.../FieldMultiInput/FieldMultiInput.tsx | 36 +++-
.../FieldSelect/FieldSelect.story.tsx | 5 +-
.../components/Validation/Validation.jsx | 105 ----------
.../components/Validation/Validation.test.tsx | 101 +++++++---
.../components/Validation/Validation.tsx | 190 ++++++++++++++++++
.../components/Validation/rules.test.ts | 59 ++++++
.../shared/components/Validation/rules.ts | 81 ++++++++
.../shared/components/Validation/useRule.js | 8 +-
.../LabelsInput/LabelsInput.test.tsx | 119 +++++++++++
.../components/LabelsInput/LabelsInput.tsx | 71 ++++++-
12 files changed, 697 insertions(+), 154 deletions(-)
delete mode 100644 web/packages/shared/components/Validation/Validation.jsx
create mode 100644 web/packages/shared/components/Validation/Validation.tsx
diff --git a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.story.tsx b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.story.tsx
index 5362236a8b24d..2f798d4d923d1 100644
--- a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.story.tsx
+++ b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.story.tsx
@@ -20,6 +20,12 @@ import React, { useState } from 'react';
import Box from 'design/Box';
+import { Button } from 'design/Button';
+
+import Validation from 'shared/components/Validation';
+
+import { arrayOf, requiredField } from '../Validation/rules';
+
import { FieldMultiInput } from './FieldMultiInput';
export default {
@@ -30,7 +36,21 @@ export function Story() {
const [items, setItems] = useState([]);
return (
-
+
+ {({ validator }) => (
+ <>
+
+ validator.validate()}>
+ Validate
+
+ >
+ )}
+
);
}
diff --git a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.test.tsx b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.test.tsx
index ce023a071053a..89b191e1e5b2d 100644
--- a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.test.tsx
+++ b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.test.tsx
@@ -19,20 +19,36 @@
import userEvent from '@testing-library/user-event';
import React, { useState } from 'react';
-import { render, screen } from 'design/utils/testing';
+import { act, render, screen } from 'design/utils/testing';
+
+import Validation, { Validator } from 'shared/components/Validation';
+
+import { arrayOf, requiredField } from '../Validation/rules';
import { FieldMultiInput, FieldMultiInputProps } from './FieldMultiInput';
const TestFieldMultiInput = ({
onChange,
+ refValidator,
...rest
-}: Partial) => {
+}: Partial & {
+ refValidator?: (v: Validator) => void;
+}) => {
const [items, setItems] = useState([]);
const handleChange = (it: string[]) => {
setItems(it);
onChange?.(it);
};
- return ;
+ return (
+
+ {({ validator }) => {
+ refValidator?.(validator);
+ return (
+
+ );
+ }}
+
+ );
};
test('adding, editing, and removing items', async () => {
@@ -69,3 +85,35 @@ test('keyboard handling', async () => {
await user.keyboard('{Enter}bananas');
expect(onChange).toHaveBeenLastCalledWith(['apples', 'bananas', 'oranges']);
});
+
+test('validation', async () => {
+ const user = userEvent.setup();
+ let validator: Validator;
+ render(
+ {
+ validator = v;
+ }}
+ rule={arrayOf(requiredField('required'))}
+ />
+ );
+
+ act(() => validator.validate());
+ expect(validator.state.valid).toBe(true);
+ expect(screen.getByRole('textbox')).toHaveAccessibleDescription('');
+
+ await user.click(screen.getByRole('button', { name: 'Add More' }));
+ await user.type(screen.getAllByRole('textbox')[1], 'foo');
+ act(() => validator.validate());
+ expect(validator.state.valid).toBe(false);
+ expect(screen.getAllByRole('textbox')[0]).toHaveAccessibleDescription(
+ 'required'
+ );
+ expect(screen.getAllByRole('textbox')[1]).toHaveAccessibleDescription('');
+
+ await user.type(screen.getAllByRole('textbox')[0], 'foo');
+ act(() => validator.validate());
+ expect(validator.state.valid).toBe(true);
+ expect(screen.getAllByRole('textbox')[0]).toHaveAccessibleDescription('');
+ expect(screen.getAllByRole('textbox')[1]).toHaveAccessibleDescription('');
+});
diff --git a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.tsx b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.tsx
index e1dbace8c97d5..f323ec7dd268d 100644
--- a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.tsx
+++ b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.tsx
@@ -21,15 +21,35 @@ import { ButtonSecondary } from 'design/Button';
import ButtonIcon from 'design/ButtonIcon';
import Flex from 'design/Flex';
import * as Icon from 'design/Icon';
-import Input from 'design/Input';
import { useRef } from 'react';
import styled, { useTheme } from 'styled-components';
+import {
+ precomputed,
+ Rule,
+ ValidationResult,
+} from 'shared/components/Validation/rules';
+import { useRule } from 'shared/components/Validation';
+
+import FieldInput from '../FieldInput';
+
+type StringListValidationResult = ValidationResult & {
+ /**
+ * A list of validation results, one per label. Note: items are optional just
+ * because `useRule` by default returns only `ValidationResult`. For the
+ * actual validation, it's not optional; if it's undefined, or there are
+ * fewer items in this list than the labels, the corresponding items will be
+ * treated as valid.
+ */
+ items?: ValidationResult[];
+};
+
export type FieldMultiInputProps = {
label?: string;
value: string[];
disabled?: boolean;
onChange?(val: string[]): void;
+ rule?: Rule;
};
/**
@@ -45,7 +65,13 @@ export function FieldMultiInput({
value,
disabled,
onChange,
+ rule = defaultRule,
}: FieldMultiInputProps) {
+ // It's important to first validate, and then treat an empty array as a
+ // single-item list with an empty string, since this "synthetic" empty
+ // string is technically not a part of the model and should not be
+ // validated.
+ const validationResult: StringListValidationResult = useRule(rule(value));
if (value.length === 0) {
value = [''];
}
@@ -90,8 +116,11 @@ export function FieldMultiInput({
// procedure whose complexity would probably outweigh the benefits).
-
onChange?.(
@@ -99,6 +128,7 @@ export function FieldMultiInput({
)
}
onKeyDown={e => handleKeyDown(i, e)}
+ mb={0}
/>
() => ({ valid: true });
+
const Fieldset = styled.fieldset`
border: none;
margin: 0;
diff --git a/web/packages/shared/components/FieldSelect/FieldSelect.story.tsx b/web/packages/shared/components/FieldSelect/FieldSelect.story.tsx
index cecfa4e84dac0..dde0dc2b97dce 100644
--- a/web/packages/shared/components/FieldSelect/FieldSelect.story.tsx
+++ b/web/packages/shared/components/FieldSelect/FieldSelect.story.tsx
@@ -53,7 +53,10 @@ export function Default() {
return (
{({ validator }) => {
- validator.validate();
+ // Prevent rendering loop.
+ if (!validator.state.validating) {
+ validator.validate();
+ }
return (
.
- */
-
-import React from 'react';
-
-import { isObject } from 'shared/utils/highbar';
-
-import Logger from '../../libs/logger';
-
-const logger = Logger.create('validation');
-
-// Validator handles input validation
-export default class Validator {
- valid = true;
-
- constructor() {
- // store subscribers
- this._subs = [];
- }
-
- // adds a callback to the list of subscribers
- subscribe(cb) {
- this._subs.push(cb);
- }
-
- // removes a callback from the list of subscribers
- unsubscribe(cb) {
- const index = this._subs.indexOf(cb);
- if (index > -1) {
- this._subs.splice(index, 1);
- }
- }
-
- addResult(result) {
- // result can be a boolean value or an object
- let isValid = false;
- if (isObject(result)) {
- isValid = result.valid;
- } else {
- logger.error(`rule should return a valid object`);
- }
-
- this.valid = this.valid && Boolean(isValid);
- }
-
- reset() {
- this.valid = true;
- this.validating = false;
- }
-
- validate() {
- this.reset();
- this.validating = true;
- this._subs.forEach(cb => {
- try {
- cb();
- } catch (err) {
- logger.error(err);
- }
- });
-
- return this.valid;
- }
-}
-
-const ValidationContext = React.createContext({});
-
-export function Validation(props) {
- const [validator] = React.useState(() => new Validator());
- // handle render functions
- const children =
- typeof props.children === 'function'
- ? props.children({ validator })
- : props.children;
-
- return (
-
- {children}
-
- );
-}
-
-export function useValidation() {
- const value = React.useContext(ValidationContext);
- if (!(value instanceof Validator)) {
- logger.warn('Missing Validation Context declaration');
- }
-
- return value;
-}
diff --git a/web/packages/shared/components/Validation/Validation.test.tsx b/web/packages/shared/components/Validation/Validation.test.tsx
index 19f40a8d44986..a933e9d916af9 100644
--- a/web/packages/shared/components/Validation/Validation.test.tsx
+++ b/web/packages/shared/components/Validation/Validation.test.tsx
@@ -17,32 +17,22 @@
*/
import React from 'react';
+import { render, fireEvent, screen, act } from 'design/utils/testing';
-import { render, fireEvent, screen } from 'design/utils/testing';
+import Validator, { Result, Validation, useValidation } from './Validation';
-import Logger from '../../libs/logger';
-
-import Validator, { Validation, useValidation } from './Validation';
-
-jest.mock('../../libs/logger', () => {
- const mockLogger = {
- error: jest.fn(),
- warn: jest.fn(),
- };
-
- return {
- create: () => mockLogger,
- };
+afterEach(() => {
+ jest.restoreAllMocks();
});
-test('methods of Validator: sub, unsub, validate', () => {
+test('methods of Validator: addRuleCallback, removeRuleCallback, validate', () => {
const mockCb1 = jest.fn();
const mockCb2 = jest.fn();
const validator = new Validator();
// test suscribe
- validator.subscribe(mockCb1);
- validator.subscribe(mockCb2);
+ validator.addRuleCallback(mockCb1);
+ validator.addRuleCallback(mockCb2);
// test validate runs all subscribed cb's
expect(validator.validate()).toBe(true);
@@ -51,42 +41,42 @@ test('methods of Validator: sub, unsub, validate', () => {
jest.clearAllMocks();
// test unsubscribe method removes correct cb
- validator.unsubscribe(mockCb2);
+ validator.removeRuleCallback(mockCb2);
expect(validator.validate()).toBe(true);
expect(mockCb1).toHaveBeenCalledTimes(1);
expect(mockCb2).toHaveBeenCalledTimes(0);
});
test('methods of Validator: addResult, reset', () => {
+ const consoleError = jest.spyOn(console, 'error').mockImplementation();
const validator = new Validator();
// test addResult for nil object
const result = null;
validator.addResult(result);
- expect(Logger.create().error).toHaveBeenCalledTimes(1);
+ expect(consoleError).toHaveBeenCalledTimes(1);
// test addResult for boolean
validator.addResult(true);
- expect(validator.valid).toBe(false);
+ expect(validator.state.valid).toBe(false);
// test addResult with incorrect object
- let resultObj = {};
- validator.addResult(resultObj);
- expect(validator.valid).toBe(false);
+ validator.addResult({} as Result);
+ expect(validator.state.valid).toBe(false);
// test addResult with correct object with "valid" prop from prior test set to false
- resultObj = { valid: true };
+ let resultObj = { valid: true };
validator.addResult(resultObj);
- expect(validator.valid).toBe(false);
+ expect(validator.state.valid).toBe(false);
// test reset
validator.reset();
- expect(validator.valid).toBe(true);
- expect(validator.validating).toBe(false);
+ expect(validator.state.valid).toBe(true);
+ expect(validator.state.validating).toBe(false);
// test addResult with correct object with "valid" prop reset to true
validator.addResult(resultObj);
- expect(validator.valid).toBe(true);
+ expect(validator.state.valid).toBe(true);
});
test('trigger validation via useValidation hook', () => {
@@ -102,7 +92,7 @@ test('trigger validation via useValidation hook', () => {
);
fireEvent.click(screen.getByRole('button'));
- expect(validator.validating).toBe(true);
+ expect(validator.state.validating).toBe(true);
});
test('trigger validation via render function', () => {
@@ -122,5 +112,56 @@ test('trigger validation via render function', () => {
);
fireEvent.click(screen.getByRole('button'));
- expect(validator.validating).toBe(true);
+ expect(validator.state.validating).toBe(true);
+});
+
+test('rendering validation result via useValidation hook', () => {
+ let validator: Validator;
+ const TestComponent = () => {
+ validator = useValidation();
+ return (
+ <>
+ Validating: {String(validator.state.validating)}
+ Valid: {String(validator.state.valid)}
+ >
+ );
+ };
+ render(
+
+
+
+ );
+ validator.addRuleCallback(() => validator.addResult({ valid: false }));
+
+ expect(screen.getByText('Validating: false')).toBeInTheDocument();
+ expect(screen.getByText('Valid: true')).toBeInTheDocument();
+
+ act(() => validator.validate());
+ expect(screen.getByText('Validating: true')).toBeInTheDocument();
+ expect(screen.getByText('Valid: false')).toBeInTheDocument();
+});
+
+test('rendering validation result via render function', () => {
+ let validator: Validator;
+ render(
+
+ {props => {
+ validator = props.validator;
+ return (
+ <>
+ Validating: {String(validator.state.validating)}
+ Valid: {String(validator.state.valid)}
+ >
+ );
+ }}
+
+ );
+ validator.addRuleCallback(() => validator.addResult({ valid: false }));
+
+ expect(screen.getByText('Validating: false')).toBeInTheDocument();
+ expect(screen.getByText('Valid: true')).toBeInTheDocument();
+
+ act(() => validator.validate());
+ expect(screen.getByText('Validating: true')).toBeInTheDocument();
+ expect(screen.getByText('Valid: false')).toBeInTheDocument();
});
diff --git a/web/packages/shared/components/Validation/Validation.tsx b/web/packages/shared/components/Validation/Validation.tsx
new file mode 100644
index 0000000000000..b032f452f4dba
--- /dev/null
+++ b/web/packages/shared/components/Validation/Validation.tsx
@@ -0,0 +1,190 @@
+/*
+ * 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 { Logger } from 'design/logger';
+
+import { isObject } from 'shared/utils/highbar';
+import { Store, useStore } from 'shared/libs/stores';
+
+import { ValidationResult } from './rules';
+
+const logger = new Logger('validation');
+
+/** A per-rule callback that will be executed during validation. */
+type RuleCallback = () => void;
+
+export type Result = ValidationResult | boolean;
+
+type ValidatorState = {
+ /** Indicates whether the last validation was successful. */
+ valid: boolean;
+ /**
+ * Indicates whether the validator has been activated by a call to
+ * `validate`.
+ */
+ validating: boolean;
+};
+
+/** A store that handles input validation and makes its results accessible. */
+export default class Validator extends Store {
+ state = {
+ valid: true,
+ validating: false,
+ };
+
+ /**
+ * @deprecated For temporary Enterprise compatibility only. Use {@link state}
+ * instead.
+ */
+ valid = true;
+
+ /** Callbacks that will be executed upon validation. */
+ private ruleCallbacks: RuleCallback[] = [];
+
+ /** Adds a rule callback that will be executed upon validation. */
+ addRuleCallback(cb: RuleCallback) {
+ this.ruleCallbacks.push(cb);
+ }
+
+ /** Removes a rule callback. */
+ removeRuleCallback(cb: RuleCallback) {
+ const index = this.ruleCallbacks.indexOf(cb);
+ if (index > -1) {
+ this.ruleCallbacks.splice(index, 1);
+ }
+ }
+
+ addResult(result: Result) {
+ // result can be a boolean value or an object
+ let isValid = false;
+ if (isObject(result)) {
+ isValid = result.valid;
+ } else {
+ logger.error(`rule should return a valid object`);
+ }
+
+ const valid = this.state.valid && Boolean(isValid);
+ this.setState({ valid });
+ this.valid = valid;
+ }
+
+ reset() {
+ this.setState({
+ valid: true,
+ validating: false,
+ });
+ this.valid = true;
+ }
+
+ validate() {
+ this.reset();
+ this.setState({ validating: true });
+ for (const cb of this.ruleCallbacks) {
+ try {
+ cb();
+ } catch (err) {
+ logger.error(err);
+ }
+ }
+
+ return this.state.valid;
+ }
+}
+
+const ValidationContext = React.createContext(undefined);
+
+type ValidationRenderFunction = (arg: { validator: Validator }) => any;
+
+/**
+ * Installs a validation context that provides a {@link Validator} store. The
+ * store can be retrieved either through {@link useValidation} hook or by a
+ * render callback, e.g.:
+ *
+ * ```
+ * function Component() {
+ * return (
+ *
+ * {({validator}) => (
+ * <>
+ * (...)
+ * validator.validate()}>Validate
+ * >
+ * )}
+ *
+ * );
+ * }
+ * ```
+ *
+ * The simplest way to use validation is validating on the view layer: just use
+ * a `rule` prop with `FieldInput` or a similar component and pass a rule like
+ * `requiredField`.
+ *
+ * Unfortunately, due to architectural limitations, this will not work well in
+ * scenarios where information about validity about given field or group of
+ * fields is required outside that field. In cases like this, the best option
+ * is to validate the model during render time on the top level (for example,
+ * execute an entire set of rules on a model using `runRules`). The result of
+ * model validation will then contain information about the validity of each
+ * field. It can then be used wherever it's needed, and also attached to
+ * appropriate inputs with a `precomputed` validation rule. Example:
+ *
+ * ```
+ * function Component(model: Model) {
+ * const rules = {
+ * name: requiredField('required'),
+ * email: requiredEmailLike,
+ * }
+ * const validationResult = runRules(model, rules);
+ * }
+ * ```
+ *
+ * Note that, as this example shows clearly, the validator itself, despite its
+ * name, doesn't really validate anything -- it merely aggregates validation
+ * results. Also it's worth mentioning that the validator will not do it
+ * without our help -- each validated field needs to be actually attached to a
+ * field, even if using a `precomputed` rule, for this to work. The validation
+ * callbacks registered by validation rules on the particular fields are the
+ * actual points where the errors are consumed by the validator.
+ */
+export function Validation(props: {
+ children?: React.ReactNode | ValidationRenderFunction;
+}) {
+ const [validator] = React.useState(() => new Validator());
+ useStore(validator);
+ // handle render functions
+ const children =
+ typeof props.children === 'function'
+ ? props.children({ validator })
+ : props.children;
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useValidation(): Validator | undefined {
+ const validator = React.useContext(ValidationContext);
+ if (!validator) {
+ logger.warn('Missing Validation Context declaration');
+ }
+ return useStore(validator);
+}
diff --git a/web/packages/shared/components/Validation/rules.test.ts b/web/packages/shared/components/Validation/rules.test.ts
index a07b16fb7aaa7..1ea9f5a15a541 100644
--- a/web/packages/shared/components/Validation/rules.test.ts
+++ b/web/packages/shared/components/Validation/rules.test.ts
@@ -25,6 +25,8 @@ import {
requiredEmailLike,
requiredIamRoleName,
requiredPort,
+ runRules,
+ arrayOf,
} from './rules';
describe('requiredField', () => {
@@ -153,3 +155,60 @@ describe('requiredPort', () => {
expect(requiredPort(port)()).toEqual(expected);
});
});
+
+test('runRules', () => {
+ expect(
+ runRules(
+ { foo: 'val1', bar: 'val2', irrelevant: undefined },
+ { foo: requiredField('no foo'), bar: requiredField('no bar') }
+ )
+ ).toEqual({
+ valid: true,
+ fields: {
+ foo: { valid: true, message: '' },
+ bar: { valid: true, message: '' },
+ },
+ });
+
+ expect(
+ runRules(
+ { foo: '', bar: 'val2', irrelevant: undefined },
+ { foo: requiredField('no foo'), bar: requiredField('no bar') }
+ )
+ ).toEqual({
+ valid: false,
+ fields: {
+ foo: { valid: false, message: 'no foo' },
+ bar: { valid: true, message: '' },
+ },
+ });
+});
+
+test.each([
+ {
+ name: 'invalid',
+ items: ['a', '', 'c'],
+ expected: {
+ valid: false,
+ items: [
+ { valid: true, message: '' },
+ { valid: false, message: 'required' },
+ { valid: true, message: '' },
+ ],
+ },
+ },
+ {
+ name: 'valid',
+ items: ['a', 'b', 'c'],
+ expected: {
+ valid: true,
+ items: [
+ { valid: true, message: '' },
+ { valid: true, message: '' },
+ { valid: true, message: '' },
+ ],
+ },
+ },
+])('arrayOf: $name', ({ items, expected }) => {
+ expect(arrayOf(requiredField('required'))(items)()).toEqual(expected);
+});
diff --git a/web/packages/shared/components/Validation/rules.ts b/web/packages/shared/components/Validation/rules.ts
index 52063d67fce99..57b07f062a5af 100644
--- a/web/packages/shared/components/Validation/rules.ts
+++ b/web/packages/shared/components/Validation/rules.ts
@@ -31,6 +31,8 @@ export interface ValidationResult {
*/
export type Rule = (value: T) => () => R;
+type RuleResult = ReturnType>;
+
/**
* requiredField checks for empty strings and arrays.
*
@@ -280,6 +282,83 @@ const requiredAll =
return { valid: true };
};
+/** A result of the {@link arrayOf} validation rule. */
+export type ArrayValidationResult = ValidationResult & {
+ /** Results of validating each separate item. */
+ items: R[];
+};
+
+/** Validates an array by executing given rule on each of its elements. */
+const arrayOf =
+ (
+ elementRule: Rule
+ ): Rule> =>
+ (values: T[]) =>
+ () => {
+ const results = values.map(v => elementRule(v)());
+ return { items: results, valid: results.every(r => r.valid) };
+ };
+
+/**
+ * Passes a precomputed validation result instead of computing it inside the
+ * rule.
+ *
+ * This rule is a hacky way to allow the validation engine to operate with
+ * validation results computed outside of the validator's validation cycle. See
+ * the `Validation` component's documentation for more information about where
+ * this is useful and a detailed usage example.
+ */
+const precomputed =
+ (res: ValidationResult): Rule =>
+ () =>
+ () =>
+ res;
+
+/**
+ * A set of rules to be executed using `runRules` on a model object. The rule
+ * set contains a subset of keys of the object.
+ */
+export type RuleSet = Record<
+ K,
+ Rule
+>;
+
+/** A result of executing a set of rules on a model object. */
+export type RuleSetValidationResult> = {
+ valid: boolean;
+ /**
+ * Each member of the `fields` object corresponds to a rule from within the
+ * rule set and contains the result of validating a model field of the same
+ * name.
+ */
+ fields: { [k in keyof R]: RuleResult }; // Record;
+};
+
+/**
+ * Executes a set of rules on a model object, producing a precomputed
+ * validation result that can be used with `precomputed` rule to inject to
+ * field components, but also allows for consuming the validation data outside
+ * these fields.
+ *
+ * `K` is the subset of model field names.
+ * `M` is the validated model.
+ */
+export const runRules = >(
+ model: M,
+ rules: RuleSet
+): RuleSetValidationResult> => {
+ const fields = {} as {
+ [k in keyof RuleSet]: RuleResult[k]>;
+ };
+ let valid = true;
+ for (const key in rules) {
+ const modelValue = model[key];
+ fields[key] = rules[key](modelValue)();
+ valid &&= fields[key].valid;
+ }
+ return { fields, valid };
+};
+
export {
requiredToken,
requiredPassword,
@@ -292,4 +371,6 @@ export {
requiredMatchingRoleNameAndRoleArn,
validAwsIAMRoleName,
requiredPort,
+ arrayOf,
+ precomputed,
};
diff --git a/web/packages/shared/components/Validation/useRule.js b/web/packages/shared/components/Validation/useRule.js
index ad0ca82157cbf..e8d2a77e391ae 100644
--- a/web/packages/shared/components/Validation/useRule.js
+++ b/web/packages/shared/components/Validation/useRule.js
@@ -39,7 +39,7 @@ export default function useRule(cb) {
// register to validation context to be called on cb()
React.useEffect(() => {
function onValidate() {
- if (validator.validating) {
+ if (validator.state.validating) {
const result = cb();
validator.addResult(result);
rerender({});
@@ -47,18 +47,18 @@ export default function useRule(cb) {
}
// subscribe to store changes
- validator.subscribe(onValidate);
+ validator.addRuleCallback(onValidate);
// unsubscribe on unmount
function cleanup() {
- validator.unsubscribe(onValidate);
+ validator.removeRuleCallback(onValidate);
}
return cleanup;
}, [cb]);
// if validation has been requested, cb right away.
- if (validator.validating) {
+ if (validator.state.validating) {
return cb();
}
diff --git a/web/packages/teleport/src/components/LabelsInput/LabelsInput.test.tsx b/web/packages/teleport/src/components/LabelsInput/LabelsInput.test.tsx
index eaee3b29c7ea6..6697f3d5f163b 100644
--- a/web/packages/teleport/src/components/LabelsInput/LabelsInput.test.tsx
+++ b/web/packages/teleport/src/components/LabelsInput/LabelsInput.test.tsx
@@ -17,7 +17,10 @@
*/
import { render, fireEvent, screen } from 'design/utils/testing';
+import Validation, { Validator } from 'shared/components/Validation';
+import { act } from '@testing-library/react';
+import { Label, LabelsInput, LabelsRule, nonEmptyLabels } from './LabelsInput';
import {
Default,
Custom,
@@ -102,3 +105,119 @@ test('removing last label is not possible due to requiring labels', async () =>
expect(screen.getByPlaceholderText('label key')).toBeInTheDocument();
expect(screen.getByPlaceholderText('label value')).toBeInTheDocument();
});
+
+describe('validation rules', () => {
+ function setup(labels: Label[], rule: LabelsRule) {
+ let validator: Validator;
+ render(
+
+ {({ validator: v }) => {
+ validator = v;
+ return (
+ {}} rule={rule} />
+ );
+ }}
+
+ );
+ return { validator };
+ }
+
+ describe.each([
+ { name: 'explicitly enforced standard rule', rule: nonEmptyLabels },
+ { name: 'implicit standard rule', rule: undefined },
+ ])('$name', ({ rule }) => {
+ test('invalid', () => {
+ const { validator } = setup(
+ [
+ { name: '', value: 'foo' },
+ { name: 'bar', value: '' },
+ { name: 'asdf', value: 'qwer' },
+ ],
+ rule
+ );
+ act(() => validator.validate());
+ expect(validator.state.valid).toBe(false);
+ expect(screen.getAllByRole('textbox')[0]).toHaveAccessibleDescription(
+ 'required'
+ ); // ''
+ expect(screen.getAllByRole('textbox')[1]).toHaveAccessibleDescription(''); // 'foo'
+ expect(screen.getAllByRole('textbox')[2]).toHaveAccessibleDescription(''); // 'bar'
+ expect(screen.getAllByRole('textbox')[3]).toHaveAccessibleDescription(
+ 'required'
+ ); // ''
+ expect(screen.getAllByRole('textbox')[4]).toHaveAccessibleDescription(''); // 'asdf'
+ expect(screen.getAllByRole('textbox')[5]).toHaveAccessibleDescription(''); // 'qwer'
+ });
+
+ test('valid', () => {
+ const { validator } = setup(
+ [
+ { name: '', value: 'foo' },
+ { name: 'bar', value: '' },
+ { name: 'asdf', value: 'qwer' },
+ ],
+ rule
+ );
+ act(() => validator.validate());
+ expect(validator.state.valid).toBe(false);
+ expect(screen.getAllByRole('textbox')[0]).toHaveAccessibleDescription(
+ 'required'
+ ); // ''
+ expect(screen.getAllByRole('textbox')[1]).toHaveAccessibleDescription(''); // 'foo'
+ expect(screen.getAllByRole('textbox')[2]).toHaveAccessibleDescription(''); // 'bar'
+ expect(screen.getAllByRole('textbox')[3]).toHaveAccessibleDescription(
+ 'required'
+ ); // ''
+ expect(screen.getAllByRole('textbox')[4]).toHaveAccessibleDescription(''); // 'asdf'
+ expect(screen.getAllByRole('textbox')[5]).toHaveAccessibleDescription(''); // 'qwer'
+ });
+ });
+
+ const nameNotFoo: LabelsRule = (labels: Label[]) => () => {
+ const results = labels.map(label => ({
+ name:
+ label.name === 'foo'
+ ? { valid: false, message: 'no foo please' }
+ : { valid: true },
+ value: { valid: true },
+ }));
+ return {
+ valid: results.every(r => r.name.valid && r.value.valid),
+ items: results,
+ };
+ };
+
+ test('custom rule, invalid', async () => {
+ const { validator } = setup(
+ [
+ { name: 'foo', value: 'bar' },
+ { name: 'bar', value: 'foo' },
+ ],
+ nameNotFoo
+ );
+ act(() => validator.validate());
+ expect(validator.state.valid).toBe(false);
+ expect(screen.getAllByRole('textbox')[0]).toHaveAccessibleDescription(
+ 'no foo please'
+ ); // 'foo' key
+ expect(screen.getAllByRole('textbox')[1]).toHaveAccessibleDescription('');
+ expect(screen.getAllByRole('textbox')[2]).toHaveAccessibleDescription('');
+ expect(screen.getAllByRole('textbox')[3]).toHaveAccessibleDescription('');
+ });
+
+ test('custom rule, valid', async () => {
+ const { validator } = setup(
+ [
+ { name: 'xyz', value: 'bar' },
+ { name: 'bar', value: 'foo' },
+ ],
+ nameNotFoo
+ );
+ act(() => validator.validate());
+ expect(validator.state.valid).toBe(true);
+ expect(screen.getAllByRole('textbox')[0]).toHaveAccessibleDescription('');
+ expect(screen.getAllByRole('textbox')[1]).toHaveAccessibleDescription('');
+ expect(screen.getAllByRole('textbox')[2]).toHaveAccessibleDescription('');
+ expect(screen.getAllByRole('textbox')[3]).toHaveAccessibleDescription('');
+ });
+});
diff --git a/web/packages/teleport/src/components/LabelsInput/LabelsInput.tsx b/web/packages/teleport/src/components/LabelsInput/LabelsInput.tsx
index f163d7df0e0de..c4256375325b7 100644
--- a/web/packages/teleport/src/components/LabelsInput/LabelsInput.tsx
+++ b/web/packages/teleport/src/components/LabelsInput/LabelsInput.tsx
@@ -19,8 +19,17 @@
import styled from 'styled-components';
import { Flex, Box, ButtonSecondary, ButtonIcon } from 'design';
import FieldInput from 'shared/components/FieldInput';
-import { Validator, useValidation } from 'shared/components/Validation';
-import { requiredField } from 'shared/components/Validation/rules';
+import {
+ Validator,
+ useRule,
+ useValidation,
+} from 'shared/components/Validation';
+import {
+ precomputed,
+ requiredField,
+ Rule,
+ ValidationResult,
+} from 'shared/components/Validation/rules';
import * as Icons from 'design/Icon';
import { inputGeometry } from 'design/Input/Input';
@@ -34,6 +43,24 @@ export type LabelInputTexts = {
placeholder: string;
};
+type LabelListValidationResult = ValidationResult & {
+ /**
+ * A list of validation results, one per label. Note: items are optional just
+ * because `useRule` by default returns only `ValidationResult`. For the
+ * actual validation, it's not optional; if it's undefined, or there are
+ * fewer items in this list than the labels, a default validation rule will
+ * be used instead.
+ */
+ items?: LabelValidationResult[];
+};
+
+type LabelValidationResult = {
+ name: ValidationResult;
+ value: ValidationResult;
+};
+
+export type LabelsRule = Rule;
+
export function LabelsInput({
labels = [],
setLabels,
@@ -44,6 +71,7 @@ export function LabelsInput({
labelKey = { fieldName: 'Key', placeholder: 'label key' },
labelVal = { fieldName: 'Value', placeholder: 'label value' },
inputWidth = 200,
+ rule = defaultRule,
}: {
labels: Label[];
setLabels(l: Label[]): void;
@@ -57,8 +85,15 @@ export function LabelsInput({
* Makes it so at least one label is required
*/
areLabelsRequired?: boolean;
+ /**
+ * A rule for validating the list of labels as a whole. Note that contrary to
+ * other input fields, the labels input will default to validating every
+ * input as required if this property is undefined.
+ */
+ rule?: LabelsRule;
}) {
const validator = useValidation() as Validator;
+ const validationResult: LabelListValidationResult = useRule(rule(labels));
function addLabel() {
setLabels([...labels, { name: '', value: '' }]);
@@ -92,11 +127,8 @@ export function LabelsInput({
setLabels(newList);
};
- const requiredUniqueKey = value => () => {
+ const requiredKey = value => () => {
// Check for empty length and duplicate key.
- // TODO(bl-nero): This function doesn't really check for uniqueness; it
- // needs to be fixed. This control should probably be merged with
- // `LabelsCreater`, which has this feature working correctly.
let notValid = !value || value.length === 0;
return {
@@ -121,12 +153,18 @@ export function LabelsInput({
)}
{labels.map((label, index) => {
+ const validationItem: LabelValidationResult | undefined =
+ validationResult.items?.[index];
return (
() => ({ valid: true });
+
const SmallText = styled.span`
font-size: ${p => p.theme.fontSizes[1]}px;
font-weight: lighter;
`;
+
+export const nonEmptyLabels: LabelsRule = labels => () => {
+ const results = labels.map(label => ({
+ name: requiredField('required')(label.name)(),
+ value: requiredField('required')(label.value)(),
+ }));
+ return {
+ valid: results.every(r => r.name.valid && r.value.valid),
+ items: results,
+ };
+};
From 729767dd528bd144b594ff1d60507572925962b6 Mon Sep 17 00:00:00 2001
From: Bartosz Leper
Date: Thu, 28 Nov 2024 12:55:55 +0100
Subject: [PATCH 02/14] Move tooltips to the design package
They will be later required in the SlideTabs component
---
.../src}/ToolTip/HoverTooltip.tsx | 4 +++-
.../src}/ToolTip/ToolTip.story.tsx | 13 +++++++++---
.../src}/ToolTip/ToolTip.tsx | 8 +++-----
web/packages/design/src/ToolTip/index.ts | 20 +++++++++++++++++++
.../src}/ToolTip/shared.tsx | 0
.../AccessDuration/AccessDurationRequest.tsx | 3 ++-
.../AccessDuration/AccessDurationReview.tsx | 2 +-
.../RequestCheckout/AdditionalOptions.tsx | 3 ++-
.../RequestCheckout/RequestCheckout.tsx | 3 ++-
.../NewRequest/ResourceList/Apps.tsx | 3 ++-
.../RequestReview/RequestReview.tsx | 3 ++-
.../RequestView/RequestView.tsx | 3 ++-
.../AccessRequests/Shared/Shared.tsx | 3 ++-
.../AdvancedSearchToggle.tsx | 2 +-
.../ClusterDropdown/ClusterDropdown.tsx | 2 +-
.../components/Controls/MultiselectMenu.tsx | 2 +-
.../shared/components/Controls/SortMenu.tsx | 2 +-
.../components/Controls/ViewModeSwitch.tsx | 2 +-
.../components/FieldInput/FieldInput.tsx | 3 ++-
.../shared/components/FieldSelect/shared.tsx | 3 ++-
.../FieldTextArea/FieldTextArea.tsx | 3 ++-
.../shared/components/ToolTip/index.ts | 9 +++++++--
.../CardsView/ResourceCard.tsx | 2 +-
.../UnifiedResources/FilterPanel.tsx | 3 ++-
.../ListView/ResourceListItem.tsx | 2 +-
.../UnifiedResources/ResourceTab.tsx | 2 +-
.../UnifiedResources/UnifiedResources.tsx | 3 ++-
.../UnifiedResources/shared/CopyButton.tsx | 2 +-
.../UnifiedResources/shared/PinButton.tsx | 2 +-
web/packages/teleport/src/Bots/List/Bots.tsx | 2 +-
.../DocumentKubeExec/KubeExecDataDialog.tsx | 2 +-
.../teleport/src/DesktopSession/TopBar.tsx | 2 +-
.../CreateAppAccess/CreateAppAccess.tsx | 2 +-
.../AutoDeploy/SelectSecurityGroups.tsx | 2 +-
.../AutoDeploy/SelectSubnetIds.tsx | 2 +-
.../EnrollRdsDatabase/AutoDiscoverToggle.tsx | 2 +-
.../EnrollEKSCluster/EnrollEksCluster.tsx | 2 +-
.../DiscoveryConfigSsm/DiscoveryConfigSsm.tsx | 2 +-
.../EnrollEc2Instance/EnrollEc2Instance.tsx | 2 +-
.../Discover/Shared/Aws/ConfigureIamPerms.tsx | 2 +-
.../ConfigureDiscoveryServiceDirections.tsx | 2 +-
.../SecurityGroupPicker.tsx | 2 +-
.../AwsOidc/ConfigureAwsOidcSummary.tsx | 2 +-
.../Enroll/AwsOidc/S3BucketConfiguration.tsx | 2 +-
.../src/Integrations/IntegrationList.tsx | 2 +-
.../status/AwsOidc/AwsOidcHeader.tsx | 2 +-
.../teleport/src/JoinTokens/JoinTokens.tsx | 2 +-
.../src/JoinTokens/UpsertJoinTokenDialog.tsx | 2 +-
.../teleport/src/Navigation/Navigation.tsx | 2 +-
.../src/Navigation/SideNavigation/Section.tsx | 2 +-
.../src/Notifications/Notifications.tsx | 2 +-
.../src/Roles/RoleEditor/EditorHeader.tsx | 2 +-
.../teleport/src/Roles/RoleEditor/Shared.tsx | 2 +-
.../src/Roles/RoleEditor/StandardEditor.tsx | 2 +-
web/packages/teleport/src/TopBar/TopBar.tsx | 2 +-
.../teleport/src/TopBar/TopBarSideNav.tsx | 2 +-
.../ExternalAuditStorageCta.tsx | 2 +-
57 files changed, 106 insertions(+), 62 deletions(-)
rename web/packages/{shared/components => design/src}/ToolTip/HoverTooltip.tsx (98%)
rename web/packages/{shared/components => design/src}/ToolTip/ToolTip.story.tsx (94%)
rename web/packages/{shared/components => design/src}/ToolTip/ToolTip.tsx (96%)
create mode 100644 web/packages/design/src/ToolTip/index.ts
rename web/packages/{shared/components => design/src}/ToolTip/shared.tsx (100%)
diff --git a/web/packages/shared/components/ToolTip/HoverTooltip.tsx b/web/packages/design/src/ToolTip/HoverTooltip.tsx
similarity index 98%
rename from web/packages/shared/components/ToolTip/HoverTooltip.tsx
rename to web/packages/design/src/ToolTip/HoverTooltip.tsx
index e9e16bba58359..6dbaab58bdf0a 100644
--- a/web/packages/shared/components/ToolTip/HoverTooltip.tsx
+++ b/web/packages/design/src/ToolTip/HoverTooltip.tsx
@@ -18,6 +18,7 @@
import React, { PropsWithChildren, useState } from 'react';
import styled, { useTheme } from 'styled-components';
+
import { Popover, Flex, Text } from 'design';
import { JustifyContentProps, FlexBasisProps } from 'design/system';
@@ -64,6 +65,7 @@ export const HoverTooltip: React.FC<
// whether we want to show the tooltip.
if (
target instanceof Element &&
+ target.parentElement &&
target.scrollWidth > target.parentElement.offsetWidth
) {
setAnchorEl(event.currentTarget);
@@ -75,7 +77,7 @@ export const HoverTooltip: React.FC<
}
function handlePopoverClose() {
- setAnchorEl(null);
+ setAnchorEl(undefined);
}
// Don't render the tooltip if the content is undefined.
diff --git a/web/packages/shared/components/ToolTip/ToolTip.story.tsx b/web/packages/design/src/ToolTip/ToolTip.story.tsx
similarity index 94%
rename from web/packages/shared/components/ToolTip/ToolTip.story.tsx
rename to web/packages/design/src/ToolTip/ToolTip.story.tsx
index 1830fc4902e12..686c005aa37a1 100644
--- a/web/packages/shared/components/ToolTip/ToolTip.story.tsx
+++ b/web/packages/design/src/ToolTip/ToolTip.story.tsx
@@ -17,11 +17,13 @@
*/
import React from 'react';
-import { Text, Flex, ButtonPrimary } from 'design';
import styled, { useTheme } from 'styled-components';
+
+import { Text, Flex, ButtonPrimary } from 'design';
import { P } from 'design/Text/Text';
-import { logos } from 'teleport/components/LogoHero/LogoHero';
+import AGPLLogoLight from 'design/assets/images/agpl-light.svg';
+import AGPLLogoDark from 'design/assets/images/agpl-dark.svg';
import { ToolTipInfo } from './ToolTip';
import { HoverTooltip } from './HoverTooltip';
@@ -65,6 +67,11 @@ const Grid = styled.div`
grid-template-rows: repeat(3, 100px);
`;
+const logos = {
+ light: AGPLLogoLight,
+ dark: AGPLLogoDark,
+};
+
export const LongContent = () => {
const theme = useTheme();
return (
@@ -92,7 +99,7 @@ export const LongContent = () => {
-
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
diff --git a/web/packages/shared/components/ToolTip/ToolTip.tsx b/web/packages/design/src/ToolTip/ToolTip.tsx
similarity index 96%
rename from web/packages/shared/components/ToolTip/ToolTip.tsx
rename to web/packages/design/src/ToolTip/ToolTip.tsx
index f80a3a6342b91..8443ecc20fbf3 100644
--- a/web/packages/shared/components/ToolTip/ToolTip.tsx
+++ b/web/packages/design/src/ToolTip/ToolTip.tsx
@@ -46,15 +46,15 @@ export const ToolTipInfo: React.FC<
kind = 'info',
}) => {
const theme = useTheme();
- const [anchorEl, setAnchorEl] = useState();
+ const [anchorEl, setAnchorEl] = useState();
const open = Boolean(anchorEl);
- function handlePopoverOpen(event) {
+ function handlePopoverOpen(event: React.MouseEvent) {
setAnchorEl(event.currentTarget);
}
function handlePopoverClose() {
- setAnchorEl(null);
+ setAnchorEl(undefined);
}
const triggerOnHoverProps = {
@@ -121,8 +121,6 @@ const ToolTipIcon = ({
return ;
case 'error':
return ;
- default:
- kind satisfies never;
}
};
diff --git a/web/packages/design/src/ToolTip/index.ts b/web/packages/design/src/ToolTip/index.ts
new file mode 100644
index 0000000000000..c6518cad9b297
--- /dev/null
+++ b/web/packages/design/src/ToolTip/index.ts
@@ -0,0 +1,20 @@
+/**
+ * Teleport
+ * Copyright (C) 2023 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+export { ToolTipInfo } from './ToolTip';
+export { HoverTooltip } from './HoverTooltip';
diff --git a/web/packages/shared/components/ToolTip/shared.tsx b/web/packages/design/src/ToolTip/shared.tsx
similarity index 100%
rename from web/packages/shared/components/ToolTip/shared.tsx
rename to web/packages/design/src/ToolTip/shared.tsx
diff --git a/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx b/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx
index 998245bbc58da..d6120fdf0eddd 100644
--- a/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx
+++ b/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx
@@ -19,8 +19,9 @@
import React from 'react';
import { Flex, LabelInput, Text } from 'design';
+import { ToolTipInfo } from 'design/ToolTip';
+
import Select, { Option } from 'shared/components/Select';
-import { ToolTipInfo } from 'shared/components/ToolTip';
export function AccessDurationRequest({
maxDuration,
diff --git a/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationReview.tsx b/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationReview.tsx
index 6779c54528111..ccb5b0c1e382a 100644
--- a/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationReview.tsx
+++ b/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationReview.tsx
@@ -19,7 +19,7 @@
import React from 'react';
import { Flex, Text } from 'design';
-import { ToolTipInfo } from 'shared/components/ToolTip';
+import { ToolTipInfo } from 'design/ToolTip';
import { AccessRequest } from 'shared/services/accessRequests';
diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx
index 21edb180842ea..91fe207f6712c 100644
--- a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx
+++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx
@@ -20,8 +20,9 @@ import React, { useState } from 'react';
import { Flex, Text, ButtonIcon, Box, LabelInput } from 'design';
import * as Icon from 'design/Icon';
+import { ToolTipInfo } from 'design/ToolTip';
+
import Select, { Option } from 'shared/components/Select';
-import { ToolTipInfo } from 'shared/components/ToolTip';
import { AccessRequest } from 'shared/services/accessRequests';
diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx
index 775c10f356267..0de1808937410 100644
--- a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx
+++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx
@@ -39,6 +39,8 @@ import { ArrowBack, ChevronDown, ChevronRight, Warning } from 'design/Icon';
import Table, { Cell } from 'design/DataTable';
import { Danger } from 'design/Alert';
+import { HoverTooltip } from 'design/ToolTip';
+
import Validation, { useRule, Validator } from 'shared/components/Validation';
import { Attempt } from 'shared/hooks/useAttemptNext';
import { pluralize } from 'shared/utils/text';
@@ -47,7 +49,6 @@ import { FieldCheckbox } from 'shared/components/FieldCheckbox';
import { mergeRefs } from 'shared/libs/mergeRefs';
import { TextSelectCopyMulti } from 'shared/components/TextSelectCopy';
import { RequestableResourceKind } from 'shared/components/AccessRequests/NewRequest/resource';
-import { HoverTooltip } from 'shared/components/ToolTip';
import { CreateRequest } from '../../Shared/types';
import { AssumeStartTime } from '../../AssumeStartTime/AssumeStartTime';
diff --git a/web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Apps.tsx b/web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Apps.tsx
index 5488ceeedaefd..e2a09bf7d47e4 100644
--- a/web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Apps.tsx
+++ b/web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Apps.tsx
@@ -24,11 +24,12 @@ import { ClickableLabelCell, Cell } from 'design/DataTable';
import { App } from 'teleport/services/apps';
+import { ToolTipInfo } from 'design/ToolTip';
+
import Select, {
Option as BaseOption,
CustomSelectComponentProps,
} from 'shared/components/Select';
-import { ToolTipInfo } from 'shared/components/ToolTip';
import { ResourceMap, RequestableResourceKind } from '../resource';
diff --git a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.tsx b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.tsx
index 6a93e92656ac0..e7ce487a17f4b 100644
--- a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.tsx
+++ b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.tsx
@@ -22,12 +22,13 @@ import { ButtonPrimary, Text, Box, Alert, Flex, Label, H3 } from 'design';
import { Warning } from 'design/Icon';
import { Radio } from 'design/RadioGroup';
+import { HoverTooltip } from 'design/ToolTip';
+
import Validation, { Validator } from 'shared/components/Validation';
import { FieldSelect } from 'shared/components/FieldSelect';
import { Option } from 'shared/components/Select';
import { Attempt } from 'shared/hooks/useAsync';
import { requiredField } from 'shared/components/Validation/rules';
-import { HoverTooltip } from 'shared/components/ToolTip';
import { FieldTextArea } from 'shared/components/FieldTextArea';
import { AccessRequest, RequestState } from 'shared/services/accessRequests';
diff --git a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.tsx b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.tsx
index 3bebbf52fc0b4..2111bbceae983 100644
--- a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.tsx
+++ b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.tsx
@@ -42,7 +42,8 @@ import { displayDateWithPrefixedTime } from 'design/datetime';
import { LabelKind } from 'design/LabelState/LabelState';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
+
import { hasFinished, Attempt } from 'shared/hooks/useAsync';
import {
diff --git a/web/packages/shared/components/AccessRequests/Shared/Shared.tsx b/web/packages/shared/components/AccessRequests/Shared/Shared.tsx
index 97bf6987b2d6e..5e035bb5cdb8d 100644
--- a/web/packages/shared/components/AccessRequests/Shared/Shared.tsx
+++ b/web/packages/shared/components/AccessRequests/Shared/Shared.tsx
@@ -21,7 +21,8 @@ import { ButtonPrimary, Text, Box, ButtonIcon, Menu } from 'design';
import { Info } from 'design/Icon';
import { displayDateWithPrefixedTime } from 'design/datetime';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
+
import { AccessRequest } from 'shared/services/accessRequests';
export function PromotedMessage({
diff --git a/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.tsx b/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.tsx
index 6800783daba6a..1cf9483f669dd 100644
--- a/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.tsx
+++ b/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.tsx
@@ -22,7 +22,7 @@ import { Text, Toggle, Link, Flex, H2 } from 'design';
import { P } from 'design/Text/Text';
-import { ToolTipInfo } from 'shared/components/ToolTip';
+import { ToolTipInfo } from 'design/ToolTip';
const GUIDE_URL =
'https://goteleport.com/docs/reference/predicate-language/#resource-filtering';
diff --git a/web/packages/shared/components/ClusterDropdown/ClusterDropdown.tsx b/web/packages/shared/components/ClusterDropdown/ClusterDropdown.tsx
index eb9babb16f43c..090a7c6fd8813 100644
--- a/web/packages/shared/components/ClusterDropdown/ClusterDropdown.tsx
+++ b/web/packages/shared/components/ClusterDropdown/ClusterDropdown.tsx
@@ -24,7 +24,7 @@ import { ChevronDown } from 'design/Icon';
import cfg from 'teleport/config';
import { Cluster } from 'teleport/services/clusters';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
export interface ClusterDropdownProps {
clusterLoader: ClusterLoader;
diff --git a/web/packages/shared/components/Controls/MultiselectMenu.tsx b/web/packages/shared/components/Controls/MultiselectMenu.tsx
index f252cf7aa21be..fb73ad0d1293c 100644
--- a/web/packages/shared/components/Controls/MultiselectMenu.tsx
+++ b/web/packages/shared/components/Controls/MultiselectMenu.tsx
@@ -29,7 +29,7 @@ import {
import { ChevronDown } from 'design/Icon';
import { CheckboxInput } from 'design/Checkbox';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
type MultiselectMenuProps = {
options: {
diff --git a/web/packages/shared/components/Controls/SortMenu.tsx b/web/packages/shared/components/Controls/SortMenu.tsx
index d6bbc5cdf0d2d..fcb91790ed69a 100644
--- a/web/packages/shared/components/Controls/SortMenu.tsx
+++ b/web/packages/shared/components/Controls/SortMenu.tsx
@@ -20,7 +20,7 @@ import React, { useState } from 'react';
import { ButtonBorder, Flex, Menu, MenuItem } from 'design';
import { ArrowDown, ArrowUp } from 'design/Icon';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
type SortMenuSort = {
fieldName: Exclude;
diff --git a/web/packages/shared/components/Controls/ViewModeSwitch.tsx b/web/packages/shared/components/Controls/ViewModeSwitch.tsx
index 7997f2de29f66..51ad9ae396db4 100644
--- a/web/packages/shared/components/Controls/ViewModeSwitch.tsx
+++ b/web/packages/shared/components/Controls/ViewModeSwitch.tsx
@@ -22,7 +22,7 @@ import { Rows, SquaresFour } from 'design/Icon';
import { ViewMode } from 'gen-proto-ts/teleport/userpreferences/v1/unified_resource_preferences_pb';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
export const ViewModeSwitch = ({
currentViewMode,
diff --git a/web/packages/shared/components/FieldInput/FieldInput.tsx b/web/packages/shared/components/FieldInput/FieldInput.tsx
index e8fceadf12214..7166cac313d5c 100644
--- a/web/packages/shared/components/FieldInput/FieldInput.tsx
+++ b/web/packages/shared/components/FieldInput/FieldInput.tsx
@@ -28,8 +28,9 @@ import styled, { useTheme } from 'styled-components';
import { IconProps } from 'design/Icon/Icon';
import { InputMode, InputSize, InputType } from 'design/Input';
+import { ToolTipInfo } from 'design/ToolTip';
+
import { useRule } from 'shared/components/Validation';
-import { ToolTipInfo } from 'shared/components/ToolTip';
const FieldInput = forwardRef(
(
diff --git a/web/packages/shared/components/FieldSelect/shared.tsx b/web/packages/shared/components/FieldSelect/shared.tsx
index 72a43e14920c8..0588356769790 100644
--- a/web/packages/shared/components/FieldSelect/shared.tsx
+++ b/web/packages/shared/components/FieldSelect/shared.tsx
@@ -25,13 +25,14 @@ import LabelInput from 'design/LabelInput';
import Flex from 'design/Flex';
+import { ToolTipInfo } from 'design/ToolTip';
+
import { HelperTextLine } from '../FieldInput/FieldInput';
import { useRule } from '../Validation';
import {
AsyncProps as AsyncSelectProps,
Props as SelectProps,
} from '../Select';
-import { ToolTipInfo } from '../ToolTip';
export const defaultRule = () => () => ({ valid: true });
diff --git a/web/packages/shared/components/FieldTextArea/FieldTextArea.tsx b/web/packages/shared/components/FieldTextArea/FieldTextArea.tsx
index 545ef4d56ce17..fc01245f6c9b7 100644
--- a/web/packages/shared/components/FieldTextArea/FieldTextArea.tsx
+++ b/web/packages/shared/components/FieldTextArea/FieldTextArea.tsx
@@ -27,9 +27,10 @@ import { TextAreaSize } from 'design/TextArea';
import { BoxProps } from 'design/Box';
+import { ToolTipInfo } from 'design/ToolTip';
+
import { useRule } from 'shared/components/Validation';
-import { ToolTipInfo } from '../ToolTip';
import { HelperTextLine } from '../FieldInput/FieldInput';
export type FieldTextAreaProps = BoxProps & {
diff --git a/web/packages/shared/components/ToolTip/index.ts b/web/packages/shared/components/ToolTip/index.ts
index c6518cad9b297..a3d07c5ab42ce 100644
--- a/web/packages/shared/components/ToolTip/index.ts
+++ b/web/packages/shared/components/ToolTip/index.ts
@@ -16,5 +16,10 @@
* along with this program. If not, see .
*/
-export { ToolTipInfo } from './ToolTip';
-export { HoverTooltip } from './HoverTooltip';
+export {
+ /** @deprecated Use `design/Tooltip` */
+ ToolTipInfo,
+
+ /** @deprecated Use `design/Tooltip` */
+ HoverTooltip,
+} from 'design/ToolTip';
diff --git a/web/packages/shared/components/UnifiedResources/CardsView/ResourceCard.tsx b/web/packages/shared/components/UnifiedResources/CardsView/ResourceCard.tsx
index 8268c8b71d599..8daaaed8c4e40 100644
--- a/web/packages/shared/components/UnifiedResources/CardsView/ResourceCard.tsx
+++ b/web/packages/shared/components/UnifiedResources/CardsView/ResourceCard.tsx
@@ -26,7 +26,7 @@ import { ResourceIcon } from 'design/ResourceIcon';
import { makeLabelTag } from 'teleport/components/formatters';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
import { ResourceItemProps } from '../types';
import { PinButton } from '../shared/PinButton';
diff --git a/web/packages/shared/components/UnifiedResources/FilterPanel.tsx b/web/packages/shared/components/UnifiedResources/FilterPanel.tsx
index 6e62a50a3d4c9..133693fb09850 100644
--- a/web/packages/shared/components/UnifiedResources/FilterPanel.tsx
+++ b/web/packages/shared/components/UnifiedResources/FilterPanel.tsx
@@ -26,7 +26,8 @@ import { ChevronDown, ArrowsIn, ArrowsOut, Refresh } from 'design/Icon';
import { ViewMode } from 'gen-proto-ts/teleport/userpreferences/v1/unified_resource_preferences_pb';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
+
import { SortMenu } from 'shared/components/Controls/SortMenu';
import { ViewModeSwitch } from 'shared/components/Controls/ViewModeSwitch';
diff --git a/web/packages/shared/components/UnifiedResources/ListView/ResourceListItem.tsx b/web/packages/shared/components/UnifiedResources/ListView/ResourceListItem.tsx
index 0f2a34024536a..70784f7b2d6df 100644
--- a/web/packages/shared/components/UnifiedResources/ListView/ResourceListItem.tsx
+++ b/web/packages/shared/components/UnifiedResources/ListView/ResourceListItem.tsx
@@ -26,7 +26,7 @@ import { ResourceIcon } from 'design/ResourceIcon';
import { makeLabelTag } from 'teleport/components/formatters';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
import { ResourceItemProps } from '../types';
import { PinButton } from '../shared/PinButton';
diff --git a/web/packages/shared/components/UnifiedResources/ResourceTab.tsx b/web/packages/shared/components/UnifiedResources/ResourceTab.tsx
index 2533e2a203165..d32879e07d961 100644
--- a/web/packages/shared/components/UnifiedResources/ResourceTab.tsx
+++ b/web/packages/shared/components/UnifiedResources/ResourceTab.tsx
@@ -20,7 +20,7 @@ import React from 'react';
import styled from 'styled-components';
import { Box, Text } from 'design';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
import { PINNING_NOT_SUPPORTED_MESSAGE } from './UnifiedResources';
diff --git a/web/packages/shared/components/UnifiedResources/UnifiedResources.tsx b/web/packages/shared/components/UnifiedResources/UnifiedResources.tsx
index 5426b0d908a74..716d6c1963273 100644
--- a/web/packages/shared/components/UnifiedResources/UnifiedResources.tsx
+++ b/web/packages/shared/components/UnifiedResources/UnifiedResources.tsx
@@ -43,7 +43,8 @@ import {
AvailableResourceMode,
} from 'gen-proto-ts/teleport/userpreferences/v1/unified_resource_preferences_pb';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
+
import {
makeEmptyAttempt,
makeSuccessAttempt,
diff --git a/web/packages/shared/components/UnifiedResources/shared/CopyButton.tsx b/web/packages/shared/components/UnifiedResources/shared/CopyButton.tsx
index a3a1a4ad12be6..2b0076bfc749d 100644
--- a/web/packages/shared/components/UnifiedResources/shared/CopyButton.tsx
+++ b/web/packages/shared/components/UnifiedResources/shared/CopyButton.tsx
@@ -22,7 +22,7 @@ import ButtonIcon from 'design/ButtonIcon';
import { Check, Copy } from 'design/Icon';
import { copyToClipboard } from 'design/utils/copyToClipboard';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
export function CopyButton({
name,
diff --git a/web/packages/shared/components/UnifiedResources/shared/PinButton.tsx b/web/packages/shared/components/UnifiedResources/shared/PinButton.tsx
index 1eedee2db68a2..713f4d38f2e71 100644
--- a/web/packages/shared/components/UnifiedResources/shared/PinButton.tsx
+++ b/web/packages/shared/components/UnifiedResources/shared/PinButton.tsx
@@ -21,7 +21,7 @@ import React, { useRef } from 'react';
import { PushPinFilled, PushPin } from 'design/Icon';
import ButtonIcon from 'design/ButtonIcon';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
import { PinningSupport } from '../types';
diff --git a/web/packages/teleport/src/Bots/List/Bots.tsx b/web/packages/teleport/src/Bots/List/Bots.tsx
index be383fbd32997..d460ce277ede8 100644
--- a/web/packages/teleport/src/Bots/List/Bots.tsx
+++ b/web/packages/teleport/src/Bots/List/Bots.tsx
@@ -19,7 +19,7 @@
import React, { useEffect, useState } from 'react';
import { useAttemptNext } from 'shared/hooks';
import { Link } from 'react-router-dom';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
import { Alert, Box, Button, Indicator } from 'design';
import {
diff --git a/web/packages/teleport/src/Console/DocumentKubeExec/KubeExecDataDialog.tsx b/web/packages/teleport/src/Console/DocumentKubeExec/KubeExecDataDialog.tsx
index 36b8b7a505820..f5b4146bcb894 100644
--- a/web/packages/teleport/src/Console/DocumentKubeExec/KubeExecDataDialog.tsx
+++ b/web/packages/teleport/src/Console/DocumentKubeExec/KubeExecDataDialog.tsx
@@ -35,7 +35,7 @@ import {
import Validation from 'shared/components/Validation';
import FieldInput from 'shared/components/FieldInput';
import { requiredField } from 'shared/components/Validation/rules';
-import { ToolTipInfo } from 'shared/components/ToolTip';
+import { ToolTipInfo } from 'design/ToolTip';
type Props = {
onClose(): void;
diff --git a/web/packages/teleport/src/DesktopSession/TopBar.tsx b/web/packages/teleport/src/DesktopSession/TopBar.tsx
index 9ab12b523067d..e8a85a87f2c49 100644
--- a/web/packages/teleport/src/DesktopSession/TopBar.tsx
+++ b/web/packages/teleport/src/DesktopSession/TopBar.tsx
@@ -21,7 +21,7 @@ import { useTheme } from 'styled-components';
import { Text, TopNav, Flex } from 'design';
import { Clipboard, FolderShared } from 'design/Icon';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
import ActionMenu from './ActionMenu';
import { AlertDropdown } from './AlertDropdown';
diff --git a/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx b/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx
index 4f0b1ea32146c..2104c11ce5fd4 100644
--- a/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx
+++ b/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx
@@ -21,7 +21,7 @@ import styled from 'styled-components';
import { Box, Flex, Link, Mark, H3 } from 'design';
import TextEditor from 'shared/components/TextEditor';
import { Danger } from 'design/Alert';
-import { ToolTipInfo } from 'shared/components/ToolTip';
+import { ToolTipInfo } from 'design/ToolTip';
import { useAsync } from 'shared/hooks/useAsync';
import { P } from 'design/Text/Text';
diff --git a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSecurityGroups.tsx b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSecurityGroups.tsx
index cc5ed4bb590b4..74035fd6774c0 100644
--- a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSecurityGroups.tsx
+++ b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSecurityGroups.tsx
@@ -21,7 +21,7 @@ import React, { useState, useEffect } from 'react';
import { Text, Flex, Box, Indicator, ButtonSecondary, Subtitle3 } from 'design';
import * as Icons from 'design/Icon';
import { FetchStatus } from 'design/DataTable/types';
-import { HoverTooltip, ToolTipInfo } from 'shared/components/ToolTip';
+import { HoverTooltip, ToolTipInfo } from 'design/ToolTip';
import useAttempt from 'shared/hooks/useAttemptNext';
import { getErrMessage } from 'shared/utils/errorType';
import { pluralize } from 'shared/utils/text';
diff --git a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSubnetIds.tsx b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSubnetIds.tsx
index 785ec15fbda9e..712102576074b 100644
--- a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSubnetIds.tsx
+++ b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSubnetIds.tsx
@@ -29,7 +29,7 @@ import {
} from 'design';
import * as Icons from 'design/Icon';
import { FetchStatus } from 'design/DataTable/types';
-import { HoverTooltip, ToolTipInfo } from 'shared/components/ToolTip';
+import { HoverTooltip, ToolTipInfo } from 'design/ToolTip';
import { pluralize } from 'shared/utils/text';
import useAttempt from 'shared/hooks/useAttemptNext';
import { getErrMessage } from 'shared/utils/errorType';
diff --git a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AutoDiscoverToggle.tsx b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AutoDiscoverToggle.tsx
index 204e30b3e79d1..790993b715957 100644
--- a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AutoDiscoverToggle.tsx
+++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AutoDiscoverToggle.tsx
@@ -19,7 +19,7 @@
import React from 'react';
import { Box, Toggle } from 'design';
-import { ToolTipInfo } from 'shared/components/ToolTip';
+import { ToolTipInfo } from 'design/ToolTip';
export function AutoDiscoverToggle({
wantAutoDiscover,
diff --git a/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx b/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx
index e73fc6dfc3e15..8cff0fc08a27d 100644
--- a/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx
+++ b/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx
@@ -31,7 +31,7 @@ import { FetchStatus } from 'design/DataTable/types';
import { Danger } from 'design/Alert';
import useAttempt from 'shared/hooks/useAttemptNext';
-import { ToolTipInfo } from 'shared/components/ToolTip';
+import { ToolTipInfo } from 'design/ToolTip';
import { getErrMessage } from 'shared/utils/errorType';
import { EksMeta, useDiscover } from 'teleport/Discover/useDiscover';
diff --git a/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
index 6a7245bacf798..5fa5f242fc019 100644
--- a/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
+++ b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
@@ -30,7 +30,7 @@ import {
import styled from 'styled-components';
import { Danger, Info } from 'design/Alert';
import TextEditor from 'shared/components/TextEditor';
-import { ToolTipInfo } from 'shared/components/ToolTip';
+import { ToolTipInfo } from 'design/ToolTip';
import FieldInput from 'shared/components/FieldInput';
import { Rule } from 'shared/components/Validation/rules';
import Validation, { Validator } from 'shared/components/Validation';
diff --git a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx
index 2cbb084ecd732..aa377197c5482 100644
--- a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx
+++ b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx
@@ -25,7 +25,7 @@ import { Danger } from 'design/Alert';
import { OutlineInfo } from 'design/Alert/Alert';
import { getErrMessage } from 'shared/utils/errorType';
-import { ToolTipInfo } from 'shared/components/ToolTip';
+import { ToolTipInfo } from 'design/ToolTip';
import useTeleport from 'teleport/useTeleport';
import cfg from 'teleport/config';
diff --git a/web/packages/teleport/src/Discover/Shared/Aws/ConfigureIamPerms.tsx b/web/packages/teleport/src/Discover/Shared/Aws/ConfigureIamPerms.tsx
index 4c491191152b8..b713235ee5a7a 100644
--- a/web/packages/teleport/src/Discover/Shared/Aws/ConfigureIamPerms.tsx
+++ b/web/packages/teleport/src/Discover/Shared/Aws/ConfigureIamPerms.tsx
@@ -21,7 +21,7 @@ import styled from 'styled-components';
import { Flex, Link, Box, H3 } from 'design';
import { assertUnreachable } from 'shared/utils/assertUnreachable';
import TextEditor from 'shared/components/TextEditor';
-import { ToolTipInfo } from 'shared/components/ToolTip';
+import { ToolTipInfo } from 'design/ToolTip';
import { P } from 'design/Text/Text';
diff --git a/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/ConfigureDiscoveryServiceDirections.tsx b/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/ConfigureDiscoveryServiceDirections.tsx
index 9017c990205f4..f5d98c03ca386 100644
--- a/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/ConfigureDiscoveryServiceDirections.tsx
+++ b/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/ConfigureDiscoveryServiceDirections.tsx
@@ -18,7 +18,7 @@
import { Box, Flex, Input, Text, Mark, H3, Subtitle3 } from 'design';
import styled from 'styled-components';
-import { ToolTipInfo } from 'shared/components/ToolTip';
+import { ToolTipInfo } from 'design/ToolTip';
import React from 'react';
diff --git a/web/packages/teleport/src/Discover/Shared/SecurityGroupPicker/SecurityGroupPicker.tsx b/web/packages/teleport/src/Discover/Shared/SecurityGroupPicker/SecurityGroupPicker.tsx
index 7b66604e54a51..3b986be833de4 100644
--- a/web/packages/teleport/src/Discover/Shared/SecurityGroupPicker/SecurityGroupPicker.tsx
+++ b/web/packages/teleport/src/Discover/Shared/SecurityGroupPicker/SecurityGroupPicker.tsx
@@ -23,7 +23,7 @@ import Table, { Cell } from 'design/DataTable';
import { Danger } from 'design/Alert';
import { CheckboxInput } from 'design/Checkbox';
import { FetchStatus } from 'design/DataTable/types';
-import { ToolTipInfo } from 'shared/components/ToolTip';
+import { ToolTipInfo } from 'design/ToolTip';
import { Attempt } from 'shared/hooks/useAttemptNext';
diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/ConfigureAwsOidcSummary.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/ConfigureAwsOidcSummary.tsx
index aecd67c00d114..1adace833d8d1 100644
--- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/ConfigureAwsOidcSummary.tsx
+++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/ConfigureAwsOidcSummary.tsx
@@ -20,7 +20,7 @@ import React from 'react';
import styled from 'styled-components';
import { Flex, Box, H3, Text } from 'design';
import TextEditor from 'shared/components/TextEditor';
-import { ToolTipInfo } from 'shared/components/ToolTip';
+import { ToolTipInfo } from 'design/ToolTip';
import useStickyClusterId from 'teleport/useStickyClusterId';
diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx
index a225196d65dfc..b316c5f3a1a56 100644
--- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx
+++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx
@@ -19,7 +19,7 @@
import React from 'react';
import { Text, Flex } from 'design';
import FieldInput from 'shared/components/FieldInput';
-import { ToolTipInfo } from 'shared/components/ToolTip';
+import { ToolTipInfo } from 'design/ToolTip';
export function S3BucketConfiguration({
s3Bucket,
diff --git a/web/packages/teleport/src/Integrations/IntegrationList.tsx b/web/packages/teleport/src/Integrations/IntegrationList.tsx
index dfd8a89159efa..4364de965e4c3 100644
--- a/web/packages/teleport/src/Integrations/IntegrationList.tsx
+++ b/web/packages/teleport/src/Integrations/IntegrationList.tsx
@@ -24,7 +24,7 @@ import { Link as InternalRouteLink } from 'react-router-dom';
import { Box, Flex } from 'design';
import Table, { Cell } from 'design/DataTable';
import { MenuButton, MenuItem } from 'shared/components/MenuAction';
-import { ToolTipInfo } from 'shared/components/ToolTip';
+import { ToolTipInfo } from 'design/ToolTip';
import { useAsync } from 'shared/hooks/useAsync';
import { ResourceIcon } from 'design/ResourceIcon';
import { saveOnDisk } from 'shared/utils/saveOnDisk';
diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx
index 8d95d16a691c4..614579d27c6f3 100644
--- a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx
+++ b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx
@@ -21,7 +21,7 @@ import { Link as InternalLink } from 'react-router-dom';
import { ButtonIcon, Flex, Label, Text } from 'design';
import { ArrowLeft } from 'design/Icon';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
import cfg from 'teleport/config';
import { getStatusAndLabel } from 'teleport/Integrations/helpers';
diff --git a/web/packages/teleport/src/JoinTokens/JoinTokens.tsx b/web/packages/teleport/src/JoinTokens/JoinTokens.tsx
index 46e9100feab8b..68ec5896346af 100644
--- a/web/packages/teleport/src/JoinTokens/JoinTokens.tsx
+++ b/web/packages/teleport/src/JoinTokens/JoinTokens.tsx
@@ -42,7 +42,7 @@ import Dialog, {
} from 'design/Dialog';
import { MenuButton } from 'shared/components/MenuAction';
import { Attempt, useAsync } from 'shared/hooks/useAsync';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
import { CopyButton } from 'shared/components/UnifiedResources/shared/CopyButton';
import { useTeleport } from 'teleport';
diff --git a/web/packages/teleport/src/JoinTokens/UpsertJoinTokenDialog.tsx b/web/packages/teleport/src/JoinTokens/UpsertJoinTokenDialog.tsx
index 6daef672649c6..7d17d7e11e769 100644
--- a/web/packages/teleport/src/JoinTokens/UpsertJoinTokenDialog.tsx
+++ b/web/packages/teleport/src/JoinTokens/UpsertJoinTokenDialog.tsx
@@ -29,7 +29,7 @@ import {
Alert,
} from 'design';
import styled from 'styled-components';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
import { Cross } from 'design/Icon';
import Validation from 'shared/components/Validation';
import FieldInput from 'shared/components/FieldInput';
diff --git a/web/packages/teleport/src/Navigation/Navigation.tsx b/web/packages/teleport/src/Navigation/Navigation.tsx
index 6450c575114bf..db95daad39759 100644
--- a/web/packages/teleport/src/Navigation/Navigation.tsx
+++ b/web/packages/teleport/src/Navigation/Navigation.tsx
@@ -21,7 +21,7 @@ import styled, { useTheme } from 'styled-components';
import { matchPath, useLocation, useHistory } from 'react-router';
import { Box, Text, Flex } from 'design';
-import { ToolTipInfo } from 'shared/components/ToolTip';
+import { ToolTipInfo } from 'design/ToolTip';
import cfg from 'teleport/config';
import {
diff --git a/web/packages/teleport/src/Navigation/SideNavigation/Section.tsx b/web/packages/teleport/src/Navigation/SideNavigation/Section.tsx
index 8217106d5fd20..495452b1452d8 100644
--- a/web/packages/teleport/src/Navigation/SideNavigation/Section.tsx
+++ b/web/packages/teleport/src/Navigation/SideNavigation/Section.tsx
@@ -23,7 +23,7 @@ import styled, { css, useTheme } from 'styled-components';
import { Box, ButtonIcon, Flex, P2, Text } from 'design';
import { Theme } from 'design/theme';
import { ArrowLineLeft } from 'design/Icon';
-import { HoverTooltip, ToolTipInfo } from 'shared/components/ToolTip';
+import { HoverTooltip, ToolTipInfo } from 'design/ToolTip';
import cfg from 'teleport/config';
diff --git a/web/packages/teleport/src/Notifications/Notifications.tsx b/web/packages/teleport/src/Notifications/Notifications.tsx
index ada3dd3761af1..ed31048b4fb14 100644
--- a/web/packages/teleport/src/Notifications/Notifications.tsx
+++ b/web/packages/teleport/src/Notifications/Notifications.tsx
@@ -24,7 +24,7 @@ import { Alert, Box, Flex, Indicator, Text } from 'design';
import { Notification as NotificationIcon, BellRinging } from 'design/Icon';
import Logger from 'shared/libs/logger';
import { useRefClickOutside } from 'shared/hooks/useRefClickOutside';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
import {
useInfiniteScroll,
diff --git a/web/packages/teleport/src/Roles/RoleEditor/EditorHeader.tsx b/web/packages/teleport/src/Roles/RoleEditor/EditorHeader.tsx
index 37059aee38594..b822474af3e34 100644
--- a/web/packages/teleport/src/Roles/RoleEditor/EditorHeader.tsx
+++ b/web/packages/teleport/src/Roles/RoleEditor/EditorHeader.tsx
@@ -18,7 +18,7 @@
import React from 'react';
import { Flex, ButtonText, H2 } from 'design';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
import { Trash } from 'design/Icon';
import useTeleport from 'teleport/useTeleport';
diff --git a/web/packages/teleport/src/Roles/RoleEditor/Shared.tsx b/web/packages/teleport/src/Roles/RoleEditor/Shared.tsx
index e6cece6752920..41af339cd2f01 100644
--- a/web/packages/teleport/src/Roles/RoleEditor/Shared.tsx
+++ b/web/packages/teleport/src/Roles/RoleEditor/Shared.tsx
@@ -17,7 +17,7 @@
*/
import { Box, ButtonPrimary, ButtonSecondary, Flex } from 'design';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
import useTeleport from 'teleport/useTeleport';
diff --git a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
index 01789e1f2f837..178c5b838631f 100644
--- a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
+++ b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
@@ -31,7 +31,7 @@ import FieldInput from 'shared/components/FieldInput';
import Validation, { Validator } from 'shared/components/Validation';
import { requiredField } from 'shared/components/Validation/rules';
import * as Icon from 'design/Icon';
-import { HoverTooltip, ToolTipInfo } from 'shared/components/ToolTip';
+import { HoverTooltip, ToolTipInfo } from 'design/ToolTip';
import styled, { useTheme } from 'styled-components';
import { MenuButton, MenuItem } from 'shared/components/MenuAction';
diff --git a/web/packages/teleport/src/TopBar/TopBar.tsx b/web/packages/teleport/src/TopBar/TopBar.tsx
index 20c828f7c5510..6ab1e81da3ff9 100644
--- a/web/packages/teleport/src/TopBar/TopBar.tsx
+++ b/web/packages/teleport/src/TopBar/TopBar.tsx
@@ -23,7 +23,7 @@ import { Flex, Image, Text, TopNav } from 'design';
import { matchPath, useHistory } from 'react-router';
import { Theme } from 'design/theme/themes/types';
import { ArrowLeft, Download, Server, SlidersVertical } from 'design/Icon';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
import useTeleport from 'teleport/useTeleport';
import { UserMenuNav } from 'teleport/components/UserMenuNav';
diff --git a/web/packages/teleport/src/TopBar/TopBarSideNav.tsx b/web/packages/teleport/src/TopBar/TopBarSideNav.tsx
index c787f984fa763..9d578eb1011a9 100644
--- a/web/packages/teleport/src/TopBar/TopBarSideNav.tsx
+++ b/web/packages/teleport/src/TopBar/TopBarSideNav.tsx
@@ -22,7 +22,7 @@ import { Link } from 'react-router-dom';
import { Flex, Image, TopNav } from 'design';
import { matchPath, useHistory } from 'react-router';
import { Theme } from 'design/theme/themes/types';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
import useTeleport from 'teleport/useTeleport';
import { UserMenuNav } from 'teleport/components/UserMenuNav';
diff --git a/web/packages/teleport/src/components/ExternalAuditStorageCta/ExternalAuditStorageCta.tsx b/web/packages/teleport/src/components/ExternalAuditStorageCta/ExternalAuditStorageCta.tsx
index c8bd62b70cb80..ff2add5d944aa 100644
--- a/web/packages/teleport/src/components/ExternalAuditStorageCta/ExternalAuditStorageCta.tsx
+++ b/web/packages/teleport/src/components/ExternalAuditStorageCta/ExternalAuditStorageCta.tsx
@@ -25,7 +25,7 @@ import { ButtonPrimary, ButtonSecondary } from 'design/Button';
import Flex from 'design/Flex';
import Text from 'design/Text';
-import { HoverTooltip } from 'shared/components/ToolTip';
+import { HoverTooltip } from 'design/ToolTip';
import cfg from 'teleport/config';
import { IntegrationKind } from 'teleport/services/integrations';
From 5752c58f3c9072510e87a5cca2d48cd627f453ef Mon Sep 17 00:00:00 2001
From: Bartosz Leper
Date: Thu, 28 Nov 2024 13:51:01 +0100
Subject: [PATCH 03/14] SlideTabs improvements
Add support for tooltips and status icons
---
.../design/src/SlideTabs/SlideTabs.story.tsx | 64 ++++++++--
.../design/src/SlideTabs/SlideTabs.test.tsx | 9 +-
.../design/src/SlideTabs/SlideTabs.tsx | 111 ++++++++++++++----
3 files changed, 148 insertions(+), 36 deletions(-)
diff --git a/web/packages/design/src/SlideTabs/SlideTabs.story.tsx b/web/packages/design/src/SlideTabs/SlideTabs.story.tsx
index fa2ce7dbafa2b..f9e532c2845e2 100644
--- a/web/packages/design/src/SlideTabs/SlideTabs.story.tsx
+++ b/web/packages/design/src/SlideTabs/SlideTabs.story.tsx
@@ -18,6 +18,8 @@
import React, { useState } from 'react';
+import { useTheme } from 'styled-components';
+
import * as Icon from 'design/Icon';
import Flex from 'design/Flex';
@@ -120,9 +122,27 @@ export const Small = () => {
{
);
};
-export const LoadingTab = () => {
+export const StatusIcons = () => {
+ const theme = useTheme();
+ const [activeIndex, setActiveIndex] = useState(0);
+ const tabs = [
+ {
+ key: 'warning',
+ title: 'warning',
+ statusIcon: Icon.Warning,
+ statusIconColor: theme.colors.interactive.solid.alert.default,
+ },
+ {
+ key: 'danger',
+ title: 'danger',
+ statusIcon: Icon.WarningCircle,
+ statusIconColor: theme.colors.interactive.solid.danger.default,
+ },
+ { key: 'neutral', title: 'neutral', statusIcon: Icon.Bubble },
+ ];
return (
- null}
- activeIndex={1}
- isProcessing={true}
- />
+
+
+
+
);
};
diff --git a/web/packages/design/src/SlideTabs/SlideTabs.test.tsx b/web/packages/design/src/SlideTabs/SlideTabs.test.tsx
index 4636655d25700..fe4137f34b28b 100644
--- a/web/packages/design/src/SlideTabs/SlideTabs.test.tsx
+++ b/web/packages/design/src/SlideTabs/SlideTabs.test.tsx
@@ -21,6 +21,8 @@ import { screen } from '@testing-library/react';
import { render, userEvent } from 'design/utils/testing';
+import * as Icon from 'design/Icon';
+
import { SlideTabs, SlideTabsProps } from './SlideTabs';
describe('design/SlideTabs', () => {
@@ -87,7 +89,12 @@ describe('design/SlideTabs', () => {
);
diff --git a/web/packages/design/src/SlideTabs/SlideTabs.tsx b/web/packages/design/src/SlideTabs/SlideTabs.tsx
index 6b92c5803fdf0..6ceb912232fc5 100644
--- a/web/packages/design/src/SlideTabs/SlideTabs.tsx
+++ b/web/packages/design/src/SlideTabs/SlideTabs.tsx
@@ -17,10 +17,12 @@
*/
import React, { useEffect, useRef } from 'react';
-import styled from 'styled-components';
+import styled, { useTheme } from 'styled-components';
import { Flex, Indicator } from 'design';
import { IconProps } from 'design/Icon/Icon';
+import { HoverTooltip } from 'design/ToolTip';
+import { Position } from 'design/Popover/Popover';
export function SlideTabs({
appearance = 'square',
@@ -32,6 +34,7 @@ export function SlideTabs({
disabled = false,
fitContent = false,
}: SlideTabsProps) {
+ const theme = useTheme();
const activeTab = useRef(null);
const tabContainer = useRef(null);
@@ -75,6 +78,11 @@ export function SlideTabs({
icon: Icon,
ariaLabel,
controls,
+ tooltipContent,
+ tooltipPosition,
+ statusIcon,
+ statusIconColor = theme.colors.text.main,
+ statusIconColorActive = theme.colors.text.primaryInverse,
} = toFullTabSpec(tabSpec, tabIndex);
let onClick = undefined;
@@ -86,32 +94,44 @@ export function SlideTabs({
}
return (
-
- {/* We need a separate tab content component, since the spinner,
- when displayed, shouldn't take up space to prevent layout
- jumping. TabContent serves as a positioning anchor whose left
- edge is the left edge of the content (not the tab button,
- which can be much wider). */}
-
- {selected && isProcessing && }
- {Icon && }
- {title}
-
-
+
+ {/* We need a separate tab content component, since the status
+ icon, when displayed, shouldn't take up space to prevent
+ layout jumping. TabContent serves as a positioning anchor
+ whose left edge is the left edge of the content (not the tab
+ button, which can be much wider). */}
+
+
+
+
+ {Icon && }
+ {title}
+
+
+
);
})}
{/* The tab slider is positioned absolutely and appears below the
@@ -190,6 +210,18 @@ type FullTabSpec = TabContentSpec & {
* attribute set to "tabpanel".
*/
controls?: string;
+ tooltipContent?: React.ReactNode;
+ tooltipPosition?: Position;
+ /**
+ * An icon that will be displayed on the side. The layout will stay the same
+ * whether the icon is there or not. If `isProcessing` prop is set to `true`,
+ * the icon for an active tab is replaced by a spinner.
+ */
+ statusIcon?: React.ComponentType;
+ /** Color of the status icon. */
+ statusIconColor?: string;
+ /** Color of the status icon on an active tab. */
+ statusIconColorActive?: string;
};
/**
@@ -220,6 +252,32 @@ function toFullTabSpec(spec: TabSpec, index: number): FullTabSpec {
};
}
+function StatusIcon({
+ spinner,
+ icon: Icon,
+ size,
+ color,
+}: {
+ spinner: boolean;
+ icon: React.ComponentType | undefined;
+ size: Size;
+ color: string | undefined;
+}) {
+ if (spinner) {
+ return ;
+ }
+ if (Icon) {
+ return (
+
+ );
+ }
+ return null;
+}
+
const TabSliderInner = styled.div<{ appearance: Appearance }>`
height: 100%;
background-color: ${({ theme }) => theme.colors.brand};
@@ -343,6 +401,9 @@ const TabList = styled.div<{ itemCount: number }>`
const Spinner = styled(Indicator)`
color: ${p => p.theme.colors.levels.deep};
+`;
+
+const StatusIconContainer = styled.div`
position: absolute;
left: -${p => p.theme.space[5]}px;
`;
From 96732fce9121bafa99c12ad57db3d3d8a2a5687d Mon Sep 17 00:00:00 2001
From: Bartosz Leper
Date: Thu, 28 Nov 2024 14:24:06 +0100
Subject: [PATCH 04/14] Role editor: hierarchical validation and tabs
---
.../src/Roles/RoleEditor/EditorHeader.tsx | 32 +-
.../src/Roles/RoleEditor/EditorTabs.tsx | 36 +-
.../src/Roles/RoleEditor/RoleEditor.test.tsx | 60 ++-
.../src/Roles/RoleEditor/RoleEditor.tsx | 67 +--
.../Roles/RoleEditor/StandardEditor.test.tsx | 403 ++++++++++++------
.../src/Roles/RoleEditor/StandardEditor.tsx | 259 ++++++++---
.../src/Roles/RoleEditor/standardmodel.ts | 185 ++++++--
web/packages/teleport/src/Roles/Roles.tsx | 4 +-
.../teleport/src/services/resources/types.ts | 41 +-
9 files changed, 799 insertions(+), 288 deletions(-)
diff --git a/web/packages/teleport/src/Roles/RoleEditor/EditorHeader.tsx b/web/packages/teleport/src/Roles/RoleEditor/EditorHeader.tsx
index b822474af3e34..0b3adbb7f5764 100644
--- a/web/packages/teleport/src/Roles/RoleEditor/EditorHeader.tsx
+++ b/web/packages/teleport/src/Roles/RoleEditor/EditorHeader.tsx
@@ -17,20 +17,32 @@
*/
import React from 'react';
-import { Flex, ButtonText, H2 } from 'design';
+import { Flex, ButtonText, H2, Indicator, Box } from 'design';
import { HoverTooltip } from 'design/ToolTip';
import { Trash } from 'design/Icon';
import useTeleport from 'teleport/useTeleport';
import { Role } from 'teleport/services/resources';
+import { EditorTab, EditorTabs } from './EditorTabs';
+
/** Renders a header button with role name and delete button. */
export const EditorHeader = ({
role = null,
onDelete,
+ selectedEditorTab,
+ onEditorTabChange,
+ isProcessing,
+ standardEditorId,
+ yamlEditorId,
}: {
- onDelete?(): void;
role?: Role;
+ onDelete(): void;
+ selectedEditorTab: EditorTab;
+ onEditorTabChange(t: EditorTab): void;
+ isProcessing: boolean;
+ standardEditorId: string;
+ yamlEditorId: string;
}) => {
const ctx = useTeleport();
const isCreating = !role;
@@ -38,8 +50,20 @@ export const EditorHeader = ({
const hasDeleteAccess = ctx.storeUser.getRoleAccess().remove;
return (
-
- {isCreating ? 'Create a New Role' : role?.metadata.name}
+
+
+ {isCreating ? 'Create a New Role' : role?.metadata.name}
+
+
+ {isProcessing && }
+
+
{!isCreating && (
{
+ const standardLabel = 'Switch to standard editor';
+ const yamlLabel = 'Switch to YAML editor';
return (
);
};
diff --git a/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.test.tsx b/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.test.tsx
index 235a0d83782a6..1376c078e80ff 100644
--- a/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.test.tsx
+++ b/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.test.tsx
@@ -75,10 +75,7 @@ afterEach(() => {
test('rendering and switching tabs for new role', async () => {
render( );
- expect(screen.getByRole('tab', { name: 'Standard' })).toHaveAttribute(
- 'aria-selected',
- 'true'
- );
+ expect(getStandardEditorTab()).toHaveAttribute('aria-selected', 'true');
expect(
screen.queryByRole('button', { name: /Reset to Standard Settings/i })
).not.toBeInTheDocument();
@@ -86,7 +83,7 @@ test('rendering and switching tabs for new role', async () => {
expect(screen.getByLabelText('Description')).toHaveValue('');
expect(screen.getByRole('button', { name: 'Create Role' })).toBeEnabled();
- await user.click(screen.getByRole('tab', { name: 'YAML' }));
+ await user.click(getYamlEditorTab());
expect(fromFauxYaml(await getTextEditorContents())).toEqual(
withDefaults({
kind: 'role',
@@ -103,7 +100,7 @@ test('rendering and switching tabs for new role', async () => {
);
expect(screen.getByRole('button', { name: 'Create Role' })).toBeEnabled();
- await user.click(screen.getByRole('tab', { name: 'Standard' }));
+ await user.click(getStandardEditorTab());
await screen.findByLabelText('Role Name');
expect(
screen.queryByRole('button', { name: /Reset to Standard Settings/i })
@@ -127,14 +124,11 @@ test('rendering and switching tabs for a non-standard role', async () => {
originalRole={{ object: originalRole, yaml: originalYaml }}
/>
);
- expect(screen.getByRole('tab', { name: 'YAML' })).toHaveAttribute(
- 'aria-selected',
- 'true'
- );
+ expect(getYamlEditorTab()).toHaveAttribute('aria-selected', 'true');
expect(fromFauxYaml(await getTextEditorContents())).toEqual(originalRole);
expect(screen.getByRole('button', { name: 'Update Role' })).toBeDisabled();
- await user.click(screen.getByRole('tab', { name: 'Standard' }));
+ await user.click(getStandardEditorTab());
expect(
screen.getByRole('button', { name: 'Reset to Standard Settings' })
).toBeVisible();
@@ -142,12 +136,12 @@ test('rendering and switching tabs for a non-standard role', async () => {
expect(screen.getByLabelText('Description')).toHaveValue('');
expect(screen.getByRole('button', { name: 'Update Role' })).toBeDisabled();
- await user.click(screen.getByRole('tab', { name: 'YAML' }));
+ await user.click(getYamlEditorTab());
expect(fromFauxYaml(await getTextEditorContents())).toEqual(originalRole);
expect(screen.getByRole('button', { name: 'Update Role' })).toBeDisabled();
// Switch once again, reset to standard
- await user.click(screen.getByRole('tab', { name: 'Standard' }));
+ await user.click(getStandardEditorTab());
expect(screen.getByRole('button', { name: 'Update Role' })).toBeDisabled();
await user.click(
screen.getByRole('button', { name: 'Reset to Standard Settings' })
@@ -155,13 +149,35 @@ test('rendering and switching tabs for a non-standard role', async () => {
expect(screen.getByRole('button', { name: 'Update Role' })).toBeEnabled();
await user.type(screen.getByLabelText('Description'), 'some description');
- await user.click(screen.getByRole('tab', { name: 'YAML' }));
+ await user.click(getYamlEditorTab());
const editorContents = fromFauxYaml(await getTextEditorContents());
expect(editorContents.metadata.description).toBe('some description');
expect(editorContents.spec.deny).toEqual({});
expect(screen.getByRole('button', { name: 'Update Role' })).toBeEnabled();
});
+test('no double conversions when clicking already active tabs', async () => {
+ render( );
+ await user.click(getYamlEditorTab());
+ await user.click(getStandardEditorTab());
+ await user.type(screen.getByLabelText('Role Name'), '_2');
+ await user.click(getStandardEditorTab());
+ expect(screen.getByLabelText('Role Name')).toHaveValue('new_role_name_2');
+
+ await user.click(getYamlEditorTab());
+ await user.clear(await findTextEditor());
+ await user.type(
+ await findTextEditor(),
+ // Note: this is actually correct JSON syntax; the testing library uses
+ // braces for special keys, so we need to use double opening braces.
+ '{{"kind":"role", metadata:{{"name":"new_role_name_3"}}'
+ );
+ await user.click(getYamlEditorTab());
+ expect(await getTextEditorContents()).toBe(
+ '{"kind":"role", metadata:{"name":"new_role_name_3"}}'
+ );
+});
+
test('canceling standard editor', async () => {
const onCancel = jest.fn();
render( );
@@ -175,7 +191,7 @@ test('canceling standard editor', async () => {
test('canceling yaml editor', async () => {
const onCancel = jest.fn();
render( );
- await user.click(screen.getByRole('tab', { name: 'YAML' }));
+ await user.click(getYamlEditorTab());
await user.click(screen.getByRole('button', { name: 'Cancel' }));
expect(onCancel).toHaveBeenCalled();
expect(userEventService.captureUserEvent).toHaveBeenCalledWith({
@@ -222,7 +238,7 @@ test('saving a new role after editing as YAML', async () => {
render( );
expect(screen.getByRole('button', { name: 'Create Role' })).toBeEnabled();
- await user.click(screen.getByRole('tab', { name: 'YAML' }));
+ await user.click(getYamlEditorTab());
await user.clear(await findTextEditor());
await user.type(await findTextEditor(), '{{"foo":"bar"}');
await user.click(screen.getByRole('button', { name: 'Create Role' }));
@@ -247,7 +263,7 @@ test('error while yamlifying', async () => {
.spyOn(yamlService, 'stringify')
.mockRejectedValue(new Error('me no speak yaml'));
render( );
- await user.click(screen.getByRole('tab', { name: 'YAML' }));
+ await user.click(getYamlEditorTab());
expect(screen.getByText('me no speak yaml')).toBeVisible();
});
@@ -256,8 +272,8 @@ test('error while parsing', async () => {
.spyOn(yamlService, 'parse')
.mockRejectedValue(new Error('me no speak yaml'));
render( );
- await user.click(screen.getByRole('tab', { name: 'YAML' }));
- await user.click(screen.getByRole('tab', { name: 'Standard' }));
+ await user.click(getYamlEditorTab());
+ await user.click(getStandardEditorTab());
expect(screen.getByText('me no speak yaml')).toBeVisible();
});
@@ -280,6 +296,12 @@ const TestRoleEditor = (props: RoleEditorProps) => {
);
};
+const getStandardEditorTab = () =>
+ screen.getByRole('tab', { name: 'Switch to standard editor' });
+
+const getYamlEditorTab = () =>
+ screen.getByRole('tab', { name: 'Switch to YAML editor' });
+
const findTextEditor = async () =>
within(await screen.findByTestId('text-editor-container')).getByRole(
'textbox'
diff --git a/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx b/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx
index cadef43e0ce26..291db0397b80a 100644
--- a/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx
+++ b/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx
@@ -16,8 +16,8 @@
* along with this program. If not, see .
*/
-import { Alert, Box, Flex } from 'design';
-import React, { useState } from 'react';
+import { Alert, Flex } from 'design';
+import React, { useId, useState } from 'react';
import { useAsync } from 'shared/hooks/useAsync';
import { Role, RoleWithYaml } from 'teleport/services/resources';
@@ -32,7 +32,7 @@ import {
roleToRoleEditorModel as roleToRoleEditorModel,
} from './standardmodel';
import { YamlEditorModel } from './yamlmodel';
-import { EditorTab, EditorTabs } from './EditorTabs';
+import { EditorTab } from './EditorTabs';
import { EditorHeader } from './EditorHeader';
import { StandardEditor } from './StandardEditor';
import { YamlEditor } from './YamlEditor';
@@ -59,6 +59,10 @@ export const RoleEditor = ({
onSave,
onDelete,
}: RoleEditorProps) => {
+ const idPrefix = useId();
+ const standardEditorId = `${idPrefix}-standard`;
+ const yamlEditorId = `${idPrefix}-yaml`;
+
const [standardModel, setStandardModel] = useState(
() => {
const role = originalRole?.object ?? newRole();
@@ -114,6 +118,10 @@ export const RoleEditor = ({
saveAttempt.status === 'processing';
async function onTabChange(activeIndex: EditorTab) {
+ // The code below is not idempotent, so we need to protect ourselves from
+ // an accidental model replacement.
+ if (activeIndex === selectedEditorTab) return;
+
switch (activeIndex) {
case EditorTab.Standard: {
if (!yamlModel.content) {
@@ -160,7 +168,15 @@ export const RoleEditor = ({
return (
-
+
{saveAttempt.status === 'error' && (
{saveAttempt.statusText}
@@ -176,32 +192,29 @@ export const RoleEditor = ({
{yamlifyAttempt.statusText}
)}
-
-
-
{selectedEditorTab === EditorTab.Standard && (
- handleSave({ object })}
- onCancel={handleCancel}
- standardEditorModel={standardModel}
- isProcessing={isProcessing}
- onChange={setStandardModel}
- />
+
+ handleSave({ object })}
+ onCancel={handleCancel}
+ standardEditorModel={standardModel}
+ isProcessing={isProcessing}
+ onChange={setStandardModel}
+ />
+
)}
{selectedEditorTab === EditorTab.Yaml && (
- void (await handleSave({ yaml }))}
- isProcessing={isProcessing}
- onCancel={handleCancel}
- originalRole={originalRole}
- />
+
+ void (await handleSave({ yaml }))}
+ isProcessing={isProcessing}
+ onCancel={handleCancel}
+ originalRole={originalRole}
+ />
+
)}
);
diff --git a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx
index 4497cbea441d7..564c7e0df7499 100644
--- a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx
+++ b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx
@@ -19,8 +19,8 @@
import { render, screen, userEvent } from 'design/utils/testing';
import React, { useState } from 'react';
-import { within } from '@testing-library/react';
-import Validation from 'shared/components/Validation';
+import { act, within } from '@testing-library/react';
+import Validation, { Validator } from 'shared/components/Validation';
import selectEvent from 'react-select-event';
import TeleportContextProvider from 'teleport/TeleportContextProvider';
@@ -36,6 +36,7 @@ import {
roleToRoleEditorModel,
ServerAccessSpec,
StandardEditorModel,
+ validateAccessSpec,
WindowsDesktopAccessSpec,
} from './standardmodel';
import {
@@ -72,6 +73,8 @@ test('adding and removing sections', async () => {
const user = userEvent.setup();
render( );
expect(getAllSectionNames()).toEqual(['Role Metadata']);
+ await user.click(screen.getByRole('tab', { name: 'Resources' }));
+ expect(getAllSectionNames()).toEqual([]);
await user.click(
screen.getByRole('button', { name: 'Add New Specifications' })
@@ -85,7 +88,7 @@ test('adding and removing sections', async () => {
]);
await user.click(screen.getByRole('menuitem', { name: 'Servers' }));
- expect(getAllSectionNames()).toEqual(['Role Metadata', 'Servers']);
+ expect(getAllSectionNames()).toEqual(['Servers']);
await user.click(
screen.getByRole('button', { name: 'Add New Specifications' })
@@ -98,25 +101,21 @@ test('adding and removing sections', async () => {
]);
await user.click(screen.getByRole('menuitem', { name: 'Kubernetes' }));
- expect(getAllSectionNames()).toEqual([
- 'Role Metadata',
- 'Servers',
- 'Kubernetes',
- ]);
+ expect(getAllSectionNames()).toEqual(['Servers', 'Kubernetes']);
await user.click(
within(getSectionByName('Servers')).getByRole('button', {
name: 'Remove section',
})
);
- expect(getAllSectionNames()).toEqual(['Role Metadata', 'Kubernetes']);
+ expect(getAllSectionNames()).toEqual(['Kubernetes']);
await user.click(
within(getSectionByName('Kubernetes')).getByRole('button', {
name: 'Remove section',
})
);
- expect(getAllSectionNames()).toEqual(['Role Metadata']);
+ expect(getAllSectionNames()).toEqual([]);
});
test('collapsed sections still apply validation', async () => {
@@ -137,6 +136,24 @@ test('collapsed sections still apply validation', async () => {
expect(onSave).toHaveBeenCalled();
});
+test('invisible tabs still apply validation', async () => {
+ const user = userEvent.setup();
+ const onSave = jest.fn();
+ render( );
+ // Intentionally cause a validation error.
+ await user.clear(screen.getByLabelText('Role Name'));
+ // Switch to a different tab.
+ await user.click(screen.getByRole('tab', { name: 'Resources' }));
+ await user.click(screen.getByRole('button', { name: 'Create Role' }));
+ expect(onSave).not.toHaveBeenCalled();
+
+ // Switch back, make it valid.
+ await user.click(screen.getByRole('tab', { name: 'Overview' }));
+ await user.type(screen.getByLabelText('Role Name'), 'foo');
+ await user.click(screen.getByRole('button', { name: 'Create Role' }));
+ expect(onSave).toHaveBeenCalled();
+});
+
const getAllMenuItemNames = () =>
screen.queryAllByRole('menuitem').map(m => m.textContent);
@@ -152,67 +169,105 @@ const StatefulSection = ({
defaultValue,
component: Component,
onChange,
+ validatorRef,
}: {
defaultValue: S;
- component: React.ComponentType>;
+ component: React.ComponentType>;
onChange(spec: S): void;
+ validatorRef?(v: Validator): void;
}) => {
const [model, setModel] = useState(defaultValue);
+ const validation = validateAccessSpec(model);
return (
- {
- setModel(spec);
- onChange(spec);
- }}
- />
+ {({ validator }) => {
+ validatorRef?.(validator);
+ return (
+ {
+ setModel(spec);
+ onChange(spec);
+ }}
+ />
+ );
+ }}
);
};
-test('ServerAccessSpecSection', async () => {
- const user = userEvent.setup();
- const onChange = jest.fn();
- render(
-
- component={ServerAccessSpecSection}
- defaultValue={newAccessSpec('node')}
- onChange={onChange}
- />
- );
- await user.click(screen.getByRole('button', { name: 'Add a Label' }));
- await user.type(screen.getByPlaceholderText('label key'), 'some-key');
- await user.type(screen.getByPlaceholderText('label value'), 'some-value');
- await selectEvent.create(screen.getByLabelText('Logins'), 'root', {
- createOptionText: 'Login: root',
- });
- await selectEvent.create(screen.getByLabelText('Logins'), 'some-user', {
- createOptionText: 'Login: some-user',
+describe('ServerAccessSpecSection', () => {
+ const setup = () => {
+ const onChange = jest.fn();
+ let validator: Validator;
+ render(
+
+ component={ServerAccessSpecSection}
+ defaultValue={newAccessSpec('node')}
+ onChange={onChange}
+ validatorRef={v => {
+ validator = v;
+ }}
+ />
+ );
+ return { user: userEvent.setup(), onChange, validator };
+ };
+
+ test('editing', async () => {
+ const { user, onChange } = setup();
+ await user.click(screen.getByRole('button', { name: 'Add a Label' }));
+ await user.type(screen.getByPlaceholderText('label key'), 'some-key');
+ await user.type(screen.getByPlaceholderText('label value'), 'some-value');
+ await selectEvent.create(screen.getByLabelText('Logins'), 'root', {
+ createOptionText: 'Login: root',
+ });
+ await selectEvent.create(screen.getByLabelText('Logins'), 'some-user', {
+ createOptionText: 'Login: some-user',
+ });
+
+ expect(onChange).toHaveBeenLastCalledWith({
+ kind: 'node',
+ labels: [{ name: 'some-key', value: 'some-value' }],
+ logins: [
+ expect.objectContaining({ label: 'root', value: 'root' }),
+ expect.objectContaining({ label: 'some-user', value: 'some-user' }),
+ ],
+ } as ServerAccessSpec);
});
- expect(onChange).toHaveBeenLastCalledWith({
- kind: 'node',
- labels: [{ name: 'some-key', value: 'some-value' }],
- logins: [
- expect.objectContaining({ label: 'root', value: 'root' }),
- expect.objectContaining({ label: 'some-user', value: 'some-user' }),
- ],
- } as ServerAccessSpec);
+ test('validation', async () => {
+ const { user, validator } = setup();
+ await user.click(screen.getByRole('button', { name: 'Add a Label' }));
+ await selectEvent.create(screen.getByLabelText('Logins'), '*', {
+ createOptionText: 'Login: *',
+ });
+ act(() => validator.validate());
+ expect(
+ screen.getByPlaceholderText('label key')
+ ).toHaveAccessibleDescription('required');
+ expect(
+ screen.getByText('Wildcard is not allowed in logins')
+ ).toBeInTheDocument();
+ });
});
describe('KubernetesAccessSpecSection', () => {
const setup = () => {
const onChange = jest.fn();
+ let validator: Validator;
render(
component={KubernetesAccessSpecSection}
defaultValue={newAccessSpec('kube_cluster')}
onChange={onChange}
+ validatorRef={v => {
+ validator = v;
+ }}
/>
);
- return { user: userEvent.setup(), onChange };
+ return { user: userEvent.setup(), onChange, validator };
};
test('editing the spec', async () => {
@@ -319,105 +374,199 @@ describe('KubernetesAccessSpecSection', () => {
expect.objectContaining({ resources: [] })
);
});
+
+ test('validation', async () => {
+ const { user, validator } = setup();
+ await user.click(screen.getByRole('button', { name: 'Add a Label' }));
+ await user.click(screen.getByRole('button', { name: 'Add a Resource' }));
+ await user.clear(screen.getByLabelText('Name'));
+ await user.clear(screen.getByLabelText('Namespace'));
+ act(() => validator.validate());
+ expect(
+ screen.getByPlaceholderText('label key')
+ ).toHaveAccessibleDescription('required');
+ expect(screen.getByLabelText('Name')).toHaveAccessibleDescription(
+ 'Resource name is required, use "*" for any resource'
+ );
+ expect(screen.getByLabelText('Namespace')).toHaveAccessibleDescription(
+ 'Namespace is required for resources of this kind'
+ );
+ });
});
-test('AppAccessSpecSection', async () => {
- const user = userEvent.setup();
- const onChange = jest.fn();
- render(
-
- component={AppAccessSpecSection}
- defaultValue={newAccessSpec('app')}
- onChange={onChange}
- />
- );
+describe('AppAccessSpecSection', () => {
+ const setup = () => {
+ const onChange = jest.fn();
+ let validator: Validator;
+ render(
+
+ component={AppAccessSpecSection}
+ defaultValue={newAccessSpec('app')}
+ onChange={onChange}
+ validatorRef={v => {
+ validator = v;
+ }}
+ />
+ );
+ return { user: userEvent.setup(), onChange, validator };
+ };
- await user.click(screen.getByRole('button', { name: 'Add a Label' }));
- await user.type(screen.getByPlaceholderText('label key'), 'env');
- await user.type(screen.getByPlaceholderText('label value'), 'prod');
- await user.type(
+ const awsRoleArn = () =>
within(screen.getByRole('group', { name: 'AWS Role ARNs' })).getByRole(
'textbox'
- ),
- 'arn:aws:iam::123456789012:role/admin'
- );
- await user.type(
+ );
+ const azureIdentity = () =>
within(screen.getByRole('group', { name: 'Azure Identities' })).getByRole(
'textbox'
- ),
- '/subscriptions/1020304050607-cafe-8090-a0b0c0d0e0f0/resourceGroups/example-resource-group/providers/Microsoft.ManagedIdentity/userAssignedIdentities/admin'
- );
- await user.type(
+ );
+ const gcpServiceAccount = () =>
within(
screen.getByRole('group', { name: 'GCP Service Accounts' })
- ).getByRole('textbox'),
- 'admin@some-project.iam.gserviceaccount.com'
- );
- expect(onChange).toHaveBeenLastCalledWith({
- kind: 'app',
- labels: [{ name: 'env', value: 'prod' }],
- awsRoleARNs: ['arn:aws:iam::123456789012:role/admin'],
- azureIdentities: [
- '/subscriptions/1020304050607-cafe-8090-a0b0c0d0e0f0/resourceGroups/example-resource-group/providers/Microsoft.ManagedIdentity/userAssignedIdentities/admin',
- ],
- gcpServiceAccounts: ['admin@some-project.iam.gserviceaccount.com'],
- } as AppAccessSpec);
-});
+ ).getByRole('textbox');
-test('DatabaseAccessSpecSection', async () => {
- const user = userEvent.setup();
- const onChange = jest.fn();
- render(
-
- component={DatabaseAccessSpecSection}
- defaultValue={newAccessSpec('db')}
- onChange={onChange}
- />
- );
+ test('editing', async () => {
+ const { user, onChange } = setup();
+ await user.click(screen.getByRole('button', { name: 'Add a Label' }));
+ await user.type(screen.getByPlaceholderText('label key'), 'env');
+ await user.type(screen.getByPlaceholderText('label value'), 'prod');
+ await user.type(awsRoleArn(), 'arn:aws:iam::123456789012:role/admin');
+ await user.type(
+ azureIdentity(),
+ '/subscriptions/1020304050607-cafe-8090-a0b0c0d0e0f0/resourceGroups/example-resource-group/providers/Microsoft.ManagedIdentity/userAssignedIdentities/admin'
+ );
+ await user.type(
+ gcpServiceAccount(),
+ 'admin@some-project.iam.gserviceaccount.com'
+ );
+ expect(onChange).toHaveBeenLastCalledWith({
+ kind: 'app',
+ labels: [{ name: 'env', value: 'prod' }],
+ awsRoleARNs: ['arn:aws:iam::123456789012:role/admin'],
+ azureIdentities: [
+ '/subscriptions/1020304050607-cafe-8090-a0b0c0d0e0f0/resourceGroups/example-resource-group/providers/Microsoft.ManagedIdentity/userAssignedIdentities/admin',
+ ],
+ gcpServiceAccounts: ['admin@some-project.iam.gserviceaccount.com'],
+ } as AppAccessSpec);
+ });
- await user.click(screen.getByRole('button', { name: 'Add a Label' }));
- await user.type(screen.getByPlaceholderText('label key'), 'env');
- await user.type(screen.getByPlaceholderText('label value'), 'prod');
- await selectEvent.create(screen.getByLabelText('Database Names'), 'stuff', {
- createOptionText: 'Database Name: stuff',
+ test('validation', async () => {
+ const { user, validator } = setup();
+ await user.click(screen.getByRole('button', { name: 'Add a Label' }));
+ await user.type(awsRoleArn(), '*');
+ await user.type(azureIdentity(), '*');
+ await user.type(gcpServiceAccount(), '*');
+ act(() => validator.validate());
+ expect(
+ screen.getByPlaceholderText('label key')
+ ).toHaveAccessibleDescription('required');
+ expect(awsRoleArn()).toHaveAccessibleDescription(
+ 'Wildcard is not allowed in AWS role ARNs'
+ );
+ expect(azureIdentity()).toHaveAccessibleDescription(
+ 'Wildcard is not allowed in Azure identities'
+ );
+ expect(gcpServiceAccount()).toHaveAccessibleDescription(
+ 'Wildcard is not allowed in GCP service accounts'
+ );
});
- await selectEvent.create(screen.getByLabelText('Database Users'), 'mary', {
- createOptionText: 'Database User: mary',
+});
+
+describe('DatabaseAccessSpecSection', () => {
+ const setup = () => {
+ const onChange = jest.fn();
+ let validator: Validator;
+ render(
+
+ component={DatabaseAccessSpecSection}
+ defaultValue={newAccessSpec('db')}
+ onChange={onChange}
+ validatorRef={v => {
+ validator = v;
+ }}
+ />
+ );
+ return { user: userEvent.setup(), onChange, validator };
+ };
+
+ test('editing', async () => {
+ const { user, onChange } = setup();
+ await user.click(screen.getByRole('button', { name: 'Add a Label' }));
+ await user.type(screen.getByPlaceholderText('label key'), 'env');
+ await user.type(screen.getByPlaceholderText('label value'), 'prod');
+ await selectEvent.create(screen.getByLabelText('Database Names'), 'stuff', {
+ createOptionText: 'Database Name: stuff',
+ });
+ await selectEvent.create(screen.getByLabelText('Database Users'), 'mary', {
+ createOptionText: 'Database User: mary',
+ });
+ await selectEvent.create(screen.getByLabelText('Database Roles'), 'admin', {
+ createOptionText: 'Database Role: admin',
+ });
+ expect(onChange).toHaveBeenLastCalledWith({
+ kind: 'db',
+ labels: [{ name: 'env', value: 'prod' }],
+ names: [expect.objectContaining({ label: 'stuff', value: 'stuff' })],
+ roles: [expect.objectContaining({ label: 'admin', value: 'admin' })],
+ users: [expect.objectContaining({ label: 'mary', value: 'mary' })],
+ } as DatabaseAccessSpec);
});
- await selectEvent.create(screen.getByLabelText('Database Roles'), 'admin', {
- createOptionText: 'Database Role: admin',
+
+ test('validation', async () => {
+ const { user, validator } = setup();
+ await user.click(screen.getByRole('button', { name: 'Add a Label' }));
+ await selectEvent.create(screen.getByLabelText('Database Roles'), '*', {
+ createOptionText: 'Database Role: *',
+ });
+ act(() => validator.validate());
+ expect(
+ screen.getByPlaceholderText('label key')
+ ).toHaveAccessibleDescription('required');
+ expect(
+ screen.getByText('Wildcard is not allowed in database roles')
+ ).toBeInTheDocument();
});
- expect(onChange).toHaveBeenLastCalledWith({
- kind: 'db',
- labels: [{ name: 'env', value: 'prod' }],
- names: [expect.objectContaining({ label: 'stuff', value: 'stuff' })],
- roles: [expect.objectContaining({ label: 'admin', value: 'admin' })],
- users: [expect.objectContaining({ label: 'mary', value: 'mary' })],
- } as DatabaseAccessSpec);
});
-test('WindowsDesktopAccessSpecSection', async () => {
- const user = userEvent.setup();
- const onChange = jest.fn();
- render(
-
- component={WindowsDesktopAccessSpecSection}
- defaultValue={newAccessSpec('windows_desktop')}
- onChange={onChange}
- />
- );
+describe('WindowsDesktopAccessSpecSection', () => {
+ const setup = () => {
+ const onChange = jest.fn();
+ let validator: Validator;
+ render(
+
+ component={WindowsDesktopAccessSpecSection}
+ defaultValue={newAccessSpec('windows_desktop')}
+ onChange={onChange}
+ validatorRef={v => {
+ validator = v;
+ }}
+ />
+ );
+ return { user: userEvent.setup(), onChange, validator };
+ };
- await user.click(screen.getByRole('button', { name: 'Add a Label' }));
- await user.type(screen.getByPlaceholderText('label key'), 'os');
- await user.type(screen.getByPlaceholderText('label value'), 'win-xp');
- await selectEvent.create(screen.getByLabelText('Logins'), 'julio', {
- createOptionText: 'Login: julio',
+ test('editing', async () => {
+ const { user, onChange } = setup();
+ await user.click(screen.getByRole('button', { name: 'Add a Label' }));
+ await user.type(screen.getByPlaceholderText('label key'), 'os');
+ await user.type(screen.getByPlaceholderText('label value'), 'win-xp');
+ await selectEvent.create(screen.getByLabelText('Logins'), 'julio', {
+ createOptionText: 'Login: julio',
+ });
+ expect(onChange).toHaveBeenLastCalledWith({
+ kind: 'windows_desktop',
+ labels: [{ name: 'os', value: 'win-xp' }],
+ logins: [expect.objectContaining({ label: 'julio', value: 'julio' })],
+ } as WindowsDesktopAccessSpec);
+ });
+
+ test('validation', async () => {
+ const { user, validator } = setup();
+ await user.click(screen.getByRole('button', { name: 'Add a Label' }));
+ act(() => validator.validate());
+ expect(
+ screen.getByPlaceholderText('label key')
+ ).toHaveAccessibleDescription('required');
});
- expect(onChange).toHaveBeenLastCalledWith({
- kind: 'windows_desktop',
- labels: [{ name: 'os', value: 'win-xp' }],
- logins: [expect.objectContaining({ label: 'julio', value: 'julio' })],
- } as WindowsDesktopAccessSpec);
});
const reactSelectValueContainer = (input: HTMLInputElement) =>
diff --git a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
index 178c5b838631f..28f2c4ca26884 100644
--- a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
+++ b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-import React, { useState } from 'react';
+import React, { useId, useState } from 'react';
import {
Box,
ButtonIcon,
@@ -28,21 +28,25 @@ import {
Text,
} from 'design';
import FieldInput from 'shared/components/FieldInput';
-import Validation, { Validator } from 'shared/components/Validation';
-import { requiredField } from 'shared/components/Validation/rules';
+import Validation, {
+ useValidation,
+ Validator,
+} from 'shared/components/Validation';
+import {
+ precomputed,
+ ValidationResult,
+} from 'shared/components/Validation/rules';
import * as Icon from 'design/Icon';
import { HoverTooltip, ToolTipInfo } from 'design/ToolTip';
import styled, { useTheme } from 'styled-components';
-
import { MenuButton, MenuItem } from 'shared/components/MenuAction';
-
import {
FieldSelect,
FieldSelectCreatable,
} from 'shared/components/FieldSelect';
+import { SlideTabs } from 'design/SlideTabs';
import { Role, RoleWithYaml } from 'teleport/services/resources';
-
import { LabelsInput } from 'teleport/components/LabelsInput';
import { FieldMultiInput } from '../../../../shared/components/FieldMultiInput/FieldMultiInput';
@@ -65,6 +69,15 @@ import {
AppAccessSpec,
DatabaseAccessSpec,
WindowsDesktopAccessSpec,
+ validateRoleEditorModel,
+ MetadataValidationResult,
+ AccessSpecValidationResult,
+ ServerSpecValidationResult,
+ KubernetesSpecValidationResult,
+ KubernetesResourceValidationResult,
+ AppSpecValidationResult,
+ DatabaseSpecValidationResult,
+ WindowsDesktopSpecValidationResult,
} from './standardmodel';
import { EditorSaveCancelButton } from './Shared';
import { RequiresResetToStandard } from './RequiresResetToStandard';
@@ -92,12 +105,28 @@ export const StandardEditor = ({
}: StandardEditorProps) => {
const isEditing = !!originalRole;
const { roleModel } = standardEditorModel;
+ const validation = validateRoleEditorModel(roleModel);
/** All spec kinds except those that are already in the role. */
const allowedSpecKinds = allAccessSpecKinds.filter(k =>
roleModel.accessSpecs.every(as => as.kind !== k)
);
+ enum StandardEditorTab {
+ Overview,
+ Resources,
+ AdminRules,
+ Options,
+ }
+
+ const theme = useTheme();
+ const [currentTab, setCurrentTab] = useState(StandardEditorTab.Overview);
+ const idPrefix = useId();
+ const overviewTabId = `${idPrefix}-overview`;
+ const resourcesTabId = `${idPrefix}-resources`;
+ const adminRulesTabId = `${idPrefix}-admin-rules`;
+ const optionsTabId = `${idPrefix}-options`;
+
function handleSave(validator: Validator) {
if (!validator.validate()) {
return;
@@ -169,53 +198,121 @@ export const StandardEditor = ({
mute={standardEditorModel.roleModel.requiresReset}
data-testid="standard-editor"
>
-
+
+ s.valid)
+ ? undefined
+ : Icon.WarningCircle,
+ statusIconColor:
+ theme.colors.interactive.solid.danger.default,
+ statusIconColorActive: 'transparent',
+ },
+ {
+ key: StandardEditorTab.AdminRules,
+ title: 'Admin Rules',
+ controls: adminRulesTabId,
+ statusIconColor:
+ theme.colors.interactive.solid.danger.default,
+ statusIconColorActive: 'transparent',
+ },
+ {
+ key: StandardEditorTab.Options,
+ title: 'Options',
+ controls: optionsTabId,
+ statusIconColor:
+ theme.colors.interactive.solid.danger.default,
+ statusIconColorActive: 'transparent',
+ },
+ ]}
+ activeIndex={currentTab}
+ onChange={setCurrentTab}
+ />
+
+
handleChange({ ...roleModel, metadata })}
/>
- {roleModel.accessSpecs.map(spec => (
- setAccessSpec(value)}
- onRemove={() => removeAccessSpec(spec.kind)}
- />
- ))}
-
-
-
- Add New Specifications
- >
- }
- buttonProps={{
- size: 'medium',
- fill: 'filled',
- disabled: isProcessing || allowedSpecKinds.length === 0,
- }}
- >
- {allowedSpecKinds.map(kind => (
- addAccessSpec(kind)}>
- {specSections[kind].title}
-
- ))}
-
-
-
+
+
+
+ {roleModel.accessSpecs.map((spec, i) => {
+ const validationResult = validation.accessSpecs[i];
+ return (
+ setAccessSpec(value)}
+ onRemove={() => removeAccessSpec(spec.kind)}
+ />
+ );
+ })}
+
+
+
+ Add New Specifications
+ >
+ }
+ buttonProps={{
+ size: 'medium',
+ fill: 'filled',
+ disabled: isProcessing || allowedSpecKinds.length === 0,
+ }}
+ >
+ {allowedSpecKinds.map(kind => (
+ addAccessSpec(kind)}>
+ {specSections[kind].title}
+
+ ))}
+
+
+
+
handleSave(validator)}
@@ -233,28 +330,31 @@ export const StandardEditor = ({
);
};
-export type SectionProps = {
+export type SectionProps = {
value: T;
isProcessing: boolean;
+ validation?: V;
onChange?(value: T): void;
};
const MetadataSection = ({
value,
isProcessing,
+ validation,
onChange,
-}: SectionProps) => (
+}: SectionProps) => (
onChange({ ...value, name: e.target.value })}
/>
) => {
const theme = useTheme();
const [expanded, setExpanded] = useState(true);
const ExpandIcon = expanded ? Icon.Minus : Icon.Plus;
const expandTooltip = expanded ? 'Collapse' : 'Expand';
+ const validator = useValidation();
const handleExpand = (e: React.MouseEvent) => {
// Don't let handle the event, we'll do it ourselves to keep
@@ -311,7 +414,11 @@ const Section = ({
as="details"
open={expanded}
border={1}
- borderColor={theme.colors.interactive.tonal.neutral[0]}
+ borderColor={
+ validator.state.validating && !validation.valid
+ ? theme.colors.interactive.solid.danger.default
+ : theme.colors.interactive.tonal.neutral[0]
+ }
borderRadius={3}
>
>;
+ component: React.ComponentType>;
}
> = {
kube_cluster: {
@@ -409,12 +516,16 @@ const specSections: Record<
* A generic access spec section. Details are rendered by components from the
* `specSections` map.
*/
-const AccessSpecSection = ({
+const AccessSpecSection = <
+ T extends AccessSpec,
+ V extends AccessSpecValidationResult,
+>({
value,
isProcessing,
+ validation,
onChange,
onRemove,
-}: SectionProps & {
+}: SectionProps & {
onRemove?(): void;
}) => {
const { component: Body, title, tooltip } = specSections[value.kind];
@@ -425,8 +536,14 @@ const AccessSpecSection = ({
onRemove={onRemove}
tooltip={tooltip}
isProcessing={isProcessing}
+ validation={validation}
>
-
+
);
};
@@ -434,8 +551,9 @@ const AccessSpecSection = ({
export function ServerAccessSpecSection({
value,
isProcessing,
+ validation,
onChange,
-}: SectionProps) {
+}: SectionProps) {
return (
<>
@@ -445,6 +563,7 @@ export function ServerAccessSpecSection({
disableBtns={isProcessing}
labels={value.labels}
setLabels={labels => onChange?.({ ...value, labels })}
+ rule={precomputed(validation.fields.labels)}
/>
onChange?.({ ...value, logins })}
+ rule={precomputed(validation.fields.logins)}
mt={3}
mb={0}
/>
@@ -467,8 +587,9 @@ export function ServerAccessSpecSection({
export function KubernetesAccessSpecSection({
value,
isProcessing,
+ validation,
onChange,
-}: SectionProps) {
+}: SectionProps) {
return (
<>
onChange?.({ ...value, labels })}
/>
@@ -498,6 +620,7 @@ export function KubernetesAccessSpecSection({
onChange?.({
@@ -540,11 +663,13 @@ export function KubernetesAccessSpecSection({
function KubernetesResourceView({
value,
+ validation,
isProcessing,
onChange,
onRemove,
}: {
value: KubernetesResourceModel;
+ validation: KubernetesResourceValidationResult;
isProcessing: boolean;
onChange(m: KubernetesResourceModel): void;
onRemove(): void;
@@ -590,6 +715,7 @@ function KubernetesResourceView({
}
disabled={isProcessing}
value={name}
+ rule={precomputed(validation.name)}
onChange={e => onChange?.({ ...value, name: e.target.value })}
/>
onChange?.({ ...value, namespace: e.target.value })}
/>
) {
+}: SectionProps) {
return (
@@ -632,6 +760,7 @@ export function AppAccessSpecSection({
disableBtns={isProcessing}
labels={value.labels}
setLabels={labels => onChange?.({ ...value, labels })}
+ rule={precomputed(validation.fields.labels)}
/>
onChange?.({ ...value, awsRoleARNs: arns })}
+ rule={precomputed(validation.fields.awsRoleARNs)}
/>
onChange?.({ ...value, azureIdentities: ids })}
+ rule={precomputed(validation.fields.azureIdentities)}
/>
onChange?.({ ...value, gcpServiceAccounts: accts })}
+ rule={precomputed(validation.fields.gcpServiceAccounts)}
/>
);
@@ -659,8 +791,9 @@ export function AppAccessSpecSection({
export function DatabaseAccessSpecSection({
value,
isProcessing,
+ validation,
onChange,
-}: SectionProps) {
+}: SectionProps) {
return (
<>
@@ -671,6 +804,7 @@ export function DatabaseAccessSpecSection({
disableBtns={isProcessing}
labels={value.labels}
setLabels={labels => onChange?.({ ...value, labels })}
+ rule={precomputed(validation.fields.labels)}
/>
onChange?.({ ...value, roles })}
+ rule={precomputed(validation.fields.roles)}
mb={0}
/>
>
@@ -730,8 +865,9 @@ export function DatabaseAccessSpecSection({
export function WindowsDesktopAccessSpecSection({
value,
isProcessing,
+ validation,
onChange,
-}: SectionProps) {
+}: SectionProps) {
return (
<>
@@ -742,6 +878,7 @@ export function WindowsDesktopAccessSpecSection({
disableBtns={isProcessing}
labels={value.labels}
setLabels={labels => onChange?.({ ...value, labels })}
+ rule={precomputed(validation.fields.labels)}
/>
;
-type KubernetesResourceKind =
- | '*'
- | 'pod'
- | 'secret'
- | 'configmap'
- | 'namespace'
- | 'service'
- | 'serviceaccount'
- | 'kube_node'
- | 'persistentvolume'
- | 'persistentvolumeclaim'
- | 'deployment'
- | 'replicaset'
- | 'statefulset'
- | 'daemonset'
- | 'clusterrole'
- | 'kube_role'
- | 'clusterrolebinding'
- | 'rolebinding'
- | 'cronjob'
- | 'job'
- | 'certificatesigningrequest'
- | 'ingress';
+const kubernetesClusterWideResourceKinds: KubernetesResourceKind[] = [
+ 'namespace',
+ 'kube_node',
+ 'persistentvolume',
+ 'clusterrole',
+ 'clusterrolebinding',
+ 'certificatesigningrequest',
+];
/**
* All possible resource kind drop-down options. This array needs to be kept in
@@ -172,19 +169,6 @@ export const kubernetesResourceKindOptions: KubernetesResourceKindOption[] = [
];
type KubernetesVerbOption = Option;
-type KubernetesVerb =
- | '*'
- | 'get'
- | 'create'
- | 'update'
- | 'patch'
- | 'delete'
- | 'list'
- | 'watch'
- | 'deletecollection'
- | 'exec'
- | 'portforward';
-
/**
* All possible Kubernetes verb drop-down options. This array needs to be kept
* in sync with `KubernetesVerbs` in `api/types/constants.go.
@@ -590,7 +574,7 @@ export function labelsModelToLabels(uiLabels: UILabel[]): Labels {
return labels;
}
-function optionsToStrings(opts: readonly Option[]): string[] {
+function optionsToStrings(opts: readonly Option[]): T[] {
return opts.map(opt => opt.value);
}
@@ -603,3 +587,124 @@ export function hasModifiedFields(
ignoreUndefined: true,
});
}
+
+export function validateRoleEditorModel({
+ metadata,
+ accessSpecs,
+}: RoleEditorModel) {
+ return {
+ metadata: validateMetadata(metadata),
+ accessSpecs: accessSpecs.map(validateAccessSpec),
+ };
+}
+
+function validateMetadata(model: MetadataModel): MetadataValidationResult {
+ return runRules(model, metadataRules);
+}
+
+const metadataRules = { name: requiredField('Role name is required') };
+export type MetadataValidationResult = RuleSetValidationResult<
+ typeof metadataRules
+>;
+
+export function validateAccessSpec(
+ spec: AccessSpec
+): AccessSpecValidationResult {
+ const { kind } = spec;
+ switch (kind) {
+ case 'kube_cluster':
+ return runRules(spec, kubernetesValidationRules);
+ case 'node':
+ return runRules(spec, serverValidationRules);
+ case 'app':
+ return runRules(spec, appSpecValidationRules);
+ case 'db':
+ return runRules(spec, databaseSpecValidationRules);
+ case 'windows_desktop':
+ return runRules(spec, windowsDesktopSpecValidationRules);
+ default:
+ kind satisfies never;
+ }
+}
+
+export type AccessSpecValidationResult =
+ | ServerSpecValidationResult
+ | KubernetesSpecValidationResult
+ | AppSpecValidationResult
+ | DatabaseSpecValidationResult
+ | WindowsDesktopSpecValidationResult;
+
+const validKubernetesResource = (res: KubernetesResourceModel) => () => {
+ const name = requiredField(
+ 'Resource name is required, use "*" for any resource'
+ )(res.name)();
+ const namespace = kubernetesClusterWideResourceKinds.includes(res.kind.value)
+ ? { valid: true }
+ : requiredField('Namespace is required for resources of this kind')(
+ res.namespace
+ )();
+ return {
+ valid: name.valid && namespace.valid,
+ name,
+ namespace,
+ };
+};
+export type KubernetesResourceValidationResult = {
+ name: ValidationResult;
+ namespace: ValidationResult;
+};
+
+const kubernetesValidationRules = {
+ labels: nonEmptyLabels,
+ resources: arrayOf(validKubernetesResource),
+};
+export type KubernetesSpecValidationResult = RuleSetValidationResult<
+ typeof kubernetesValidationRules
+>;
+
+const noWildcard = (message: string) => (value: string) => () => {
+ const valid = value !== '*';
+ return { valid, message: valid ? '' : message };
+};
+
+const noWildcardOptions = (message: string) => (options: Option[]) => () => {
+ const valid = options.every(o => o.value !== '*');
+ return { valid, message: valid ? '' : message };
+};
+
+const serverValidationRules = {
+ labels: nonEmptyLabels,
+ logins: noWildcardOptions('Wildcard is not allowed in logins'),
+};
+export type ServerSpecValidationResult = RuleSetValidationResult<
+ typeof serverValidationRules
+>;
+
+const appSpecValidationRules = {
+ labels: nonEmptyLabels,
+ awsRoleARNs: arrayOf(noWildcard('Wildcard is not allowed in AWS role ARNs')),
+ azureIdentities: arrayOf(
+ noWildcard('Wildcard is not allowed in Azure identities')
+ ),
+ gcpServiceAccounts: arrayOf(
+ noWildcard('Wildcard is not allowed in GCP service accounts')
+ ),
+};
+export type AppSpecValidationResult = RuleSetValidationResult<
+ typeof appSpecValidationRules
+>;
+
+const databaseSpecValidationRules = {
+ labels: nonEmptyLabels,
+ roles: noWildcardOptions('Wildcard is not allowed in database roles'),
+};
+export type DatabaseSpecValidationResult = RuleSetValidationResult<
+ typeof databaseSpecValidationRules
+>;
+
+const windowsDesktopSpecValidationRules = {
+ labels: nonEmptyLabels,
+};
+export type WindowsDesktopSpecValidationResult = RuleSetValidationResult<
+ typeof windowsDesktopSpecValidationRules
+>;
diff --git a/web/packages/teleport/src/Roles/Roles.tsx b/web/packages/teleport/src/Roles/Roles.tsx
index d04034b475609..985c01c19117c 100644
--- a/web/packages/teleport/src/Roles/Roles.tsx
+++ b/web/packages/teleport/src/Roles/Roles.tsx
@@ -165,7 +165,7 @@ export function Roles(props: State) {
)}
-
+
{convertAttempt.status === 'processing' && (
;
export type KubernetesResource = {
- kind?: string;
+ kind?: KubernetesResourceKind;
name?: string;
namespace?: string;
- verbs?: string[];
+ verbs?: KubernetesVerb[];
};
+export type KubernetesResourceKind =
+ | '*'
+ | 'pod'
+ | 'secret'
+ | 'configmap'
+ | 'namespace'
+ | 'service'
+ | 'serviceaccount'
+ | 'kube_node'
+ | 'persistentvolume'
+ | 'persistentvolumeclaim'
+ | 'deployment'
+ | 'replicaset'
+ | 'statefulset'
+ | 'daemonset'
+ | 'clusterrole'
+ | 'kube_role'
+ | 'clusterrolebinding'
+ | 'rolebinding'
+ | 'cronjob'
+ | 'job'
+ | 'certificatesigningrequest'
+ | 'ingress';
+
+export type KubernetesVerb =
+ | '*'
+ | 'get'
+ | 'create'
+ | 'update'
+ | 'patch'
+ | 'delete'
+ | 'list'
+ | 'watch'
+ | 'deletecollection'
+ | 'exec'
+ | 'portforward';
+
/**
* Teleport role options in full format, as returned from Teleport API. Note
* that its fields follow the snake case convention to match the wire format.
From 98104070629678d7c55c2304f24312b17dc99183 Mon Sep 17 00:00:00 2001
From: Bartosz Leper
Date: Thu, 28 Nov 2024 18:57:25 +0100
Subject: [PATCH 05/14] Review
---
.../design/src/ToolTip/ToolTip.story.tsx | 22 +++++++++----------
web/packages/design/src/ToolTip/ToolTip.tsx | 2 +-
web/packages/design/src/ToolTip/index.ts | 2 +-
.../AccessDuration/AccessDurationRequest.tsx | 6 ++---
.../AccessDuration/AccessDurationReview.tsx | 6 ++---
.../RequestCheckout/AdditionalOptions.tsx | 10 ++++-----
.../NewRequest/ResourceList/Apps.tsx | 6 ++---
.../AdvancedSearchToggle.tsx | 6 ++---
.../components/FieldInput/FieldInput.tsx | 4 ++--
.../shared/components/FieldSelect/shared.tsx | 4 ++--
.../FieldTextArea/FieldTextArea.tsx | 4 ++--
.../shared/components/ToolTip/index.ts | 6 ++---
.../DocumentKubeExec/KubeExecDataDialog.tsx | 6 ++---
.../CreateAppAccess/CreateAppAccess.tsx | 6 ++---
.../AutoDeploy/SelectSecurityGroups.tsx | 6 ++---
.../AutoDeploy/SelectSubnetIds.tsx | 6 ++---
.../EnrollRdsDatabase/AutoDiscoverToggle.tsx | 6 ++---
.../EnrollEKSCluster/EnrollEksCluster.tsx | 10 ++++-----
.../DiscoveryConfigSsm/DiscoveryConfigSsm.tsx | 6 ++---
.../EnrollEc2Instance/EnrollEc2Instance.tsx | 6 ++---
.../Discover/Shared/Aws/ConfigureIamPerms.tsx | 6 ++---
.../ConfigureDiscoveryServiceDirections.tsx | 6 ++---
.../SecurityGroupPicker.tsx | 6 ++---
.../AwsOidc/ConfigureAwsOidcSummary.tsx | 6 ++---
.../Enroll/AwsOidc/S3BucketConfiguration.tsx | 6 ++---
.../src/Integrations/IntegrationList.tsx | 4 ++--
.../teleport/src/Navigation/Navigation.tsx | 6 ++---
.../src/Navigation/SideNavigation/Section.tsx | 6 ++---
.../src/Roles/RoleEditor/StandardEditor.tsx | 4 ++--
29 files changed, 90 insertions(+), 90 deletions(-)
diff --git a/web/packages/design/src/ToolTip/ToolTip.story.tsx b/web/packages/design/src/ToolTip/ToolTip.story.tsx
index 686c005aa37a1..ceb48d73edfff 100644
--- a/web/packages/design/src/ToolTip/ToolTip.story.tsx
+++ b/web/packages/design/src/ToolTip/ToolTip.story.tsx
@@ -25,11 +25,11 @@ import { P } from 'design/Text/Text';
import AGPLLogoLight from 'design/assets/images/agpl-light.svg';
import AGPLLogoDark from 'design/assets/images/agpl-dark.svg';
-import { ToolTipInfo } from './ToolTip';
+import { TooltipInfo } from './Tooltip';
import { HoverTooltip } from './HoverTooltip';
export default {
- title: 'Shared/ToolTip',
+ title: 'Design/Tooltip',
};
export const ShortContent = () => (
@@ -38,25 +38,25 @@ export const ShortContent = () => (
Hover the icon
- "some popover content"
+ "some popover content"
Hover the icon
- "some popover content"
+ "some popover content"
Hover the icon
- "some popover content"
+ "some popover content"
Hover the icon
- "some popover content"
+ "some popover content"
);
@@ -78,7 +78,7 @@ export const LongContent = () => {
<>
Hover the icon
-
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
@@ -91,7 +91,7 @@ export const LongContent = () => {
cupidatat non proident, sunt in culpa qui officia deserunt mollit
anim id est laborum.
-
+
Here's some content that shouldn't interfere with the semi-transparent
@@ -120,7 +120,7 @@ export const WithMutedIconColor = () => (
Hover the icon
- "some popover content"
+ "some popover content"
>
);
@@ -129,7 +129,7 @@ export const WithKindWarning = () => (
Hover the icon
- "some popover content"
+ "some popover content"
>
);
@@ -138,7 +138,7 @@ export const WithKindError = () => (
Hover the icon
- "some popover content"
+ "some popover content"
>
);
diff --git a/web/packages/design/src/ToolTip/ToolTip.tsx b/web/packages/design/src/ToolTip/ToolTip.tsx
index 8443ecc20fbf3..7e227f2cc4bc3 100644
--- a/web/packages/design/src/ToolTip/ToolTip.tsx
+++ b/web/packages/design/src/ToolTip/ToolTip.tsx
@@ -27,7 +27,7 @@ import { anchorOriginForPosition, transformOriginForPosition } from './shared';
type ToolTipKind = 'info' | 'warning' | 'error';
-export const ToolTipInfo: React.FC<
+export const TooltipInfo: React.FC<
PropsWithChildren<{
trigger?: 'click' | 'hover';
position?: Position;
diff --git a/web/packages/design/src/ToolTip/index.ts b/web/packages/design/src/ToolTip/index.ts
index c6518cad9b297..f511148deb903 100644
--- a/web/packages/design/src/ToolTip/index.ts
+++ b/web/packages/design/src/ToolTip/index.ts
@@ -16,5 +16,5 @@
* along with this program. If not, see .
*/
-export { ToolTipInfo } from './ToolTip';
+export { TooltipInfo } from './Tooltip';
export { HoverTooltip } from './HoverTooltip';
diff --git a/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx b/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx
index d6120fdf0eddd..015d34c9a476c 100644
--- a/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx
+++ b/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx
@@ -19,7 +19,7 @@
import React from 'react';
import { Flex, LabelInput, Text } from 'design';
-import { ToolTipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/ToolTip';
import Select, { Option } from 'shared/components/Select';
@@ -36,11 +36,11 @@ export function AccessDurationRequest({
Access Duration
-
+
How long you would be given elevated privileges. Note that the time it
takes to approve this request will be subtracted from the duration you
requested.
-
+
-
+
How long the access will be granted for after approval.
-
+
);
}
diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx
index 91fe207f6712c..e42a3c160f82e 100644
--- a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx
+++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx
@@ -20,7 +20,7 @@ import React, { useState } from 'react';
import { Flex, Text, ButtonIcon, Box, LabelInput } from 'design';
import * as Icon from 'design/Icon';
-import { ToolTipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/ToolTip';
import Select, { Option } from 'shared/components/Select';
@@ -73,10 +73,10 @@ export function AdditionalOptions({
Request expires if not reviewed in
-
+
The request TTL which is the amount of time this request will
be in the PENDING state before it expires.
-
+
Access Request Lifetime
-
+
The max duration of an access request, starting from its
creation, until it expires.
-
+
{getFormattedDurationTxt({
diff --git a/web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Apps.tsx b/web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Apps.tsx
index e2a09bf7d47e4..71f16dacdc6d3 100644
--- a/web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Apps.tsx
+++ b/web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Apps.tsx
@@ -24,7 +24,7 @@ import { ClickableLabelCell, Cell } from 'design/DataTable';
import { App } from 'teleport/services/apps';
-import { ToolTipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/ToolTip';
import Select, {
Option as BaseOption,
@@ -231,11 +231,11 @@ function ActionCell({
)}
-
+
This application {agent.name} can be alternatively requested by
members of user groups. You can alternatively select user groups
instead to access this application.
-
+
Advanced
-
+
-
+
);
}
diff --git a/web/packages/shared/components/FieldInput/FieldInput.tsx b/web/packages/shared/components/FieldInput/FieldInput.tsx
index 7166cac313d5c..367e836e11fa0 100644
--- a/web/packages/shared/components/FieldInput/FieldInput.tsx
+++ b/web/packages/shared/components/FieldInput/FieldInput.tsx
@@ -28,7 +28,7 @@ import styled, { useTheme } from 'styled-components';
import { IconProps } from 'design/Icon/Icon';
import { InputMode, InputSize, InputType } from 'design/Input';
-import { ToolTipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/ToolTip';
import { useRule } from 'shared/components/Validation';
@@ -114,7 +114,7 @@ const FieldInput = forwardRef(
>
{label}
-
+
>
) : (
<>{label}>
diff --git a/web/packages/shared/components/FieldSelect/shared.tsx b/web/packages/shared/components/FieldSelect/shared.tsx
index 0588356769790..eabfd1a75e412 100644
--- a/web/packages/shared/components/FieldSelect/shared.tsx
+++ b/web/packages/shared/components/FieldSelect/shared.tsx
@@ -25,7 +25,7 @@ import LabelInput from 'design/LabelInput';
import Flex from 'design/Flex';
-import { ToolTipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/ToolTip';
import { HelperTextLine } from '../FieldInput/FieldInput';
import { useRule } from '../Validation';
@@ -96,7 +96,7 @@ export const FieldSelectWrapper = ({
{toolTipContent ? (
{label}
-
+
) : (
label
diff --git a/web/packages/shared/components/FieldTextArea/FieldTextArea.tsx b/web/packages/shared/components/FieldTextArea/FieldTextArea.tsx
index fc01245f6c9b7..26f9e54032cde 100644
--- a/web/packages/shared/components/FieldTextArea/FieldTextArea.tsx
+++ b/web/packages/shared/components/FieldTextArea/FieldTextArea.tsx
@@ -27,7 +27,7 @@ import { TextAreaSize } from 'design/TextArea';
import { BoxProps } from 'design/Box';
-import { ToolTipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/ToolTip';
import { useRule } from 'shared/components/Validation';
@@ -141,7 +141,7 @@ export const FieldTextArea = forwardRef<
>
{label}
-
+
>
) : (
<>{label}>
diff --git a/web/packages/shared/components/ToolTip/index.ts b/web/packages/shared/components/ToolTip/index.ts
index a3d07c5ab42ce..0e038d19f4f29 100644
--- a/web/packages/shared/components/ToolTip/index.ts
+++ b/web/packages/shared/components/ToolTip/index.ts
@@ -17,9 +17,9 @@
*/
export {
- /** @deprecated Use `design/Tooltip` */
- ToolTipInfo,
+ /** @deprecated Use `TooltipInfo` from `design/Tooltip` */
+ TooltipInfo as ToolTipInfo,
- /** @deprecated Use `design/Tooltip` */
+ /** @deprecated Use `HoverTooltip` from `design/Tooltip` */
HoverTooltip,
} from 'design/ToolTip';
diff --git a/web/packages/teleport/src/Console/DocumentKubeExec/KubeExecDataDialog.tsx b/web/packages/teleport/src/Console/DocumentKubeExec/KubeExecDataDialog.tsx
index f5b4146bcb894..24bca7f59c036 100644
--- a/web/packages/teleport/src/Console/DocumentKubeExec/KubeExecDataDialog.tsx
+++ b/web/packages/teleport/src/Console/DocumentKubeExec/KubeExecDataDialog.tsx
@@ -35,7 +35,7 @@ import {
import Validation from 'shared/components/Validation';
import FieldInput from 'shared/components/FieldInput';
import { requiredField } from 'shared/components/Validation/rules';
-import { ToolTipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/ToolTip';
type Props = {
onClose(): void;
@@ -123,11 +123,11 @@ function KubeExecDataDialog({ onClose, onExec }: Props) {
Interactive shell
-
+
You can start an interactive shell and have a
bidirectional communication with the target pod, or you
can run one-off command and see its output.
-
+
diff --git a/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx b/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx
index 2104c11ce5fd4..d388481b547c2 100644
--- a/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx
+++ b/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx
@@ -21,7 +21,7 @@ import styled from 'styled-components';
import { Box, Flex, Link, Mark, H3 } from 'design';
import TextEditor from 'shared/components/TextEditor';
import { Danger } from 'design/Alert';
-import { ToolTipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/ToolTip';
import { useAsync } from 'shared/hooks/useAsync';
import { P } from 'design/Text/Text';
@@ -81,7 +81,7 @@ export function CreateAppAccess() {
First configure your AWS IAM permissions
-
+
The following IAM permissions will be added as an inline policy
named {IAM_POLICY_NAME} to IAM role{' '}
{iamRoleName}
@@ -94,7 +94,7 @@ export function CreateAppAccess() {
/>
-
+
Run the command below on your{' '}
diff --git a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSecurityGroups.tsx b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSecurityGroups.tsx
index 74035fd6774c0..5cbabf6e97147 100644
--- a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSecurityGroups.tsx
+++ b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSecurityGroups.tsx
@@ -21,7 +21,7 @@ import React, { useState, useEffect } from 'react';
import { Text, Flex, Box, Indicator, ButtonSecondary, Subtitle3 } from 'design';
import * as Icons from 'design/Icon';
import { FetchStatus } from 'design/DataTable/types';
-import { HoverTooltip, ToolTipInfo } from 'design/ToolTip';
+import { HoverTooltip, TooltipInfo } from 'design/ToolTip';
import useAttempt from 'shared/hooks/useAttemptNext';
import { getErrMessage } from 'shared/utils/errorType';
import { pluralize } from 'shared/utils/text';
@@ -126,7 +126,7 @@ export const SelectSecurityGroups = ({
<>
Select ECS Security Groups
-
+
Select ECS security group(s) based on the following requirements:
@@ -141,7 +141,7 @@ export const SelectSecurityGroups = ({
-
+
diff --git a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSubnetIds.tsx b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSubnetIds.tsx
index 712102576074b..7d8af78645290 100644
--- a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSubnetIds.tsx
+++ b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSubnetIds.tsx
@@ -29,7 +29,7 @@ import {
} from 'design';
import * as Icons from 'design/Icon';
import { FetchStatus } from 'design/DataTable/types';
-import { HoverTooltip, ToolTipInfo } from 'design/ToolTip';
+import { HoverTooltip, TooltipInfo } from 'design/ToolTip';
import { pluralize } from 'shared/utils/text';
import useAttempt from 'shared/hooks/useAttemptNext';
import { getErrMessage } from 'shared/utils/errorType';
@@ -121,12 +121,12 @@ export function SelectSubnetIds({
<>
Select ECS Subnets
-
+
A subnet has an outbound internet route if it has a route to an
internet gateway or a NAT gateway in a public subnet.
-
+
diff --git a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AutoDiscoverToggle.tsx b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AutoDiscoverToggle.tsx
index 790993b715957..4272820f64a5a 100644
--- a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AutoDiscoverToggle.tsx
+++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AutoDiscoverToggle.tsx
@@ -19,7 +19,7 @@
import React from 'react';
import { Box, Toggle } from 'design';
-import { ToolTipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/ToolTip';
export function AutoDiscoverToggle({
wantAutoDiscover,
@@ -40,11 +40,11 @@ export function AutoDiscoverToggle({
Auto-enroll all databases for the selected VPC
-
+
Auto-enroll will automatically identify all RDS databases (e.g.
PostgreSQL, MySQL, Aurora) from the selected VPC and register them as
database resources in your infrastructure.
-
+
);
diff --git a/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx b/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx
index 8cff0fc08a27d..3fb5d8a513cf3 100644
--- a/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx
+++ b/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx
@@ -31,7 +31,7 @@ import { FetchStatus } from 'design/DataTable/types';
import { Danger } from 'design/Alert';
import useAttempt from 'shared/hooks/useAttemptNext';
-import { ToolTipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/ToolTip';
import { getErrMessage } from 'shared/utils/errorType';
import { EksMeta, useDiscover } from 'teleport/Discover/useDiscover';
@@ -435,11 +435,11 @@ export function EnrollEksCluster(props: AgentStepProps) {
Enable Kubernetes App Discovery
-
+
Teleport's Kubernetes App Discovery will automatically identify
and enroll to Teleport HTTP applications running inside a
Kubernetes cluster.
-
+
Auto-enroll all EKS clusters for selected region
-
+
Auto-enroll will automatically identify all EKS clusters from
the selected region and register them as Kubernetes resources in
your infrastructure.
-
+
{showTable && (
diff --git a/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
index 5fa5f242fc019..4cf227456669b 100644
--- a/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
+++ b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
@@ -30,7 +30,7 @@ import {
import styled from 'styled-components';
import { Danger, Info } from 'design/Alert';
import TextEditor from 'shared/components/TextEditor';
-import { ToolTipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/ToolTip';
import FieldInput from 'shared/components/FieldInput';
import { Rule } from 'shared/components/Validation/rules';
import Validation, { Validator } from 'shared/components/Validation';
@@ -317,7 +317,7 @@ export function DiscoveryConfigSsm() {
{' '}
to configure your IAM permissions.
-
+
The following IAM permissions will be added as an inline
policy named {IAM_POLICY_NAME} to IAM role{' '}
{arnResourceName}
@@ -330,7 +330,7 @@ export function DiscoveryConfigSsm() {
/>
-
+
Auto-enroll all EC2 instances for selected region
-
+
Auto-enroll will automatically identify all EC2 instances from
the selected region and register them as node resources in
your infrastructure.
-
+
{wantAutoDiscover && (
diff --git a/web/packages/teleport/src/Discover/Shared/Aws/ConfigureIamPerms.tsx b/web/packages/teleport/src/Discover/Shared/Aws/ConfigureIamPerms.tsx
index b713235ee5a7a..323a3feb7323b 100644
--- a/web/packages/teleport/src/Discover/Shared/Aws/ConfigureIamPerms.tsx
+++ b/web/packages/teleport/src/Discover/Shared/Aws/ConfigureIamPerms.tsx
@@ -21,7 +21,7 @@ import styled from 'styled-components';
import { Flex, Link, Box, H3 } from 'design';
import { assertUnreachable } from 'shared/utils/assertUnreachable';
import TextEditor from 'shared/components/TextEditor';
-import { ToolTipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/ToolTip';
import { P } from 'design/Text/Text';
@@ -179,11 +179,11 @@ export function ConfigureIamPerms({
<>
Configure your AWS IAM permissions
-
+
The following IAM permissions will be added as an inline policy
named {iamPolicyName} to IAM role {iamRoleName}
{editor}
-
+
{msg} Run the command below on your{' '}
diff --git a/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/ConfigureDiscoveryServiceDirections.tsx b/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/ConfigureDiscoveryServiceDirections.tsx
index f5d98c03ca386..34bb7a37a0690 100644
--- a/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/ConfigureDiscoveryServiceDirections.tsx
+++ b/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/ConfigureDiscoveryServiceDirections.tsx
@@ -18,7 +18,7 @@
import { Box, Flex, Input, Text, Mark, H3, Subtitle3 } from 'design';
import styled from 'styled-components';
-import { ToolTipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/ToolTip';
import React from 'react';
@@ -71,7 +71,7 @@ discovery_service:
Auto-enrolling requires you to configure a{' '}
Discovery Service
-
+
>
@@ -100,7 +100,7 @@ discovery_service:
Step 2
Define a Discovery Group name{' '}
-
+
diff --git a/web/packages/teleport/src/Discover/Shared/SecurityGroupPicker/SecurityGroupPicker.tsx b/web/packages/teleport/src/Discover/Shared/SecurityGroupPicker/SecurityGroupPicker.tsx
index 3b986be833de4..48823788401b1 100644
--- a/web/packages/teleport/src/Discover/Shared/SecurityGroupPicker/SecurityGroupPicker.tsx
+++ b/web/packages/teleport/src/Discover/Shared/SecurityGroupPicker/SecurityGroupPicker.tsx
@@ -23,7 +23,7 @@ import Table, { Cell } from 'design/DataTable';
import { Danger } from 'design/Alert';
import { CheckboxInput } from 'design/Checkbox';
import { FetchStatus } from 'design/DataTable/types';
-import { ToolTipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/ToolTip';
import { Attempt } from 'shared/hooks/useAttemptNext';
@@ -163,13 +163,13 @@ export const SecurityGroupPicker = ({
if (sg.recommended && sg.tips?.length) {
return (
-
+
{sg.tips.map((tip, index) => (
{tip}
))}
-
+
|
);
}
diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/ConfigureAwsOidcSummary.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/ConfigureAwsOidcSummary.tsx
index 1adace833d8d1..7480fd7c52248 100644
--- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/ConfigureAwsOidcSummary.tsx
+++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/ConfigureAwsOidcSummary.tsx
@@ -20,7 +20,7 @@ import React from 'react';
import styled from 'styled-components';
import { Flex, Box, H3, Text } from 'design';
import TextEditor from 'shared/components/TextEditor';
-import { ToolTipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/ToolTip';
import useStickyClusterId from 'teleport/useStickyClusterId';
@@ -61,7 +61,7 @@ export function ConfigureAwsOidcSummary({
}`;
return (
-
+
Running the command in AWS CloudShell does the following:
1. Configures an AWS IAM OIDC Identity Provider (IdP)
@@ -76,7 +76,7 @@ export function ConfigureAwsOidcSummary({
/>
-
+
);
}
diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx
index b316c5f3a1a56..d69019a460017 100644
--- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx
+++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx
@@ -19,7 +19,7 @@
import React from 'react';
import { Text, Flex } from 'design';
import FieldInput from 'shared/components/FieldInput';
-import { ToolTipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/ToolTip';
export function S3BucketConfiguration({
s3Bucket,
@@ -32,11 +32,11 @@ export function S3BucketConfiguration({
<>
Amazon S3 Location
-
+
Deprecated. Amazon is now validating the IdP certificate against a
list of root CAs. Storing the OpenID Configuration in S3 is no longer
required, and should be removed to improve security.
-
+
{
{getStatusCodeTitle(item.statusCode)}
{statusDescription && (
- {statusDescription}
+ {statusDescription}
)}
diff --git a/web/packages/teleport/src/Navigation/Navigation.tsx b/web/packages/teleport/src/Navigation/Navigation.tsx
index db95daad39759..030f14057a2a0 100644
--- a/web/packages/teleport/src/Navigation/Navigation.tsx
+++ b/web/packages/teleport/src/Navigation/Navigation.tsx
@@ -21,7 +21,7 @@ import styled, { useTheme } from 'styled-components';
import { matchPath, useLocation, useHistory } from 'react-router';
import { Box, Text, Flex } from 'design';
-import { ToolTipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/ToolTip';
import cfg from 'teleport/config';
import {
@@ -195,9 +195,9 @@ function LicenseFooter({
{title}
-
+
{infoContent}
-
+
{subText}
diff --git a/web/packages/teleport/src/Navigation/SideNavigation/Section.tsx b/web/packages/teleport/src/Navigation/SideNavigation/Section.tsx
index 495452b1452d8..e744afba9cff2 100644
--- a/web/packages/teleport/src/Navigation/SideNavigation/Section.tsx
+++ b/web/packages/teleport/src/Navigation/SideNavigation/Section.tsx
@@ -23,7 +23,7 @@ import styled, { css, useTheme } from 'styled-components';
import { Box, ButtonIcon, Flex, P2, Text } from 'design';
import { Theme } from 'design/theme';
import { ArrowLineLeft } from 'design/Icon';
-import { HoverTooltip, ToolTipInfo } from 'design/ToolTip';
+import { HoverTooltip, TooltipInfo } from 'design/ToolTip';
import cfg from 'teleport/config';
@@ -470,9 +470,9 @@ function LicenseFooter({
{title}
-
+
{infoContent}
-
+
{subText}
diff --git a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
index 178c5b838631f..634b55c6b56ce 100644
--- a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
+++ b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
@@ -31,7 +31,7 @@ import FieldInput from 'shared/components/FieldInput';
import Validation, { Validator } from 'shared/components/Validation';
import { requiredField } from 'shared/components/Validation/rules';
import * as Icon from 'design/Icon';
-import { HoverTooltip, ToolTipInfo } from 'design/ToolTip';
+import { HoverTooltip, TooltipInfo } from 'design/ToolTip';
import styled, { useTheme } from 'styled-components';
import { MenuButton, MenuItem } from 'shared/components/MenuAction';
@@ -326,7 +326,7 @@ const Section = ({
{/* TODO(bl-nero): Show validation result in the summary. */}
{title}
- {tooltip && {tooltip} }
+ {tooltip && {tooltip} }
{removable && (
Date: Thu, 28 Nov 2024 19:14:39 +0100
Subject: [PATCH 06/14] Also, rename the tooltip directory
---
web/packages/design/src/{ToolTip => Tooltip}/HoverTooltip.tsx | 0
.../{ToolTip/ToolTip.story.tsx => Tooltip/Tooltip.story.tsx} | 0
.../design/src/{ToolTip/ToolTip.tsx => Tooltip/Tooltip.tsx} | 0
web/packages/design/src/{ToolTip => Tooltip}/index.ts | 0
web/packages/design/src/{ToolTip => Tooltip}/shared.tsx | 0
.../AccessRequests/AccessDuration/AccessDurationRequest.tsx | 2 +-
.../AccessRequests/AccessDuration/AccessDurationReview.tsx | 2 +-
.../NewRequest/RequestCheckout/AdditionalOptions.tsx | 2 +-
.../NewRequest/RequestCheckout/RequestCheckout.tsx | 2 +-
.../components/AccessRequests/NewRequest/ResourceList/Apps.tsx | 2 +-
.../ReviewRequests/RequestView/RequestReview/RequestReview.tsx | 2 +-
.../AccessRequests/ReviewRequests/RequestView/RequestView.tsx | 2 +-
web/packages/shared/components/AccessRequests/Shared/Shared.tsx | 2 +-
.../components/AdvancedSearchToggle/AdvancedSearchToggle.tsx | 2 +-
.../shared/components/ClusterDropdown/ClusterDropdown.tsx | 2 +-
web/packages/shared/components/Controls/MultiselectMenu.tsx | 2 +-
web/packages/shared/components/Controls/SortMenu.tsx | 2 +-
web/packages/shared/components/Controls/ViewModeSwitch.tsx | 2 +-
web/packages/shared/components/FieldInput/FieldInput.tsx | 2 +-
web/packages/shared/components/FieldSelect/shared.tsx | 2 +-
web/packages/shared/components/FieldTextArea/FieldTextArea.tsx | 2 +-
web/packages/shared/components/ToolTip/index.ts | 2 +-
.../components/UnifiedResources/CardsView/ResourceCard.tsx | 2 +-
web/packages/shared/components/UnifiedResources/FilterPanel.tsx | 2 +-
.../components/UnifiedResources/ListView/ResourceListItem.tsx | 2 +-
web/packages/shared/components/UnifiedResources/ResourceTab.tsx | 2 +-
.../shared/components/UnifiedResources/UnifiedResources.tsx | 2 +-
.../shared/components/UnifiedResources/shared/CopyButton.tsx | 2 +-
.../shared/components/UnifiedResources/shared/PinButton.tsx | 2 +-
web/packages/teleport/src/Bots/List/Bots.tsx | 2 +-
.../src/Console/DocumentKubeExec/KubeExecDataDialog.tsx | 2 +-
web/packages/teleport/src/DesktopSession/TopBar.tsx | 2 +-
.../AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx | 2 +-
.../Database/DeployService/AutoDeploy/SelectSecurityGroups.tsx | 2 +-
.../Database/DeployService/AutoDeploy/SelectSubnetIds.tsx | 2 +-
.../Discover/Database/EnrollRdsDatabase/AutoDiscoverToggle.tsx | 2 +-
.../Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx | 2 +-
.../Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx | 2 +-
.../src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx | 2 +-
.../teleport/src/Discover/Shared/Aws/ConfigureIamPerms.tsx | 2 +-
.../ConfigureDiscoveryServiceDirections.tsx | 2 +-
.../Discover/Shared/SecurityGroupPicker/SecurityGroupPicker.tsx | 2 +-
.../src/Integrations/Enroll/AwsOidc/ConfigureAwsOidcSummary.tsx | 2 +-
.../src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx | 2 +-
web/packages/teleport/src/Integrations/IntegrationList.tsx | 2 +-
.../teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx | 2 +-
web/packages/teleport/src/JoinTokens/JoinTokens.tsx | 2 +-
web/packages/teleport/src/JoinTokens/UpsertJoinTokenDialog.tsx | 2 +-
web/packages/teleport/src/Navigation/Navigation.tsx | 2 +-
web/packages/teleport/src/Navigation/SideNavigation/Section.tsx | 2 +-
web/packages/teleport/src/Notifications/Notifications.tsx | 2 +-
web/packages/teleport/src/Roles/RoleEditor/EditorHeader.tsx | 2 +-
web/packages/teleport/src/Roles/RoleEditor/Shared.tsx | 2 +-
web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx | 2 +-
web/packages/teleport/src/TopBar/TopBar.tsx | 2 +-
web/packages/teleport/src/TopBar/TopBarSideNav.tsx | 2 +-
.../ExternalAuditStorageCta/ExternalAuditStorageCta.tsx | 2 +-
57 files changed, 52 insertions(+), 52 deletions(-)
rename web/packages/design/src/{ToolTip => Tooltip}/HoverTooltip.tsx (100%)
rename web/packages/design/src/{ToolTip/ToolTip.story.tsx => Tooltip/Tooltip.story.tsx} (100%)
rename web/packages/design/src/{ToolTip/ToolTip.tsx => Tooltip/Tooltip.tsx} (100%)
rename web/packages/design/src/{ToolTip => Tooltip}/index.ts (100%)
rename web/packages/design/src/{ToolTip => Tooltip}/shared.tsx (100%)
diff --git a/web/packages/design/src/ToolTip/HoverTooltip.tsx b/web/packages/design/src/Tooltip/HoverTooltip.tsx
similarity index 100%
rename from web/packages/design/src/ToolTip/HoverTooltip.tsx
rename to web/packages/design/src/Tooltip/HoverTooltip.tsx
diff --git a/web/packages/design/src/ToolTip/ToolTip.story.tsx b/web/packages/design/src/Tooltip/Tooltip.story.tsx
similarity index 100%
rename from web/packages/design/src/ToolTip/ToolTip.story.tsx
rename to web/packages/design/src/Tooltip/Tooltip.story.tsx
diff --git a/web/packages/design/src/ToolTip/ToolTip.tsx b/web/packages/design/src/Tooltip/Tooltip.tsx
similarity index 100%
rename from web/packages/design/src/ToolTip/ToolTip.tsx
rename to web/packages/design/src/Tooltip/Tooltip.tsx
diff --git a/web/packages/design/src/ToolTip/index.ts b/web/packages/design/src/Tooltip/index.ts
similarity index 100%
rename from web/packages/design/src/ToolTip/index.ts
rename to web/packages/design/src/Tooltip/index.ts
diff --git a/web/packages/design/src/ToolTip/shared.tsx b/web/packages/design/src/Tooltip/shared.tsx
similarity index 100%
rename from web/packages/design/src/ToolTip/shared.tsx
rename to web/packages/design/src/Tooltip/shared.tsx
diff --git a/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx b/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx
index 015d34c9a476c..c9729b80eabda 100644
--- a/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx
+++ b/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx
@@ -19,7 +19,7 @@
import React from 'react';
import { Flex, LabelInput, Text } from 'design';
-import { TooltipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/Tooltip';
import Select, { Option } from 'shared/components/Select';
diff --git a/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationReview.tsx b/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationReview.tsx
index fd06e66ec96e1..aa10ee5f024a9 100644
--- a/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationReview.tsx
+++ b/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationReview.tsx
@@ -19,7 +19,7 @@
import React from 'react';
import { Flex, Text } from 'design';
-import { TooltipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/Tooltip';
import { AccessRequest } from 'shared/services/accessRequests';
diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx
index e42a3c160f82e..c500410532da5 100644
--- a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx
+++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx
@@ -20,7 +20,7 @@ import React, { useState } from 'react';
import { Flex, Text, ButtonIcon, Box, LabelInput } from 'design';
import * as Icon from 'design/Icon';
-import { TooltipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/Tooltip';
import Select, { Option } from 'shared/components/Select';
diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx
index 0de1808937410..fd4bc1578869d 100644
--- a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx
+++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout.tsx
@@ -39,7 +39,7 @@ import { ArrowBack, ChevronDown, ChevronRight, Warning } from 'design/Icon';
import Table, { Cell } from 'design/DataTable';
import { Danger } from 'design/Alert';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
import Validation, { useRule, Validator } from 'shared/components/Validation';
import { Attempt } from 'shared/hooks/useAttemptNext';
diff --git a/web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Apps.tsx b/web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Apps.tsx
index 71f16dacdc6d3..28a8105f67c02 100644
--- a/web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Apps.tsx
+++ b/web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Apps.tsx
@@ -24,7 +24,7 @@ import { ClickableLabelCell, Cell } from 'design/DataTable';
import { App } from 'teleport/services/apps';
-import { TooltipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/Tooltip';
import Select, {
Option as BaseOption,
diff --git a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.tsx b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.tsx
index e7ce487a17f4b..c21e4295c3afc 100644
--- a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.tsx
+++ b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.tsx
@@ -22,7 +22,7 @@ import { ButtonPrimary, Text, Box, Alert, Flex, Label, H3 } from 'design';
import { Warning } from 'design/Icon';
import { Radio } from 'design/RadioGroup';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
import Validation, { Validator } from 'shared/components/Validation';
import { FieldSelect } from 'shared/components/FieldSelect';
diff --git a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.tsx b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.tsx
index 2111bbceae983..9a3e424787378 100644
--- a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.tsx
+++ b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.tsx
@@ -42,7 +42,7 @@ import { displayDateWithPrefixedTime } from 'design/datetime';
import { LabelKind } from 'design/LabelState/LabelState';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
import { hasFinished, Attempt } from 'shared/hooks/useAsync';
diff --git a/web/packages/shared/components/AccessRequests/Shared/Shared.tsx b/web/packages/shared/components/AccessRequests/Shared/Shared.tsx
index 5e035bb5cdb8d..2159f6309c95e 100644
--- a/web/packages/shared/components/AccessRequests/Shared/Shared.tsx
+++ b/web/packages/shared/components/AccessRequests/Shared/Shared.tsx
@@ -21,7 +21,7 @@ import { ButtonPrimary, Text, Box, ButtonIcon, Menu } from 'design';
import { Info } from 'design/Icon';
import { displayDateWithPrefixedTime } from 'design/datetime';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
import { AccessRequest } from 'shared/services/accessRequests';
diff --git a/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.tsx b/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.tsx
index 2cbfe29821118..8756ccbf3ddb5 100644
--- a/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.tsx
+++ b/web/packages/shared/components/AdvancedSearchToggle/AdvancedSearchToggle.tsx
@@ -22,7 +22,7 @@ import { Text, Toggle, Link, Flex, H2 } from 'design';
import { P } from 'design/Text/Text';
-import { TooltipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/Tooltip';
const GUIDE_URL =
'https://goteleport.com/docs/reference/predicate-language/#resource-filtering';
diff --git a/web/packages/shared/components/ClusterDropdown/ClusterDropdown.tsx b/web/packages/shared/components/ClusterDropdown/ClusterDropdown.tsx
index 090a7c6fd8813..60846510fe90d 100644
--- a/web/packages/shared/components/ClusterDropdown/ClusterDropdown.tsx
+++ b/web/packages/shared/components/ClusterDropdown/ClusterDropdown.tsx
@@ -24,7 +24,7 @@ import { ChevronDown } from 'design/Icon';
import cfg from 'teleport/config';
import { Cluster } from 'teleport/services/clusters';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
export interface ClusterDropdownProps {
clusterLoader: ClusterLoader;
diff --git a/web/packages/shared/components/Controls/MultiselectMenu.tsx b/web/packages/shared/components/Controls/MultiselectMenu.tsx
index fb73ad0d1293c..98acea2a75d28 100644
--- a/web/packages/shared/components/Controls/MultiselectMenu.tsx
+++ b/web/packages/shared/components/Controls/MultiselectMenu.tsx
@@ -29,7 +29,7 @@ import {
import { ChevronDown } from 'design/Icon';
import { CheckboxInput } from 'design/Checkbox';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
type MultiselectMenuProps = {
options: {
diff --git a/web/packages/shared/components/Controls/SortMenu.tsx b/web/packages/shared/components/Controls/SortMenu.tsx
index fcb91790ed69a..a55dabad7929c 100644
--- a/web/packages/shared/components/Controls/SortMenu.tsx
+++ b/web/packages/shared/components/Controls/SortMenu.tsx
@@ -20,7 +20,7 @@ import React, { useState } from 'react';
import { ButtonBorder, Flex, Menu, MenuItem } from 'design';
import { ArrowDown, ArrowUp } from 'design/Icon';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
type SortMenuSort = {
fieldName: Exclude;
diff --git a/web/packages/shared/components/Controls/ViewModeSwitch.tsx b/web/packages/shared/components/Controls/ViewModeSwitch.tsx
index 51ad9ae396db4..62e5f94b36a3a 100644
--- a/web/packages/shared/components/Controls/ViewModeSwitch.tsx
+++ b/web/packages/shared/components/Controls/ViewModeSwitch.tsx
@@ -22,7 +22,7 @@ import { Rows, SquaresFour } from 'design/Icon';
import { ViewMode } from 'gen-proto-ts/teleport/userpreferences/v1/unified_resource_preferences_pb';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
export const ViewModeSwitch = ({
currentViewMode,
diff --git a/web/packages/shared/components/FieldInput/FieldInput.tsx b/web/packages/shared/components/FieldInput/FieldInput.tsx
index 367e836e11fa0..5ac9d4185c61c 100644
--- a/web/packages/shared/components/FieldInput/FieldInput.tsx
+++ b/web/packages/shared/components/FieldInput/FieldInput.tsx
@@ -28,7 +28,7 @@ import styled, { useTheme } from 'styled-components';
import { IconProps } from 'design/Icon/Icon';
import { InputMode, InputSize, InputType } from 'design/Input';
-import { TooltipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/Tooltip';
import { useRule } from 'shared/components/Validation';
diff --git a/web/packages/shared/components/FieldSelect/shared.tsx b/web/packages/shared/components/FieldSelect/shared.tsx
index eabfd1a75e412..3c3b9c4087ecc 100644
--- a/web/packages/shared/components/FieldSelect/shared.tsx
+++ b/web/packages/shared/components/FieldSelect/shared.tsx
@@ -25,7 +25,7 @@ import LabelInput from 'design/LabelInput';
import Flex from 'design/Flex';
-import { TooltipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/Tooltip';
import { HelperTextLine } from '../FieldInput/FieldInput';
import { useRule } from '../Validation';
diff --git a/web/packages/shared/components/FieldTextArea/FieldTextArea.tsx b/web/packages/shared/components/FieldTextArea/FieldTextArea.tsx
index 26f9e54032cde..8c73f80ea5f3e 100644
--- a/web/packages/shared/components/FieldTextArea/FieldTextArea.tsx
+++ b/web/packages/shared/components/FieldTextArea/FieldTextArea.tsx
@@ -27,7 +27,7 @@ import { TextAreaSize } from 'design/TextArea';
import { BoxProps } from 'design/Box';
-import { TooltipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/Tooltip';
import { useRule } from 'shared/components/Validation';
diff --git a/web/packages/shared/components/ToolTip/index.ts b/web/packages/shared/components/ToolTip/index.ts
index 0e038d19f4f29..da5647c5d0af2 100644
--- a/web/packages/shared/components/ToolTip/index.ts
+++ b/web/packages/shared/components/ToolTip/index.ts
@@ -22,4 +22,4 @@ export {
/** @deprecated Use `HoverTooltip` from `design/Tooltip` */
HoverTooltip,
-} from 'design/ToolTip';
+} from 'design/Tooltip';
diff --git a/web/packages/shared/components/UnifiedResources/CardsView/ResourceCard.tsx b/web/packages/shared/components/UnifiedResources/CardsView/ResourceCard.tsx
index 8daaaed8c4e40..b592e0acddca0 100644
--- a/web/packages/shared/components/UnifiedResources/CardsView/ResourceCard.tsx
+++ b/web/packages/shared/components/UnifiedResources/CardsView/ResourceCard.tsx
@@ -26,7 +26,7 @@ import { ResourceIcon } from 'design/ResourceIcon';
import { makeLabelTag } from 'teleport/components/formatters';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
import { ResourceItemProps } from '../types';
import { PinButton } from '../shared/PinButton';
diff --git a/web/packages/shared/components/UnifiedResources/FilterPanel.tsx b/web/packages/shared/components/UnifiedResources/FilterPanel.tsx
index 133693fb09850..d470abe9a9e9e 100644
--- a/web/packages/shared/components/UnifiedResources/FilterPanel.tsx
+++ b/web/packages/shared/components/UnifiedResources/FilterPanel.tsx
@@ -26,7 +26,7 @@ import { ChevronDown, ArrowsIn, ArrowsOut, Refresh } from 'design/Icon';
import { ViewMode } from 'gen-proto-ts/teleport/userpreferences/v1/unified_resource_preferences_pb';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
import { SortMenu } from 'shared/components/Controls/SortMenu';
import { ViewModeSwitch } from 'shared/components/Controls/ViewModeSwitch';
diff --git a/web/packages/shared/components/UnifiedResources/ListView/ResourceListItem.tsx b/web/packages/shared/components/UnifiedResources/ListView/ResourceListItem.tsx
index 70784f7b2d6df..582a7c5ece831 100644
--- a/web/packages/shared/components/UnifiedResources/ListView/ResourceListItem.tsx
+++ b/web/packages/shared/components/UnifiedResources/ListView/ResourceListItem.tsx
@@ -26,7 +26,7 @@ import { ResourceIcon } from 'design/ResourceIcon';
import { makeLabelTag } from 'teleport/components/formatters';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
import { ResourceItemProps } from '../types';
import { PinButton } from '../shared/PinButton';
diff --git a/web/packages/shared/components/UnifiedResources/ResourceTab.tsx b/web/packages/shared/components/UnifiedResources/ResourceTab.tsx
index d32879e07d961..2deb131ffcde9 100644
--- a/web/packages/shared/components/UnifiedResources/ResourceTab.tsx
+++ b/web/packages/shared/components/UnifiedResources/ResourceTab.tsx
@@ -20,7 +20,7 @@ import React from 'react';
import styled from 'styled-components';
import { Box, Text } from 'design';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
import { PINNING_NOT_SUPPORTED_MESSAGE } from './UnifiedResources';
diff --git a/web/packages/shared/components/UnifiedResources/UnifiedResources.tsx b/web/packages/shared/components/UnifiedResources/UnifiedResources.tsx
index 716d6c1963273..a9f0699018d42 100644
--- a/web/packages/shared/components/UnifiedResources/UnifiedResources.tsx
+++ b/web/packages/shared/components/UnifiedResources/UnifiedResources.tsx
@@ -43,7 +43,7 @@ import {
AvailableResourceMode,
} from 'gen-proto-ts/teleport/userpreferences/v1/unified_resource_preferences_pb';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
import {
makeEmptyAttempt,
diff --git a/web/packages/shared/components/UnifiedResources/shared/CopyButton.tsx b/web/packages/shared/components/UnifiedResources/shared/CopyButton.tsx
index 2b0076bfc749d..43b9cb2217165 100644
--- a/web/packages/shared/components/UnifiedResources/shared/CopyButton.tsx
+++ b/web/packages/shared/components/UnifiedResources/shared/CopyButton.tsx
@@ -22,7 +22,7 @@ import ButtonIcon from 'design/ButtonIcon';
import { Check, Copy } from 'design/Icon';
import { copyToClipboard } from 'design/utils/copyToClipboard';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
export function CopyButton({
name,
diff --git a/web/packages/shared/components/UnifiedResources/shared/PinButton.tsx b/web/packages/shared/components/UnifiedResources/shared/PinButton.tsx
index 713f4d38f2e71..cde3b87142c04 100644
--- a/web/packages/shared/components/UnifiedResources/shared/PinButton.tsx
+++ b/web/packages/shared/components/UnifiedResources/shared/PinButton.tsx
@@ -21,7 +21,7 @@ import React, { useRef } from 'react';
import { PushPinFilled, PushPin } from 'design/Icon';
import ButtonIcon from 'design/ButtonIcon';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
import { PinningSupport } from '../types';
diff --git a/web/packages/teleport/src/Bots/List/Bots.tsx b/web/packages/teleport/src/Bots/List/Bots.tsx
index d460ce277ede8..594a1840d7c52 100644
--- a/web/packages/teleport/src/Bots/List/Bots.tsx
+++ b/web/packages/teleport/src/Bots/List/Bots.tsx
@@ -19,7 +19,7 @@
import React, { useEffect, useState } from 'react';
import { useAttemptNext } from 'shared/hooks';
import { Link } from 'react-router-dom';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
import { Alert, Box, Button, Indicator } from 'design';
import {
diff --git a/web/packages/teleport/src/Console/DocumentKubeExec/KubeExecDataDialog.tsx b/web/packages/teleport/src/Console/DocumentKubeExec/KubeExecDataDialog.tsx
index 24bca7f59c036..507b5fb34d4ab 100644
--- a/web/packages/teleport/src/Console/DocumentKubeExec/KubeExecDataDialog.tsx
+++ b/web/packages/teleport/src/Console/DocumentKubeExec/KubeExecDataDialog.tsx
@@ -35,7 +35,7 @@ import {
import Validation from 'shared/components/Validation';
import FieldInput from 'shared/components/FieldInput';
import { requiredField } from 'shared/components/Validation/rules';
-import { TooltipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/Tooltip';
type Props = {
onClose(): void;
diff --git a/web/packages/teleport/src/DesktopSession/TopBar.tsx b/web/packages/teleport/src/DesktopSession/TopBar.tsx
index e8a85a87f2c49..dcd5beaec4881 100644
--- a/web/packages/teleport/src/DesktopSession/TopBar.tsx
+++ b/web/packages/teleport/src/DesktopSession/TopBar.tsx
@@ -21,7 +21,7 @@ import { useTheme } from 'styled-components';
import { Text, TopNav, Flex } from 'design';
import { Clipboard, FolderShared } from 'design/Icon';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
import ActionMenu from './ActionMenu';
import { AlertDropdown } from './AlertDropdown';
diff --git a/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx b/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx
index d388481b547c2..d84b32563b990 100644
--- a/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx
+++ b/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx
@@ -21,7 +21,7 @@ import styled from 'styled-components';
import { Box, Flex, Link, Mark, H3 } from 'design';
import TextEditor from 'shared/components/TextEditor';
import { Danger } from 'design/Alert';
-import { TooltipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/Tooltip';
import { useAsync } from 'shared/hooks/useAsync';
import { P } from 'design/Text/Text';
diff --git a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSecurityGroups.tsx b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSecurityGroups.tsx
index 5cbabf6e97147..0c64bcb482670 100644
--- a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSecurityGroups.tsx
+++ b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSecurityGroups.tsx
@@ -21,7 +21,7 @@ import React, { useState, useEffect } from 'react';
import { Text, Flex, Box, Indicator, ButtonSecondary, Subtitle3 } from 'design';
import * as Icons from 'design/Icon';
import { FetchStatus } from 'design/DataTable/types';
-import { HoverTooltip, TooltipInfo } from 'design/ToolTip';
+import { HoverTooltip, TooltipInfo } from 'design/Tooltip';
import useAttempt from 'shared/hooks/useAttemptNext';
import { getErrMessage } from 'shared/utils/errorType';
import { pluralize } from 'shared/utils/text';
diff --git a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSubnetIds.tsx b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSubnetIds.tsx
index 7d8af78645290..f13e5de573a21 100644
--- a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSubnetIds.tsx
+++ b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSubnetIds.tsx
@@ -29,7 +29,7 @@ import {
} from 'design';
import * as Icons from 'design/Icon';
import { FetchStatus } from 'design/DataTable/types';
-import { HoverTooltip, TooltipInfo } from 'design/ToolTip';
+import { HoverTooltip, TooltipInfo } from 'design/Tooltip';
import { pluralize } from 'shared/utils/text';
import useAttempt from 'shared/hooks/useAttemptNext';
import { getErrMessage } from 'shared/utils/errorType';
diff --git a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AutoDiscoverToggle.tsx b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AutoDiscoverToggle.tsx
index 4272820f64a5a..3efbdd3c5230a 100644
--- a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AutoDiscoverToggle.tsx
+++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AutoDiscoverToggle.tsx
@@ -19,7 +19,7 @@
import React from 'react';
import { Box, Toggle } from 'design';
-import { TooltipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/Tooltip';
export function AutoDiscoverToggle({
wantAutoDiscover,
diff --git a/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx b/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx
index 3fb5d8a513cf3..fd5d3c9ae47cb 100644
--- a/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx
+++ b/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx
@@ -31,7 +31,7 @@ import { FetchStatus } from 'design/DataTable/types';
import { Danger } from 'design/Alert';
import useAttempt from 'shared/hooks/useAttemptNext';
-import { TooltipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/Tooltip';
import { getErrMessage } from 'shared/utils/errorType';
import { EksMeta, useDiscover } from 'teleport/Discover/useDiscover';
diff --git a/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
index 4cf227456669b..fcf513c35da04 100644
--- a/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
+++ b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
@@ -30,7 +30,7 @@ import {
import styled from 'styled-components';
import { Danger, Info } from 'design/Alert';
import TextEditor from 'shared/components/TextEditor';
-import { TooltipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/Tooltip';
import FieldInput from 'shared/components/FieldInput';
import { Rule } from 'shared/components/Validation/rules';
import Validation, { Validator } from 'shared/components/Validation';
diff --git a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx
index fb5df207be1df..a1c7cff720039 100644
--- a/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx
+++ b/web/packages/teleport/src/Discover/Server/EnrollEc2Instance/EnrollEc2Instance.tsx
@@ -25,7 +25,7 @@ import { Danger } from 'design/Alert';
import { OutlineInfo } from 'design/Alert/Alert';
import { getErrMessage } from 'shared/utils/errorType';
-import { TooltipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/Tooltip';
import useTeleport from 'teleport/useTeleport';
import cfg from 'teleport/config';
diff --git a/web/packages/teleport/src/Discover/Shared/Aws/ConfigureIamPerms.tsx b/web/packages/teleport/src/Discover/Shared/Aws/ConfigureIamPerms.tsx
index 323a3feb7323b..46e7127c3dfe7 100644
--- a/web/packages/teleport/src/Discover/Shared/Aws/ConfigureIamPerms.tsx
+++ b/web/packages/teleport/src/Discover/Shared/Aws/ConfigureIamPerms.tsx
@@ -21,7 +21,7 @@ import styled from 'styled-components';
import { Flex, Link, Box, H3 } from 'design';
import { assertUnreachable } from 'shared/utils/assertUnreachable';
import TextEditor from 'shared/components/TextEditor';
-import { TooltipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/Tooltip';
import { P } from 'design/Text/Text';
diff --git a/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/ConfigureDiscoveryServiceDirections.tsx b/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/ConfigureDiscoveryServiceDirections.tsx
index 34bb7a37a0690..7bf32874c9306 100644
--- a/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/ConfigureDiscoveryServiceDirections.tsx
+++ b/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/ConfigureDiscoveryServiceDirections.tsx
@@ -18,7 +18,7 @@
import { Box, Flex, Input, Text, Mark, H3, Subtitle3 } from 'design';
import styled from 'styled-components';
-import { TooltipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/Tooltip';
import React from 'react';
diff --git a/web/packages/teleport/src/Discover/Shared/SecurityGroupPicker/SecurityGroupPicker.tsx b/web/packages/teleport/src/Discover/Shared/SecurityGroupPicker/SecurityGroupPicker.tsx
index 48823788401b1..6760a8a85d01f 100644
--- a/web/packages/teleport/src/Discover/Shared/SecurityGroupPicker/SecurityGroupPicker.tsx
+++ b/web/packages/teleport/src/Discover/Shared/SecurityGroupPicker/SecurityGroupPicker.tsx
@@ -23,7 +23,7 @@ import Table, { Cell } from 'design/DataTable';
import { Danger } from 'design/Alert';
import { CheckboxInput } from 'design/Checkbox';
import { FetchStatus } from 'design/DataTable/types';
-import { TooltipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/Tooltip';
import { Attempt } from 'shared/hooks/useAttemptNext';
diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/ConfigureAwsOidcSummary.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/ConfigureAwsOidcSummary.tsx
index 7480fd7c52248..21487e5add122 100644
--- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/ConfigureAwsOidcSummary.tsx
+++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/ConfigureAwsOidcSummary.tsx
@@ -20,7 +20,7 @@ import React from 'react';
import styled from 'styled-components';
import { Flex, Box, H3, Text } from 'design';
import TextEditor from 'shared/components/TextEditor';
-import { TooltipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/Tooltip';
import useStickyClusterId from 'teleport/useStickyClusterId';
diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx
index d69019a460017..c09cacdd713bc 100644
--- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx
+++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx
@@ -19,7 +19,7 @@
import React from 'react';
import { Text, Flex } from 'design';
import FieldInput from 'shared/components/FieldInput';
-import { TooltipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/Tooltip';
export function S3BucketConfiguration({
s3Bucket,
diff --git a/web/packages/teleport/src/Integrations/IntegrationList.tsx b/web/packages/teleport/src/Integrations/IntegrationList.tsx
index 34022e407a48b..b3eebcf385d64 100644
--- a/web/packages/teleport/src/Integrations/IntegrationList.tsx
+++ b/web/packages/teleport/src/Integrations/IntegrationList.tsx
@@ -24,7 +24,7 @@ import { Link as InternalRouteLink } from 'react-router-dom';
import { Box, Flex } from 'design';
import Table, { Cell } from 'design/DataTable';
import { MenuButton, MenuItem } from 'shared/components/MenuAction';
-import { TooltipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/Tooltip';
import { useAsync } from 'shared/hooks/useAsync';
import { ResourceIcon } from 'design/ResourceIcon';
import { saveOnDisk } from 'shared/utils/saveOnDisk';
diff --git a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx
index 614579d27c6f3..773efe7beb610 100644
--- a/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx
+++ b/web/packages/teleport/src/Integrations/status/AwsOidc/AwsOidcHeader.tsx
@@ -21,7 +21,7 @@ import { Link as InternalLink } from 'react-router-dom';
import { ButtonIcon, Flex, Label, Text } from 'design';
import { ArrowLeft } from 'design/Icon';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
import cfg from 'teleport/config';
import { getStatusAndLabel } from 'teleport/Integrations/helpers';
diff --git a/web/packages/teleport/src/JoinTokens/JoinTokens.tsx b/web/packages/teleport/src/JoinTokens/JoinTokens.tsx
index 68ec5896346af..862085cfd0157 100644
--- a/web/packages/teleport/src/JoinTokens/JoinTokens.tsx
+++ b/web/packages/teleport/src/JoinTokens/JoinTokens.tsx
@@ -42,7 +42,7 @@ import Dialog, {
} from 'design/Dialog';
import { MenuButton } from 'shared/components/MenuAction';
import { Attempt, useAsync } from 'shared/hooks/useAsync';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
import { CopyButton } from 'shared/components/UnifiedResources/shared/CopyButton';
import { useTeleport } from 'teleport';
diff --git a/web/packages/teleport/src/JoinTokens/UpsertJoinTokenDialog.tsx b/web/packages/teleport/src/JoinTokens/UpsertJoinTokenDialog.tsx
index 7d17d7e11e769..357c6a3d59471 100644
--- a/web/packages/teleport/src/JoinTokens/UpsertJoinTokenDialog.tsx
+++ b/web/packages/teleport/src/JoinTokens/UpsertJoinTokenDialog.tsx
@@ -29,7 +29,7 @@ import {
Alert,
} from 'design';
import styled from 'styled-components';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
import { Cross } from 'design/Icon';
import Validation from 'shared/components/Validation';
import FieldInput from 'shared/components/FieldInput';
diff --git a/web/packages/teleport/src/Navigation/Navigation.tsx b/web/packages/teleport/src/Navigation/Navigation.tsx
index 030f14057a2a0..4bd0453b40f74 100644
--- a/web/packages/teleport/src/Navigation/Navigation.tsx
+++ b/web/packages/teleport/src/Navigation/Navigation.tsx
@@ -21,7 +21,7 @@ import styled, { useTheme } from 'styled-components';
import { matchPath, useLocation, useHistory } from 'react-router';
import { Box, Text, Flex } from 'design';
-import { TooltipInfo } from 'design/ToolTip';
+import { TooltipInfo } from 'design/Tooltip';
import cfg from 'teleport/config';
import {
diff --git a/web/packages/teleport/src/Navigation/SideNavigation/Section.tsx b/web/packages/teleport/src/Navigation/SideNavigation/Section.tsx
index e744afba9cff2..509cb0bd112d9 100644
--- a/web/packages/teleport/src/Navigation/SideNavigation/Section.tsx
+++ b/web/packages/teleport/src/Navigation/SideNavigation/Section.tsx
@@ -23,7 +23,7 @@ import styled, { css, useTheme } from 'styled-components';
import { Box, ButtonIcon, Flex, P2, Text } from 'design';
import { Theme } from 'design/theme';
import { ArrowLineLeft } from 'design/Icon';
-import { HoverTooltip, TooltipInfo } from 'design/ToolTip';
+import { HoverTooltip, TooltipInfo } from 'design/Tooltip';
import cfg from 'teleport/config';
diff --git a/web/packages/teleport/src/Notifications/Notifications.tsx b/web/packages/teleport/src/Notifications/Notifications.tsx
index ed31048b4fb14..b64d1460e8041 100644
--- a/web/packages/teleport/src/Notifications/Notifications.tsx
+++ b/web/packages/teleport/src/Notifications/Notifications.tsx
@@ -24,7 +24,7 @@ import { Alert, Box, Flex, Indicator, Text } from 'design';
import { Notification as NotificationIcon, BellRinging } from 'design/Icon';
import Logger from 'shared/libs/logger';
import { useRefClickOutside } from 'shared/hooks/useRefClickOutside';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
import {
useInfiniteScroll,
diff --git a/web/packages/teleport/src/Roles/RoleEditor/EditorHeader.tsx b/web/packages/teleport/src/Roles/RoleEditor/EditorHeader.tsx
index b822474af3e34..541e6f08bfefa 100644
--- a/web/packages/teleport/src/Roles/RoleEditor/EditorHeader.tsx
+++ b/web/packages/teleport/src/Roles/RoleEditor/EditorHeader.tsx
@@ -18,7 +18,7 @@
import React from 'react';
import { Flex, ButtonText, H2 } from 'design';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
import { Trash } from 'design/Icon';
import useTeleport from 'teleport/useTeleport';
diff --git a/web/packages/teleport/src/Roles/RoleEditor/Shared.tsx b/web/packages/teleport/src/Roles/RoleEditor/Shared.tsx
index 41af339cd2f01..3652e87a537ca 100644
--- a/web/packages/teleport/src/Roles/RoleEditor/Shared.tsx
+++ b/web/packages/teleport/src/Roles/RoleEditor/Shared.tsx
@@ -17,7 +17,7 @@
*/
import { Box, ButtonPrimary, ButtonSecondary, Flex } from 'design';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
import useTeleport from 'teleport/useTeleport';
diff --git a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
index 634b55c6b56ce..37af3d1a8bdd8 100644
--- a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
+++ b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
@@ -31,7 +31,7 @@ import FieldInput from 'shared/components/FieldInput';
import Validation, { Validator } from 'shared/components/Validation';
import { requiredField } from 'shared/components/Validation/rules';
import * as Icon from 'design/Icon';
-import { HoverTooltip, TooltipInfo } from 'design/ToolTip';
+import { HoverTooltip, TooltipInfo } from 'design/Tooltip';
import styled, { useTheme } from 'styled-components';
import { MenuButton, MenuItem } from 'shared/components/MenuAction';
diff --git a/web/packages/teleport/src/TopBar/TopBar.tsx b/web/packages/teleport/src/TopBar/TopBar.tsx
index 6ab1e81da3ff9..e19e71ca780cd 100644
--- a/web/packages/teleport/src/TopBar/TopBar.tsx
+++ b/web/packages/teleport/src/TopBar/TopBar.tsx
@@ -23,7 +23,7 @@ import { Flex, Image, Text, TopNav } from 'design';
import { matchPath, useHistory } from 'react-router';
import { Theme } from 'design/theme/themes/types';
import { ArrowLeft, Download, Server, SlidersVertical } from 'design/Icon';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
import useTeleport from 'teleport/useTeleport';
import { UserMenuNav } from 'teleport/components/UserMenuNav';
diff --git a/web/packages/teleport/src/TopBar/TopBarSideNav.tsx b/web/packages/teleport/src/TopBar/TopBarSideNav.tsx
index 9d578eb1011a9..54b8b95bcd8e2 100644
--- a/web/packages/teleport/src/TopBar/TopBarSideNav.tsx
+++ b/web/packages/teleport/src/TopBar/TopBarSideNav.tsx
@@ -22,7 +22,7 @@ import { Link } from 'react-router-dom';
import { Flex, Image, TopNav } from 'design';
import { matchPath, useHistory } from 'react-router';
import { Theme } from 'design/theme/themes/types';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
import useTeleport from 'teleport/useTeleport';
import { UserMenuNav } from 'teleport/components/UserMenuNav';
diff --git a/web/packages/teleport/src/components/ExternalAuditStorageCta/ExternalAuditStorageCta.tsx b/web/packages/teleport/src/components/ExternalAuditStorageCta/ExternalAuditStorageCta.tsx
index ff2add5d944aa..6db1714a2fe41 100644
--- a/web/packages/teleport/src/components/ExternalAuditStorageCta/ExternalAuditStorageCta.tsx
+++ b/web/packages/teleport/src/components/ExternalAuditStorageCta/ExternalAuditStorageCta.tsx
@@ -25,7 +25,7 @@ import { ButtonPrimary, ButtonSecondary } from 'design/Button';
import Flex from 'design/Flex';
import Text from 'design/Text';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
import cfg from 'teleport/config';
import { IntegrationKind } from 'teleport/services/integrations';
From b10b766949ef37dbb5afeb7f9435ac1d7f966cfb Mon Sep 17 00:00:00 2001
From: Bartosz Leper
Date: Thu, 28 Nov 2024 19:44:23 +0100
Subject: [PATCH 07/14] Fix an import
---
web/packages/design/src/SlideTabs/SlideTabs.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/packages/design/src/SlideTabs/SlideTabs.tsx b/web/packages/design/src/SlideTabs/SlideTabs.tsx
index 6ceb912232fc5..e8a2cd8c4e2e0 100644
--- a/web/packages/design/src/SlideTabs/SlideTabs.tsx
+++ b/web/packages/design/src/SlideTabs/SlideTabs.tsx
@@ -21,7 +21,7 @@ import styled, { useTheme } from 'styled-components';
import { Flex, Indicator } from 'design';
import { IconProps } from 'design/Icon/Icon';
-import { HoverTooltip } from 'design/ToolTip';
+import { HoverTooltip } from 'design/Tooltip';
import { Position } from 'design/Popover/Popover';
export function SlideTabs({
From d4c6ce1daa62d3dd3ebf24690e7e51b4be076363 Mon Sep 17 00:00:00 2001
From: Bartosz Leper
Date: Mon, 2 Dec 2024 18:08:01 +0100
Subject: [PATCH 08/14] Review: status-related props, extract StatusIcon
---
web/packages/design/src/Alert/Alert.tsx | 61 +++++++-------
.../design/src/SlideTabs/SlideTabs.story.tsx | 47 ++++++-----
.../design/src/SlideTabs/SlideTabs.tsx | 81 +++++++++++++------
.../src/StatusIcon/StatusIcon.story.tsx | 44 ++++++++++
.../design/src/StatusIcon/StatusIcon.tsx | 78 ++++++++++++++++++
web/packages/design/src/StatusIcon/index.ts | 19 +++++
6 files changed, 248 insertions(+), 82 deletions(-)
create mode 100644 web/packages/design/src/StatusIcon/StatusIcon.story.tsx
create mode 100644 web/packages/design/src/StatusIcon/StatusIcon.tsx
create mode 100644 web/packages/design/src/StatusIcon/index.ts
diff --git a/web/packages/design/src/Alert/Alert.tsx b/web/packages/design/src/Alert/Alert.tsx
index 68c5487b7788e..f8ce4b7269310 100644
--- a/web/packages/design/src/Alert/Alert.tsx
+++ b/web/packages/design/src/Alert/Alert.tsx
@@ -22,6 +22,8 @@ import { style, color, ColorProps } from 'styled-system';
import { IconProps } from 'design/Icon/Icon';
+import { StatusIcon, StatusKind } from 'design/StatusIcon';
+
import { space, SpaceProps, width, WidthProps } from '../system';
import { Theme } from '../theme';
import * as Icon from '../Icon';
@@ -193,7 +195,12 @@ export const Alert = ({
-
+
`
${backgroundColor}
`;
-const AlertIcon = ({
- kind,
- customIcon: CustomIcon,
- ...otherProps
-}: {
- kind: AlertKind | BannerKind;
- customIcon?: React.ComponentType;
-} & IconProps) => {
- const commonProps = { role: 'graphics-symbol', ...otherProps };
- if (CustomIcon) {
- return ;
- }
- switch (kind) {
- case 'success':
- return ;
- case 'danger':
- case 'outline-danger':
- return ;
- case 'info':
- case 'outline-info':
- return ;
- case 'warning':
- case 'outline-warn':
- return ;
- case 'neutral':
- case 'primary':
- return ;
- }
-};
-
const iconContainerStyles = ({
kind,
theme,
@@ -468,7 +445,12 @@ export const Banner = ({
gap={3}
alignItems="center"
>
-
+
{children}
{details}
@@ -525,3 +507,18 @@ const bannerColors = (theme: Theme, kind: BannerKind) => {
};
}
};
+
+const iconKind = (kind: AlertKind | BannerKind): StatusKind => {
+ switch (kind) {
+ case 'outline-danger':
+ return 'danger';
+ case 'outline-warn':
+ return 'warning';
+ case 'outline-info':
+ return 'info';
+ case 'primary':
+ return 'neutral';
+ default:
+ return kind;
+ }
+};
diff --git a/web/packages/design/src/SlideTabs/SlideTabs.story.tsx b/web/packages/design/src/SlideTabs/SlideTabs.story.tsx
index f9e532c2845e2..bb3eb8c49818a 100644
--- a/web/packages/design/src/SlideTabs/SlideTabs.story.tsx
+++ b/web/packages/design/src/SlideTabs/SlideTabs.story.tsx
@@ -18,12 +18,10 @@
import React, { useState } from 'react';
-import { useTheme } from 'styled-components';
-
import * as Icon from 'design/Icon';
import Flex from 'design/Flex';
-import { SlideTabs } from './SlideTabs';
+import { SlideTabs, TabSpec } from './SlideTabs';
export default {
title: 'Design/SlideTabs',
@@ -126,22 +124,28 @@ export const Small = () => {
key: 'alarm',
icon: Icon.AlarmRing,
ariaLabel: 'alarm',
- tooltipContent: 'ring ring',
- tooltipPosition: 'bottom',
+ tooltip: {
+ content: 'ring ring',
+ position: 'bottom',
+ },
},
{
key: 'bots',
icon: Icon.Bots,
ariaLabel: 'bots',
- tooltipContent: 'beep boop',
- tooltipPosition: 'bottom',
+ tooltip: {
+ content: 'beep boop',
+ position: 'bottom',
+ },
},
{
key: 'check',
icon: Icon.Check,
ariaLabel: 'check',
- tooltipContent: 'Do or do not. There is no try.',
- tooltipPosition: 'right',
+ tooltip: {
+ content: 'Do or do not. There is no try.',
+ position: 'right',
+ },
},
]}
size="small"
@@ -175,22 +179,11 @@ export const Small = () => {
};
export const StatusIcons = () => {
- const theme = useTheme();
const [activeIndex, setActiveIndex] = useState(0);
- const tabs = [
- {
- key: 'warning',
- title: 'warning',
- statusIcon: Icon.Warning,
- statusIconColor: theme.colors.interactive.solid.alert.default,
- },
- {
- key: 'danger',
- title: 'danger',
- statusIcon: Icon.WarningCircle,
- statusIconColor: theme.colors.interactive.solid.danger.default,
- },
- { key: 'neutral', title: 'neutral', statusIcon: Icon.Bubble },
+ const tabs: TabSpec[] = [
+ { key: 'warning', title: 'warning', status: { kind: 'warning' } },
+ { key: 'danger', title: 'danger', status: { kind: 'danger' } },
+ { key: 'neutral', title: 'neutral', status: { kind: 'neutral' } },
];
return (
@@ -205,6 +198,12 @@ export const StatusIcons = () => {
activeIndex={activeIndex}
onChange={setActiveIndex}
/>
+
);
};
diff --git a/web/packages/design/src/SlideTabs/SlideTabs.tsx b/web/packages/design/src/SlideTabs/SlideTabs.tsx
index e8a2cd8c4e2e0..888c1a6dcbbf1 100644
--- a/web/packages/design/src/SlideTabs/SlideTabs.tsx
+++ b/web/packages/design/src/SlideTabs/SlideTabs.tsx
@@ -23,6 +23,7 @@ import { Flex, Indicator } from 'design';
import { IconProps } from 'design/Icon/Icon';
import { HoverTooltip } from 'design/Tooltip';
import { Position } from 'design/Popover/Popover';
+import { StatusIcon, StatusKind } from 'design/StatusIcon';
export function SlideTabs({
appearance = 'square',
@@ -33,6 +34,7 @@ export function SlideTabs({
isProcessing = false,
disabled = false,
fitContent = false,
+ hideStatusIconOnActiveTab,
}: SlideTabsProps) {
const theme = useTheme();
const activeTab = useRef(null);
@@ -78,12 +80,15 @@ export function SlideTabs({
icon: Icon,
ariaLabel,
controls,
- tooltipContent,
- tooltipPosition,
- statusIcon,
- statusIconColor = theme.colors.text.main,
- statusIconColorActive = theme.colors.text.primaryInverse,
+ tooltip: {
+ content: tooltipContent,
+ position: tooltipPosition,
+ } = {},
+ status: { kind: statusKind, ariaLabel: statusAriaLabel } = {},
} = toFullTabSpec(tabSpec, tabIndex);
+ const statusIconColorActive = hideStatusIconOnActiveTab
+ ? 'transparent'
+ : theme.colors.text.primaryInverse;
let onClick = undefined;
if (!disabled && !isProcessing) {
@@ -120,11 +125,12 @@ export function SlideTabs({
button, which can be much wider). */}
-
{Icon && }
@@ -189,6 +195,13 @@ export type SlideTabsProps = {
* but instead wraps its contents.
*/
fitContent?: boolean;
+ /**
+ * Hides the status icon on active tab. Note that this is not the same as
+ * simply making some tab's `tabSpec.status` field set conditionally on
+ * whether that tab is active or not. Using this field provides a smooth
+ * animation for transitions between active and inactive state.
+ */
+ hideStatusIconOnActiveTab?: boolean;
};
/**
@@ -199,7 +212,7 @@ export type SlideTabsProps = {
* TODO(bl-nero): remove the string option once Enterprise is migrated to
* simplify it a bit.
*/
-type TabSpec = string | FullTabSpec;
+export type TabSpec = string | FullTabSpec;
type FullTabSpec = TabContentSpec & {
/** Iteration key for the tab. */
@@ -210,18 +223,19 @@ type FullTabSpec = TabContentSpec & {
* attribute set to "tabpanel".
*/
controls?: string;
- tooltipContent?: React.ReactNode;
- tooltipPosition?: Position;
+ tooltip?: {
+ content: React.ReactNode;
+ position?: Position;
+ };
/**
* An icon that will be displayed on the side. The layout will stay the same
* whether the icon is there or not. If `isProcessing` prop is set to `true`,
* the icon for an active tab is replaced by a spinner.
*/
- statusIcon?: React.ComponentType;
- /** Color of the status icon. */
- statusIconColor?: string;
- /** Color of the status icon on an active tab. */
- statusIconColorActive?: string;
+ status?: {
+ kind: StatusKind;
+ ariaLabel?: string;
+ };
};
/**
@@ -252,26 +266,41 @@ function toFullTabSpec(spec: TabSpec, index: number): FullTabSpec {
};
}
-function StatusIcon({
- spinner,
- icon: Icon,
+function StatusIconOrSpinner({
+ showSpinner,
+ statusKind,
size,
color,
+ ariaLabel,
}: {
- spinner: boolean;
- icon: React.ComponentType | undefined;
+ showSpinner: boolean;
+ statusKind: StatusKind | undefined;
size: Size;
color: string | undefined;
+ ariaLabel: string | undefined;
}) {
- if (spinner) {
+ if (showSpinner) {
return ;
}
- if (Icon) {
+
+ // This is one of these rare cases when there is a difference between
+ // property being undefined and not present at all: undefined props would
+ // override the default ones, but we want it them to interfere at all.
+ const optionalProps: { color?: string; 'aria-label'?: string } = {};
+ if (color != undefined) {
+ optionalProps.color = color;
+ }
+ if (ariaLabel != undefined) {
+ optionalProps['aria-label'] = ariaLabel;
+ }
+
+ if (statusKind) {
return (
-
);
}
diff --git a/web/packages/design/src/StatusIcon/StatusIcon.story.tsx b/web/packages/design/src/StatusIcon/StatusIcon.story.tsx
new file mode 100644
index 0000000000000..5c9897c6d3896
--- /dev/null
+++ b/web/packages/design/src/StatusIcon/StatusIcon.story.tsx
@@ -0,0 +1,44 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { StoryObj } from '@storybook/react';
+
+import Flex from 'design/Flex';
+
+import { StatusIcon } from '.';
+
+export default {
+ title: 'Design',
+};
+
+export const Story: StoryObj = {
+ name: 'StatusIcon',
+ render() {
+ return (
+
+ {(['neutral', 'danger', 'info', 'warning', 'success'] as const).map(
+ status => (
+
+ {status}
+
+ )
+ )}
+
+ );
+ },
+};
diff --git a/web/packages/design/src/StatusIcon/StatusIcon.tsx b/web/packages/design/src/StatusIcon/StatusIcon.tsx
new file mode 100644
index 0000000000000..08fe46a020b22
--- /dev/null
+++ b/web/packages/design/src/StatusIcon/StatusIcon.tsx
@@ -0,0 +1,78 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import React from 'react';
+
+import { useTheme } from 'styled-components';
+
+import * as Icon from 'design/Icon';
+import { IconProps } from 'design/Icon/Icon';
+
+export type StatusKind = 'neutral' | 'danger' | 'info' | 'warning' | 'success';
+
+export const StatusIcon = ({
+ kind,
+ customIcon: CustomIcon,
+ ...otherProps
+}: {
+ kind: StatusKind;
+ customIcon?: React.ComponentType;
+} & IconProps) => {
+ const commonProps = { role: 'graphics-symbol', ...otherProps };
+ const theme = useTheme();
+
+ if (CustomIcon) {
+ return ;
+ }
+ switch (kind) {
+ case 'success':
+ return (
+
+ );
+ case 'danger':
+ return (
+
+ );
+ case 'info':
+ return (
+
+ );
+ case 'warning':
+ return (
+
+ );
+ case 'neutral':
+ return ;
+ }
+};
diff --git a/web/packages/design/src/StatusIcon/index.ts b/web/packages/design/src/StatusIcon/index.ts
new file mode 100644
index 0000000000000..7c674b723d6af
--- /dev/null
+++ b/web/packages/design/src/StatusIcon/index.ts
@@ -0,0 +1,19 @@
+/**
+ * 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 { StatusIcon, type StatusKind } from './StatusIcon';
From fe8a96047870e8f715844d2f40daa23af6825851 Mon Sep 17 00:00:00 2001
From: Bartosz Leper
Date: Mon, 2 Dec 2024 18:21:59 +0100
Subject: [PATCH 09/14] Update after changing the SlideTabs interface
---
.../src/Roles/RoleEditor/EditorTabs.tsx | 6 ++---
.../Roles/RoleEditor/StandardEditor.test.tsx | 2 +-
.../src/Roles/RoleEditor/StandardEditor.tsx | 27 +++++++------------
3 files changed, 13 insertions(+), 22 deletions(-)
diff --git a/web/packages/teleport/src/Roles/RoleEditor/EditorTabs.tsx b/web/packages/teleport/src/Roles/RoleEditor/EditorTabs.tsx
index b7438c3ce0de9..cf93b5e4945a3 100644
--- a/web/packages/teleport/src/Roles/RoleEditor/EditorTabs.tsx
+++ b/web/packages/teleport/src/Roles/RoleEditor/EditorTabs.tsx
@@ -47,16 +47,14 @@ export const EditorTabs = ({
{
key: 'standard',
icon: Icon.ListAddCheck,
- tooltipContent: standardLabel,
- tooltipPosition: 'bottom',
+ tooltip: { content: standardLabel, position: 'bottom' },
ariaLabel: standardLabel,
controls: standardEditorId,
},
{
key: 'yaml',
icon: Icon.Code,
- tooltipContent: yamlLabel,
- tooltipPosition: 'bottom',
+ tooltip: { content: yamlLabel, position: 'bottom' },
ariaLabel: yamlLabel,
controls: yamlEditorId,
},
diff --git a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx
index 564c7e0df7499..db7f899b99fdb 100644
--- a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx
+++ b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx
@@ -148,7 +148,7 @@ test('invisible tabs still apply validation', async () => {
expect(onSave).not.toHaveBeenCalled();
// Switch back, make it valid.
- await user.click(screen.getByRole('tab', { name: 'Overview' }));
+ await user.click(screen.getByRole('tab', { name: 'Invalid data Overview' }));
await user.type(screen.getByLabelText('Role Name'), 'foo');
await user.click(screen.getByRole('button', { name: 'Create Role' }));
expect(onSave).toHaveBeenCalled();
diff --git a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
index 66353c46deeec..3f487d45876b0 100644
--- a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
+++ b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
@@ -119,7 +119,6 @@ export const StandardEditor = ({
Options,
}
- const theme = useTheme();
const [currentTab, setCurrentTab] = useState(StandardEditorTab.Overview);
const idPrefix = useId();
const overviewTabId = `${idPrefix}-overview`;
@@ -201,44 +200,33 @@ export const StandardEditor = ({
s.valid)
+ status: validation.accessSpecs.every(s => s.valid)
? undefined
- : Icon.WarningCircle,
- statusIconColor:
- theme.colors.interactive.solid.danger.default,
- statusIconColorActive: 'transparent',
+ : validationErrorTabStatus,
},
{
key: StandardEditorTab.AdminRules,
title: 'Admin Rules',
controls: adminRulesTabId,
- statusIconColor:
- theme.colors.interactive.solid.danger.default,
- statusIconColorActive: 'transparent',
},
{
key: StandardEditorTab.Options,
title: 'Options',
controls: optionsTabId,
- statusIconColor:
- theme.colors.interactive.solid.danger.default,
- statusIconColorActive: 'transparent',
},
]}
activeIndex={currentTab}
@@ -337,6 +325,11 @@ export type SectionProps = {
onChange?(value: T): void;
};
+const validationErrorTabStatus = {
+ kind: 'danger',
+ ariaLabel: 'Invalid data',
+} as const;
+
const MetadataSection = ({
value,
isProcessing,
From 0f803c3195b06963cdff64e694dbd73634679838 Mon Sep 17 00:00:00 2001
From: Bartosz Leper
Date: Mon, 2 Dec 2024 19:47:51 +0100
Subject: [PATCH 10/14] Also, rename the tooltip component
---
...ooltip.story.tsx => IconTooltip.story.tsx} | 20 +++++++++----------
.../Tooltip/{Tooltip.tsx => IconTooltip.tsx} | 2 +-
web/packages/design/src/Tooltip/index.ts | 2 +-
.../AccessDuration/AccessDurationRequest.tsx | 6 +++---
.../AccessDuration/AccessDurationReview.tsx | 6 +++---
.../RequestCheckout/AdditionalOptions.tsx | 10 +++++-----
.../NewRequest/ResourceList/Apps.tsx | 6 +++---
.../AdvancedSearchToggle.tsx | 6 +++---
.../components/FieldInput/FieldInput.tsx | 4 ++--
.../shared/components/FieldSelect/shared.tsx | 4 ++--
.../FieldTextArea/FieldTextArea.tsx | 4 ++--
.../shared/components/ToolTip/index.ts | 2 +-
.../DocumentKubeExec/KubeExecDataDialog.tsx | 6 +++---
.../CreateAppAccess/CreateAppAccess.tsx | 6 +++---
.../AutoDeploy/SelectSecurityGroups.tsx | 6 +++---
.../AutoDeploy/SelectSubnetIds.tsx | 6 +++---
.../EnrollRdsDatabase/AutoDiscoverToggle.tsx | 6 +++---
.../EnrollEKSCluster/EnrollEksCluster.tsx | 10 +++++-----
.../DiscoveryConfigSsm/DiscoveryConfigSsm.tsx | 6 +++---
.../EnrollEc2Instance/EnrollEc2Instance.tsx | 6 +++---
.../Discover/Shared/Aws/ConfigureIamPerms.tsx | 6 +++---
.../ConfigureDiscoveryServiceDirections.tsx | 6 +++---
.../SecurityGroupPicker.tsx | 6 +++---
.../AwsOidc/ConfigureAwsOidcSummary.tsx | 6 +++---
.../Enroll/AwsOidc/S3BucketConfiguration.tsx | 6 +++---
.../src/Integrations/IntegrationList.tsx | 4 ++--
.../teleport/src/Navigation/Navigation.tsx | 6 +++---
.../src/Navigation/SideNavigation/Section.tsx | 6 +++---
.../src/Roles/RoleEditor/StandardEditor.tsx | 4 ++--
29 files changed, 87 insertions(+), 87 deletions(-)
rename web/packages/design/src/Tooltip/{Tooltip.story.tsx => IconTooltip.story.tsx} (88%)
rename web/packages/design/src/Tooltip/{Tooltip.tsx => IconTooltip.tsx} (99%)
diff --git a/web/packages/design/src/Tooltip/Tooltip.story.tsx b/web/packages/design/src/Tooltip/IconTooltip.story.tsx
similarity index 88%
rename from web/packages/design/src/Tooltip/Tooltip.story.tsx
rename to web/packages/design/src/Tooltip/IconTooltip.story.tsx
index ceb48d73edfff..8062ae357752a 100644
--- a/web/packages/design/src/Tooltip/Tooltip.story.tsx
+++ b/web/packages/design/src/Tooltip/IconTooltip.story.tsx
@@ -25,7 +25,7 @@ import { P } from 'design/Text/Text';
import AGPLLogoLight from 'design/assets/images/agpl-light.svg';
import AGPLLogoDark from 'design/assets/images/agpl-dark.svg';
-import { TooltipInfo } from './Tooltip';
+import { IconTooltip } from './IconTooltip';
import { HoverTooltip } from './HoverTooltip';
export default {
@@ -38,25 +38,25 @@ export const ShortContent = () => (
Hover the icon
- "some popover content"
+ "some popover content"
Hover the icon
- "some popover content"
+ "some popover content"
Hover the icon
- "some popover content"
+ "some popover content"
Hover the icon
- "some popover content"
+ "some popover content"
);
@@ -78,7 +78,7 @@ export const LongContent = () => {
<>
Hover the icon
-
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
@@ -91,7 +91,7 @@ export const LongContent = () => {
cupidatat non proident, sunt in culpa qui officia deserunt mollit
anim id est laborum.
-
+
Here's some content that shouldn't interfere with the semi-transparent
@@ -120,7 +120,7 @@ export const WithMutedIconColor = () => (
Hover the icon
- "some popover content"
+ "some popover content"
>
);
@@ -129,7 +129,7 @@ export const WithKindWarning = () => (
Hover the icon
- "some popover content"
+ "some popover content"
>
);
@@ -138,7 +138,7 @@ export const WithKindError = () => (
Hover the icon
- "some popover content"
+ "some popover content"
>
);
diff --git a/web/packages/design/src/Tooltip/Tooltip.tsx b/web/packages/design/src/Tooltip/IconTooltip.tsx
similarity index 99%
rename from web/packages/design/src/Tooltip/Tooltip.tsx
rename to web/packages/design/src/Tooltip/IconTooltip.tsx
index 7e227f2cc4bc3..e7434272fe6f3 100644
--- a/web/packages/design/src/Tooltip/Tooltip.tsx
+++ b/web/packages/design/src/Tooltip/IconTooltip.tsx
@@ -27,7 +27,7 @@ import { anchorOriginForPosition, transformOriginForPosition } from './shared';
type ToolTipKind = 'info' | 'warning' | 'error';
-export const TooltipInfo: React.FC<
+export const IconTooltip: React.FC<
PropsWithChildren<{
trigger?: 'click' | 'hover';
position?: Position;
diff --git a/web/packages/design/src/Tooltip/index.ts b/web/packages/design/src/Tooltip/index.ts
index f511148deb903..1be43077b8952 100644
--- a/web/packages/design/src/Tooltip/index.ts
+++ b/web/packages/design/src/Tooltip/index.ts
@@ -16,5 +16,5 @@
* along with this program. If not, see .
*/
-export { TooltipInfo } from './Tooltip';
+export { IconTooltip } from './IconTooltip';
export { HoverTooltip } from './HoverTooltip';
diff --git a/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx b/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx
index c9729b80eabda..35cfac569c462 100644
--- a/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx
+++ b/web/packages/shared/components/AccessRequests/AccessDuration/AccessDurationRequest.tsx
@@ -19,7 +19,7 @@
import React from 'react';
import { Flex, LabelInput, Text } from 'design';
-import { TooltipInfo } from 'design/Tooltip';
+import { IconTooltip } from 'design/Tooltip';
import Select, { Option } from 'shared/components/Select';
@@ -36,11 +36,11 @@ export function AccessDurationRequest({
Access Duration
-
+
How long you would be given elevated privileges. Note that the time it
takes to approve this request will be subtracted from the duration you
requested.
-
+
-
+
How long the access will be granted for after approval.
-
+
);
}
diff --git a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx
index c500410532da5..f27c721e77914 100644
--- a/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx
+++ b/web/packages/shared/components/AccessRequests/NewRequest/RequestCheckout/AdditionalOptions.tsx
@@ -20,7 +20,7 @@ import React, { useState } from 'react';
import { Flex, Text, ButtonIcon, Box, LabelInput } from 'design';
import * as Icon from 'design/Icon';
-import { TooltipInfo } from 'design/Tooltip';
+import { IconTooltip } from 'design/Tooltip';
import Select, { Option } from 'shared/components/Select';
@@ -73,10 +73,10 @@ export function AdditionalOptions({
Request expires if not reviewed in
-
+
The request TTL which is the amount of time this request will
be in the PENDING state before it expires.
-
+
Access Request Lifetime
-
+
The max duration of an access request, starting from its
creation, until it expires.
-
+
{getFormattedDurationTxt({
diff --git a/web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Apps.tsx b/web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Apps.tsx
index 28a8105f67c02..341f37b61ec21 100644
--- a/web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Apps.tsx
+++ b/web/packages/shared/components/AccessRequests/NewRequest/ResourceList/Apps.tsx
@@ -24,7 +24,7 @@ import { ClickableLabelCell, Cell } from 'design/DataTable';
import { App } from 'teleport/services/apps';
-import { TooltipInfo } from 'design/Tooltip';
+import { IconTooltip } from 'design/Tooltip';
import Select, {
Option as BaseOption,
@@ -231,11 +231,11 @@ function ActionCell({
)}
-
+
This application {agent.name} can be alternatively requested by
members of user groups. You can alternatively select user groups
instead to access this application.
-
+
Advanced
-
+
-
+
);
}
diff --git a/web/packages/shared/components/FieldInput/FieldInput.tsx b/web/packages/shared/components/FieldInput/FieldInput.tsx
index 5ac9d4185c61c..1ea08bf39ed8f 100644
--- a/web/packages/shared/components/FieldInput/FieldInput.tsx
+++ b/web/packages/shared/components/FieldInput/FieldInput.tsx
@@ -28,7 +28,7 @@ import styled, { useTheme } from 'styled-components';
import { IconProps } from 'design/Icon/Icon';
import { InputMode, InputSize, InputType } from 'design/Input';
-import { TooltipInfo } from 'design/Tooltip';
+import { IconTooltip } from 'design/Tooltip';
import { useRule } from 'shared/components/Validation';
@@ -114,7 +114,7 @@ const FieldInput = forwardRef(
>
{label}
-
+
>
) : (
<>{label}>
diff --git a/web/packages/shared/components/FieldSelect/shared.tsx b/web/packages/shared/components/FieldSelect/shared.tsx
index 3c3b9c4087ecc..35086cc5c3842 100644
--- a/web/packages/shared/components/FieldSelect/shared.tsx
+++ b/web/packages/shared/components/FieldSelect/shared.tsx
@@ -25,7 +25,7 @@ import LabelInput from 'design/LabelInput';
import Flex from 'design/Flex';
-import { TooltipInfo } from 'design/Tooltip';
+import { IconTooltip } from 'design/Tooltip';
import { HelperTextLine } from '../FieldInput/FieldInput';
import { useRule } from '../Validation';
@@ -96,7 +96,7 @@ export const FieldSelectWrapper = ({
{toolTipContent ? (
{label}
-
+
) : (
label
diff --git a/web/packages/shared/components/FieldTextArea/FieldTextArea.tsx b/web/packages/shared/components/FieldTextArea/FieldTextArea.tsx
index 8c73f80ea5f3e..c8bd7a1e0439d 100644
--- a/web/packages/shared/components/FieldTextArea/FieldTextArea.tsx
+++ b/web/packages/shared/components/FieldTextArea/FieldTextArea.tsx
@@ -27,7 +27,7 @@ import { TextAreaSize } from 'design/TextArea';
import { BoxProps } from 'design/Box';
-import { TooltipInfo } from 'design/Tooltip';
+import { IconTooltip } from 'design/Tooltip';
import { useRule } from 'shared/components/Validation';
@@ -141,7 +141,7 @@ export const FieldTextArea = forwardRef<
>
{label}
-
+
>
) : (
<>{label}>
diff --git a/web/packages/shared/components/ToolTip/index.ts b/web/packages/shared/components/ToolTip/index.ts
index da5647c5d0af2..f1be185cb4ae6 100644
--- a/web/packages/shared/components/ToolTip/index.ts
+++ b/web/packages/shared/components/ToolTip/index.ts
@@ -18,7 +18,7 @@
export {
/** @deprecated Use `TooltipInfo` from `design/Tooltip` */
- TooltipInfo as ToolTipInfo,
+ IconTooltip as ToolTipInfo,
/** @deprecated Use `HoverTooltip` from `design/Tooltip` */
HoverTooltip,
diff --git a/web/packages/teleport/src/Console/DocumentKubeExec/KubeExecDataDialog.tsx b/web/packages/teleport/src/Console/DocumentKubeExec/KubeExecDataDialog.tsx
index 507b5fb34d4ab..e8261cdf026c3 100644
--- a/web/packages/teleport/src/Console/DocumentKubeExec/KubeExecDataDialog.tsx
+++ b/web/packages/teleport/src/Console/DocumentKubeExec/KubeExecDataDialog.tsx
@@ -35,7 +35,7 @@ import {
import Validation from 'shared/components/Validation';
import FieldInput from 'shared/components/FieldInput';
import { requiredField } from 'shared/components/Validation/rules';
-import { TooltipInfo } from 'design/Tooltip';
+import { IconTooltip } from 'design/Tooltip';
type Props = {
onClose(): void;
@@ -123,11 +123,11 @@ function KubeExecDataDialog({ onClose, onExec }: Props) {
Interactive shell
-
+
You can start an interactive shell and have a
bidirectional communication with the target pod, or you
can run one-off command and see its output.
-
+
diff --git a/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx b/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx
index d84b32563b990..2c357df859a58 100644
--- a/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx
+++ b/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx
@@ -21,7 +21,7 @@ import styled from 'styled-components';
import { Box, Flex, Link, Mark, H3 } from 'design';
import TextEditor from 'shared/components/TextEditor';
import { Danger } from 'design/Alert';
-import { TooltipInfo } from 'design/Tooltip';
+import { IconTooltip } from 'design/Tooltip';
import { useAsync } from 'shared/hooks/useAsync';
import { P } from 'design/Text/Text';
@@ -81,7 +81,7 @@ export function CreateAppAccess() {
First configure your AWS IAM permissions
-
+
The following IAM permissions will be added as an inline policy
named {IAM_POLICY_NAME} to IAM role{' '}
{iamRoleName}
@@ -94,7 +94,7 @@ export function CreateAppAccess() {
/>
-
+
Run the command below on your{' '}
diff --git a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSecurityGroups.tsx b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSecurityGroups.tsx
index 0c64bcb482670..1717a082208ba 100644
--- a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSecurityGroups.tsx
+++ b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSecurityGroups.tsx
@@ -21,7 +21,7 @@ import React, { useState, useEffect } from 'react';
import { Text, Flex, Box, Indicator, ButtonSecondary, Subtitle3 } from 'design';
import * as Icons from 'design/Icon';
import { FetchStatus } from 'design/DataTable/types';
-import { HoverTooltip, TooltipInfo } from 'design/Tooltip';
+import { HoverTooltip, IconTooltip } from 'design/Tooltip';
import useAttempt from 'shared/hooks/useAttemptNext';
import { getErrMessage } from 'shared/utils/errorType';
import { pluralize } from 'shared/utils/text';
@@ -126,7 +126,7 @@ export const SelectSecurityGroups = ({
<>
Select ECS Security Groups
-
+
Select ECS security group(s) based on the following requirements:
@@ -141,7 +141,7 @@ export const SelectSecurityGroups = ({
-
+
diff --git a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSubnetIds.tsx b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSubnetIds.tsx
index f13e5de573a21..8a6e93a0491b1 100644
--- a/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSubnetIds.tsx
+++ b/web/packages/teleport/src/Discover/Database/DeployService/AutoDeploy/SelectSubnetIds.tsx
@@ -29,7 +29,7 @@ import {
} from 'design';
import * as Icons from 'design/Icon';
import { FetchStatus } from 'design/DataTable/types';
-import { HoverTooltip, TooltipInfo } from 'design/Tooltip';
+import { HoverTooltip, IconTooltip } from 'design/Tooltip';
import { pluralize } from 'shared/utils/text';
import useAttempt from 'shared/hooks/useAttemptNext';
import { getErrMessage } from 'shared/utils/errorType';
@@ -121,12 +121,12 @@ export function SelectSubnetIds({
<>
Select ECS Subnets
-
+
A subnet has an outbound internet route if it has a route to an
internet gateway or a NAT gateway in a public subnet.
-
+
diff --git a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AutoDiscoverToggle.tsx b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AutoDiscoverToggle.tsx
index 3efbdd3c5230a..617d10ba79790 100644
--- a/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AutoDiscoverToggle.tsx
+++ b/web/packages/teleport/src/Discover/Database/EnrollRdsDatabase/AutoDiscoverToggle.tsx
@@ -19,7 +19,7 @@
import React from 'react';
import { Box, Toggle } from 'design';
-import { TooltipInfo } from 'design/Tooltip';
+import { IconTooltip } from 'design/Tooltip';
export function AutoDiscoverToggle({
wantAutoDiscover,
@@ -40,11 +40,11 @@ export function AutoDiscoverToggle({
Auto-enroll all databases for the selected VPC
-
+
Auto-enroll will automatically identify all RDS databases (e.g.
PostgreSQL, MySQL, Aurora) from the selected VPC and register them as
database resources in your infrastructure.
-
+
);
diff --git a/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx b/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx
index fd5d3c9ae47cb..2505da7275658 100644
--- a/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx
+++ b/web/packages/teleport/src/Discover/Kubernetes/EnrollEKSCluster/EnrollEksCluster.tsx
@@ -31,7 +31,7 @@ import { FetchStatus } from 'design/DataTable/types';
import { Danger } from 'design/Alert';
import useAttempt from 'shared/hooks/useAttemptNext';
-import { TooltipInfo } from 'design/Tooltip';
+import { IconTooltip } from 'design/Tooltip';
import { getErrMessage } from 'shared/utils/errorType';
import { EksMeta, useDiscover } from 'teleport/Discover/useDiscover';
@@ -435,11 +435,11 @@ export function EnrollEksCluster(props: AgentStepProps) {
Enable Kubernetes App Discovery
-
+
Teleport's Kubernetes App Discovery will automatically identify
and enroll to Teleport HTTP applications running inside a
Kubernetes cluster.
-
+
Auto-enroll all EKS clusters for selected region
-
+
Auto-enroll will automatically identify all EKS clusters from
the selected region and register them as Kubernetes resources in
your infrastructure.
-
+
{showTable && (
diff --git a/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
index fcf513c35da04..6846a09779e53 100644
--- a/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
+++ b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
@@ -30,7 +30,7 @@ import {
import styled from 'styled-components';
import { Danger, Info } from 'design/Alert';
import TextEditor from 'shared/components/TextEditor';
-import { TooltipInfo } from 'design/Tooltip';
+import { IconTooltip } from 'design/Tooltip';
import FieldInput from 'shared/components/FieldInput';
import { Rule } from 'shared/components/Validation/rules';
import Validation, { Validator } from 'shared/components/Validation';
@@ -317,7 +317,7 @@ export function DiscoveryConfigSsm() {
{' '}
to configure your IAM permissions.
-
+
The following IAM permissions will be added as an inline
policy named {IAM_POLICY_NAME} to IAM role{' '}
{arnResourceName}
@@ -330,7 +330,7 @@ export function DiscoveryConfigSsm() {
/>
-
+
Auto-enroll all EC2 instances for selected region
-
+
Auto-enroll will automatically identify all EC2 instances from
the selected region and register them as node resources in
your infrastructure.
-
+
{wantAutoDiscover && (
diff --git a/web/packages/teleport/src/Discover/Shared/Aws/ConfigureIamPerms.tsx b/web/packages/teleport/src/Discover/Shared/Aws/ConfigureIamPerms.tsx
index 46e7127c3dfe7..0c244462b7507 100644
--- a/web/packages/teleport/src/Discover/Shared/Aws/ConfigureIamPerms.tsx
+++ b/web/packages/teleport/src/Discover/Shared/Aws/ConfigureIamPerms.tsx
@@ -21,7 +21,7 @@ import styled from 'styled-components';
import { Flex, Link, Box, H3 } from 'design';
import { assertUnreachable } from 'shared/utils/assertUnreachable';
import TextEditor from 'shared/components/TextEditor';
-import { TooltipInfo } from 'design/Tooltip';
+import { IconTooltip } from 'design/Tooltip';
import { P } from 'design/Text/Text';
@@ -179,11 +179,11 @@ export function ConfigureIamPerms({
<>
Configure your AWS IAM permissions
-
+
The following IAM permissions will be added as an inline policy
named {iamPolicyName} to IAM role {iamRoleName}
{editor}
-
+
{msg} Run the command below on your{' '}
diff --git a/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/ConfigureDiscoveryServiceDirections.tsx b/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/ConfigureDiscoveryServiceDirections.tsx
index 7bf32874c9306..1b56b2d69e270 100644
--- a/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/ConfigureDiscoveryServiceDirections.tsx
+++ b/web/packages/teleport/src/Discover/Shared/ConfigureDiscoveryService/ConfigureDiscoveryServiceDirections.tsx
@@ -18,7 +18,7 @@
import { Box, Flex, Input, Text, Mark, H3, Subtitle3 } from 'design';
import styled from 'styled-components';
-import { TooltipInfo } from 'design/Tooltip';
+import { IconTooltip } from 'design/Tooltip';
import React from 'react';
@@ -71,7 +71,7 @@ discovery_service:
Auto-enrolling requires you to configure a{' '}
Discovery Service
-
+
>
@@ -100,7 +100,7 @@ discovery_service:
Step 2
Define a Discovery Group name{' '}
-
+
diff --git a/web/packages/teleport/src/Discover/Shared/SecurityGroupPicker/SecurityGroupPicker.tsx b/web/packages/teleport/src/Discover/Shared/SecurityGroupPicker/SecurityGroupPicker.tsx
index 6760a8a85d01f..890eeee6cc60a 100644
--- a/web/packages/teleport/src/Discover/Shared/SecurityGroupPicker/SecurityGroupPicker.tsx
+++ b/web/packages/teleport/src/Discover/Shared/SecurityGroupPicker/SecurityGroupPicker.tsx
@@ -23,7 +23,7 @@ import Table, { Cell } from 'design/DataTable';
import { Danger } from 'design/Alert';
import { CheckboxInput } from 'design/Checkbox';
import { FetchStatus } from 'design/DataTable/types';
-import { TooltipInfo } from 'design/Tooltip';
+import { IconTooltip } from 'design/Tooltip';
import { Attempt } from 'shared/hooks/useAttemptNext';
@@ -163,13 +163,13 @@ export const SecurityGroupPicker = ({
if (sg.recommended && sg.tips?.length) {
return (
-
+
{sg.tips.map((tip, index) => (
{tip}
))}
-
+
|
);
}
diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/ConfigureAwsOidcSummary.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/ConfigureAwsOidcSummary.tsx
index 21487e5add122..b99521e8719ae 100644
--- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/ConfigureAwsOidcSummary.tsx
+++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/ConfigureAwsOidcSummary.tsx
@@ -20,7 +20,7 @@ import React from 'react';
import styled from 'styled-components';
import { Flex, Box, H3, Text } from 'design';
import TextEditor from 'shared/components/TextEditor';
-import { TooltipInfo } from 'design/Tooltip';
+import { IconTooltip } from 'design/Tooltip';
import useStickyClusterId from 'teleport/useStickyClusterId';
@@ -61,7 +61,7 @@ export function ConfigureAwsOidcSummary({
}`;
return (
-
+
Running the command in AWS CloudShell does the following:
1. Configures an AWS IAM OIDC Identity Provider (IdP)
@@ -76,7 +76,7 @@ export function ConfigureAwsOidcSummary({
/>
-
+
);
}
diff --git a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx
index c09cacdd713bc..47452f3aa720e 100644
--- a/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx
+++ b/web/packages/teleport/src/Integrations/Enroll/AwsOidc/S3BucketConfiguration.tsx
@@ -19,7 +19,7 @@
import React from 'react';
import { Text, Flex } from 'design';
import FieldInput from 'shared/components/FieldInput';
-import { TooltipInfo } from 'design/Tooltip';
+import { IconTooltip } from 'design/Tooltip';
export function S3BucketConfiguration({
s3Bucket,
@@ -32,11 +32,11 @@ export function S3BucketConfiguration({
<>
Amazon S3 Location
-
+
Deprecated. Amazon is now validating the IdP certificate against a
list of root CAs. Storing the OpenID Configuration in S3 is no longer
required, and should be removed to improve security.
-
+
{
{getStatusCodeTitle(item.statusCode)}
{statusDescription && (
- {statusDescription}
+ {statusDescription}
)}
diff --git a/web/packages/teleport/src/Navigation/Navigation.tsx b/web/packages/teleport/src/Navigation/Navigation.tsx
index 4bd0453b40f74..e50295ea5a1f9 100644
--- a/web/packages/teleport/src/Navigation/Navigation.tsx
+++ b/web/packages/teleport/src/Navigation/Navigation.tsx
@@ -21,7 +21,7 @@ import styled, { useTheme } from 'styled-components';
import { matchPath, useLocation, useHistory } from 'react-router';
import { Box, Text, Flex } from 'design';
-import { TooltipInfo } from 'design/Tooltip';
+import { IconTooltip } from 'design/Tooltip';
import cfg from 'teleport/config';
import {
@@ -195,9 +195,9 @@ function LicenseFooter({
{title}
-
+
{infoContent}
-
+
{subText}
diff --git a/web/packages/teleport/src/Navigation/SideNavigation/Section.tsx b/web/packages/teleport/src/Navigation/SideNavigation/Section.tsx
index 509cb0bd112d9..eb9d10c111b82 100644
--- a/web/packages/teleport/src/Navigation/SideNavigation/Section.tsx
+++ b/web/packages/teleport/src/Navigation/SideNavigation/Section.tsx
@@ -23,7 +23,7 @@ import styled, { css, useTheme } from 'styled-components';
import { Box, ButtonIcon, Flex, P2, Text } from 'design';
import { Theme } from 'design/theme';
import { ArrowLineLeft } from 'design/Icon';
-import { HoverTooltip, TooltipInfo } from 'design/Tooltip';
+import { HoverTooltip, IconTooltip } from 'design/Tooltip';
import cfg from 'teleport/config';
@@ -470,9 +470,9 @@ function LicenseFooter({
{title}
-
+
{infoContent}
-
+
{subText}
diff --git a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
index 37af3d1a8bdd8..9eefd10718705 100644
--- a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
+++ b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
@@ -31,7 +31,7 @@ import FieldInput from 'shared/components/FieldInput';
import Validation, { Validator } from 'shared/components/Validation';
import { requiredField } from 'shared/components/Validation/rules';
import * as Icon from 'design/Icon';
-import { HoverTooltip, TooltipInfo } from 'design/Tooltip';
+import { HoverTooltip, IconTooltip } from 'design/Tooltip';
import styled, { useTheme } from 'styled-components';
import { MenuButton, MenuItem } from 'shared/components/MenuAction';
@@ -326,7 +326,7 @@ const Section = ({
{/* TODO(bl-nero): Show validation result in the summary. */}
{title}
- {tooltip && {tooltip} }
+ {tooltip && {tooltip} }
{removable && (
Date: Tue, 3 Dec 2024 16:12:08 +0100
Subject: [PATCH 11/14] review
---
.../components/FieldMultiInput/FieldMultiInput.tsx | 14 +++++++-------
.../shared/components/Validation/Validation.tsx | 6 ++++--
.../shared/components/Validation/rules.test.ts | 4 ++--
web/packages/shared/components/Validation/rules.ts | 4 ++--
.../components/LabelsInput/LabelsInput.test.tsx | 2 +-
.../src/components/LabelsInput/LabelsInput.tsx | 6 +++---
6 files changed, 19 insertions(+), 17 deletions(-)
diff --git a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.tsx b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.tsx
index f323ec7dd268d..48a12d403f313 100644
--- a/web/packages/shared/components/FieldMultiInput/FieldMultiInput.tsx
+++ b/web/packages/shared/components/FieldMultiInput/FieldMultiInput.tsx
@@ -35,13 +35,13 @@ import FieldInput from '../FieldInput';
type StringListValidationResult = ValidationResult & {
/**
- * A list of validation results, one per label. Note: items are optional just
- * because `useRule` by default returns only `ValidationResult`. For the
- * actual validation, it's not optional; if it's undefined, or there are
- * fewer items in this list than the labels, the corresponding items will be
- * treated as valid.
+ * A list of validation results, one per list item. Note: results are
+ * optional just because `useRule` by default returns only
+ * `ValidationResult`. For the actual validation, it's not optional; if it's
+ * undefined, or there are fewer results in this list than the list items,
+ * the corresponding items will be treated as valid.
*/
- items?: ValidationResult[];
+ results?: ValidationResult[];
};
export type FieldMultiInputProps = {
@@ -119,7 +119,7 @@ export function FieldMultiInput({
diff --git a/web/packages/shared/components/Validation/Validation.tsx b/web/packages/shared/components/Validation/Validation.tsx
index b032f452f4dba..e319d061164da 100644
--- a/web/packages/shared/components/Validation/Validation.tsx
+++ b/web/packages/shared/components/Validation/Validation.tsx
@@ -110,7 +110,9 @@ export default class Validator extends Store {
const ValidationContext = React.createContext(undefined);
-type ValidationRenderFunction = (arg: { validator: Validator }) => any;
+type ValidationRenderFunction = (arg: {
+ validator: Validator;
+}) => React.ReactNode;
/**
* Installs a validation context that provides a {@link Validator} store. The
@@ -184,7 +186,7 @@ export function Validation(props: {
export function useValidation(): Validator | undefined {
const validator = React.useContext(ValidationContext);
if (!validator) {
- logger.warn('Missing Validation Context declaration');
+ throw new Error('useValidation() called without a validation context');
}
return useStore(validator);
}
diff --git a/web/packages/shared/components/Validation/rules.test.ts b/web/packages/shared/components/Validation/rules.test.ts
index 1ea9f5a15a541..07ee1bf434d01 100644
--- a/web/packages/shared/components/Validation/rules.test.ts
+++ b/web/packages/shared/components/Validation/rules.test.ts
@@ -190,7 +190,7 @@ test.each([
items: ['a', '', 'c'],
expected: {
valid: false,
- items: [
+ results: [
{ valid: true, message: '' },
{ valid: false, message: 'required' },
{ valid: true, message: '' },
@@ -202,7 +202,7 @@ test.each([
items: ['a', 'b', 'c'],
expected: {
valid: true,
- items: [
+ results: [
{ valid: true, message: '' },
{ valid: true, message: '' },
{ valid: true, message: '' },
diff --git a/web/packages/shared/components/Validation/rules.ts b/web/packages/shared/components/Validation/rules.ts
index 57b07f062a5af..545f28a348fce 100644
--- a/web/packages/shared/components/Validation/rules.ts
+++ b/web/packages/shared/components/Validation/rules.ts
@@ -285,7 +285,7 @@ const requiredAll =
/** A result of the {@link arrayOf} validation rule. */
export type ArrayValidationResult = ValidationResult & {
/** Results of validating each separate item. */
- items: R[];
+ results: R[];
};
/** Validates an array by executing given rule on each of its elements. */
@@ -296,7 +296,7 @@ const arrayOf =
(values: T[]) =>
() => {
const results = values.map(v => elementRule(v)());
- return { items: results, valid: results.every(r => r.valid) };
+ return { results: results, valid: results.every(r => r.valid) };
};
/**
diff --git a/web/packages/teleport/src/components/LabelsInput/LabelsInput.test.tsx b/web/packages/teleport/src/components/LabelsInput/LabelsInput.test.tsx
index 6697f3d5f163b..8f8c07ea95d0c 100644
--- a/web/packages/teleport/src/components/LabelsInput/LabelsInput.test.tsx
+++ b/web/packages/teleport/src/components/LabelsInput/LabelsInput.test.tsx
@@ -183,7 +183,7 @@ describe('validation rules', () => {
}));
return {
valid: results.every(r => r.name.valid && r.value.valid),
- items: results,
+ results: results,
};
};
diff --git a/web/packages/teleport/src/components/LabelsInput/LabelsInput.tsx b/web/packages/teleport/src/components/LabelsInput/LabelsInput.tsx
index c4256375325b7..eee6025249817 100644
--- a/web/packages/teleport/src/components/LabelsInput/LabelsInput.tsx
+++ b/web/packages/teleport/src/components/LabelsInput/LabelsInput.tsx
@@ -51,7 +51,7 @@ type LabelListValidationResult = ValidationResult & {
* fewer items in this list than the labels, a default validation rule will
* be used instead.
*/
- items?: LabelValidationResult[];
+ results?: LabelValidationResult[];
};
type LabelValidationResult = {
@@ -154,7 +154,7 @@ export function LabelsInput({
{labels.map((label, index) => {
const validationItem: LabelValidationResult | undefined =
- validationResult.items?.[index];
+ validationResult.results?.[index];
return (
@@ -246,6 +246,6 @@ export const nonEmptyLabels: LabelsRule = labels => () => {
}));
return {
valid: results.every(r => r.name.valid && r.value.valid),
- items: results,
+ results: results,
};
};
From 4aa406cece28656f82a1cff459f4620ed2b373ed Mon Sep 17 00:00:00 2001
From: Bartosz Leper
Date: Tue, 3 Dec 2024 16:39:27 +0100
Subject: [PATCH 12/14] review
---
.../design/src/SlideTabs/SlideTabs.tsx | 25 ++++++++++---------
1 file changed, 13 insertions(+), 12 deletions(-)
diff --git a/web/packages/design/src/SlideTabs/SlideTabs.tsx b/web/packages/design/src/SlideTabs/SlideTabs.tsx
index 888c1a6dcbbf1..9a81f79fd99ac 100644
--- a/web/packages/design/src/SlideTabs/SlideTabs.tsx
+++ b/web/packages/design/src/SlideTabs/SlideTabs.tsx
@@ -287,24 +287,25 @@ function StatusIconOrSpinner({
// property being undefined and not present at all: undefined props would
// override the default ones, but we want it them to interfere at all.
const optionalProps: { color?: string; 'aria-label'?: string } = {};
- if (color != undefined) {
+ if (color !== undefined) {
optionalProps.color = color;
}
- if (ariaLabel != undefined) {
+ if (ariaLabel !== undefined) {
optionalProps['aria-label'] = ariaLabel;
}
- if (statusKind) {
- return (
-
- );
+ if (!statusKind) {
+ return null;
}
- return null;
+
+ return (
+
+ );
}
const TabSliderInner = styled.div<{ appearance: Appearance }>`
From 17823aa558cf40155ec4518cf6db11ac93589594 Mon Sep 17 00:00:00 2001
From: Bartosz Leper
Date: Tue, 3 Dec 2024 17:27:52 +0100
Subject: [PATCH 13/14] REview
---
.../src/Roles/RoleEditor/EditorHeader.tsx | 6 +-
.../src/Roles/RoleEditor/RoleEditor.tsx | 2 +
.../Roles/RoleEditor/StandardEditor.test.tsx | 2 +-
.../src/Roles/RoleEditor/StandardEditor.tsx | 21 ++-
.../src/Roles/RoleEditor/standardmodel.ts | 141 +--------------
.../src/Roles/RoleEditor/validation.ts | 168 ++++++++++++++++++
.../teleport/src/services/resources/types.ts | 12 ++
7 files changed, 202 insertions(+), 150 deletions(-)
create mode 100644 web/packages/teleport/src/Roles/RoleEditor/validation.ts
diff --git a/web/packages/teleport/src/Roles/RoleEditor/EditorHeader.tsx b/web/packages/teleport/src/Roles/RoleEditor/EditorHeader.tsx
index 52c3255215e33..2ee25880cedad 100644
--- a/web/packages/teleport/src/Roles/RoleEditor/EditorHeader.tsx
+++ b/web/packages/teleport/src/Roles/RoleEditor/EditorHeader.tsx
@@ -52,7 +52,11 @@ export const EditorHeader = ({
return (
- {isCreating ? 'Create a New Role' : role?.metadata.name}
+
+ {isCreating
+ ? 'Create a New Role'
+ : `Edit Role ${role?.metadata.name}`}
+
{isProcessing && }
diff --git a/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx b/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx
index 291db0397b80a..38e2902ac2878 100644
--- a/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx
+++ b/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx
@@ -60,6 +60,8 @@ export const RoleEditor = ({
onDelete,
}: RoleEditorProps) => {
const idPrefix = useId();
+ // These IDs are needed to connect accessibility attributes between the
+ // standard/YAML tab switcher and the switched panels.
const standardEditorId = `${idPrefix}-standard`;
const yamlEditorId = `${idPrefix}-yaml`;
diff --git a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx
index db7f899b99fdb..59a7af9928081 100644
--- a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx
+++ b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.test.tsx
@@ -36,7 +36,6 @@ import {
roleToRoleEditorModel,
ServerAccessSpec,
StandardEditorModel,
- validateAccessSpec,
WindowsDesktopAccessSpec,
} from './standardmodel';
import {
@@ -49,6 +48,7 @@ import {
StandardEditorProps,
WindowsDesktopAccessSpecSection,
} from './StandardEditor';
+import { validateAccessSpec } from './validation';
const TestStandardEditor = (props: Partial) => {
const ctx = createTeleportContext();
diff --git a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
index 2fae996bbe931..bf1567ee235cd 100644
--- a/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
+++ b/web/packages/teleport/src/Roles/RoleEditor/StandardEditor.tsx
@@ -69,6 +69,8 @@ import {
AppAccessSpec,
DatabaseAccessSpec,
WindowsDesktopAccessSpec,
+} from './standardmodel';
+import {
validateRoleEditorModel,
MetadataValidationResult,
AccessSpecValidationResult,
@@ -78,7 +80,7 @@ import {
AppSpecValidationResult,
DatabaseSpecValidationResult,
WindowsDesktopSpecValidationResult,
-} from './standardmodel';
+} from './validation';
import { EditorSaveCancelButton } from './Shared';
import { RequiresResetToStandard } from './RequiresResetToStandard';
@@ -206,17 +208,20 @@ export const StandardEditor = ({
key: StandardEditorTab.Overview,
title: 'Overview',
controls: overviewTabId,
- status: validation.metadata.valid
- ? undefined
- : validationErrorTabStatus,
+ status:
+ validator.state.validating && !validation.metadata.valid
+ ? validationErrorTabStatus
+ : undefined,
},
{
key: StandardEditorTab.Resources,
title: 'Resources',
controls: resourcesTabId,
- status: validation.accessSpecs.every(s => s.valid)
- ? undefined
- : validationErrorTabStatus,
+ status:
+ validator.state.validating &&
+ validation.accessSpecs.some(s => !s.valid)
+ ? validationErrorTabStatus
+ : undefined,
},
{
key: StandardEditorTab.AdminRules,
@@ -613,7 +618,7 @@ export function KubernetesAccessSpecSection({
onChange?.({
diff --git a/web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts b/web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts
index 70cd233bbf083..f50158add2ba6 100644
--- a/web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts
+++ b/web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts
@@ -18,13 +18,6 @@
import { equalsDeep } from 'shared/utils/highbar';
import { Option } from 'shared/components/Select';
-import {
- arrayOf,
- requiredField,
- RuleSetValidationResult,
- runRules,
- ValidationResult,
-} from 'shared/components/Validation/rules';
import {
KubernetesResource,
@@ -32,10 +25,7 @@ import {
Role,
RoleConditions,
} from 'teleport/services/resources';
-import {
- nonEmptyLabels,
- Label as UILabel,
-} from 'teleport/components/LabelsInput/LabelsInput';
+import { Label as UILabel } from 'teleport/components/LabelsInput/LabelsInput';
import {
KubernetesResourceKind,
KubernetesVerb,
@@ -120,14 +110,6 @@ export type KubernetesResourceModel = {
};
type KubernetesResourceKindOption = Option;
-const kubernetesClusterWideResourceKinds: KubernetesResourceKind[] = [
- 'namespace',
- 'kube_node',
- 'persistentvolume',
- 'clusterrole',
- 'clusterrolebinding',
- 'certificatesigningrequest',
-];
/**
* All possible resource kind drop-down options. This array needs to be kept in
@@ -587,124 +569,3 @@ export function hasModifiedFields(
ignoreUndefined: true,
});
}
-
-export function validateRoleEditorModel({
- metadata,
- accessSpecs,
-}: RoleEditorModel) {
- return {
- metadata: validateMetadata(metadata),
- accessSpecs: accessSpecs.map(validateAccessSpec),
- };
-}
-
-function validateMetadata(model: MetadataModel): MetadataValidationResult {
- return runRules(model, metadataRules);
-}
-
-const metadataRules = { name: requiredField('Role name is required') };
-export type MetadataValidationResult = RuleSetValidationResult<
- typeof metadataRules
->;
-
-export function validateAccessSpec(
- spec: AccessSpec
-): AccessSpecValidationResult {
- const { kind } = spec;
- switch (kind) {
- case 'kube_cluster':
- return runRules(spec, kubernetesValidationRules);
- case 'node':
- return runRules(spec, serverValidationRules);
- case 'app':
- return runRules(spec, appSpecValidationRules);
- case 'db':
- return runRules(spec, databaseSpecValidationRules);
- case 'windows_desktop':
- return runRules(spec, windowsDesktopSpecValidationRules);
- default:
- kind satisfies never;
- }
-}
-
-export type AccessSpecValidationResult =
- | ServerSpecValidationResult
- | KubernetesSpecValidationResult
- | AppSpecValidationResult
- | DatabaseSpecValidationResult
- | WindowsDesktopSpecValidationResult;
-
-const validKubernetesResource = (res: KubernetesResourceModel) => () => {
- const name = requiredField(
- 'Resource name is required, use "*" for any resource'
- )(res.name)();
- const namespace = kubernetesClusterWideResourceKinds.includes(res.kind.value)
- ? { valid: true }
- : requiredField('Namespace is required for resources of this kind')(
- res.namespace
- )();
- return {
- valid: name.valid && namespace.valid,
- name,
- namespace,
- };
-};
-export type KubernetesResourceValidationResult = {
- name: ValidationResult;
- namespace: ValidationResult;
-};
-
-const kubernetesValidationRules = {
- labels: nonEmptyLabels,
- resources: arrayOf(validKubernetesResource),
-};
-export type KubernetesSpecValidationResult = RuleSetValidationResult<
- typeof kubernetesValidationRules
->;
-
-const noWildcard = (message: string) => (value: string) => () => {
- const valid = value !== '*';
- return { valid, message: valid ? '' : message };
-};
-
-const noWildcardOptions = (message: string) => (options: Option[]) => () => {
- const valid = options.every(o => o.value !== '*');
- return { valid, message: valid ? '' : message };
-};
-
-const serverValidationRules = {
- labels: nonEmptyLabels,
- logins: noWildcardOptions('Wildcard is not allowed in logins'),
-};
-export type ServerSpecValidationResult = RuleSetValidationResult<
- typeof serverValidationRules
->;
-
-const appSpecValidationRules = {
- labels: nonEmptyLabels,
- awsRoleARNs: arrayOf(noWildcard('Wildcard is not allowed in AWS role ARNs')),
- azureIdentities: arrayOf(
- noWildcard('Wildcard is not allowed in Azure identities')
- ),
- gcpServiceAccounts: arrayOf(
- noWildcard('Wildcard is not allowed in GCP service accounts')
- ),
-};
-export type AppSpecValidationResult = RuleSetValidationResult<
- typeof appSpecValidationRules
->;
-
-const databaseSpecValidationRules = {
- labels: nonEmptyLabels,
- roles: noWildcardOptions('Wildcard is not allowed in database roles'),
-};
-export type DatabaseSpecValidationResult = RuleSetValidationResult<
- typeof databaseSpecValidationRules
->;
-
-const windowsDesktopSpecValidationRules = {
- labels: nonEmptyLabels,
-};
-export type WindowsDesktopSpecValidationResult = RuleSetValidationResult<
- typeof windowsDesktopSpecValidationRules
->;
diff --git a/web/packages/teleport/src/Roles/RoleEditor/validation.ts b/web/packages/teleport/src/Roles/RoleEditor/validation.ts
new file mode 100644
index 0000000000000..95cde89ed036c
--- /dev/null
+++ b/web/packages/teleport/src/Roles/RoleEditor/validation.ts
@@ -0,0 +1,168 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import {
+ arrayOf,
+ requiredField,
+ RuleSetValidationResult,
+ runRules,
+ ValidationResult,
+} from 'shared/components/Validation/rules';
+
+import { Option } from 'shared/components/Select';
+
+import { KubernetesResourceKind } from 'teleport/services/resources';
+
+import { nonEmptyLabels } from 'teleport/components/LabelsInput/LabelsInput';
+
+import {
+ AccessSpec,
+ KubernetesResourceModel,
+ MetadataModel,
+ RoleEditorModel,
+} from './standardmodel';
+
+const kubernetesClusterWideResourceKinds: KubernetesResourceKind[] = [
+ 'namespace',
+ 'kube_node',
+ 'persistentvolume',
+ 'clusterrole',
+ 'clusterrolebinding',
+ 'certificatesigningrequest',
+];
+
+export function validateRoleEditorModel({
+ metadata,
+ accessSpecs,
+}: RoleEditorModel) {
+ return {
+ metadata: validateMetadata(metadata),
+ accessSpecs: accessSpecs.map(validateAccessSpec),
+ };
+}
+
+function validateMetadata(model: MetadataModel): MetadataValidationResult {
+ return runRules(model, metadataRules);
+}
+
+const metadataRules = { name: requiredField('Role name is required') };
+export type MetadataValidationResult = RuleSetValidationResult<
+ typeof metadataRules
+>;
+
+export function validateAccessSpec(
+ spec: AccessSpec
+): AccessSpecValidationResult {
+ const { kind } = spec;
+ switch (kind) {
+ case 'kube_cluster':
+ return runRules(spec, kubernetesValidationRules);
+ case 'node':
+ return runRules(spec, serverValidationRules);
+ case 'app':
+ return runRules(spec, appSpecValidationRules);
+ case 'db':
+ return runRules(spec, databaseSpecValidationRules);
+ case 'windows_desktop':
+ return runRules(spec, windowsDesktopSpecValidationRules);
+ default:
+ kind satisfies never;
+ }
+}
+
+export type AccessSpecValidationResult =
+ | ServerSpecValidationResult
+ | KubernetesSpecValidationResult
+ | AppSpecValidationResult
+ | DatabaseSpecValidationResult
+ | WindowsDesktopSpecValidationResult;
+
+const validKubernetesResource = (res: KubernetesResourceModel) => () => {
+ const name = requiredField(
+ 'Resource name is required, use "*" for any resource'
+ )(res.name)();
+ const namespace = kubernetesClusterWideResourceKinds.includes(res.kind.value)
+ ? { valid: true }
+ : requiredField('Namespace is required for resources of this kind')(
+ res.namespace
+ )();
+ return {
+ valid: name.valid && namespace.valid,
+ name,
+ namespace,
+ };
+};
+export type KubernetesResourceValidationResult = {
+ name: ValidationResult;
+ namespace: ValidationResult;
+};
+
+const kubernetesValidationRules = {
+ labels: nonEmptyLabels,
+ resources: arrayOf(validKubernetesResource),
+};
+export type KubernetesSpecValidationResult = RuleSetValidationResult<
+ typeof kubernetesValidationRules
+>;
+
+const noWildcard = (message: string) => (value: string) => () => {
+ const valid = value !== '*';
+ return { valid, message: valid ? '' : message };
+};
+
+const noWildcardOptions = (message: string) => (options: Option[]) => () => {
+ const valid = options.every(o => o.value !== '*');
+ return { valid, message: valid ? '' : message };
+};
+
+const serverValidationRules = {
+ labels: nonEmptyLabels,
+ logins: noWildcardOptions('Wildcard is not allowed in logins'),
+};
+export type ServerSpecValidationResult = RuleSetValidationResult<
+ typeof serverValidationRules
+>;
+
+const appSpecValidationRules = {
+ labels: nonEmptyLabels,
+ awsRoleARNs: arrayOf(noWildcard('Wildcard is not allowed in AWS role ARNs')),
+ azureIdentities: arrayOf(
+ noWildcard('Wildcard is not allowed in Azure identities')
+ ),
+ gcpServiceAccounts: arrayOf(
+ noWildcard('Wildcard is not allowed in GCP service accounts')
+ ),
+};
+export type AppSpecValidationResult = RuleSetValidationResult<
+ typeof appSpecValidationRules
+>;
+
+const databaseSpecValidationRules = {
+ labels: nonEmptyLabels,
+ roles: noWildcardOptions('Wildcard is not allowed in database roles'),
+};
+export type DatabaseSpecValidationResult = RuleSetValidationResult<
+ typeof databaseSpecValidationRules
+>;
+
+const windowsDesktopSpecValidationRules = {
+ labels: nonEmptyLabels,
+};
+export type WindowsDesktopSpecValidationResult = RuleSetValidationResult<
+ typeof windowsDesktopSpecValidationRules
+>;
diff --git a/web/packages/teleport/src/services/resources/types.ts b/web/packages/teleport/src/services/resources/types.ts
index c14366ac518c9..c5ab066e2c894 100644
--- a/web/packages/teleport/src/services/resources/types.ts
+++ b/web/packages/teleport/src/services/resources/types.ts
@@ -94,6 +94,12 @@ export type KubernetesResource = {
verbs?: KubernetesVerb[];
};
+/**
+ * Supported Kubernetes resource kinds. This type needs to be kept in sync with
+ * `KubernetesResourcesKinds` in `api/types/constants.go, as well as
+ * `kubernetesResourceKindOptions` in
+ * `web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts`.
+ */
export type KubernetesResourceKind =
| '*'
| 'pod'
@@ -118,6 +124,12 @@ export type KubernetesResourceKind =
| 'certificatesigningrequest'
| 'ingress';
+/**
+ * Supported Kubernetes resource verbs. This type needs to be kept in sync with
+ * `KubernetesVerbs` in `api/types/constants.go, as well as
+ * `kubernetesVerbOptions` in
+ * `web/packages/teleport/src/Roles/RoleEditor/standardmodel.ts`.
+ */
export type KubernetesVerb =
| '*'
| 'get'
From 5e020d9010cb036085d997eb8eeb0fbe1047f638 Mon Sep 17 00:00:00 2001
From: Bartosz Leper
Date: Wed, 4 Dec 2024 15:41:42 +0100
Subject: [PATCH 14/14] Never return undefined from useValidation()
---
web/packages/shared/components/Validation/Validation.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/packages/shared/components/Validation/Validation.tsx b/web/packages/shared/components/Validation/Validation.tsx
index e319d061164da..6450c2915a61d 100644
--- a/web/packages/shared/components/Validation/Validation.tsx
+++ b/web/packages/shared/components/Validation/Validation.tsx
@@ -183,7 +183,7 @@ export function Validation(props: {
);
}
-export function useValidation(): Validator | undefined {
+export function useValidation(): Validator {
const validator = React.useContext(ValidationContext);
if (!validator) {
throw new Error('useValidation() called without a validation context');