Merge branch '400-zero-trust-mvp-venn-diagram' of https://github.com/guardicore/monkey into 400-zero-trust-mvp-venn-diagram

This commit is contained in:
Shay Nehmad 2019-08-25 18:08:43 +03:00
commit 05eab34d45
11 changed files with 447 additions and 538 deletions

View File

@ -1,17 +1,64 @@
import React, {Fragment} from 'react'; import React from 'react';
import {Col, Grid, Row} from 'react-bootstrap'; import {Button, Col} from 'react-bootstrap';
import AuthComponent from '../AuthComponent'; import AuthComponent from '../AuthComponent';
import ReportHeader, {ReportTypes} from "../report-components/common/ReportHeader"; import ReportHeader, {ReportTypes} from "../report-components/common/ReportHeader";
import PillarsOverview from "../report-components/zerotrust/PillarOverview"; import PillarGrades from "../report-components/zerotrust/PillarGrades";
import PillarLabel from "../report-components/zerotrust/PillarLabel";
import ResponsiveVennDiagram from "../report-components/zerotrust/venn-components/ResponsiveVennDiagram";
import FindingsTable from "../report-components/zerotrust/FindingsTable"; import FindingsTable from "../report-components/zerotrust/FindingsTable";
import SinglePillarDirectivesStatus from "../report-components/zerotrust/SinglePillarDirectivesStatus"; import {SinglePillarRecommendationsStatus} from "../report-components/zerotrust/SinglePillarRecommendationsStatus";
import MonkeysStillAliveWarning from "../report-components/common/MonkeysStillAliveWarning";
import ReportLoader from "../report-components/common/ReportLoader"; let mockup = [
import MustRunMonkeyWarning from "../report-components/common/MustRunMonkeyWarning"; {
import SecurityIssuesGlance from "../report-components/common/SecurityIssuesGlance"; "Conclusive": 4,
import StatusesToPillarsSummary from "../report-components/zerotrust/StatusesToPillarsSummary"; "Inconclusive": 0,
import PrintReportButton from "../report-components/common/PrintReportButton"; "Positive": 1,
import {extractExecutionStatusFromServerResponse} from "../report-components/common/ExecutionStatus"; "Unexecuted": 2,
"pillar": "Data"
},
{
"Conclusive": 0,
"Inconclusive": 5,
"Positive": 0,
"Unexecuted": 2,
"pillar": "People"
},
{
"Conclusive": 0,
"Inconclusive": 0,
"Positive": 6,
"Unexecuted": 3,
"pillar": "Networks"
},
{
"Conclusive": 2,
"Inconclusive": 0,
"Positive": 1,
"Unexecuted": 1,
"pillar": "Devices"
},
{
"Conclusive": 0,
"Inconclusive": 0,
"Positive": 0,
"Unexecuted": 0,
"pillar": "Workloads"
},
{
"Conclusive": 0,
"Inconclusive": 2,
"Positive": 0,
"Unexecuted": 0,
"pillar": "Visibility & Analytics"
},
{
"Conclusive": 0,
"Inconclusive": 0,
"Positive": 0,
"Unexecuted": 0,
"pillar": "Automation & Orchestration"
}
];
class ZeroTrustReportPageComponent extends AuthComponent { class ZeroTrustReportPageComponent extends AuthComponent {
@ -20,30 +67,16 @@ class ZeroTrustReportPageComponent extends AuthComponent {
this.state = { this.state = {
allMonkeysAreDead: false, allMonkeysAreDead: false,
runStarted: true runStarted: false
}; };
} }
componentDidMount() {
this.updateMonkeysRunning().then(res => this.getZeroTrustReportFromServer(res));
}
updateMonkeysRunning = () => {
return this.authFetch('/api')
.then(res => res.json())
.then(res => {
this.setState(extractExecutionStatusFromServerResponse(res));
return res;
});
};
render() { render() {
let content; let res;
if (this.state.runStarted) { // Todo move to componentDidMount
content = this.generateReportContent(); this.getZeroTrustReportFromServer(res);
} else {
content = <MustRunMonkeyWarning/>; const content = this.generateReportContent();
}
return ( return (
<Col xs={12} lg={10}> <Col xs={12} lg={10}>
@ -59,75 +92,63 @@ class ZeroTrustReportPageComponent extends AuthComponent {
let content; let content;
if (this.stillLoadingDataFromServer()) { if (this.stillLoadingDataFromServer()) {
content = <ReportLoader loading={true}/>; content = "Still empty";
} else { } else {
content = <div id="MainContentSection"> const pillarsSection = <div>
{this.generateOverviewSection()} <h2>Pillars Overview</h2>
{this.generateDirectivesSection()} <PillarGrades pillars={this.state.pillars}/>
{this.generateFindingsSection()} </div>;
const recommendationsSection = <div><h2>Recommendations Status</h2>
{
this.state.recommendations.map((recommendation) =>
<SinglePillarRecommendationsStatus
key={recommendation.pillar}
pillar={recommendation.pillar}
recommendationStatus={recommendation.recommendationStatus}/>
)
}
</div>;
const findingSection = <div><h2>Findings</h2>
<FindingsTable findings={this.state.findings}/></div>;
content = <div>
{pillarsSection}
{recommendationsSection}
{findingSection}
</div>; </div>;
} }
return ( return (
<Fragment> <div>
<div style={{marginBottom: '20px'}}> <div className="text-center no-print" style={{marginBottom: '20px'}}>
<PrintReportButton onClick={() => {print();}} /> <Button bsSize="large" onClick={() => {
this.print();
}}><i className="glyphicon glyphicon-print"/> Print Report</Button>
</div> </div>
<div className="report-page"> <div className="report-page">
<ReportHeader report_type={ReportTypes.zeroTrust}/> <ReportHeader report_type={ReportTypes.zeroTrust}/>
<hr/> <hr/>
{content} {content}
<hr/>
<pre>{JSON.stringify(this.state.pillars, undefined, 2)}</pre>
<br/>
<ResponsiveVennDiagram pillarsGrades={mockup} />
<pre>{JSON.stringify(this.state.recommendations, undefined, 2)}</pre>
<br/>
<pre>{JSON.stringify(this.state.findings, undefined, 2)}</pre>
</div> </div>
<div style={{marginTop: '20px'}}> </div>
<PrintReportButton onClick={() => {print();}} />
</div>
</Fragment>
) )
} }
generateFindingsSection() {
return (<div id="findings-overview">
<h2>Findings</h2>
<FindingsTable pillarsToStatuses={this.state.pillars.pillarsToStatuses} findings={this.state.findings}/>
</div>);
}
generateDirectivesSection() {
return (<div id="directives-overview">
<h2>Directives</h2>
{
Object.keys(this.state.directives).map((pillar) =>
<SinglePillarDirectivesStatus
key={pillar}
pillar={pillar}
directivesStatus={this.state.directives[pillar]}
pillarsToStatuses={this.state.pillars.pillarsToStatuses}/>
)
}
</div>);
}
generateOverviewSection() {
return (<div id="overview-section">
<h2>Overview</h2>
<Grid fluid={true}>
<Row className="show-grid">
<Col xs={8} sm={8} md={8} lg={8}>
<PillarsOverview pillarsToStatuses={this.state.pillars.pillarsToStatuses}
grades={this.state.pillars.grades}/>
</Col>
<Col xs={4} sm={4} md={4} lg={4}>
<MonkeysStillAliveWarning allMonkeysAreDead={this.state.allMonkeysAreDead}/>
<SecurityIssuesGlance issuesFound={this.anyIssuesFound()}/>
<StatusesToPillarsSummary statusesToPillars={this.state.pillars.statusesToPillars}/>
</Col>
</Row>
</Grid>
</div>);
}
stillLoadingDataFromServer() { stillLoadingDataFromServer() {
return typeof this.state.findings === "undefined" || typeof this.state.pillars === "undefined" || typeof this.state.directives === "undefined"; return typeof this.state.findings === "undefined" || typeof this.state.pillars === "undefined" || typeof this.state.recommendations === "undefined";
}
print() {
alert("unimplemented");
} }
getZeroTrustReportFromServer() { getZeroTrustReportFromServer() {
@ -139,11 +160,11 @@ class ZeroTrustReportPageComponent extends AuthComponent {
findings: res findings: res
}); });
}); });
this.authFetch('/api/report/zero_trust/directives') this.authFetch('/api/report/zero_trust/recommendations')
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
this.setState({ this.setState({
directives: res recommendations: res
}); });
}); });
this.authFetch('/api/report/zero_trust/pillars') this.authFetch('/api/report/zero_trust/pillars')
@ -154,14 +175,6 @@ class ZeroTrustReportPageComponent extends AuthComponent {
}); });
}); });
} }
anyIssuesFound() {
const severe = function(finding) {
return (finding.status === "Conclusive" || finding.status === "Inconclusive");
};
return this.state.findings.some(severe);
}
} }
export default ZeroTrustReportPageComponent; export default ZeroTrustReportPageComponent;

View File

@ -1,46 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types';
import * as d3 from 'd3'
class ArcNode extends React.Component{
render() {
let {prefix, ref, index, data} = this.props;;
let arc = d3.arc().innerRadius(data.inner).outerRadius(data.outer).startAngle(0).endAngle(Math.PI * 2.0);
return (
<g transform={'rotate(180)'} id={data.node.pillar}>
<path
id={this._reactInternalFiber.key}
className={'arcNode'}
data-tooltip={data.tooltip}
d={arc()}
fill={data.hex}
/>
<text x={0} dy={data.fontStyle.size * 1.2} fontSize={data.fontStyle.size} textAnchor='middle' pointerEvents={'none'}>
<textPath href={'#' + this._reactInternalFiber.key} startOffset={'26.4%'}>
{data.label}
</textPath>
</text>
</g>
)
}
}
ArcNode.propTypes = {
prefix: PropTypes.string,
index: PropTypes.number,
data: PropTypes.object
}
export default ArcNode;

View File

@ -1,383 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types';
import Dimensions from 'react-dimensions'
import Tooltip from './Tooltip'
import CircularNode from './CircularNode'
import ArcNode from './ArcNode'
import { TypographicUtilities } from './utility.js'
import './index.css'
/*
TODO LIST
UPDATED [21.08.2019]
[-] SVG > ViewBox 0 0 512 512, so it would be responsive and scalable
[-] Add resize listener to ResponsiveVennDiagram wrapper
[-] I have noticed that you have PillarGrades at ZeroTrustReportPage, so how I can fetch the data out of it?
UPDATED [20.08.2019]
[x] I've seen that lots of D3 responsive examples are using 'wrapper' around the main component to
get parent container dimensions. And lister for resize.
So, here it's Responsive(VennDiagram)
If it doesn't work, I have another alternative:
import React, { useRef, useEffect, useState, useLayoutEffect } from 'react'
const ResponsiveVennDiagram = props => {
const minWidth = 256;
const targetRef = useRef();
const [dimensions, setDimensions] = useState({ width: 0, heigth: 0 });
let movement_timer = null;
const RESET_TIMEOUT = 100;
const checkForDimensionsUpdate = () => {
if (targetRef.current) {
setDimensions({
width: Math.min(targetRef.current.offsetWidth, targetRef.current.offsetHeight),
height: targetRef.current.offsetHeight
});
}
};
useLayoutEffect(() => { checkForDimensionsUpdate(); }, []);
window.addEventListener('resize', () => { clearInterval(movement_timer); movement_timer = setTimeout(checkForDimensionsUpdate, RESET_TIMEOUT); });
return (
<div ref={targetRef} }>
<VennDiagram pillarsGrades={props.pillarsGrades} width={Math.max(minWidth, dimensions.width)} height={Math.max(minWidth, dimensions.width)} />
</div>
);
};
ResponsiveVennDiagram.propTypes = {
pillarsGrades: PropTypes.array
}
export default ResponsiveVennDiagram;
While your diagram laout is squared, the VennDiaagram gets the min(width, height)
[x] Colours have been updated.
[x] String prototypes have been moved to '.utilities.js'
[x] I have use the prefix 'vennDiagram' for all children elements to prevent naming conflicts with other components
DOM objects.
[x] I have used PropTypes already a year ago on ThreeJS/ReactJS stack project.
[!] External callback is still on my list, can you make a mockup for external function which have to get pillar variable?
[!] Z-indices sorting on mouseover
Would be n my 21.08.2019 TODO list. With D3.JS it's an easy task, however would try do
make these z-soring without D3 framework.
UPDATED [20.08.2019]
[!] By now, there are three input props for VennDiagram component:
data, width and height.
[-] Since layout has to be hardcoded, it's driven by this.layout object.
There're many ways of setting circles/arc positions, now I'm using the
stright-forward one.
Usually, I am put all hardcoded params to external JSON file, i.e config.json.
Let me know if it's a good idea.
[-] Could rearange z-indecies for nodes on hover, so highlighted node would have highest z-index.
[-] If you want callback on click even, please provide additional info what are you expecting to pass
through this.
[!] I don't used to make lots of comments in my code, but treying to name everything in a way,
so third person could instantly get its job and concept.
If it is not enoough just let me know.
[!] I have tried to avoid using D3 so much, especially its mouse events, cause it creates a bunch
of listeners for every children DOM elements. I have tried to use raw SVG objects.
The ArcNode is the only component where I've to call d3.arc constrictor.
[!] There are lots of discussion over different blogs an forums that ReactJS and D3.JS have a DOM
issue conflict, [could find lots of articels at medium.org about that, for example,
https://medium.com/@tibotiber/react-d3-js-balancing-performance-developer-experience-4da35f912484].
Since the current component has only a few DOM elements, I don't thing we would have any troubles,
but please keep in mind that I could tweak current version with react-faux-dom.
Actually, by now, I'm using D3 only for math, for arc path calculations.
[!] Don't mind about code spacings, it's just for me, the final code would be clear out of them.
[-] On click, an EXTERNAL callback should be called with the pillar name as a parameter. That is to enable us to expand the click functionality in future without editing the internal implementation of the component.
[-] planned, [x] done, [!] see comments
@author Vladimir V KUCHINOV
@email helloworld@vkuchinov.co.uk
*/
const VENN_MIN_WIDTH = '300px';
class ResponsiveVennDiagram extends React.Component {
constructor(props) { super(props); }
render() {
const {options, pillarsGrades} = this.props;
let childrenWidth = this.props.containerWidth, childrenHeight = this.props.containerHeight;
if(childrenHeight === 0 || childrenHeight === NaN){ childrenHeight = childrenWidth; }
else{ childrenWidth = Math.min(childrenWidth, childrenHeight) }
return (
<div ref={this.divElement} style={{textAlign: 'center'}}>
<VennDiagram style={{minWidth: VENN_MIN_WIDTH, minHeight: VENN_MIN_WIDTH}} pillarsGrades={pillarsGrades} />
</div> )
}
}
ResponsiveVennDiagram.propTypes = {
pillarsGrades: PropTypes.array
}
export default Dimensions()(ResponsiveVennDiagram);
class VennDiagram extends React.Component{
constructor(props_){
super(props_);
this.state = { tooltip: { top: 0, left: 0, display: 'none', html: '' } }
this.width = this.height = 512, this.zOrder = [];
this.colors = ['#777777', '#D9534F', '#F0AD4E', '#5CB85C'];
this.prefix = 'vennDiagram';
this.suffices = ['', '|tests are|conclusive', '|tests were|inconclusive', '|tests|performed'];
this.fontStyles = [{size: Math.max(9, this.width / 32), color: 'white'}, {size: Math.max(6, this.width / 52), color: 'black'}];
this.offset = this.width / 16;
this.thirdWidth = this.width / 3;
this.sixthWidth = this.width / 6;
this.width2By7 = 2 * this.width / 7
this.width1By11 = this.width / 11;
this.width1By28 = this.width / 28;
this.toggle = false;
this.layout = {
Data: { cx: 0, cy: 0, r: this.thirdWidth - this.offset * 2, offset: {x: 0, y: 0} },
People: { cx: -this.width2By7, cy: 0, r: this.sixthWidth, offset: {x: this.width1By11, y: 0} },
Networks: { cx: this.width2By7, cy: 0, r: this.sixthWidth, offset: {x: -this.width1By11, y: 0} },
Devices: { cx: 0, cy: this.width2By7, r: this.sixthWidth, offset: {x: 0, y: -this.width1By11} },
Workloads : { cx: 0, cy: -this.width2By7, r: this.sixthWidth, offset: {x: 0, y: this.width1By11} },
VisibilityAndAnalytics : { inner: this.thirdWidth - this.width1By28, outer: this.thirdWidth },
AutomationAndOrchestration: { inner: this.thirdWidth - this.width1By28 * 2, outer: this.thirdWidth - this.width1By28 }
};
this._onScroll = this._onScroll.bind(this);
}
componentDidMount() {
this.parseData();
window.addEventListener('scroll', this._onScroll);
}
_onMouseMove(e) {
let self = this;
if(!this.toggle){
let hidden = 'none';
let html = '';
let bcolor = '#DEDEDE';
document.querySelectorAll('circle, path').forEach((d_, i_) => { d_.setAttribute('opacity', 0.8)});
if(e.target.id.includes('Node')) {
html = e.target.dataset.tooltip;
this.divElement.style.cursor = 'pointer';
hidden = 'block'; e.target.setAttribute('opacity', 0.95);
bcolor = e.target.getAttribute('fill');
//set highest z-index
e.target.parentNode.parentNode.appendChild(e.target.parentNode);
}else{
this.divElement.style.cursor = 'default';
//return z indices to default
Object.keys(this.layout).forEach(function(d_, i_){ document.querySelector('#' + self.prefix).appendChild(document.querySelector('#' + self.prefix + 'Node_' + i_).parentNode); })
}
this.setState({target: e, tooltip: { target: e.target, bcolor: bcolor, top: e.clientY + 8, left: e.clientX + 8, display: hidden, html: html } });
}
}
_onScroll(e){
this.divElement.style.cursor = 'default';
this.setState({target: e, tooltip: { target: null, bcolor: 'none', top: 0, left: 0, display: 'none', html: '' } });
}
_onClick(e) {
if(this.state.tooltip.target === e.target) { this.toggle = true; } else { this.toggle = false; }
//variable to external callback
//e.target.parentNode.id)
}
relativeCoords (e) {
let bounds = e.target.getBoundingClientRect();
var x = e.clientX - bounds.left;
var y = e.clientY - bounds.top;
return {x: x, y: y};
}
parseData(){
let self = this;
let data = [];
const omit = (prop, { [prop]: _, ...rest }) => rest;
this.props.pillarsGrades.forEach((d_, i_) => {
let params = omit('Unexpected', omit('pillar', d_));
let sum = Object.keys(params).reduce((sum_, key_) => sum_ + parseFloat(params[key_]||0), 0);
let key = TypographicUtilities.removeAmpersand(d_.pillar);
let html = self.buildTooltipHtmlContent(d_);
let rule = 3;
if(sum === 0){ rule = 0 }
else if(d_['Conclusive'] > 0){ rule = 1 }
else if(d_['Conclusive'] === 0 && d_['Inconclusive'] > 0) { rule = 2 }
self.setLayoutElement(rule, key, html, d_);
data.push(this.layout[key])
})
this.setState({ data: data });
this.render();
}
buildTooltipHtmlContent(object_){ return Object.keys(object_).reduce((out_, key_) => out_ + TypographicUtilities.setTitle(key_) + ': ' + object_[key_] + '\n', ''); }
setLayoutElement(rule_, key_, html_, d_){
if(key_ === 'Data'){ this.layout[key_].fontStyle = this.fontStyles[0]; }
else {this.layout[key_].fontStyle = this.fontStyles[1]; }
this.layout[key_].hex = this.colors[rule_];
this.layout[key_].label = d_.pillar + this.suffices[rule_];
this.layout[key_].node = d_;
this.layout[key_].tooltip = html_;
}
render() {
if(this.state.data === undefined) { return null; }
else {
//equivalent to center translate (width/2, height/2)
let viewPortParameters = (-this.width / 2) + ' ' + (-this.height / 2) + ' ' + this.width + ' ' + this.height;
let translate = 'translate(' + this.width /2 + ',' + this.height/2 + ')';
let nodes = Object.values(this.layout).map((d_, i_) =>{
if(d_.hasOwnProperty('cx')){
return (
<CircularNode
prefix={this.prefix}
key={this.prefix + 'Node_' + i_}
index={i_}
data={d_}
/>
);
}else{
d_.label = TypographicUtilities.removeBrokenBar(d_.label);
return (
<ArcNode
prefix={this.prefix}
key={this.prefix + 'Node_' + i_}
index={i_}
data={d_}
/>
);
}
});
return (
<div ref={ (divElement) => this.divElement = divElement} onMouseMove={this._onMouseMove.bind(this)} onClick={this._onClick.bind(this)} >
<svg id={this.prefix} viewBox={viewPortParameters} width={'100%'} height={'100%'} xmlns='http://www.w3.org/2000/svg' xmlnsXlink='http://www.w3.org/1999/xlink'>
{nodes}
</svg>
<Tooltip id={this.prefix + 'Tooltip'} prefix={this.prefix} {...this.state.tooltip} />
</div>
)
}
}
}
VennDiagram.propTypes = {
pillarsGrades: PropTypes.array
}

View File

@ -0,0 +1,44 @@
import React from 'react'
import PropTypes from 'prop-types';
import * as d3 from 'd3'
class ArcNode extends React.Component{
render() {
let {prefix, index, data} = this.props;
let arc = d3.arc().innerRadius(data.inner).outerRadius(data.outer).startAngle(0).endAngle(Math.PI * 2.0);
return (
<g transform={'rotate(180)'} id={data.node.pillar}>
<path
id={prefix + 'Node_' + index}
className={'arcNode'}
data-tooltip={data.tooltip}
d={arc()}
fill={data.hex}
/>
<text x={0} dy={data.fontStyle.size * 1.2} fontSize={data.fontStyle.size} textAnchor='middle' pointerEvents={'none'}>
<textPath href={'#' + this._reactInternalFiber.key} startOffset={'26.4%'}>
{data.label}
</textPath>
</text>
</g>
)
}
}
ArcNode.propTypes = {
data: PropTypes.object
}
export default ArcNode;

View File

@ -5,7 +5,7 @@ class CircularNode extends React.Component{
render() { render() {
let {prefix, ref, index, data} = this.props; let {prefix, index, data} = this.props;
let tspans = data.label.split("|").map((d_, i_) =>{ let tspans = data.label.split("|").map((d_, i_) =>{
@ -21,23 +21,23 @@ class CircularNode extends React.Component{
}) })
let translate = 'translate(' + data.cx + ',' + data.cy + ')'; let translate = 'translate(' + data.cx + ',' + data.cy + ')';
return ( return (
<g transform={translate} id={data.node.pillar}> <g transform={translate} id={data.node.pillar}>
<circle <circle
id={this._reactInternalFiber.key} id={prefix + 'Node_' + index}
className={'circularNode'} className={'circularNode'}
data-tooltip={data.tooltip} data-tooltip={data.tooltip}
r={data.r} r={data.r}
opacity={0.8} opacity={0.8}
fill={data.hex} fill={data.hex}
/> />
<text textAnchor='middle' fill={data.fontStyle.color} dominantBaseline={'middle'} fontSize={data.fontStyle.size + 'px'} pointerEvents={'none'}> <text textAnchor='middle' fill={data.fontStyle.color} dominantBaseline={'middle'} fontSize={data.fontStyle.size + 'px'} pointerEvents={'none'}>
{tspans} {tspans}
</text> </text>
</g> </g>
) )
@ -48,7 +48,6 @@ class CircularNode extends React.Component{
CircularNode.propTypes = { CircularNode.propTypes = {
prefix: PropTypes.string,
index: PropTypes.number, index: PropTypes.number,
data: PropTypes.object data: PropTypes.object

View File

@ -0,0 +1,36 @@
import React from 'react'
import PropTypes from 'prop-types'
import Dimensions from 'react-dimensions'
import VennDiagram from './VennDiagram'
const VENN_MIN_WIDTH = '300px';
class ResponsiveVennDiagram extends React.Component {
constructor(props) { super(props); }
render() {
const {pillarsGrades} = this.props;
let childrenWidth = this.props.containerWidth, childrenHeight = this.props.containerHeight;
if(childrenHeight === 0 || isNaN(childrenHeight)){ childrenHeight = childrenWidth; }
else{ childrenWidth = Math.min(childrenWidth, childrenHeight) }
return (
<div ref={this.divElement} style={{textAlign: 'center'}}>
<VennDiagram style={{minWidth: VENN_MIN_WIDTH, minHeight: VENN_MIN_WIDTH}} pillarsGrades={pillarsGrades} />
</div> )
}
}
ResponsiveVennDiagram.propTypes = {
pillarsGrades: PropTypes.array
}
export default Dimensions()(ResponsiveVennDiagram);

View File

@ -0,0 +1,246 @@
import React from 'react'
import PropTypes from 'prop-types'
import Tooltip from './Tooltip'
import CircularNode from './CircularNode'
import ArcNode from './ArcNode'
import { TypographicUtilities } from './Utility.js'
import './VennDiagram.css'
class VennDiagram extends React.Component{
constructor(props_){
super(props_);
this.state = { tooltip: { top: 0, left: 0, display: 'none', html: '' } }
this.width = this.height = 512, this.zOrder = [];
this.colors = ['#777777', '#D9534F', '#F0AD4E', '#5CB85C'];
this.prefix = 'vennDiagram';
this.suffices = ['', '|tests are|conclusive', '|tests were|inconclusive', '|tests|performed'];
this.fontStyles = [{size: Math.max(9, this.width / 32), color: 'white'}, {size: Math.max(6, this.width / 52), color: 'black'}];
this.offset = this.width / 16;
this.thirdWidth = this.width / 3;
this.sixthWidth = this.width / 6;
this.width2By7 = 2 * this.width / 7
this.width1By11 = this.width / 11;
this.width1By28 = this.width / 28;
this.toggle = false;
this.layout = {
Data: { cx: 0, cy: 0, r: this.thirdWidth - this.offset * 2, offset: {x: 0, y: 0} },
People: { cx: -this.width2By7, cy: 0, r: this.sixthWidth, offset: {x: this.width1By11, y: 0} },
Networks: { cx: this.width2By7, cy: 0, r: this.sixthWidth, offset: {x: -this.width1By11, y: 0} },
Devices: { cx: 0, cy: this.width2By7, r: this.sixthWidth, offset: {x: 0, y: -this.width1By11} },
Workloads : { cx: 0, cy: -this.width2By7, r: this.sixthWidth, offset: {x: 0, y: this.width1By11} },
VisibilityAndAnalytics : { inner: this.thirdWidth - this.width1By28, outer: this.thirdWidth },
AutomationAndOrchestration: { inner: this.thirdWidth - this.width1By28 * 2, outer: this.thirdWidth - this.width1By28 }
};
/*
RULE #1: All scores have to be equal 0, except Unexecuted [U] which could be also a negative integer
sum(C, I, P, U) has to be <=0
RULE #2: Conclusive [C] has to be > 0,
sum(C) > 0
RULE #3: Inconclusive [I] has to be > 0 while Conclusive has to be 0,
sum(C, I) > 0 and C * I = 0, while C has to be 0
RULE #4: Positive [P] and Unexecuted have to be positive
sum(P, U) >= 2 and P * U = positive integer, while
if the P is bigger by 2 then negative U, first conditional
would be true.
*/
this.rules = [
{ id: 'Rule #1', f: function(d_){ return d_['Conclusive'] + d_['Inconclusive'] + d_['Positive'] + d_['Unexecuted'] <= 0; } },
{ id: 'Rule #2', f: function(d_){ return d_['Conclusive'] > 0; } },
{ id: 'Rule #3', f: function(d_){ return d_['Conclusive'] === 0 && d_['Inconclusive'] > 0; } },
{ id: 'Rule #4', f: function(d_){ return d_['Positive'] + d_['Unexecuted'] >= 2 && d_['Positive'] * d_['Unexecuted'] > 0; } }
];
this._onScroll = this._onScroll.bind(this);
}
componentDidMount() {
this.parseData();
window.addEventListener('scroll', this._onScroll);
}
_onMouseMove(e) {
let self = this;
if(!this.toggle){
let hidden = 'none';
let html = '';
let bcolor = '#DEDEDE';
document.querySelectorAll('circle, path').forEach((d_, i_) => { d_.setAttribute('opacity', 0.8)});
if(e.target.id.includes('Node')) {
html = e.target.dataset.tooltip;
this.divElement.style.cursor = 'pointer';
hidden = 'block'; e.target.setAttribute('opacity', 0.95);
bcolor = e.target.getAttribute('fill');
//set highest z-index
e.target.parentNode.parentNode.appendChild(e.target.parentNode);
}else{
this.divElement.style.cursor = 'default';
//return z indices to default
Object.keys(this.layout).forEach(function(d_, i_){ document.querySelector('#' + self.prefix).appendChild(document.querySelector('#' + self.prefix + 'Node_' + i_).parentNode); })
}
this.setState({target: e, tooltip: { target: e.target, bcolor: bcolor, top: e.clientY + 8, left: e.clientX + 8, display: hidden, html: html } });
}
}
_onScroll(e){
this.divElement.style.cursor = 'default';
this.setState({target: null, tooltip: { target: null, bcolor: 'none', top: 0, left: 0, display: 'none', html: '' } });
}
_onClick(e) {
if(this.state.tooltip.target === e.target) { this.toggle = true; } else { this.toggle = false; }
//variable to external callback
//e.target.parentNode.id)
}
parseData(){
let self = this;
let data = [];
const omit = (prop, { [prop]: _, ...rest }) => rest;
this.props.pillarsGrades.forEach((d_, i_) => {
let params = omit('pillar', d_);
let sum = Object.keys(params).reduce((sum_, key_) => sum_ + parseFloat(params[key_]||0), 0);
let key = TypographicUtilities.removeAmpersand(d_.pillar);
let html = self.buildTooltipHtmlContent(d_);
let rule = null;
for(let j = 0; j < self.rules.length; j++){ if(self.rules[j].f(d_)) { rule = j; break; }}
self.setLayoutElement(rule, key, html, d_);
data.push(this.layout[key]);
})
this.setState({ data: data });
this.render();
}
buildTooltipHtmlContent(object_){ return Object.keys(object_).reduce((out_, key_) => out_ + TypographicUtilities.setTitle(key_) + ': ' + object_[key_] + '\n', ''); }
setLayoutElement(rule_, key_, html_, d_){
if(rule_ == null) { throw Error('The node scores are invalid'); }
if(key_ === 'Data'){ this.layout[key_].fontStyle = this.fontStyles[0]; }
else {this.layout[key_].fontStyle = this.fontStyles[1]; }
this.layout[key_].hex = this.colors[rule_];
this.layout[key_].label = d_.pillar + this.suffices[rule_];
this.layout[key_].node = d_;
this.layout[key_].tooltip = html_;
}
render() {
if(this.state.data === undefined) { return null; }
else {
//equivalent to center translate (width/2, height/2)
let viewPortParameters = (-this.width / 2) + ' ' + (-this.height / 2) + ' ' + this.width + ' ' + this.height;
let translate = 'translate(' + this.width /2 + ',' + this.height/2 + ')';
let nodes = Object.values(this.layout).map((d_, i_) =>{
if(d_.hasOwnProperty('cx')){
return (
<CircularNode
prefix={this.prefix}
key={this.prefix + 'CircularNode' + i_}
index={i_}
data={d_}
/>
);
}else{
d_.label = TypographicUtilities.removeBrokenBar(d_.label);
return (
<ArcNode
prefix={this.prefix}
key={this.prefix + 'ArcNode' + i_}
index={i_}
data={d_}
/>
);
}
});
return (
<div ref={ (divElement) => this.divElement = divElement} onMouseMove={this._onMouseMove.bind(this)} onClick={this._onClick.bind(this)} >
<svg id={this.prefix} viewBox={viewPortParameters} width={'100%'} height={'100%'} xmlns='http://www.w3.org/2000/svg' xmlnsXlink='http://www.w3.org/1999/xlink'>
{nodes}
</svg>
<Tooltip id={this.prefix + 'Tooltip'} prefix={this.prefix} {...this.state.tooltip} />
</div>
)
}
}
}
VennDiagram.propTypes = {
pillarsGrades: PropTypes.array
}
export default VennDiagram;