mirror of
https://github.com/ansible/awx.git
synced 2026-01-17 04:31:21 -03:30
feature hop node topology updates (#14142)
This commit is contained in:
parent
ed2a59c1a3
commit
fdb359a67b
@ -148,6 +148,7 @@ function InstanceForm({
|
||||
node_state: instance.node_state || 'installed',
|
||||
listener_port: instance.listener_port || 27199,
|
||||
enabled: instance.enabled || true,
|
||||
managed_by_policy: instance.managed_by_policy || true,
|
||||
peers_from_control_nodes: instance.peers_from_control_nodes
|
||||
? true
|
||||
: !isEdit,
|
||||
|
||||
@ -33,7 +33,8 @@ function RemoveInstanceButton({ itemsToRemove, onRemove, isK8s }) {
|
||||
const [removeDetails, setRemoveDetails] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const cannotRemove = (item) => !(item.node_type === 'execution' || item.node_type === 'hop');
|
||||
const cannotRemove = (item) =>
|
||||
!(item.node_type === 'execution' || item.node_type === 'hop');
|
||||
|
||||
const toggleModal = async (isOpen) => {
|
||||
setRemoveDetails(null);
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
SearchPlusIcon,
|
||||
ExpandArrowsAltIcon,
|
||||
ExpandIcon,
|
||||
RedoAltIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
|
||||
const Header = ({
|
||||
@ -26,6 +27,7 @@ const Header = ({
|
||||
zoomOut,
|
||||
resetZoom,
|
||||
zoomFit,
|
||||
refresh,
|
||||
showZoomControls,
|
||||
}) => {
|
||||
const { light } = PageSectionVariants;
|
||||
@ -48,6 +50,18 @@ const Header = ({
|
||||
</Title>
|
||||
</div>
|
||||
<div>
|
||||
<Tooltip content={t`Refresh`} position="top">
|
||||
<Button
|
||||
ouiaId="refresh-button"
|
||||
aria-label={t`Refresh`}
|
||||
variant="plain"
|
||||
icon={<RedoAltIcon />}
|
||||
onClick={refresh}
|
||||
isDisabled={!showZoomControls}
|
||||
>
|
||||
<RedoAltIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip content={t`Zoom in`} position="top">
|
||||
<Button
|
||||
ouiaId="zoom-in-button"
|
||||
|
||||
@ -245,7 +245,7 @@ function Legend() {
|
||||
y1="9"
|
||||
x2="20"
|
||||
y2="9"
|
||||
stroke="#666"
|
||||
stroke="#6A6E73"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
</svg>
|
||||
@ -260,8 +260,9 @@ function Legend() {
|
||||
y1="9"
|
||||
x2="20"
|
||||
y2="9"
|
||||
stroke="#c9700b"
|
||||
stroke="#ccc"
|
||||
strokeWidth="4"
|
||||
strokeDasharray="6"
|
||||
/>
|
||||
</svg>
|
||||
</DescriptionListTerm>
|
||||
@ -275,7 +276,7 @@ function Legend() {
|
||||
y1="9"
|
||||
x2="20"
|
||||
y2="9"
|
||||
stroke="#666"
|
||||
stroke="#3E8635"
|
||||
strokeWidth="4"
|
||||
strokeDasharray="6"
|
||||
/>
|
||||
|
||||
@ -13,6 +13,7 @@ import Tooltip from './Tooltip';
|
||||
import ContentLoading from './ContentLoading';
|
||||
import {
|
||||
renderStateColor,
|
||||
renderLinkStatusColor,
|
||||
renderLabelText,
|
||||
renderNodeType,
|
||||
renderNodeIcon,
|
||||
@ -177,7 +178,13 @@ function MeshGraph({
|
||||
mesh
|
||||
.append('defs')
|
||||
.selectAll('marker')
|
||||
.data(['end', 'end-active'])
|
||||
.data([
|
||||
'end',
|
||||
'end-active',
|
||||
'end-disconnected',
|
||||
'end-adding',
|
||||
'end-removing',
|
||||
])
|
||||
.join('marker')
|
||||
.attr('id', String)
|
||||
.attr('viewBox', '0 -5 10 10')
|
||||
@ -187,8 +194,10 @@ function MeshGraph({
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M0,-5L10,0L0,5');
|
||||
|
||||
mesh.select('#end').attr('refX', 23).attr('fill', '#ccc');
|
||||
mesh.select('#end').attr('refX', 23).attr('fill', '#6A6E73');
|
||||
mesh.select('#end-removing').attr('refX', 23).attr('fill', '#C9190B');
|
||||
mesh.select('#end-adding').attr('refX', 23).attr('fill', '#3E8635');
|
||||
mesh.select('#end-disconnected').attr('refX', 23).attr('fill', '#CCC');
|
||||
mesh.select('#end-active').attr('refX', 18).attr('fill', '#0066CC');
|
||||
|
||||
// Add links
|
||||
@ -204,24 +213,27 @@ function MeshGraph({
|
||||
.attr('y1', (d) => d.source.y)
|
||||
.attr('x2', (d) => d.target.x)
|
||||
.attr('y2', (d) => d.target.y)
|
||||
.attr('marker-end', 'url(#end)')
|
||||
.attr('marker-end', (d) => {
|
||||
if (d.link_state === 'disconnected') {
|
||||
return 'url(#end-disconnected)';
|
||||
}
|
||||
if (d.link_state === 'adding') {
|
||||
return 'url(#end-adding)';
|
||||
}
|
||||
if (d.link_state === 'removing') {
|
||||
return 'url(#end-removing)';
|
||||
}
|
||||
return 'url(#end)';
|
||||
})
|
||||
.attr('class', (_, i) => `link-${i}`)
|
||||
.attr('data-cy', (d) => `${d.source.hostname}-${d.target.hostname}`)
|
||||
.style('fill', 'none')
|
||||
.style('stroke', (d) => {
|
||||
if (d.link_state === 'removing') {
|
||||
return '#C9190B';
|
||||
}
|
||||
if (d.link_state === 'disconnected') {
|
||||
return '#c9700b';
|
||||
}
|
||||
return '#CCC';
|
||||
})
|
||||
.style('stroke', (d) => renderLinkStatusColor(d.link_state))
|
||||
.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');
|
||||
d3.select(this).style('cursor', 'pointer');
|
||||
});
|
||||
// add nodes
|
||||
const node = mesh
|
||||
@ -234,7 +246,7 @@ function MeshGraph({
|
||||
.append('g')
|
||||
.attr('data-cy', (d) => `node-${d.id}`)
|
||||
.on('mouseenter', function handleNodeHover(_, d) {
|
||||
d3.select(this).transition().style('cursor', 'pointer');
|
||||
d3.select(this).style('cursor', 'pointer');
|
||||
highlightSiblings(d);
|
||||
})
|
||||
.on('mouseleave', (_, d) => {
|
||||
@ -245,7 +257,8 @@ function MeshGraph({
|
||||
});
|
||||
|
||||
// node circles
|
||||
node
|
||||
const nodeCircles = node.append('g');
|
||||
nodeCircles
|
||||
.append('circle')
|
||||
.attr('r', DEFAULT_RADIUS)
|
||||
.attr('cx', (d) => d.x)
|
||||
@ -254,7 +267,8 @@ function MeshGraph({
|
||||
.attr('class', (d) => `id-${d.id}`)
|
||||
.attr('fill', DEFAULT_NODE_COLOR)
|
||||
.attr('stroke-dasharray', (d) => (d.enabled ? `1 0` : `5`))
|
||||
.attr('stroke', DEFAULT_NODE_STROKE_COLOR);
|
||||
.attr('stroke', (d) => renderStateColor(d.node_state));
|
||||
|
||||
// node type labels
|
||||
node
|
||||
.append('text')
|
||||
@ -265,64 +279,62 @@ function MeshGraph({
|
||||
.attr('dominant-baseline', 'central')
|
||||
.attr('fill', DEFAULT_NODE_SYMBOL_TEXT_COLOR);
|
||||
|
||||
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)
|
||||
.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('path')
|
||||
.attr('d', (d) => renderLabelIcons(d.node_state))
|
||||
.attr('transform', (d) => renderIconPosition(d.node_state, bbox))
|
||||
.style('fill', 'black');
|
||||
});
|
||||
|
||||
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');
|
||||
// node hostname labels
|
||||
const hostNames = node.append('g').attr('class', 'node-state-label');
|
||||
hostNames
|
||||
.append('text')
|
||||
.attr('x', (d) => d.x)
|
||||
.attr('y', (d) => d.y + 40)
|
||||
.text((d) => renderLabelText(d.node_state, d.hostname))
|
||||
.attr('x', (d) => d.x + 6)
|
||||
.attr('y', (d) => d.y + 42)
|
||||
.attr('class', 'placeholder')
|
||||
.attr('fill', 'white')
|
||||
.attr('font-size', DEFAULT_FONT_SIZE)
|
||||
.attr('text-anchor', 'middle')
|
||||
.each(function calculateLabelWidth() {
|
||||
// eslint-disable-next-line react/no-this-in-sfc
|
||||
const bbox = this.getBBox();
|
||||
const padding = 10;
|
||||
// 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');
|
||||
.append('rect')
|
||||
.attr('x', bbox.x - padding / 2)
|
||||
.attr('y', bbox.y)
|
||||
.attr('width', bbox.width + padding)
|
||||
.attr('height', bbox.height)
|
||||
.style('stroke-width', 1)
|
||||
.attr('rx', 4)
|
||||
.attr('ry', 4)
|
||||
.attr('fill', 'white')
|
||||
.style('stroke', DEFAULT_NODE_STROKE_COLOR);
|
||||
});
|
||||
svg.selectAll('g.placeholder').remove();
|
||||
svg.selectAll('text.placeholder').remove();
|
||||
hostNames
|
||||
.append('text')
|
||||
.attr('x', (d) => d.x)
|
||||
.attr('y', (d) => d.y + 38)
|
||||
.text((d) => renderLabelText(d.node_state, d.hostname))
|
||||
.attr('font-size', DEFAULT_FONT_SIZE)
|
||||
.attr('fill', 'black')
|
||||
.attr('text-anchor', 'middle');
|
||||
|
||||
// add badge icons
|
||||
const badges = nodeCircles.append('g').attr('class', 'node-state-badge');
|
||||
badges.each(function drawStateBadge() {
|
||||
// eslint-disable-next-line react/no-this-in-sfc
|
||||
const bbox = this.parentNode.getBBox();
|
||||
// eslint-disable-next-line react/no-this-in-sfc
|
||||
d3.select(this)
|
||||
.append('circle')
|
||||
.attr('r', 9)
|
||||
.attr('cx', bbox.x)
|
||||
.attr('cy', bbox.y)
|
||||
.attr('fill', (d) => renderStateColor(d.node_state));
|
||||
d3.select(this)
|
||||
.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.call(zoom);
|
||||
|
||||
function highlightSiblings(n) {
|
||||
@ -336,7 +348,6 @@ function MeshGraph({
|
||||
immediate.forEach((s) => {
|
||||
svg
|
||||
.selectAll(`.link-${s.index}`)
|
||||
.transition()
|
||||
.style('stroke', '#0066CC')
|
||||
.style('stroke-width', '3px')
|
||||
.attr('marker-end', 'url(#end-active)');
|
||||
@ -352,19 +363,20 @@ function MeshGraph({
|
||||
immediate.forEach((s) => {
|
||||
svg
|
||||
.selectAll(`.link-${s.index}`)
|
||||
.transition()
|
||||
.duration(50)
|
||||
.style('stroke', (d) => {
|
||||
if (d.link_state === 'removing') {
|
||||
return '#C9190B';
|
||||
}
|
||||
if (d.link_state === 'disconnected') {
|
||||
return '#c9700b';
|
||||
}
|
||||
return '#CCC';
|
||||
})
|
||||
.style('stroke', (d) => renderLinkStatusColor(d.link_state))
|
||||
.style('stroke-width', '2px')
|
||||
.attr('marker-end', 'url(#end)');
|
||||
.attr('marker-end', (d) => {
|
||||
if (d.link_state === 'disconnected') {
|
||||
return 'url(#end-disconnected)';
|
||||
}
|
||||
if (d.link_state === 'adding') {
|
||||
return 'url(#end-adding)';
|
||||
}
|
||||
if (d.link_state === 'removing') {
|
||||
return 'url(#end-removing)';
|
||||
}
|
||||
return 'url(#end)';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -373,7 +385,7 @@ function MeshGraph({
|
||||
// toggle rings
|
||||
svg
|
||||
.select(`circle.id-${n.id}`)
|
||||
.attr('stroke', '#ccc')
|
||||
.attr('stroke', (d) => renderStateColor(d.node_state))
|
||||
.attr('stroke-width', null);
|
||||
// show default empty state of tooltip
|
||||
setIsNodeSelected(false);
|
||||
@ -382,7 +394,7 @@ function MeshGraph({
|
||||
}
|
||||
svg
|
||||
.selectAll('circle')
|
||||
.attr('stroke', '#ccc')
|
||||
.attr('stroke', (d) => renderStateColor(d.node_state))
|
||||
.attr('stroke-width', null);
|
||||
svg
|
||||
.select(`circle.id-${n.id}`)
|
||||
|
||||
@ -45,6 +45,7 @@ function TopologyView() {
|
||||
zoomIn={zoomIn}
|
||||
zoomOut={zoomOut}
|
||||
zoomFit={zoomFit}
|
||||
refresh={fetchMeshVisualizer}
|
||||
resetZoom={resetZoom}
|
||||
showZoomControls={showZoomControls}
|
||||
/>
|
||||
|
||||
@ -13,7 +13,7 @@ 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_NODE_STROKE_COLOR = '#6A6E73';
|
||||
export const DEFAULT_FONT_SIZE = '12px';
|
||||
export const LABEL_TEXT_MAX_LENGTH = 15;
|
||||
export const MARGIN = 15;
|
||||
@ -27,6 +27,13 @@ export const NODE_STATE_COLOR_KEY = {
|
||||
deprovisioning: '#666',
|
||||
};
|
||||
|
||||
export const LINK_STATE_COLOR_KEY = {
|
||||
established: '#6A6E73',
|
||||
disconnected: '#CCC',
|
||||
adding: '#3E8635',
|
||||
removing: '#C9190B',
|
||||
};
|
||||
|
||||
export const NODE_TYPE_SYMBOL_KEY = {
|
||||
hop: 'h',
|
||||
execution: 'Ex',
|
||||
|
||||
@ -4,6 +4,7 @@ import { truncateString } from '../../../util/strings';
|
||||
import {
|
||||
NODE_STATE_COLOR_KEY,
|
||||
NODE_TYPE_SYMBOL_KEY,
|
||||
LINK_STATE_COLOR_KEY,
|
||||
LABEL_TEXT_MAX_LENGTH,
|
||||
ICONS,
|
||||
} from '../constants';
|
||||
@ -20,6 +21,12 @@ export function renderStateColor(nodeState) {
|
||||
return NODE_STATE_COLOR_KEY[nodeState] ? NODE_STATE_COLOR_KEY[nodeState] : '';
|
||||
}
|
||||
|
||||
export function renderLinkStatusColor(linkState) {
|
||||
return LINK_STATE_COLOR_KEY[linkState]
|
||||
? LINK_STATE_COLOR_KEY[linkState]
|
||||
: '#ccc';
|
||||
}
|
||||
|
||||
export function renderLabelText(nodeState, name) {
|
||||
if (typeof nodeState === 'string' && typeof name === 'string') {
|
||||
return `${truncateString(name, LABEL_TEXT_MAX_LENGTH)}`;
|
||||
@ -45,8 +52,8 @@ export function renderLabelIcons(nodeState) {
|
||||
ready: 'checkmark',
|
||||
installed: 'clock',
|
||||
unavailable: 'exclaimation',
|
||||
'provision-fail': 'exclaimation',
|
||||
'deprovision-fail': 'exclaimation',
|
||||
'provision-fail': 'exclaimation',
|
||||
provisioning: 'plus',
|
||||
deprovisioning: 'minus',
|
||||
};
|
||||
@ -59,15 +66,17 @@ export function renderLabelIcons(nodeState) {
|
||||
export function renderIconPosition(nodeState, bbox) {
|
||||
if (nodeState) {
|
||||
const iconPositionMapper = {
|
||||
ready: `translate(${bbox.x - 15}, ${bbox.y + 3}), scale(0.02)`,
|
||||
installed: `translate(${bbox.x - 18}, ${bbox.y + 1}), scale(0.03)`,
|
||||
unavailable: `translate(${bbox.x - 9}, ${bbox.y + 3}), scale(0.02)`,
|
||||
'provision-fail': `translate(${bbox.x - 9}, ${bbox.y + 3}), scale(0.02)`,
|
||||
'deprovision-fail': `translate(${bbox.x - 9}, ${
|
||||
bbox.y + 3
|
||||
ready: `translate(${bbox.x - 4.5}, ${bbox.y - 4.5}), scale(0.02)`,
|
||||
installed: `translate(${bbox.x - 6.5}, ${bbox.y - 6.5}), scale(0.025)`,
|
||||
unavailable: `translate(${bbox.x - 2}, ${bbox.y - 4.4}), scale(0.02)`,
|
||||
'provision-fail': `translate(${bbox.x - 2}, ${bbox.y - 4}), scale(0.02)`,
|
||||
'deprovision-fail': `translate(${bbox.x - 2}, ${
|
||||
bbox.y - 4
|
||||
}), scale(0.02)`,
|
||||
provisioning: `translate(${bbox.x - 4.5}, ${bbox.y - 4.5}), scale(0.02)`,
|
||||
deprovisioning: `translate(${bbox.x - 4.5}, ${
|
||||
bbox.y - 4.5
|
||||
}), 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] : ``;
|
||||
}
|
||||
@ -86,6 +95,7 @@ export function redirectToDetailsPage(selectedNode, history) {
|
||||
export function renderLinkState(linkState) {
|
||||
const linkPattern = {
|
||||
established: null,
|
||||
disconnected: 3,
|
||||
adding: 3,
|
||||
removing: 3,
|
||||
};
|
||||
@ -101,7 +111,9 @@ export function getRandomInt(min, max) {
|
||||
const generateRandomLinks = (n, r) => {
|
||||
const links = [];
|
||||
function getRandomLinkState() {
|
||||
return ['established', 'adding', 'removing'][getRandomInt(0, 2)];
|
||||
return ['established', 'adding', 'removing', 'disconnected'][
|
||||
getRandomInt(0, 3)
|
||||
];
|
||||
}
|
||||
for (let i = 0; i < r; i++) {
|
||||
const link = {
|
||||
@ -142,7 +154,7 @@ export const generateRandomNodes = (n) => {
|
||||
hostname: `node-${id}`,
|
||||
node_type: randomType,
|
||||
node_state: randomState,
|
||||
enabled: Math.random() < 0.5,
|
||||
enabled: Math.random() < 0.9,
|
||||
};
|
||||
nodes.push(node);
|
||||
}
|
||||
|
||||
@ -73,7 +73,7 @@ describe('renderIconPosition', () => {
|
||||
const bbox = { x: 400, y: 400, width: 10, height: 20 };
|
||||
test('returns correct label icon', () => {
|
||||
expect(renderIconPosition('ready', bbox)).toBe(
|
||||
`translate(${bbox.x - 15}, ${bbox.y + 3}), scale(0.02)`
|
||||
`translate(${bbox.x - 4.5}, ${bbox.y - 4.5}), scale(0.02)`
|
||||
);
|
||||
});
|
||||
test('returns empty string if state is not found', () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user