mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-28 10:53:13 +01:00
feat: masking support for SingleLineEditor (#2240)
* mask support for SingleLineEditor * add secret visibility toggle button * move visibility toggle into SingleLineComponent Co-authored-by: Liz MacLean <18120837+lizziemac@users.noreply.github.com> * fix eye button focus state * center enabled and secret toggle * fix input field scales to 100% width --------- Co-authored-by: Liz MacLean <18120837+lizziemac@users.noreply.github.com>
This commit is contained in:
parent
7c33fd413e
commit
741250068f
@ -5,7 +5,6 @@ import { useDispatch } from 'react-redux';
|
|||||||
import SingleLineEditor from 'components/SingleLineEditor';
|
import SingleLineEditor from 'components/SingleLineEditor';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
import { uuid } from 'utils/common';
|
import { uuid } from 'utils/common';
|
||||||
import { maskInputValue } from 'utils/collections';
|
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { variableNameRegex } from 'utils/common/regex';
|
import { variableNameRegex } from 'utils/common/regex';
|
||||||
@ -96,10 +95,10 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Enabled</td>
|
<td className="text-center">Enabled</td>
|
||||||
<td>Name</td>
|
<td>Name</td>
|
||||||
<td>Value</td>
|
<td>Value</td>
|
||||||
<td>Secret</td>
|
<td className="text-center">Secret</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -109,7 +108,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
|||||||
<td className="text-center">
|
<td className="text-center">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="mr-3 mousetrap"
|
className="mousetrap"
|
||||||
name={`${index}.enabled`}
|
name={`${index}.enabled`}
|
||||||
checked={variable.enabled}
|
checked={variable.enabled}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
@ -130,23 +129,22 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
|||||||
/>
|
/>
|
||||||
<ErrorMessage name={`${index}.name`} />
|
<ErrorMessage name={`${index}.name`} />
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td className="flex flex-row flex-nowrap">
|
||||||
{variable.secret ? (
|
<div className="overflow-hidden grow w-full relative">
|
||||||
<div className="overflow-hidden text-ellipsis">{maskInputValue(variable.value)}</div>
|
|
||||||
) : (
|
|
||||||
<SingleLineEditor
|
<SingleLineEditor
|
||||||
theme={storedTheme}
|
theme={storedTheme}
|
||||||
collection={collection}
|
collection={collection}
|
||||||
name={`${index}.value`}
|
name={`${index}.value`}
|
||||||
value={variable.value}
|
value={variable.value}
|
||||||
|
isSecret={variable.secret}
|
||||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td className="text-center">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="mr-3 mousetrap"
|
className="mousetrap"
|
||||||
name={`${index}.secret`}
|
name={`${index}.secret`}
|
||||||
checked={variable.secret}
|
checked={variable.secret}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
import { getAllVariables } from 'utils/collections';
|
import { getAllVariables } from 'utils/collections';
|
||||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
import { defineCodeMirrorBrunoVariablesMode, MaskedEditor } from 'utils/common/codemirror';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
import { IconEye, IconEyeOff } from '@tabler/icons';
|
||||||
|
|
||||||
let CodeMirror;
|
let CodeMirror;
|
||||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||||
@ -20,6 +21,10 @@ class SingleLineEditor extends Component {
|
|||||||
this.cachedValue = props.value || '';
|
this.cachedValue = props.value || '';
|
||||||
this.editorRef = React.createRef();
|
this.editorRef = React.createRef();
|
||||||
this.variables = {};
|
this.variables = {};
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
maskInput: props.isSecret || false // Always mask the input by default (if it's a secret)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// Initialize CodeMirror as a single line editor
|
// Initialize CodeMirror as a single line editor
|
||||||
@ -81,8 +86,24 @@ class SingleLineEditor extends Component {
|
|||||||
this.editor.setValue(String(this.props.value) || '');
|
this.editor.setValue(String(this.props.value) || '');
|
||||||
this.editor.on('change', this._onEdit);
|
this.editor.on('change', this._onEdit);
|
||||||
this.addOverlay(variables);
|
this.addOverlay(variables);
|
||||||
|
this._enableMaskedEditor(this.props.isSecret);
|
||||||
|
this.setState({ maskInput: this.props.isSecret });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Enable or disable masking the rendered content of the editor */
|
||||||
|
_enableMaskedEditor = (enabled) => {
|
||||||
|
if (typeof enabled !== 'boolean') return;
|
||||||
|
|
||||||
|
console.log('Enabling masked editor: ' + enabled);
|
||||||
|
if (enabled == true) {
|
||||||
|
if (!this.maskedEditor) this.maskedEditor = new MaskedEditor(this.editor, '*');
|
||||||
|
this.maskedEditor.enable();
|
||||||
|
} else {
|
||||||
|
this.maskedEditor?.disable();
|
||||||
|
this.maskedEditor = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
_onEdit = () => {
|
_onEdit = () => {
|
||||||
if (!this.ignoreChangeEvent && this.editor) {
|
if (!this.ignoreChangeEvent && this.editor) {
|
||||||
this.cachedValue = this.editor.getValue();
|
this.cachedValue = this.editor.getValue();
|
||||||
@ -110,6 +131,12 @@ class SingleLineEditor extends Component {
|
|||||||
this.cachedValue = String(this.props.value);
|
this.cachedValue = String(this.props.value);
|
||||||
this.editor.setValue(String(this.props.value) || '');
|
this.editor.setValue(String(this.props.value) || '');
|
||||||
}
|
}
|
||||||
|
if (!isEqual(this.props.isSecret, prevProps.isSecret)) {
|
||||||
|
// If the secret flag has changed, update the editor to reflect the change
|
||||||
|
this._enableMaskedEditor(this.props.isSecret);
|
||||||
|
// also set the maskInput flag to the new value
|
||||||
|
this.setState({ maskInput: this.props.isSecret });
|
||||||
|
}
|
||||||
this.ignoreChangeEvent = false;
|
this.ignoreChangeEvent = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,8 +150,35 @@ class SingleLineEditor extends Component {
|
|||||||
this.editor.setOption('mode', 'brunovariables');
|
this.editor.setOption('mode', 'brunovariables');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toggleVisibleSecret = () => {
|
||||||
|
const isVisible = !this.state.maskInput;
|
||||||
|
this.setState({ maskInput: isVisible });
|
||||||
|
this._enableMaskedEditor(isVisible);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Eye icon to show/hide the secret value
|
||||||
|
* @returns ReactComponent The eye icon
|
||||||
|
*/
|
||||||
|
secretEye = (isSecret) => {
|
||||||
|
return isSecret === true ? (
|
||||||
|
<button className="mx-2" onClick={() => this.toggleVisibleSecret()}>
|
||||||
|
{this.state.maskInput === true ? (
|
||||||
|
<IconEyeOff size={18} strokeWidth={2} />
|
||||||
|
) : (
|
||||||
|
<IconEye size={18} strokeWidth={2} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <StyledWrapper ref={this.editorRef} className="single-line-editor"></StyledWrapper>;
|
return (
|
||||||
|
<div className="flex flex-row justify-between w-full">
|
||||||
|
<StyledWrapper ref={this.editorRef} className="single-line-editor grow" />
|
||||||
|
{this.secretEye(this.props.isSecret)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default SingleLineEditor;
|
export default SingleLineEditor;
|
||||||
|
@ -12,6 +12,64 @@ const pathFoundInVariables = (path, obj) => {
|
|||||||
return value !== undefined;
|
return value !== undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the render behaviour for a given CodeMirror editor.
|
||||||
|
* Replaces all **rendered** characters, not the actual value, with the provided character.
|
||||||
|
*/
|
||||||
|
export class MaskedEditor {
|
||||||
|
/**
|
||||||
|
* @param {import('codemirror').Editor} editor CodeMirror editor instance
|
||||||
|
* @param {string} maskChar Target character being applied to all content
|
||||||
|
*/
|
||||||
|
constructor(editor, maskChar) {
|
||||||
|
this.editor = editor;
|
||||||
|
this.maskChar = maskChar;
|
||||||
|
this.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set and apply new masking character
|
||||||
|
*/
|
||||||
|
enable = () => {
|
||||||
|
this.enabled = true;
|
||||||
|
this.editor.setValue(this.editor.getValue());
|
||||||
|
this.editor.on('inputRead', this.maskContent);
|
||||||
|
this.update();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Disables masking of the editor field. */
|
||||||
|
disable = () => {
|
||||||
|
this.enabled = false;
|
||||||
|
this.editor.off('inputRead', this.maskContent);
|
||||||
|
this.editor.setValue(this.editor.getValue());
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Updates the rendered content if enabled. */
|
||||||
|
update = () => {
|
||||||
|
if (this.enabled) this.maskContent();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Replaces all rendered characters, with the provided character. */
|
||||||
|
maskContent = () => {
|
||||||
|
const content = this.editor.getValue();
|
||||||
|
this.editor.operation(() => {
|
||||||
|
// Clear previous masked text
|
||||||
|
this.editor.getAllMarks().forEach((mark) => mark.clear());
|
||||||
|
// Apply new masked text
|
||||||
|
for (let i = 0; i < content.length; i++) {
|
||||||
|
if (content[i] !== '\n') {
|
||||||
|
const maskedNode = document.createTextNode(this.maskChar);
|
||||||
|
this.editor.markText(
|
||||||
|
{ line: this.editor.posFromIndex(i).line, ch: this.editor.posFromIndex(i).ch },
|
||||||
|
{ line: this.editor.posFromIndex(i + 1).line, ch: this.editor.posFromIndex(i + 1).ch },
|
||||||
|
{ replacedWith: maskedNode, handleMouseEvents: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const defineCodeMirrorBrunoVariablesMode = (_variables, mode, highlightPathParams) => {
|
export const defineCodeMirrorBrunoVariablesMode = (_variables, mode, highlightPathParams) => {
|
||||||
CodeMirror.defineMode('brunovariables', function (config, parserConfig) {
|
CodeMirror.defineMode('brunovariables', function (config, parserConfig) {
|
||||||
const { pathParams = {}, ...variables } = _variables || {};
|
const { pathParams = {}, ...variables } = _variables || {};
|
||||||
|
Loading…
Reference in New Issue
Block a user