diff --git a/accounts/src/actions/login.js b/accounts/src/actions/login.js index b486126bed..5446227a1f 100755 --- a/accounts/src/actions/login.js +++ b/accounts/src/actions/login.js @@ -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)); diff --git a/accounts/src/config.js b/accounts/src/config.js index bf83180a9a..93e1283bab 100755 --- a/accounts/src/config.js +++ b/accounts/src/config.js @@ -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'); diff --git a/accounts/src/pages/VerifyAuthToken.js b/accounts/src/pages/VerifyAuthToken.js index e52a9ed2ac..5578ae14b2 100644 --- a/accounts/src/pages/VerifyAuthToken.js +++ b/accounts/src/pages/VerifyAuthToken.js @@ -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; diff --git a/accounts/src/pages/VerifyBackupCode.js b/accounts/src/pages/VerifyBackupCode.js index 7730aef897..2077485af8 100644 --- a/accounts/src/pages/VerifyBackupCode.js +++ b/accounts/src/pages/VerifyBackupCode.js @@ -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 { +
+

+ Have a google app authenticator?{' '} + + Enter auth token + + . +

+
@@ -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 = { diff --git a/admin-dashboard/src/constants/user.js b/admin-dashboard/src/constants/user.js index e7e71cc069..ed835a129e 100644 --- a/admin-dashboard/src/constants/user.js +++ b/admin-dashboard/src/constants/user.js @@ -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'; diff --git a/admin-dashboard/src/reducers/user.js b/admin-dashboard/src/reducers/user.js index 7240ae4c4e..715c368330 100644 --- a/admin-dashboard/src/reducers/user.js +++ b/admin-dashboard/src/reducers/user.js @@ -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; } diff --git a/admin-dashboard/src/test/Users.test.enterprise.js b/admin-dashboard/src/test/Users.test.enterprise.js index 8d30368efc..9e05d8404f 100644 --- a/admin-dashboard/src/test/Users.test.enterprise.js +++ b/admin-dashboard/src/test/Users.test.enterprise.js @@ -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 + ); }); diff --git a/backend/.env b/backend/.env index 75461598c4..fd9e233d5d 100755 --- a/backend/.env +++ b/backend/.env @@ -11,4 +11,4 @@ REDIS_HOST=localhost CLUSTER_KEY=f414c23b4cdf4e84a6a66ecfd528eff2 TEST_TWILIO_NUMBER=+919910568840 # IS_SAAS_SERVICE=true -ENCRYPTION_KEY=01234567890123456789012345678901 \ No newline at end of file +# ENCRYPTION_KEY=01234567890123456789012345678901 diff --git a/backend/backend/api/applicationSecurity.js b/backend/backend/api/applicationSecurity.js index 345cc5d7e6..506a1961cd 100644 --- a/backend/backend/api/applicationSecurity.js +++ b/backend/backend/api/applicationSecurity.js @@ -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', diff --git a/backend/backend/api/statusPage.js b/backend/backend/api/statusPage.js index f956918bf1..1de65c2be2 100755 --- a/backend/backend/api/statusPage.js +++ b/backend/backend/api/statusPage.js @@ -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); } diff --git a/backend/backend/api/user.js b/backend/backend/api/user.js index ffbd1dd6cf..4052a3fbbc 100755 --- a/backend/backend/api/user.js +++ b/backend/backend/api/user.js @@ -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.', diff --git a/backend/backend/services/statusPageService.js b/backend/backend/services/statusPageService.js index 95c1211a23..654ba77ba9 100755 --- a/backend/backend/services/statusPageService.js +++ b/backend/backend/services/statusPageService.js @@ -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(); diff --git a/backend/backend/services/userService.js b/backend/backend/services/userService.js index 68d4ecfbb7..879a642141 100755 --- a/backend/backend/services/userService.js +++ b/backend/backend/services/userService.js @@ -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 }; } diff --git a/dashboard/public/assets/css/Selector.css b/dashboard/public/assets/css/Selector.css index b1d5a12e58..334783994e 100755 --- a/dashboard/public/assets/css/Selector.css +++ b/dashboard/public/assets/css/Selector.css @@ -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; diff --git a/dashboard/public/assets/css/newdashboard.css b/dashboard/public/assets/css/newdashboard.css index eb62445934..60eade9ac1 100755 --- a/dashboard/public/assets/css/newdashboard.css +++ b/dashboard/public/assets/css/newdashboard.css @@ -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; +} diff --git a/dashboard/public/assets/icons/next.svg b/dashboard/public/assets/icons/next.svg new file mode 100644 index 0000000000..8ee0578f48 --- /dev/null +++ b/dashboard/public/assets/icons/next.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dashboard/src/components/application/ApplicationLogDetail.js b/dashboard/src/components/application/ApplicationLogDetail.js index 0755b3d3bd..1f8c855637 100644 --- a/dashboard/src/components/application/ApplicationLogDetail.js +++ b/dashboard/src/components/application/ApplicationLogDetail.js @@ -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, diff --git a/dashboard/src/components/application/ApplicationLogHeader.js b/dashboard/src/components/application/ApplicationLogHeader.js index 29c7517639..1c73087cdb 100644 --- a/dashboard/src/components/application/ApplicationLogHeader.js +++ b/dashboard/src/components/application/ApplicationLogHeader.js @@ -66,7 +66,7 @@ class ApplicationLogHeader extends Component {
diff --git a/dashboard/src/components/basic/CustomDateTimeSelector.js b/dashboard/src/components/basic/CustomDateTimeSelector.js index 98a2d4041a..d86a41fa94 100644 --- a/dashboard/src/components/basic/CustomDateTimeSelector.js +++ b/dashboard/src/components/basic/CustomDateTimeSelector.js @@ -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 = ({
- + - + {incidents ? incidents.length + (incidents.length > 1 diff --git a/dashboard/src/components/modals/BackupCodes.js b/dashboard/src/components/modals/BackupCodes.js index 5e29f3c931..aa008fbb23 100644 --- a/dashboard/src/components/modals/BackupCodes.js +++ b/dashboard/src/components/modals/BackupCodes.js @@ -59,9 +59,9 @@ class BackupCodesModal extends React.Component { }} > - +
Two Factor Authentication Backup Codes - +
@@ -69,6 +69,10 @@ class BackupCodesModal extends React.Component {
+
+ You have to print these codes or + keep them in the safe place. +
@@ -103,7 +108,7 @@ class BackupCodesModal extends React.Component { }} >
- +
{ diff --git a/dashboard/src/components/modals/CreateIncident.js b/dashboard/src/components/modals/CreateIncident.js index ffd7123077..fddab6edb9 100755 --- a/dashboard/src/components/modals/CreateIncident.js +++ b/dashboard/src/components/modals/CreateIncident.js @@ -251,7 +251,7 @@ class CreateIncident extends Component { RenderSelect } name="incidentType" - id="incidentType" + id="incidentTypeId" placeholder="Incident type" disabled={ this.props diff --git a/dashboard/src/components/monitor/MonitorViewDeleteBox.js b/dashboard/src/components/monitor/MonitorViewDeleteBox.js index 6777904632..5918bb6d50 100755 --- a/dashboard/src/components/monitor/MonitorViewDeleteBox.js +++ b/dashboard/src/components/monitor/MonitorViewDeleteBox.js @@ -87,6 +87,7 @@ export class MonitorViewDeleteBox extends Component { + ); + render() { const { handleSubmit } = this.props; @@ -132,35 +160,7 @@ export class OnCallAlertBox extends Component {
- + {this.renderAddEscalationPolicyButton()}
@@ -215,6 +215,7 @@ export class OnCallAlertBox extends Component {
+ {this.renderAddEscalationPolicyButton()} + + ); + render() { const { handleSubmit, subProjects } = this.props; const { status } = this.props.statusPage; @@ -124,49 +154,11 @@ export class Monitors extends Component {
- 0 && - (IsAdminSubProject(subProject) || - IsOwnerSubProject(subProject)) - } - > -
-
- -
+
+
+ {this.renderAddMonitorButton(subProject)}
- +
@@ -273,6 +265,7 @@ export class Monitors extends Component {
+ {this.renderAddMonitorButton(subProject)} 0 && diff --git a/dashboard/src/components/statusPage/Setting.js b/dashboard/src/components/statusPage/Setting.js index eedefeec04..f455796dba 100755 --- a/dashboard/src/components/statusPage/Setting.js +++ b/dashboard/src/components/statusPage/Setting.js @@ -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) => ( +
+ + +
+ + +
+
+
+
+
+
+ + {this.props.addDomain.error} + +
+
+
+
+

+ {IS_LOCALHOST && ( + + If you want to preview your status page. Please + check{' '} + + {publicStatusPageUrl}{' '} + + + )} + {IS_SAAS_SERVICE && !IS_LOCALHOST && ( + + Add statuspage.fyipeapp.com to your CNAME. If you + want to preview your status page. Please check{' '} + + {publicStatusPageUrl}{' '} + + + )} + {!IS_SAAS_SERVICE && !IS_LOCALHOST && ( + + If you want to preview your status page. Please + check{' '} + + {publicStatusPageUrl}{' '} + + + )} +

+
+ +
+
+
+ ); + + renderAddDomainButton = publicStatusPageUrl => ( ); + 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 {
- {this.renderAddDomainBButton()} + {this.renderAddDomainButton( + publicStatusPageUrl + )}
@@ -435,6 +585,8 @@ export class Setting extends Component { 'center', paddingLeft: 0, paddingBottom: 0, + paddingTop: + '5px', }} >
- -
- -
- - {subscribers.name - ? subscribers.name - : subscriber.monitorId && - subscriber.monitorName - ? subscriber.monitorName - : 'Unknown Monitor'} - -
-
-
- - - - + - - - - + - + + + + - - - )) + + + + + + ) + ) ) : ( )} diff --git a/dashboard/src/pages/Incident.js b/dashboard/src/pages/Incident.js index 317a61c3f5..e8024d172e 100755 --- a/dashboard/src/pages/Incident.js +++ b/dashboard/src/pages/Incident.js @@ -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'} > - + Basic - - Logs + + Monitor Logs - - Timeline + + Alert Logs - - Notes + + Incident Timeline - + + Incident Notes + + Advanced Options
@@ -281,10 +290,6 @@ class Incident extends React.Component { -
+
+
+ + + + - +
diff --git a/dashboard/src/pages/MonitorView.js b/dashboard/src/pages/MonitorView.js index e662cd73db..68c57fa73e 100755 --- a/dashboard/src/pages/MonitorView.js +++ b/dashboard/src/pages/MonitorView.js @@ -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( diff --git a/dashboard/src/pages/Schedule.js b/dashboard/src/pages/Schedule.js index 702bd01af7..59c8502dbf 100755 --- a/dashboard/src/pages/Schedule.js +++ b/dashboard/src/pages/Schedule.js @@ -71,6 +71,7 @@ class Schedule extends Component { route={pathname} name={name} pageTitle="Schedule" + containerType="Call Schedule" />
diff --git a/dashboard/src/pages/ScheduledEventDetail.js b/dashboard/src/pages/ScheduledEventDetail.js index e422905f8f..f5d452bbcd 100644 --- a/dashboard/src/pages/ScheduledEventDetail.js +++ b/dashboard/src/pages/ScheduledEventDetail.js @@ -112,6 +112,7 @@ class ScheduledEvent extends Component { route={pathname} name={eventName} pageTitle="Scheduled Event Detail" + containerType="Scheduled Event" /> diff --git a/dashboard/src/pages/StatusPage.js b/dashboard/src/pages/StatusPage.js index 0729b81f00..b95277b718 100755 --- a/dashboard/src/pages/StatusPage.js +++ b/dashboard/src/pages/StatusPage.js @@ -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 - Domain Settings + Custom Domains Branding diff --git a/dashboard/src/reducers/monitor.js b/dashboard/src/reducers/monitor.js index d6065d0938..6783e0c893 100755 --- a/dashboard/src/reducers/monitor.js +++ b/dashboard/src/reducers/monitor.js @@ -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; }), }, }); diff --git a/dashboard/src/test/puppeteer/IncidentSubProject.test.js b/dashboard/src/test/puppeteer/IncidentSubProject.test.js index 87456b91d8..5abd8954d2 100755 --- a/dashboard/src/test/puppeteer/IncidentSubProject.test.js +++ b/dashboard/src/test/puppeteer/IncidentSubProject.test.js @@ -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' diff --git a/dashboard/src/test/puppeteer/IncidentTimeline.test.js b/dashboard/src/test/puppeteer/IncidentTimeline.test.js index e5dca08670..0cb28b09f9 100644 --- a/dashboard/src/test/puppeteer/IncidentTimeline.test.js +++ b/dashboard/src/test/puppeteer/IncidentTimeline.test.js @@ -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]', - `

${bodyText}

` - ); - 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]', - `

${bodyText}

` - ); - 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(); diff --git a/dashboard/src/test/puppeteer/MonitorDetail.test.js b/dashboard/src/test/puppeteer/MonitorDetail.test.js index 048103a417..d56af1d30c 100644 --- a/dashboard/src/test/puppeteer/MonitorDetail.test.js +++ b/dashboard/src/test/puppeteer/MonitorDetail.test.js @@ -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 diff --git a/dashboard/src/test/puppeteer/MonitorSubscribers.test.js b/dashboard/src/test/puppeteer/MonitorSubscribers.test.js index 5404f6f3f5..d98974ef95 100644 --- a/dashboard/src/test/puppeteer/MonitorSubscribers.test.js +++ b/dashboard/src/test/puppeteer/MonitorSubscribers.test.js @@ -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 + ); }); diff --git a/dashboard/src/test/puppeteer/StatusPage.test.js b/dashboard/src/test/puppeteer/StatusPage.test.js index e7412f84cc..649138dca1 100644 --- a/dashboard/src/test/puppeteer/StatusPage.test.js +++ b/dashboard/src/test/puppeteer/StatusPage.test.js @@ -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 + ); }); diff --git a/git-hooks/README.md b/git-hooks/README.md new file mode 100644 index 0000000000..9bb981e33f --- /dev/null +++ b/git-hooks/README.md @@ -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" +``` diff --git a/git-hooks/pre-commit b/git-hooks/pre-commit new file mode 100644 index 0000000000..07a5297992 --- /dev/null +++ b/git-hooks/pre-commit @@ -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 \ No newline at end of file diff --git a/smoke-test/home.test.js b/smoke-test/home.test.js index 78e9682d05..b3b57e0760 100755 --- a/smoke-test/home.test.js +++ b/smoke-test/home.test.js @@ -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');
( +
-
+
- -
+ +
- {(subscriber.statusPageId !== - undefined && - subscriber.statusPageId !== - null && - subscriber.statusPageName) || - 'Fyipe Dashboard'} + {subscribers.name + ? subscribers.name + : subscriber.monitorId && + subscriber.monitorName + ? subscriber.monitorName + : 'Unknown Monitor'}
-
-
-
-
- -
-
-
- {subscriber.contactWebhook || - subscriber.contactEmail || - (subscriber.contactPhone && - `+${countryTelephoneCode( - subscriber.countryCode.toUpperCase() - )}${ - subscriber.contactPhone - }`) || - ''} -
+
+
+
+ +
+ + {(subscriber.statusPageId !== + undefined && + subscriber.statusPageId !== + null && + subscriber.statusPageName) || + 'Fyipe Dashboard'} +
-
- -
- -
-
-
-
- - - { - subscriber.alertVia - } -
-
-
-
- -
+
+
+
+
+
+ + + { + subscriber.alertVia } - type="button" - disabled={ - deleting - } - onClick={() => - this.props.deleteSubscriber( - subscriber - .projectId - ._id, - subscriber._id - ) - } - > - - - - Remove - - - - - - - +
+
+
+
+ + + +
+
+
+