diff --git a/awx/ui/src/screens/Instances/Shared/InstanceForm.js b/awx/ui/src/screens/Instances/Shared/InstanceForm.js
index c5392940bf..152c16b4fa 100644
--- a/awx/ui/src/screens/Instances/Shared/InstanceForm.js
+++ b/awx/ui/src/screens/Instances/Shared/InstanceForm.js
@@ -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,
diff --git a/awx/ui/src/screens/Instances/Shared/RemoveInstanceButton.js b/awx/ui/src/screens/Instances/Shared/RemoveInstanceButton.js
index 9a9e382dbf..a7b276bd27 100644
--- a/awx/ui/src/screens/Instances/Shared/RemoveInstanceButton.js
+++ b/awx/ui/src/screens/Instances/Shared/RemoveInstanceButton.js
@@ -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);
diff --git a/awx/ui/src/screens/TopologyView/Header.js b/awx/ui/src/screens/TopologyView/Header.js
index 1b287023e5..475b68366a 100644
--- a/awx/ui/src/screens/TopologyView/Header.js
+++ b/awx/ui/src/screens/TopologyView/Header.js
@@ -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 = ({
+
+ }
+ onClick={refresh}
+ isDisabled={!showZoomControls}
+ >
+
+
+
@@ -260,8 +260,9 @@ function Legend() {
y1="9"
x2="20"
y2="9"
- stroke="#c9700b"
+ stroke="#ccc"
strokeWidth="4"
+ strokeDasharray="6"
/>
@@ -275,7 +276,7 @@ function Legend() {
y1="9"
x2="20"
y2="9"
- stroke="#666"
+ stroke="#3E8635"
strokeWidth="4"
strokeDasharray="6"
/>
diff --git a/awx/ui/src/screens/TopologyView/MeshGraph.js b/awx/ui/src/screens/TopologyView/MeshGraph.js
index e829cb97ff..1d60ab174d 100644
--- a/awx/ui/src/screens/TopologyView/MeshGraph.js
+++ b/awx/ui/src/screens/TopologyView/MeshGraph.js
@@ -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}`)
diff --git a/awx/ui/src/screens/TopologyView/TopologyView.js b/awx/ui/src/screens/TopologyView/TopologyView.js
index c174577833..94e529d0ce 100644
--- a/awx/ui/src/screens/TopologyView/TopologyView.js
+++ b/awx/ui/src/screens/TopologyView/TopologyView.js
@@ -45,6 +45,7 @@ function TopologyView() {
zoomIn={zoomIn}
zoomOut={zoomOut}
zoomFit={zoomFit}
+ refresh={fetchMeshVisualizer}
resetZoom={resetZoom}
showZoomControls={showZoomControls}
/>
diff --git a/awx/ui/src/screens/TopologyView/constants.js b/awx/ui/src/screens/TopologyView/constants.js
index 1748a94c9e..ff57c98bef 100644
--- a/awx/ui/src/screens/TopologyView/constants.js
+++ b/awx/ui/src/screens/TopologyView/constants.js
@@ -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',
diff --git a/awx/ui/src/screens/TopologyView/utils/helpers.js b/awx/ui/src/screens/TopologyView/utils/helpers.js
index d2d875dc91..b75c72d4d1 100644
--- a/awx/ui/src/screens/TopologyView/utils/helpers.js
+++ b/awx/ui/src/screens/TopologyView/utils/helpers.js
@@ -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);
}
diff --git a/awx/ui/src/screens/TopologyView/utils/helpers__RTL.test.js b/awx/ui/src/screens/TopologyView/utils/helpers__RTL.test.js
index 38a0c55931..8575c2b108 100644
--- a/awx/ui/src/screens/TopologyView/utils/helpers__RTL.test.js
+++ b/awx/ui/src/screens/TopologyView/utils/helpers__RTL.test.js
@@ -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', () => {