diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ZeroTrustReportPage.js b/monkey/monkey_island/cc/ui/src/components/pages/ZeroTrustReportPage.js old mode 100644 new mode 100755 index 2fe43c42e..e80af2366 --- a/monkey/monkey_island/cc/ui/src/components/pages/ZeroTrustReportPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ZeroTrustReportPage.js @@ -1,17 +1,64 @@ -import React, {Fragment} from 'react'; -import {Col, Grid, Row} from 'react-bootstrap'; +import React from 'react'; +import {Button, Col} from 'react-bootstrap'; import AuthComponent from '../AuthComponent'; 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 SinglePillarDirectivesStatus from "../report-components/zerotrust/SinglePillarDirectivesStatus"; -import MonkeysStillAliveWarning from "../report-components/common/MonkeysStillAliveWarning"; -import ReportLoader from "../report-components/common/ReportLoader"; -import MustRunMonkeyWarning from "../report-components/common/MustRunMonkeyWarning"; -import SecurityIssuesGlance from "../report-components/common/SecurityIssuesGlance"; -import StatusesToPillarsSummary from "../report-components/zerotrust/StatusesToPillarsSummary"; -import PrintReportButton from "../report-components/common/PrintReportButton"; -import {extractExecutionStatusFromServerResponse} from "../report-components/common/ExecutionStatus"; +import {SinglePillarRecommendationsStatus} from "../report-components/zerotrust/SinglePillarRecommendationsStatus"; + +let mockup = [ + { + "Conclusive": 4, + "Inconclusive": 0, + "Positive": 1, + "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 { @@ -20,30 +67,16 @@ class ZeroTrustReportPageComponent extends AuthComponent { this.state = { 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() { - let content; - if (this.state.runStarted) { - content = this.generateReportContent(); - } else { - content = ; - } + let res; + // Todo move to componentDidMount + this.getZeroTrustReportFromServer(res); + + const content = this.generateReportContent(); return ( @@ -59,75 +92,63 @@ class ZeroTrustReportPageComponent extends AuthComponent { let content; if (this.stillLoadingDataFromServer()) { - content = ; + content = "Still empty"; } else { - content =
- {this.generateOverviewSection()} - {this.generateDirectivesSection()} - {this.generateFindingsSection()} + const pillarsSection =
+

Pillars Overview

+ +
; + + const recommendationsSection =

Recommendations Status

+ { + this.state.recommendations.map((recommendation) => + + ) + } +
; + + const findingSection =

Findings

+
; + + content =
+ {pillarsSection} + {recommendationsSection} + {findingSection}
; } return ( - -
- {print();}} /> +
+
+

{content} +
+
{JSON.stringify(this.state.pillars, undefined, 2)}
+
+ +
{JSON.stringify(this.state.recommendations, undefined, 2)}
+
+
{JSON.stringify(this.state.findings, undefined, 2)}
-
- {print();}} /> -
- +
) } - generateFindingsSection() { - return (
-

Findings

- -
); - } - - generateDirectivesSection() { - return (
-

Directives

- { - Object.keys(this.state.directives).map((pillar) => - - ) - } -
); - } - - generateOverviewSection() { - return (
-

Overview

- - - - - - - - - - - - -
); - } - 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() { @@ -139,11 +160,11 @@ class ZeroTrustReportPageComponent extends AuthComponent { findings: res }); }); - this.authFetch('/api/report/zero_trust/directives') + this.authFetch('/api/report/zero_trust/recommendations') .then(res => res.json()) .then(res => { this.setState({ - directives: res + recommendations: res }); }); 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; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/VennDiagram/ArcNode/index.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/VennDiagram/ArcNode/index.js deleted file mode 100644 index b9861a55e..000000000 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/VennDiagram/ArcNode/index.js +++ /dev/null @@ -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 ( - - - - - - {data.label} - - - - - ) - - } - -} - -ArcNode.propTypes = { - - prefix: PropTypes.string, - index: PropTypes.number, - data: PropTypes.object - -} - -export default ArcNode; \ No newline at end of file diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/VennDiagram/index.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/VennDiagram/index.js deleted file mode 100644 index de28fc012..000000000 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/VennDiagram/index.js +++ /dev/null @@ -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 ( - -
- -
- - ); - - }; - - 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 ( - -
- -
) - - } - -} - -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 ( - - - ); - - }else{ - - d_.label = TypographicUtilities.removeBrokenBar(d_.label); - - return ( - - - ); - - } - - }); - - return ( - -
this.divElement = divElement} onMouseMove={this._onMouseMove.bind(this)} onClick={this._onClick.bind(this)} > - - {nodes} - - -
- - ) - - } - - } - -} - -VennDiagram.propTypes = { - - pillarsGrades: PropTypes.array - -} \ No newline at end of file diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/.DS_Store b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/.DS_Store new file mode 100644 index 000000000..344923cf9 Binary files /dev/null and b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/.DS_Store differ diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ArcNode.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ArcNode.js new file mode 100644 index 000000000..1306ecc54 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ArcNode.js @@ -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 ( + + + + + + {data.label} + + + + + ) + + } + +} + +ArcNode.propTypes = { + + data: PropTypes.object + +} + +export default ArcNode; \ No newline at end of file diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/VennDiagram/CircularNode/index.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/CircularNode.js similarity index 61% rename from monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/VennDiagram/CircularNode/index.js rename to monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/CircularNode.js index a8caf5bb0..8d8df10bf 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/VennDiagram/CircularNode/index.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/CircularNode.js @@ -5,7 +5,7 @@ class CircularNode extends React.Component{ render() { - let {prefix, ref, index, data} = this.props; + let {prefix, index, data} = this.props; let tspans = data.label.split("|").map((d_, i_) =>{ @@ -21,23 +21,23 @@ class CircularNode extends React.Component{ }) let translate = 'translate(' + data.cx + ',' + data.cy + ')'; - + return ( - - - {tspans} - + /> + + {tspans} + ) @@ -48,7 +48,6 @@ class CircularNode extends React.Component{ CircularNode.propTypes = { - prefix: PropTypes.string, index: PropTypes.number, data: PropTypes.object diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ResponsiveVennDiagram.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ResponsiveVennDiagram.js new file mode 100644 index 000000000..d20abf94a --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ResponsiveVennDiagram.js @@ -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 ( + +
+ +
) + + } + +} + +ResponsiveVennDiagram.propTypes = { + + pillarsGrades: PropTypes.array + +} + +export default Dimensions()(ResponsiveVennDiagram); diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/VennDiagram/Tooltip/index.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/Tooltip.js similarity index 100% rename from monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/VennDiagram/Tooltip/index.js rename to monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/Tooltip.js diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/VennDiagram/utility.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/Utility.js similarity index 100% rename from monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/VennDiagram/utility.js rename to monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/Utility.js diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/VennDiagram/index.css b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.css similarity index 100% rename from monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/VennDiagram/index.css rename to monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.css diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.js new file mode 100644 index 000000000..0f154cdfe --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.js @@ -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 ( + + + ); + + }else{ + + d_.label = TypographicUtilities.removeBrokenBar(d_.label); + + return ( + + + ); + + } + + }); + + return ( + +
this.divElement = divElement} onMouseMove={this._onMouseMove.bind(this)} onClick={this._onClick.bind(this)} > + + {nodes} + + +
+ + ) + + } + + } + +} + +VennDiagram.propTypes = { + + pillarsGrades: PropTypes.array + +} + +export default VennDiagram; \ No newline at end of file