Skip to content

Commit a3ec77b

Browse files
ENH Refactor sudo mode components to reduce code reuse (#1905)
1 parent 1c5bc20 commit a3ec77b

File tree

8 files changed

+38
-306
lines changed

8 files changed

+38
-306
lines changed

client/dist/js/bundle.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/dist/styles/bundle.css

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/src/components/SudoModePasswordField/SudoModePasswordField.js

+10-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ function SudoModePasswordField(props) {
1515
const {
1616
onSuccess,
1717
autocomplete,
18+
verifyMessage,
1819
} = props;
1920
const passwordFieldRef = createRef();
2021
const [responseMessage, setResponseMessage] = useState('');
@@ -80,12 +81,16 @@ function SudoModePasswordField(props) {
8081
*/
8182
function renderConfirm() {
8283
const helpLink = clientConfig.helpLink;
84+
let verifyMessageValue = verifyMessage;
85+
if (!verifyMessageValue) {
86+
verifyMessageValue = i18n._t(
87+
'Admin.SUDO_MODE_PASSWORD_FIELD_VERIFY',
88+
'This section is protected and is in read-only mode. Before editing please verify that it\'s you first.'
89+
);
90+
}
8391
return <div className="sudo-mode__notice sudo-mode-password-field__notice--required">
8492
<p className="sudo-mode-password-field__notice-message">
85-
{ i18n._t(
86-
'Admin.SUDO_MODE_PASSWORD_FIELD_VERIFY',
87-
'This section is protected and is in read-only mode. Before editing please verify that it\'s you first.'
88-
) }
93+
{ verifyMessageValue }
8994
{ helpLink && (
9095
<a href={helpLink} className="sudo-mode-password-field__notice-help" target="_blank" rel="noopener noreferrer">
9196
{ i18n._t('Admin.WHATS_THIS', 'What is this?') }
@@ -150,6 +155,7 @@ function SudoModePasswordField(props) {
150155
}
151156

152157
SudoModePasswordField.propTypes = {
158+
verifyMessage: PropTypes.string,
153159
onSuccess: PropTypes.func.isRequired,
154160
autocomplete: PropTypes.string.isRequired,
155161
};

client/src/containers/SudoMode/SudoMode.js

+7-183
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import React, { Component } from 'react';
22
import PropTypes from 'prop-types';
3-
import { Button, InputGroup, InputGroupAddon, Input, FormGroup, Label, FormFeedback } from 'reactstrap';
4-
import { loadComponent } from 'lib/Injector';
5-
import fetch from 'isomorphic-fetch';
63
import Config from 'lib/Config';
4+
import i18n from 'i18n';
5+
import SudoModePasswordField from '../../components/SudoModePasswordField/SudoModePasswordField';
76

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

3029
this.state = {
3130
active: Config.getSection(configSectionKey).sudoModeActive || false,
32-
showVerification: false,
33-
loading: false,
34-
errorMessage: null,
3531
};
36-
37-
this.handleConfirmNotice = this.handleConfirmNotice.bind(this);
38-
this.handleVerify = this.handleVerify.bind(this);
39-
this.handleVerifyInputKeyPress = this.handleVerifyInputKeyPress.bind(this);
40-
41-
// React 15 compatible ref callback
42-
this.passwordInput = null;
43-
this.setPasswordInput = element => {
44-
this.passwordInput = element;
45-
};
46-
}
47-
48-
/**
49-
* Action called when clicking the button to confirm the sudo mode notice
50-
* and trigger the verification form to be rendered.
51-
*/
52-
handleConfirmNotice() {
53-
this.setState({
54-
showVerification: true,
55-
}, () => this.passwordInput && this.passwordInput.focus());
56-
}
57-
58-
/**
59-
* Action called when the user has entered their password and requested
60-
* verification of sudo mode state.
61-
*/
62-
handleVerify() {
63-
this.setState({
64-
loading: true,
65-
});
66-
67-
const payload = new FormData();
68-
payload.append('SecurityID', Config.get('SecurityID'));
69-
payload.append('Password', this.passwordInput.value);
70-
71-
// Validate the request
72-
fetch(Config.getSection(configSectionKey).endpoints.activate, {
73-
method: 'POST',
74-
body: payload,
75-
}).then(response => response.json().then(result => {
76-
// Happy path, send the user to the wrapped component
77-
if (result.result) {
78-
return this.setState({
79-
loading: false,
80-
active: true,
81-
});
82-
}
83-
84-
// Validation error, show them the message
85-
return this.setState({
86-
loading: false,
87-
errorMessage: result.message,
88-
}, () => this.passwordInput.focus());
89-
}));
90-
}
91-
92-
/**
93-
* Treat pressing enter on the password field the same as clicking the
94-
* verify button.
95-
*
96-
* @param {object} event
97-
*/
98-
handleVerifyInputKeyPress(event) {
99-
if (event.charCode === 13) {
100-
event.stopPropagation();
101-
event.preventDefault();
102-
this.handleVerify();
103-
}
10432
}
10533

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

115-
/**
116-
* Renders a notice to the user that they will need to verify themself
117-
* to enter sudo mode and continue to use this functionality.
118-
*
119-
* @returns {HTMLElement}
120-
*/
121-
renderSudoModeNotice() {
122-
const { i18n } = window;
123-
const { showVerification } = this.state;
124-
125-
const helpLink = Config.getSection(configSectionKey).helpLink || null;
126-
127-
return (
128-
<div className="sudo-mode__notice sudo-mode__notice--required">
129-
<p className="sudo-mode__notice-message">
130-
{ i18n._t('Admin.VERIFY_ITS_YOU', 'Verify it\'s you first.') }
131-
{ helpLink && (
132-
<a href={helpLink} className="sudo-mode__notice-help" target="_blank" rel="noopener noreferrer">
133-
{ i18n._t('Admin.WHATS_THIS', 'What is this?') }
134-
</a>
135-
) }
136-
</p>
137-
{ !showVerification && (
138-
<Button
139-
className="sudo-mode__notice-button font-icon-lock"
140-
color="info"
141-
onClick={this.handleConfirmNotice}
142-
>
143-
{ i18n._t('Admin.VERIFY_TO_CONTINUE', 'Verify to continue') }
144-
</Button>
145-
) }
146-
</div>
147-
);
148-
}
149-
150-
/**
151-
* Renders the password verification form to enter sudo mode
152-
*
153-
* @returns {HTMLElement}
154-
*/
155-
renderSudoModeVerification() {
156-
const { i18n } = window;
157-
const { errorMessage } = this.state;
158-
159-
const inputProps = {
160-
type: 'password',
161-
name: 'sudoModePassword',
162-
id: 'sudoModePassword',
163-
className: 'no-change-track',
164-
onKeyPress: this.handleVerifyInputKeyPress,
165-
innerRef: this.setPasswordInput,
166-
};
167-
const validationProps = errorMessage ? { valid: false, invalid: true } : {};
168-
169-
return (
170-
<div className="sudo-mode__verify">
171-
<FormGroup className="sudo-mode__verify-form-group">
172-
<Label for="sudoModePassword">
173-
{ i18n._t('Admin.ENTER_PASSWORD', 'Enter your password') }
174-
</Label>
175-
176-
<InputGroup>
177-
<Input {...inputProps} {...validationProps} />
178-
<InputGroupAddon addonType="append">
179-
<Button
180-
className="sudo-mode__verify-button"
181-
color="info"
182-
onClick={this.handleVerify}
183-
>
184-
{ i18n._t('Admin.VERIFY', 'Verify') }
185-
</Button>
186-
</InputGroupAddon>
187-
<FormFeedback>{ errorMessage }</FormFeedback>
188-
</InputGroup>
189-
</FormGroup>
190-
</div>
191-
);
192-
}
193-
194-
/**
195-
* Renders the "sudo mode" notice or verification screen
196-
*
197-
* @returns {HTMLElement}
198-
*/
199-
renderSudoMode() {
200-
const { showVerification, loading } = this.state;
201-
202-
const LoadingComponent = this.props.LoadingComponent || loadComponent(
203-
'CircularLoading',
204-
'SudoMode'
205-
);
206-
207-
if (loading) {
208-
return (
209-
<div className="sudo-mode alert alert-info">
210-
<LoadingComponent block />
211-
</div>
212-
);
213-
}
214-
215-
return (
216-
<div className="sudo-mode alert alert-info">
217-
{ this.renderSudoModeNotice() }
218-
{ showVerification && this.renderSudoModeVerification() }
219-
</div>
220-
);
221-
}
222-
22343
render() {
22444
if (!this.isSudoModeActive()) {
225-
return this.renderSudoMode();
45+
return <SudoModePasswordField
46+
verifyMessage={i18n._t('Admin.VERIFY_ITS_YOU', 'Verify it\'s you first.')}
47+
onSuccess={() => this.setState({ active: true })}
48+
autocomplete="off"
49+
/>; // this.renderSudoMode();
22650
}
22751
return <WrappedComponent {...this.props} />;
22852
}

client/src/containers/SudoMode/SudoMode.scss

-39
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
/* global jest, test, describe, it, expect */
22
import React from 'react';
3-
import fetch from 'isomorphic-fetch';
4-
import { fireEvent, render, screen } from '@testing-library/react';
3+
import { render } from '@testing-library/react';
54
import withSudoMode from '../SudoMode';
65

7-
jest.mock('isomorphic-fetch');
8-
96
const sectionConfigKey = 'SilverStripe\\Admin\\SudoModeController';
107
const TestComponent = () => <div className="test-component" />;
11-
const LoadingComponent = () => <div className="loading-component" data-testid="loading-component" />;
128
const ComponentWithSudoMode = withSudoMode(TestComponent);
139

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

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

5349
test('SudoMode renders a notice', () => {
5450
resetWindowConfig({ sudoModeActive: false });
5551
const { container } = render(<ComponentWithSudoMode />);
56-
expect(container.querySelector('.sudo-mode__notice')).not.toBeNull();
57-
});
58-
59-
test('SudoMode renders a loading component after entering password and clicking verify', async () => {
60-
fetch.mockClear();
61-
fetch.mockImplementation(() => Promise.resolve({
62-
status: 200,
63-
json: () => Promise.resolve({
64-
result: true,
65-
}),
66-
}));
67-
resetWindowConfig({ sudoModeActive: false });
68-
const { container } = render(
69-
<ComponentWithSudoMode {...{
70-
LoadingComponent
71-
}}
72-
/>
73-
);
74-
fireEvent.click(container.querySelector('.sudo-mode__notice-button'));
75-
fireEvent.change(container.querySelector('#sudoModePassword'), {
76-
target: { value: 'password' }
77-
});
78-
fireEvent.click(container.querySelector('.sudo-mode__verify-button'));
79-
expect(await screen.findByTestId('loading-component')).not.toBeNull();
52+
expect(container.querySelector('.sudo-mode-password-field__notice-message').textContent).toBe('Verify it\'s you first.');
8053
});
8154

8255
test('SudoMode renders a help link when one is provided', () => {
8356
resetWindowConfig({ sudoModeActive: false, helpLink: 'http://google.com' });
8457
const { container } = render(<ComponentWithSudoMode />);
85-
expect(container.querySelector('.sudo-mode__notice-help').href).toBe('http://google.com/');
86-
});
87-
88-
test('Sudo mode shows errors on failure', async () => {
89-
resetWindowConfig({ sudoModeActive: false });
90-
fetch.mockClear();
91-
fetch.mockImplementation(() => Promise.resolve({
92-
status: 200,
93-
json: () => Promise.resolve({
94-
result: false,
95-
message: 'It broke because its a test.',
96-
}),
97-
}));
98-
const { container } = render(
99-
<ComponentWithSudoMode {...{
100-
LoadingComponent
101-
}}
102-
/>
103-
);
104-
fireEvent.click(container.querySelector('.sudo-mode__notice-button'));
105-
fireEvent.change(container.querySelector('#sudoModePassword'), {
106-
target: { value: 'password' }
107-
});
108-
fireEvent.click(container.querySelector('.sudo-mode__verify-button'));
109-
await screen.findByTestId('loading-component');
110-
expect(container.querySelector('.invalid-feedback').innerHTML).toBe('It broke because its a test.');
58+
expect(container.querySelector('.sudo-mode-password-field__notice-help').href).toBe('http://google.com/');
11159
});

0 commit comments

Comments
 (0)