feat(ui-toolkit): remove topology

This commit is contained in:
Sérgio Ramos 2017-10-17 16:27:26 +01:00
parent af817cf059
commit ee30f5e7bf
25 changed files with 134 additions and 3513 deletions

View File

@ -42,21 +42,17 @@
"babel-template": "^6.26.0",
"camel-case": "^3.0.0",
"cross-env": "^5.0.5",
"d3": "^4.11.0",
"disable-scroll": "^0.3.0",
"file-loader": "^1.1.5",
"fontfaceobserver": "^2.0.13",
"joy-react-broadcast": "^0.6.9",
"joyent-manifest-editor": "^1.4.0",
"lodash.difference": "^4.5.0",
"lodash.differenceby": "^4.8.0",
"lodash.isequal": "^4.5.0",
"lodash.isequalwith": "^4.4.0",
"lodash.isstring": "^4.0.1",
"moment": "^2.19.1",
"normalized-styled-components": "^1.0.17",
"pascal-case": "^2.0.1",
"polished": "^1.8.1",
"prop-types": "^15.6.0",
"react-bundle": "^1.0.4",
"react-input-range": "^1.2.1",

View File

@ -12,7 +12,6 @@ export { default as Small } from './text/small';
export { default as Title } from './text/title';
export { default as theme } from './theme';
export { default as typography, fonts } from './typography';
export { default as Topology } from './topology';
export { default as Modal, ModalHeading, ModalText } from './modal';
export { default as Chevron } from './chevron';
export { default as CloseButton } from './close-button';

View File

@ -1,94 +0,0 @@
const Lengths = {
paddingLeft: 12,
nodeWidth: 180,
statusHeight: 18
};
const Sizes = {
buttonSize: {
width: 40,
height: 48
},
contentSize: {
width: Lengths.nodeWidth,
// height: 101 // This is the height w/o info comp
height: 42
},
childContentSize: {
width: Lengths.nodeWidth,
height: 60
},
nodeSize: {
width: Lengths.nodeWidth,
// height: 156
height: 90
},
nodeSizeWithChildren: {
width: Lengths.nodeWidth,
// height: 276
height: 176
}
};
const Points = {
buttonPosition: {
x: Lengths.nodeWidth - Sizes.buttonSize.width,
y: 0
},
contentPosition: {
x: 0,
y: Sizes.buttonSize.height
},
infoPosition: {
x: Lengths.paddingLeft,
y: 11
},
metricsPosition: {
x: Lengths.paddingLeft,
y: 41
},
subtitlePosition: {
x: Lengths.paddingLeft,
y: 23
}
};
const Rects = {
// X, y, width, height
buttonRect: {
...Sizes.buttonSize,
...Points.buttonPosition
},
contentRect: {
...Sizes.contentSize,
...Points.contentPosition
},
childContentRect: {
...Sizes.childContentSize,
...Points.contentPosition
},
// Top, bottom, left, right - from 'centre'
nodeRect: {
...Sizes.nodeSize,
left: -Sizes.nodeSize.width / 2,
right: Sizes.nodeSize.width / 2,
top: -Sizes.nodeSize.height / 2,
bottom: Sizes.nodeSize.height / 2
},
nodeRectWithChildren: {
...Sizes.nodeSizeWithChildren,
left: -Sizes.nodeSizeWithChildren.width / 2,
right: Sizes.nodeSizeWithChildren.width / 2,
top: -Sizes.nodeSizeWithChildren.height / 2 + Sizes.contentSize.height / 3,
bottom: Sizes.nodeSizeWithChildren.height / 2 + Sizes.contentSize.height / 3
}
};
const Constants = {
...Lengths,
...Sizes,
...Points,
...Rects
};
export default Constants;

File diff suppressed because it is too large Load Diff

View File

@ -1,147 +0,0 @@
[
{
"uuid": "081a792c-47e0-4439-924b-2efa9788ae9e",
"id": "nginx",
"name": "Nginx",
"project": "e0ea0c02-55cc-45fe-8064-3e5176a59401",
"instances": [""],
"metrics": [
{
"name": "CPU",
"value": "50%"
},
{
"name": "Memory",
"value": "20%"
},
{
"name": "Network",
"value": "2.9Kb/sec"
}
],
"connections": ["be227788-74f1-4e5b-a85f-b5c71cbae8d8"],
"healthy": true,
"datacentres": 1
},
{
"uuid": "be227788-74f1-4e5b-a85f-b5c71cbae8d8",
"id": "wordpress",
"name": "Wordpress",
"project": "e0ea0c02-55cc-45fe-8064-3e5176a59401",
"instances": ["", ""],
"metrics": [
{
"name": "CPU",
"value": "50%"
},
{
"name": "Memory",
"value": "20%"
},
{
"name": "Network",
"value": "2.9Kb/sec"
}
],
"connections": [
"6a0eee76-c019-413b-9d5f-44712b55b993",
"6d31aff4-de1e-4042-a983-fbd23d5c530c",
"4ee4103e-1a52-4099-a48e-01588f597c70"
],
"healthy": true,
"datacentres": 2
},
{
"uuid": "6a0eee76-c019-413b-9d5f-44712b55b993",
"id": "nfs",
"name": "NFS",
"project": "e0ea0c02-55cc-45fe-8064-3e5176a59401",
"instances": ["", ""],
"metrics": [
{
"name": "CPU",
"value": "50%"
},
{
"name": "Memory",
"value": "20%"
},
{
"name": "Network",
"value": "2.9Kb/sec"
}
],
"healthy": true,
"datacentres": 2
},
{
"uuid": "6d31aff4-de1e-4042-a983-fbd23d5c530c",
"id": "memcached",
"name": "Memcached",
"project": "e0ea0c02-55cc-45fe-8064-3e5176a59401",
"instances": ["", ""],
"metrics": [
{
"name": "CPU",
"value": "50%"
},
{
"name": "Memory",
"value": "20%"
},
{
"name": "Network",
"value": "2.9Kb/sec"
}
],
"healthy": true,
"datacentres": 2
},
{
"uuid": "4ee4103e-1a52-4099-a48e-01588f597c70",
"id": "percona",
"name": "Percona",
"project": "e0ea0c02-55cc-45fe-8064-3e5176a59401",
"instances": ["", ""],
"metrics": [
{
"name": "CPU",
"value": "50%"
},
{
"name": "Memory",
"value": "20%"
},
{
"name": "Network",
"value": "2.9Kb/sec"
}
],
"healthy": true,
"datacentres": 1
},
{
"uuid": "97c68055-db88-45c9-ad49-f26da4264777",
"id": "consul",
"name": "Consul",
"project": "e0ea0c02-55cc-45fe-8064-3e5176a59401",
"instances": ["", ""],
"isConsul": true,
"metrics": [
{
"name": "CPU",
"value": "50%"
},
{
"name": "Memory",
"value": "20%"
},
{
"name": "Network",
"value": "2.9Kb/sec"
}
],
"healthy": true,
"datacentres": 2
}
]

View File

@ -1,148 +0,0 @@
import Constants from './constants';
const getAngleFromPoints = (source, target) => {
const lineAngle = Math.atan2(target.y - source.y, target.x - source.x);
const lineAngleDeg = lineAngle * 180 / Math.PI;
const zeroToThreeSixty = lineAngleDeg < 0 ? 360 + lineAngleDeg : lineAngleDeg;
return zeroToThreeSixty;
};
const getPosition = (angle, positions, position, noCorners = false) => {
const positionIndex = noCorners
? Math.round(angle / 90) * 2
: Math.round(angle / 45);
const offsetPosition = positions[positionIndex];
return {
id: offsetPosition.id,
x: position.x + offsetPosition.x,
y: position.y + offsetPosition.y
};
};
const getPositions = (rect, halfCorner = 0) => [
{
id: 'r',
x: rect.right,
y: 0
},
{
id: 'br',
x: rect.right - halfCorner,
y: rect.bottom - halfCorner
},
{
id: 'b',
x: 0,
y: rect.bottom
},
{
id: 'bl',
x: rect.left + halfCorner,
y: rect.bottom - halfCorner
},
{
id: 'l',
x: rect.left,
y: 0
},
{
id: 'tl',
x: rect.left + halfCorner,
y: rect.top + halfCorner
},
{
id: 't',
x: 0,
y: rect.top
},
{
id: 'tr',
x: rect.right - halfCorner,
y: rect.top + halfCorner
},
{
id: 'r',
x: rect.right,
y: 0
}
];
/* const getRect = data =>
data.children ? Constants.nodeRectWithChildren : Constants.nodeRect; */
const calculateLineLayout = ({ source, target }) => {
// Actually, this will need to be got dynamically, in case them things are different sizes
// yeah right, now you'll get to do exactly that
const halfCorner = 2;
const sourcePositions = getPositions(source.nodeRect, halfCorner);
const sourceAngle = getAngleFromPoints(source, target);
const sourcePosition = getPosition(sourceAngle, sourcePositions, source);
const targetPositions = getPositions(target.nodeRect, halfCorner);
const targetAngle = getAngleFromPoints(target, sourcePosition);
const targetPosition = getPosition(targetAngle, targetPositions, target); // , true);
const arrowAngle = getAngleFromPoints(sourcePosition, targetPosition);
return {
source,
target,
sourcePosition,
targetPosition,
arrowAngle
};
};
const getStatusesLength = data =>
data.transitionalStatus ? 1 : data.instanceStatuses.length;
const getStatusesHeight = data => {
const statuses = data.children
? data.children.reduce(
(statuses, child) => statuses + getStatusesLength(child),
0
)
: getStatusesLength(data);
return statuses ? Constants.statusHeight * statuses + 6 : 0;
};
const getContentRect = (data, isChild = false) => {
const contentSize = isChild
? Constants.childContentSize
: Constants.contentSize;
const { height } = contentSize;
const contentHeight = height + getStatusesHeight(data);
return {
...Constants.contentPosition,
width: contentSize.width,
height: contentHeight
};
};
const getNodeRect = data => {
const nodeSize = data.children
? Constants.nodeSizeWithChildren
: Constants.nodeSize;
const { width, height } = nodeSize;
const nodeHeight = height + getStatusesHeight(data);
return {
left: -width / 2,
right: width / 2,
top: -height / 2,
bottom: nodeHeight - height / 2,
width,
height: nodeHeight
};
};
export { getContentRect, getNodeRect, calculateLineLayout };

View File

@ -1,462 +0,0 @@
import React from 'react';
import { Svg } from 'normalized-styled-components';
import PropTypes from 'prop-types';
import difference from 'lodash.difference';
import differenceBy from 'lodash.differenceby';
import Baseline from '../baseline';
import Constants from './constants';
import { createSimulation } from './simulation';
import TopologyNode from './node';
import TopologyLink from './link';
import TopologyLinkArrow from './link/arrow';
import { getNodeRect, calculateLineLayout } from './functions';
const StyledSvg = Svg.extend`
width: 100%;
height: 1000px;
`;
/**
* @example ./usage.md
*/
class Topology extends React.Component {
componentWillMount() {
this.create(this.props);
}
componentDidMount() {
this.boundResize = this.handleResize.bind(this);
window.addEventListener('resize', this.boundResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.boundResize);
}
shouldComponentUpdate() {
return false;
}
getChangedConnections(services, nextServices) {
return nextServices.reduce((changed, nextService) => {
if (changed.added || changed.removed) {
return changed;
}
const service = services
.filter(service => service.id === nextService.id)
.shift();
const connectionsAdded = difference(
nextService.connections,
service.connections
).length;
// there's a new connection, we need to redraw
if (connectionsAdded) {
return { added: true };
}
const connectionsRemoved = difference(
service.connections,
nextService.connections
).length;
// we'll need to remove the offending connections from links
if (connectionsRemoved) {
return { removed: true };
}
return changed;
}, {});
}
getNextLinks(nextServices) {
const links = this.state.links;
return links.reduce((nextLinks, link) => {
const sourceExists = nextServices.filter(
nextService => nextService.id === link.source.id
);
if (sourceExists.length) {
const source = sourceExists.shift();
const targetExists = nextServices.filter(
nextService => nextService.id === link.target.id
).length;
const connectionExists = source.connections.filter(
connection => connection === link.target.id
).length;
if (targetExists && connectionExists) {
nextLinks.push(link);
}
}
return nextLinks;
}, []);
}
getNextNodes(nextServices) {
const nodes = this.state.nodes;
// let notConnectedX = 0;
return nodes.reduce((nextNodes, node) => {
const keep = nextServices.filter(
nextService => nextService.id === node.id
).length;
if (keep) {
nextNodes.push(node);
}
return nextNodes;
}, []);
}
componentWillReceiveProps(nextProps) {
// if we remove a node, it should just be removed from the simulation nodes and links
// if we add a node, then we should recreate the damn thing
// on other updates, we should update the services on the state and that's it
// we should forceUpdate once the state has been updated
const nextServices = nextProps.services.sort();
const connectedNextServices = nextServices.filter(
service => service.connected
);
const notConnectedNextServices = nextServices.filter(
service => !service.connected
);
const { services } = this.state;
if (nextServices.length > services.length) {
// new service added, we need to redraw
this.create(nextProps);
} else if (nextServices.length <= services.length) {
const servicesRemoved = differenceBy(services, nextServices, 'id');
const servicesChanged = differenceBy(nextServices, services, 'id');
if (
servicesChanged.length ||
servicesRemoved.length !== services.length - nextServices.length
) {
this.create(nextProps);
} else {
// check whether there are new connections. if so, we need to redraw
// if we just dropped one, we need to remove it from links
// comparison to yield 3 possible outcomes; no change, added, dropped
const changedConnections = this.getChangedConnections(
services,
nextServices
);
// if connections are added, we'll need to redraw
if (changedConnections.added) {
this.create(nextProps);
} else if (servicesRemoved.length || changedConnections.removed) {
const nextNodes = this.getNextNodes(connectedNextServices);
const notConnectedNodes = this.getNotConnectedNodes(
notConnectedNextServices
);
const nextLinks = this.getNextLinks(nextServices);
this.setState(
{
services: nextServices,
links: nextLinks,
nodes: nextNodes,
notConnectedNodes
},
() => this.forceUpdate()
);
} else {
// we've got the same services, no links changed, so we just need to set them to the state
this.setState({ services: nextServices }, () => this.forceUpdate());
}
}
}
}
getNotConnectedNodes(notConnectedServices) {
return notConnectedServices.map((notConnectedService, index) => {
const svgSize = this.getSvgSize();
const x = notConnectedService.isConsul
? svgSize.width - Constants.nodeSize.width
: (Constants.nodeSize.width + 10) * index;
return {
id: notConnectedService.id,
x,
y: 0
};
});
}
handleResize(evt) {
this.create(this.props);
// resize should just rejig the positions
}
create(props) {
// other updates should also just update the services rather than recreate the simulation
const services = props.services.sort();
const connectedServices = services.filter(service => service.connected);
const notConnectedServices = services.filter(service => !service.connected);
const svgSize = this.getSvgSize();
const { nodes, links, simulation } = createSimulation(
connectedServices,
svgSize
);
const notConnectedNodes = this.getNotConnectedNodes(notConnectedServices);
this.setState(
{
notConnectedNodes,
nodes,
links,
simulation,
services
},
() => {
this.forceUpdate();
}
);
}
getSvgSize() {
if (document.getElementById('topology-svg')) {
return document.getElementById('topology-svg').getBoundingClientRect();
}
const windowWidth =
window.innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth;
return {
width: windowWidth - 2 * 24,
height: 1000
};
}
constrainNodePosition(x, y, nodeRect, children = false) {
const svgSize = this.getSvgSize();
/* const nodeRect = children
? Constants.nodeRectWithChildren
: Constants.nodeRect; */
if (x < nodeRect.right + 2) {
x = nodeRect.right + 2;
} else if (x > svgSize.width + nodeRect.left - 2) {
x = svgSize.width + nodeRect.left - 2;
}
if (y < -nodeRect.top + 2) {
y = -nodeRect.top + 2;
} else if (y > svgSize.height - nodeRect.bottom - 2) {
y = svgSize.height - nodeRect.bottom - 2;
}
return {
x,
y
};
}
findNode(nodeId) {
return this.state.nodes.reduce(
(acc, simNode, index) => (simNode.id === nodeId ? simNode : acc),
{}
);
}
getConstrainedNodePosition(nodeId, nodeRect, children = false) {
const node = this.findNode(nodeId);
return this.constrainNodePosition(node.x, node.y, nodeRect, children);
}
getNotConnectedNodePosition(nodeId) {
return this.state.notConnectedNodes
.filter(ncn => ncn.id === nodeId)
.shift();
}
findNodeData(nodesData, nodeId) {
return nodesData.filter(nodeData => nodeData.id === nodeId).shift();
}
setDragInfo(dragging, nodeId = null, position = {}) {
this.dragInfo = {
dragging,
nodeId,
position
};
}
render() {
const { onQuickActionsClick, onNodeTitleClick } = this.props;
const { nodes, links, services } = this.state;
const nodesData = services.map((service, index) => {
const nodeRect = getNodeRect(service);
const nodePosition = service.connected
? this.getConstrainedNodePosition(
service.id,
nodeRect,
service.children
)
: this.getNotConnectedNodePosition(service.id);
return {
...service,
...nodePosition,
nodeRect
};
});
// TODO links will need to know whether a service has children
// if it does, the height of it will be different
const linksData = links
.map((link, index) => ({
source: this.findNodeData(nodesData, link.source.id),
target: this.findNodeData(nodesData, link.target.id)
}))
.map((linkData, index) => {
return calculateLineLayout(linkData, index);
});
const onDragStart = (evt, nodeId) => {
// It's this node's position that we'll need to update
const x = evt.changedTouches ? evt.changedTouches[0].pageX : evt.clientX;
const y = evt.changedTouches ? evt.changedTouches[0].pageY : evt.clientY;
this.setDragInfo(true, nodeId, {
x,
y
});
};
const onDragMove = evt => {
if (this.dragInfo && this.dragInfo.dragging) {
const x = evt.changedTouches
? evt.changedTouches[0].pageX
: evt.clientX;
const y = evt.changedTouches
? evt.changedTouches[0].pageY
: evt.clientY;
const offset = {
x: x - this.dragInfo.position.x,
y: y - this.dragInfo.position.y
};
const dragNodes = nodes.map((simNode, index) => {
if (simNode.id === this.dragInfo.nodeId) {
return {
...simNode,
x: simNode.x + offset.x,
y: simNode.y + offset.y
};
}
return {
...simNode
};
});
this.setState(
{
nodes: dragNodes
},
() => this.forceUpdate()
);
this.setDragInfo(true, this.dragInfo.nodeId, {
x,
y
});
}
};
const onDragEnd = evt => {
this.setDragInfo(false);
};
const renderedNode = (n, index) => (
<TopologyNode
key={index}
data={n}
index={index}
onDragStart={onDragStart}
onNodeTitleClick={onNodeTitleClick}
onQuickActions={onQuickActionsClick}
/>
);
const renderedLink = (l, index) => (
<TopologyLink key={index} data={l} index={index} />
);
const renderedLinkArrow = (l, index) => (
<TopologyLinkArrow key={index} data={l} index={index} />
);
const renderedNodes =
this.dragInfo && this.dragInfo.dragging
? nodesData
.filter((n, index) => n.id !== this.dragInfo.nodeId)
.map((n, index) => renderedNode(n, index))
: nodesData.map((n, index) => renderedNode(n, index));
const renderedLinks = linksData.map((l, index) => renderedLink(l, index));
const renderedLinkArrows =
this.dragInfo && this.dragInfo.dragging
? linksData
.filter((l, index) => l.target.id !== this.dragInfo.nodeId)
.map((l, index) => renderedLinkArrow(l, index))
: linksData.map((l, index) => renderedLinkArrow(l, index));
const dragNode =
!this.dragInfo || !this.dragInfo.dragging
? null
: renderedNode(
nodesData.reduce((dragNode, n, index) => {
if (n.id === this.dragInfo.nodeId) {
return n;
}
return dragNode;
}, {})
);
const dragLinkArrow =
!this.dragInfo ||
!this.dragInfo.dragging ||
renderedLinkArrows.length === renderedLinks.length
? null
: renderedLinkArrow(
linksData.reduce((dragLinkArrow, l, index) => {
if (l.target.id === this.dragInfo.nodeId) {
return l;
}
return dragLinkArrow;
}, {})
);
return (
<StyledSvg
onMouseMove={onDragMove}
onTouchMove={onDragMove}
onMouseUp={onDragEnd}
onTouchEnd={onDragEnd}
onTouchCancel={onDragEnd}
id="topology-svg"
>
<g>{renderedNodes}</g>
<g>{renderedLinks}</g>
<g>{renderedLinkArrows}</g>
<g>{dragNode}</g>
<g>{dragLinkArrow}</g>
</StyledSvg>
);
}
}
Topology.propTypes = {
onQuickActionsClick: PropTypes.func,
onNodeTitleClick: PropTypes.func,
services: PropTypes.array
};
export default Baseline(Topology);
export { default as TopologyNode } from './node';
export { default as TopologyLink } from './link';

View File

@ -1,26 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { GraphLinkCircle, GraphLinkArrowLine } from './shapes';
import Baseline from '../../baseline';
const GraphLinkArrow = ({ data, index }) => {
const { targetPosition, arrowAngle } = data;
return (
<g
transform={// eslint-disable-next-line max-len
`translate(${targetPosition.x}, ${targetPosition.y}) rotate(${arrowAngle})`}
>
<GraphLinkCircle cx={0} cy={0} r={9} />
<GraphLinkArrowLine x1={-1} x2={2} y1={-3} y2={0} />
<GraphLinkArrowLine x1={-1} x2={2} y1={3} y2={0} />
</g>
);
};
GraphLinkArrow.propTypes = {
data: PropTypes.object.isRequired,
index: PropTypes.number
};
export default Baseline(GraphLinkArrow);

View File

@ -1,24 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { GraphLinkLine } from './shapes';
import Baseline from '../../baseline';
const GraphLink = ({ data, index }) => {
const { sourcePosition, targetPosition } = data;
return (
<GraphLinkLine
x1={sourcePosition.x}
x2={targetPosition.x}
y1={sourcePosition.y}
y2={targetPosition.y}
/>
);
};
GraphLink.propTypes = {
data: PropTypes.object.isRequired,
index: PropTypes.number
};
export default Baseline(GraphLink);

View File

@ -1,18 +0,0 @@
import styled from 'styled-components';
export const GraphLinkLine = styled.line`
stroke: ${props => props.theme.secondaryActive};
stroke-width: 1.5;
`;
export const GraphLinkCircle = styled.circle`
stroke: ${props => props.theme.secondaryActive};
fill: ${props => props.theme.secondary};
stroke-width: 1.5;
`;
export const GraphLinkArrowLine = styled.line`
stroke: ${props => props.theme.white};
stroke-width: 2;
stroke-linecap: round;
`;

View File

@ -1,58 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import Baseline from '../../baseline';
import Constants from '../constants';
import { GraphLine, GraphButtonRect, GraphButtonCircle } from './shapes';
const NodeButton = ({ onButtonClick, index, isConsul, instancesActive }) => {
const { x, y, width, height } = Constants.buttonRect;
const buttonCircleRadius = 2;
const buttonCircleSpacing = 2;
const buttonCircleY =
(height - buttonCircleRadius * 4 - buttonCircleSpacing * 2) / 2;
const buttonCircles = [1, 2, 3].map((item, index) => (
<GraphButtonCircle
cx={width / 2}
cy={
buttonCircleY + (buttonCircleRadius * 2 + buttonCircleSpacing) * index
}
key={index}
r={2}
consul={isConsul}
active={instancesActive}
/>
));
return (
<g transform={`translate(${x}, ${y})`}>
<GraphLine
x1={0}
y1={0}
x2={0}
y2={height}
consul={isConsul}
active={instancesActive}
/>
{buttonCircles}
<GraphButtonRect
height={height}
onClick={onButtonClick}
onKeyDown={onButtonClick}
width={width}
role="button"
tabIndex={100 + index}
/>
</g>
);
};
NodeButton.propTypes = {
index: PropTypes.number.isRequired,
onButtonClick: PropTypes.func.isRequired,
isConsul: PropTypes.bool,
instancesActive: PropTypes.bool
};
export default Baseline(NodeButton);

View File

@ -1,57 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import Baseline from '../../baseline';
import Constants from '../constants';
import { GraphLine, GraphSubtitle } from './shapes';
import GraphNodeInfo from './info';
const GraphNodeContent = ({
child = false,
data,
y = Constants.contentRect.y,
index = 0
}) => {
const { x, width } = Constants.contentRect;
const nodeInfoPos = child
? {
x: Constants.infoPosition.x,
y: Constants.infoPosition.y + 21
}
: Constants.infoPosition;
const nodeSubtitle = child ? (
<GraphSubtitle
{...Constants.subtitlePosition}
consul={data.isConsul}
active={data.instancesActive}
>
{data.name}
</GraphSubtitle>
) : null;
const nodeInfo = <GraphNodeInfo data={data} pos={nodeInfoPos} />;
return (
<g transform={`translate(${x}, ${y})`}>
<GraphLine
x1={0}
y1={0}
x2={width}
y2={0}
consul={data.isConsul}
active={data.instancesActive}
/>
{nodeSubtitle}
{nodeInfo}
</g>
);
};
GraphNodeContent.propTypes = {
child: PropTypes.bool,
data: PropTypes.object.isRequired,
index: PropTypes.number
};
export default Baseline(GraphNodeContent);

View File

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="9px" height="13px" viewBox="0 0 9 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
<title>icon: data center</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill-rule="evenodd">
<g id="Project:-topology-of-services" transform="translate(-575.000000, -487.000000)">
<g id="services-copy" transform="translate(213.000000, 426.000000)">
<g id="service:-nginx" transform="translate(263.000000, 0.000000)">
<g id="metric">
<g id="data-centers-&amp;-instanecs" transform="translate(18.000000, 59.000000)">
<path d="M81,15 L90,15 L90,2 L81,2 L81,15 Z M83,13 L88,13 L88,4 L83,4 L83,13 Z M84,6 L87.001,6 L87.001,5 L84,5 L84,6 Z M84,8 L87.001,8 L87.001,7 L84,7 L84,8 Z M84,10 L87.001,10 L87.001,9 L84,9 L84,10 Z" id="icon:--data-center"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="80px" height="70px" viewBox="0 0 80 70" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
<title>icon: health</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Project:-topology-of-services" transform="translate(0, 0)" fill="#FFFFFF">
<g id="services-copy" transform="translate(0, 0)">
<g id="service:-nginx" transform="translate(0, 0)">
<g id="icon:-state" transform="translate(0, 0)">
<path d="M9.47745233,6.60270759 L8.95496861,7.04565311 L8.51133742,6.60270759 C7.70841297,5.79909747 6.40563205,5.79909747 5.60270759,6.60270759 C4.79909747,7.40631772 4.79909747,8.70841297 5.60270759,9.5120231 L8.95496861,12.8642841 L12.3668833,9.5120231 C13.1698077,8.70841297 13.2301471,7.40631772 12.4265369,6.60270759 C11.6229268,5.79909747 10.2810625,5.79909747 9.47745233,6.60270759 Z" id="icon:-health"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="18px" height="9px" viewBox="0 0 18 9" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
<title>icon: instances</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill-rule="evenodd">
<g id="Project:-topology-of-services" transform="translate(-494.000000, -489.000000)">
<g id="services-copy" transform="translate(213.000000, 426.000000)">
<g id="service:-nginx" transform="translate(263.000000, 0.000000)">
<g id="metric">
<g id="data-centers-&amp;-instanecs" transform="translate(18.000000, 59.000000)">
<path d="M4.5,4 C2.015,4 0,6.015 0,8.5 C0,10.985 2.015,13 4.5,13 C6.985,13 9,10.985 9,8.5 C9,6.015 6.985,4 4.5,4 M13.0909091,4 C12.7145455,4 12.3512727,4.047 12,4.12 C14.184,4.576 15.8181818,6.359 15.8181818,8.5 C15.8181818,10.641 14.184,12.424 12,12.88 C12.3512727,12.953 12.7145455,13 13.0909091,13 C15.8018182,13 18,10.985 18,8.5 C18,6.015 15.8018182,4 13.0909091,4 M14,8.5 C14,10.985 11.8018182,13 9.09090909,13 C8.71454545,13 8.35127273,12.953 8,12.88 C10.184,12.424 11.8181818,10.641 11.8181818,8.5 C11.8181818,6.359 10.184,4.576 8,4.12 C8.35127273,4.047 8.71454545,4 9.09090909,4 C11.8018182,4 14,6.015 14,8.5" id="icon:-instances"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,124 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import Constants from '../constants';
import { getContentRect } from '../functions';
import GraphNodeTitle from './title';
import GraphNodeButton from './button';
import GraphNodeContent from './content';
import { GraphNodeRect, GraphShadowRect } from './shapes';
import Baseline from '../../baseline';
const GraphNode = ({
data,
index,
onDragStart,
onNodeTitleClick,
onQuickActions
}) => {
const { left, top, width, height } = data.nodeRect;
const { connected, id, children, instancesActive, isConsul } = data;
let x = data.x;
let y = data.y;
if (connected) {
x = data.x + left;
y = data.y + top;
}
const onButtonClick = evt => {
const tooltipPosition = {
x: data.x + Constants.buttonRect.x + Constants.buttonRect.width / 2,
y: data.y + Constants.buttonRect.y + Constants.buttonRect.height
};
if (connected) {
tooltipPosition.x += left;
tooltipPosition.y += top;
}
const d = {
service: data,
position: {
left: tooltipPosition.x,
top: tooltipPosition.y
}
};
onQuickActions(evt, d);
};
const onTitleClick = evt => onNodeTitleClick(evt, { service: data });
const onStart = evt => {
evt.preventDefault();
onDragStart(evt, id);
};
const nodeRectEvents = connected
? {
onMouseDown: onStart,
onTouchStart: onStart
}
: {};
const nodeContent = children ? (
children.reduce(
(acc, d, i) => {
acc.children.push(
<GraphNodeContent key={i} child data={d} index={i} y={acc.y} />
);
acc.y += getContentRect(d, true).height;
return acc;
},
{ y: Constants.contentRect.y, children: [] }
).children
) : (
<GraphNodeContent data={data} />
);
const nodeShadow = instancesActive ? (
<GraphShadowRect
x={0}
y={3}
width={width}
height={height}
consul={isConsul}
active={instancesActive}
/>
) : null;
return (
<g transform={`translate(${x}, ${y})`}>
{nodeShadow}
<GraphNodeRect
x={0}
y={0}
width={width}
height={height}
consul={isConsul}
active={instancesActive}
connected={connected}
{...nodeRectEvents}
/>
<GraphNodeTitle data={data} onNodeTitleClick={onTitleClick} />
<GraphNodeButton
index={index}
onButtonClick={onButtonClick}
isConsul={isConsul}
instancesActive={instancesActive}
/>
{nodeContent}
</g>
);
};
GraphNode.propTypes = {
data: PropTypes.object.isRequired,
index: PropTypes.number.isRequired,
onDragStart: PropTypes.func,
onNodeTitleClick: PropTypes.func,
onQuickActions: PropTypes.func
};
export default Baseline(GraphNode);

View File

@ -1,96 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import is, { isNot } from 'styled-is';
import PropTypes from 'prop-types';
import Baseline from '../../baseline';
import InstancesIcon from './icon-instances.svg';
import { Point } from '../prop-types';
import { GraphText } from './shapes';
import { HealthyIcon } from '../../icons';
const StyledInstancesIcon = styled(InstancesIcon)`
fill: ${props => props.theme.white};
${is('consul')`
fill: ${props => props.theme.secondary};
`};
${isNot('active')`
fill: ${props => props.theme.secondary};
`};
`;
// const StyledDataCentresIcon = styled(DataCentresIcon)`
// fill: ${props => props.theme.white};
//
// ${is('consul')`
// fill: ${props => props.theme.secondary};
// `};
//
// ${isNot('active')`
// fill: ${props => props.theme.secondary};
// `};
// `;
const GraphNodeInfo = ({ data, pos }) => {
const {
instances,
instanceStatuses,
instancesHealthy,
isConsul,
instancesActive,
transitionalStatus,
status
} = data;
const { x, y } = pos;
const statuses = transitionalStatus ? (
<GraphText consul={isConsul} active={instancesActive}>
{status.toLowerCase()}
</GraphText>
) : (
instanceStatuses.map((instanceStatus, index) => (
<GraphText key={index} consul={isConsul} active={instancesActive}>
{`${instanceStatus.count}
${instanceStatus.status.toLowerCase()}`}
</GraphText>
))
);
const healthy = (
<HealthyIcon
healthy={
instancesHealthy && instancesHealthy.total === instancesHealthy.healthy
? 'HEALTHY'
: 'UNHEALTHY'
}
/>
);
return (
<g transform={`translate(${x}, ${y})`}>
<g transform={`translate(0, 0)`}>{healthy}</g>
<g transform={'translate(30, 4.5)'}>
<StyledInstancesIcon consul={isConsul} active={instancesActive} />
</g>
<GraphText x={54} y={14} consul={isConsul} active={instancesActive}>
{`${instances.length} inst.`}
</GraphText>
<g transform={'translate(54, 36)'}>{statuses}</g>
{/* <g transform={'translate(82, 0)'}>
<StyledDataCentresIcon connected={connected} />
</g>
<GraphText x={96} y={12} connected={connected}>
{datacenter}
</GraphText> */}
</g>
);
};
GraphNodeInfo.propTypes = {
data: PropTypes.object.isRequired,
pos: Point.isRequired
};
export default Baseline(GraphNodeInfo);

View File

@ -1,36 +0,0 @@
import React from 'react';
import Baseline from '../../baseline';
import { Point } from '../prop-types';
import { GraphText } from './shapes';
import PropTypes from 'prop-types';
const GraphNodeMetrics = ({ connected, metrics, pos }) => {
const { x, y } = pos;
const metricSpacing = 18;
const metricsText = metrics.map((metric, index) => (
<GraphText
key={index}
x={0}
y={12 + metricSpacing * index}
connected={connected}
>
{`${metric.name}: ${metric.value}`}
</GraphText>
));
return <g transform={`translate(${x}, ${y})`}>{metricsText}</g>;
};
GraphNodeMetrics.propTypes = {
connected: PropTypes.bool,
metrics: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired
})
),
pos: Point.isRequired
};
export default Baseline(GraphNodeMetrics);

View File

@ -1,124 +0,0 @@
import styled from 'styled-components';
import is, { isNot } from 'styled-is';
import typography from '../../typography';
export const GraphLine = styled.line`
stroke: ${props => props.theme.secondaryActive};
stroke-width: 1.5;
${is('consul')`
stroke: ${props => props.theme.grey};
`};
${isNot('active')`
stroke: ${props => props.theme.grey};
`};
`;
export const GraphNodeRect = styled.rect`
fill: ${props => props.theme.secondary};
stroke: ${props => props.theme.secondaryActive};
stroke-width: 1.5;
rx: 4;
ry: 4;
${is('consul')`
stroke: ${props => props.theme.grey};
fill: ${props => props.theme.white};
`};
${isNot('active')`
stroke: ${props => props.theme.grey};
fill: ${props => props.theme.whiteActive};
`};
${is('connected')`
cursor: move;
`};
`;
export const GraphShadowRect = styled.rect`
fill: ${props => props.theme.secondary};
opacity: 0.33;
rx: 4;
ry: 4;
${is('consul')`
fill: ${props => props.theme.grey};
`};
`;
export const GraphTitle = styled.text`
${typography.normal};
font-size: 16px;
font-weight: 600;
fill: ${props => props.theme.white};
${is('consul')`
fill: ${props => props.theme.secondary};
`};
${isNot('active')`
fill: ${props => props.theme.secondary};
`};
cursor: pointer;
`;
export const GraphSubtitle = styled.text`
${typography.normal};
font-size: 12px;
font-weight: 600;
fill: ${props => props.theme.white};
${is('consul')`
fill: ${props => props.theme.secondary};
`};
${isNot('active')`
fill: ${props => props.theme.secondary};
`};
`;
export const GraphText = styled.text`
${typography.normal};
font-size: 12px;
fill: ${props => props.theme.white};
opacity: 0.8;
${is('consul')`
fill: ${props => props.theme.secondary};
`};
${isNot('active')`
fill: ${props => props.theme.secondary};
`};
`;
export const GraphButtonRect = styled.rect`
cursor: pointer;
opacity: 0;
&:focus {
outline: none;
}
`;
export const GraphButtonCircle = styled.circle`
fill: ${props => props.theme.white};
${is('consul')`
fill: ${props => props.theme.secondary};
`};
${isNot('active')`
fill: ${props => props.theme.secondary};
`};
`;
export const GraphHealthyCircle = styled.circle`
fill: ${props => props.theme.green};
`;

View File

@ -1,31 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import Baseline from '../../baseline';
import Constants from '../constants';
import { GraphTitle } from './shapes';
const GraphNodeTitle = ({ data, onNodeTitleClick }) => (
<g>
<GraphTitle
x={Constants.paddingLeft}
y={30}
onClick={onNodeTitleClick}
onKeyDown={onNodeTitleClick}
consul={data.isConsul}
active={data.instancesActive}
>
{data.name}
</GraphTitle>
{/* <g transform={`translate(${115}, ${15})`}>
<GraphHealthyCircle cx={9} cy={9} r={9} />
<HeartIcon />
</g> */}
</g>
);
GraphNodeTitle.propTypes = {
data: PropTypes.object.isRequired,
onNodeTitleClick: PropTypes.func
};
export default Baseline(GraphNodeTitle);

View File

@ -1,26 +0,0 @@
import PropTypes from 'prop-types';
const p = {
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired
};
const s = {
width: PropTypes.number,
height: PropTypes.number
};
const Point = PropTypes.shape({
...p
});
const Size = PropTypes.shape({
...s
});
const Rect = PropTypes.shape({
...p,
...s
});
export { Point, Rect, Size };

View File

@ -1,140 +0,0 @@
import { forceSimulation, forceLink, forceCollide, forceCenter } from 'd3';
import Constants from './constants';
const hypotenuse = (a, b) => Math.sqrt(a * a + b * b);
const rectRadius = ({ width, height }) =>
Math.round(hypotenuse(width, height) / 2);
const forcePlayAnimation = (simulation, animationTicks) => {
const n =
Math.ceil(
Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay())
) + 100; // - animationTicks;
for (let i = 0; i < n; ++i) {
simulation.tick();
}
};
const createLinks = services =>
services.reduce(
(acc, service, index) =>
service.connections
? acc.concat(
service.connections.reduce((connections, connection, index) => {
const targetExists = services.filter(
service => service.id === connection
).length;
if (targetExists) {
connections.push({
source: service.id,
target: connection
});
}
return connections;
}, [])
)
: acc,
[]
);
const createSimulation = (services, svgSize, animationTicks = 0) => {
// This is not going to work given that as well as the d3 layout stuff, other things might be at play too
// We should pass two objects to the components - one for positioning and one for data
const nodes = services.map((service, index) => {
return {
id: service.id,
index
};
});
const links = createLinks(services);
const { width, height } = svgSize;
const nodeRadius = rectRadius(Constants.nodeSizeWithChildren);
const simulation = forceSimulation(nodes)
.force('link', forceLink(links).id(d => d.id))
.force('collide', forceCollide(nodeRadius))
.force('center', forceCenter(width / 2, height / 2));
forcePlayAnimation(simulation, animationTicks);
return {
nodes,
links,
simulation
};
};
// TODO we need to kill the previous simulation
const updateSimulation = (
simulation,
services,
simNodes,
simLinks,
svgSize,
onTick,
onEnd
) => {
const nodes = services.map((service, index) => {
const simNode = simNodes.reduce((acc, n, i) => {
return service.id === n.id ? n : acc;
}, null);
return simNode
? {
id: simNode.id,
// Fx: simNode.x,
// fy: simNode.y,
index
}
: {
id: service.id,
index
};
});
const links = createLinks(services);
const { width, height } = svgSize;
const nodeRadius = rectRadius(Constants.nodeSizeWithChildren);
return {
simulation: forceSimulation(nodes)
.force('link', forceLink(links).id(d => d.id))
.force('collide', forceCollide(nodeRadius))
.force('center', forceCenter(width / 2, height / 2))
.on('tick', onTick)
.on('end', onEnd),
nodes,
links
};
};
export { createSimulation, updateSimulation };
/*
Const simulation = forceSimulation(dataNodes)
// .alpha(1).alphaDecay(0.1)
// .force('charge', forceManyBody())
.force('link', forceLink(dataLinks)
//.distance(() => linkDistance)
.id(d => d.id))
.force('collide', forceCollide(nodeRadius))
.force('center', forceCenter(1024/2, 860/2))
.on('tick', () => {
console.log('SIMULATION TICK');
console.log('tickCounter = ', tickCounter);
tickCounter++;
this.forceUpdate();
})
.on('end', () => {
console.log('SIMULATION END');
console.log('tickCounter = ', tickCounter);
// this.forceUpdate();
})
*/

View File

@ -1,94 +0,0 @@
```
<Topology services=
{[
{
"index": 0,
"id": "af6a5cd2-291f-490b-bf3b-141b010635db",
"name": "frontend",
"slug": "frontend",
"status": "ACTIVE",
"__typename": "Service",
"branches": [],
"connections": [
"aea06a05-830a-46d3-bdc1-9dcba97303de"
],
"instances": [
{
"id": "f1fb3c1d-9e0e-4538-b2ad-1124bce2459e",
"status": "RUNNING",
"healthy": "UNKNOWN",
"__typename": "Instance"
},
{
"id": "c5c7ae33-cfe1-43cc-9e9b-6f453de3888d",
"status": "FAILED",
"healthy": "UNAVAILABLE",
"__typename": "Instance"
}
],
"instanceStatuses": [
{
"status": "RUNNING",
"count": 1
},
{
"status": "FAILED",
"count": 1
}
],
"instancesActive": true,
"instancesHealthy": {
"total": 2,
"healthy": 0
},
"transitionalStatus": false,
"isConsul": false,
"connected": true
},
{
"index": 1,
"id": "af6a5cd2-291f-490b-bf3b-asdasads",
"name": "consul",
"slug": "consul",
"status": "ACTIVE",
"__typename": "Service",
"branches": [],
"connections": [
"aea06a05-830a-46d3-bdc1-9dcba97303de"
],
"instances": [
{
"id": "f1fb3c1d-9e0e-4538-b2ad-1124bce2459e",
"status": "RUNNING",
"healthy": "UNKNOWN",
"__typename": "Instance"
},
{
"id": "c5c7ae33-cfe1-43cc-9e9b-6f453de3888d",
"status": "FAILED",
"healthy": "UNAVAILABLE",
"__typename": "Instance"
}
],
"instanceStatuses": [
{
"status": "RUNNING",
"count": 1
},
{
"status": "RUNNING",
"count": 1
}
],
"instancesActive": true,
"instancesHealthy": {
"total": 2,
"healthy": 2
},
"transitionalStatus": false,
"isConsul": true,
"connected": true
}
]
} />
```

View File

@ -105,8 +105,7 @@ module.exports = {
'src/form/radio.js',
'src/section-list/index.js',
'src/form/select.js',
'src/form/toggle.js',
'src/topology/index.js'
'src/form/toggle.js'
]
}
],

664
yarn.lock

File diff suppressed because it is too large Load Diff