Skip to content

Commit

Permalink
ENH Refactor sudo mode components to reduce code reuse (#1905)
Browse files Browse the repository at this point in the history
  • Loading branch information
GuySartorelli authored Feb 20, 2025
1 parent 1c5bc20 commit a3ec77b
Show file tree
Hide file tree
Showing 8 changed files with 38 additions and 306 deletions.
2 changes: 1 addition & 1 deletion client/dist/js/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion client/dist/styles/bundle.css

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ function SudoModePasswordField(props) {
const {
onSuccess,
autocomplete,
verifyMessage,
} = props;
const passwordFieldRef = createRef();
const [responseMessage, setResponseMessage] = useState('');
Expand Down Expand Up @@ -80,12 +81,16 @@ function SudoModePasswordField(props) {
*/
function renderConfirm() {
const helpLink = clientConfig.helpLink;
let verifyMessageValue = verifyMessage;
if (!verifyMessageValue) {
verifyMessageValue = i18n._t(
'Admin.SUDO_MODE_PASSWORD_FIELD_VERIFY',
'This section is protected and is in read-only mode. Before editing please verify that it\'s you first.'
);
}
return <div className="sudo-mode__notice sudo-mode-password-field__notice--required">
<p className="sudo-mode-password-field__notice-message">
{ i18n._t(
'Admin.SUDO_MODE_PASSWORD_FIELD_VERIFY',
'This section is protected and is in read-only mode. Before editing please verify that it\'s you first.'
) }
{ verifyMessageValue }
{ helpLink && (
<a href={helpLink} className="sudo-mode-password-field__notice-help" target="_blank" rel="noopener noreferrer">
{ i18n._t('Admin.WHATS_THIS', 'What is this?') }
Expand Down Expand Up @@ -150,6 +155,7 @@ function SudoModePasswordField(props) {
}

SudoModePasswordField.propTypes = {
verifyMessage: PropTypes.string,
onSuccess: PropTypes.func.isRequired,
autocomplete: PropTypes.string.isRequired,
};
Expand Down
190 changes: 7 additions & 183 deletions client/src/containers/SudoMode/SudoMode.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Button, InputGroup, InputGroupAddon, Input, FormGroup, Label, FormFeedback } from 'reactstrap';
import { loadComponent } from 'lib/Injector';
import fetch from 'isomorphic-fetch';
import Config from 'lib/Config';
import i18n from 'i18n';
import SudoModePasswordField from '../../components/SudoModePasswordField/SudoModePasswordField';

// See SudoModeController::getClientConfig()
const configSectionKey = 'SilverStripe\\Admin\\SudoModeController';
Expand All @@ -29,78 +28,7 @@ const withSudoMode = (WrappedComponent) => {

this.state = {
active: Config.getSection(configSectionKey).sudoModeActive || false,
showVerification: false,
loading: false,
errorMessage: null,
};

this.handleConfirmNotice = this.handleConfirmNotice.bind(this);
this.handleVerify = this.handleVerify.bind(this);
this.handleVerifyInputKeyPress = this.handleVerifyInputKeyPress.bind(this);

// React 15 compatible ref callback
this.passwordInput = null;
this.setPasswordInput = element => {
this.passwordInput = element;
};
}

/**
* Action called when clicking the button to confirm the sudo mode notice
* and trigger the verification form to be rendered.
*/
handleConfirmNotice() {
this.setState({
showVerification: true,
}, () => this.passwordInput && this.passwordInput.focus());
}

/**
* Action called when the user has entered their password and requested
* verification of sudo mode state.
*/
handleVerify() {
this.setState({
loading: true,
});

const payload = new FormData();
payload.append('SecurityID', Config.get('SecurityID'));
payload.append('Password', this.passwordInput.value);

// Validate the request
fetch(Config.getSection(configSectionKey).endpoints.activate, {
method: 'POST',
body: payload,
}).then(response => response.json().then(result => {
// Happy path, send the user to the wrapped component
if (result.result) {
return this.setState({
loading: false,
active: true,
});
}

// Validation error, show them the message
return this.setState({
loading: false,
errorMessage: result.message,
}, () => this.passwordInput.focus());
}));
}

/**
* Treat pressing enter on the password field the same as clicking the
* verify button.
*
* @param {object} event
*/
handleVerifyInputKeyPress(event) {
if (event.charCode === 13) {
event.stopPropagation();
event.preventDefault();
this.handleVerify();
}
}

/**
Expand All @@ -112,117 +40,13 @@ const withSudoMode = (WrappedComponent) => {
return this.state.active === true;
}

/**
* Renders a notice to the user that they will need to verify themself
* to enter sudo mode and continue to use this functionality.
*
* @returns {HTMLElement}
*/
renderSudoModeNotice() {
const { i18n } = window;
const { showVerification } = this.state;

const helpLink = Config.getSection(configSectionKey).helpLink || null;

return (
<div className="sudo-mode__notice sudo-mode__notice--required">
<p className="sudo-mode__notice-message">
{ i18n._t('Admin.VERIFY_ITS_YOU', 'Verify it\'s you first.') }
{ helpLink && (
<a href={helpLink} className="sudo-mode__notice-help" target="_blank" rel="noopener noreferrer">
{ i18n._t('Admin.WHATS_THIS', 'What is this?') }
</a>
) }
</p>
{ !showVerification && (
<Button
className="sudo-mode__notice-button font-icon-lock"
color="info"
onClick={this.handleConfirmNotice}
>
{ i18n._t('Admin.VERIFY_TO_CONTINUE', 'Verify to continue') }
</Button>
) }
</div>
);
}

/**
* Renders the password verification form to enter sudo mode
*
* @returns {HTMLElement}
*/
renderSudoModeVerification() {
const { i18n } = window;
const { errorMessage } = this.state;

const inputProps = {
type: 'password',
name: 'sudoModePassword',
id: 'sudoModePassword',
className: 'no-change-track',
onKeyPress: this.handleVerifyInputKeyPress,
innerRef: this.setPasswordInput,
};
const validationProps = errorMessage ? { valid: false, invalid: true } : {};

return (
<div className="sudo-mode__verify">
<FormGroup className="sudo-mode__verify-form-group">
<Label for="sudoModePassword">
{ i18n._t('Admin.ENTER_PASSWORD', 'Enter your password') }
</Label>

<InputGroup>
<Input {...inputProps} {...validationProps} />
<InputGroupAddon addonType="append">
<Button
className="sudo-mode__verify-button"
color="info"
onClick={this.handleVerify}
>
{ i18n._t('Admin.VERIFY', 'Verify') }
</Button>
</InputGroupAddon>
<FormFeedback>{ errorMessage }</FormFeedback>
</InputGroup>
</FormGroup>
</div>
);
}

/**
* Renders the "sudo mode" notice or verification screen
*
* @returns {HTMLElement}
*/
renderSudoMode() {
const { showVerification, loading } = this.state;

const LoadingComponent = this.props.LoadingComponent || loadComponent(
'CircularLoading',
'SudoMode'
);

if (loading) {
return (
<div className="sudo-mode alert alert-info">
<LoadingComponent block />
</div>
);
}

return (
<div className="sudo-mode alert alert-info">
{ this.renderSudoModeNotice() }
{ showVerification && this.renderSudoModeVerification() }
</div>
);
}

render() {
if (!this.isSudoModeActive()) {
return this.renderSudoMode();
return <SudoModePasswordField
verifyMessage={i18n._t('Admin.VERIFY_ITS_YOU', 'Verify it\'s you first.')}
onSuccess={() => this.setState({ active: true })}
autocomplete="off"
/>; // this.renderSudoMode();
}
return <WrappedComponent {...this.props} />;
}
Expand Down
39 changes: 0 additions & 39 deletions client/src/containers/SudoMode/SudoMode.scss

This file was deleted.

62 changes: 5 additions & 57 deletions client/src/containers/SudoMode/tests/SudoMode-test.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
/* global jest, test, describe, it, expect */
import React from 'react';
import fetch from 'isomorphic-fetch';
import { fireEvent, render, screen } from '@testing-library/react';
import { render } from '@testing-library/react';
import withSudoMode from '../SudoMode';

jest.mock('isomorphic-fetch');

const sectionConfigKey = 'SilverStripe\\Admin\\SudoModeController';
const TestComponent = () => <div className="test-component" />;
const LoadingComponent = () => <div className="loading-component" data-testid="loading-component" />;
const ComponentWithSudoMode = withSudoMode(TestComponent);

function resetWindowConfig(options) {
Expand Down Expand Up @@ -40,72 +36,24 @@ test('SudoMode renders the wrapped component when sudo mode is active', () => {
resetWindowConfig({ sudoModeActive: true });
const { container } = render(<ComponentWithSudoMode />);
expect(container.querySelector('.test-component')).not.toBeNull();
expect(container.querySelector('.sudo-mode')).toBeNull();
expect(container.querySelector('.sudo-mode-password-field')).toBeNull();
});

test('SudoMode renders a sudo mode verification screen when sudo mode is inactive', () => {
resetWindowConfig({ sudoModeActive: false });
const { container } = render(<ComponentWithSudoMode />);
expect(container.querySelector('.test-component')).toBeNull();
expect(container.querySelector('.sudo-mode')).not.toBeNull();
expect(container.querySelector('.sudo-mode-password-field')).not.toBeNull();
});

test('SudoMode renders a notice', () => {
resetWindowConfig({ sudoModeActive: false });
const { container } = render(<ComponentWithSudoMode />);
expect(container.querySelector('.sudo-mode__notice')).not.toBeNull();
});

test('SudoMode renders a loading component after entering password and clicking verify', async () => {
fetch.mockClear();
fetch.mockImplementation(() => Promise.resolve({
status: 200,
json: () => Promise.resolve({
result: true,
}),
}));
resetWindowConfig({ sudoModeActive: false });
const { container } = render(
<ComponentWithSudoMode {...{
LoadingComponent
}}
/>
);
fireEvent.click(container.querySelector('.sudo-mode__notice-button'));
fireEvent.change(container.querySelector('#sudoModePassword'), {
target: { value: 'password' }
});
fireEvent.click(container.querySelector('.sudo-mode__verify-button'));
expect(await screen.findByTestId('loading-component')).not.toBeNull();
expect(container.querySelector('.sudo-mode-password-field__notice-message').textContent).toBe('Verify it\'s you first.');
});

test('SudoMode renders a help link when one is provided', () => {
resetWindowConfig({ sudoModeActive: false, helpLink: 'http://google.com' });
const { container } = render(<ComponentWithSudoMode />);
expect(container.querySelector('.sudo-mode__notice-help').href).toBe('http://google.com/');
});

test('Sudo mode shows errors on failure', async () => {
resetWindowConfig({ sudoModeActive: false });
fetch.mockClear();
fetch.mockImplementation(() => Promise.resolve({
status: 200,
json: () => Promise.resolve({
result: false,
message: 'It broke because its a test.',
}),
}));
const { container } = render(
<ComponentWithSudoMode {...{
LoadingComponent
}}
/>
);
fireEvent.click(container.querySelector('.sudo-mode__notice-button'));
fireEvent.change(container.querySelector('#sudoModePassword'), {
target: { value: 'password' }
});
fireEvent.click(container.querySelector('.sudo-mode__verify-button'));
await screen.findByTestId('loading-component');
expect(container.querySelector('.invalid-feedback').innerHTML).toBe('It broke because its a test.');
expect(container.querySelector('.sudo-mode-password-field__notice-help').href).toBe('http://google.com/');
});
Loading

0 comments on commit a3ec77b

Please sign in to comment.