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 }) => ( + <> + + + + )} + ); } 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}) => ( + * <> + * (...) + * + * + * )} + * + * ); + * } + * ``` + * + * 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. - + 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. - + 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');