Skip to content

Commit

Permalink
#38 add conflict overview
Browse files Browse the repository at this point in the history
  • Loading branch information
m4nv3ru committed Aug 31, 2022
1 parent d93071d commit d216798
Show file tree
Hide file tree
Showing 13 changed files with 492 additions and 90 deletions.
67 changes: 65 additions & 2 deletions ui/src/visualizations/team-awareness/chart/chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,82 @@

import React from 'react';
import BubbleChart from '../components/BubbleChart/BubbleChart';
import ConflictOverview from '../components/ConflictOverview/ConflictOverview';
import _ from 'lodash';

export default class TeamAwareness extends React.PureComponent {
constructor(props) {
super(props);

this.state = {
componentMounted: false,
colors: this.createColorSchema(_.map(this.props.data.stakeholders, 'id'))
};
}

componentWillUnmount() {
this.setState({ componentMounted: false });
}

componentDidMount() {
this.setState({ componentMounted: true });
}

// eslint-disable-next-line no-unused-vars
componentDidUpdate(prevProps, prevState, snapshot) {
const { componentMounted } = this.state;
const { data: { stakeholders } } = this.props;

if (componentMounted && prevProps.data.stakeholders !== stakeholders) {
console.log('stakeholders changed, updating colors');
this.setState({
colors: this.createColorSchema(_.map(stakeholders, 'id'))
});
}
}

createColorSchema(data) {
return new Map(
data.map((v, i) => {
return [v, getColor((i + 1) / data.length)];
})
);
}

render() {
const { stakeholders } = this.props.data;
const {
data: { stakeholders, conflicts },
isConflictsProcessing,
hasConflictBranchSelected,
highlightPartners,
highlightedStakeholders
} = this.props;
const { colors } = this.state;

return (
<div>
<BubbleChart content={stakeholders} />
<ConflictOverview
conflicts={conflicts}
branchSelected={hasConflictBranchSelected}
loading={isConflictsProcessing}
colors={colors}
highlightPartners={highlightPartners}
/>
<BubbleChart content={stakeholders} colors={colors} highlightedStakeholders={highlightedStakeholders} />
</div>
);
}
}

function getColor(t) {
t = Math.max(0, Math.min(1, t));
return (
'rgb(' +
Math.max(0, Math.min(255, Math.round(34.61 + t * (1172.33 - t * (10793.56 - t * (33300.12 - t * (38394.49 - t * 14825.05))))))) +
', ' +
Math.max(0, Math.min(255, Math.round(23.31 + t * (557.33 + t * (1225.33 - t * (3574.96 - t * (1073.77 + t * 707.56))))))) +
', ' +
Math.max(0, Math.min(255, Math.round(27.2 + t * (3211.1 - t * (15327.97 - t * (27814 - t * (22569.18 - t * 6838.66))))))) +
')'
);
}
12 changes: 10 additions & 2 deletions ui/src/visualizations/team-awareness/chart/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,27 @@
import { connect } from 'react-redux';
import { getState } from '../util/util.js';
import Chart from './chart.js';
import { setConflictPartners } from '../sagas';

const mapStateToProps = (appState /*, chartState */) => {
const { data } = getState(appState);
const { data, config } = getState(appState);

console.log('chart', data);
return {
highlightedStakeholders: config.highlightedStakeholders,
hasConflictBranchSelected: config.selectedConflictBranch !== 'not_set',
selectedStakeholders: config.selectedStakeholders,
isConflictsProcessing: data.isConflictsProcessing,
data: {
conflicts: data.data.conflicts,
stakeholders: data.data.stakeholders,
activityTimeline: data.data.activityTimeline
}
};
};

const mapDispatchToProps = () => ({});
const mapDispatchToProps = dispatch => ({
highlightPartners: partners => dispatch(setConflictPartners(partners))
});

export default connect(mapStateToProps, mapDispatchToProps)(Chart);
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export default class BubbleChart extends React.Component {
this.state = {
componentMounted: false,
content: this.props.content,
colors: this.createColorSchema(_.map(this.props.content, 'id')),
width: 0,
height: 0
};
Expand All @@ -28,7 +27,7 @@ export default class BubbleChart extends React.Component {
<svg className={this.styles.chartDrawingArea} ref={svg => (this.svgRef = svg)}>
<g>
{this.generateBubbles()}
{content && content.length > 0 && <Legend x={10} y={10} categories={legend} />}
{content && content.length > 0 && <Legend x={10} y={50} categories={legend} />}
</g>
</svg>
<div className={this.styles.chartTooltip} ref={div => (this.tooltipRef = div)} />
Expand All @@ -44,7 +43,7 @@ export default class BubbleChart extends React.Component {
return {
name: c.signature,
style: {
fill: this.state.colors.get(c.id)
fill: this.props.colors.get(c.id)
}
};
})
Expand All @@ -56,7 +55,7 @@ export default class BubbleChart extends React.Component {
const legend = [
{
name: stakeholder.signature + ' Activity: ' + stakeholder.activity,
style: { fill: this.state.colors.get(stakeholder.id) }
style: { fill: this.props.colors.get(stakeholder.id) }
}
];

Expand All @@ -72,10 +71,10 @@ export default class BubbleChart extends React.Component {
}
// eslint-disable-next-line no-unused-vars
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.state.componentMounted && this.state.content !== this.props.content) {
const { componentMounted, content } = this.state;
if (componentMounted && content !== this.props.content) {
this.setState({
content: this.props.content,
colors: this.createColorSchema(_.map(this.props.content, 'id'))
content: this.props.content
});
}
}
Expand All @@ -97,35 +96,21 @@ export default class BubbleChart extends React.Component {
return [];
}

return _.map(leaves, (l, i) =>
<g
key={`circle_${i}`}
transform={`translate(${l.x},${l.y})`}
onMouseEnter={() => this.constructActiveLegend(l.data)}
onMouseOut={() => this.setState({ activeLegend: null })}>
<circle fill={this.state.colors.get(l.data.id)} r={l.r} />
</g>
);
}

createColorSchema(data) {
return new Map(
data.map((v, i) => {
return [v, getColor((i + 1) / data.length)];
})
);
return _.map(leaves, (l, i) => {
return (
<g
key={`circle_${i}`}
transform={`translate(${l.x},${l.y})`}
onMouseEnter={() => this.constructActiveLegend(l.data)}
onMouseOut={() => this.setState({ activeLegend: null })}>
<circle
fill={this.props.colors.get(l.data.id)}
stroke={this.props.highlightedStakeholders.includes(l.data.id) ? '#c0392b' : 'none'}
strokeWidth={5}
r={l.r}
/>
</g>
);
});
}
}

function getColor(t) {
t = Math.max(0, Math.min(1, t));
return (
'rgb(' +
Math.max(0, Math.min(255, Math.round(34.61 + t * (1172.33 - t * (10793.56 - t * (33300.12 - t * (38394.49 - t * 14825.05))))))) +
', ' +
Math.max(0, Math.min(255, Math.round(23.31 + t * (557.33 + t * (1225.33 - t * (3574.96 - t * (1073.77 + t * 707.56))))))) +
', ' +
Math.max(0, Math.min(255, Math.round(27.2 + t * (3211.1 - t * (15327.97 - t * (27814 - t * (22569.18 - t * 6838.66))))))) +
')'
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/* eslint-disable max-len */
import React from 'react';
import _ from 'lodash';
import * as style from './ConflictOverview.scss';
import ConflictOverviewItem from './ConflictOverviewItem';

export default class ConflictOverview extends React.Component {
constructor(props) {
super(props);
this.styles = _.assign({}, style);

this.state = {
showContent: false,
contentCloseTimeout: -1
};
}

calculateConflictAmount() {
let amount = 0;
this.props.conflicts.forEach(c => (amount += c.data.length));
return amount;
}

reduceConflictsToStakeholders() {
const conflictedFiles = this.props.conflicts ? this.props.conflicts : [];
const conflicts = new Map();

for (const conflictedFile of conflictedFiles) {
for (const branch of conflictedFile.data) {
for (const conflict of branch.conflicts) {
const participants = `${conflict.conflictStakeholder.id}${conflict.otherStakeholder.id}`;
if (!conflicts.has(participants) && conflict.conflictStakeholder.id !== conflict.otherStakeholder.id) {
conflicts.set(participants, { conflictStakeholder: conflict.conflictStakeholder, otherStakeholder: conflict.otherStakeholder });
}
}
}
}
return Array.from(conflicts.values());
}

render() {
const conflicts = this.reduceConflictsToStakeholders();
const participantsTag = conflicts.length > 1 ? 'participants' : 'participant';

const handleContentMouseOver = () => {
clearTimeout(this.state.contentCloseTimeout);
this.setState({ showContent: true });
};

const handleContentClose = () => {
this.setState({ contentCloseTimeout: setTimeout(() => this.setState({ showContent: false }), 250) });
};

if (!this.props.branchSelected) {
return <div className={this.styles.conflictOverviewNoSelection}>No conflict branch selected</div>;
}

if (this.props.loading) {
return (
<div className={[this.styles.conflictOverview, this.styles.conflictOverviewHeader].join(' ')}>
<svg
className={this.styles.conflictOverviewIcon}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Processing conflicts...</span>
</div>
);
}

if (conflicts.length === 0) {
return (
<div className={[this.styles.conflictOverview, this.styles.conflictOverviewHeader].join(' ')}>
<svg
className={this.styles.conflictOverviewIcon}
fill="none"
stroke="#27ae60"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"
/>
</svg>
<span>Could not detect any possible conflicts with selected branches</span>
</div>
);
}

return (
<div className={this.styles.conflictOverview}>
<div
className={this.styles.conflictOverviewHeader}
onMouseOver={() => this.setState({ showContent: true })}
onMouseOut={handleContentClose}>
<svg className={this.styles.conflictOverviewIcon} fill="#E67E22" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
<span>
Detected conflicts with {conflicts.length} {participantsTag}
</span>
</div>
{this.state.showContent &&
<div className={this.styles.conflictOverviewContent} onMouseOver={handleContentMouseOver} onMouseOut={handleContentClose}>
{conflicts.map((conflict, i) =>
<ConflictOverviewItem
highlightPartners={this.props.highlightPartners}
colors={this.props.colors}
conflict={conflict}
key={i}
/>
)}
</div>}
</div>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
.conflictOverview {
top: 10px;
left: 6px;
position: absolute;
}

.conflictOverviewIcon {
height: 25px;
width: 25px;
}

.conflictOverviewHeader {
display: flex;
color: black;
}

.conflictOverviewContent {
max-height: 750px;
padding-left: 20px;
width: 500px;
background: white;
overflow: scroll;
}

.conflictOverviewItem {
color: black;
cursor: pointer;
}

.conflictOverviewItemColors {
width: 15px;
height: 15px;
}

.conflictOverviewItemText {
padding-left: 5px;
}

.conflictOverviewNoSelection {
color: black;
top: 10px;
left: 10px;
position: absolute;
}
Loading

0 comments on commit d216798

Please sign in to comment.