forked from p15670423/monkey
ui improvements post review
This commit is contained in:
parent
f0c43f8bb5
commit
0846258bbd
|
@ -1,8 +1,6 @@
|
|||
from bson import ObjectId
|
||||
from flask import request
|
||||
import flask_restful
|
||||
|
||||
from cc.database import mongo
|
||||
from cc.services.edge import EdgeService
|
||||
|
||||
__author__ = 'Barak'
|
||||
|
@ -11,7 +9,6 @@ __author__ = 'Barak'
|
|||
class Edge(flask_restful.Resource):
|
||||
def get(self):
|
||||
edge_id = request.args.get('id')
|
||||
|
||||
if edge_id:
|
||||
return {"edge": EdgeService.get_displayed_edge_by_id(edge_id)}
|
||||
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
from bson import ObjectId
|
||||
from flask import request
|
||||
import flask_restful
|
||||
|
||||
from cc.database import mongo
|
||||
from cc.services.edge import EdgeService
|
||||
from cc.services.node import NodeService
|
||||
|
||||
__author__ = 'Barak'
|
||||
|
|
|
@ -169,11 +169,11 @@ class EdgeService:
|
|||
|
||||
@staticmethod
|
||||
def get_edge_group(edge):
|
||||
if edge["exploited"]:
|
||||
if edge.get("exploited"):
|
||||
return "exploited"
|
||||
if edge["tunnel"]:
|
||||
if edge.get("tunnel"):
|
||||
return "tunnel"
|
||||
if (len(edge["scans"]) > 0) or (len(edge["exploits"]) > 0):
|
||||
if (len(edge.get("scans", [])) > 0) or (len(edge.get("exploits", [])) > 0):
|
||||
return "scan"
|
||||
return "empty"
|
||||
|
||||
|
|
|
@ -101,10 +101,7 @@ class NodeService:
|
|||
|
||||
@staticmethod
|
||||
def get_node_group(node):
|
||||
if node["exploited"]:
|
||||
return "exploited"
|
||||
else:
|
||||
return "clean"
|
||||
return "exploited" if node.get("exploited") else "clean"
|
||||
|
||||
@staticmethod
|
||||
def monkey_to_net_node(monkey):
|
||||
|
|
|
@ -70,8 +70,6 @@
|
|||
"react-bootstrap": "^0.31.2",
|
||||
"react-copy-to-clipboard": "^5.0.0",
|
||||
"react-data-components": "^1.1.1",
|
||||
"react-data-grid": "^2.0.58",
|
||||
"react-data-grid-addons": "^2.0.58",
|
||||
"react-dom": "^15.6.1",
|
||||
"react-fa": "^4.2.0",
|
||||
"react-graph-vis": "^0.1.3",
|
||||
|
|
|
@ -14,6 +14,7 @@ require('react-data-components/css/table-twbs.css');
|
|||
require('styles/App.css');
|
||||
|
||||
let logoImage = require('../images/monkey-logo.png');
|
||||
let guardicoreLogoImage = require('../images/guardicore-logo.png');
|
||||
|
||||
class AppComponent extends React.Component {
|
||||
render() {
|
||||
|
@ -24,52 +25,56 @@ class AppComponent extends React.Component {
|
|||
<Col sm={3} md={2} className="sidebar">
|
||||
<div className="header">
|
||||
<img src={logoImage} alt="Infection Monkey"/>
|
||||
by GuardiCore
|
||||
</div>
|
||||
|
||||
<ul className="navigation">
|
||||
<li>
|
||||
<NavLink to="/" exact={true}>
|
||||
<span className="number">1.</span>
|
||||
Run Server
|
||||
Run C&C Server
|
||||
<Icon name="check" className="pull-right checkmark text-success"/>
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink to="/configure">
|
||||
<span className="number">2.</span>
|
||||
Configure
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink to="/run-monkey">
|
||||
<span className="number">3.</span>
|
||||
<span className="number">2.</span>
|
||||
Run Monkey
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<a className="disabled">
|
||||
<span className="number">4.</span>
|
||||
Infection
|
||||
</a>
|
||||
<ul>
|
||||
<li><NavLink to="/infection/map">Map</NavLink></li>
|
||||
<li><NavLink to="/infection/logs">Full Logs</NavLink></li>
|
||||
</ul>
|
||||
<NavLink to="/infection/map">
|
||||
<span className="number">3.</span>
|
||||
Infection Map
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink to="/report">
|
||||
<span className="number">5.</span>
|
||||
<span className="number">4.</span>
|
||||
Pen. Test Report
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink to="/start-over">
|
||||
<span className="number">5.</span>
|
||||
Start Over
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<hr/>
|
||||
<ul>
|
||||
<li><a>Clear DB</a></li>
|
||||
<li><a>Kill All Monkeys</a></li>
|
||||
<li><NavLink to="/configure">Configuration</NavLink></li>
|
||||
<li><NavLink to="/infection/logs">Monkey Telemetry</NavLink></li>
|
||||
</ul>
|
||||
|
||||
<hr/>
|
||||
<div className="guardicore-link">
|
||||
<span>Powered by</span>
|
||||
<a href="http://www.guardicore.com" target="_blank">
|
||||
<img src={guardicoreLogoImage} alt="GuardiCore"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</Col>
|
||||
<Col sm={9} md={10} smOffset={3} mdOffset={2} className="main">
|
||||
<Route exact path="/" component={RunServerPage}/>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import Form from 'react-jsonschema-form';
|
||||
import {Col} from 'react-bootstrap';
|
||||
import {Col, Nav, NavItem} from 'react-bootstrap';
|
||||
|
||||
class ConfigurePageComponent extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -47,9 +47,9 @@ class ConfigurePageComponent extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
setSelectedSection = (event) => {
|
||||
setSelectedSection = (key) => {
|
||||
this.setState({
|
||||
selectedSection: event.target.value
|
||||
selectedSection: key
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -57,17 +57,14 @@ class ConfigurePageComponent extends React.Component {
|
|||
return (
|
||||
<Col xs={8}>
|
||||
<h1 className="page-title">Monkey Configuration</h1>
|
||||
<div className="alert alert-info">
|
||||
<i className="glyphicon glyphicon-info-sign" style={{'marginRight': '5px'}}/>
|
||||
This configuration will only apply on new infections.
|
||||
</div>
|
||||
|
||||
<select value={this.state.selectedSection} onChange={this.setSelectedSection}
|
||||
className="form-control input-lg" style={{'margin-bottom': '1em'}}>
|
||||
<Nav bsStyle="tabs" justified
|
||||
activeKey={this.state.selectedSection} onSelect={this.setSelectedSection}
|
||||
style={{'marginBottom': '2em'}}>
|
||||
{this.state.sections.map(section =>
|
||||
<option value={section.key}>{section.title}</option>
|
||||
<NavItem key={section.key} eventKey={section.key}>{section.title}</NavItem>
|
||||
)}
|
||||
</select>
|
||||
</Nav>
|
||||
|
||||
{ this.state.selectedSection ?
|
||||
<Form schema={this.state.schema.properties[this.state.selectedSection]}
|
||||
|
@ -75,6 +72,11 @@ class ConfigurePageComponent extends React.Component {
|
|||
onSubmit={this.onSubmit}/>
|
||||
: ''}
|
||||
|
||||
<div className="alert alert-info">
|
||||
<i className="glyphicon glyphicon-info-sign" style={{'marginRight': '5px'}}/>
|
||||
This configuration will only apply to new infections.
|
||||
</div>
|
||||
|
||||
{ this.state.saved ?
|
||||
<p>Configuration saved successfully.</p>
|
||||
: ''}
|
||||
|
|
|
@ -3,13 +3,13 @@ import {Col} from 'react-bootstrap';
|
|||
import JSONTree from 'react-json-tree'
|
||||
import {DataTable} from 'react-data-components';
|
||||
|
||||
const renderJson = (val, row) => <JSONTree data={val} level={1} theme="eighties" invertTheme={true} />;
|
||||
const renderTime = (val, row) => val.split('.')[0];
|
||||
const renderJson = (val) => <JSONTree data={val} level={1} theme="eighties" invertTheme={true} />;
|
||||
const renderTime = (val) => val.split('.')[0];
|
||||
|
||||
const columns = [
|
||||
{ title: 'Type', prop: 'telem_type' },
|
||||
{ title: 'Monkey ID', prop: 'monkey_guid' },
|
||||
{ title: 'Time', prop: 'timestamp', render: renderTime},
|
||||
{ title: 'Monkey ID', prop: 'monkey_guid' },
|
||||
{ title: 'Type', prop: 'telem_type' },
|
||||
{ title: 'More Info', prop: 'data', render: renderJson, width: '40%' }
|
||||
];
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react';
|
||||
import {Col} from 'react-bootstrap';
|
||||
import Graph from 'react-graph-vis';
|
||||
import {Icon} from 'react-fa'
|
||||
|
||||
import PreviewPane from 'components/preview-pane/PreviewPane';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {Icon} from 'react-fa';
|
||||
|
||||
let options = {
|
||||
layout: {
|
||||
|
@ -44,7 +45,9 @@ class MapPageComponent extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
graph: {nodes: [], edges: []}
|
||||
graph: {nodes: [], edges: []},
|
||||
selected: null,
|
||||
selectedType: null
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -60,13 +63,19 @@ class MapPageComponent extends React.Component {
|
|||
|
||||
selectionChanged(event) {
|
||||
if (event.nodes.length === 1) {
|
||||
console.log('selected node:', event.nodes[0]);
|
||||
console.log('selected node:', event.nodes[0]); // eslint-disable-line no-console
|
||||
fetch('/api/netmap/node?id='+event.nodes[0])
|
||||
.then(res => res.json())
|
||||
.then(res => this.setState({selected: res, selectedType: 'node'}));
|
||||
}
|
||||
else if (event.edges.length === 1) {
|
||||
console.log('selected edge:', event.edges[0]);
|
||||
fetch('/api/netmap/edge?id='+event.edges[0])
|
||||
.then(res => res.json())
|
||||
.then(res => this.setState({selected: res.edge, selectedType: 'edge'}));
|
||||
}
|
||||
else {
|
||||
console.log('no preview.');
|
||||
console.log('selection cleared.'); // eslint-disable-line no-console
|
||||
this.setState({selected: null, selectedType: null});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,44 +86,21 @@ class MapPageComponent extends React.Component {
|
|||
<h1 className="page-title">Infection Map</h1>
|
||||
</Col>
|
||||
<Col xs={8}>
|
||||
<div className="pull-left">
|
||||
<input placeholder="Search" />
|
||||
</div>
|
||||
<Graph graph={this.state.graph} options={options} events={this.events}/>
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
<div className="panel panel-default preview">
|
||||
<div className="panel-heading">
|
||||
<h3>
|
||||
<Icon name="fire"/>
|
||||
vm4
|
||||
<small>Infected Asset</small>
|
||||
</h3>
|
||||
</div>
|
||||
<input className="form-control input-block"
|
||||
placeholder="Find on map"
|
||||
style={{'marginBottom': '1em'}}/>
|
||||
|
||||
<div className="panel-body">
|
||||
<h4>Machine Info</h4>
|
||||
<p>...</p>
|
||||
<PreviewPane item={this.state.selected} type={this.state.selectedType} />
|
||||
|
||||
<h4 style={{'marginTop': '2em'}}>Exploit Method</h4>
|
||||
<p>...</p>
|
||||
|
||||
<h4 style={{'marginTop': '2em'}}>Timeline</h4>
|
||||
<ul className="timeline">
|
||||
<li>
|
||||
<div className="bullet"></div>
|
||||
failed attempt1
|
||||
</li>
|
||||
<li>
|
||||
<div className="bullet"></div>
|
||||
failed attempt2
|
||||
</li>
|
||||
<li>
|
||||
<div className="bullet bad"></div>
|
||||
Infection!
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<Link to="/infection/logs" className="btn btn-default btn-block" style={{'marginBottom': '0.5em'}}>Monkey Telemetry</Link>
|
||||
<button onClick={this.killAllMonkeys} className="btn btn-danger btn-block">
|
||||
<Icon name="stop-circle" style={{'marginRight': '0.5em'}}></Icon>
|
||||
Kill All Monkeys
|
||||
</button>
|
||||
</div>
|
||||
</Col>
|
||||
</div>
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import {Button, Col, Well} from 'react-bootstrap';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import {Icon} from 'react-fa';
|
||||
import {Link} from "react-router-dom";
|
||||
|
||||
class RunMonkeyPageComponent extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -9,7 +10,8 @@ class RunMonkeyPageComponent extends React.Component {
|
|||
this.state = {
|
||||
ips: [],
|
||||
selectedIp: '0.0.0.0',
|
||||
isRunning: true
|
||||
isRunningOnIsland: false,
|
||||
isRunningLocally: false
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -17,14 +19,13 @@ class RunMonkeyPageComponent extends React.Component {
|
|||
fetch('/api')
|
||||
.then(res => res.json())
|
||||
.then(res => this.setState({
|
||||
ips: res['ip_addresses'],
|
||||
selectedIp: res['ip_addresses'][0]
|
||||
ips: res['ip_addresses']
|
||||
}));
|
||||
|
||||
fetch('/api/local-monkey')
|
||||
.then(res => res.json())
|
||||
.then(res => this.setState({
|
||||
isRunning: res['is_running']
|
||||
isRunningOnIsland: res['is_running']
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -37,50 +38,64 @@ class RunMonkeyPageComponent extends React.Component {
|
|||
{
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({action: 'run', ip: this.state.selectedIp})
|
||||
body: JSON.stringify({action: 'run'})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
this.setState({
|
||||
isRunning: res['is_running']
|
||||
isRunningOnIsland: res['is_running']
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
setSelectedIp = (event) => {
|
||||
this.setState({selectedIp: event.target.value});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Col xs={8}>
|
||||
<h1 className="page-title">Run the Monkey</h1>
|
||||
<p>
|
||||
Select one of the server's IP addresses:
|
||||
<select value={this.state.selectedIp} onChange={this.setSelectedIp}
|
||||
className="form-control inline-select">
|
||||
{this.state.ips.map(ip =>
|
||||
<option value={ip}>{ip}</option>
|
||||
)}
|
||||
</select>
|
||||
<br/>That address will be used as the monkey's C&C address.
|
||||
<p style={{'fontSize': '1.2em', 'marginBottom': '2em'}}>
|
||||
You can run the monkey on the C&C server, on your local machine and basically everywhere.
|
||||
The more the merrier 😄
|
||||
</p>
|
||||
<p>Run this snippet on a host for manually infecting it with a Monkey:</p>
|
||||
<Well>
|
||||
<CopyToClipboard text={this.generateCmd(this.state.selectedIp)} className="pull-right">
|
||||
<Button style={{margin: '-0.5em'}} title="Copy to Clipboard">
|
||||
<Icon name="clipboard"/>
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
<code>{this.generateCmd(this.state.selectedIp)}</code>
|
||||
</Well>
|
||||
<p>
|
||||
Or simply click here to <a onClick={this.runLocalMonkey}
|
||||
className="btn btn-default btn-sm"
|
||||
style={{'marginLeft': '0.2em'}}>Run on <b>{this.state.selectedIp}</b></a>
|
||||
{ this.state.isRunning ?
|
||||
<i className="text-success" style={{'marginLeft': '5px'}}>Running...</i>
|
||||
: ''}
|
||||
<p style={{'marginBottom': '2em'}}>
|
||||
<button onClick={this.runLocalMonkey}
|
||||
className="btn btn-default"
|
||||
disabled={this.state.isRunningOnIsland}>
|
||||
Run on C&C Server
|
||||
{ !this.state.isRunningOnIsland ?
|
||||
<Icon name="check" className="text-success" style={{'marginLeft': '5px'}}/>
|
||||
: ''}
|
||||
</button>
|
||||
<a href="/download-monkey"
|
||||
className="btn btn-default"
|
||||
disabled={this.state.isRunningLocally}
|
||||
style={{'margin-left': '1em'}}>
|
||||
Download and run locally
|
||||
{ !this.state.isRunningLocally ?
|
||||
<Icon name="check" className="text-success" style={{'marginLeft': '5px'}}/>
|
||||
: ''}
|
||||
</a>
|
||||
</p>
|
||||
<div className="run-monkey-snippets" style={{'marginBottom': '3em'}}>
|
||||
<p>
|
||||
Run one of those snippets on a host for infecting it with a Monkey:
|
||||
<br/>
|
||||
<span className="text-muted">(The IP address is used as the monkey's C&C address)</span>
|
||||
</p>
|
||||
<Well className="well-sm">
|
||||
{this.state.ips.map(ip =>
|
||||
<div style={{'overflow': 'auto', 'padding': '0.5em'}}>
|
||||
<CopyToClipboard text={this.generateCmd(ip)} className="pull-right btn-sm">
|
||||
<Button style={{margin: '-0.5em'}} title="Copy to Clipboard">
|
||||
<Icon name="clipboard"/>
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
<code>{this.generateCmd(ip)}</code>
|
||||
</div>
|
||||
)}
|
||||
</Well>
|
||||
</div>
|
||||
<p style={{'fontSize': '1.2em'}}>
|
||||
Go ahead and monitor the ongoing infection in the <Link to="/infection/map">Infection Map</Link> view.
|
||||
</p>
|
||||
</Col>
|
||||
);
|
||||
|
|
|
@ -21,7 +21,10 @@ class RunServerPageComponent extends React.Component {
|
|||
<div style={{'fontSize': '1.5em'}}>
|
||||
<p>Your Monkey Island server is up and running on <b>{this.state.ip}</b> 👏 👏</p>
|
||||
<p>
|
||||
Now <Link to="/configure">configure the monkey</Link> (or just stick with the default configuration) and <Link to="/run-monkey">run the monkey</Link>.
|
||||
Go ahead and <Link to="/run-monkey">run the monkey</Link>.
|
||||
</p>
|
||||
<p style={{'marginTop': '30px'}}>
|
||||
<i>(You can make further adjustments by <Link to="/configure">configuring the monkey</Link>)</i>
|
||||
</p>
|
||||
</div>
|
||||
</Col>
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
import React from 'react';
|
||||
import {Icon} from "react-fa";
|
||||
|
||||
class PreviewPaneComponent extends React.Component {
|
||||
assetInfo(asset) {
|
||||
return (
|
||||
<div>
|
||||
<table className="table table-condensed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Operating System</th>
|
||||
<td>{asset.os}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>IP Addresses</th>
|
||||
<td>{asset.ip_addresses.map(val => <div key={val}>{val}</div>)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Services</th>
|
||||
<td>{asset.services.map(val => <div key={val}>{val}</div>)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Accessible From</th>
|
||||
<td>{asset.accessible_from_nodes.map(val => <div key={val.id}>{val.id}</div>)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
infectedAssetInfo(asset) {
|
||||
return (
|
||||
<div>
|
||||
{this.assetInfo(asset)}
|
||||
|
||||
<h4 style={{'marginTop': '2em'}}>Timeline</h4>
|
||||
<ul className="timeline">
|
||||
{ asset.exploits.map(exploit =>
|
||||
<li key={exploit.start_timestamp}>
|
||||
<div className={'bullet ' + (exploit.result ? 'bad' : '')}></div>
|
||||
<div>{exploit.start_timestamp}</div>
|
||||
<div>{exploit.origin}</div>
|
||||
<div>{exploit.exploiter}</div>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
infectionInfo(edge) {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
scanInfo(edge) {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let info = null;
|
||||
switch (this.props.type) {
|
||||
case 'edge':
|
||||
info = this.props.item.exploits.length ?
|
||||
this.infectionInfo(this.props.item) : this.scanInfo(this.props.item);
|
||||
case 'node':
|
||||
info = this.props.item.exploits.some(exploit => exploit.result) ?
|
||||
this.infectedAssetInfo(this.props.item) : this.assetInfo(this.props.item);
|
||||
}
|
||||
return (
|
||||
<div className="preview-pane">
|
||||
{ !this.props.item ?
|
||||
<span>
|
||||
<Icon name="hand-o-left" style={{'marginRight': '0.5em'}}></Icon>
|
||||
Select an item on the map for a preview
|
||||
</span>
|
||||
:
|
||||
<div>
|
||||
<h3>
|
||||
<b>{this.props.item.label}</b>
|
||||
</h3>
|
||||
|
||||
<hr/>
|
||||
{info}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PreviewPaneComponent;
|
Binary file not shown.
After Width: | Height: | Size: 9.6 KiB |
|
@ -91,6 +91,22 @@ body {
|
|||
|
||||
li .checkmark {
|
||||
font-size: 1.3em;
|
||||
margin-right: -15px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-top-color: #ccc !important;
|
||||
}
|
||||
|
||||
.guardicore-link span {
|
||||
color: #999;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.guardicore-link img {
|
||||
height: 24px;
|
||||
margin-left: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -162,6 +178,10 @@ body {
|
|||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.run-monkey-snippets .well {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Map Preview Pane
|
||||
*/
|
||||
|
@ -172,10 +192,16 @@ body {
|
|||
border-bottom: 3px solid #e8e8e8;
|
||||
padding: 1.5em 1em;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.preview-pane hr {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.preview-pane h3 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
|
||||
}
|
||||
.preview-pane h3 small {
|
||||
|
@ -191,6 +217,15 @@ body {
|
|||
font-size: 1em;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.preview-pane .table tr:first-child th , .preview-pane .table tr:first-child td {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.preview-pane .table th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.preview-pane p, .preview-pane .timeline {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
@ -220,6 +255,7 @@ body {
|
|||
|
||||
.timeline .bullet {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #ccc;
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
|
@ -247,8 +283,16 @@ body {
|
|||
padding: 15px 8px;
|
||||
}
|
||||
|
||||
#search-field , #page-menu {
|
||||
.data-table-container > .container > .row:first-child > div:first-child > div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.data-table-container > .container > .row:first-child > div:first-child > div:last-child {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
#search-field , #page-menu {
|
||||
margin-left: 0.5em;
|
||||
margin-bottom: 1em;
|
||||
height: 34px;
|
||||
padding: 6px 12px;
|
||||
|
@ -265,3 +309,11 @@ body {
|
|||
-o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||
}
|
||||
|
||||
#search-field {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.data-table-container .pagination {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue