mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 10:00:01 -03:30
Topology changes:
- add new node and link states
- add directionality to links
- update icons
This commit is contained in:
parent
d2c63a9b36
commit
4bf9925cf7
@ -11,6 +11,9 @@ import {
|
||||
renderLabelText,
|
||||
renderNodeType,
|
||||
renderNodeIcon,
|
||||
renderLinkState,
|
||||
renderLabelIcons,
|
||||
renderIconPosition,
|
||||
redirectToDetailsPage,
|
||||
getHeight,
|
||||
getWidth,
|
||||
@ -20,7 +23,8 @@ import {
|
||||
DEFAULT_RADIUS,
|
||||
DEFAULT_NODE_COLOR,
|
||||
DEFAULT_NODE_HIGHLIGHT_COLOR,
|
||||
DEFAULT_NODE_LABEL_TEXT_COLOR,
|
||||
DEFAULT_NODE_SYMBOL_TEXT_COLOR,
|
||||
DEFAULT_NODE_STROKE_COLOR,
|
||||
DEFAULT_FONT_SIZE,
|
||||
SELECTOR,
|
||||
} from './constants';
|
||||
@ -95,6 +99,24 @@ function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
|
||||
.forceSimulation(nodes)
|
||||
.force('center', d3.forceCenter(width / 2, height / 2));
|
||||
simulation.tick();
|
||||
// build the arrow.
|
||||
mesh
|
||||
.append('defs')
|
||||
.selectAll('marker')
|
||||
.data(['end', 'end-active'])
|
||||
.join('marker')
|
||||
.attr('id', String)
|
||||
.attr('viewBox', '0 -5 10 10')
|
||||
.attr('refY', 0)
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M0,-5L10,0L0,5');
|
||||
|
||||
mesh.select('#end').attr('refX', 23).attr('fill', '#ccc');
|
||||
mesh.select('#end-active').attr('refX', 18).attr('fill', '#0066CC');
|
||||
|
||||
// Add links
|
||||
mesh
|
||||
.append('g')
|
||||
@ -108,11 +130,13 @@ function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
|
||||
.attr('y1', (d) => d.source.y)
|
||||
.attr('x2', (d) => d.target.x)
|
||||
.attr('y2', (d) => d.target.y)
|
||||
.attr('marker-end', 'url(#end)')
|
||||
.attr('class', (_, i) => `link-${i}`)
|
||||
.attr('data-cy', (d) => `${d.source.hostname}-${d.target.hostname}`)
|
||||
.style('fill', 'none')
|
||||
.style('stroke', '#ccc')
|
||||
.style('stroke-width', '2px')
|
||||
.style('stroke-dasharray', (d) => renderLinkState(d.link_state))
|
||||
.attr('pointer-events', 'none')
|
||||
.on('mouseover', function showPointer() {
|
||||
d3.select(this).transition().style('cursor', 'pointer');
|
||||
@ -147,7 +171,7 @@ function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
|
||||
.attr('class', (d) => d.node_type)
|
||||
.attr('class', (d) => `id-${d.id}`)
|
||||
.attr('fill', DEFAULT_NODE_COLOR)
|
||||
.attr('stroke', DEFAULT_NODE_LABEL_TEXT_COLOR);
|
||||
.attr('stroke', DEFAULT_NODE_STROKE_COLOR);
|
||||
|
||||
// node type labels
|
||||
node
|
||||
@ -157,41 +181,65 @@ function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
|
||||
.attr('y', (d) => d.y)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'central')
|
||||
.attr('fill', DEFAULT_NODE_LABEL_TEXT_COLOR);
|
||||
.attr('fill', DEFAULT_NODE_SYMBOL_TEXT_COLOR);
|
||||
|
||||
// node hostname labels
|
||||
const hostNames = node.append('g');
|
||||
hostNames
|
||||
const placeholder = node.append('g').attr('class', 'placeholder');
|
||||
|
||||
placeholder
|
||||
.append('text')
|
||||
.text((d) => renderLabelText(d.node_state, d.hostname))
|
||||
.attr('x', (d) => d.x)
|
||||
.attr('y', (d) => d.y + 40)
|
||||
.text((d) => renderLabelText(d.node_state, d.hostname))
|
||||
.attr('class', 'placeholder')
|
||||
.attr('fill', DEFAULT_NODE_LABEL_TEXT_COLOR)
|
||||
.attr('fill', 'black')
|
||||
.attr('font-size', '18px')
|
||||
.attr('text-anchor', 'middle')
|
||||
.each(function calculateLabelWidth() {
|
||||
// eslint-disable-next-line react/no-this-in-sfc
|
||||
const bbox = this.getBBox();
|
||||
// eslint-disable-next-line react/no-this-in-sfc
|
||||
d3.select(this.parentNode)
|
||||
.append('rect')
|
||||
.attr('x', bbox.x)
|
||||
.attr('y', bbox.y)
|
||||
.attr('width', bbox.width)
|
||||
.attr('height', bbox.height)
|
||||
.attr('rx', 8)
|
||||
.attr('ry', 8)
|
||||
.style('fill', (d) => renderStateColor(d.node_state));
|
||||
.append('path')
|
||||
.attr('d', (d) => renderLabelIcons(d.node_state))
|
||||
.attr('transform', (d) => renderIconPosition(d.node_state, bbox))
|
||||
.style('fill', 'black');
|
||||
});
|
||||
svg.selectAll('text.placeholder').remove();
|
||||
|
||||
placeholder.each(function calculateLabelWidth() {
|
||||
// eslint-disable-next-line react/no-this-in-sfc
|
||||
const bbox = this.getBBox();
|
||||
// eslint-disable-next-line react/no-this-in-sfc
|
||||
d3.select(this.parentNode)
|
||||
.append('rect')
|
||||
.attr('x', (d) => d.x - bbox.width / 2)
|
||||
.attr('y', bbox.y + 5)
|
||||
.attr('width', bbox.width)
|
||||
.attr('height', bbox.height)
|
||||
.attr('rx', 8)
|
||||
.attr('ry', 8)
|
||||
.style('fill', (d) => renderStateColor(d.node_state));
|
||||
});
|
||||
|
||||
const hostNames = node.append('g');
|
||||
hostNames
|
||||
.append('text')
|
||||
.attr('x', (d) => d.x)
|
||||
.attr('y', (d) => d.y + 38)
|
||||
.text((d) => renderLabelText(d.node_state, d.hostname))
|
||||
.attr('x', (d) => d.x + 6)
|
||||
.attr('y', (d) => d.y + 42)
|
||||
.attr('fill', 'white')
|
||||
.attr('font-size', DEFAULT_FONT_SIZE)
|
||||
.attr('fill', DEFAULT_NODE_LABEL_TEXT_COLOR)
|
||||
.attr('text-anchor', 'middle');
|
||||
.attr('text-anchor', 'middle')
|
||||
.each(function calculateLabelWidth() {
|
||||
// eslint-disable-next-line react/no-this-in-sfc
|
||||
const bbox = this.getBBox();
|
||||
// eslint-disable-next-line react/no-this-in-sfc
|
||||
d3.select(this.parentNode)
|
||||
.append('path')
|
||||
.attr('class', (d) => `icon-${d.node_state}`)
|
||||
.attr('d', (d) => renderLabelIcons(d.node_state))
|
||||
.attr('transform', (d) => renderIconPosition(d.node_state, bbox))
|
||||
.attr('fill', 'white');
|
||||
});
|
||||
svg.selectAll('g.placeholder').remove();
|
||||
|
||||
svg.call(zoom);
|
||||
|
||||
@ -208,7 +256,8 @@ function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
|
||||
.selectAll(`.link-${s.index}`)
|
||||
.transition()
|
||||
.style('stroke', '#0066CC')
|
||||
.style('stroke-width', '3px');
|
||||
.style('stroke-width', '3px')
|
||||
.attr('marker-end', 'url(#end-active)');
|
||||
});
|
||||
}
|
||||
|
||||
@ -222,25 +271,33 @@ function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
|
||||
svg
|
||||
.selectAll(`.link-${s.index}`)
|
||||
.transition()
|
||||
.duration(50)
|
||||
.style('stroke', '#ccc')
|
||||
.style('stroke-width', '2px');
|
||||
.style('stroke-width', '2px')
|
||||
.attr('marker-end', 'url(#end)');
|
||||
});
|
||||
}
|
||||
|
||||
function highlightSelected(n) {
|
||||
if (svg.select(`circle.id-${n.id}`).attr('stroke-width') !== null) {
|
||||
// toggle rings
|
||||
svg.select(`circle.id-${n.id}`).attr('stroke-width', null);
|
||||
svg
|
||||
.select(`circle.id-${n.id}`)
|
||||
.attr('stroke', '#ccc')
|
||||
.attr('stroke-width', null);
|
||||
// show default empty state of tooltip
|
||||
setIsNodeSelected(false);
|
||||
setSelectedNode(null);
|
||||
return;
|
||||
}
|
||||
svg.selectAll('circle').attr('stroke-width', null);
|
||||
svg
|
||||
.selectAll('circle')
|
||||
.attr('stroke', '#ccc')
|
||||
.attr('stroke-width', null);
|
||||
svg
|
||||
.select(`circle.id-${n.id}`)
|
||||
.attr('stroke-width', '5px')
|
||||
.attr('stroke', '#D2D2D2');
|
||||
.attr('stroke', '#0066CC');
|
||||
setIsNodeSelected(true);
|
||||
setSelectedNode(n);
|
||||
}
|
||||
|
||||
@ -9,21 +9,22 @@ export const MESH_FORCE_LAYOUT = {
|
||||
defaultForceX: 0,
|
||||
defaultForceY: 0,
|
||||
};
|
||||
export const DEFAULT_NODE_COLOR = '#0066CC';
|
||||
export const DEFAULT_NODE_HIGHLIGHT_COLOR = '#16407C';
|
||||
export const DEFAULT_NODE_COLOR = 'white';
|
||||
export const DEFAULT_NODE_HIGHLIGHT_COLOR = '#eee';
|
||||
export const DEFAULT_NODE_LABEL_TEXT_COLOR = 'white';
|
||||
export const DEFAULT_NODE_SYMBOL_TEXT_COLOR = 'black';
|
||||
export const DEFAULT_NODE_STROKE_COLOR = '#ccc';
|
||||
export const DEFAULT_FONT_SIZE = '12px';
|
||||
export const LABEL_TEXT_MAX_LENGTH = 15;
|
||||
export const MARGIN = 15;
|
||||
export const NODE_STATE_COLOR_KEY = {
|
||||
disabled: '#6A6E73',
|
||||
healthy: '#3E8635',
|
||||
error: '#C9190B',
|
||||
};
|
||||
export const NODE_STATE_HTML_ENTITY_KEY = {
|
||||
disabled: '\u25EF',
|
||||
healthy: '\u2713',
|
||||
error: '\u0021',
|
||||
unavailable: '#F0AB00',
|
||||
ready: '#3E8635',
|
||||
'provision-fail': '#C9190B',
|
||||
'deprovision-fail': '#C9190B',
|
||||
installed: '#0066CC',
|
||||
provisioning: '#666',
|
||||
deprovisioning: '#666',
|
||||
};
|
||||
|
||||
export const NODE_TYPE_SYMBOL_KEY = {
|
||||
@ -32,3 +33,17 @@ export const NODE_TYPE_SYMBOL_KEY = {
|
||||
hybrid: 'Hy',
|
||||
control: 'C',
|
||||
};
|
||||
|
||||
export const ICONS = {
|
||||
clock:
|
||||
'M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm61.8-104.4l-84.9-61.7c-3.1-2.3-4.9-5.9-4.9-9.7V116c0-6.6 5.4-12 12-12h32c6.6 0 12 5.4 12 12v141.7l66.8 48.6c5.4 3.9 6.5 11.4 2.6 16.8L334.6 349c-3.9 5.3-11.4 6.5-16.8 2.6z',
|
||||
checkmark:
|
||||
'M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z',
|
||||
exclaimation:
|
||||
'M176 432c0 44.112-35.888 80-80 80s-80-35.888-80-80 35.888-80 80-80 80 35.888 80 80zM25.26 25.199l13.6 272C39.499 309.972 50.041 320 62.83 320h66.34c12.789 0 23.331-10.028 23.97-22.801l13.6-272C167.425 11.49 156.496 0 142.77 0H49.23C35.504 0 24.575 11.49 25.26 25.199z',
|
||||
minus:
|
||||
'M416 208H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h384c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z',
|
||||
plus: 'M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z',
|
||||
empty:
|
||||
'M512,896 C300.2,896 128,723.9 128,512 C128,300.3 300.2,128 512,128 C723.7,128 896,300.2 896,512 C896,723.8 723.7,896 512,896 L512,896 Z M512.1,0 C229.7,0 0,229.8 0,512 C0,794.3 229.8,1024 512.1,1024 C794.4,1024 1024,794.3 1024,512 C1024,229.7 794.4,0 512.1,0 L512.1,0 Z',
|
||||
};
|
||||
|
||||
@ -3,9 +3,9 @@ import { truncateString } from '../../../util/strings';
|
||||
|
||||
import {
|
||||
NODE_STATE_COLOR_KEY,
|
||||
NODE_STATE_HTML_ENTITY_KEY,
|
||||
NODE_TYPE_SYMBOL_KEY,
|
||||
LABEL_TEXT_MAX_LENGTH,
|
||||
ICONS,
|
||||
} from '../constants';
|
||||
|
||||
export function getWidth(selector) {
|
||||
@ -22,12 +22,7 @@ export function renderStateColor(nodeState) {
|
||||
|
||||
export function renderLabelText(nodeState, name) {
|
||||
if (typeof nodeState === 'string' && typeof name === 'string') {
|
||||
return NODE_STATE_HTML_ENTITY_KEY[nodeState]
|
||||
? `${NODE_STATE_HTML_ENTITY_KEY[nodeState]} ${truncateString(
|
||||
name,
|
||||
LABEL_TEXT_MAX_LENGTH
|
||||
)}`
|
||||
: ` ${truncateString(name, LABEL_TEXT_MAX_LENGTH)}`;
|
||||
return `${truncateString(name, LABEL_TEXT_MAX_LENGTH)}`;
|
||||
}
|
||||
return ``;
|
||||
}
|
||||
@ -44,6 +39,41 @@ export function renderNodeIcon(selectedNode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function renderLabelIcons(nodeState) {
|
||||
if (nodeState) {
|
||||
const nodeLabelIconMapper = {
|
||||
unavailable: 'empty',
|
||||
ready: 'checkmark',
|
||||
installed: 'clock',
|
||||
'provision-fail': 'exclaimation',
|
||||
'deprovision-fail': 'exclaimation',
|
||||
provisioning: 'plus',
|
||||
deprovisioning: 'minus',
|
||||
};
|
||||
return ICONS[nodeLabelIconMapper[nodeState]]
|
||||
? ICONS[nodeLabelIconMapper[nodeState]]
|
||||
: ``;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
export function renderIconPosition(nodeState, bbox) {
|
||||
if (nodeState) {
|
||||
const iconPositionMapper = {
|
||||
unavailable: `translate(${bbox.x - 12}, ${bbox.y + 3}), scale(0.01)`,
|
||||
ready: `translate(${bbox.x - 15}, ${bbox.y + 3}), scale(0.02)`,
|
||||
installed: `translate(${bbox.x - 18}, ${bbox.y + 1}), scale(0.03)`,
|
||||
'provision-fail': `translate(${bbox.x - 9}, ${bbox.y + 3}), scale(0.02)`,
|
||||
'deprovision-fail': `translate(${bbox.x - 9}, ${
|
||||
bbox.y + 3
|
||||
}), scale(0.02)`,
|
||||
provisioning: `translate(${bbox.x - 12}, ${bbox.y + 3}), scale(0.02)`,
|
||||
deprovisioning: `translate(${bbox.x - 12}, ${bbox.y + 3}), scale(0.02)`,
|
||||
};
|
||||
return iconPositionMapper[nodeState] ? iconPositionMapper[nodeState] : ``;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function redirectToDetailsPage(selectedNode, history) {
|
||||
if (selectedNode && history) {
|
||||
const { id: nodeId } = selectedNode;
|
||||
@ -53,6 +83,14 @@ export function redirectToDetailsPage(selectedNode, history) {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function renderLinkState(linkState) {
|
||||
const linkPattern = {
|
||||
established: null,
|
||||
adding: 3,
|
||||
removing: 3,
|
||||
};
|
||||
return linkPattern[linkState] ? linkPattern[linkState] : null;
|
||||
}
|
||||
// DEBUG TOOLS
|
||||
export function getRandomInt(min, max) {
|
||||
min = Math.ceil(min);
|
||||
@ -62,13 +100,20 @@ export function getRandomInt(min, max) {
|
||||
|
||||
const generateRandomLinks = (n, r) => {
|
||||
const links = [];
|
||||
function getRandomLinkState() {
|
||||
return ['established', 'adding', 'removing'][getRandomInt(0, 2)];
|
||||
}
|
||||
for (let i = 0; i < r; i++) {
|
||||
const link = {
|
||||
source: n[getRandomInt(0, n.length - 1)].hostname,
|
||||
target: n[getRandomInt(0, n.length - 1)].hostname,
|
||||
link_state: getRandomLinkState(),
|
||||
};
|
||||
links.push(link);
|
||||
if (link.source !== link.target) {
|
||||
links.push(link);
|
||||
}
|
||||
}
|
||||
|
||||
return { nodes: n, links };
|
||||
};
|
||||
|
||||
@ -78,7 +123,15 @@ export const generateRandomNodes = (n) => {
|
||||
return ['hybrid', 'execution', 'control', 'hop'][getRandomInt(0, 3)];
|
||||
}
|
||||
function getRandomState() {
|
||||
return ['healthy', 'error', 'disabled'][getRandomInt(0, 2)];
|
||||
return [
|
||||
'ready',
|
||||
'provisioning',
|
||||
'deprovisioning',
|
||||
'installed',
|
||||
'unavailable',
|
||||
'provision-fail',
|
||||
'deprovision-fail',
|
||||
][getRandomInt(0, 6)];
|
||||
}
|
||||
for (let i = 0; i < n; i++) {
|
||||
const id = i + 1;
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
|
||||
describe('renderStateColor', () => {
|
||||
test('returns correct node state color', () => {
|
||||
expect(renderStateColor('healthy')).toBe('#3E8635');
|
||||
expect(renderStateColor('ready')).toBe('#3E8635');
|
||||
});
|
||||
test('returns empty string if state is not found', () => {
|
||||
expect(renderStateColor('foo')).toBe('');
|
||||
@ -68,10 +68,10 @@ describe('getHeight', () => {
|
||||
});
|
||||
describe('renderLabelText', () => {
|
||||
test('returns label text correctly', () => {
|
||||
expect(renderLabelText('error', 'foo')).toBe('! foo');
|
||||
expect(renderLabelText('error', 'foo')).toBe('foo');
|
||||
});
|
||||
test('returns label text if invalid node state is passed', () => {
|
||||
expect(renderLabelText('foo', 'bar')).toBe(' bar');
|
||||
expect(renderLabelText('foo', 'bar')).toBe('bar');
|
||||
});
|
||||
test('returns empty string if non string params are passed', () => {
|
||||
expect(renderLabelText(0, null)).toBe('');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user