Highlight assigned networks and roles on hover

When a network is hovered, all its connections to roles are
highlighted.

Implements: blueprint networks-crud-ui
Change-Id: I221050cfe06658c0f5193adf64431690bdd0274b
This commit is contained in:
Jiri Tomasek 2018-06-20 12:05:59 +02:00
parent 2006759d61
commit 3691bab67d
9 changed files with 236 additions and 93 deletions

View File

@ -0,0 +1,9 @@
---
features:
- |
Added a network topology view accessible under the Network Configuration
step in Deployment plan overview page. The Network topology diagram
represents Deployment roles, networks and draws connections based on
roles-networks assignment. The Diagram also shows high level information
about each network. When you hover over a network, all its connections are
highlighted for easy identification of assigned Roles.

View File

@ -14,11 +14,13 @@
* under the License.
*/
import cx from 'classnames';
import { startCase } from 'lodash';
import PropTypes from 'prop-types';
import React, { Fragment } from 'react';
import { getNetworkColorStyle } from './utils';
import { NetworksHighlightContext } from './NetworksHighlighter';
const NetworkListItem = ({
className,
@ -32,15 +34,35 @@ const NetworkListItem = ({
disabled ? 'disabled' : name
);
return (
<Fragment>
<div ref={lineRef} className="network-line" style={{ borderColor }} />
<div className="network" style={{ backgroundColor }}>
<h5>
<strong>{startCase(name)}</strong>
</h5>
{children}
</div>
</Fragment>
<NetworksHighlightContext.Consumer>
{({ highlightedNetworks, setHighlightedNetworks }) => (
<Fragment>
<div
className={cx('network-line', {
highlighted: highlightedNetworks.includes(name)
})}
>
<div className="network-line-ends" style={{ borderColor }} />
<div
ref={lineRef}
className="network-line-edge"
style={{ borderColor }}
/>
</div>
<div
className="network"
style={{ backgroundColor }}
onMouseEnter={() => setHighlightedNetworks([name])}
onMouseLeave={() => setHighlightedNetworks([])}
>
<h5>
<strong>{startCase(name)}</strong>
</h5>
{children}
</div>
</Fragment>
)}
</NetworksHighlightContext.Consumer>
);
};
NetworkListItem.propTypes = {

View File

@ -30,6 +30,7 @@ import {
} from '../../selectors/networks';
import { getParameters } from '../../selectors/parameters';
import { getRoles } from '../../selectors/roles';
import NetworksHighlighter from './NetworksHighlighter';
import RolesList from './RolesList';
import NetworksList from './NetworksList';
@ -50,27 +51,39 @@ const messages = defineMessages({
defaultMessage: 'Enable network isolation'
}
});
class NetworkTopology extends Component {
constructor() {
super();
this.networkLineElements = {};
this.state = { networkLinePositions: {} };
this.calculateNetworkPositions = debounce(
this.calculateNetworkPositions,
class NetworkTopology extends Component {
constructor(props) {
super(props);
this.networkLineElements = {};
this.roleNetworkLineElements = {};
this.state = { networkLineHeights: {} };
this.calculateNetworkLineHeights = debounce(
this.calculateNetworkLineHeights,
100
);
}
componentDidMount() {
this.calculateNetworkPositions();
this.calculateNetworkLineHeights();
}
calculateNetworkPositions = () =>
Object.keys(this.networkLineElements).map(key => {
const rect = this.networkLineElements[key].getBoundingClientRect();
this.setState(state => (state.networkLinePositions[key] = rect.y));
});
calculateNetworkLineHeights = () => {
const resultObject = Object.assign({}, this.roleNetworkLineElements);
Object.keys(this.roleNetworkLineElements).forEach(role =>
Object.keys(this.roleNetworkLineElements[role]).forEach(network => {
const { y, height } = this.roleNetworkLineElements[role][
network
].getBoundingClientRect();
const start = y + height;
const end = this.networkLineElements[network].getBoundingClientRect().y;
resultObject[role][network] = end - start;
})
);
this.setState({ networkLineHeights: resultObject });
};
render() {
const {
@ -81,7 +94,7 @@ class NetworkTopology extends Component {
parameters
} = this.props;
return (
<div className="flex-container">
<NetworksHighlighter>
{networkResourceExistsByNetwork.includes(false) && (
<Alert type="info">
<strong>
@ -104,7 +117,8 @@ class NetworkTopology extends Component {
<div className="network-topology">
<RolesList
roles={roles}
networkLinePositions={this.state.networkLinePositions}
networkLineHeights={this.state.networkLineHeights}
roleNetworkLineElements={this.roleNetworkLineElements}
/>
<NetworksList
networks={networks}
@ -113,7 +127,7 @@ class NetworkTopology extends Component {
networkLineElements={this.networkLineElements}
/>
</div>
</div>
</NetworksHighlighter>
);
}
}

View File

@ -0,0 +1,47 @@
/**
* Copyright 2018 Red Hat Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
import React, { Component, createContext } from 'react';
export const NetworksHighlightContext = createContext({
highlightedNetworks: [],
setHighlightedNetworks: () => {}
});
export default class NetworksHighlighter extends Component {
constructor(props) {
super(props);
this.setHighlightedNetworks = highlightedNetworks =>
this.setState({ highlightedNetworks });
this.state = {
highlightedNetworks: [],
setHighlightedNetworks: this.setHighlightedNetworks
};
}
render() {
return (
<NetworksHighlightContext.Provider value={this.state}>
{this.props.children}
</NetworksHighlightContext.Provider>
);
}
}
NetworksHighlighter.propTypes = {
children: PropTypes.node
};

View File

@ -28,6 +28,7 @@ const NetworksList = ({
}) => {
const ctlPlaneDefaultRoute = parameters.get('ControlPlaneDefaultRoute');
const ctlPlaneSubnetCidr = parameters.get('ControlPlaneSubnetCidr');
return (
<Fragment>
<NetworkListItem

View File

@ -17,15 +17,20 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { set } from 'lodash';
import RoleNetworkLine from './RoleNetworkLine';
export class RoleCard extends Component {
render() {
const { role: { name, networks }, networkLinePositions } = this.props;
const {
networkLineHeights,
role: { name, networks },
roleNetworkLineElements
} = this.props;
const allNetworks = networks.unshift('Provisioning');
return (
<div className="card-pf card-pf-view card-pf-view-select role-card">
<div className="card-pf card-pf-view role-card">
<div className="card-pf-body">
<h2 className="card-pf-title">{name}</h2>
</div>
@ -33,9 +38,12 @@ export class RoleCard extends Component {
<ul className="role-networks-list list-unstyled">
{allNetworks.map(network => (
<RoleNetworkLine
lineRef={el =>
set(roleNetworkLineElements, [name, network], el)
}
key={network}
networkName={network}
networkLinePosition={networkLinePositions[network]}
networkLineHeight={networkLineHeights[network]}
/>
))}
</ul>
@ -45,8 +53,12 @@ export class RoleCard extends Component {
}
}
RoleCard.propTypes = {
networkLinePositions: PropTypes.object,
role: ImmutablePropTypes.record.isRequired
networkLineHeights: PropTypes.object.isRequired,
role: ImmutablePropTypes.record.isRequired,
roleNetworkLineElements: PropTypes.object.isRequired
};
RoleCard.defaultProps = {
networkLineHeights: {}
};
export default RoleCard;

View File

@ -21,63 +21,67 @@ import React from 'react';
import { getNetworkColorStyle } from './utils';
import { getNetworkResourceExistsByNetwork } from '../../selectors/networks';
import { NetworksHighlightContext } from './NetworksHighlighter';
class RoleNetworkLine extends React.Component {
getStartingPoint = nicElement => {
const rect = nicElement.getBoundingClientRect();
return rect.y + rect.height;
};
render() {
const {
className,
disabled,
networkName,
networkLinePosition
} = this.props;
const { backgroundColor, borderColor } = getNetworkColorStyle(
disabled ? 'disabled' : networkName
);
return (
<li className={cx('role-nic', className)} ref={el => (this.element = el)}>
<div
className="role-network-line"
style={{
transform: networkLinePosition && 'rotateX(0deg)',
height:
networkLinePosition &&
networkLinePosition - this.getStartingPoint(this.element),
bottom:
networkLinePosition &&
-(networkLinePosition - this.getStartingPoint(this.element)),
opacity: networkLinePosition && 1,
backgroundColor,
borderColor
}}
/>
<div
className="role-network-point"
style={{
bottom:
networkLinePosition &&
-(networkLinePosition - this.getStartingPoint(this.element) + 5),
opacity: networkLinePosition && 1,
backgroundColor,
borderColor
}}
/>
</li>
<NetworksHighlightContext.Consumer>
{({ highlightedNetworks, setHighlightedNetworks }) => {
const {
className,
disabled,
lineRef,
networkName,
networkLineHeight
} = this.props;
const highlighted = highlightedNetworks.includes(networkName);
const { backgroundColor, borderColor } = getNetworkColorStyle(
disabled ? 'disabled' : networkName
);
return (
<li className={cx('role-nic', className)} ref={lineRef}>
<div
style={{
height: networkLineHeight,
bottom: -networkLineHeight,
backgroundColor,
borderColor
}}
className={cx('role-network-line', {
highlighted,
in: networkLineHeight > 0
})}
/>
<div
className={cx('role-network-point', { highlighted })}
style={{
bottom: -networkLineHeight - 5,
opacity: 1,
backgroundColor,
borderColor
}}
/>
</li>
);
}}
</NetworksHighlightContext.Consumer>
);
}
}
RoleNetworkLine.propTypes = {
className: PropTypes.string,
disabled: PropTypes.bool.isRequired,
networkLinePosition: PropTypes.number,
lineRef: PropTypes.func.isRequired,
networkLineHeight: PropTypes.number.isRequired,
networkName: PropTypes.string.isRequired
};
RoleNetworkLine.defaultProps = {
disabled: false
disabled: false,
networkLineHeight: 0
};
const mapStateToProps = (state, { networkName }) => ({

View File

@ -22,7 +22,7 @@ import { splitListIntoChunks } from '../../utils/immutablejs';
import RoleCard from './RoleCard';
import { Role } from '../../immutableRecords/roles';
const RolesList = ({ roles, networkLinePositions }) => {
const RolesList = ({ roles, networkLineHeights, roleNetworkLineElements }) => {
const rolesChunks = splitListIntoChunks(roles.toList(), 2);
return (
<Fragment>
@ -32,15 +32,17 @@ const RolesList = ({ roles, networkLinePositions }) => {
.map(role => (
<RoleCard
key={role.name}
networkLinePositions={networkLinePositions}
networkLineHeights={networkLineHeights[role.name]}
role={role}
roleNetworkLineElements={roleNetworkLineElements}
/>
))}
</div>
<div className="roles-list">
<RoleCard
networkLineHeights={networkLineHeights['Undercloud']}
role={new Role({ name: 'Undercloud', identifier: 'undercloud' })}
networkLinePositions={networkLinePositions}
roleNetworkLineElements={roleNetworkLineElements}
/>
</div>
<div className="roles-list">
@ -49,8 +51,9 @@ const RolesList = ({ roles, networkLinePositions }) => {
.map(role => (
<RoleCard
key={role.name}
networkLinePositions={networkLinePositions}
networkLineHeights={networkLineHeights[role.name]}
role={role}
roleNetworkLineElements={roleNetworkLineElements}
/>
))}
</div>
@ -58,8 +61,12 @@ const RolesList = ({ roles, networkLinePositions }) => {
);
};
RolesList.propTypes = {
networkLinePositions: PropTypes.object.isRequired,
networkLineHeights: PropTypes.object.isRequired,
roleNetworkLineElements: PropTypes.object.isRequired,
roles: ImmutablePropTypes.map.isRequired
};
RolesList.defaultProps = {
networkLineHeights: {}
};
export default RolesList;

View File

@ -63,9 +63,17 @@
border-right: 1px solid;
background-color: inherit;
transform: rotateX(90deg);
transform-origin: top right;
transform-origin: top center;
transition: transform .5s ease-in, opacity .3s ease-in;
opacity: 0;
&.in {
transition: transform .1s ease-in;
transform: rotateX(0deg);
opacity: 1
}
&.highlighted {
transform: scaleX(3);
}
}
.role-network-point {
position: absolute;
@ -78,7 +86,10 @@
border-radius: 5px;
border: 1px solid;
opacity: 0;
transition: opacity .5s .3s ease-in;
transition: transform .1s ease-in, opacity .5s .3s ease-in;
&.highlighted {
transform: scale(1.2);
}
}
}
}
@ -86,20 +97,36 @@
.network-line {
grid-column: ~"1 / 4";
position: relative;
border-top: 1px solid;
margin: 20px 9px 0 9px;
&:before, &:after {
content: "";
position: absolute;
top: -5px;
width: 9px;
height: 9px;
border-radius: 5px;
border: 1px solid;
border-color: inherit;
.network-line-edge {
transition: transform .1s ease-in;
border-top: 1px solid;
}
.network-line-ends {
&:before, &:after {
content: "";
position: absolute;
top: -4px;
width: 9px;
height: 9px;
border-radius: 5px;
border: 1px solid;
border-color: inherit;
transition: transform .1s ease-in;
}
&:before { left: -9px; }
&:after { right: -9px; }
}
&.highlighted {
.network-line-edge {
transform: scaleY(3);
}
.network-line-ends {
&:before, &:after {
transform: scale(1.3);
}
}
}
&:before { left: -9px; }
&:after { right: -9px; }
}
.network {