зеркало из https://github.com/mozilla/fleet.git
App settings page (#615)
* AppSettingsPage at /admin/settings * Adds App Settings to site nav items * SMTP not configured warning * Creates AppConfigForm * Avatar preview * API client to update app config * Creates OrgLogoIcon component * Hide username/password when no auth type
This commit is contained in:
Родитель
f092c614cf
Коммит
ee6832c743
|
@ -39,7 +39,7 @@ class Button extends React.Component<IButtonProps, IButtonState> {
|
|||
render () {
|
||||
const { handleClick } = this;
|
||||
const { className, disabled, size, tabIndex, text, type, variant } = this.props;
|
||||
const fullClassName = classnames(`${baseClass}--${variant}`, className, {
|
||||
const fullClassName = classnames(baseClass, `${baseClass}--${variant}`, className, {
|
||||
[baseClass]: variant !== 'unstyled',
|
||||
[`${baseClass}--disabled`]: disabled,
|
||||
[`${baseClass}--${size}`]: size,
|
||||
|
|
|
@ -121,6 +121,7 @@ $base-class: 'button';
|
|||
background-color: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
|
@ -131,6 +132,10 @@ $base-class: 'button';
|
|||
top: 0;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const Slider = ({ onChange, value }) => {
|
||||
const baseClass = 'slider-wrap';
|
||||
|
||||
const sliderBtnClass = classnames(
|
||||
baseClass,
|
||||
{ [`${baseClass}--active`]: value }
|
||||
);
|
||||
|
||||
const sliderDotClass = classnames(
|
||||
`${baseClass}__dot`,
|
||||
{ [`${baseClass}__dot--active`]: value }
|
||||
);
|
||||
|
||||
const handleClick = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
return onChange(!value);
|
||||
};
|
||||
|
||||
return (
|
||||
<button className={`button button--unstyled ${sliderBtnClass}`} onClick={handleClick}>
|
||||
<div className={sliderDotClass} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
Slider.propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Slider;
|
|
@ -0,0 +1,37 @@
|
|||
.slider-wrap {
|
||||
&:hover {
|
||||
background-color: $text-medium;
|
||||
}
|
||||
|
||||
@include transition(background-color 400ms ease-in-out);
|
||||
background-color: $text-medium;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #eaeaea;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
height: 22px;
|
||||
min-width: 40px;
|
||||
position: relative;
|
||||
width: 40px;
|
||||
|
||||
&--active {
|
||||
background-color: $brand;
|
||||
|
||||
&:hover {
|
||||
background-color: $brand;
|
||||
}
|
||||
}
|
||||
|
||||
&__dot {
|
||||
@include transition(left 300ms ease-in-out);
|
||||
@include size(14px);
|
||||
@include position(absolute, 0 null null 5px);
|
||||
margin-top: 3px;
|
||||
border-radius: 50%;
|
||||
background-color: $white;
|
||||
|
||||
&--active {
|
||||
left: 21px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
.app-config-form {
|
||||
&__section {
|
||||
@include clearfix;
|
||||
|
||||
.smtp-options {
|
||||
font-size: 15px;
|
||||
font-weight: $bold;
|
||||
line-height: 3.2;
|
||||
letter-spacing: 0.6px;
|
||||
color: $text-dark;
|
||||
padding-left: 15px;
|
||||
|
||||
em {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
&--configured {
|
||||
em {
|
||||
color: $success;
|
||||
}
|
||||
}
|
||||
|
||||
&--notconfigured {
|
||||
em {
|
||||
color: $alert;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__inputs {
|
||||
width: 60%;
|
||||
float: left;
|
||||
padding: 0 40px 0 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&--smtp {
|
||||
margin: 0 0 $pad-base;
|
||||
|
||||
.input-field__wrapper {
|
||||
width: 18%;
|
||||
float: right;
|
||||
|
||||
&:first-child {
|
||||
float: left;
|
||||
width: 78%;
|
||||
}
|
||||
}
|
||||
|
||||
.kolide-checkbox {
|
||||
clear: left;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__details {
|
||||
float: right;
|
||||
width: 40%;
|
||||
|
||||
p {
|
||||
font-size: 15px;
|
||||
font-weight: $normal;
|
||||
line-height: 1.6;
|
||||
letter-spacing: 0.5px;
|
||||
color: $text-dark;
|
||||
|
||||
&.app-config-form__note {
|
||||
font-size: 13px;
|
||||
color: $text-medium;
|
||||
}
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: $text-medium;
|
||||
|
||||
&--brand {
|
||||
color: $brand;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__avatar-preview {
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
border-radius: 80px;
|
||||
height: 160px;
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $link;
|
||||
font-size: 18px;
|
||||
font-weight: $bold;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__show-options {
|
||||
color: $text-dark;
|
||||
}
|
||||
|
||||
&__smtp-section {
|
||||
border-radius: 2px;
|
||||
background-color: $bg-medium;
|
||||
border: solid 1px $accent-medium;
|
||||
padding: 30px;
|
||||
|
||||
.input-field__wrapper {
|
||||
@include clearfix;
|
||||
|
||||
&:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.input-field__label {
|
||||
float: left;
|
||||
font-size: 15px;
|
||||
font-weight: $bold;
|
||||
letter-spacing: 0.6px;
|
||||
text-align: right;
|
||||
color: $text-dark;
|
||||
width: 30%;
|
||||
margin: 0;
|
||||
line-height: 40px;
|
||||
|
||||
@media (min-width: $medium-width + 1) and (max-width: 1195px) {
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-field,
|
||||
.Select,
|
||||
.slide-wrapper {
|
||||
width: 60%;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.slide-wrapper {
|
||||
.slider-option {
|
||||
font-size: 18px;
|
||||
font-weight: $light;
|
||||
letter-spacing: 1.3px;
|
||||
text-align: left;
|
||||
vertical-align: text-bottom;
|
||||
|
||||
&--off {
|
||||
color: $text-light;
|
||||
}
|
||||
|
||||
&--on {
|
||||
color: $brand;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -22,7 +22,15 @@ export default (WrappedComponent, { fields, validate = defaultValidate }) => {
|
|||
|
||||
const { errors, formData } = props;
|
||||
|
||||
if (!errors) {
|
||||
this.state = { errors: {}, formData };
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
this.state = { errors, formData };
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
|
@ -81,9 +89,13 @@ export default (WrappedComponent, { fields, validate = defaultValidate }) => {
|
|||
const { errors } = this.state;
|
||||
const { errors: serverErrors } = this.props;
|
||||
|
||||
if (serverErrors) {
|
||||
return errors[fieldName] || serverErrors[fieldName];
|
||||
}
|
||||
|
||||
return errors[fieldName];
|
||||
}
|
||||
|
||||
getFields = () => {
|
||||
const { getError, getValue, onFieldChange } = this;
|
||||
const fieldProps = {};
|
||||
|
|
|
@ -5,10 +5,10 @@ const baseClass = 'form-field';
|
|||
|
||||
class FormField extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.element,
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
error: PropTypes.string,
|
||||
hint: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
|
||||
hint: PropTypes.oneOfType([PropTypes.array, PropTypes.node, PropTypes.string]),
|
||||
label: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
|
||||
name: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
|
|
|
@ -4,87 +4,41 @@ import { mount } from 'enzyme';
|
|||
import { noop } from 'lodash';
|
||||
|
||||
import AdminDetails from 'components/forms/RegistrationForm/AdminDetails';
|
||||
import { fillInFormInput } from 'test/helpers';
|
||||
import { fillInFormInput, itBehavesLikeAFormInputElement } from 'test/helpers';
|
||||
|
||||
describe('AdminDetails - form', () => {
|
||||
afterEach(restoreSpies);
|
||||
|
||||
let form = mount(<AdminDetails handleSubmit={noop} />);
|
||||
|
||||
describe('username input', () => {
|
||||
it('renders an input field', () => {
|
||||
const form = mount(<AdminDetails handleSubmit={noop} />);
|
||||
const usernameField = form.find({ name: 'username' });
|
||||
|
||||
expect(usernameField.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('updates state when the field changes', () => {
|
||||
const form = mount(<AdminDetails handleSubmit={noop} />);
|
||||
const usernameField = form.find({ name: 'username' }).find('input');
|
||||
|
||||
fillInFormInput(usernameField, 'Gnar');
|
||||
|
||||
expect(form.state().formData).toInclude({ username: 'Gnar' });
|
||||
itBehavesLikeAFormInputElement(form, 'username');
|
||||
});
|
||||
});
|
||||
|
||||
describe('password input', () => {
|
||||
it('renders an input field', () => {
|
||||
const form = mount(<AdminDetails handleSubmit={noop} />);
|
||||
const passwordField = form.find({ name: 'password' });
|
||||
|
||||
expect(passwordField.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('updates state when the field changes', () => {
|
||||
const form = mount(<AdminDetails handleSubmit={noop} />);
|
||||
const passwordField = form.find({ name: 'password' }).find('input');
|
||||
|
||||
fillInFormInput(passwordField, 'p@ssw0rd');
|
||||
|
||||
expect(form.state().formData).toInclude({ password: 'p@ssw0rd' });
|
||||
itBehavesLikeAFormInputElement(form, 'password');
|
||||
});
|
||||
});
|
||||
|
||||
describe('password confirmation input', () => {
|
||||
it('renders an input field', () => {
|
||||
const form = mount(<AdminDetails handleSubmit={noop} />);
|
||||
const passwordConfirmationField = form.find({ name: 'password_confirmation' });
|
||||
|
||||
expect(passwordConfirmationField.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('updates state when the field changes', () => {
|
||||
const form = mount(<AdminDetails handleSubmit={noop} />);
|
||||
const passwordConfirmationField = form.find({ name: 'password_confirmation' }).find('input');
|
||||
|
||||
fillInFormInput(passwordConfirmationField, 'p@ssw0rd');
|
||||
|
||||
expect(form.state().formData).toInclude({ password_confirmation: 'p@ssw0rd' });
|
||||
itBehavesLikeAFormInputElement(form, 'password_confirmation');
|
||||
});
|
||||
});
|
||||
|
||||
describe('email input', () => {
|
||||
it('renders an input field', () => {
|
||||
const form = mount(<AdminDetails handleSubmit={noop} />);
|
||||
const emailField = form.find({ name: 'email' });
|
||||
|
||||
expect(emailField.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('updates state when the field changes', () => {
|
||||
const form = mount(<AdminDetails handleSubmit={noop} />);
|
||||
const emailField = form.find({ name: 'email' }).find('input');
|
||||
|
||||
fillInFormInput(emailField, 'hi@gnar.dog');
|
||||
|
||||
expect(form.state().formData).toInclude({ email: 'hi@gnar.dog' });
|
||||
itBehavesLikeAFormInputElement(form, 'email');
|
||||
});
|
||||
});
|
||||
|
||||
describe('submitting the form', () => {
|
||||
it('validates the email field', () => {
|
||||
const onSubmitSpy = createSpy();
|
||||
const form = mount(<AdminDetails handleSubmit={onSubmitSpy} />);
|
||||
form = mount(<AdminDetails handleSubmit={onSubmitSpy} />);
|
||||
const submitBtn = form.find('Button');
|
||||
|
||||
|
||||
|
@ -101,7 +55,7 @@ describe('AdminDetails - form', () => {
|
|||
|
||||
it('validates the email field', () => {
|
||||
const onSubmitSpy = createSpy();
|
||||
const form = mount(<AdminDetails handleSubmit={onSubmitSpy} />);
|
||||
form = mount(<AdminDetails handleSubmit={onSubmitSpy} />);
|
||||
const emailField = form.find({ name: 'email' }).find('input');
|
||||
const submitBtn = form.find('Button');
|
||||
|
||||
|
@ -114,7 +68,7 @@ describe('AdminDetails - form', () => {
|
|||
|
||||
it('validates the password fields match', () => {
|
||||
const onSubmitSpy = createSpy();
|
||||
const form = mount(<AdminDetails handleSubmit={onSubmitSpy} />);
|
||||
form = mount(<AdminDetails handleSubmit={onSubmitSpy} />);
|
||||
const passwordConfirmationField = form.find({ name: 'password_confirmation' }).find('input');
|
||||
const passwordField = form.find({ name: 'password' }).find('input');
|
||||
const submitBtn = form.find('Button');
|
||||
|
@ -131,7 +85,7 @@ describe('AdminDetails - form', () => {
|
|||
|
||||
it('submits the form when valid', () => {
|
||||
const onSubmitSpy = createSpy();
|
||||
const form = mount(<AdminDetails handleSubmit={onSubmitSpy} />);
|
||||
form = mount(<AdminDetails handleSubmit={onSubmitSpy} />);
|
||||
const emailField = form.find({ name: 'email' }).find('input');
|
||||
const passwordConfirmationField = form.find({ name: 'password_confirmation' }).find('input');
|
||||
const passwordField = form.find({ name: 'password' }).find('input');
|
||||
|
|
|
@ -73,9 +73,12 @@
|
|||
color: $text-dark;
|
||||
margin: 30px 0 0;
|
||||
|
||||
label {
|
||||
.kolide-checkbox {
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
&__label {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
|
|
|
@ -0,0 +1,228 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
import Button from 'components/buttons/Button';
|
||||
import Checkbox from 'components/forms/fields/Checkbox';
|
||||
import Dropdown from 'components/forms/fields/Dropdown';
|
||||
import Form from 'components/forms/Form';
|
||||
import formFieldInterface from 'interfaces/form_field';
|
||||
import Icon from 'components/icons/Icon';
|
||||
import InputField from 'components/forms/fields/InputField';
|
||||
import OrgLogoIcon from 'components/icons/OrgLogoIcon';
|
||||
import Slider from 'components/forms/fields/Slider';
|
||||
import validate from 'components/forms/admin/AppConfigForm/validate';
|
||||
|
||||
const authMethodOptions = [
|
||||
{ label: 'Plain', value: 'authmethod_plain' },
|
||||
{ label: 'Cram MD5', value: 'authmethod_cram_md5' },
|
||||
];
|
||||
const authTypeOptions = [
|
||||
{ label: 'Username and Password', value: 'authtype_username_password' },
|
||||
{ label: 'None', value: 'authtype_none' },
|
||||
];
|
||||
const baseClass = 'app-config-form';
|
||||
const formFields = [
|
||||
'authentication_method', 'authentication_type', 'domain', 'enable_ssl_tls', 'enable_start_tls',
|
||||
'kolide_server_url', 'org_logo_url', 'org_name', 'password', 'port', 'sender_address',
|
||||
'server', 'user_name', 'verify_ssl_certs',
|
||||
];
|
||||
const Header = ({ showAdvancedOptions }) => {
|
||||
const CaratIcon = <Icon name={showAdvancedOptions ? 'downcarat' : 'upcarat'} />;
|
||||
|
||||
return <span>Advanced Options {CaratIcon} <small>You normally don’t need to change these settings they are for special setups.</small></span>;
|
||||
};
|
||||
|
||||
Header.propTypes = { showAdvancedOptions: PropTypes.bool.isRequired };
|
||||
|
||||
class AppConfigForm extends Component {
|
||||
static propTypes = {
|
||||
fields: PropTypes.shape({
|
||||
authentication_method: formFieldInterface.isRequired,
|
||||
authentication_type: formFieldInterface.isRequired,
|
||||
domain: formFieldInterface.isRequired,
|
||||
enable_ssl_tls: formFieldInterface.isRequired,
|
||||
enable_start_tls: formFieldInterface.isRequired,
|
||||
kolide_server_url: formFieldInterface.isRequired,
|
||||
org_logo_url: formFieldInterface.isRequired,
|
||||
org_name: formFieldInterface.isRequired,
|
||||
password: formFieldInterface.isRequired,
|
||||
port: formFieldInterface.isRequired,
|
||||
sender_address: formFieldInterface.isRequired,
|
||||
server: formFieldInterface.isRequired,
|
||||
user_name: formFieldInterface.isRequired,
|
||||
verify_ssl_certs: formFieldInterface.isRequired,
|
||||
}).isRequired,
|
||||
handleSubmit: PropTypes.func,
|
||||
smtpConfigured: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = { showAdvancedOptions: false };
|
||||
}
|
||||
|
||||
onToggleAdvancedOptions = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { showAdvancedOptions } = this.state;
|
||||
|
||||
this.setState({ showAdvancedOptions: !showAdvancedOptions });
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
renderAdvancedOptions = () => {
|
||||
const { fields } = this.props;
|
||||
const { showAdvancedOptions } = this.state;
|
||||
|
||||
if (!showAdvancedOptions) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={`${baseClass}__inputs`}>
|
||||
<div className={`${baseClass}__smtp-section`}>
|
||||
<InputField {...fields.domain} label="Domain" />
|
||||
<Slider {...fields.verify_ssl_certs} label="Verify SSL Certs?" />
|
||||
<Slider {...fields.enable_start_tls} label="Enable STARTTLS?" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${baseClass}__details`}>
|
||||
<p><strong>Domain</strong> - If you need to specify a HELO domain, you can do it here <em className="hint hint--brand">(Default: <strong>Blank</strong>)</em></p>
|
||||
<p><strong>Verify SSL Certs</strong> - Turn this off (not recommended) if you use a self-signed certificate <em className="hint hint--brand">(Default: <strong>On</strong>)</em></p>
|
||||
<p><strong>Enable STARTTLS</strong> - Detects if STARTTLS is enabled in your SMTP server and starts to use it. <em className="hint hint--brand">(Default: <strong>On</strong>)</em></p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderSmtpSection = () => {
|
||||
const { fields } = this.props;
|
||||
|
||||
if (fields.authentication_type.value === 'authtype_none') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__smtp-section`}>
|
||||
<InputField
|
||||
{...fields.user_name}
|
||||
label="SMTP Username"
|
||||
/>
|
||||
<InputField
|
||||
{...fields.password}
|
||||
label="SMTP Password"
|
||||
/>
|
||||
<Dropdown
|
||||
{...fields.authentication_method}
|
||||
label="Auth Method"
|
||||
options={authMethodOptions}
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { fields, handleSubmit, smtpConfigured } = this.props;
|
||||
const { onToggleAdvancedOptions, renderAdvancedOptions, renderSmtpSection } = this;
|
||||
const { showAdvancedOptions } = this.state;
|
||||
|
||||
return (
|
||||
<form className={baseClass} onSubmit={handleSubmit}>
|
||||
<div className={`${baseClass}__section`}>
|
||||
<h2>Organization Info</h2>
|
||||
<div className={`${baseClass}__inputs`}>
|
||||
<InputField
|
||||
{...fields.org_name}
|
||||
label="Organization Name"
|
||||
/>
|
||||
<InputField
|
||||
{...fields.org_logo_url}
|
||||
label="Organization Avatar"
|
||||
/>
|
||||
</div>
|
||||
<div className={`${baseClass}__details ${baseClass}__avatar-preview`}>
|
||||
<OrgLogoIcon src={fields.org_logo_url.value} />
|
||||
<p>Avatar Preview</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__section`}>
|
||||
<h2>Kolide Web Address</h2>
|
||||
<div className={`${baseClass}__inputs`}>
|
||||
<InputField
|
||||
{...fields.kolide_server_url}
|
||||
label="Kolide App URL"
|
||||
hint={<span>Include base path only (eg. no <code>/v1</code>)</span>}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${baseClass}__details`}>
|
||||
<p>What base URL should <strong>osqueryd</strong> clients user to connect and register with <strong>Kolide</strong>?</p>
|
||||
<p className={`${baseClass}__note`}><strong>Note:</strong>Please ensure the URL you choose is accessible to all endpoints that need to communicate with Kolide, otherwise they will not be able to correctly register.</p>
|
||||
<Button text="SEND TEST" variant="inverse" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__section`}>
|
||||
<h2>SMTP Options <small className={`smtp-options smtp-options--${smtpConfigured ? 'configured' : 'notconfigured'}`}>STATUS: <em>{smtpConfigured ? 'CONFIGURED' : 'NOT CONFIGURED'}</em></small></h2>
|
||||
<div className={`${baseClass}__inputs`}>
|
||||
<InputField
|
||||
{...fields.sender_address}
|
||||
label="Sender Address"
|
||||
/>
|
||||
</div>
|
||||
<div className={`${baseClass}__details`}>
|
||||
<p>The address email recipients will see all messages that are sent from the <strong>Kolide</strong> application.</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__inputs ${baseClass}__inputs--smtp`}>
|
||||
<InputField
|
||||
{...fields.server}
|
||||
label="SMTP Server"
|
||||
/>
|
||||
<InputField
|
||||
{...fields.port}
|
||||
label=" "
|
||||
/>
|
||||
<Checkbox
|
||||
{...fields.enable_ssl_tls}
|
||||
>
|
||||
User SSL/TLS to connect (recommended)
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div className={`${baseClass}__details`}>
|
||||
<p>The hostname / IP address and corresponding port of your organization's SMTP server.</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__inputs`}>
|
||||
<Dropdown
|
||||
{...fields.authentication_type}
|
||||
label="Authentication Type"
|
||||
options={authTypeOptions}
|
||||
/>
|
||||
{renderSmtpSection()}
|
||||
</div>
|
||||
<div className={`${baseClass}__details`}>
|
||||
<p>If your mail server requires authentication, you need to specify the authentication type here.</p>
|
||||
<p><strong>No Authentication</strong> - Select this if your SMTP is open.</p>
|
||||
<p><strong>Username & Password</strong> - Select this if your SMTP server requires username and password before use.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__section`}>
|
||||
<h2><a href="#advancedOptions" onClick={onToggleAdvancedOptions} className={`${baseClass}__show-options`}><Header showAdvancedOptions={showAdvancedOptions} /></a></h2>
|
||||
{renderAdvancedOptions()}
|
||||
</div>
|
||||
<Button
|
||||
text="UPDATE SETTINGS"
|
||||
type="submit"
|
||||
variant="brand"
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form(AppConfigForm, {
|
||||
fields: formFields,
|
||||
validate,
|
||||
});
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import React from 'react';
|
||||
import expect from 'expect';
|
||||
import { mount } from 'enzyme';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import AppConfigForm from 'components/forms/admin/AppConfigForm';
|
||||
import { itBehavesLikeAFormInputElement } from 'test/helpers';
|
||||
|
||||
describe('AppConfigForm - form', () => {
|
||||
const form = mount(<AppConfigForm handleSubmit={noop} />);
|
||||
|
||||
describe('Organization Name input', () => {
|
||||
it('renders an input field', () => {
|
||||
itBehavesLikeAFormInputElement(form, 'org_name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Organization Avatar input', () => {
|
||||
it('renders an input field', () => {
|
||||
itBehavesLikeAFormInputElement(form, 'org_logo_url');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Kolide App URL input', () => {
|
||||
it('renders an input field', () => {
|
||||
itBehavesLikeAFormInputElement(form, 'kolide_server_url');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sender Address input', () => {
|
||||
it('renders an input field', () => {
|
||||
itBehavesLikeAFormInputElement(form, 'sender_address');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SMTP Server input', () => {
|
||||
it('renders an input field', () => {
|
||||
itBehavesLikeAFormInputElement(form, 'server');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Port input', () => {
|
||||
it('renders an input field', () => {
|
||||
itBehavesLikeAFormInputElement(form, 'port');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Enable SSL/TLS input', () => {
|
||||
it('renders an input field', () => {
|
||||
itBehavesLikeAFormInputElement(form, 'enable_ssl_tls', 'Checkbox');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SMTP user name input', () => {
|
||||
it('renders an input field', () => {
|
||||
itBehavesLikeAFormInputElement(form, 'user_name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SMTP user password input', () => {
|
||||
it('renders an input field', () => {
|
||||
itBehavesLikeAFormInputElement(form, 'password');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Advanced options', () => {
|
||||
it('does not render advanced options by default', () => {
|
||||
expect(form.find({ name: 'domain' }).length).toEqual(0);
|
||||
expect(form.find('Slider').length).toEqual(0);
|
||||
});
|
||||
|
||||
it('renders advanced options when "Advanced Options" is clicked', () => {
|
||||
form.find('.app-config-form__show-options').simulate('click');
|
||||
|
||||
expect(form.find({ name: 'domain' }).length).toEqual(1);
|
||||
expect(form.find('Slider').length).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
export default from './AppConfigForm';
|
|
@ -0,0 +1,41 @@
|
|||
import { size, some } from 'lodash';
|
||||
|
||||
export default (formData) => {
|
||||
const errors = {};
|
||||
const {
|
||||
authentication_type: authType,
|
||||
kolide_server_url: kolideServerUrl,
|
||||
sender_address: smtpSenderAddress,
|
||||
server: smtpServer,
|
||||
user_name: smtpUserName,
|
||||
password: smtpPassword,
|
||||
} = formData;
|
||||
|
||||
if (!kolideServerUrl) {
|
||||
errors.kolide_server_url = 'Kolide Server URL must be present';
|
||||
}
|
||||
|
||||
if (some([smtpSenderAddress, smtpPassword, smtpServer, smtpUserName])) {
|
||||
if (!smtpSenderAddress) {
|
||||
errors.sender_address = 'SMTP Sender Address must be present';
|
||||
}
|
||||
|
||||
if (!smtpServer) {
|
||||
errors.server = 'SMTP Server must be present';
|
||||
}
|
||||
|
||||
if (authType !== 'authtype_none') {
|
||||
if (!smtpUserName) {
|
||||
errors.user_name = 'SMTP Username must be present';
|
||||
}
|
||||
|
||||
if (!smtpPassword) {
|
||||
errors.password = 'SMTP Password must be present';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const valid = !size(errors);
|
||||
|
||||
return { valid, errors };
|
||||
};
|
|
@ -8,6 +8,7 @@
|
|||
visibility: hidden;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
|
||||
&:checked + .kolide-checkbox__tick {
|
||||
&::after {
|
||||
|
@ -36,6 +37,7 @@
|
|||
position: absolute;
|
||||
left: 0;
|
||||
display: inline-block;
|
||||
float: left;
|
||||
|
||||
&::after {
|
||||
@include transition(border 75ms ease-in-out, background 75ms ease-in-out);
|
||||
|
|
|
@ -26,6 +26,7 @@ class Dropdown extends Component {
|
|||
static defaultProps = {
|
||||
onChange: noop,
|
||||
clearable: false,
|
||||
name: 'targets',
|
||||
placeholder: 'Select One...',
|
||||
};
|
||||
|
||||
|
@ -68,7 +69,7 @@ class Dropdown extends Component {
|
|||
<Select
|
||||
className={`${baseClass}__select ${className}`}
|
||||
clearable={clearable}
|
||||
name={`${name}-select` || 'targets'}
|
||||
name={`${name}-select`}
|
||||
onChange={handleChange}
|
||||
options={options}
|
||||
placeholder={placeholder}
|
||||
|
|
|
@ -4,7 +4,8 @@ import { pick } from 'lodash';
|
|||
|
||||
import FormField from 'components/forms/FormField';
|
||||
|
||||
const Slider = ({ onChange, value, inactiveText = 'Off', activeText = 'On' }) => {
|
||||
const Slider = (props) => {
|
||||
const { onChange, value, inactiveText = 'Off', activeText = 'On' } = props;
|
||||
const baseClass = 'kolide-slider';
|
||||
|
||||
const sliderBtnClass = classnames(
|
||||
|
@ -23,7 +24,7 @@ const Slider = ({ onChange, value, inactiveText = 'Off', activeText = 'On' }) =>
|
|||
return onChange(!value);
|
||||
};
|
||||
|
||||
const formFieldProps = pick(this.props, ['hint', 'label', 'error', 'name']);
|
||||
const formFieldProps = pick(props, ['hint', 'label', 'error', 'name']);
|
||||
|
||||
return (
|
||||
<FormField {...formFieldProps} type="slider">
|
||||
|
|
|
@ -251,7 +251,7 @@ class QueryForm extends Component {
|
|||
<Dropdown
|
||||
options={platformOptions}
|
||||
onChange={onFieldChange('platform')}
|
||||
value={platform.value}
|
||||
value={platform}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
import kolideLogo from '../../../../assets/images/kolide-logo.svg';
|
||||
|
||||
class OrgLogoIcon extends Component {
|
||||
static propTypes = {
|
||||
src: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
src: kolideLogo,
|
||||
};
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = { imageSrc: kolideLogo };
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
const { src } = this.props;
|
||||
|
||||
this.setState({ imageSrc: src });
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
const { src } = nextProps;
|
||||
|
||||
this.setState({ imageSrc: src });
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onError = () => {
|
||||
this.setState({ imageSrc: kolideLogo });
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { imageSrc } = this.state;
|
||||
const { onError } = this;
|
||||
|
||||
return (
|
||||
<img
|
||||
alt="Organization Logo"
|
||||
onError={onError}
|
||||
src={imageSrc}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default OrgLogoIcon;
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
import expect from 'expect';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import kolideLogo from '../../../../assets/images/kolide-logo.svg';
|
||||
import OrgLogoIcon from './OrgLogoIcon';
|
||||
|
||||
describe('OrgLogoIcon - component', () => {
|
||||
it('renders the Kolide Logo by default', () => {
|
||||
const component = mount(<OrgLogoIcon />);
|
||||
|
||||
expect(component.state('imageSrc')).toEqual(kolideLogo);
|
||||
});
|
||||
|
||||
it('renders the image source when it is valid', () => {
|
||||
const component = mount(<OrgLogoIcon src="/assets/images/avatar.svg" />);
|
||||
|
||||
expect(component.state('imageSrc')).toEqual('/assets/images/avatar.svg');
|
||||
});
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
export default from './OrgLogoIcon';
|
|
@ -4,10 +4,27 @@ export default (admin) => {
|
|||
icon: 'admin',
|
||||
name: 'Admin',
|
||||
location: {
|
||||
regex: /^\/admin\/users/,
|
||||
regex: /^\/admin/,
|
||||
pathname: '/admin/users',
|
||||
},
|
||||
subItems: [],
|
||||
subItems: [
|
||||
{
|
||||
icon: 'admin',
|
||||
name: 'Manage Users',
|
||||
location: {
|
||||
regex: /\/admin\/users/,
|
||||
pathname: '/admin/users',
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'user-settings',
|
||||
name: 'App Settings',
|
||||
location: {
|
||||
regex: /\/admin\/settings/,
|
||||
pathname: '/admin/settings',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -1,6 +1,19 @@
|
|||
import { PropTypes } from 'react';
|
||||
|
||||
export default PropTypes.shape({
|
||||
authentication_method: PropTypes.string,
|
||||
authentication_type: PropTypes.string,
|
||||
configured: PropTypes.bool,
|
||||
domain: PropTypes.string,
|
||||
enable_ssl_tls: PropTypes.bool,
|
||||
enable_start_tls: PropTypes.bool,
|
||||
kolide_server_url: PropTypes.string,
|
||||
org_logo_url: PropTypes.string,
|
||||
org_name: PropTypes.string,
|
||||
password: PropTypes.string,
|
||||
port: PropTypes.number,
|
||||
sender_address: PropTypes.string,
|
||||
server: PropTypes.string,
|
||||
user_name: PropTypes.string,
|
||||
verify_sll_certs: PropTypes.bool,
|
||||
});
|
||||
|
|
|
@ -4,6 +4,6 @@ export default PropTypes.shape({
|
|||
error: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.any,
|
||||
value: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]),
|
||||
});
|
||||
|
||||
|
|
|
@ -32,6 +32,21 @@ const filterTarget = (targetType) => {
|
|||
};
|
||||
};
|
||||
|
||||
export const formatConfigDataForServer = (config) => {
|
||||
const orgInfoAttrs = ['org_logo_url', 'org_name'];
|
||||
const serverSettingsAttrs = ['kolide_server_url'];
|
||||
const smtpSettingsAttrs = [
|
||||
'authentication_method', 'authentication_type', 'email_enabled', 'enable_ssl_tls',
|
||||
'enable_start_tls', 'password', 'port', 'sender_address', 'server', 'user_name', 'verify_ssl_certs',
|
||||
];
|
||||
|
||||
return {
|
||||
org_info: pick(config, orgInfoAttrs),
|
||||
server_settings: pick(config, serverSettingsAttrs),
|
||||
smtp_settings: pick(config, smtpSettingsAttrs),
|
||||
};
|
||||
};
|
||||
|
||||
export const formatSelectedTargetsForApi = (selectedTargets) => {
|
||||
const targets = selectedTargets || [];
|
||||
const hosts = flatMap(targets, filterTarget('hosts'));
|
||||
|
@ -56,4 +71,4 @@ const setupData = (formData) => {
|
|||
};
|
||||
};
|
||||
|
||||
export default { addGravatarUrlToResource, formatSelectedTargetsForApi, labelSlug, setupData };
|
||||
export default { addGravatarUrlToResource, formatConfigDataForServer, formatSelectedTargetsForApi, labelSlug, setupData };
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import expect from 'expect';
|
||||
import { omit } from 'lodash';
|
||||
|
||||
import { configStub } from 'test/stubs';
|
||||
import helpers from 'kolide/helpers';
|
||||
|
||||
const label1 = { id: 1, target_type: 'labels' };
|
||||
|
@ -15,6 +17,31 @@ describe('Kolide API - helpers', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#formatConfigDataForServer', () => {
|
||||
const { formatConfigDataForServer } = helpers;
|
||||
const config = {
|
||||
org_name: 'Kolide',
|
||||
org_logo_url: '0.0.0.0:8080/logo.png',
|
||||
kolide_server_url: '',
|
||||
configured: false,
|
||||
sender_address: '',
|
||||
server: '',
|
||||
port: 587,
|
||||
authentication_type: 'authtype_username_password',
|
||||
user_name: '',
|
||||
password: '',
|
||||
enable_ssl_tls: true,
|
||||
authentication_method: 'authmethod_plain',
|
||||
verify_ssl_certs: true,
|
||||
enable_start_tls: true,
|
||||
email_enabled: false,
|
||||
};
|
||||
|
||||
it('splits config into categories for the server', () => {
|
||||
expect(formatConfigDataForServer(config)).toEqual(omit(configStub, ['smtp_settings.configured']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#formatSelectedTargetsForApi', () => {
|
||||
const { formatSelectedTargetsForApi } = helpers;
|
||||
|
||||
|
|
|
@ -80,8 +80,7 @@ class Kolide extends Base {
|
|||
getConfig = () => {
|
||||
const { CONFIG } = endpoints;
|
||||
|
||||
return this.authenticatedGet(this.endpoint(CONFIG))
|
||||
.then((response) => { return response.org_info; });
|
||||
return this.authenticatedGet(this.endpoint(CONFIG));
|
||||
}
|
||||
|
||||
getInvites = () => {
|
||||
|
@ -330,6 +329,13 @@ class Kolide extends Base {
|
|||
return Base.post(this.endpoint(SETUP), JSON.stringify(setupData));
|
||||
}
|
||||
|
||||
updateConfig = (formData) => {
|
||||
const { CONFIG } = endpoints;
|
||||
const configData = helpers.formatConfigDataForServer(formData);
|
||||
|
||||
return this.authenticatedPatch(this.endpoint(CONFIG), JSON.stringify(configData));
|
||||
}
|
||||
|
||||
updateQuery = ({ id: queryID }, updateParams) => {
|
||||
const { QUERIES } = endpoints;
|
||||
const updateQueryEndpoint = `${this.baseURL}${QUERIES}/${queryID}`;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import expect from 'expect';
|
||||
import nock from 'nock';
|
||||
|
||||
import Kolide from './index';
|
||||
import mocks from '../test/mocks';
|
||||
import Kolide from 'kolide';
|
||||
import helpers from 'kolide/helpers';
|
||||
import mocks from 'test/mocks';
|
||||
|
||||
const {
|
||||
invalidForgotPasswordRequest,
|
||||
|
@ -29,6 +30,7 @@ const {
|
|||
validRevokeInviteRequest,
|
||||
validRunQueryRequest,
|
||||
validSetupRequest,
|
||||
validUpdateConfigRequest,
|
||||
validUpdateQueryRequest,
|
||||
validUpdateUserRequest,
|
||||
validUser,
|
||||
|
@ -463,6 +465,39 @@ describe('Kolide - API client', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#updateConfig', () => {
|
||||
it('calls the appropriate endpoint with the correct parameters', (done) => {
|
||||
const bearerToken = 'valid-bearer-token';
|
||||
const formData = {
|
||||
org_name: 'Kolide',
|
||||
org_logo_url: '0.0.0.0:8080/logo.png',
|
||||
kolide_server_url: '',
|
||||
configured: false,
|
||||
sender_address: '',
|
||||
server: '',
|
||||
port: 587,
|
||||
authentication_type: 'authtype_username_password',
|
||||
user_name: '',
|
||||
password: '',
|
||||
enable_ssl_tls: true,
|
||||
authentication_method: 'authmethod_plain',
|
||||
verify_ssl_certs: true,
|
||||
enable_start_tls: true,
|
||||
email_enabled: false,
|
||||
};
|
||||
const configData = helpers.formatConfigDataForServer(formData);
|
||||
const request = validUpdateConfigRequest(bearerToken, configData);
|
||||
|
||||
Kolide.setBearerToken(bearerToken);
|
||||
Kolide.updateConfig(formData)
|
||||
.then(() => {
|
||||
expect(request.isDone()).toEqual(true);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateQuery', () => {
|
||||
it('calls the appropriate endpoint with the correct parameters', (done) => {
|
||||
const bearerToken = 'valid-bearer-token';
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { size } from 'lodash';
|
||||
|
||||
import AppConfigForm from 'components/forms/admin/AppConfigForm';
|
||||
import configInterface from 'interfaces/config';
|
||||
import { renderFlash } from 'redux/nodes/notifications/actions';
|
||||
import SmtpWarning from 'pages/Admin/AppSettingsPage/SmtpWarning';
|
||||
import { updateConfig } from 'redux/nodes/app/actions';
|
||||
|
||||
export const baseClass = 'app-settings';
|
||||
|
||||
class AppSettingsPage extends Component {
|
||||
static propTypes = {
|
||||
appConfig: configInterface,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
};
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = { showSmtpWarning: true };
|
||||
}
|
||||
|
||||
onDismissSmtpWarning = () => {
|
||||
this.setState({ showSmtpWarning: false });
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onFormSubmit = (formData) => {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(updateConfig(formData))
|
||||
.then(() => {
|
||||
dispatch(renderFlash('success', 'Settings updated!'));
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { appConfig, error } = this.props;
|
||||
const { onDismissSmtpWarning, onFormSubmit } = this;
|
||||
const { showSmtpWarning } = this.state;
|
||||
const { configured: smtpConfigured } = appConfig;
|
||||
const shouldShowWarning = !smtpConfigured && showSmtpWarning;
|
||||
|
||||
if (!size(appConfig)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${baseClass} body-wrap`}>
|
||||
<h1>App Settings</h1>
|
||||
<SmtpWarning
|
||||
onDismiss={onDismissSmtpWarning}
|
||||
shouldShowWarning={shouldShowWarning}
|
||||
/>
|
||||
<AppConfigForm
|
||||
formData={appConfig}
|
||||
errors={error}
|
||||
handleSubmit={onFormSubmit}
|
||||
smtpConfigured={smtpConfigured}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ app }) => {
|
||||
const { config: appConfig, error } = app;
|
||||
|
||||
return { appConfig, error };
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(AppSettingsPage);
|
|
@ -0,0 +1,58 @@
|
|||
import expect from 'expect';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import AppSettingsPage from 'pages/Admin/AppSettingsPage';
|
||||
import testHelpers from 'test/helpers';
|
||||
|
||||
const { connectedComponent, reduxMockStore } = testHelpers;
|
||||
|
||||
describe('AppSettingsPage - component', () => {
|
||||
it('renders', () => {
|
||||
const mockStore = reduxMockStore({ app: { config: {} } });
|
||||
const page = mount(connectedComponent(AppSettingsPage, { mockStore }));
|
||||
|
||||
expect(page.find('AppSettingsPage').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('renders a warning if SMTP has not been configured', () => {
|
||||
const storeWithoutSMTPConfig = { app: { config: { configured: false } } };
|
||||
|
||||
const mockStore = reduxMockStore(storeWithoutSMTPConfig);
|
||||
const page = mount(
|
||||
connectedComponent(AppSettingsPage, { mockStore })
|
||||
).find('AppSettingsPage');
|
||||
|
||||
const smtpWarning = page.find('SmtpWarning');
|
||||
|
||||
expect(smtpWarning.length).toEqual(1);
|
||||
expect(smtpWarning.find('Icon').length).toEqual(1);
|
||||
expect(smtpWarning.text()).toInclude('Email is not currently configured in Kolide');
|
||||
});
|
||||
|
||||
it('dismisses the smtp warning when "DISMISS" is clicked', () => {
|
||||
const storeWithoutSMTPConfig = { app: { config: { configured: false } } };
|
||||
|
||||
const mockStore = reduxMockStore(storeWithoutSMTPConfig);
|
||||
const page = mount(
|
||||
connectedComponent(AppSettingsPage, { mockStore })
|
||||
).find('AppSettingsPage');
|
||||
|
||||
const smtpWarning = page.find('SmtpWarning');
|
||||
const dismissButton = smtpWarning.find('Button').first();
|
||||
|
||||
dismissButton.simulate('click');
|
||||
|
||||
expect(page.find('SmtpWarning').html()).toNotExist();
|
||||
});
|
||||
|
||||
it('does not render a warning if SMTP has been configured', () => {
|
||||
const storeWithoutSMTPConfig = { app: { config: { configured: true } } };
|
||||
|
||||
const mockStore = reduxMockStore(storeWithoutSMTPConfig);
|
||||
const page = mount(
|
||||
connectedComponent(AppSettingsPage, { mockStore })
|
||||
).find('AppSettingsPage');
|
||||
|
||||
expect(page.find('SmtpWarning').html()).toNotExist();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
|
||||
import Button from 'components/buttons/Button';
|
||||
import Icon from 'components/icons/Icon';
|
||||
|
||||
const baseClass = 'smtp-warning';
|
||||
|
||||
const SmtpWarning = ({ onDismiss, shouldShowWarning }) => {
|
||||
if (!shouldShowWarning) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__icon-wrap`}>
|
||||
<Icon name="warning-filled" />
|
||||
<span className={`${baseClass}__label`}>Warning!</span>
|
||||
</div>
|
||||
<span className={`${baseClass}__text`}>Email is not currently configured in Kolide. Many features rely on email to work.</span>
|
||||
<Button onClick={onDismiss} text="Dismiss" variant="unstyled" />
|
||||
<Button text="Resolve" variant="unstyled" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SmtpWarning.propTypes = {
|
||||
onDismiss: PropTypes.func.isRequired,
|
||||
shouldShowWarning: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default SmtpWarning;
|
|
@ -0,0 +1,77 @@
|
|||
.app-settings {
|
||||
padding: 30px;
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: $light;
|
||||
line-height: 1.33;
|
||||
letter-spacing: 1px;
|
||||
color: $text-dark;
|
||||
margin: 0 0 22px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
font-weight: $normal;
|
||||
line-height: 2.4;
|
||||
letter-spacing: 0.8px;
|
||||
color: $text-dark;
|
||||
border-bottom: dashed 1px $accent-dark;
|
||||
margin: 0 0 15px;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.kolidecon {
|
||||
font-size: 8px;
|
||||
margin: 0 20px;
|
||||
vertical-align: 4px;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 15px;
|
||||
font-weight: $normal;
|
||||
line-height: 1.6;
|
||||
letter-spacing: 0.5px;
|
||||
color: $text-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.smtp-warning {
|
||||
@include display(flex);
|
||||
@include justify-content(space-between);
|
||||
@include align-items(flex-start);
|
||||
background-color: $alert;
|
||||
color: $white;
|
||||
padding: 15px 20px;
|
||||
border-radius: 2px;
|
||||
font-size: 15px;
|
||||
margin: 0 0 25px;
|
||||
|
||||
&__icon-wrap {
|
||||
min-width: 130px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-weight: $bold;
|
||||
margin-left: 15px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&__text {
|
||||
@include flex-grow(1);
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.button {
|
||||
color: $white;
|
||||
font-size: 15px;
|
||||
text-decoration: underline;
|
||||
font-weight: $light;
|
||||
letter-spacing: 0.6px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export default from './AppSettingsPage';
|
|
@ -3,7 +3,7 @@ import classnames from 'classnames';
|
|||
|
||||
import Avatar from '../../../../components/Avatar';
|
||||
import Dropdown from '../../../../components/forms/fields/Dropdown';
|
||||
import EditUserForm from '../../../../components/forms/Admin/EditUserForm';
|
||||
import EditUserForm from '../../../../components/forms/admin/EditUserForm';
|
||||
import userInterface from '../../../../interfaces/user';
|
||||
import { userStatusLabel } from './helpers';
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import Kolide from '../../../kolide';
|
||||
import Kolide from 'kolide';
|
||||
|
||||
import formatApiErrors from 'utilities/format_api_errors';
|
||||
import { frontendFormattedConfig } from 'redux/nodes/app/helpers';
|
||||
|
||||
export const CONFIG_FAILURE = 'CONFIG_FAILURE';
|
||||
export const CONFIG_START = 'CONFIG_START';
|
||||
|
@ -25,9 +28,11 @@ export const getConfig = () => {
|
|||
|
||||
return Kolide.getConfig()
|
||||
.then((config) => {
|
||||
dispatch(configSuccess(config));
|
||||
const formattedConfig = frontendFormattedConfig(config);
|
||||
|
||||
return config;
|
||||
dispatch(configSuccess(formattedConfig));
|
||||
|
||||
return formattedConfig;
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch(configFailure(error));
|
||||
|
@ -36,3 +41,24 @@ export const getConfig = () => {
|
|||
});
|
||||
};
|
||||
};
|
||||
export const updateConfig = (configData) => {
|
||||
return (dispatch) => {
|
||||
dispatch(loadConfig);
|
||||
|
||||
return Kolide.updateConfig(configData)
|
||||
.then((config) => {
|
||||
const formattedConfig = frontendFormattedConfig(config);
|
||||
|
||||
dispatch(configSuccess(formattedConfig));
|
||||
|
||||
return formattedConfig;
|
||||
})
|
||||
.catch((error) => {
|
||||
const formattedErrors = formatApiErrors(error);
|
||||
|
||||
dispatch(configFailure(formattedErrors));
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import expect from 'expect';
|
||||
|
||||
import { CONFIG_START, CONFIG_SUCCESS, getConfig } from './actions';
|
||||
import Kolide from '../../../kolide';
|
||||
import { reduxMockStore } from '../../../test/helpers';
|
||||
import { validGetConfigRequest } from '../../../test/mocks';
|
||||
import { CONFIG_START, CONFIG_SUCCESS, getConfig, updateConfig } from 'redux/nodes/app/actions';
|
||||
import { configStub } from 'test/stubs';
|
||||
import { frontendFormattedConfig } from 'redux/nodes/app/helpers';
|
||||
import Kolide from 'kolide';
|
||||
import { reduxMockStore } from 'test/helpers';
|
||||
import { validGetConfigRequest, validUpdateConfigRequest } from 'test/mocks';
|
||||
|
||||
describe('App - actions', () => {
|
||||
describe('getConfig action', () => {
|
||||
|
@ -39,4 +41,39 @@ describe('App - actions', () => {
|
|||
.catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateConfig action', () => {
|
||||
const store = reduxMockStore({});
|
||||
const configFormData = frontendFormattedConfig(configStub);
|
||||
|
||||
it('calls the api update config endpoint', (done) => {
|
||||
const bearerToken = 'abc123';
|
||||
const request = validUpdateConfigRequest(bearerToken);
|
||||
|
||||
Kolide.setBearerToken(bearerToken);
|
||||
store.dispatch(updateConfig(configFormData))
|
||||
.then(() => {
|
||||
expect(request.isDone()).toEqual(true);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('dispatches CONFIG_START & CONFIG_SUCCESS actions', (done) => {
|
||||
const bearerToken = 'abc123';
|
||||
validUpdateConfigRequest(bearerToken);
|
||||
|
||||
Kolide.setBearerToken(bearerToken);
|
||||
store.dispatch(updateConfig(configFormData))
|
||||
.then(() => {
|
||||
const actions = store.getActions()
|
||||
.map((action) => { return action.type; });
|
||||
|
||||
expect(actions).toInclude(CONFIG_START);
|
||||
expect(actions).toInclude(CONFIG_SUCCESS);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
export const frontendFormattedConfig = (config) => {
|
||||
const {
|
||||
org_info: orgInfo,
|
||||
server_settings: serverSettings,
|
||||
smtp_settings: smtpSettings,
|
||||
} = config;
|
||||
|
||||
return {
|
||||
...orgInfo,
|
||||
...serverSettings,
|
||||
...smtpSettings,
|
||||
};
|
||||
};
|
||||
|
||||
export default { frontendFormattedConfig };
|
|
@ -0,0 +1,24 @@
|
|||
import expect from 'expect';
|
||||
|
||||
import { configStub } from 'test/stubs';
|
||||
import helpers from 'redux/nodes/app/helpers';
|
||||
|
||||
describe('redux app node - helpers', () => {
|
||||
describe('#frontendFormattedConfig', () => {
|
||||
const { frontendFormattedConfig } = helpers;
|
||||
|
||||
it('returns a flattened config object', () => {
|
||||
const {
|
||||
org_info: orgInfo,
|
||||
server_settings: serverSettings,
|
||||
smtp_settings: smtpSettings,
|
||||
} = configStub;
|
||||
|
||||
expect(frontendFormattedConfig(configStub)).toEqual({
|
||||
...orgInfo,
|
||||
...serverSettings,
|
||||
...smtpSettings,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -3,6 +3,7 @@ import { browserHistory, IndexRoute, Route, Router } from 'react-router';
|
|||
import { Provider } from 'react-redux';
|
||||
import { syncHistoryWithStore } from 'react-router-redux';
|
||||
|
||||
import AdminAppSettingsPage from 'pages/Admin/AppSettingsPage';
|
||||
import AdminUserManagementPage from 'pages/Admin/UserManagementPage';
|
||||
import AllPacksPage from 'pages/packs/AllPacksPage';
|
||||
import App from 'components/App';
|
||||
|
@ -40,6 +41,7 @@ const routes = (
|
|||
<IndexRoute component={HomePage} />
|
||||
<Route path="admin" component={AuthenticatedAdminRoutes}>
|
||||
<Route path="users" component={AdminUserManagementPage} />
|
||||
<Route path="settings" component={AdminAppSettingsPage} />
|
||||
</Route>
|
||||
<Route path="hosts">
|
||||
<Route path="new" component={NewHostPage} />
|
||||
|
|
|
@ -17,7 +17,7 @@ export const reduxMockStore = (store = {}) => {
|
|||
};
|
||||
|
||||
export const connectedComponent = (ComponentClass, options = {}) => {
|
||||
const { props = {}, mockStore = reduxMockStore() } = options;
|
||||
const { mockStore = reduxMockStore(), props = {} } = options;
|
||||
|
||||
return (
|
||||
<Provider store={mockStore}>
|
||||
|
|
|
@ -295,6 +295,16 @@ export const validSetupRequest = (formData) => {
|
|||
.reply(200, {});
|
||||
};
|
||||
|
||||
export const validUpdateConfigRequest = (bearerToken, configData) => {
|
||||
return nock('http://localhost:8080', {
|
||||
reqHeaders: {
|
||||
Authorization: `Bearer ${bearerToken}`,
|
||||
},
|
||||
})
|
||||
.patch('/api/v1/kolide/config', JSON.stringify(configData))
|
||||
.reply(200, {});
|
||||
};
|
||||
|
||||
export const validUpdateQueryRequest = (bearerToken, query, formData) => {
|
||||
return nock('http://localhost:8080', {
|
||||
reqHeaders: {
|
||||
|
@ -338,6 +348,7 @@ export default {
|
|||
validRevokeInviteRequest,
|
||||
validRunQueryRequest,
|
||||
validSetupRequest,
|
||||
validUpdateConfigRequest,
|
||||
validUpdateQueryRequest,
|
||||
validUpdateUserRequest,
|
||||
validUser,
|
||||
|
|
|
@ -6,6 +6,30 @@ export const adminUserStub = {
|
|||
username: 'gnardog',
|
||||
};
|
||||
|
||||
export const configStub = {
|
||||
org_info: {
|
||||
org_name: 'Kolide',
|
||||
org_logo_url: '0.0.0.0:8080/logo.png',
|
||||
},
|
||||
server_settings: {
|
||||
kolide_server_url: '',
|
||||
},
|
||||
smtp_settings: {
|
||||
configured: false,
|
||||
sender_address: '',
|
||||
server: '',
|
||||
port: 587,
|
||||
authentication_type: 'authtype_username_password',
|
||||
user_name: '',
|
||||
password: '',
|
||||
enable_ssl_tls: true,
|
||||
authentication_method: 'authmethod_plain',
|
||||
verify_ssl_certs: true,
|
||||
enable_start_tls: true,
|
||||
email_enabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const packStub = {
|
||||
created_at: '0001-01-01T00:00:00Z',
|
||||
updated_at: '0001-01-01T00:00:00Z',
|
||||
|
@ -57,6 +81,7 @@ export const userStub = {
|
|||
|
||||
export default {
|
||||
adminUserStub,
|
||||
configStub,
|
||||
packStub,
|
||||
queryStub,
|
||||
scheduledQueryStub,
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
export default (error) => {
|
||||
if (!error.response || !error.response.errors) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { errors: errorsArray } = error.response;
|
||||
const result = {};
|
||||
|
||||
errorsArray.forEach((errorObject) => {
|
||||
result[errorObject.name] = errorObject.reason;
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
import expect from 'expect';
|
||||
|
||||
import formatApiErrors from 'utilities/format_api_errors';
|
||||
|
||||
describe('formatApiErrors', () => {
|
||||
const errorStub = {
|
||||
response: {
|
||||
errors: [
|
||||
{
|
||||
name: 'email',
|
||||
reason: 'is not the correct format',
|
||||
},
|
||||
{
|
||||
name: 'kolide_server_url',
|
||||
reason: 'must be present',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
it('formats errors for the Form HOC', () => {
|
||||
expect(formatApiErrors(errorStub)).toEqual({
|
||||
email: 'is not the correct format',
|
||||
kolide_server_url: 'must be present',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
export default from './format_api_errors';
|
Загрузка…
Ссылка в новой задаче