Fixes for issues posted by Shay at VennDiagram component #412

[x] ResponsiveVennDiagram.js, VennDiagram.js
[x] VennDiagram.css (rename)
[x] ArcNode, CicularNode, Tooltip as .js
and other minor issues
This commit is contained in:
vkuchinov 2019-08-25 17:32:21 +03:00
parent 5c4797108e
commit b9cb655114
10 changed files with 210 additions and 330 deletions

View File

@ -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 = <MustRunMonkeyWarning/>;
}
let res;
// Todo move to componentDidMount
this.getZeroTrustReportFromServer(res);
const content = this.generateReportContent();
return (
<Col xs={12} lg={10}>
@ -59,75 +92,63 @@ class ZeroTrustReportPageComponent extends AuthComponent {
let content;
if (this.stillLoadingDataFromServer()) {
content = <ReportLoader loading={true}/>;
content = "Still empty";
} else {
content = <div id="MainContentSection">
{this.generateOverviewSection()}
{this.generateDirectivesSection()}
{this.generateFindingsSection()}
const pillarsSection = <div>
<h2>Pillars Overview</h2>
<PillarGrades pillars={this.state.pillars}/>
</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>;
}
return (
<Fragment>
<div style={{marginBottom: '20px'}}>
<PrintReportButton onClick={() => {print();}} />
<div>
<div className="text-center no-print" style={{marginBottom: '20px'}}>
<Button bsSize="large" onClick={() => {
this.print();
}}><i className="glyphicon glyphicon-print"/> Print Report</Button>
</div>
<div className="report-page">
<ReportHeader report_type={ReportTypes.zeroTrust}/>
<hr/>
{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 style={{marginTop: '20px'}}>
<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() {
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;

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

@ -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() {
let {prefix, ref, index, data} = this.props;
let {prefix, index, data} = this.props;
let tspans = data.label.split("|").map((d_, i_) =>{
@ -27,7 +27,7 @@ class CircularNode extends React.Component{
<g transform={translate} id={data.node.pillar}>
<circle
id={this._reactInternalFiber.key}
id={prefix + 'Node_' + index}
className={'circularNode'}
data-tooltip={data.tooltip}
r={data.r}
@ -48,7 +48,6 @@ class CircularNode extends React.Component{
CircularNode.propTypes = {
prefix: PropTypes.string,
index: PropTypes.number,
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

@ -1,169 +1,10 @@
import React from 'react'
import PropTypes from 'prop-types';
import Dimensions from 'react-dimensions'
import PropTypes from 'prop-types'
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);
import { TypographicUtilities } from './Utility.js'
import './VennDiagram.css'
class VennDiagram extends React.Component{
@ -292,15 +133,6 @@ class VennDiagram extends React.Component{
}
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;
@ -318,7 +150,7 @@ class VennDiagram extends React.Component{
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])
data.push(this.layout[key]);
})
@ -361,7 +193,7 @@ class VennDiagram extends React.Component{
<CircularNode
prefix={this.prefix}
key={this.prefix + 'Node_' + i_}
key={this.prefix + 'CircularNode' + i_}
index={i_}
data={d_}
@ -377,7 +209,7 @@ class VennDiagram extends React.Component{
<ArcNode
prefix={this.prefix}
key={this.prefix + 'Node_' + i_}
key={this.prefix + 'ArcNode' + i_}
index={i_}
data={d_}
@ -410,3 +242,5 @@ VennDiagram.propTypes = {
pillarsGrades: PropTypes.array
}
export default VennDiagram;