mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
Merge branch 'master' of https://gitlab.com/fyipe-project/app into feature-custom-tutorial
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
15
admin-dashboard/package-lock.json
generated
15
admin-dashboard/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
337
admin-dashboard/src/components/user/TwoFactorAuth.js
Normal file
337
admin-dashboard/src/components/user/TwoFactorAuth.js
Normal 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);
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
@@ -11,4 +11,4 @@ REDIS_HOST=localhost
|
||||
CLUSTER_KEY=f414c23b4cdf4e84a6a66ecfd528eff2
|
||||
TEST_TWILIO_NUMBER=+919910568840
|
||||
# IS_SAAS_SERVICE=true
|
||||
ENCRYPTION_KEY=01234567890123456789012345678901
|
||||
# ENCRYPTION_KEY=01234567890123456789012345678901
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
44
dashboard/public/assets/icons/next.svg
Normal file
44
dashboard/public/assets/icons/next.svg
Normal 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 |
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -356,6 +356,7 @@ class LogList extends Component {
|
||||
src={
|
||||
'/dashboard/assets/img/more.svg'
|
||||
}
|
||||
alt="more-logs"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
|
||||
@@ -251,7 +251,7 @@ class CreateIncident extends Component {
|
||||
RenderSelect
|
||||
}
|
||||
name="incidentType"
|
||||
id="incidentType"
|
||||
id="incidentTypeId"
|
||||
placeholder="Incident type"
|
||||
disabled={
|
||||
this.props
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -71,6 +71,7 @@ class Schedule extends Component {
|
||||
route={pathname}
|
||||
name={name}
|
||||
pageTitle="Schedule"
|
||||
containerType="Call Schedule"
|
||||
/>
|
||||
<div className="Box-root">
|
||||
<div>
|
||||
|
||||
@@ -112,6 +112,7 @@ class ScheduledEvent extends Component {
|
||||
route={pathname}
|
||||
name={eventName}
|
||||
pageTitle="Scheduled Event Detail"
|
||||
containerType="Scheduled Event"
|
||||
/>
|
||||
<ShouldRender if={requesting}>
|
||||
<LoadingState />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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
14
git-hooks/README.md
Normal 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
23
git-hooks/pre-commit
Normal 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
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user