Merge branch 'master' of https://gitlab.com/fyipe-project/app into feature-custom-tutorial

This commit is contained in:
Zadat Olayinka
2020-09-14 08:22:58 +01:00
51 changed files with 1727 additions and 747 deletions

View File

@@ -151,7 +151,7 @@ export function verifyAuthToken(values) {
const redirect = getQueryVar('redirectTo', initialUrl);
if (redirect) values.redirect = redirect;
const email = User.getEmail();
values.email = email;
values.email = values.email || email;
return function(dispatch) {
const promise = postApi('user/totp/verifyToken', values);
dispatch(verifyTokenRequest(promise));
@@ -205,7 +205,7 @@ export function verifyBackupCode(values) {
const redirect = getQueryVar('redirectTo', initialUrl);
if (redirect) values.redirect = redirect;
const email = User.getEmail();
values.email = email;
values.email = values.email || email;
return function(dispatch) {
const promise = postApi('user/verify/backupCode', values);
dispatch(useBackupCodeRequest(promise));

View File

@@ -8,6 +8,7 @@ import { emaildomains } from './constants/emaildomains';
let apiUrl = window.location.origin + '/api';
let dashboardUrl = window.location.origin + '/dashboard';
let adminDashboardUrl = window.location.origin + '/admin';
let accountsUrl = window.location.origin + '/accounts';
if (
window &&
@@ -25,6 +26,7 @@ if (
apiUrl = window.location.protocol + `//${address}:3002/api`;
dashboardUrl = window.location.protocol + `//${address}:3000/dashboard`;
adminDashboardUrl = window.location.protocol + `//${address}:3100/admin`;
accountsUrl = window.location.protocol + `//${address}:3003/accounts`;
}
export function env(value) {
@@ -41,6 +43,8 @@ export const DASHBOARD_URL = dashboardUrl;
export const ADMIN_DASHBOARD_URL = adminDashboardUrl;
export const ACCOUNTS_URL = accountsUrl;
export const SHOULD_LOG_ANALYTICS = !!env('AMPLITUDE_PUBLIC_KEY');
export const IS_SAAS_SERVICE = !!env('IS_SAAS_SERVICE');

View File

@@ -8,7 +8,7 @@ import { bindActionCreators } from 'redux';
import { RenderField } from '../components/basic/RenderField';
import { Link } from 'react-router-dom';
import { identify, setUserId, logEvent } from '../analytics';
import { SHOULD_LOG_ANALYTICS } from '../config';
import { SHOULD_LOG_ANALYTICS, ACCOUNTS_URL } from '../config';
const errorStyle = { color: '#c23d4b' };
@@ -27,7 +27,8 @@ export class VerifyAuthToken extends Component {
}
submitForm = values => {
this.props.verifyAuthToken(values).then(user => {
const email = this.props.login.user.email;
this.props.verifyAuthToken({ ...values, email }).then(user => {
if (user && user.data && user.data.id) {
if (SHOULD_LOG_ANALYTICS) {
identify(user.data.id);
@@ -39,6 +40,8 @@ export class VerifyAuthToken extends Component {
};
render() {
if (!this.props.login.user.email)
window.location = ACCOUNTS_URL + '/login';
const { error } = this.props.login.authToken;
let header;

View File

@@ -8,7 +8,7 @@ import { bindActionCreators } from 'redux';
import { RenderField } from '../components/basic/RenderField';
import { Link } from 'react-router-dom';
import { logEvent, setUserId, identify } from '../analytics';
import { SHOULD_LOG_ANALYTICS } from '../config';
import { SHOULD_LOG_ANALYTICS, ACCOUNTS_URL } from '../config';
const errorStyle = { color: '#c23d4b' };
@@ -27,7 +27,8 @@ export class VerifyBackupCode extends Component {
}
submitForm = values => {
this.props.verifyBackupCode(values).then(user => {
const email = this.props.login.user.email;
this.props.verifyBackupCode({ ...values, email }).then(user => {
if (user && user.data && user.data.id) {
if (SHOULD_LOG_ANALYTICS) {
setUserId(user.data.id);
@@ -39,6 +40,8 @@ export class VerifyBackupCode extends Component {
};
render() {
if (!this.props.login.user.email)
window.location = ACCOUNTS_URL + '/login';
const { backupCode } = this.props.login;
let header;
@@ -104,6 +107,15 @@ export class VerifyBackupCode extends Component {
</form>
</div>
</div>
<div className="below-box">
<p>
Have a google app authenticator?{' '}
<Link to="/accounts/user-auth/token">
Enter auth token
</Link>
.
</p>
</div>
<div id="footer_spacer" />
<div id="bottom">
<ul>

View File

@@ -120,6 +120,7 @@ export default function register(state = initialState, action) {
error: null,
success: true,
},
user: { ...state.user },
});
case AUTH_VERIFICATION_SUCCESS:
return Object.assign({}, state, {
@@ -140,6 +141,7 @@ export default function register(state = initialState, action) {
error: action.payload,
success: false,
},
user: { ...state.user },
});
case RESET_AUTH_VERIFICATION:
@@ -156,6 +158,7 @@ export default function register(state = initialState, action) {
error: null,
success: true,
},
user: { ...state.user },
});
case BACKUP_CODE_VERIFICATION_SUCCESS:
return Object.assign({}, state, {
@@ -176,6 +179,7 @@ export default function register(state = initialState, action) {
error: action.payload,
success: false,
},
user: { ...state.user },
});
case RESET_BACKUP_CODE_VERIFICATION:

View File

@@ -15501,6 +15501,21 @@
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
"integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc="
},
"qr.js": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz",
"integrity": "sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8="
},
"qrcode.react": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-1.0.0.tgz",
"integrity": "sha512-jBXleohRTwvGBe1ngV+62QvEZ/9IZqQivdwzo9pJM4LQMoCM2VnvNBnKdjvGnKyDZ/l0nCDgsPod19RzlPvm/Q==",
"requires": {
"loose-envify": "^1.4.0",
"prop-types": "^15.6.0",
"qr.js": "0.0.0"
}
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",

View File

@@ -22,6 +22,7 @@
"prop-types": "^15.6.1",
"puppeteer": "^2.1.1",
"puppeteer-cluster": "^0.19.0",
"qrcode.react": "^1.0.0",
"react": "^16.5.2",
"react-click-outside": "github:tj/react-click-outside",
"react-dom": "^16.5.2",

View File

@@ -15,32 +15,36 @@
.tooltip .tooltiptext {
visibility: hidden;
width: 70px;
background-color: #555;
color: #fff;
text-align: center;
border-radius: 6px;
z-index: 1;
opacity: 0;
height: 25px;
right: 0;
position: absolute;
line-height: 25px;
display: flex;
align-items: center;
justify-content: space-evenly;
padding: 5px;
width: 90px;
height: 28px;
}
.tooltip .tooltiptext strong {
text-transform: capitalize;
background: #818181;
border-radius: 5px;
width: 19px;
height: 18px;
line-height: 19px;
font-size: 13px;
}
.tooltip .tooltiptext span {
font-size: 12px;
}
.tooltip .tooltiptext::after {
/* .tooltip .tooltiptext::after {
content: " ";
position: absolute;
bottom: 30%;
@@ -48,7 +52,7 @@
border-width: 5px;
border-style: solid;
border-color: transparent #555 transparent transparent;
}
} */
.tooltip:hover .tooltiptext {
animation: fadeIn 0.2s;

View File

@@ -548,3 +548,139 @@ export const searchUsers = (filter, skip, limit) => async dispatch => {
dispatch(searchUsersError(errors(errorMsg)));
}
};
// Update user twoFactorAuthToken
export function twoFactorAuthTokenRequest() {
return {
type: types.UPDATE_TWO_FACTOR_AUTH_REQUEST,
};
}
export function twoFactorAuthTokenSuccess(payload) {
return {
type: types.UPDATE_TWO_FACTOR_AUTH_SUCCESS,
payload: payload,
};
}
export function twoFactorAuthTokenError(error) {
return {
type: types.UPDATE_TWO_FACTOR_AUTH_FAILURE,
payload: error,
};
}
export function updateTwoFactorAuthToken(data) {
return function(dispatch) {
const promise = putApi('user/profile', data);
dispatch(twoFactorAuthTokenRequest());
promise.then(
function(response) {
const payload = response.data;
dispatch(twoFactorAuthTokenSuccess(payload));
return payload;
},
function(error) {
if (error && error.response && error.response.data)
error = error.response.data;
if (error && error.data) {
error = error.data;
}
if (error && error.message) {
error = error.message;
} else {
error = 'Network Error';
}
dispatch(twoFactorAuthTokenError(errors(error)));
}
);
return promise;
};
}
export function setTwoFactorAuth(enabled) {
return {
type: types.SET_TWO_FACTOR_AUTH,
payload: enabled,
};
}
export function verifyTwoFactorAuthToken(values) {
return function(dispatch) {
const promise = postApi('user/totp/verifyToken', values);
dispatch(twoFactorAuthTokenRequest());
promise.then(
function(response) {
const payload = response.data;
dispatch(twoFactorAuthTokenSuccess(payload));
return payload;
},
function(error) {
if (error && error.response && error.response.data)
error = error.response.data;
if (error && error.data) {
error = error.data;
}
if (error && error.message) {
error = error.message;
} else {
error = 'Network Error';
}
dispatch(twoFactorAuthTokenError(errors(error)));
}
);
return promise;
};
}
// Generate user's QR code
export function generateTwoFactorQRCodeRequest() {
return {
type: types.GENERATE_TWO_FACTOR_QR_REQUEST,
};
}
export function generateTwoFactorQRCodeSuccess(payload) {
return {
type: types.GENERATE_TWO_FACTOR_QR_SUCCESS,
payload: payload,
};
}
export function generateTwoFactorQRCodeError(error) {
return {
type: types.GENERATE_TWO_FACTOR_QR_FAILURE,
payload: error,
};
}
export function generateTwoFactorQRCode(userId) {
return function(dispatch) {
const promise = postApi(`user/totp/token/${userId}`);
dispatch(generateTwoFactorQRCodeRequest());
promise.then(
function(response) {
const payload = response.data;
dispatch(generateTwoFactorQRCodeSuccess(payload));
return payload;
},
function(error) {
if (error && error.response && error.response.data)
error = error.response.data;
if (error && error.data) {
error = error.data;
}
if (error && error.message) {
error = error.message;
} else {
error = 'Network Error';
}
dispatch(generateTwoFactorQRCodeError(errors(error)));
}
);
return promise;
};
}

View File

@@ -0,0 +1,337 @@
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { closeModal } from '../../actions/modal';
import ShouldRender from '../basic/ShouldRender';
import { reduxForm, Field, SubmissionError } from 'redux-form';
import { Spinner } from '../basic/Loader';
import QRCode from 'qrcode.react';
import { RenderField } from '../basic/RenderField';
import { ListLoader } from '../basic/Loader.js';
import {
setTwoFactorAuth,
verifyTwoFactorAuthToken,
generateTwoFactorQRCode,
twoFactorAuthTokenError,
} from '../../actions/user';
function validate() {
return undefined;
}
class TwoFactorAuthModal extends Component {
state = { next: false };
async componentDidMount() {
const {
user: { user },
generateTwoFactorQRCode,
} = this.props;
generateTwoFactorQRCode(user._id);
}
handleKeyBoard = e => {
const { twoFactorAuthId, closeModal } = this.props;
switch (e.key) {
case 'Escape':
return closeModal({
id: twoFactorAuthId,
});
default:
return false;
}
};
nextHandler = event => {
event.preventDefault();
this.setState({ next: true });
};
submitForm = values => {
if (!values.token) {
throw new SubmissionError({ token: 'Auth token is required.' });
}
const { twoFactorAuthId, closeModal } = this.props;
if (values.token) {
const {
setTwoFactorAuth,
verifyTwoFactorAuthToken,
user: { user },
} = this.props;
values.userId = user._id;
verifyTwoFactorAuthToken(values).then(response => {
setTwoFactorAuth(response.data.twoFactorAuthEnabled);
return closeModal({
id: twoFactorAuthId,
});
});
}
};
render() {
const { handleSubmit, qrCode, twoFactorAuthSetting } = this.props;
const { next } = this.state;
return (
<form onSubmit={handleSubmit(this.submitForm)}>
<div
onKeyDown={this.handleKeyBoard}
className="ModalLayer-wash Box-root Flex-flex Flex-alignItems--flexStart Flex-justifyContent--center"
>
<div
className="ModalLayer-contents"
tabIndex={-1}
style={{ marginTop: 40 }}
>
<div className="bs-BIM">
<div className="bs-Modal bs-Modal--medium">
<div className="bs-Modal-header">
<div className="bs-Modal-header-copy">
<span className="Text-color--inherit Text-display--inline Text-fontSize--20 Text-fontWeight--medium Text-lineHeight--24 Text-typeface--base Text-wrap--wrap">
<span>
Two Factor Authentication
</span>
</span>
</div>
<div className="bs-Modal-messages">
<ShouldRender
if={twoFactorAuthSetting.error}
>
<p
className="bs-Modal-message"
id="modal-message"
>
{twoFactorAuthSetting.error}
</p>
</ShouldRender>
</div>
</div>
<div>
<div className="bs-Fieldset-wrapper Box-root Margin-bottom--2">
<div className="bs-u-paddingless">
<div className="bs-Modal-block">
<div>
{next ? (
<div>
<div
className="bs-Fieldset-wrapper Box-root"
style={{
width:
'90%',
margin:
'1px 0 6px 2%',
}}
>
<p>
Input a
token from
your mobile
device to
complete
setup
</p>
</div>
<div className="bs-Modal-body">
<Field
className="bs-TextInput"
component={
RenderField
}
name="token"
id="token"
placeholder="Verification token"
disabled={
twoFactorAuthSetting.requesting
}
style={{
width:
'90%',
margin:
'5px 0 10px 2%',
}}
/>
</div>
</div>
) : (
<div className="bs-Fieldset-wrapper Box-root">
<div
className="bs-Fieldset-wrapper Box-root"
style={{
marginBottom:
'10px',
marginTop:
'-5px',
}}
>
<p>
Download the
Google
Authenticator
Mobile app
on your
mobile
device
<span>
{' '}
(
<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en">
Android
</a>
,
<a href="https://apps.apple.com/us/app/google-authenticator/id388497605">
{' '}
IOS
</a>
)
</span>{' '}
and then
scan the QR
code below
to set up
Two-factor
Authentication
with an
Authenticator
app.
</p>
</div>
{qrCode.data
.otpauth_url ? (
<QRCode
size={230}
value={`${qrCode.data.otpauth_url}`}
style={{
display:
'block',
margin:
'0 auto',
}}
/>
) : (
<ListLoader />
)}
</div>
)}
</div>
</div>
</div>
</div>
</div>
<div className="bs-Modal-footer">
<div className="bs-Modal-footer-actions">
<button
className={`bs-Button bs-DeprecatedButton ${twoFactorAuthSetting.requesting &&
'bs-is-disabled'}`}
type="button"
onClick={() => {
this.props.twoFactorAuthTokenError(
''
);
this.props.closeModal({
id: this.props
.twoFactorAuthId,
});
}}
disabled={
twoFactorAuthSetting.requesting
}
>
<span>Cancel</span>
</button>
{!next ? (
<button
id="nextFormButton"
className={`bs-Button bs-DeprecatedButton bs-Button--blue ${twoFactorAuthSetting.requesting &&
'bs-is-disabled'}`}
disabled={
twoFactorAuthSetting.requesting
}
onClick={event =>
this.nextHandler(event)
}
type="button"
>
<ShouldRender
if={
twoFactorAuthSetting.requesting
}
>
<Spinner />
</ShouldRender>
<span>Next</span>
</button>
) : (
<button
id="enableTwoFactorAuthButton"
className={`bs-Button bs-DeprecatedButton bs-Button--blue ${twoFactorAuthSetting.requesting &&
'bs-is-disabled'}`}
type="submit"
disabled={
twoFactorAuthSetting.requesting
}
>
<ShouldRender
if={
twoFactorAuthSetting.requesting
}
>
<Spinner />
</ShouldRender>
<span>Verify</span>
</button>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</form>
);
}
}
TwoFactorAuthModal.displayName = 'TwoFactorAuthModal';
const TwoFactorAuthForm = reduxForm({
form: 'TwoFactorAuthForm',
enableReinitialize: true,
validate,
})(TwoFactorAuthModal);
TwoFactorAuthModal.propTypes = {
handleSubmit: PropTypes.func.isRequired,
closeModal: PropTypes.func.isRequired,
generateTwoFactorQRCode: PropTypes.func,
setTwoFactorAuth: PropTypes.func,
user: PropTypes.object,
qrCode: PropTypes.object,
twoFactorAuthId: PropTypes.string,
twoFactorAuthSetting: PropTypes.object,
verifyTwoFactorAuthToken: PropTypes.func,
twoFactorAuthTokenError: PropTypes.func,
};
const mapStateToProps = state => {
return {
user: state.user.user,
qrCode: state.user.qrCode,
twoFactorAuthSetting: state.user.twoFactorAuthSetting,
};
};
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
closeModal,
setTwoFactorAuth,
verifyTwoFactorAuthToken,
generateTwoFactorQRCode,
twoFactorAuthTokenError,
},
dispatch
);
export default connect(mapStateToProps, mapDispatchToProps)(TwoFactorAuthForm);

View File

@@ -112,7 +112,7 @@ UserBlockBox.propTypes = {
blockUser: PropTypes.func.isRequired,
closeModal: PropTypes.func,
openModal: PropTypes.func.isRequired,
userId: PropTypes.string.isRequired,
userId: PropTypes.string,
};
UserBlockBox.contextTypes = {

View File

@@ -69,7 +69,7 @@ const mapStateToProps = state => {
UserProject.propTypes = {
fetchUserProjects: PropTypes.func.isRequired,
userId: PropTypes.string,
projects: PropTypes.array,
projects: PropTypes.object,
};
export default connect(mapStateToProps, mapDispatchToProps)(UserProject);

View File

@@ -3,6 +3,10 @@ import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import ShouldRender from '../basic/ShouldRender';
import PropTypes from 'prop-types';
import DataPathHoC from '../DataPathHoC';
import TwoFactorAuthModal from './TwoFactorAuth';
import { updateTwoFactorAuthToken, setTwoFactorAuth } from '../../actions/user';
import { openModal } from '../../actions/modal';
export class UserSetting extends Component {
constructor(props) {
@@ -16,7 +20,35 @@ export class UserSetting extends Component {
}
}
handleChange = () => {
const {
user,
updateTwoFactorAuthToken,
openModal,
setTwoFactorAuth,
} = this.props;
if (user.twoFactorAuthEnabled) {
updateTwoFactorAuthToken({
twoFactorAuthEnabled: false,
email: user.email,
}).then(() => {
setTwoFactorAuth(!user.twoFactorAuthEnabled);
});
} else {
openModal({
twoFactorAuthId: user.id,
onClose: () => '',
content: DataPathHoC(TwoFactorAuthModal, {}),
});
}
};
render() {
let { twoFactorAuthEnabled } = this.props.user;
if (twoFactorAuthEnabled === undefined) {
twoFactorAuthEnabled = false;
}
return (
<div className="bs-ContentSection Card-root Card-shadow--medium">
<div className="Box-root">
@@ -121,6 +153,34 @@ export class UserSetting extends Component {
</span>
</div>
</div>
<div className="bs-Fieldset-row">
<label className="bs-Fieldset-label">
Two Factor Authentication <br />{' '}
by Google Authenticator
</label>
<div className="bs-Fieldset-fields">
<label
className="Toggler-wrap"
style={{
marginTop: '10px',
}}
>
<input
className="btn-toggler"
type="checkbox"
onChange={
this.handleChange
}
name="twoFactorAuthEnabled"
id="twoFactorAuthEnabled"
checked={
twoFactorAuthEnabled
}
/>
<span className="TogglerBtn-slider round"></span>
</label>
</div>
</div>
</div>
</fieldset>
</div>
@@ -156,7 +216,10 @@ export class UserSetting extends Component {
UserSetting.displayName = 'UserSetting';
const mapDispatchToProps = dispatch => {
return bindActionCreators({}, dispatch);
return bindActionCreators(
{ updateTwoFactorAuthToken, openModal, setTwoFactorAuth },
dispatch
);
};
function mapStateToProps(state) {
@@ -169,6 +232,9 @@ function mapStateToProps(state) {
UserSetting.propTypes = {
user: PropTypes.object.isRequired,
updateTwoFactorAuthToken: PropTypes.func,
openModal: PropTypes.func,
setTwoFactorAuth: PropTypes.func,
};
UserSetting.contextTypes = {

View File

@@ -61,3 +61,14 @@ export const SEARCH_USERS_REQUEST = 'SEARCH_USERS_REQUEST';
export const SEARCH_USERS_RESET = 'SEARCH_USERS_RESET';
export const SEARCH_USERS_SUCCESS = 'SEARCH_USERS_SUCCESS';
export const SEARCH_USERS_FAILURE = 'SEARCH_USERS_FAILURE';
//update user's two factor auth settings
export const UPDATE_TWO_FACTOR_AUTH_REQUEST = 'UPDATE_TWO_FACTOR_AUTH_REQUEST';
export const UPDATE_TWO_FACTOR_AUTH_SUCCESS = 'UPDATE_TWO_FACTOR_AUTH_SUCCESS';
export const UPDATE_TWO_FACTOR_AUTH_FAILURE = 'UPDATE_TWO_FACTOR_AUTH_FAILURE';
export const SET_TWO_FACTOR_AUTH = 'SET_TWO_FACTOR_AUTH';
// Generate QR code
export const GENERATE_TWO_FACTOR_QR_REQUEST = 'GENERATE_TWO_FACTOR_QR_REQUEST';
export const GENERATE_TWO_FACTOR_QR_SUCCESS = 'GENERATE_TWO_FACTOR_QR_SUCCESS';
export const GENERATE_TWO_FACTOR_QR_FAILURE = 'GENERATE_TWO_FACTOR_QR_FAILURE';

View File

@@ -39,6 +39,13 @@ import {
SEARCH_USERS_RESET,
SEARCH_USERS_SUCCESS,
SEARCH_USERS_FAILURE,
UPDATE_TWO_FACTOR_AUTH_REQUEST,
UPDATE_TWO_FACTOR_AUTH_SUCCESS,
UPDATE_TWO_FACTOR_AUTH_FAILURE,
SET_TWO_FACTOR_AUTH,
GENERATE_TWO_FACTOR_QR_REQUEST,
GENERATE_TWO_FACTOR_QR_SUCCESS,
GENERATE_TWO_FACTOR_QR_FAILURE,
} from '../constants/user';
const INITIAL_STATE = {
@@ -98,6 +105,18 @@ const INITIAL_STATE = {
error: null,
success: false,
},
twoFactorAuthSetting: {
error: null,
requesting: false,
success: false,
data: {},
},
qrCode: {
error: null,
requesting: false,
success: false,
data: {},
},
};
export default function user(state = INITIAL_STATE, action) {
@@ -524,6 +543,80 @@ export default function user(state = INITIAL_STATE, action) {
...INITIAL_STATE,
});
//update user's two factor auth settings
case UPDATE_TWO_FACTOR_AUTH_REQUEST:
return Object.assign({}, state, {
twoFactorAuthSetting: {
requesting: true,
error: null,
success: false,
data: state.twoFactorAuthSetting.data,
},
});
case UPDATE_TWO_FACTOR_AUTH_SUCCESS:
return Object.assign({}, state, {
user: {
...INITIAL_STATE.user,
user: action.payload,
},
twoFactorAuthSetting: {
requesting: false,
error: null,
success: false,
data: state.twoFactorAuthSetting.data,
},
});
case UPDATE_TWO_FACTOR_AUTH_FAILURE:
return Object.assign({}, state, {
twoFactorAuthSetting: {
requesting: false,
error: action.payload,
success: false,
data: state.twoFactorAuthSetting.data,
},
});
case SET_TWO_FACTOR_AUTH:
return Object.assign({}, state, {
user: {
...state.user,
twoFactorAuthEnabled: action.payload,
},
});
//generate user's QR code
case GENERATE_TWO_FACTOR_QR_REQUEST:
return Object.assign({}, state, {
qrCode: {
requesting: true,
error: null,
success: false,
data: state.qrCode.data,
},
});
case GENERATE_TWO_FACTOR_QR_SUCCESS:
return Object.assign({}, state, {
qrCode: {
requesting: false,
error: null,
success: false,
data: action.payload,
},
});
case GENERATE_TWO_FACTOR_QR_FAILURE:
return Object.assign({}, state, {
qrCode: {
requesting: false,
error: action.payload,
success: false,
data: state.qrCode.data,
},
});
default:
return state;
}

View File

@@ -91,4 +91,82 @@ describe('Users Component (IS_SAAS_SERVICE=false)', () => {
},
operationTimeOut
);
test(
'Should not activate google authenticator if the verification code field is empty',
async () => {
return await cluster.execute(null, async ({ page }) => {
// visit the dashboard
await page.goto(utils.ADMIN_DASHBOARD_URL, {
waitUntil: 'networkidle0',
});
await page.waitForSelector(
'.bs-ObjectList-rows > a:nth-child(2)'
);
await page.click('.bs-ObjectList-rows > a:nth-child(2)');
await page.waitFor(5000);
// toggle the google authenticator
await page.$eval('input[name=twoFactorAuthEnabled]', e =>
e.click()
);
// click on the next button
await page.waitForSelector('#nextFormButton');
await page.click('#nextFormButton');
// click the verification button
await page.waitForSelector('#enableTwoFactorAuthButton');
await page.click('#enableTwoFactorAuthButton');
// verify there is an error message
let spanElement = await page.waitForSelector('.field-error');
spanElement = await spanElement.getProperty('innerText');
spanElement = await spanElement.jsonValue();
spanElement.should.be.exactly('Auth token is required.');
});
},
operationTimeOut
);
test(
'Should not activate google authenticator if the verification code is invalid',
async () => {
return await cluster.execute(null, async ({ page }) => {
// visit the dashboard
await page.goto(utils.ADMIN_DASHBOARD_URL, {
waitUntil: 'networkidle0',
});
await page.waitForSelector(
'.bs-ObjectList-rows > a:nth-child(2)'
);
await page.click('.bs-ObjectList-rows > a:nth-child(2)');
await page.waitFor(5000);
// toggle the google authenticator
await page.$eval('input[name=twoFactorAuthEnabled]', e =>
e.click()
);
// click on the next button
await page.waitForSelector('#nextFormButton');
await page.click('#nextFormButton');
// enter a random verification code
await page.waitForSelector('input[name=token]');
await page.type('input[name=token]', '021196');
// click the verification button
await page.waitForSelector('#enableTwoFactorAuthButton');
await page.click('#enableTwoFactorAuthButton');
// verify there is an error message
let spanElement = await page.waitForSelector('#modal-message');
spanElement = await spanElement.getProperty('innerText');
spanElement = await spanElement.jsonValue();
spanElement.should.be.exactly('Invalid token.');
});
},
operationTimeOut
);
});

View File

@@ -11,4 +11,4 @@ REDIS_HOST=localhost
CLUSTER_KEY=f414c23b4cdf4e84a6a66ecfd528eff2
TEST_TWILIO_NUMBER=+919910568840
# IS_SAAS_SERVICE=true
ENCRYPTION_KEY=01234567890123456789012345678901
# ENCRYPTION_KEY=01234567890123456789012345678901

View File

@@ -30,21 +30,21 @@ router.post(
});
}
if (!data.name.trim()) {
if (!data.name || !data.name.trim()) {
return sendErrorResponse(req, res, {
code: 400,
message: 'Application Security Name is required',
});
}
if (!data.gitRepositoryUrl.trim()) {
if (!data.gitRepositoryUrl || !data.gitRepositoryUrl.trim()) {
return sendErrorResponse(req, res, {
code: 400,
message: 'Git Repository URL is required',
});
}
if (!data.gitCredential.trim()) {
if (!data.gitCredential || !data.gitCredential.trim()) {
return sendErrorResponse(req, res, {
code: 400,
message: 'Git Credential is required',

View File

@@ -67,39 +67,72 @@ router.put(
const { projectId, statusPageId } = req.params;
const subDomain = req.body.domain;
if (typeof subDomain !== 'string') {
return sendErrorResponse(req, res, {
code: 400,
message: 'Domain is not of type string.',
});
}
if (Array.isArray(subDomain)) {
if (subDomain.length === 0) {
return sendErrorResponse(req, res, {
code: 400,
message: 'Domain is required.',
});
}
} else {
if (typeof subDomain !== 'string') {
return sendErrorResponse(req, res, {
code: 400,
message: 'Domain is not of type string.',
});
}
if (!UtilService.isDomainValid(subDomain)) {
return sendErrorResponse(req, res, {
code: 400,
message: 'Domain is not valid.',
});
if (!UtilService.isDomainValid(subDomain)) {
return sendErrorResponse(req, res, {
code: 400,
message: 'Domain is not valid.',
});
}
}
try {
const doesDomainBelongToProject = await DomainVerificationService.doesDomainBelongToProject(
projectId,
subDomain
);
if (Array.isArray(subDomain)) {
const response = [];
for (const domain of subDomain) {
const belongsToProject = await DomainVerificationService.doesDomainBelongToProject(
projectId,
domain.domain
);
if (!belongsToProject) {
response.push(
await StatusPageService.createDomain(
domain.domain,
projectId,
statusPageId
)
);
}
}
return sendItemResponse(
req,
res,
response[response.length - 1]
);
} else {
const doesDomainBelongToProject = await DomainVerificationService.doesDomainBelongToProject(
projectId,
subDomain
);
if (doesDomainBelongToProject) {
return sendErrorResponse(req, res, {
message:
'This domain is already associated with another project',
code: 400,
});
if (doesDomainBelongToProject) {
return sendErrorResponse(req, res, {
message:
'This domain is already associated with another project',
code: 400,
});
}
const resp = await StatusPageService.createDomain(
subDomain,
projectId,
statusPageId
);
return sendItemResponse(req, res, resp);
}
const response = await StatusPageService.createDomain(
subDomain,
projectId,
statusPageId
);
return sendItemResponse(req, res, response);
} catch (error) {
return sendErrorResponse(req, res, error);
}

View File

@@ -554,12 +554,19 @@ router.post('/verify/backupCode', async function(req, res) {
const backupCode = user.backupCodes.filter(
code => code.code === data.code
);
user = await UserService.verifyUserBackupCode(
data.code,
user.twoFactorSecretCode,
backupCode[0].counter
);
if (!user || !user._id) {
if (backupCode.length > 0 && backupCode[0].used) {
return sendErrorResponse(req, res, {
code: 400,
message: 'This backup code was used once, show another code.',
});
}
if (backupCode.length > 0)
user = await UserService.verifyUserBackupCode(
data.code,
user.twoFactorSecretCode,
backupCode[0].counter
);
if (backupCode.length === 0 || !user || !user._id) {
return sendErrorResponse(req, res, {
code: 400,
message: 'Invalid backup code.',

View File

@@ -103,6 +103,15 @@ module.exports = {
if (statusPage) {
// attach the domain id to statuspage collection and update it
const domain = statusPage.domains.find(domain =>
domain.domain === subDomain ? true : false
);
if (domain) {
const error = new Error('Domain already exists');
error.code = 400;
ErrorService.log('statusPageService.createDomain', error);
throw error;
}
statusPage.domains = [
...statusPage.domains,
{
@@ -111,8 +120,8 @@ module.exports = {
createdDomain._id || existingBaseDomain._id,
},
];
const result = await statusPage.save();
return result
.populate('domains.domainVerificationToken')
.execPopulate();

View File

@@ -410,6 +410,14 @@ module.exports = {
const user = await _this.findOneBy({
twoFactorSecretCode: secretKey,
});
const backupCodes = user.backupCodes.map(backupCode => {
if (backupCode.code === code) backupCode.used = true;
return backupCode;
});
await _this.updateOneBy(
{ twoFactorSecretCode: secretKey },
{ backupCodes }
);
return user;
}
return isValid;
@@ -571,7 +579,7 @@ module.exports = {
password,
encryptedPassword
);
if (user.twoFactorAuthEnabled) {
if (res && user.twoFactorAuthEnabled) {
return { message: 'Login with 2FA token', email };
}

View File

@@ -20,32 +20,36 @@
.tooltip .tooltiptext {
visibility: hidden;
width: 70px;
background-color: #555;
color: #fff;
text-align: center;
border-radius: 6px;
z-index: 1;
opacity: 0;
height: 25px;
right: 0;
position: absolute;
line-height: 25px;
display: flex;
align-items: center;
justify-content: space-evenly;
padding: 5px;
width: 90px;
height: 28px;
}
.tooltip .tooltiptext strong {
text-transform: capitalize;
background: #818181;
border-radius: 5px;
width: 19px;
height: 18px;
line-height: 19px;
font-size: 13px;
}
.tooltip .tooltiptext span {
font-size: 12px;
}
.tooltip .tooltiptext::after {
/* .tooltip .tooltiptext::after {
content: " ";
position: absolute;
bottom: 30%;
@@ -53,7 +57,7 @@
border-width: 5px;
border-style: solid;
border-color: transparent #555 transparent transparent;
}
} */
.tooltip:hover .tooltiptext {
animation: fadeIn 0.2s;

View File

@@ -11164,6 +11164,9 @@ table {
.custom-tab-5 {
width: 20%;
}
.custom-tab-6 {
width: 16.67%;
}
.custom-tab-selected {
color: #000;
}
@@ -11197,3 +11200,6 @@ table {
grid-template-columns: auto;
}
}
.MuiInputBase-input {
cursor: pointer;
}

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 492.004 492.004" style="enable-background:new 0 0 492.004 492.004;" xml:space="preserve">
<g>
<g>
<path d="M484.14,226.886L306.46,49.202c-5.072-5.072-11.832-7.856-19.04-7.856c-7.216,0-13.972,2.788-19.044,7.856l-16.132,16.136
c-5.068,5.064-7.86,11.828-7.86,19.04c0,7.208,2.792,14.2,7.86,19.264L355.9,207.526H26.58C11.732,207.526,0,219.15,0,234.002
v22.812c0,14.852,11.732,27.648,26.58,27.648h330.496L252.248,388.926c-5.068,5.072-7.86,11.652-7.86,18.864
c0,7.204,2.792,13.88,7.86,18.948l16.132,16.084c5.072,5.072,11.828,7.836,19.044,7.836c7.208,0,13.968-2.8,19.04-7.872
l177.68-177.68c5.084-5.088,7.88-11.88,7.86-19.1C492.02,238.762,489.228,231.966,484.14,226.886z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -114,13 +114,7 @@ class ApplicationLogDetail extends Component {
fetchLogs,
} = this.props;
const { filter, logType } = this.state;
let endDate = '';
let i = 0;
while (i < 29) {
endDate += val[i];
i += 1;
}
endDate = moment(endDate);
const endDate = moment(val);
if (moment(startDate).isBefore(endDate)) {
fetchLogs(
currentProject._id,
@@ -370,7 +364,7 @@ ApplicationLogDetail.propTypes = {
stats: PropTypes.object,
fetchLogs: PropTypes.func,
startDate: PropTypes.string,
endDate: PropTypes.string,
endDate: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
};
export default connect(
mapStateToProps,

View File

@@ -66,7 +66,7 @@ class ApplicationLogHeader extends Component {
<div>
<button
id={`filter_${applicationLog.name}`}
className="bs-Button bs-DeprecatedButton db-Trends-editButton bs-Button--icon bs-Button--settings"
className="bs-Button bs-DeprecatedButton db-Trends-editButton bs-Button--icon bs-Button--filter"
type="button"
onClick={() =>
this.setState(
@@ -76,7 +76,11 @@ class ApplicationLogHeader extends Component {
)
}
>
<span>Filter Logs</span>
<span>
{this.state.showFilters
? 'Hide Filters'
: 'Filter Logs'}
</span>
</button>
<button
id={`key_${applicationLog.name}`}
@@ -185,12 +189,7 @@ class ApplicationLogHeader extends Component {
cursor: 'pointer',
}}
>
<span
className="db-DateRangeInput-start"
style={{
padding: '3px',
}}
>
<span className="db-DateRangeInput-start">
<Field
type="text"
name="startDate"
@@ -198,28 +197,20 @@ class ApplicationLogHeader extends Component {
CustomDateTimeSelector
}
id="startDate"
style={{
marginTop:
'0px',
width: '180px',
}}
maxDate={
currentDate
}
/>
</span>
<span
className="db-DateRangeInput-input-arrow"
<img
alt="next"
src="/dashboard/assets/icons/next.svg"
style={{
padding: '3px',
height: '14px',
width: '14px',
}}
/>
<span
className="db-DateRangeInput-end"
style={{
padding: '3px',
}}
>
<span className="db-DateRangeInput-end">
<Field
type="text"
name="endDate"
@@ -227,11 +218,6 @@ class ApplicationLogHeader extends Component {
CustomDateTimeSelector
}
id="endDate"
style={{
marginTop:
'0px',
width: '180px',
}}
maxDate={
currentDate
}

View File

@@ -356,6 +356,7 @@ class LogList extends Component {
src={
'/dashboard/assets/img/more.svg'
}
alt="more-logs"
/>
</button>
</div>

View File

@@ -2,8 +2,9 @@ import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { createMuiTheme, MuiThemeProvider } from '@material-ui/core/styles';
import { DateTimePicker, MuiPickersUtilsProvider } from '@material-ui/pickers';
import DateFnsUtils from '@date-io/date-fns';
import MomentUtils from '@date-io/moment';
import { withStyles } from '@material-ui/core/styles';
import * as moment from 'moment';
const theme = createMuiTheme({
palette: {
@@ -19,7 +20,7 @@ const theme = createMuiTheme({
const styles = () => ({
input: {
flex: '0 0 auto',
padding: '4px 7px 2px',
padding: '4px 0px 2px 7px',
color: '#000000',
cursor: 'text',
fontSize: '14px',
@@ -36,6 +37,7 @@ const styles = () => ({
userSelect: 'auto',
'font-family': 'unset',
height: '32px',
fontWeight: '500',
},
});
@@ -56,7 +58,7 @@ const CustomDateTimeSelector = ({
const handleChange = option => {
setValue(option);
if (input.onChange) {
input.onChange(new Date(option).toUTCString());
input.onChange(moment(option));
}
};
@@ -64,20 +66,20 @@ const CustomDateTimeSelector = ({
<span>
<div
style={{
//width: '10px',
height: '28px',
marginTop:
style && style.marginTop ? style.marginTop : '-15px',
// zIndex: 1000,
style && style.marginTop ? style.marginTop : '-32px',
fontWeight: '500',
}}
>
<MuiThemeProvider theme={theme}>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<MuiPickersUtilsProvider utils={MomentUtils}>
<DateTimePicker
name={input.name}
margin="normal"
id={id ? id + 'time-picker' : 'time-picker'}
value={value}
format={'lll'}
error={false}
invalidDateMessage={false}
variant="modal"

View File

@@ -807,7 +807,10 @@ export class IncidentList extends Component {
<div className="Box-root Flex-flex Flex-alignItems--center Padding-all--20">
<span className="Text-color--inherit Text-display--inline Text-fontSize--14 Text-fontWeight--regular Text-lineHeight--20 Text-typeface--base Text-wrap--wrap">
<span>
<span className="Text-color--inherit Text-display--inline Text-fontSize--14 Text-fontWeight--medium Text-lineHeight--20 Text-typeface--base Text-wrap--wrap">
<span
id={`incident_count`}
className="Text-color--inherit Text-display--inline Text-fontSize--14 Text-fontWeight--medium Text-lineHeight--20 Text-typeface--base Text-wrap--wrap"
>
{incidents
? incidents.length +
(incidents.length > 1

View File

@@ -59,9 +59,9 @@ class BackupCodesModal extends React.Component {
}}
>
<span className="Text-color--inherit Text-display--inline Text-fontSize--20 Text-fontWeight--medium Text-lineHeight--24 Text-typeface--base Text-wrap--wrap">
<span>
<div>
Two Factor Authentication Backup Codes
</span>
</div>
</span>
</div>
</div>
@@ -69,6 +69,10 @@ class BackupCodesModal extends React.Component {
<div className="bs-Modal-block">
<div>
<div className="bs-Fieldset-wrapper">
<div className="Text-color--inherit Text-display--inline Text-fontSize--16 Text-fontWeight--normal Text-lineHeight--24 Text-typeface--base Text-wrap--wrap Padding-bottom--48">
You have to print these codes or
keep them in the safe place.
</div>
<fieldset style={{ marginTop: -10 }}>
<div className="bs-Fieldset-rows">
<div
@@ -79,6 +83,7 @@ class BackupCodesModal extends React.Component {
style={{
overflow: 'hidden',
overflowX: 'auto',
width: '100%',
}}
>
<table className="Table">
@@ -103,7 +108,7 @@ class BackupCodesModal extends React.Component {
}}
>
<div className="db-ListViewItem-cellContent Box-root Padding-all--8">
<span className="db-ListViewItem-text Text-display--inline Text-fontSize--14 Text-fontWeight--medium Text-lineHeight--20 Text-typeface--base Text-wrap--wrap">
<span className="db-ListViewItem-text Text-display--inline Text-fontSize--14 Text-fontWeight--medium Text-lineHeight--20 Text-typeface--base Text-wrap--wrap Text-align--center">
<div className="Margin-right--7">
<span>
{

View File

@@ -251,7 +251,7 @@ class CreateIncident extends Component {
RenderSelect
}
name="incidentType"
id="incidentType"
id="incidentTypeId"
placeholder="Incident type"
disabled={
this.props

View File

@@ -87,6 +87,7 @@ export class MonitorViewDeleteBox extends Component {
<button
className="bs-Button bs-Button--red Box-background--red"
disabled={deleting}
id={`delete_${this.props.monitor.name}`}
onClick={() =>
this.props.openModal({
id: deleteModalId,

View File

@@ -108,6 +108,34 @@ export class OnCallAlertBox extends Component {
}
};
renderAddEscalationPolicyButton = () => (
<button
type="button"
className="bs-Button bs-FileUploadButton bs-Button--icon bs-Button--new"
onClick={() =>
this.props.pushArray('OnCallAlertBox', 'OnCallAlertBox', {
callReminders: 3,
smsReminders: 3,
emailReminders: 3,
email: true,
sms: false,
call: false,
rotateBy: '',
rotationInterval: '',
firstRotationOn: '',
rotationTimezone: '',
teams: [
{
teamMembers: [],
},
],
})
}
>
Add Escalation Policy
</button>
);
render() {
const { handleSubmit } = this.props;
@@ -132,35 +160,7 @@ export class OnCallAlertBox extends Component {
</div>
<div className="ContentHeader-end Box-root Flex-flex Flex-alignItems--center Margin-left--16">
<div className="Box-root">
<button
type="button"
className="bs-Button bs-FileUploadButton bs-Button--icon bs-Button--new"
onClick={() =>
this.props.pushArray(
'OnCallAlertBox',
'OnCallAlertBox',
{
callReminders: 3,
smsReminders: 3,
emailReminders: 3,
email: true,
sms: false,
call: false,
rotateBy: '',
rotationInterval: '',
firstRotationOn: '',
rotationTimezone: '',
teams: [
{
teamMembers: [],
},
],
}
)
}
>
Add Escalation Policy
</button>
{this.renderAddEscalationPolicyButton()}
</div>
</div>
</div>
@@ -215,6 +215,7 @@ export class OnCallAlertBox extends Component {
</div>
<div>
{this.renderAddEscalationPolicyButton()}
<button
className="bs-Button bs-DeprecatedButton bs-Button--blue"
disabled={

View File

@@ -93,6 +93,36 @@ export class Monitors extends Component {
}
};
renderAddMonitorButton = subProject => (
<ShouldRender
if={
this.props.monitors.length > 0 &&
(IsAdminSubProject(subProject) || IsOwnerSubProject(subProject))
}
>
<button
id="addMoreMonitors"
className="bs-Button bs-Button--icon bs-Button--new"
type="button"
onClick={() =>
this.props.pushArray('StatuspageMonitors', 'monitors', {
monitor: null,
description: '',
uptime: true,
memory: false,
cpu: false,
storage: false,
responseTime: false,
temperature: false,
runtime: false,
})
}
>
<span>Add Monitor</span>
</button>
</ShouldRender>
);
render() {
const { handleSubmit, subProjects } = this.props;
const { status } = this.props.statusPage;
@@ -124,49 +154,11 @@ export class Monitors extends Component {
</span>
</span>
</div>
<ShouldRender
if={
this.props.monitors.length > 0 &&
(IsAdminSubProject(subProject) ||
IsOwnerSubProject(subProject))
}
>
<div className="ContentHeader-end Box-root Flex-flex Flex-alignItems--center Margin-left--16">
<div className="Box-root">
<button
id="addMoreMonitors"
className="Button bs-ButtonLegacy ActionIconParent"
type="button"
onClick={() =>
this.props.pushArray(
'StatuspageMonitors',
'monitors',
{
monitor: null,
description: '',
uptime: true,
memory: false,
cpu: false,
storage: false,
responseTime: false,
temperature: false,
runtime: false,
}
)
}
>
<div className="bs-ButtonLegacy-fill Box-root Box-background--white Flex-inlineFlex Flex-alignItems--center Flex-direction--row Padding-horizontal--8 Padding-vertical--4">
<div className="Box-root Margin-right--8">
<div className="SVGInline SVGInline--cleaned Button-icon ActionIcon ActionIcon--color--inherit Box-root Flex-flex"></div>
</div>
<span className="bs-Button bs-FileUploadButton bs-Button--icon bs-Button--new">
<span>Add Monitor</span>
</span>
</div>
</button>
</div>
<div className="ContentHeader-end Box-root Flex-flex Flex-alignItems--center Margin-left--16">
<div className="Box-root">
{this.renderAddMonitorButton(subProject)}
</div>
</ShouldRender>
</div>
</div>
</div>
<form onSubmit={handleSubmit(this.submitForm)}>
@@ -273,6 +265,7 @@ export class Monitors extends Component {
</div>
</div>
<div>
{this.renderAddMonitorButton(subProject)}
<ShouldRender
if={
this.props.monitors.length > 0 &&

View File

@@ -1,7 +1,7 @@
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { reduxForm, Field } from 'redux-form';
import { reduxForm, Field, SubmissionError } from 'redux-form';
import uuid from 'uuid';
import {
updateStatusPageSetting,
@@ -30,21 +30,16 @@ import {
createDomain,
deleteDomain,
updateDomain,
createDomainFailure,
} from '../../actions/domain';
import { openModal, closeModal } from '../../actions/modal';
import VerifyDomainModal from './VerifyDomainModal';
import DeleteDomainModal from './DeleteDomainModal';
//Client side validation
function validate(value, allValues, props, name) {
let error = undefined;
if (name === 'domain' && !value) {
error = 'Domain is required';
}
if (name === 'domain' && value && !Validate.isDomain(value)) {
error = 'Domain is not valid.';
}
// eslint-disable-next-line no-unused-vars
function validate(_values) {
const error = undefined;
return error;
}
@@ -52,11 +47,31 @@ export class Setting extends Component {
state = {
verifyModalId: uuid.v4(),
deleteDomainModalId: uuid.v4(),
fields: [],
};
submitForm = values => {
if ('domain' in values) {
return this.handleCreateDomain({ domain: values.domain });
const { fields } = this.state;
const domains = [];
for (const [key, value] of Object.entries(values)) {
if (key.includes('domain') && !Validate.isDomain(value)) {
throw new SubmissionError({ [key]: 'Domain is not valid.' });
}
if (key.includes('domain') && Validate.isDomain(value)) {
domains.push({ domain: value });
}
}
if (fields.length > 0) {
fields.forEach((_field, index) => {
if (!Object.keys(values).includes(`domain_${index + 1}`)) {
throw new SubmissionError({
[`domain_${index + 1}`]: 'Domain is required.',
});
}
});
}
if (fields.length > 0 && domains.length > 0) {
return this.handleCreateDomain(domains);
}
const isChanged =
@@ -78,18 +93,18 @@ export class Setting extends Component {
handleCreateDomain = values => {
const { reset } = this.props;
const { domain } = values;
const { _id, projectId } = this.props.statusPage.status;
if (!domain) return;
if (values.length === 0) return;
const data = {
domain,
domain: values,
projectId: projectId._id || projectId,
statusPageId: _id,
};
this.props.createDomain(data).then(
() => {
this.setState({ fields: [] });
reset();
},
function() {}
@@ -208,17 +223,150 @@ export class Setting extends Component {
}
};
renderAddDomainBButton = () => (
renderNewDomainField = (publicStatusPageUrl, index) => (
<div className="bs-Fieldset-row Margin-bottom--20" key={index}>
<label className="bs-Fieldset-label">
{' '}
Your Status Page is hosted at{' '}
</label>
<div className="bs-Fieldset-fields">
<Field
className="db-BusinessSettings-input TextInput bs-TextInput"
component={RenderField}
type="text"
name={`domain_${index}`}
id={`domain_${index}`}
disabled={this.props.statusPage.setting.requesting}
placeholder="domain"
/>
<ShouldRender
if={
!this.props.addDomain.requesting &&
this.props.addDomain.error
}
>
<div id="verifyDomainError" className="bs-Tail-copy">
<div
className="Box-root Flex-flex Flex-alignItems--stretch Flex-direction--row Flex-justifyContent--flexStart"
style={{
marginTop: '10px',
}}
>
<div className="Box-root Margin-right--8">
<div
className="Icon Icon--info Icon--color--red Icon--size--14 Box-root Flex-flex"
style={{
marginTop: '2px',
}}
></div>
</div>
<div className="Box-root">
<span
style={{
color: 'red',
}}
>
{this.props.addDomain.error}
</span>
</div>
</div>
</div>
</ShouldRender>
<p className="bs-Fieldset-explanation" id="publicStatusPageUrl">
{IS_LOCALHOST && (
<span>
If you want to preview your status page. Please
check{' '}
<a
target="_blank"
rel="noopener noreferrer"
href={publicStatusPageUrl}
>
{publicStatusPageUrl}{' '}
</a>
</span>
)}
{IS_SAAS_SERVICE && !IS_LOCALHOST && (
<span>
Add statuspage.fyipeapp.com to your CNAME. If you
want to preview your status page. Please check{' '}
<a
target="_blank"
rel="noopener noreferrer"
href={publicStatusPageUrl}
>
{publicStatusPageUrl}{' '}
</a>
</span>
)}
{!IS_SAAS_SERVICE && !IS_LOCALHOST && (
<span>
If you want to preview your status page. Please
check{' '}
<a
target="_blank"
rel="noopener noreferrer"
href={publicStatusPageUrl}
>
{publicStatusPageUrl}{' '}
</a>
</span>
)}
</p>
<div
className="bs-Fieldset-row"
style={{
padding: '5px 0 0 0',
}}
>
<button
className="btnDeleteDomain bs-Button"
onClick={event => this.removeInputField(event)}
>
<span className="bs-Button--icon bs-Button--delete"></span>
<span>Delete Domain</span>
</button>
</div>
</div>
</div>
);
renderAddDomainButton = publicStatusPageUrl => (
<button
id="addMoreDomain"
className="bs-Button bs-Button--icon bs-Button--new"
type="button"
onClick={this.props.addMoreDomain}
onClick={() => {
this.props.createDomainFailure('');
this.props.addMoreDomain();
this.setState(prevState => {
return {
fields: [
...prevState.fields,
this.renderNewDomainField(
publicStatusPageUrl,
prevState.fields.length + 1
),
],
};
});
}}
>
<span>Add Domain</span>
</button>
);
removeInputField = event => {
event.preventDefault();
this.setState(prevState => {
prevState.fields.pop();
return {
fields: [...prevState.fields],
};
});
};
render() {
let statusPageId = '';
let hosted = '';
@@ -279,7 +427,9 @@ export class Setting extends Component {
</div>
<div className="ContentHeader-end Box-root Flex-flex Flex-alignItems--center Margin-left--16">
<div className="Box-root">
{this.renderAddDomainBButton()}
{this.renderAddDomainButton(
publicStatusPageUrl
)}
</div>
</div>
</div>
@@ -435,6 +585,8 @@ export class Setting extends Component {
'center',
paddingLeft: 0,
paddingBottom: 0,
paddingTop:
'5px',
}}
>
<ShouldRender
@@ -449,8 +601,7 @@ export class Setting extends Component {
<div
className="bs-Fieldset-row"
style={{
paddingLeft: 0,
paddingRight: 0,
padding: 0,
marginRight:
'15px',
}}
@@ -487,9 +638,7 @@ export class Setting extends Component {
<div
className="bs-Fieldset-row"
style={{
paddingLeft: 0,
paddingRight: 0,
paddingBottom: 0,
padding: 0,
}}
>
<button
@@ -585,10 +734,10 @@ export class Setting extends Component {
if={
this.props.domains &&
this.props.domains.length === 0 &&
!this.props.showDomainField
this.state.fields.length === 0
}
>
<div className="bs-Fieldset-wrapper Box-root Margin-bottom--2 Padding-all--16 Text-align--center">
<div className="bs-Fieldset-wrapper Box-root Margin-bottom--2 Padding-all--16 Text-align--center Padding-top--20">
<span>No domains added</span>
</div>
</ShouldRender>
@@ -601,168 +750,7 @@ export class Setting extends Component {
IsOwnerSubProject(
subProject
) ? (
<div className="bs-Fieldset-row">
<label className="bs-Fieldset-label">
{' '}
Your Status Page is
hosted at{' '}
</label>
<div className="bs-Fieldset-fields">
<Field
className="db-BusinessSettings-input TextInput bs-TextInput"
component={
RenderField
}
type="text"
name="domain"
id="domain"
disabled={
this.props
.statusPage
.setting
.requesting
}
placeholder="domain"
validate={
validate
}
/>
<ShouldRender
if={
!this.props
.addDomain
.requesting &&
this.props
.addDomain
.error
}
>
<div
id="verifyDomainError"
className="bs-Tail-copy"
>
<div
className="Box-root Flex-flex Flex-alignItems--stretch Flex-direction--row Flex-justifyContent--flexStart"
style={{
marginTop:
'10px',
}}
>
<div className="Box-root Margin-right--8">
<div
className="Icon Icon--info Icon--color--red Icon--size--14 Box-root Flex-flex"
style={{
marginTop:
'2px',
}}
></div>
</div>
<div className="Box-root">
<span
style={{
color:
'red',
}}
>
{
this
.props
.addDomain
.error
}
</span>
</div>
</div>
</div>
</ShouldRender>
<p
className="bs-Fieldset-explanation"
id="publicStatusPageUrl"
>
{IS_LOCALHOST && (
<span>
If you
want to
preview
your
status
page.
Please
check{' '}
<a
target="_blank"
rel="noopener noreferrer"
href={
publicStatusPageUrl
}
>
{
publicStatusPageUrl
}{' '}
</a>
</span>
)}
{IS_SAAS_SERVICE &&
!IS_LOCALHOST && (
<span>
Add
statuspage.fyipeapp.com
to
your
CNAME.
If
you
want
to
preview
your
status
page.
Please
check{' '}
<a
target="_blank"
rel="noopener noreferrer"
href={
publicStatusPageUrl
}
>
{
publicStatusPageUrl
}{' '}
</a>
</span>
)}
{!IS_SAAS_SERVICE &&
!IS_LOCALHOST && (
<span>
If
you
want
to
preview
your
status
page.
Please
check{' '}
<a
target="_blank"
rel="noopener noreferrer"
href={
publicStatusPageUrl
}
>
{
publicStatusPageUrl
}{' '}
</a>
</span>
)}
</p>
</div>
</div>
this.state.fields
) : (
<div className="bs-Fieldset-row">
<label className="bs-Fieldset-label">
@@ -799,8 +787,7 @@ export class Setting extends Component {
<div className="Box-root">
<ShouldRender
if={
this.props.statusPage.setting
.error ||
this.props.addDomain.error ||
this.props.updateDomainError
}
>
@@ -810,17 +797,13 @@ export class Setting extends Component {
display: 'block',
}}
>
{this.props.statusPage.setting
.error ||
{this.props.addDomain.error ||
this.props
.updateDomainError}
</span>
</ShouldRender>
<ShouldRender
if={
!this.props.statusPage.setting
.error
}
if={!this.props.addDomain.error}
>
<span>
Changes to these settings will
@@ -835,7 +818,9 @@ export class Setting extends Component {
<RenderIfSubProjectAdmin
subProjectId={projectId}
>
{this.renderAddDomainBButton()}
{this.renderAddDomainButton(
publicStatusPageUrl
)}
<ShouldRender
if={
this.props.showDomainField ||
@@ -852,7 +837,7 @@ export class Setting extends Component {
}
type="submit"
>
{(!this.props.statusPage.setting
{(!this.props.addDomain
.requesting ||
!this.props
.updateDomainRequesting) && (
@@ -860,8 +845,7 @@ export class Setting extends Component {
Save Domain Settings{' '}
</span>
)}
{(this.props.statusPage.setting
.requesting ||
{(this.props.addDomain.requesting ||
this.props
.updateDomainRequesting) && (
<FormLoader />
@@ -910,6 +894,7 @@ Setting.propTypes = {
]),
updateDomainRequesting: PropTypes.bool,
initialFormValues: PropTypes.object,
createDomainFailure: PropTypes.func,
};
const SettingForm = reduxForm({
@@ -933,6 +918,7 @@ const mapDispatchToProps = dispatch => {
deleteDomain,
openModal,
closeModal,
createDomainFailure,
},
dispatch
);

View File

@@ -171,204 +171,205 @@ export class SubscriberList extends Component {
{subscribers &&
subscribers.subscribers &&
subscribers.subscribers.length > 0 ? (
subscribers.subscribers.map(subscriber => (
<tr
className="subscriber-list-item Table-row db-ListViewItem bs-ActionsParent db-ListViewItem--hasLink"
key={subscriber._id}
>
<td
className="Table-cell Table-cell--align--left Table-cell--verticalAlign--top Table-cell--width--minimized Table-cell--wrap--wrap db-ListViewItem-cell db-ListViewItem-cell--breakWord"
style={{
height: '1px',
minWidth: '270px',
}}
subscribers.subscribers.map(
(subscriber, index) => (
<tr
className="subscriber-list-item Table-row db-ListViewItem bs-ActionsParent db-ListViewItem--hasLink"
key={subscriber._id}
>
<div className="db-ListViewItem-cellContent Box-root Padding-all--8">
<span className="db-ListViewItem-text Text-color--cyan Text-display--inline Text-fontSize--14 Text-fontWeight--medium Text-lineHeight--20 Text-typeface--base Text-wrap--wrap">
<div className="Box-root Margin-right--16">
<span>
{subscribers.name
? subscribers.name
: subscriber.monitorId &&
subscriber.monitorName
? subscriber.monitorName
: 'Unknown Monitor'}
</span>
</div>
</span>
</div>
</td>
<td
className="Table-cell Table-cell--align--right Table-cell--verticalAlign--top Table-cell--width--minimized Table-cell--wrap--noWrap db-ListViewItem-cell"
style={{ height: '1px' }}
>
<div className="db-ListViewItem-link">
<td
className="Table-cell Table-cell--align--left Table-cell--verticalAlign--top Table-cell--width--minimized Table-cell--wrap--wrap db-ListViewItem-cell db-ListViewItem-cell--breakWord"
style={{
height: '1px',
minWidth: '270px',
}}
>
<div className="db-ListViewItem-cellContent Box-root Padding-all--8">
<span className="db-ListViewItem-text Text-color--inherit Text-display--inline Text-fontSize--14 Text-fontWeight--regular Text-lineHeight--20 Text-typeface--base Text-wrap--wrap">
<div className="Box-root">
<span className="db-ListViewItem-text Text-color--cyan Text-display--inline Text-fontSize--14 Text-fontWeight--medium Text-lineHeight--20 Text-typeface--base Text-wrap--wrap">
<div className="Box-root Margin-right--16">
<span>
{(subscriber.statusPageId !==
undefined &&
subscriber.statusPageId !==
null &&
subscriber.statusPageName) ||
'Fyipe Dashboard'}
{subscribers.name
? subscribers.name
: subscriber.monitorId &&
subscriber.monitorName
? subscriber.monitorName
: 'Unknown Monitor'}
</span>
</div>
</span>
</div>
</div>
</td>
<td
aria-hidden="true"
className="Table-cell Table-cell--align--left Table-cell--verticalAlign--top Table-cell--wrap--noWrap db-ListViewItem-cell"
style={{
height: '1px',
maxWidth: '48px',
minWidth: '48px',
width: '48px',
}}
>
<div className="db-ListViewItem-link">
<div className="db-ListViewItem-cellContent Box-root Padding-all--8">
</div>
</div>
</td>
<td
className="Table-cell Table-cell--align--left Table-cell--verticalAlign--top Table-cell--width--minimized Table-cell--wrap--noWrap db-ListViewItem-cell"
style={{ height: '1px' }}
>
<div className="db-ListViewItem-link">
<div className="db-ListViewItem-cellContent Box-root Padding-all--8">
<span className="db-ListViewItem-text Text-color--inherit Text-display--inline Text-fontSize--14 Text-fontWeight--regular Text-lineHeight--20 Text-typeface--base Text-wrap--wrap">
<div className="Box-root Flex-flex">
<div className="Box-root Flex-flex">
<div className="contact db-RadarRulesListUserName Box-root Flex-flex Flex-alignItems--center Flex-direction--row Flex-justifyContent--flexStart">
{subscriber.contactWebhook ||
subscriber.contactEmail ||
(subscriber.contactPhone &&
`+${countryTelephoneCode(
subscriber.countryCode.toUpperCase()
)}${
subscriber.contactPhone
}`) ||
''}
</div>
</td>
<td
className="Table-cell Table-cell--align--right Table-cell--verticalAlign--top Table-cell--width--minimized Table-cell--wrap--noWrap db-ListViewItem-cell"
style={{ height: '1px' }}
>
<div className="db-ListViewItem-link">
<div className="db-ListViewItem-cellContent Box-root Padding-all--8">
<span className="db-ListViewItem-text Text-color--inherit Text-display--inline Text-fontSize--14 Text-fontWeight--regular Text-lineHeight--20 Text-typeface--base Text-wrap--wrap">
<div className="Box-root">
<span>
{(subscriber.statusPageId !==
undefined &&
subscriber.statusPageId !==
null &&
subscriber.statusPageName) ||
'Fyipe Dashboard'}
</span>
</div>
</div>
</span>
</div>
</div>
</td>
<td
aria-hidden="true"
className="Table-cell Table-cell--align--left Table-cell--verticalAlign--top Table-cell--wrap--noWrap db-ListViewItem-cell"
style={{
height: '1px',
maxWidth: '48px',
minWidth: '48px',
width: '48px',
}}
>
<div className="db-ListViewItem-link">
<div className="db-ListViewItem-cellContent Box-root Padding-all--8">
</div>
</div>
</td>
<td
className="Table-cell Table-cell--align--left Table-cell--verticalAlign--top Table-cell--width--minimized Table-cell--wrap--noWrap db-ListViewItem-cell"
style={{ height: '1px' }}
>
<div className="db-ListViewItem-link">
<div className="db-ListViewItem-cellContent Box-root Padding-all--8">
<div className="Badge Badge--color--green Box-background--green Box-root Flex-inlineFlex Flex-alignItems--center Padding-horizontal--8 Padding-vertical--2">
<span className="Badge-text Text-color--white Text-display--inline Text-fontSize--12 Text-fontWeight--bold Text-lineHeight--16 Text-typeface--upper Text-wrap--noWrap">
<span>
{
subscriber.alertVia
}
</span>
</span>
</div>
</div>
</div>
</td>
<td
aria-hidden="true"
className="Table-cell Table-cell--align--left Table-cell--verticalAlign--top Table-cell--wrap--noWrap db-ListViewItem-cell"
style={{
height: '1px',
maxWidth: '48px',
minWidth: '48px',
width: '48px',
}}
>
<div className="db-ListViewItem-link">
<div className="db-ListViewItem-cellContent Box-root Padding-all--8">
</td>
<td
aria-hidden="true"
className="Table-cell Table-cell--align--left Table-cell--verticalAlign--top Table-cell--wrap--noWrap db-ListViewItem-cell"
style={{
height: '1px',
maxWidth: '48px',
minWidth: '48px',
width: '48px',
}}
>
<div className="db-ListViewItem-link">
<div className="db-ListViewItem-cellContent Box-root Padding-all--8">
</div>
</div>
</div>
</td>
<RenderIfSubProjectAdmin
subProjectId={subProjectId}
>
</td>
<td
className="Table-cell Table-cell--align--left Table-cell--verticalAlign--top Table-cell--width--minimized Table-cell--wrap--noWrap db-ListViewItem-cell"
style={{ height: '1px' }}
>
<div className="db-ListViewItem-link">
<div className="db-ListViewItem-cellContent Box-root Padding-all--8">
<div className="Box-root">
<span>
<button
className={
deleting
? 'bs-Button bs-Button--blue'
: 'bs-Button bs-ButtonLegacy ActionIconParent'
<span className="db-ListViewItem-text Text-color--inherit Text-display--inline Text-fontSize--14 Text-fontWeight--regular Text-lineHeight--20 Text-typeface--base Text-wrap--wrap">
<div className="Box-root Flex-flex">
<div className="Box-root Flex-flex">
<div className="contact db-RadarRulesListUserName Box-root Flex-flex Flex-alignItems--center Flex-direction--row Flex-justifyContent--flexStart">
{subscriber.contactWebhook ||
subscriber.contactEmail ||
(subscriber.contactPhone &&
`+${countryTelephoneCode(
subscriber.countryCode.toUpperCase()
)}${
subscriber.contactPhone
}`) ||
''}
</div>
</div>
</div>
</span>
</div>
</div>
</td>
<td
aria-hidden="true"
className="Table-cell Table-cell--align--left Table-cell--verticalAlign--top Table-cell--wrap--noWrap db-ListViewItem-cell"
style={{
height: '1px',
maxWidth: '48px',
minWidth: '48px',
width: '48px',
}}
>
<div className="db-ListViewItem-link">
<div className="db-ListViewItem-cellContent Box-root Padding-all--8">
</div>
</div>
</td>
<td
className="Table-cell Table-cell--align--left Table-cell--verticalAlign--top Table-cell--width--minimized Table-cell--wrap--noWrap db-ListViewItem-cell"
style={{ height: '1px' }}
>
<div className="db-ListViewItem-link">
<div className="db-ListViewItem-cellContent Box-root Padding-all--8">
<div className="Badge Badge--color--green Box-background--green Box-root Flex-inlineFlex Flex-alignItems--center Padding-horizontal--8 Padding-vertical--2">
<span className="Badge-text Text-color--white Text-display--inline Text-fontSize--12 Text-fontWeight--bold Text-lineHeight--16 Text-typeface--upper Text-wrap--noWrap">
<span>
{
subscriber.alertVia
}
type="button"
disabled={
deleting
}
onClick={() =>
this.props.deleteSubscriber(
subscriber
.projectId
._id,
subscriber._id
)
}
>
<ShouldRender
if={
!deleting
}
>
<span className="bs-Button--icon bs-Button--trash">
<span>
Remove
</span>
</span>
</ShouldRender>
<ShouldRender
if={
deleting
}
>
<FormLoader />
</ShouldRender>
</button>
</span>
</span>
</div>
</div>
</div>
</td>
</RenderIfSubProjectAdmin>
</tr>
))
<td
aria-hidden="true"
className="Table-cell Table-cell--align--left Table-cell--verticalAlign--top Table-cell--wrap--noWrap db-ListViewItem-cell"
style={{
height: '1px',
maxWidth: '48px',
minWidth: '48px',
width: '48px',
}}
>
<div className="db-ListViewItem-link">
<div className="db-ListViewItem-cellContent Box-root Padding-all--8">
</div>
</div>
</td>
<RenderIfSubProjectAdmin
subProjectId={subProjectId}
>
<td
className="Table-cell Table-cell--align--left Table-cell--verticalAlign--top Table-cell--width--minimized Table-cell--wrap--noWrap db-ListViewItem-cell"
style={{ height: '1px' }}
>
<div className="db-ListViewItem-link">
<div className="db-ListViewItem-cellContent Box-root Padding-all--8">
<div className="Box-root">
<span>
<button
className={
deleting
? 'bs-Button bs-Button--blue'
: 'bs-Button bs-ButtonLegacy ActionIconParent'
}
type="button"
disabled={
deleting
}
onClick={() =>
this.props.deleteSubscriber(
subscriber.projectId,
subscriber._id
)
}
id={`deleteSubscriber_${index}`}
>
<ShouldRender
if={
!deleting
}
>
<span className="bs-Button--icon bs-Button--trash">
<span>
Remove
</span>
</span>
</ShouldRender>
<ShouldRender
if={
deleting
}
>
<FormLoader />
</ShouldRender>
</button>
</span>
</div>
</div>
</div>
</td>
</RenderIfSubProjectAdmin>
</tr>
)
)
) : (
<tr></tr>
)}

View File

@@ -29,7 +29,7 @@ import { logEvent } from '../analytics';
import { SHOULD_LOG_ANALYTICS } from '../config';
import BreadCrumbItem from '../components/breadCrumb/BreadCrumbItem';
import getParentRoute from '../utils/getParentRoute';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import { Tab, Tabs, TabList, TabPanel, resetIdCounter } from 'react-tabs';
import { fetchBasicIncidentSettings } from '../actions/incidentBasicsSettings';
class Incident extends React.Component {
@@ -37,6 +37,9 @@ class Incident extends React.Component {
super(props);
this.props = props;
}
componentWillMount() {
resetIdCounter();
}
componentDidMount() {
if (SHOULD_LOG_ANALYTICS) {
logEvent('PAGE VIEW: DASHBOARD > PROJECT > INCIDENT');
@@ -249,24 +252,30 @@ class Incident extends React.Component {
id="customTabList"
className={'custom-tab-list'}
>
<Tab className={'custom-tab custom-tab-5'}>
<Tab className={'custom-tab custom-tab-6'}>
Basic
</Tab>
<Tab className={'custom-tab custom-tab-5'}>
Logs
<Tab className={'custom-tab custom-tab-6'}>
Monitor Logs
</Tab>
<Tab className={'custom-tab custom-tab-5'}>
Timeline
<Tab className={'custom-tab custom-tab-6'}>
Alert Logs
</Tab>
<Tab className={'custom-tab custom-tab-5'}>
Notes
<Tab className={'custom-tab custom-tab-6'}>
Incident Timeline
</Tab>
<Tab className={'custom-tab custom-tab-5'}>
<Tab className={'custom-tab custom-tab-6'}>
Incident Notes
</Tab>
<Tab
id="tab-advance"
className={'custom-tab custom-tab-6'}
>
Advanced Options
</Tab>
<div
id="tab-slider"
className="custom-tab-5"
className="custom-tab-6"
></div>
</TabList>
</div>
@@ -281,10 +290,6 @@ class Incident extends React.Component {
</TabPanel>
<TabPanel>
<Fade>
<IncidentAlert
next={this.nextAlerts}
previous={this.previousAlerts}
/>
<div className="Box-root Margin-bottom--12">
<MonitorViewLogsBox
incidentId={this.props.incident._id}
@@ -292,6 +297,15 @@ class Incident extends React.Component {
monitorName={monitorName}
/>
</div>
</Fade>
</TabPanel>
<TabPanel>
<Fade>
<IncidentAlert
next={this.nextAlerts}
previous={this.previousAlerts}
/>
<SubscriberAlert
next={this.nextSubscribers}
previous={this.previousSubscribers}
@@ -386,7 +400,11 @@ class Incident extends React.Component {
route={getParentRoute(pathname, null, 'incident-log')}
name="Incident Log"
/>
<BreadCrumbItem route={pathname} name="Incident" />
<BreadCrumbItem
route={pathname}
name="Incident"
containerType="Incident"
/>
<div>
<div>
<div className="db-BackboneViewContainer">

View File

@@ -31,7 +31,7 @@ import getParentRoute from '../utils/getParentRoute';
import { getProbes } from '../actions/probe';
import MSTeamsBox from '../components/webHooks/MSTeamsBox';
import SlackBox from '../components/webHooks/SlackBox';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import { Tab, Tabs, TabList, TabPanel, resetIdCounter } from 'react-tabs';
import { fetchBasicIncidentSettings } from '../actions/incidentBasicsSettings';
class MonitorView extends React.Component {
@@ -40,6 +40,9 @@ class MonitorView extends React.Component {
super(props);
}
componentWillMount() {
resetIdCounter();
}
componentDidMount() {
if (SHOULD_LOG_ANALYTICS) {
logEvent(

View File

@@ -71,6 +71,7 @@ class Schedule extends Component {
route={pathname}
name={name}
pageTitle="Schedule"
containerType="Call Schedule"
/>
<div className="Box-root">
<div>

View File

@@ -112,6 +112,7 @@ class ScheduledEvent extends Component {
route={pathname}
name={eventName}
pageTitle="Scheduled Event Detail"
containerType="Scheduled Event"
/>
<ShouldRender if={requesting}>
<LoadingState />

View File

@@ -26,7 +26,7 @@ import {
import CustomStyles from '../components/statusPage/CustomStyles';
import BreadCrumbItem from '../components/breadCrumb/BreadCrumbItem';
import getParentRoute from '../utils/getParentRoute';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import { Tab, Tabs, TabList, TabPanel, resetIdCounter } from 'react-tabs';
class StatusPage extends Component {
async componentDidMount() {
@@ -62,6 +62,9 @@ class StatusPage extends Component {
);
}
}
componentWillMount() {
resetIdCounter();
}
tabSelected = index => {
const tabSlider = document.getElementById('tab-slider');
tabSlider.style.transform = `translate(calc(${tabSlider.offsetWidth}px*${index}), 0px)`;
@@ -100,7 +103,7 @@ class StatusPage extends Component {
Basic
</Tab>
<Tab className={'custom-tab custom-tab-4'}>
Domain Settings
Custom Domains
</Tab>
<Tab className={'custom-tab custom-tab-4'}>
Branding

View File

@@ -1080,19 +1080,23 @@ export default function monitor(state = INITIAL_STATE, action) {
error: null,
success: true,
monitors: state.monitorsList.monitors.map(monitor => {
if (
monitor.monitors[0]._id === action.payload.monitorId
) {
monitor.monitors[0].subscribers.subscribers = monitor.monitors[0].subscribers.subscribers.filter(
subscriber =>
subscriber._id !== action.payload._id
);
monitor.monitors[0].subscribers.count =
monitor.monitors[0].subscribers.count - 1;
return monitor;
} else {
return monitor;
}
monitor.monitors.find((targetMonitor, index) => {
if (
targetMonitor._id === action.payload.monitorId
) {
monitor.monitors[
index
].subscribers.subscribers = monitor.monitors[
index
].subscribers.subscribers.filter(
subscriber =>
subscriber._id !== action.payload._id
);
return true;
}
return false;
});
return monitor;
}),
},
});

View File

@@ -252,6 +252,10 @@ describe('Incident API With SubProjects', () => {
);
await page.waitFor(2000);
// click on incident notes tab
await page.waitForSelector('#react-tabs-8');
await page.click('#react-tabs-8');
let type = 'internal';
// fill internal message thread form
await page.waitForSelector(`#add-${type}-message`);
@@ -336,6 +340,9 @@ describe('Incident API With SubProjects', () => {
await page.$eval(`#incident_${projectMonitorName1}_0`, e =>
e.click()
);
// click on incident notes tab
await page.waitForSelector('#react-tabs-8');
await page.click('#react-tabs-8');
await page.waitFor(2000);
for (let i = 0; i < 10; i++) {
@@ -352,6 +359,10 @@ describe('Incident API With SubProjects', () => {
await page.click(`#${type}-addButton`);
await page.waitFor(2000);
}
// click on incident timeline tab
await page.waitForSelector('#react-tabs-6');
await page.click('#react-tabs-6');
await page.waitFor(2000);
await page.waitForSelector(
'#incidentTimeline tr.incidentListItem'

View File

@@ -11,7 +11,6 @@ const projectName = utils.generateRandomString();
const projectMonitorName = utils.generateRandomString();
const componentName = utils.generateRandomString();
const bodyText = utils.generateRandomString();
const message = utils.generateRandomString();
describe('Incident Timeline API', () => {
@@ -70,136 +69,6 @@ describe('Incident Timeline API', () => {
await cluster.close();
});
test(
'should create incident in project with multi-probes and add to incident timeline',
async () => {
expect.assertions(2);
const testServer = async ({ page }) => {
await page.goto(utils.HTTP_TEST_SERVER_URL + '/settings');
await page.evaluate(
() => (document.getElementById('responseTime').value = '')
);
await page.evaluate(
() => (document.getElementById('statusCode').value = '')
);
await page.evaluate(
() => (document.getElementById('body').value = '')
);
await page.waitForSelector('#responseTime');
await page.$eval('input[name=responseTime]', e => e.click());
await page.type('input[name=responseTime]', '0');
await page.waitForSelector('#statusCode');
await page.$eval('input[name=statusCode]', e => e.click());
await page.type('input[name=statusCode]', '400');
await page.select('#responseType', 'html');
await page.waitForSelector('#body');
await page.$eval('textarea[name=body]', e => e.click());
await page.type(
'textarea[name=body]',
`<h1 id="html"><span>${bodyText}</span></h1>`
);
await page.$eval('button[type=submit]', e => e.click());
await page.waitForSelector('#save-btn');
};
const dashboard = async ({ page }) => {
await page.waitFor(350000);
// Navigate to Component details
await init.navigateToComponentDetails(componentName, page);
await page.waitForSelector('#incident_span_0');
const incidentTitleSelector = await page.$('#incident_span_0');
let textContent = await incidentTitleSelector.getProperty(
'innerText'
);
textContent = await textContent.jsonValue();
expect(textContent.toLowerCase()).toEqual(
`${projectMonitorName}'s Incident Status`.toLowerCase()
);
await page.waitForSelector(`#incident_${projectMonitorName}_0`);
await page.$eval(`#incident_${projectMonitorName}_0`, e =>
e.click()
);
await page.waitFor(5000);
const incidentTimelineRows = await page.$$(
'#incidentTimeline tr.incidentListItem'
);
const countIncidentTimelines = incidentTimelineRows.length;
expect(countIncidentTimelines).toEqual(2);
};
await cluster.execute(null, testServer);
await cluster.execute(null, dashboard);
},
operationTimeOut
);
test(
'should auto-resolve incident in project with multi-probes and add to incident timeline',
async () => {
expect.assertions(2);
const testServer = async ({ page }) => {
await page.goto(utils.HTTP_TEST_SERVER_URL + '/settings', {
waitUntil: 'networkidle2',
});
await page.evaluate(
() => (document.getElementById('responseTime').value = '')
);
await page.evaluate(
() => (document.getElementById('statusCode').value = '')
);
await page.evaluate(
() => (document.getElementById('body').value = '')
);
await page.waitForSelector('#responseTime');
await page.$eval('input[name=responseTime]', e => e.click());
await page.type('input[name=responseTime]', '0');
await page.waitForSelector('#statusCode');
await page.$eval('input[name=statusCode]', e => e.click());
await page.type('input[name=statusCode]', '200');
await page.select('#responseType', 'html');
await page.waitForSelector('#body');
await page.$eval('textarea[name=body]', e => e.click());
await page.type(
'textarea[name=body]',
`<h1 id="html"><span>${bodyText}</span></h1>`
);
await page.$eval('button[type=submit]', e => e.click());
await page.waitForSelector('#save-btn');
};
const dashboard = async ({ page }) => {
await page.waitFor(350000);
// Navigate to Component details
await init.navigateToComponentDetails(componentName, page);
await page.waitForSelector('#ResolveText_0');
const resolveTextSelector = await page.$('#ResolveText_0');
expect(resolveTextSelector).not.toBeNull();
await page.waitForSelector(`#incident_${projectMonitorName}_0`);
await page.$eval(`#incident_${projectMonitorName}_0`, e =>
e.click()
);
await page.waitFor(5000);
const incidentTimelineRows = await page.$$(
'#incidentTimeline tr.incidentListItem'
);
const countIncidentTimelines = incidentTimelineRows.length;
expect(countIncidentTimelines).toEqual(6);
};
await cluster.execute(null, testServer);
await cluster.execute(null, dashboard);
},
operationTimeOut
);
test(
'should create incident in project and add to message to the incident message thread',
async () => {
@@ -210,28 +79,38 @@ describe('Incident Timeline API', () => {
await init.navigateToComponentDetails(componentName, page);
await page.waitForSelector(
`#more-details-${projectMonitorName}`
);
await page.click(`#more-details-${projectMonitorName}`);
// create incident
await page.waitForSelector(
`#monitorCreateIncident_${projectMonitorName}`
);
await page.click(
`#monitorCreateIncident_${projectMonitorName}`
`#create_incident_${projectMonitorName}`
);
await page.click(`#create_incident_${projectMonitorName}`);
await page.waitForSelector('#createIncident');
await init.selectByText('#incidentType', 'Offline', page);
await page.type('#title', 'new incident');
await page.waitForSelector('#createIncident');
await page.click('#createIncident');
await page.waitFor(2000);
// navigate to monitor details
await page.waitForSelector(
`#more-details-${projectMonitorName}`
);
await page.click(`#more-details-${projectMonitorName}`);
await page.waitFor(2000);
// click on incident tab
await page.waitForSelector('#react-tabs-2');
await page.click('#react-tabs-2');
await page.waitForSelector(`#incident_${projectMonitorName}_0`);
await page.click(`#incident_${projectMonitorName}_0`);
await page.waitFor(2000);
// click on incident notes tab
await page.waitForSelector('#react-tabs-8');
await page.click('#react-tabs-8');
// fill investigation message thread form
await page.waitFor(2000);
await page.waitForSelector(`#add-${type}-message`);
await page.click(`#add-${type}-message`);
await page.waitForSelector(
`#form-new-incident-${type}-message`
@@ -269,11 +148,24 @@ describe('Incident Timeline API', () => {
// Navigate to Component details
await init.navigateToComponentDetails(componentName, page);
await page.waitFor(2000);
// navigate to monitor details
await page.waitForSelector(
`#more-details-${projectMonitorName}`
);
await page.click(`#more-details-${projectMonitorName}`);
// click on incident tab
await page.waitForSelector('#react-tabs-2');
await page.click('#react-tabs-2');
await page.waitForSelector(`#incident_${projectMonitorName}_0`);
await page.click(`#incident_${projectMonitorName}_0`);
await page.waitFor(2000);
// click on incident notes tab
await page.waitForSelector('#react-tabs-8');
await page.click('#react-tabs-8');
await page.waitForSelector(`#edit_${type}_incident_message_0`);
await page.click(`#edit_${type}_incident_message_0`);
await page.waitFor(5000);
@@ -312,10 +204,23 @@ describe('Incident Timeline API', () => {
await init.navigateToComponentDetails(componentName, page);
await page.waitFor(2000);
// navigate to monitor details
await page.waitForSelector(
`#more-details-${projectMonitorName}`
);
await page.click(`#more-details-${projectMonitorName}`);
// click on incident tab
await page.waitForSelector('#react-tabs-2');
await page.click('#react-tabs-2');
await page.waitForSelector(`#incident_${projectMonitorName}_0`);
await page.click(`#incident_${projectMonitorName}_0`);
await page.waitFor(2000);
// click on incident notes tab
await page.waitForSelector('#react-tabs-8');
await page.click('#react-tabs-8');
// fill internal message thread form
await page.click(`#add-${type}-message`);
await page.waitForSelector(
@@ -357,16 +262,29 @@ describe('Incident Timeline API', () => {
await init.navigateToComponentDetails(componentName, page);
await page.waitFor(2000);
// navigate to monitor details
await page.waitForSelector(
`#more-details-${projectMonitorName}`
);
await page.click(`#more-details-${projectMonitorName}`);
// click on incident tab
await page.waitForSelector('#react-tabs-2');
await page.click('#react-tabs-2');
await page.waitForSelector(`#incident_${projectMonitorName}_0`);
await page.click(`#incident_${projectMonitorName}_0`);
await page.waitFor(2000);
// click on incident notes tab
await page.waitForSelector('#react-tabs-8');
await page.click('#react-tabs-8');
await page.waitForSelector(`#edit_${type}_incident_message_0`);
await page.click(`#edit_${type}_incident_message_0`);
await page.waitFor(5000);
// edit investigation message thread form
await page.waitForSelector(`#edit-${type}`);
await page.waitForSelector(`#${type}-editButton`);
await page.click(`textarea[id=edit-${type}]`);
await page.type(`textarea[id=edit-${type}]`, '-updated');
await init.selectByText(
@@ -403,10 +321,23 @@ describe('Incident Timeline API', () => {
await init.navigateToComponentDetails(componentName, page);
await page.waitFor(2000);
// navigate to monitor details
await page.waitForSelector(
`#more-details-${projectMonitorName}`
);
await page.click(`#more-details-${projectMonitorName}`);
// click on incident tab
await page.waitForSelector('#react-tabs-2');
await page.click('#react-tabs-2');
await page.waitForSelector(`#incident_${projectMonitorName}_0`);
await page.click(`#incident_${projectMonitorName}_0`);
await page.waitFor(2000);
// click on incident notes tab
await page.waitForSelector('#react-tabs-8');
await page.click('#react-tabs-8');
await page.waitForSelector(
`#delete_${type}_incident_message_0`
);
@@ -438,10 +369,21 @@ describe('Incident Timeline API', () => {
await init.navigateToComponentDetails(componentName, page);
await page.waitFor(2000);
// navigate to monitor details
await page.waitForSelector(`#more-details-${projectMonitorName}`);
await page.click(`#more-details-${projectMonitorName}`);
// click on incident tab
await page.waitForSelector('#react-tabs-2');
await page.click('#react-tabs-2');
await page.waitForSelector(`#incident_${projectMonitorName}_0`);
await page.$eval(`#incident_${projectMonitorName}_0`, e =>
e.click()
);
// click on incident notes tab
await page.waitForSelector('#react-tabs-8');
await page.click('#react-tabs-8');
await page.waitFor(2000);
for (let i = 0; i < 10; i++) {
@@ -458,6 +400,11 @@ describe('Incident Timeline API', () => {
await page.waitFor(2000);
}
// click on timeline tab
await page.waitForSelector('#react-tabs-6');
await page.click('#react-tabs-6');
await page.waitFor(2000);
await page.waitForSelector('#incidentTimeline tr.incidentListItem');
let incidentTimelineRows = await page.$$(
'#incidentTimeline tr.incidentListItem'
@@ -466,14 +413,14 @@ describe('Incident Timeline API', () => {
expect(countIncidentTimelines).toEqual(10);
const nextSelector = await page.$('#btnTimelineNext');
await nextSelector.click();
await page.waitForSelector('#btnTimelineNext');
await page.click('#btnTimelineNext');
await page.waitFor(7000);
incidentTimelineRows = await page.$$(
'#incidentTimeline tr.incidentListItem'
);
countIncidentTimelines = incidentTimelineRows.length;
expect(countIncidentTimelines).toEqual(6);
expect(countIncidentTimelines).toEqual(10);
const prevSelector = await page.$('#btnTimelinePrev');
await prevSelector.click();

View File

@@ -146,8 +146,7 @@ describe('Monitor Detail API', () => {
await page.waitForSelector('#react-tabs-2');
await page.click('#react-tabs-2');
const selector =
'tr.incidentListItem:first-of-type > td:nth-of-type(2)';
const selector = `#incident_${monitorName}_0`;
await page.waitForSelector(selector);
await page.click(selector);
await page.waitFor(3000);
@@ -265,8 +264,8 @@ describe('Monitor Detail API', () => {
await page.waitFor(5000);
// click on advance option tab
await page.waitForSelector('#react-tabs-8');
await page.click('#react-tabs-8');
await page.waitForSelector('#react-tabs-10');
await page.click('#react-tabs-10');
await page.waitForSelector('button[id=deleteIncidentButton]');
await page.$eval('#deleteIncidentButton', e => e.click());
@@ -277,11 +276,19 @@ describe('Monitor Detail API', () => {
await page.$eval('#confirmDeleteIncident', e => e.click());
await page.waitForNavigation();
const incidentList = 'tr.incidentListItem';
await page.waitForSelector(incidentList);
await page.waitFor(35000);
// click on Incident tab
await page.waitForSelector('#react-tabs-2');
await page.click('#react-tabs-2');
expect((await page.$$(incidentList)).length).toEqual(0);
let incidentCountSpanElement = await page.waitForSelector(
`#incident_count`
);
incidentCountSpanElement = await incidentCountSpanElement.getProperty(
'innerText'
);
incidentCountSpanElement = await incidentCountSpanElement.jsonValue();
expect(incidentCountSpanElement).toMatch('0 Incident');
});
},
operationTimeOut

View File

@@ -197,4 +197,40 @@ describe('Monitor Detail API', () => {
},
operationTimeOut
);
test(
'Should delete a subscriber',
async () => {
return await cluster.execute(null, async ({ page }) => {
// Navigate to Monitor details
await init.navigateToMonitorDetails(
componentName,
monitorName,
page
);
// click on subscribers tab
await page.waitForSelector('#react-tabs-4');
await page.click('#react-tabs-4');
let initialSubscribers =
'#subscribersList > tbody > tr.subscriber-list-item';
await page.waitForSelector(initialSubscribers);
initialSubscribers = await page.$$(initialSubscribers);
const initialCount = initialSubscribers.length;
await page.waitForSelector('button[id=deleteSubscriber_0]');
await page.click('button[id=deleteSubscriber_0]');
let finalSubscribers =
'#subscribersList > tbody > tr.subscriber-list-item';
await page.waitForSelector(finalSubscribers);
finalSubscribers = await page.$$(finalSubscribers);
const finalCount = finalSubscribers.length;
expect(finalCount).toEqual(3);
expect(initialCount).toBeGreaterThan(finalCount);
});
},
operationTimeOut
);
});

View File

@@ -60,6 +60,13 @@ describe('Status Page', () => {
//component + monitor
await init.addComponent(componentName, page);
await init.addMonitorToComponent(null, monitorName, page);
await page.goto(utils.DASHBOARD_URL);
await page.waitForSelector('#components', { visible: true });
await page.click('#components');
await page.waitForSelector(`#more-details-${componentName}`, {
visible: true,
});
await page.click(`#more-details-${componentName}`);
await init.addMonitorToComponent(null, monitorName1, page);
await page.waitForSelector('.ball-beat', { hidden: true });
}
@@ -381,8 +388,8 @@ describe('Status Page', () => {
await page.click('#react-tabs-2');
await page.waitForSelector('#addMoreDomain');
await page.click('#addMoreDomain');
await page.waitForSelector('#domain', { visible: true });
await page.type('#domain', 'fyipeapp.com');
await page.waitForSelector('#domain_1', { visible: true });
await page.type('#domain_1', 'fyipeapp.com');
await page.click('#btnAddDomain');
// if domain was not added sucessfully, list will be undefined
// it will timeout
@@ -492,8 +499,8 @@ describe('Status Page', () => {
// create one more domain on the status page
await page.waitForSelector('#addMoreDomain');
await page.click('#addMoreDomain');
await page.waitForSelector('#domain', { visible: true });
await page.type('#domain', 'app.fyipeapp.com');
await page.waitForSelector('#domain_1', { visible: true });
await page.type('#domain_1', 'app.fyipeapp.com');
await page.click('#btnAddDomain');
await page.reload({ waitUntil: 'networkidle0' });
@@ -539,8 +546,8 @@ describe('Status Page', () => {
// create one more domain on the status page
await page.waitForSelector('#addMoreDomain');
await page.click('#addMoreDomain');
await page.waitForSelector('#domain', { visible: true });
await page.type('#domain', 'app.fyipeapp.com');
await page.waitForSelector('#domain_1', { visible: true });
await page.type('#domain_1', 'server.fyipeapp.com');
await page.click('#btnAddDomain');
await page.reload({ waitUntil: 'networkidle0' });
await page.waitForSelector('#react-tabs-2');
@@ -653,7 +660,7 @@ describe('Status Page', () => {
let elem = await page.$('#field-error');
elem = await elem.getProperty('innerText');
elem = await elem.jsonValue();
expect(elem).toEqual('Domain is required');
expect(elem).toEqual('Domain is required.');
});
},
operationTimeOut
@@ -669,8 +676,8 @@ describe('Status Page', () => {
await page.click('#react-tabs-2');
await page.waitForSelector('#addMoreDomain');
await page.click('#addMoreDomain');
await page.waitForSelector('#domain', { visible: true });
await page.type('#domain', 'fyipeapp');
await page.waitForSelector('#domain_1', { visible: true });
await page.type('#domain_1', 'fyipeapp');
await page.waitForSelector('#btnAddDomain');
await page.click('#btnAddDomain');
let elem = await page.$('#field-error');
@@ -681,4 +688,61 @@ describe('Status Page', () => {
},
operationTimeOut
);
test(
'should add multiple domains',
async () => {
return await cluster.execute(null, async ({ page }) => {
await gotoTheFirstStatusPage(page);
await page.waitForNavigation({ waitUntil: 'networkidle0' });
await page.waitForSelector('#react-tabs-2');
await page.click('#react-tabs-2');
await page.waitForSelector('#addMoreDomain');
await page.click('#addMoreDomain');
await page.waitForSelector('#domain_1', { visible: true });
await page.type('#domain_1', 'fyipe.fyipeapp.com');
await page.click('#addMoreDomain');
await page.waitForSelector('#domain_2', { visible: true });
await page.type('#domain_2', 'api.fyipeapp.com');
await page.waitForSelector('#btnAddDomain');
await page.click('#btnAddDomain');
await page.waitFor(10000);
const domains = await page.$$eval(
'fieldset[name="added-domain"]',
domains => domains.length
);
expect(domains).toEqual(4);
});
},
operationTimeOut
);
test(
'should not add an existing domain',
async () => {
return await cluster.execute(null, async ({ page }) => {
await gotoTheFirstStatusPage(page);
await page.waitForNavigation({ waitUntil: 'networkidle0' });
await page.waitForSelector('#react-tabs-2');
await page.click('#react-tabs-2');
const initialDomains = await page.$$eval(
'fieldset[name="added-domain"]',
domains => domains.length
);
await page.waitForSelector('#addMoreDomain');
await page.click('#addMoreDomain');
await page.waitForSelector('#domain_1', { visible: true });
await page.type('#domain_1', 'fyipe.fyipeapp.com');
await page.click('#addMoreDomain');
await page.waitFor(5000);
const domains = await page.$$eval(
'fieldset[name="added-domain"]',
domains => domains.length
);
expect(domains).toEqual(initialDomains);
});
},
operationTimeOut
);
});

14
git-hooks/README.md Normal file
View File

@@ -0,0 +1,14 @@
# Hooks
Like many other Version Control Systems, Git has a way to fire off custom scripts when certain important actions occur. Hooks are triggered by operations such as committing and merging.
This project has a git pre-commit hook that automatically runs when you execute `git commit`. This pre-commit is responsible for linting the project. This is one of the ways to be sure any code git committed has been linted properly.
## Enable git hooks
Run these command in your terminal from the root of this project.
```
root="$(pwd)"
ln -s "$root/git-hooks" "$root/.git/hooks"
```

23
git-hooks/pre-commit Normal file
View File

@@ -0,0 +1,23 @@
#!/bin/sh
# Run these command in your terminal from the root of this project.
# ```
# root="$(pwd)"
# ln -s "$root/git-hooks" "$root/.git/hooks"
# ```
function jslint {
printf "\nValidating and Linting Javascript. This will take few minutes...\n"
npm run lint
if [[ "$?" != 0 ]]; then
printf "There are lint errors for your commit. Please run 'npm run fix-lint` to fix them.\n"
exit 1
fi
popd
printf "\nJavascript Validating and Linting Completed!\n"
}
jslint

View File

@@ -49,7 +49,7 @@ describe('Request demo', () => {
await page.waitForSelector('#form-section');
await page.type('#fullname', util.user.name);
await page.type('#email', util.user.email);
await page.type('#Phone', util.user.phone);
await page.type('#phone', util.user.phone);
await page.type('#website', util.user.website);
await page.click('#country');
await page.keyboard.press('ArrowDown');
@@ -74,7 +74,7 @@ describe('Request demo', () => {
await page.waitForSelector('#form-section');
await page.type('#fullname', util.user.name);
await page.type('#email', util.user.email);
await page.type('#Phone', util.user.phone);
await page.type('#phone', util.user.phone);
await page.type('#website', util.user.website);
await page.click('#country');
await page.keyboard.press('ArrowDown');
@@ -99,7 +99,7 @@ describe('Request demo', () => {
await page.waitForSelector('#form-section');
await page.type('#fullname', util.user.name);
await page.type('#email', util.user.email);
await page.type('#Phone', util.user.phone);
await page.type('#phone', util.user.phone);
await page.type('#website', util.user.website);
await page.click('#country');
await page.keyboard.press('ArrowDown');
@@ -124,7 +124,7 @@ describe('Request demo', () => {
await page.waitForSelector('#form-section');
await page.type('#fullname', util.user.name);
await page.type('#email', util.user.email);
await page.type('#Phone', util.user.phone);
await page.type('#phone', util.user.phone);
await page.type('#website', util.user.website);
await page.click('#country');
await page.keyboard.press('ArrowDown');