UI: Replace startOverPage with an improved Island reset modal

New modal allows to save clicks, explains the situation better, offers to export the config and allows deleting agent data without deleting config
This commit is contained in:
vakarisz 2022-04-21 17:41:03 +03:00
parent 551439dcc2
commit 75034f37f6
10 changed files with 192 additions and 216 deletions

View File

@ -6,7 +6,6 @@ import ConfigurePage from './pages/ConfigurePage.js';
import RunMonkeyPage from './pages/RunMonkeyPage/RunMonkeyPage';
import MapPage from './pages/MapPage';
import TelemetryPage from './pages/TelemetryPage';
import StartOverPage from './pages/ResetPage';
import ReportPage from './pages/ReportPage';
import LicensePage from './pages/LicensePage';
import AuthComponent from './AuthComponent';
@ -47,7 +46,6 @@ export const Routes = {
RunMonkeyPage: '/run-monkey',
MapPage: '/infection/map',
TelemetryPage: '/infection/telemetry',
StartOverPage: '/start-over',
LicensePage: '/license'
}
@ -232,8 +230,6 @@ class AppComponent extends AuthComponent {
<SidebarLayoutComponent component={MapPage} {...defaultSideNavProps}/>)}
{this.renderRoute(Routes.TelemetryPage,
<SidebarLayoutComponent component={TelemetryPage} {...defaultSideNavProps}/>)}
{this.renderRoute(Routes.StartOverPage,
<SidebarLayoutComponent component={StartOverPage} {...defaultSideNavProps}/>)}
{this.redirectToReport()}
{this.renderRoute(Routes.SecurityReport,
<SidebarLayoutComponent component={ReportPage}

View File

@ -1,30 +1,37 @@
import React, {ReactFragment} from 'react';
import React, {ReactFragment, useState} from 'react';
import {Button} from 'react-bootstrap';
import {NavLink} from 'react-router-dom';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck';
import {faUndo} from '@fortawesome/free-solid-svg-icons/faUndo';
import '../styles/components/SideNav.scss';
import {CompletedSteps} from "./side-menu/CompletedSteps";
import {isReportRoute, Routes} from "./Main";
import {CompletedSteps} from './side-menu/CompletedSteps';
import {isReportRoute, Routes} from './Main';
import Logo from './logo/LogoComponent';
import IslandResetModal from './ui-components/IslandResetModal';
const logoImage = require('../images/monkey-icon.svg');
const infectionMonkeyImage = require('../images/infection-monkey.svg');
import Logo from "./logo/LogoComponent";
type Props = {
disabled?: boolean,
completedSteps: CompletedSteps,
defaultReport: string,
header?: ReactFragment
header?: ReactFragment,
onStatusChange: () => void
}
const SideNavComponent = ({disabled,
completedSteps,
defaultReport,
header=null}: Props) => {
const SideNavComponent = ({
disabled,
completedSteps,
defaultReport,
header = null,
onStatusChange,
}: Props) => {
const [showResetModal, setShowResetModal] = useState(false);
return (
<>
@ -37,12 +44,12 @@ const SideNavComponent = ({disabled,
<ul className='navigation'>
{(header !== null) &&
<>
<li>
{header}
</li>
<hr/>
</>}
<>
<li>
{header}
</li>
<hr/>
</>}
<li>
<NavLink to={Routes.RunMonkeyPage} className={getNavLinkClass()}>
@ -76,10 +83,17 @@ const SideNavComponent = ({disabled,
</NavLink>
</li>
<li>
<NavLink to={Routes.StartOverPage} className={getNavLinkClass()}>
<Button variant={null} className={'island-reset-button'}
onClick={() => setShowResetModal(true)} href='#'>
<span className='number'><FontAwesomeIcon icon={faUndo} style={{'marginLeft': '-1px'}}/></span>
Reset
</NavLink>
</Button>
<IslandResetModal show={showResetModal}
allMonkeysAreDead={areMonkeysDead()}
onClose={() => {
setShowResetModal(false);
onStatusChange();
}}/>
</li>
</ul>
@ -90,7 +104,7 @@ const SideNavComponent = ({disabled,
Configuration
</NavLink></li>
<li><NavLink to='/infection/telemetry'
className={getNavLinkClass()}>
className={getNavLinkClass()}>
Telemetries
</NavLink></li>
</ul>
@ -98,8 +112,12 @@ const SideNavComponent = ({disabled,
<Logo/>
</>);
function areMonkeysDead() {
return (!completedSteps['runMonkey']) || (completedSteps['infectionDone'])
}
function getNavLinkClass() {
if(disabled){
if (disabled) {
return `nav-link disabled`
} else {
return ''

View File

@ -9,6 +9,7 @@ const SidebarLayoutComponent = ({component: Component,
completedSteps = null,
defaultReport = '',
sideNavHeader = (<></>),
onStatusChange = () => {},
...other
}) => (
<Route {...other} render={() => {
@ -18,9 +19,10 @@ const SidebarLayoutComponent = ({component: Component,
<SideNavComponent disabled={sideNavDisabled}
completedSteps={completedSteps}
defaultReport={defaultReport}
header={sideNavHeader}/>
header={sideNavHeader}
onStatusChange={onStatusChange}/>
</Col>}
<Component {...other} />
<Component onStatusChange={onStatusChange} {...other} />
</Row>)
}}/>
)

View File

@ -12,8 +12,11 @@ import Logo from "../logo/LogoComponent";
const monkeyIcon = require('../../images/monkey-icon.svg')
const infectionMonkey = require('../../images/infection-monkey.svg')
const LandingPageComponent = (props) => {
type Props = {
onStatusChange: () => void
}
const LandingPageComponent = (props: Props) => {
return (
<>
<ParticleBackground/>

View File

@ -1,104 +0,0 @@
import React from 'react';
import {Col, Button} from 'react-bootstrap';
import {Link} from 'react-router-dom';
import AuthComponent from '../AuthComponent';
import IslandResetModal from '../ui-components/IslandResetModal';
import '../../styles/pages/StartOverPage.scss';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faInfoCircle} from '@fortawesome/free-solid-svg-icons/faInfoCircle';
import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck';
class ResetPageComponent extends AuthComponent {
constructor(props) {
super(props);
this.state = {
cleaned: false,
showCleanDialog: false,
allMonkeysAreDead: false
};
this.cleanup = this.cleanup.bind(this);
this.closeModal = this.closeModal.bind(this);
}
updateMonkeysRunning = () => {
this.authFetch('/api')
.then(res => res.json())
.then(res => {
// This check is used to prevent unnecessary re-rendering
this.setState({
allMonkeysAreDead: (!res['completed_steps']['run_monkey']) || (res['completed_steps']['infection_done'])
});
});
};
render() {
return (
<Col sm={{offset: 3, span: 9}} md={{offset: 3, span: 9}}
lg={{offset: 3, span: 9}} xl={{offset: 2, span: 7}}
className={'main'}>
<IslandResetModal cleaned={this.state.cleaned}
showCleanDialog={this.state.showCleanDialog}
allMonkeysAreDead={this.state.allMonkeysAreDead}
onVerify={this.cleanup}
onClose={this.closeModal}/>
<h1 className="page-title">Reset</h1>
<div style={{'fontSize': '1.2em'}}>
<p>
If you are finished and want to start over with a fresh configuration, erase the logs and clear the map
you can go ahead and
</p>
<p style={{margin: '20px'}} className={'text-center'}>
<Button className="btn btn-danger btn-lg center-block"
onClick={() => {
this.setState({showCleanDialog: true});
this.updateMonkeysRunning();
}
}>
Reset the Environment
</Button>
</p>
<div className="alert alert-info">
<FontAwesomeIcon icon={faInfoCircle} style={{'marginRight': '5px'}}/>
You don't have to reset the environment to keep running monkeys.
You can continue and <Link to="/run-monkey">Run More Monkeys</Link> as you wish,
and see the results on the <Link to="/infection/map">Infection Map</Link> without deleting anything.
</div>
{this.state.cleaned ?
<div className="alert alert-success">
<FontAwesomeIcon icon={faCheck} style={{'marginRight': '5px'}}/>
Environment was reset successfully
</div>
: ''}
</div>
</Col>
);
}
cleanup = () => {
this.setState({
cleaned: false
});
return this.authFetch('/api?action=reset')
.then(res => res.json())
.then(res => {
if (res['status'] === 'OK') {
this.setState({
cleaned: true
});
}
}).then(() => {
this.updateMonkeysRunning();
this.props.onStatusChange();
});
};
closeModal = () => {
this.setState({
showCleanDialog: false
})
};
}
export default ResetPageComponent;

View File

@ -1,76 +0,0 @@
import {Modal} from 'react-bootstrap';
import React from 'react';
import {GridLoader} from 'react-spinners';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons/faExclamationTriangle';
class IslandResetModal extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
showCleanDialog: this.props.showCleanDialog,
allMonkeysAreDead: this.props.allMonkeysAreDead,
loading: false
};
}
componentDidUpdate(prevProps) {
if (this.props !== prevProps) {
this.setState({ showCleanDialog: this.props.showCleanDialog,
allMonkeysAreDead: this.props.allMonkeysAreDead})
}
}
render = () => {
return (
<Modal show={this.state.showCleanDialog} onHide={() => this.props.onClose()}>
<Modal.Body>
<h2>
<div className='text-center'>Reset environment</div>
</h2>
<p style={{'fontSize': '1.2em', 'marginBottom': '2em'}}>
Are you sure you want to reset the environment?
</p>
{
!this.state.allMonkeysAreDead ?
<div className='alert alert-warning'>
<FontAwesomeIcon icon={faExclamationTriangle} style={{'marginRight': '5px'}}/>
Some monkeys are still running. It's advised to kill all monkeys before resetting.
</div>
:
<div/>
}
{
this.state.loading ? <div className={'modalLoader'}><GridLoader/></div> : this.showModalButtons()
}
</Modal.Body>
</Modal>
)
};
showModalButtons() {
return (<div className='text-center'>
<button type='button' className='btn btn-danger btn-lg' style={{margin: '5px'}}
onClick={this.modalVerificationOnClick}>
Reset environment
</button>
<button type='button' className='btn btn-success btn-lg' style={{margin: '5px'}}
onClick={() => {this.props.onClose(); this.setState({showCleanDialog: false})}}>
Cancel
</button>
</div>)
}
modalVerificationOnClick = async () => {
this.setState({loading: true});
this.props.onVerify()
.then(() => {this.setState({loading: false});
this.props.onClose();})
}
}
export default IslandResetModal;

View File

@ -0,0 +1,130 @@
import {Button, Col, Container, Modal, NavLink, Row} from 'react-bootstrap';
import React, {useState} from 'react';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons/faExclamationTriangle';
import '../../styles/components/IslandResetModal.scss';
import {Routes} from '../Main';
import LoadingIcon from './LoadingIcon';
import {faCheck} from '@fortawesome/free-solid-svg-icons';
import AuthService from '../../services/AuthService';
type Props = {
show: boolean,
allMonkeysAreDead: boolean,
onClose: () => void
}
// Button statuses
const Idle = 1;
const Loading = 2;
const Done = 3;
const IslandResetModal = (props: Props) => {
const [resetAllStatus, setResetAll] = useState(Idle);
const [deleteStatus, setDeleteStatus] = useState(Idle);
const auth = new AuthService();
return (
<Modal show={props.show} onHide={() => {
setDeleteStatus(Idle);
props.onClose()
}} size={'lg'}>
<Modal.Header closeButton>
<Modal.Title>Reset the Island</Modal.Title>
</Modal.Header>
<Modal.Body>
{
!props.allMonkeysAreDead ?
<div className='alert alert-warning'>
<FontAwesomeIcon icon={faExclamationTriangle} style={{'marginRight': '5px'}}/>
Can't reset the Island while Monkey agents are still running!
</div>
:
showModalButtons()
}
</Modal.Body>
</Modal>
)
function displayDeleteData() {
if (deleteStatus === Idle) {
return (
<button type='button' className='btn btn-danger btn-lg' style={{margin: '5px'}}
onClick={() => {
setDeleteStatus(Loading);
resetIsland('/api?action=delete-agent-data',
() => {
setDeleteStatus(Done)
})
}}>
Delete data
</button>
)
} else if (deleteStatus === Loading) {
return (<LoadingIcon/>)
} else if (deleteStatus === Done) {
return (<FontAwesomeIcon icon={faCheck} className={'status-success'} size={'2x'}/>)
}
}
function displayResetAll() {
if (resetAllStatus === Idle) {
return (
<button type='button' className='btn btn-danger btn-lg' style={{margin: '5px'}}
onClick={() => {
setResetAll(Loading);
resetIsland('/api?action=reset',
() => {
setResetAll(Done);
props.onClose();
})
}}>
Reset the Island
</button>
)
} else if (resetAllStatus === Loading) {
return (<LoadingIcon/>)
} else if (resetAllStatus === Done) {
return (<FontAwesomeIcon icon={faCheck} className={'status-success'} size={'2x'}/>)
}
}
function resetIsland(url: string, callback: () => void) {
auth.authFetch(url)
.then(res => res.json())
.then(res => {
if (res['status'] === 'OK') {
callback()
}
})
}
function showModalButtons() {
return (<Container className={`text-left island-reset-modal`}>
<Row>
<Col>
<p>Delete data gathered by Monkey agents.</p>
<p>This will reset the Map and reports.</p>
</Col>
<Col sm={4} className={'text-center'}>
{displayDeleteData()}
</Col>
</Row>
<hr/>
<Row>
<Col>
<p>Reset everything.</p>
<p>You might want to <Button variant={'link'} href={Routes.ConfigurePage}>export
configuration</Button> before doing this.</p>
</Col>
<Col sm={4} className={'text-center'}>
{displayResetAll()}
</Col>
</Row>
</Container>)
}
}
export default IslandResetModal;

View File

@ -0,0 +1,9 @@
.island-reset-modal p {
font-size: 16.5px;
margin: 0;
padding-left: 5px;
}
.island-reset-modal a {
padding: 0 0 4px 0;
}

View File

@ -1,6 +1,13 @@
.sidebar .version-text {
position: relative;
}
.sidebar .license-link {
position: relative;
}
.navigation .island-reset-button {
width: 100%;
text-align: left;
padding-left: 18px;
}

View File

@ -1,9 +0,0 @@
$yellow: #ffcc00;
.modalLoader div{
margin-left: auto;
margin-right: auto;
}
.modalLoader div>div{
background-color: $yellow;
}