From 5e8a08e30a2975319e4028dcdc4d4540d3fb65e3 Mon Sep 17 00:00:00 2001 From: yongsikgi Date: Wed, 28 Aug 2024 08:35:12 +0900 Subject: [PATCH 01/28] At #528 topology performance improvement --- frontend/src/hosts/configurations/topology.ts | 33 ++++++++++++------- .../hosts/containers/InventoryTopology.tsx | 24 ++++---------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/frontend/src/hosts/configurations/topology.ts b/frontend/src/hosts/configurations/topology.ts index 2ebad10c..ae709158 100644 --- a/frontend/src/hosts/configurations/topology.ts +++ b/frontend/src/hosts/configurations/topology.ts @@ -394,8 +394,6 @@ export const applyHandler = function ( let isInputPassword = false if (newValue !== oldValue) { - graph.getModel().beginUpdate() - try { if (attribute.nodeName === 'data-label') { const title = getContainerTitle(containerElement) @@ -466,8 +464,7 @@ export const applyHandler = function ( if (isInputPassword) { graph.setSelectionCell(cell) } - graph.getModel().endUpdate() - this.graphUpdateSave() + this.graphUpdateSave(cell) } } } @@ -1338,9 +1335,10 @@ export const filteredIpmiPowerStatus = function (cells: mxCellType[]) { export const ipmiPowerIndicator = function (ipmiCellsStatus: IpmiCell[]) { if (!this.graph) return - const model = this.graph.getModel() + if (!ipmiCellsStatus || ipmiCellsStatus.length === 0) { + return + } - model.beginUpdate() try { _.forEach(ipmiCellsStatus, ipmiCellStatus => { const childrenCell = ipmiCellStatus.cell.getChildAt(0) @@ -1376,8 +1374,11 @@ export const ipmiPowerIndicator = function (ipmiCellsStatus: IpmiCell[]) { } }) } finally { - model.endUpdate() - this.graphUpdate() + if (ipmiCellsStatus.length > 1) { + this.graph.refresh() + } else if (ipmiCellsStatus.length === 1) { + this.graph.refresh(ipmiCellsStatus[0].cell) + } } } @@ -1456,12 +1457,17 @@ export const detectedHostsStatus = function ( ) { if (!this.graph) return const {cpu, memory, disk, temperature} = TOOLTIP_TYPE - const model = this.graph.getModel() - model.beginUpdate() + if (!cells || cells.length === 0) { + return + } + + let nodeCount = 0 + try { _.forEach(cells, cell => { if (cell.getStyle().includes('node')) { + nodeCount++ const containerElement = getContainerElement(cell.value) const name = containerElement.getAttribute('data-name') @@ -1549,8 +1555,11 @@ export const detectedHostsStatus = function ( } }) } finally { - model.endUpdate() - this.graphUpdate() + if (nodeCount > 1) { + this.graph.refresh() + } else if (nodeCount === 1) { + this.graph.refresh(cells[0]) + } } return null diff --git a/frontend/src/hosts/containers/InventoryTopology.tsx b/frontend/src/hosts/containers/InventoryTopology.tsx index d84ceeb7..3444413e 100644 --- a/frontend/src/hosts/containers/InventoryTopology.tsx +++ b/frontend/src/hosts/containers/InventoryTopology.tsx @@ -1680,7 +1680,6 @@ export class InventoryTopology extends PureComponent { this.graph.getFoldingImage = getFoldingImage.bind(this) const outln = new mxOutline(this.graph, this.outline) - outln.outline.labelsVisible = true outln.outline.setHtmlLabels(true) } @@ -2179,23 +2178,14 @@ export class InventoryTopology extends PureComponent { } // @ts-ignore - private graphUpdate = () => { - this.graph.getModel().beginUpdate() + private graphUpdateSave = (cell: mxCellType) => { try { - } finally { - this.graph.getModel().endUpdate() - this.graph.refresh() - } - } - - // @ts-ignore - private graphUpdateSave = () => { - this.graph.getModel().beginUpdate() - try { - } finally { - this.graph.getModel().endUpdate() - this.graph.refresh() - this.handleGraphModel(this.graph.getModel()) + if (cell) { + this.graph.refresh(cell) + this.handleGraphModel(this.graph.getModel()) + } + } catch (error) { + console.error('Error updating graph:', error) } } From 1c2d64363f34090d9bb241e2848e0471873395ae Mon Sep 17 00:00:00 2001 From: jinhyeong Date: Tue, 10 Sep 2024 05:01:56 +0000 Subject: [PATCH 02/28] At #528 Remove redundant calculations to enhance topology performance --- .../src/hosts/actions/inventoryTopology.ts | 23 ++++- frontend/src/hosts/apis/index.ts | 11 +++ frontend/src/hosts/configurations/topology.ts | 78 ++++++++++----- .../hosts/containers/InventoryTopology.tsx | 99 +++++++++---------- frontend/src/types/hosts.ts | 2 + 5 files changed, 136 insertions(+), 77 deletions(-) diff --git a/frontend/src/hosts/actions/inventoryTopology.ts b/frontend/src/hosts/actions/inventoryTopology.ts index 91a4b25b..937bb031 100644 --- a/frontend/src/hosts/actions/inventoryTopology.ts +++ b/frontend/src/hosts/actions/inventoryTopology.ts @@ -167,6 +167,12 @@ export const updateInventoryTopologyAsync = ( } } +export const emptyIpmiStatus = async () => { + return { + return: [{emptyHost: 'empty'}], + } +} + export const getIpmiStatusAsync = ( pUrl: string, pToken: string, @@ -175,17 +181,22 @@ export const getIpmiStatusAsync = ( try { const ipmis = await Promise.all( pIpmis.map(pIpmi => { - return getIpmiStatusSaltApi(pUrl, pToken, pIpmi) + return pIpmi.powerStatus === 'empty' + ? emptyIpmiStatus() + : getIpmiStatusSaltApi(pUrl, pToken, pIpmi) }) ) let error = '' let ipmiHost = '' let resultIpmis: IpmiCell[] = pIpmis - _.map(ipmis, (ipmiAPIResponse, index) => { const ipmi = ipmiAPIResponse?.return?.[0] - if (_.values(ipmi)[0] !== 'on' && _.values(ipmi)[0] !== 'off') { + if ( + _.values(ipmi)[0] !== 'on' && + _.values(ipmi)[0] !== 'off' && + _.values(ipmi)[0] !== 'empty' + ) { if (error !== null) { error += '\n' } @@ -194,7 +205,11 @@ export const getIpmiStatusAsync = ( resultIpmis[index].powerStatus = '' } else { - resultIpmis[index].powerStatus = _.values(ipmi)[0] + if (_.values(ipmi)[0] === 'empty') { + resultIpmis[index].powerStatus = '' + } else { + resultIpmis[index].powerStatus = _.values(ipmi)[0] + } } }) diff --git a/frontend/src/hosts/apis/index.ts b/frontend/src/hosts/apis/index.ts index cb2fbede..3662cdf0 100644 --- a/frontend/src/hosts/apis/index.ts +++ b/frontend/src/hosts/apis/index.ts @@ -1547,6 +1547,7 @@ export const getHostsInfoWithIpmi = async ( /system_temp|exhaust_temp|outlet_temp|mb_cpu_out_temp|temp_mb_outlet/ )}) GROUP BY hostname ) GROUP BY hostname; SELECT last(value) as "ipmi_ip" FROM \":db:\".\":rp:\".\"ipmi_sensor\" WHERE time > now() - 10m GROUP BY "hostname", "server" FILL(null); + SELECT mean("value") AS "powerStatus" FROM \":db:\".\":rp:\".\"ipmi_sensor\" WHERE "name" = 'chassis_power_status' AND "hostname" != '' AND time > now() - 10m GROUP BY hostname, server fill(null); `, tempVars ) @@ -1566,6 +1567,8 @@ export const getHostsInfoWithIpmi = async ( const ipmiInsideSeries = getDeep(data, 'results.[4].series', []) const ipmiOutletSeries = getDeep(data, 'results.[5].series', []) const ipmiHostsAndIp = getDeep(data, 'results.[6].series', []) + const ipmiPowerStatus = getDeep(data, 'results.[7].series', []) + allHostsSeries.forEach(s => { const hostnameIndex = s.columns.findIndex(col => col === 'value') s.values.forEach(v => { @@ -1632,6 +1635,14 @@ export const getHostsInfoWithIpmi = async ( } } }) + ipmiPowerStatus.forEach(s => { + const meanIndex = s.columns.findIndex(col => col === 'powerStatus') + if (meanIndex !== -1) { + hosts[s.tags.hostname].powerStatus = + Number(s.values[0][meanIndex]) === 1 ? 'on' : 'off' + } + }) + if ( meRole !== SUPERADMIN_ROLE || (meRole === SUPERADMIN_ROLE && diff --git a/frontend/src/hosts/configurations/topology.ts b/frontend/src/hosts/configurations/topology.ts index ae709158..cccccf01 100644 --- a/frontend/src/hosts/configurations/topology.ts +++ b/frontend/src/hosts/configurations/topology.ts @@ -16,7 +16,7 @@ import { } from 'mxgraph' // Types -import {Host, IpmiCell} from 'src/types' +import {Host, IpmiCell, RemoteDataState} from 'src/types' import {CloudServiceProvider, Instance} from 'src/hosts/types/cloud' // Utils @@ -377,12 +377,9 @@ export const createTextField = function ( mxEvent.addListener(input, updateEvent, async () => { applyHandler.bind(this)(graph, cell, attribute, input.value) - - await this.getIpmiStatus(cell.getId()) - this.getDetectedHostStatus(cell.getId()) }) } -export const applyHandler = function ( +export const applyHandler = async function ( graph: mxGraphType, cell: mxCellType, attribute: any, @@ -390,7 +387,6 @@ export const applyHandler = function ( ) { const containerElement = getContainerElement(cell.value) const oldValue = containerElement.getAttribute(attribute.nodeName) || '' - let isInputPassword = false if (newValue !== oldValue) { @@ -457,17 +453,39 @@ export const applyHandler = function ( `mxgraph-cell--icon-${newValue.replaceAll(`-`, '').toLowerCase()}` ) } - containerElement.setAttribute(attribute.nodeName, newValue) cell.setValue(containerElement.outerHTML) + this.setState({fetchIntervalDataStatus: RemoteDataState.Loading}) + await getIpmiStatus.bind(this)(cell.getId()) + this.getDetectedHostStatus(cell.getId()) } finally { if (isInputPassword) { graph.setSelectionCell(cell) } this.graphUpdateSave(cell) + this.setState({fetchIntervalDataStatus: RemoteDataState.Done}) } } } +async function getIpmiStatus(focusedCellId: string) { + if (!this.graph) return + + const graph = this.graph + const parent = graph.getDefaultParent() + const cells = this.getAllCells(parent, true) + + const filteredCells = getFocusedCell(cells, focusedCellId) + + const ipmiCells: IpmiCell[] = filteredIpmiPowerStatus.bind(this)( + filteredCells + ) + const ipmiCellsStatus: IpmiCell[] = await this.props.handleGetIpmiStatus( + this.salt.url, + this.salt.token, + ipmiCells + ) + ipmiPowerIndicator.bind(this)(ipmiCellsStatus) +} export const createHTMLValue = function (node: Menu, style: string) { const cell = document.createElement('div') @@ -708,6 +726,7 @@ export const dragCell = (node: Menu, self: any) => ( if (dataType === 'Server') { self.getDetectedHostStatus(v1.getId()) + graph.refresh(v1) } } @@ -1314,6 +1333,7 @@ export const filteredIpmiPowerStatus = function (cells: mxCellType[]) { pass: originalPass, powerStatus: '', cell: cell, + hostname: hostname, } ipmiCells = [...ipmiCells, ipmiCell] @@ -1324,6 +1344,17 @@ export const filteredIpmiPowerStatus = function (cells: mxCellType[]) { ) ) } + } else { + const emptyIpmiCell: IpmiCell = { + target: '', + host: '', + user: '', + pass: '', + powerStatus: 'empty', + cell: cell, + hostname: hostname, + } + ipmiCells = [...ipmiCells, emptyIpmiCell] } } } @@ -1373,12 +1404,8 @@ export const ipmiPowerIndicator = function (ipmiCellsStatus: IpmiCell[]) { childrenCell.setValue(childrenContainerElement.outerHTML) } }) - } finally { - if (ipmiCellsStatus.length > 1) { - this.graph.refresh() - } else if (ipmiCellsStatus.length === 1) { - this.graph.refresh(ipmiCellsStatus[0].cell) - } + } catch (err) { + console.error('ipmiPowerIndicator Err: ', err) } } @@ -1463,14 +1490,13 @@ export const detectedHostsStatus = function ( } let nodeCount = 0 - + let error = null try { _.forEach(cells, cell => { if (cell.getStyle().includes('node')) { nodeCount++ const containerElement = getContainerElement(cell.value) const name = containerElement.getAttribute('data-name') - const findHost = _.find(hostsObject, host => host.name === name) if (!_.isEmpty(findHost)) { @@ -1554,15 +1580,11 @@ export const detectedHostsStatus = function ( } } }) - } finally { - if (nodeCount > 1) { - this.graph.refresh() - } else if (nodeCount === 1) { - this.graph.refresh(cells[0]) - } + } catch (err) { + error = err } - return null + return [nodeCount, error] } export const getFromOptions = (focusedInstance: Instance) => { @@ -1633,3 +1655,15 @@ export const mouseOverTooltipStatus = ( ) return tooltipStatus } + +export function refreshGraph( + nodeCount: number | null, + graph: any, + cell: mxCellType +) { + if (nodeCount && nodeCount > 1) { + graph.refresh() + } else if (nodeCount && nodeCount === 1) { + graph.refresh(cell) + } +} diff --git a/frontend/src/hosts/containers/InventoryTopology.tsx b/frontend/src/hosts/containers/InventoryTopology.tsx index 3444413e..72da458f 100644 --- a/frontend/src/hosts/containers/InventoryTopology.tsx +++ b/frontend/src/hosts/containers/InventoryTopology.tsx @@ -206,6 +206,7 @@ import { getFocusedCell, onMouseMovexGraph, mouseOverTooltipStatus, + refreshGraph, } from 'src/hosts/configurations/topology' import {WindowResizeEventTrigger} from 'src/shared/utils/trigger' @@ -601,18 +602,15 @@ export class InventoryTopology extends PureComponent { public async componentDidMount() { const {notify} = this.props - this.createEditor() this.configureEditor() this.setActionInEditor() this.configureStylesheet(mx) - this.addToolsButton(this.tools) this.setToolbar(this.editor, this.toolbar) const layoutResults = await getLayouts() const layouts = getDeep(layoutResults, 'data.layouts', []) - this.setState({ layouts, }) @@ -621,9 +619,9 @@ export class InventoryTopology extends PureComponent { if (this.isUsingCSP) { await this.handleLoadCsps() } - await this.getInventoryTopology() this.setTopologySetting() + await this.fetchIntervalData() await this.getIpmiTargetList() } catch (error) { @@ -1034,27 +1032,28 @@ export class InventoryTopology extends PureComponent { private handleClickTemperatureOkButton = async () => { const {notify} = this.props const {hostsObject, preferenceTemperatureValues} = this.state - + let updateNode = null if (this.isValidTemperature()) { return } - + if (!this.graph) return + const graph = this.graph + const parent = graph.getDefaultParent() + const cells = this.getAllCells(parent, true) try { - if (!this.graph) return - - const graph = this.graph - const parent = graph.getDefaultParent() - const cells = this.getAllCells(parent, true) const selectedTemperatureValue = _.filter( preferenceTemperatureValues, temperatureValue => temperatureValue.includes('active:1') ) - detectedHostsStatus.bind(this)( + const [count, err] = detectedHostsStatus.bind(this)( cells, hostsObject, selectedTemperatureValue?.[0] ) - + if (err) { + throw err + } + updateNode = count notify(notifyPreferencesTemperatureApplySucceeded()) this.setState({ @@ -1065,6 +1064,7 @@ export class InventoryTopology extends PureComponent { } catch (error) { notify(notifyGetDetectedHostStatusFailed(error.message)) } finally { + refreshGraph(updateNode, graph, cells?.[0]) this.setState({ isPreferencesOverlayVisible: !this.state.isPreferencesOverlayVisible, }) @@ -1074,26 +1074,29 @@ export class InventoryTopology extends PureComponent { private handleClickTemperatureApplyButton = async () => { const {notify} = this.props const {hostsObject, preferenceTemperatureValues} = this.state + let updateNode = null if (this.isValidTemperature()) { return } - + if (!this.graph) return + const graph = this.graph + const parent = graph.getDefaultParent() + const cells = this.getAllCells(parent, true) try { - if (!this.graph) return - - const graph = this.graph - const parent = graph.getDefaultParent() - const cells = this.getAllCells(parent, true) const selectedTemperatureValue = _.filter( preferenceTemperatureValues, temperatureValue => temperatureValue.includes('active:1') ) - detectedHostsStatus.bind(this)( + const [count, err] = detectedHostsStatus.bind(this)( cells, hostsObject, selectedTemperatureValue?.[0] ) + if (err) { + throw err + } + updateNode = count notify(notifyPreferencesTemperatureApplySucceeded()) @@ -1103,6 +1106,8 @@ export class InventoryTopology extends PureComponent { }) } catch (error) { notify(notifyGetDetectedHostStatusFailed(error.message)) + } finally { + refreshGraph(updateNode, graph, cells?.[0]) } } @@ -1337,25 +1342,28 @@ export class InventoryTopology extends PureComponent { private fetchIntervalData = async () => { const {notify} = this.props - + if (!this.graph) return this.setState(preState => ({ ...preState, fetchIntervalDataStatus: RemoteDataState.Loading, })) - try { + this.graph.model.beginUpdate() await this.getHostData() - await this.getIpmiStatus() + this.fetchIpmiStatus() this.getDetectedHostStatus() } catch (error) { notify(notifyFetchIntervalDataFailed(error.message)) } finally { + this.graph.model.endUpdate() + this.graph.refresh() this.setState(preState => ({ ...preState, fetchIntervalDataStatus: RemoteDataState.Done, })) } } + private getIpmiData = async () => { const {source, auth} = this.props @@ -1392,7 +1400,6 @@ export class InventoryTopology extends PureComponent { ) const ipmiObject = await this.getIpmiData() const hostsObject = _.defaultsDeep({}, agentObject, ipmiObject) - const hostsError = notifyUnableToGetHosts().message if (!hostsObject) { throw new Error(hostsError) @@ -1419,40 +1426,28 @@ export class InventoryTopology extends PureComponent { ? getFocusedCell(cells, focusedCellId) : cells - detectedHostsStatus.bind(this)( + const [updateCount, err] = detectedHostsStatus.bind(this)( filteredCells, hostsObject, selectedTemperatureValue ) + return [updateCount, err] } - private getIpmiStatus = async (focusedCellId?: string) => { + private fetchIpmiStatus = () => { if (!this.graph) return const graph = this.graph + const {hostsObject} = this.state const parent = graph.getDefaultParent() const cells = this.getAllCells(parent, true) - - const filteredCells = focusedCellId - ? getFocusedCell(cells, focusedCellId) - : cells - let ipmiCells: IpmiCell[] = filteredIpmiPowerStatus.bind(this)( - filteredCells - ) - - if (_.isEmpty(ipmiCells)) return - - this.setIpmiStatus(ipmiCells) - } - - private setIpmiStatus = async (ipmiCells: IpmiCell[]) => { - const ipmiCellsStatus: IpmiCell[] = await this.props.handleGetIpmiStatus( - this.salt.url, - this.salt.token, - ipmiCells - ) - - ipmiPowerIndicator.bind(this)(ipmiCellsStatus) + const ipmiCells: IpmiCell[] = filteredIpmiPowerStatus.bind(this)(cells) + _.forEach(ipmiCells, ipmiCell => { + if (hostsObject[ipmiCell.hostname]?.powerStatus) { + ipmiCell.powerStatus = hostsObject[ipmiCell.hostname].powerStatus + } + }) + ipmiPowerIndicator.bind(this)(ipmiCells) } private getIpmiTargetList = async () => { @@ -2324,14 +2319,16 @@ export class InventoryTopology extends PureComponent {
+ {fetchIntervalDataStatus === RemoteDataState.Loading && ( + + )}
{topologyStatus === RemoteDataState.Loading && ( )} - {fetchIntervalDataStatus === RemoteDataState.Loading && ( - - )} -
+ +
+ Date: Thu, 12 Sep 2024 11:25:56 +0900 Subject: [PATCH 03/28] At #528 Update Topology to disable cell selection, movement and edge creation on right-click,allowing screen panning instead --- frontend/src/hosts/configurations/topology.ts | 25 +++++++ .../hosts/containers/InventoryTopology.tsx | 65 +++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/frontend/src/hosts/configurations/topology.ts b/frontend/src/hosts/configurations/topology.ts index cccccf01..af64e50d 100644 --- a/frontend/src/hosts/configurations/topology.ts +++ b/frontend/src/hosts/configurations/topology.ts @@ -199,6 +199,28 @@ export const getConnectImage = function (state: mxCellStateType) { } export const isCellSelectable = function (cell: mxCellType) { + if ( + this.graph && + this.graph.lastEvent && + this.graph.lastEvent instanceof PointerEvent && + this.graph.lastEvent.button === 2 + ) { + return false + } + + return !this.graph.isCellLocked(cell) +} + +export const isCellMovable = function (cell: mxCellType) { + if ( + this.graph && + this.graph.lastEvent && + this.graph.lastEvent instanceof PointerEvent && + this.graph.lastEvent.button === 2 + ) { + return false + } + return !this.graph.isCellLocked(cell) } @@ -1042,6 +1064,9 @@ export const onClickMxGraph = function ( _graph: mxGraphType, me: mxEventObjectType ) { + const evt = me.getProperty('event') + if (evt?.button === 2 || evt?.buttons === 2) return + const cell: mxCellType = me.getProperty('cell') if (!_.isEmpty(cell) && cell.style.includes('node')) { diff --git a/frontend/src/hosts/containers/InventoryTopology.tsx b/frontend/src/hosts/containers/InventoryTopology.tsx index 72da458f..973b0d86 100644 --- a/frontend/src/hosts/containers/InventoryTopology.tsx +++ b/frontend/src/hosts/containers/InventoryTopology.tsx @@ -207,6 +207,7 @@ import { onMouseMovexGraph, mouseOverTooltipStatus, refreshGraph, + isCellMovable, } from 'src/hosts/configurations/topology' import {WindowResizeEventTrigger} from 'src/shared/utils/trigger' @@ -245,6 +246,7 @@ export const { mxGeometry, mxPopupMenu, mxEventObject, + mxConnectionHandler, } = mx window['mxGraph'] = mxGraph @@ -272,6 +274,7 @@ window['mxOutline'] = mxOutline window['mxPoint'] = mxPoint window['mxPopupMenu'] = mxPopupMenu window['mxEventObject'] = mxEventObject +window['mxConnectionHandler'] = mxConnectionHandler interface Auth { me: Me @@ -1552,6 +1555,62 @@ export class InventoryTopology extends PureComponent { return group } + mxConnectionHandler.prototype.mouseDown = function (_, me) { + this.mouseDownCounter++ + + const event = me.getEvent() + + if (event.button === 2 || event.buttons === 2) { + return + } + + if ( + this.isEnabled() && + this.graph.isEnabled() && + !me.isConsumed() && + !this.isConnecting() && + this.isStartEvent(me) + ) { + if ( + this.constraintHandler.currentConstraint != null && + this.constraintHandler.currentFocus != null && + this.constraintHandler.currentPoint != null + ) { + this.sourceConstraint = this.constraintHandler.currentConstraint + this.previous = this.constraintHandler.currentFocus + this.first = this.constraintHandler.currentPoint.clone() + } else { + // Stores the location of the initial mousedown + this.first = new mxPoint(me.getGraphX(), me.getGraphY()) + } + + this.edgeState = this.createEdgeState(me) + this.mouseDownCounter = 1 + + if (this.waypointsEnabled && this.shape == null) { + this.waypoints = null + this.shape = this.createShape() + + if (this.edgeState != null) { + this.shape.apply(this.edgeState) + } + } + + // Stores the starting point in the geometry of the preview + if (this.previous == null && this.edgeState != null) { + var pt = this.graph.getPointForEvent(me.getEvent()) + this.edgeState.cell.geometry.setTerminalPoint(pt, true) + } + + this.fireEvent(new mxEventObject(mxEvent.START, 'state', this.previous)) + + me.consume() + } + + this.selectedIcon = this.icon + this.icon = null + } + mxGraph.prototype.groupCells = function (group, border, cells) { if (cells == null) { cells = mxUtils.sortCells(this.getSelectionCells(), true) @@ -1621,7 +1680,13 @@ export class InventoryTopology extends PureComponent { return group } + this.graph.panningHandler.ignoreCell = true + this.graph.panningHandler.isForcePanningEvent = me => { + return me.getEvent().button === 2 + } + this.graph.isCellSelectable = isCellSelectable.bind(this) + this.graph.isCellMovable = isCellMovable.bind(this) this.graph.setConnectable(true) From 5cb4c6ad3345bd4137786a3d33ca855894dbed38 Mon Sep 17 00:00:00 2001 From: jaegeunha Date: Fri, 13 Sep 2024 10:43:53 +0900 Subject: [PATCH 04/28] At #528 Update 'user-select'property to 'none' in Inventory Topology Tab --- frontend/src/hosts/components/TopologyTooltip.tsx | 1 + frontend/src/hosts/containers/InventoryTopology.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/hosts/components/TopologyTooltip.tsx b/frontend/src/hosts/components/TopologyTooltip.tsx index 94789b65..51b700b6 100644 --- a/frontend/src/hosts/components/TopologyTooltip.tsx +++ b/frontend/src/hosts/components/TopologyTooltip.tsx @@ -61,6 +61,7 @@ export default function TopologyTooltip({targetPosition, tooltipNode}: Props) { width: `${TOOLTIP_WIDTH}px`, top: dynamicTopOffset, left: targetPosition.left, + userSelect: 'text', }} >
{ const isExportXML = modalTitle === 'Export XML' return ( -
+
{!mxClient.isBrowserSupported() ? ( <>this Browser Not Supported ) : ( From 0f79425ec02661a93bb6d0af23681f18f1488aec Mon Sep 17 00:00:00 2001 From: jaegeunha Date: Thu, 19 Sep 2024 15:17:11 +0900 Subject: [PATCH 05/28] At #528 Update Where clause in query to fetch inlet temperature --- frontend/src/hosts/apis/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/hosts/apis/index.ts b/frontend/src/hosts/apis/index.ts index 3662cdf0..d456f1d2 100644 --- a/frontend/src/hosts/apis/index.ts +++ b/frontend/src/hosts/apis/index.ts @@ -1537,9 +1537,7 @@ export const getHostsInfoWithIpmi = async ( `SELECT mean("value") AS "ipmiCpu" FROM \":db:\".\":rp:\".\"ipmi_sensor\" WHERE "name" = 'cpu_usage' AND "hostname" != '' AND time > now() - 10m GROUP BY hostname fill(null); SELECT mean("value") AS "ipmiMemory" FROM \":db:\".\":rp:\".\"ipmi_sensor\" WHERE "name" = 'mem_usage' AND "hostname" != '' AND time > now() - 10m GROUP BY hostname; SHOW TAG VALUES FROM \":db:\".\":rp:\".\"ipmi_sensor\" WITH KEY = "hostname" WHERE TIME > now() - 10m AND "hostname" != ''; - SELECT max("inlet") AS "inlet" FROM ( SELECT last("value") AS "inlet" FROM \":db:\".\":rp:\".\"ipmi_sensor\" WHERE "hostname" != '' AND time > now() - 10m AND ("name" =~ ${new RegExp( - /inlet_temp|mb_cpu_in_temp|temp_mb_inlet/ - )}) GROUP BY hostname ) GROUP BY hostname; + SELECT max("inlet") AS "inlet" FROM ( SELECT last("value") AS "inlet" FROM \":db:\".\":rp:\".\"ipmi_sensor\" WHERE "hostname" != '' AND time > now() - 10m AND ("name" = 'inlet_temp' OR "name" = 'mb_cpu_in_temp' OR "name" = 'temp_mb_inlet') GROUP BY hostname ) GROUP BY hostname; SELECT max("inside") AS "inside", "name" as "cpu_count" FROM ( SELECT last("value") AS "inside" FROM \":db:\".\":rp:\".\"ipmi_sensor\" WHERE "hostname" != '' AND time > now() - 10m AND ("name" =~ ${new RegExp( /cpu_temp|cpu\d+_temp|cpu_temp_\d+|cpu_dimmg\d+_temp|temp_cpu\d+/ )}) GROUP BY hostname, "name" ) GROUP BY hostname; From c55bf1c2b22e95180c20a470e79ca7b7531adc37 Mon Sep 17 00:00:00 2001 From: jaegeunha Date: Mon, 23 Sep 2024 11:15:39 +0900 Subject: [PATCH 06/28] At #528 Update IPMI status refresh in componentDidMount, DragCell, FocusOut and ImportTopology functions. --- frontend/src/hosts/configurations/topology.ts | 179 ++++++++++-------- .../hosts/containers/InventoryTopology.tsx | 56 +++++- frontend/src/shared/copy/notifications.ts | 8 + 3 files changed, 155 insertions(+), 88 deletions(-) diff --git a/frontend/src/hosts/configurations/topology.ts b/frontend/src/hosts/configurations/topology.ts index af64e50d..60a066e5 100644 --- a/frontend/src/hosts/configurations/topology.ts +++ b/frontend/src/hosts/configurations/topology.ts @@ -401,6 +401,39 @@ export const createTextField = function ( applyHandler.bind(this)(graph, cell, attribute, input.value) }) } + +export const createIPMIStatusIcon = function ( + graph: mxGraphType, + cell: mxCellType +) { + if (!graph || !cell) { + console.error('Invalid graph or cell provided.') + return + } + + if (!cell.children || cell.children.length === 0) { + console.error('No children in the provided cell.') + return + } + + const childrenCell = cell.getChildAt(0) + if (!childrenCell || !childrenCell.style) { + console.error('No valid children cell or missing style.') + return + } + + const sepCellStyle = _.split(childrenCell.style, ';') + if (sepCellStyle.length === 0 || !sepCellStyle[0]) { + console.error('Invalid or empty style string.') + return + } + + if (sepCellStyle[0] === 'ipmi') { + graph.setCellStyles(mxConstants.STYLE_STROKECOLOR, 'white', [childrenCell]) + childrenCell.setVisible(true) + } +} + export const applyHandler = async function ( graph: mxGraphType, cell: mxCellType, @@ -410,6 +443,7 @@ export const applyHandler = async function ( const containerElement = getContainerElement(cell.value) const oldValue = containerElement.getAttribute(attribute.nodeName) || '' let isInputPassword = false + let shouldFetchData = false if (newValue !== oldValue) { try { @@ -435,19 +469,6 @@ export const applyHandler = async function ( } } - if (attribute.nodeName === 'data-ipmi_host') { - if (cell.children) { - const childrenCell = cell.getChildAt(0) - const sepCellStyle = _.split(childrenCell.style, ';') - if (sepCellStyle[0] === 'ipmi') { - graph.setCellStyles(mxConstants.STYLE_STROKECOLOR, 'white', [ - childrenCell, - ]) - childrenCell.setVisible(getIsHasString(newValue)) - } - } - } - if (attribute.nodeName === 'data-ipmi_pass') { if (newValue.length > 0) { newValue = CryptoJS.AES.encrypt( @@ -464,6 +485,7 @@ export const applyHandler = async function ( if (childrenCell.style.includes('status')) { childrenCell.setVisible(newValue !== 'false' ? true : false) + shouldFetchData = newValue !== 'false' } } @@ -475,11 +497,17 @@ export const applyHandler = async function ( `mxgraph-cell--icon-${newValue.replaceAll(`-`, '').toLowerCase()}` ) } + + if (attribute.nodeName === 'data-name') shouldFetchData = true + containerElement.setAttribute(attribute.nodeName, newValue) cell.setValue(containerElement.outerHTML) this.setState({fetchIntervalDataStatus: RemoteDataState.Loading}) - await getIpmiStatus.bind(this)(cell.getId()) - this.getDetectedHostStatus(cell.getId()) + + if (shouldFetchData) { + await this.fetchIpmiStatus(cell.getId()) + await this.getDetectedHostStatus(cell.getId()) + } } finally { if (isInputPassword) { graph.setSelectionCell(cell) @@ -489,25 +517,6 @@ export const applyHandler = async function ( } } } -async function getIpmiStatus(focusedCellId: string) { - if (!this.graph) return - - const graph = this.graph - const parent = graph.getDefaultParent() - const cells = this.getAllCells(parent, true) - - const filteredCells = getFocusedCell(cells, focusedCellId) - - const ipmiCells: IpmiCell[] = filteredIpmiPowerStatus.bind(this)( - filteredCells - ) - const ipmiCellsStatus: IpmiCell[] = await this.props.handleGetIpmiStatus( - this.salt.url, - this.salt.token, - ipmiCells - ) - ipmiPowerIndicator.bind(this)(ipmiCellsStatus) -} export const createHTMLValue = function (node: Menu, style: string) { const cell = document.createElement('div') @@ -647,7 +656,8 @@ export const dragCell = (node: Menu, self: any) => ( ipmiStatus.geometry.offset = new mxPoint(-12, -12) ipmiStatus.setConnectable(false) - ipmiStatus.setVisible(false) + ipmiStatus.setVisible(true) + graph.setCellStyles(mxConstants.STYLE_STROKECOLOR, 'white', [ipmiStatus]) const linkBox = document.createElement('div') linkBox.setAttribute('btn-type', 'href') @@ -747,11 +757,16 @@ export const dragCell = (node: Menu, self: any) => ( const dataType = cell.getAttribute('data-type') if (dataType === 'Server') { - self.getDetectedHostStatus(v1.getId()) - graph.refresh(v1) + fetchIntervalData(graph, v1, self) } } +const fetchIntervalData = async (graph, cell, self) => { + await self.fetchIpmiStatus(cell.getId()) + await self.getDetectedHostStatus(cell.getId()) + graph.refresh(cell) +} + export const drawCellInGroup = (nodes: Menu[]) => ( graph: mxGraphType, _event: any, @@ -1331,56 +1346,54 @@ export const filteredIpmiPowerStatus = function (cells: mxCellType[]) { if (cell.getStyle().includes('node')) { const containerElement = getContainerElement(cell.value) - if (containerElement.hasAttribute('data-ipmi_host')) { - const ipmiTarget = containerElement.getAttribute('data-using_minion') - const ipmiHost = containerElement.getAttribute('data-ipmi_host') - const ipmiUser = containerElement.getAttribute('data-ipmi_user') - const ipmiPass = containerElement.getAttribute('data-ipmi_pass') - const hostname = containerElement.getAttribute('data-label') - - if ( - !_.isEmpty(ipmiTarget) && - !_.isEmpty(ipmiHost) && - !_.isEmpty(ipmiUser) && - !_.isEmpty(ipmiPass) - ) { - try { - const decryptedBytes = CryptoJS.AES.decrypt( - ipmiPass, - this.secretKey.url - ) - const originalPass = decryptedBytes.toString(CryptoJS.enc.Utf8) - - const ipmiCell: IpmiCell = { - target: ipmiTarget, - host: ipmiHost, - user: ipmiUser, - pass: originalPass, - powerStatus: '', - cell: cell, - hostname: hostname, - } - - ipmiCells = [...ipmiCells, ipmiCell] - } catch (error) { - this.props.notify( - notifyDecryptedBytesFailed( - `incorrect IPMI Password for ${hostname} : ${ipmiHost}` - ) - ) - } - } else { - const emptyIpmiCell: IpmiCell = { - target: '', - host: '', - user: '', - pass: '', - powerStatus: 'empty', + const ipmiTarget = containerElement.getAttribute('data-using_minion') + const ipmiHost = containerElement.getAttribute('data-ipmi_host') + const ipmiUser = containerElement.getAttribute('data-ipmi_user') + const ipmiPass = containerElement.getAttribute('data-ipmi_pass') + const hostname = containerElement.getAttribute('data-name') + + if ( + !_.isEmpty(ipmiTarget) && + !_.isEmpty(ipmiHost) && + !_.isEmpty(ipmiUser) && + !_.isEmpty(ipmiPass) + ) { + try { + const decryptedBytes = CryptoJS.AES.decrypt( + ipmiPass, + this.secretKey.url + ) + const originalPass = decryptedBytes.toString(CryptoJS.enc.Utf8) + + const ipmiCell: IpmiCell = { + target: ipmiTarget, + host: ipmiHost, + user: ipmiUser, + pass: originalPass, + powerStatus: '', cell: cell, hostname: hostname, } - ipmiCells = [...ipmiCells, emptyIpmiCell] + + ipmiCells = [...ipmiCells, ipmiCell] + } catch (error) { + this.props.notify( + notifyDecryptedBytesFailed( + `incorrect IPMI Password for ${hostname} : ${ipmiHost}` + ) + ) + } + } else { + const emptyIpmiCell: IpmiCell = { + target: '', + host: '', + user: '', + pass: '', + powerStatus: '', + cell: cell, + hostname: hostname, } + ipmiCells = [...ipmiCells, emptyIpmiCell] } } }) diff --git a/frontend/src/hosts/containers/InventoryTopology.tsx b/frontend/src/hosts/containers/InventoryTopology.tsx index 57163d5d..afdd32b5 100644 --- a/frontend/src/hosts/containers/InventoryTopology.tsx +++ b/frontend/src/hosts/containers/InventoryTopology.tsx @@ -61,6 +61,7 @@ import { notifyPreferencesTemperatureApplySucceeded, notifyFetchIntervalDataFailed, notifyGetDetectedHostStatusFailed, + notifySetIpmiStatusFailed, } from 'src/shared/copy/notifications' import {notIncludeApps} from 'src/hosts/constants/apps' import { @@ -208,6 +209,7 @@ import { mouseOverTooltipStatus, refreshGraph, isCellMovable, + createIPMIStatusIcon, } from 'src/hosts/configurations/topology' import {WindowResizeEventTrigger} from 'src/shared/utils/trigger' @@ -835,7 +837,7 @@ export class InventoryTopology extends PureComponent { @@ -862,7 +864,14 @@ export class InventoryTopology extends PureComponent { ) } - private onImportTopology = async (importedTopology: string) => { + private handleImportTopologyAndFetchIntervalData = async ( + importedTopology: string + ) => { + await this.importTopology(importedTopology) + this.fetchIntervalData() + } + + private importTopology = async (importedTopology: string) => { const topology = importedTopology const graph = this.graph @@ -905,6 +914,8 @@ export class InventoryTopology extends PureComponent { containerElement.removeAttribute(attr.nodeName) cell.setValue(containerElement.outerHTML) }) + + createIPMIStatusIcon(graph, cell) } }) } finally { @@ -1310,6 +1321,7 @@ export class InventoryTopology extends PureComponent { containerElement.removeAttribute(attr.nodeName) cell.setValue(containerElement.outerHTML) }) + createIPMIStatusIcon(graph, cell) } }) } finally { @@ -1437,14 +1449,22 @@ export class InventoryTopology extends PureComponent { return [updateCount, err] } - private fetchIpmiStatus = () => { + private fetchIpmiStatus = (focusedCellId?: string) => { if (!this.graph) return const graph = this.graph const {hostsObject} = this.state const parent = graph.getDefaultParent() const cells = this.getAllCells(parent, true) - const ipmiCells: IpmiCell[] = filteredIpmiPowerStatus.bind(this)(cells) + + const filteredCells = focusedCellId + ? getFocusedCell(cells, focusedCellId) + : cells + + const ipmiCells: IpmiCell[] = filteredIpmiPowerStatus.bind(this)( + filteredCells + ) + _.forEach(ipmiCells, ipmiCell => { if (hostsObject[ipmiCell.hostname]?.powerStatus) { ipmiCell.powerStatus = hostsObject[ipmiCell.hostname].powerStatus @@ -1930,11 +1950,37 @@ export class InventoryTopology extends PureComponent { state: IpmiSetPowerStatus, popupText: string ) => { + const credentialKeys = { + target: 'Using Minion', + host: 'IPMI Host', + user: 'IPMI User', + pass: 'IPMI Password', + } + + const ipmiCredentials = { + target, + host: ipmiHost, + user: ipmiUser, + pass: ipmiPass, + } + for (const key in ipmiCredentials) { + if (!ipmiCredentials[key]) { + this.props.notify( + notifySetIpmiStatusFailed( + `The "${credentialKeys[key]}" field cannot be empty.` + ) + ) + return + } + } + + const decryptedPass = cryptoJSAESdecrypt(ipmiPass, this.secretKey.url) + const ipmi: Ipmi = { target, host: ipmiHost, user: ipmiUser, - pass: cryptoJSAESdecrypt(ipmiPass, this.secretKey.url), + pass: decryptedPass, } const onConfirm = async () => { diff --git a/frontend/src/shared/copy/notifications.ts b/frontend/src/shared/copy/notifications.ts index 5d636000..c883b7c4 100644 --- a/frontend/src/shared/copy/notifications.ts +++ b/frontend/src/shared/copy/notifications.ts @@ -1432,3 +1432,11 @@ export const notifyGetDetectedHostStatusFailed = ( duration: INFINITE, message: `Failed to Get Detected Host Status : ${errorMessage}`, }) + +export const notifySetIpmiStatusFailed = ( + errorMessage: string +): Notification => ({ + ...defaultErrorNotification, + duration: INFINITE, + message: `Failed to Set IPMI Status : ${errorMessage}`, +}) From 6c2fb6a21233422ca7e8aa985ff07c16fdcd6c03 Mon Sep 17 00:00:00 2001 From: jaegeunha Date: Tue, 24 Sep 2024 14:09:27 +0900 Subject: [PATCH 07/28] At #528 Add Minimap, HostStatus, IPMI and Link Visibility options in Topology (Backend) --- backend/cloudhub.go | 18 ++++++++--- backend/kv/internal/internal.go | 15 +++++++++ backend/kv/internal/internal.proto | 8 +++++ backend/kv/internal/internal_test.go | 6 ++++ backend/kv/topologys_test.go | 24 ++++++++++++-- backend/server/swagger.json | 42 ++++++++++++++++++++++++- backend/server/topologys.go | 47 ++++++++++++++++++++++------ backend/server/topologys_test.go | 38 +++++++++++++++++++--- 8 files changed, 177 insertions(+), 21 deletions(-) diff --git a/backend/cloudhub.go b/backend/cloudhub.go index 8d167b60..680c7b91 100644 --- a/backend/cloudhub.go +++ b/backend/cloudhub.go @@ -1013,10 +1013,20 @@ type Environment struct { // Topology is represents represents an topology type Topology struct { - ID string `json:"id,string,omitempty"` - Organization string `json:"organization,omitempty"` // Organization is the organization ID that resource belongs to - Diagram string `json:"diagram,string,omitempty"` // diagram xml - Preferences []string `json:"preferences,omitempty"` // User preferences + ID string `json:"id,string,omitempty"` + Organization string `json:"organization,omitempty"` // Organization is the organization ID that resource belongs to + Diagram string `json:"diagram,string,omitempty"` // diagram xml + Preferences []string `json:"preferences,omitempty"` // User preferences + TopologyOptions TopologyOptions `json:"topologyOptions,omitempty"` // Configuration options for the topology, defined in TopologyOptions +} + +// TopologyOptions represents various settings for displaying elements of the topology. +// Each field controls the visibility of specific icons or features within the topology. +type TopologyOptions struct { + MinimapVisible bool `json:"minimapVisible"` // Controls whether the minimap is visible in the mxgraph + HostStatusVisible bool `json:"hostStatusVisible"` // Controls whether the host status is visible + IPMIVisible bool `json:"ipmiVisible"` // Controls whether the IPMI icon is visible + LinkVisible bool `json:"linkVisible"` // Controls whether the dashboard link icon is visible } // TopologyQuery represents the attributes that a topology may be retrieved by. diff --git a/backend/kv/internal/internal.go b/backend/kv/internal/internal.go index c5fcff7d..827783cd 100644 --- a/backend/kv/internal/internal.go +++ b/backend/kv/internal/internal.go @@ -972,6 +972,12 @@ func MarshalTopology(t *cloudhub.Topology) ([]byte, error) { Organization: t.Organization, Diagram: t.Diagram, Preferences: t.Preferences, + TopologyOptions: &TopologyOptions{ + MinimapVisible: t.TopologyOptions.MinimapVisible, + HostStatusVisible: t.TopologyOptions.HostStatusVisible, + IpmiVisible: t.TopologyOptions.IPMIVisible, + LinkVisible: t.TopologyOptions.LinkVisible, + }, }) } @@ -987,6 +993,15 @@ func UnmarshalTopology(data []byte, t *cloudhub.Topology) error { t.Diagram = pb.Diagram t.Preferences = pb.Preferences + if pb.TopologyOptions != nil { + t.TopologyOptions = cloudhub.TopologyOptions{ + MinimapVisible: pb.TopologyOptions.MinimapVisible, + HostStatusVisible: pb.TopologyOptions.HostStatusVisible, + IPMIVisible: pb.TopologyOptions.IpmiVisible, + LinkVisible: pb.TopologyOptions.LinkVisible, + } + } + return nil } diff --git a/backend/kv/internal/internal.proto b/backend/kv/internal/internal.proto index 0a769b9a..c0331ae3 100644 --- a/backend/kv/internal/internal.proto +++ b/backend/kv/internal/internal.proto @@ -275,6 +275,14 @@ message Topology { string Organization = 2; // Organization is the organization ID that resource belongs to string Diagram = 3; // diagram xml repeated string Preferences = 4; // Temperature type and values + TopologyOptions topologyOptions = 5; // Options for the topology +} + +message TopologyOptions { + bool minimapVisible = 1; // Whether to show the minimap + bool hostStatusVisible = 2; // Whether to show the host status + bool ipmiVisible = 3; // Whether to show the IPMI icon + bool linkVisible = 4; // Whether to show the dashboard link icon } message CSP { diff --git a/backend/kv/internal/internal_test.go b/backend/kv/internal/internal_test.go index 9c1c72cd..775645cf 100644 --- a/backend/kv/internal/internal_test.go +++ b/backend/kv/internal/internal_test.go @@ -526,6 +526,12 @@ func TestMarshalTopology(t *testing.T) { "type:inside,active:0,min:38,max:55", "type:outlet,active:0,min:30,max:50", }, + TopologyOptions: cloudhub.TopologyOptions{ + MinimapVisible: true, + HostStatusVisible: false, + IPMIVisible: true, + LinkVisible: true, + }, } var vv cloudhub.Topology diff --git a/backend/kv/topologys_test.go b/backend/kv/topologys_test.go index 342cf7fd..dceb0243 100644 --- a/backend/kv/topologys_test.go +++ b/backend/kv/topologys_test.go @@ -25,12 +25,24 @@ func TestTopologiesStore(t *testing.T) { Organization: "133", Diagram: "", Preferences: []string{"type:inlet,active:1,min:15,max:30"}, + TopologyOptions: cloudhub.TopologyOptions{ + MinimapVisible: true, + HostStatusVisible: false, + IPMIVisible: true, + LinkVisible: true, + }, }, { ID: "", Organization: "226541", Diagram: "", Preferences: []string{"type:inlet,active:1,min:15,max:30"}, + TopologyOptions: cloudhub.TopologyOptions{ + MinimapVisible: false, + HostStatusVisible: true, + IPMIVisible: false, + LinkVisible: true, + }, }, } @@ -56,11 +68,17 @@ func TestTopologiesStore(t *testing.T) { tss[1].Preferences = []string{ "type:inlet,active:1,min:15,max:30", } + tss[1].TopologyOptions = cloudhub.TopologyOptions{ + MinimapVisible: true, + HostStatusVisible: true, + IPMIVisible: true, + LinkVisible: false, + } if err := s.Update(ctx, &tss[1]); err != nil { t.Fatal(err) } - // Confirm topology have updated. + // Confirm topology has updated. ts, err := s.Get(ctx, cloudhub.TopologyQuery{ID: &tss[1].ID}) fmt.Println(ts) if err != nil { @@ -69,9 +87,11 @@ func TestTopologiesStore(t *testing.T) { t.Fatalf("topology 1 update error: got %v, expected %v", ts.Diagram, "") } else if ts.Preferences[0] != "type:inlet,active:1,min:15,max:30" { t.Fatalf("topology 1 update error: got %v, expected %v", ts.Preferences[0], "type:inlet,active:1,min:15,max:30") + } else if !ts.TopologyOptions.MinimapVisible || !ts.TopologyOptions.HostStatusVisible || !ts.TopologyOptions.IPMIVisible || ts.TopologyOptions.LinkVisible { + t.Fatalf("topology 1 update error: topology options not updated correctly") } - // Delete an topology. + // Delete a topology. if err := s.Delete(ctx, ts); err != nil { t.Fatal(err) } diff --git a/backend/server/swagger.json b/backend/server/swagger.json index cbf56cb1..5d975f3b 100644 --- a/backend/server/swagger.json +++ b/backend/server/swagger.json @@ -8129,6 +8129,27 @@ "password": "dkeidj@!@" } }, + "TopologyOptions": { + "type": "object", + "properties": { + "minimapVisible": { + "type": "boolean", + "description": "Visibility of the minimap" + }, + "hostStatusVisible": { + "type": "boolean", + "description": "Visibility of the host status" + }, + "ipmiVisible": { + "type": "boolean", + "description": "Visibility of the IPMI (Intelligent Platform Management Interface)" + }, + "linkVisible": { + "type": "boolean", + "description": "Visibility of the network links" + } + } + }, "InventoryTopologyReq": { "type": "object", "description": "inventory topology diagram xml content", @@ -8149,6 +8170,9 @@ "type:inside,active:1,min:56,max:55", "type:outlet,active:0,min:34,max:50" ] + }, + "topologyOptions": { + "$ref": "#/definitions/TopologyOptions" } } }, @@ -8176,9 +8200,19 @@ "type": "string" } }, + "topologyOptions": { + "$ref": "#/definitions/TopologyOptions" + }, "links": {"type": "string", "description": "self url"} }, - "required": ["id", "organization", "diagram", "links", "preferences"], + "required": [ + "id", + "organization", + "diagram", + "links", + "preferences", + "topologyOptions" + ], "example": { "id": "1", "organization": "2", @@ -8190,6 +8224,12 @@ ], "links": { "self": "/cloudhub/v1/topologies/1" + }, + "topologyOptions": { + "minimapVisible": true, + "hostStatusVisible": true, + "ipmiVisible": true, + "linkVisible": true } } }, diff --git a/backend/server/topologys.go b/backend/server/topologys.go index 2ba9df10..95970608 100644 --- a/backend/server/topologys.go +++ b/backend/server/topologys.go @@ -12,16 +12,27 @@ import ( ) type topologyResponse struct { - ID string `json:"id"` - Organization string `json:"organization"` - Links selfLinks `json:"links"` - Diagram string `json:"diagram,omitempty"` - Preferences []string `json:"preferences,omitempty"` + ID string `json:"id"` + Organization string `json:"organization"` + Links selfLinks `json:"links"` + Diagram string `json:"diagram,omitempty"` + Preferences []string `json:"preferences,omitempty"` + TopologyOptions cloudhub.TopologyOptions `json:"topologyOptions,omitempty"` } +type topologyOptionsResponse struct { + MinimapVisible bool `json:"minimapVisible"` + HostStatusVisible bool `json:"hostStatusVisible"` + IPMIVisible bool `json:"ipmiVisible"` + LinkVisible bool `json:"linkVisible"` +} + +// RequestBody represents the structure of the request payload +// containing cells, user preferences, and topology options for the topology. type RequestBody struct { - Cells string `json:"cells"` - Preferences []string `json:"preferences"` + Cells string `json:"cells"` + Preferences []string `json:"preferences"` + TopologyOptions cloudhub.TopologyOptions `json:"topologyOptions"` } func newTopologyResponse(t *cloudhub.Topology, resDiagram bool) *topologyResponse { @@ -32,6 +43,12 @@ func newTopologyResponse(t *cloudhub.Topology, resDiagram bool) *topologyRespons Organization: t.Organization, Links: selfLinks{Self: selfLink}, Preferences: t.Preferences, + TopologyOptions: cloudhub.TopologyOptions{ + MinimapVisible: t.TopologyOptions.MinimapVisible, + HostStatusVisible: t.TopologyOptions.HostStatusVisible, + IPMIVisible: t.TopologyOptions.IPMIVisible, + LinkVisible: t.TopologyOptions.LinkVisible, + }, } if resDiagram { @@ -102,6 +119,12 @@ func (s *Service) NewTopology(w http.ResponseWriter, r *http.Request) { Diagram: requestData.Cells, Preferences: requestData.Preferences, Organization: defaultOrg.ID, + TopologyOptions: cloudhub.TopologyOptions{ + MinimapVisible: requestData.TopologyOptions.MinimapVisible, + HostStatusVisible: requestData.TopologyOptions.HostStatusVisible, + IPMIVisible: requestData.TopologyOptions.IPMIVisible, + LinkVisible: requestData.TopologyOptions.LinkVisible, + }, } if err := ValidTopologRequest(topology, defaultOrg.ID); err != nil { @@ -115,7 +138,7 @@ func (s *Service) NewTopology(w http.ResponseWriter, r *http.Request) { return } - // log registrationte + // log registration org, _ := s.Store.Organizations(ctx).Get(ctx, cloudhub.OrganizationQuery{ID: &res.Organization}) msg := fmt.Sprintf(MsgTopologyCreated.String(), org.Name) s.logRegistration(ctx, "Topologies", msg) @@ -190,6 +213,12 @@ func (s *Service) UpdateTopology(w http.ResponseWriter, r *http.Request) { topology.Diagram = requestData.Cells topology.Preferences = requestData.Preferences + topology.TopologyOptions = cloudhub.TopologyOptions{ + MinimapVisible: requestData.TopologyOptions.MinimapVisible, + HostStatusVisible: requestData.TopologyOptions.HostStatusVisible, + IPMIVisible: requestData.TopologyOptions.IPMIVisible, + LinkVisible: requestData.TopologyOptions.LinkVisible, + } if err := s.Store.Topologies(ctx).Update(ctx, topology); err != nil { msg := fmt.Sprintf("Error updating topology ID %s: %v", id, err) @@ -197,7 +226,7 @@ func (s *Service) UpdateTopology(w http.ResponseWriter, r *http.Request) { return } - // log registrationte + // log registration org, _ := s.Store.Organizations(ctx).Get(ctx, cloudhub.OrganizationQuery{ID: &topology.Organization}) msg := fmt.Sprintf(MsgTopologyModified.String(), org.Name) s.logRegistration(ctx, "Topologies", msg) diff --git a/backend/server/topologys_test.go b/backend/server/topologys_test.go index cfe0c987..99c87217 100644 --- a/backend/server/topologys_test.go +++ b/backend/server/topologys_test.go @@ -56,6 +56,12 @@ func TestTopology(t *testing.T) { "type:inside,active:0,min:38,max:55", "type:outlet,active:0,min:30,max:50", }, + TopologyOptions: cloudhub.TopologyOptions{ + MinimapVisible: true, + HostStatusVisible: false, + IPMIVisible: true, + LinkVisible: true, + }, }, nil }, }, @@ -70,7 +76,7 @@ func TestTopology(t *testing.T) { }, wantStatus: http.StatusOK, wantContentType: "application/json", - wantBody: `{"id":"1","organization":"225","links":{"self":"/cloudhub/v1/topologies/1"},"diagram":"","preferences":["type:inlet,active:1,min:15,max:30","type:inside,active:0,min:38,max:55","type:outlet,active:0,min:30,max:50"]}`, + wantBody: `{"id":"1","organization":"225","links":{"self":"/cloudhub/v1/topologies/1"},"diagram":"\u003cxml\u003e\u003c/xml\u003e","preferences":["type:inlet,active:1,min:15,max:30","type:inside,active:0,min:38,max:55","type:outlet,active:0,min:30,max:50"],"topologyOptions":{"minimapVisible":true,"hostStatusVisible":false,"ipmiVisible":true,"linkVisible":true}}`, }, } @@ -135,7 +141,12 @@ func TestUpdateTopology(t *testing.T) { "type:inlet,active:0,min:15,max:30", "type:inside,active:1,min:56,max:55", "type:outlet,active:0,min:34,max:50" - ]}`, + ], "topologyOptions": { + "minimapVisible": true, + "hostStatusVisible": false, + "ipmiVisible": true, + "linkVisible": true + }}`, }, fields: fields{ Logger: log.New(log.DebugLevel), @@ -153,6 +164,12 @@ func TestUpdateTopology(t *testing.T) { "type:inside,active:0,min:38,max:55", "type:outlet,active:0,min:30,max:50", }, + TopologyOptions: cloudhub.TopologyOptions{ + MinimapVisible: true, + HostStatusVisible: false, + IPMIVisible: true, + LinkVisible: true, + }, }, nil }, }, @@ -160,7 +177,7 @@ func TestUpdateTopology(t *testing.T) { id: "1", wantStatus: http.StatusOK, wantContentType: "application/json", - wantBody: `{"id":"1","organization":"225","links":{"self":"/cloudhub/v1/topologies/1"},"preferences":["type:inlet,active:1,min:15,max:30","type:inside,active:0,min:38,max:55","type:outlet,active:0,min:30,max:50"]}`, + wantBody: `{"id":"1","organization":"225","links":{"self":"/cloudhub/v1/topologies/1"},"preferences":["type:inlet,active:1,min:15,max:30","type:inside,active:0,min:38,max:55","type:outlet,active:0,min:30,max:50"],"topologyOptions":{"minimapVisible":true,"hostStatusVisible":false,"ipmiVisible":true,"linkVisible":true}}`, }, } @@ -317,7 +334,12 @@ func TestNewTopology(t *testing.T) { "type:inlet,active:0,min:15,max:30", "type:inside,active:1,min:56,max:55", "type:outlet,active:0,min:34,max:50" - ]}`, + ], "topologyOptions": { + "minimapVisible": true, + "hostStatusVisible": false, + "ipmiVisible": true, + "linkVisible": true + }}`, }, fields: fields{ Logger: log.New(log.DebugLevel), @@ -332,6 +354,12 @@ func TestNewTopology(t *testing.T) { "type:inside,active:0,min:38,max:55", "type:outlet,active:0,min:30,max:50", }, + TopologyOptions: cloudhub.TopologyOptions{ + MinimapVisible: true, + HostStatusVisible: false, + IPMIVisible: true, + LinkVisible: true, + }, }, nil }, GetF: func(ctx context.Context, q cloudhub.TopologyQuery) (*cloudhub.Topology, error) { @@ -365,7 +393,7 @@ func TestNewTopology(t *testing.T) { }, wantStatus: http.StatusCreated, wantContentType: "application/json", - wantBody: `{"id":"1","links":{"self":"/cloudhub/v1/topologies/1"},"organization":"225","preferences":["type:inlet,active:1,min:15,max:30","type:inside,active:0,min:38,max:55","type:outlet,active:0,min:30,max:50"]}`, + wantBody: `{"id":"1","links":{"self":"/cloudhub/v1/topologies/1"},"organization":"225","preferences":["type:inlet,active:1,min:15,max:30","type:inside,active:0,min:38,max:55","type:outlet,active:0,min:30,max:50"],"topologyOptions":{"minimapVisible":true,"hostStatusVisible":false,"ipmiVisible":true,"linkVisible":true}}`, }, { name: "Fail to create topology - no body", From 53c5376e36e6d2f7202a8c2761f794ad03c2418a Mon Sep 17 00:00:00 2001 From: jaeheesnet Date: Tue, 24 Sep 2024 07:31:03 +0000 Subject: [PATCH 08/28] At #528 NewFeature Topology Options Setting Modal and Setting Button --- .../src/hosts/actions/inventoryTopology.ts | 12 +- frontend/src/hosts/apis/index.ts | 11 +- .../components/TopologySettingOverlay.tsx | 157 ++++++++++++++++++ frontend/src/hosts/components/host.scss | 36 ++++ frontend/src/hosts/configurations/topology.ts | 48 +++++- frontend/src/hosts/constants/tools.ts | 5 + .../hosts/containers/InventoryTopology.tsx | 155 ++++++++++++++++- frontend/src/hosts/types/cloud.ts | 7 + frontend/src/hosts/types/index.ts | 2 + frontend/src/shared/copy/notifications.ts | 14 ++ 10 files changed, 429 insertions(+), 18 deletions(-) create mode 100644 frontend/src/hosts/components/TopologySettingOverlay.tsx diff --git a/frontend/src/hosts/actions/inventoryTopology.ts b/frontend/src/hosts/actions/inventoryTopology.ts index 937bb031..64b590ec 100644 --- a/frontend/src/hosts/actions/inventoryTopology.ts +++ b/frontend/src/hosts/actions/inventoryTopology.ts @@ -39,7 +39,11 @@ import { notifygetGCPInstancesFailed, } from 'src/shared/copy/notifications' import {IpmiSetPowerStatus} from 'src/shared/apis/saltStack' -import {CloudServiceProvider, CSPFileWriteParam} from 'src/hosts/types' +import { + CloudServiceProvider, + CSPFileWriteParam, + TopologyOption, +} from 'src/hosts/types' export enum ActionTypes { LoadInventoryTopology = 'LOAD_INVENTORY_TOPOLOGY', @@ -150,14 +154,16 @@ export const updateInventoryTopologyAsync = ( links: Links, cellsId: string, cells: string, - preferences: string[] + preferences: string[], + topologyOptions: TopologyOption ) => async (dispatch: Dispatch) => { try { const resultUpdateInventoryTopology = await updateInventoryTopology( links, cellsId, cells, - preferences + preferences, + topologyOptions ) return resultUpdateInventoryTopology diff --git a/frontend/src/hosts/apis/index.ts b/frontend/src/hosts/apis/index.ts index d456f1d2..a2d6112e 100644 --- a/frontend/src/hosts/apis/index.ts +++ b/frontend/src/hosts/apis/index.ts @@ -13,7 +13,11 @@ import { // Types import {Template, Layout, Source, Host, Links} from 'src/types' import {HostNames, HostName, Ipmi, IpmiCell} from 'src/types/hosts' -import {CloudServiceProvider, CSPFileWriteParam} from 'src/hosts/types' +import { + CloudServiceProvider, + CSPFileWriteParam, + TopologyOption, +} from 'src/hosts/types' import {DashboardSwitcherLinks} from 'src/types/dashboards' // APIs @@ -903,12 +907,13 @@ export const updateInventoryTopology = async ( links: Links, cellsId: string, cells: string, - preferences: string[] + preferences: string[], + topologyOptions: TopologyOption ) => { return await AJAX({ url: `${_.get(links, 'topologies')}/${cellsId}`, method: 'PATCH', - data: {cells, preferences}, + data: {cells, preferences, topologyOptions}, headers: {'Content-Type': 'text/xml'}, }) } diff --git a/frontend/src/hosts/components/TopologySettingOverlay.tsx b/frontend/src/hosts/components/TopologySettingOverlay.tsx new file mode 100644 index 00000000..d48625ba --- /dev/null +++ b/frontend/src/hosts/components/TopologySettingOverlay.tsx @@ -0,0 +1,157 @@ +import React, {PureComponent} from 'react' +import Container from 'src/reusable_ui/components/overlays/OverlayContainer' +import Body from 'src/reusable_ui/components/overlays/OverlayBody' +import {Button, ComponentColor} from 'src/reusable_ui' +import PageSpinner from 'src/shared/components/PageSpinner' +import {RemoteDataState} from 'src/types' +import {TopologyOption} from '../types' + +interface Props { + isOptionOverlayVisible: boolean + setIsOptionOverlayVisible: (value: boolean) => void + state: RemoteDataState + topologyOption: TopologyOption + setTopologyOption: (value: TopologyOption) => void +} + +interface State { + topologyOption: TopologyOption +} + +class TopologySettingOverlay extends PureComponent { + constructor(props: Props) { + super(props) + + this.state = { + topologyOption: { + hostStatusVisible: props.topologyOption.hostStatusVisible, + ipmiVisible: props.topologyOption.ipmiVisible, + linkVisible: props.topologyOption.linkVisible, + minimapVisible: props.topologyOption.minimapVisible, + }, + } + } + + private get renderSettingHeader() { + return ( +
+
+ {'Display Setting'} +
+
+ ) + } + + private get renderSetSettingPopup() { + const {topologyOption} = this.props + return ( +
+
+ + this.onCheckClick('minimapVisible', e.currentTarget.checked) + } + /> + +
+
+ + this.onCheckClick('ipmiVisible', e.currentTarget.checked) + } + /> + +
+
+ + this.onCheckClick('linkVisible', e.currentTarget.checked) + } + /> + +
+
+ + this.onCheckClick('hostStatusVisible', e.currentTarget.checked) + } + /> + +
+
+ ) + } + + public render() { + const {state} = this.props + return ( + <> +
+ {state === RemoteDataState.Loading && ( +
+ +
+ )} + + {this.renderSettingHeader} + {this.renderSetSettingPopup} + +
+ + ) + } + + private onCheckClick(option: string, value: boolean) { + const prevOption = {...this.state.topologyOption} + prevOption[option] = value + + this.setState({ + topologyOption: { + ...prevOption, + }, + }) + } + + private onOkClick() { + const {setTopologyOption} = this.props + const {topologyOption} = this.state + + setTopologyOption({ + ...topologyOption, + }) + + this.onClose() + } + + private onClose() { + const {setIsOptionOverlayVisible} = this.props + setIsOptionOverlayVisible(false) + } +} + +export default TopologySettingOverlay diff --git a/frontend/src/hosts/components/host.scss b/frontend/src/hosts/components/host.scss index 519923f1..46f15960 100644 --- a/frontend/src/hosts/components/host.scss +++ b/frontend/src/hosts/components/host.scss @@ -446,6 +446,42 @@ margin: 0 4px; } } + +.topology-setting { + position: fixed; + top: 20%; + left: 50%; + transform: translate(-50%, -50%); + width: 300px; + border: 2px solid $g5-pepper; + + #setting-title { + font-size: 17px; + padding-right: 25px; + } + + .btn-ok { + background-color: $c-rainforest; + color: $g20-white; + display: inline-block; + width: 58px; + margin: 0 4px; + } + + .btn-cancel { + display: inline-block; + width: 58px; + margin: 0 4px; + } + + .topology-spinner { + position: absolute; + z-index: 3; + background-color: rgba(0, 0, 0, 0.5); + width: 100%; + height: 100%; + } +} .fetchIntervalDots { position: absolute; margin-left: 5px; diff --git a/frontend/src/hosts/configurations/topology.ts b/frontend/src/hosts/configurations/topology.ts index 60a066e5..ef98e235 100644 --- a/frontend/src/hosts/configurations/topology.ts +++ b/frontend/src/hosts/configurations/topology.ts @@ -354,6 +354,7 @@ export const createTextField = function ( 'ipmi', attribute.nodeValue === 'ipmi' ? true : false ) + form.addOption( input, 'FALSE', @@ -433,7 +434,6 @@ export const createIPMIStatusIcon = function ( childrenCell.setVisible(true) } } - export const applyHandler = async function ( graph: mxGraphType, cell: mxCellType, @@ -464,7 +464,7 @@ export const applyHandler = async function ( childrenLink.setAttribute('href', newValue) childrenCell.setValue(childrenContainerElement.outerHTML) - childrenCell.setVisible(getIsHasString(newValue)) + childrenCell.setVisible(this.state.topologyOption.linkVisible) } } } @@ -475,7 +475,6 @@ export const applyHandler = async function ( newValue, this.secretKey.url ).toString() - isInputPassword = true } } @@ -515,6 +514,42 @@ export const applyHandler = async function ( this.graphUpdateSave(cell) this.setState({fetchIntervalDataStatus: RemoteDataState.Done}) } + } else { + if (attribute === 'data-link') { + if (cell.children) { + const childrenCell = cell.getChildAt(1) + const dataLink = cell.value.match(/data-link="([^"]+)"/) + if (childrenCell.style.includes('href') && !!dataLink) { + const childrenContainerElement = getContainerElement( + childrenCell.value + ) + + const childrenLink = childrenContainerElement.querySelector('a') + childrenLink.setAttribute('href', newValue) + childrenCell.setValue(childrenContainerElement.outerHTML) + childrenCell.setVisible(this.state.topologyOption.linkVisible) + } + } + } + + if (attribute === 'data-ipmi_host') { + if (cell.children) { + const childrenCell = cell.getChildAt(0) + childrenCell.setVisible(this.state.topologyOption.ipmiVisible) + if (childrenCell.value.includes('ipmi')) { + } + } + } + if (attribute === 'data-status') { + const childrenCell = cell.getChildAt(2) + const dataStatus = + cell.value.match(/data-status="([^"]+)"/)[1].trim() ?? true + if (childrenCell.style.includes('status')) { + childrenCell.setVisible( + dataStatus !== 'false' && this.state.topologyOption.hostStatusVisible + ) + } + } } } @@ -627,7 +662,6 @@ export const dragCell = (node: Menu, self: any) => ( ) v1.setConnectable(true) - const ipmiBox = document.createElement('div') ipmiBox.classList.add('vertex') ipmiBox.setAttribute('btn-type', 'ipmi') @@ -656,7 +690,7 @@ export const dragCell = (node: Menu, self: any) => ( ipmiStatus.geometry.offset = new mxPoint(-12, -12) ipmiStatus.setConnectable(false) - ipmiStatus.setVisible(true) + ipmiStatus.setVisible(self.state.topologyOption.ipmiVisible) graph.setCellStyles(mxConstants.STYLE_STROKECOLOR, 'white', [ipmiStatus]) const linkBox = document.createElement('div') @@ -747,7 +781,9 @@ export const dragCell = (node: Menu, self: any) => ( statusCell.geometry.offset = new mxPoint(-24, 6) statusCell.setConnectable(false) const statusCheck = _.get(node, 'status') ? true : false - statusCell.setVisible(statusCheck) + statusCell.setVisible( + statusCheck && self.state.topologyOption.hostStatusVisible + ) } finally { model.endUpdate() } diff --git a/frontend/src/hosts/constants/tools.ts b/frontend/src/hosts/constants/tools.ts index cae2dc5e..9529d642 100644 --- a/frontend/src/hosts/constants/tools.ts +++ b/frontend/src/hosts/constants/tools.ts @@ -81,6 +81,11 @@ export const toolbarMenu: ToolbarMenu[] = [ label: 'Preferences', icon: 'wrench', }, + { + actionName: 'option', + label: 'Option', + icon: 'cog-thick', + }, ] export interface Menu { diff --git a/frontend/src/hosts/containers/InventoryTopology.tsx b/frontend/src/hosts/containers/InventoryTopology.tsx index afdd32b5..7c84151d 100644 --- a/frontend/src/hosts/containers/InventoryTopology.tsx +++ b/frontend/src/hosts/containers/InventoryTopology.tsx @@ -62,6 +62,8 @@ import { notifyFetchIntervalDataFailed, notifyGetDetectedHostStatusFailed, notifySetIpmiStatusFailed, + notifyTopologyOptionChangeFailed, + notifyMapReloadFailed, } from 'src/shared/copy/notifications' import {notIncludeApps} from 'src/hosts/constants/apps' import { @@ -92,6 +94,7 @@ import { CSPFileWriteParam, Instance, PreferenceType, + TopologyOption, } from 'src/hosts/types' import { default as mxgraph, @@ -219,6 +222,7 @@ import { getLocalStorage, setLocalStorage, } from 'src/shared/middleware/localStorage' +import TopologySettingOverlay from 'src/hosts/components/TopologySettingOverlay' const mx = mxgraph() @@ -403,6 +407,7 @@ interface State { hostDetailInfoWithSalt: Partial | {} isGetHostDetailInfo: RemoteDataState isPreferencesOverlayVisible: boolean + isOptionOverlayVisible: boolean preferenceTemperatureValues: string[] unsavedTopology: string preferencesStatus: RemoteDataState @@ -419,6 +424,8 @@ interface State { } tooltipNode: Partial isMouseUp: boolean + isSettingOverlayOpen: boolean + topologyOption: TopologyOption } @ErrorHandling @@ -527,6 +534,7 @@ export class InventoryTopology extends PureComponent { hostDetailInfoWithSalt: {}, isGetHostDetailInfo: RemoteDataState.NotStarted, isPreferencesOverlayVisible: false, + isOptionOverlayVisible: false, preferenceTemperatureValues: [], unsavedPreferenceTemperatureValues: [], unsavedTopology: '', @@ -543,6 +551,13 @@ export class InventoryTopology extends PureComponent { }, tooltipNode: {}, isMouseUp: true, + isSettingOverlayOpen: false, + topologyOption: { + minimapVisible: true, + ipmiVisible: true, + linkVisible: true, + hostStatusVisible: true, + }, } } @@ -656,6 +671,7 @@ export class InventoryTopology extends PureComponent { hostsObject, isDetectedServer, activeEditorTab, + topologyOption, } = this.state if (layouts) { @@ -687,6 +703,36 @@ export class InventoryTopology extends PureComponent { if (prevState.cloudAccessInfos !== cloudAccessInfos) { this.makeTreemenu() } + + if ( + !_.isEqual( + prevState.topologyOption.minimapVisible, + topologyOption.minimapVisible + ) + ) { + this.mapReload() + } + + if ( + !_.isEqual( + prevState.topologyOption.ipmiVisible, + topologyOption.ipmiVisible + ) || + !_.isEqual( + prevState.topologyOption.hostStatusVisible, + topologyOption.hostStatusVisible + ) || + !_.isEqual( + prevState.topologyOption.linkVisible, + topologyOption.linkVisible + ) + ) { + this.fetchIntervalData() + } + + if (!_.isEqual(prevState.topologyOption, topologyOption)) { + this.handleChangeTopologyOption() + } } if (prevState.selectItem !== selectItem && selectItem === 'Host') { @@ -785,10 +831,12 @@ export class InventoryTopology extends PureComponent { isImportTopologyOverlayVisible, isPreferencesOverlayVisible, preferencesStatus, + isOptionOverlayVisible, + topologyOption, } = this.state const {notify} = this.props const isExportXML = modalTitle === 'Export XML' - + //modal return (
{!mxClient.isBrowserSupported() ? ( @@ -858,6 +906,19 @@ export class InventoryTopology extends PureComponent { notify={notify} /> )} + {isOptionOverlayVisible && ( + + this.setState({isOptionOverlayVisible: value}) + } + state={RemoteDataState.Done} + topologyOption={topologyOption} + setTopologyOption={(value: TopologyOption) => + this.setState({topologyOption: value}) + } + /> + )} )}
@@ -1202,6 +1263,60 @@ export class InventoryTopology extends PureComponent { }) } + private handleChangeTopologyOption = () => { + if (this.isValidTemperature()) { + return + } + if (!this.graph) return + const graph = this.graph + + graph.getModel().beginUpdate() + + try { + _.forEach(graph.getModel().cells, (cell: mxCellType) => { + const containerElement = getContainerElement(cell.value) + + if (containerElement && containerElement.hasAttribute('data-type')) { + const dataType = containerElement.getAttribute('data-type') + const attrsKeys = _.map( + _.keys(eachNodeTypeAttrs[dataType].attrs), + attr => `data-${attr}` + ) + + _.forEach(attrsKeys, attr => { + applyHandler.bind(this)(graph, cell, attr) + }) + } + }) + } catch (error) { + notify(notifyTopologyOptionChangeFailed(error.message)) + } finally { + graph.getModel().endUpdate() + } + } + + private mapReload = () => { + if (this.isValidTemperature()) { + return + } + if (!this.graph) return + + const graph = this.graph + const currentTranslate = graph.getView().getTranslate() + const currentScale = graph.getView().getScale() + + graph.getModel().beginUpdate() + try { + graph.refresh() + graph.getView().setTranslate(currentTranslate.x, currentTranslate.y) + graph.getView().setScale(currentScale) + } catch (error) { + notify(notifyMapReloadFailed(error.message)) + } finally { + graph.getModel().endUpdate() + } + } + private handleCloseInstanceTypeModal = () => { this.setState({isInstanceTypeModalVisible: false}) } @@ -1299,7 +1414,6 @@ export class InventoryTopology extends PureComponent { _.keys(eachNodeTypeAttrs[dataType].attrs), attr => `data-${attr}` ) - const filterdAttrs = _.difference( _.map( _.filter( @@ -1326,6 +1440,10 @@ export class InventoryTopology extends PureComponent { }) } finally { graph.getModel().endUpdate() + + if (_.get(topology, 'topologyOptions')) { + this.setState({topologyOption: topology.topologyOptions}) + } } } @@ -1450,7 +1568,7 @@ export class InventoryTopology extends PureComponent { } private fetchIpmiStatus = (focusedCellId?: string) => { - if (!this.graph) return + if (!this.graph || !this.state.topologyOption.ipmiVisible) return const graph = this.graph const {hostsObject} = this.state @@ -2277,10 +2395,24 @@ export class InventoryTopology extends PureComponent { meRole === EDITOR_ROLE ) { this.setState({ + isOptionOverlayVisible: false, isPreferencesOverlayVisible: true, }) } }) + + this.editor.addAction('option', async () => { + if ( + meRole === SUPERADMIN_ROLE || + meRole === ADMIN_ROLE || + meRole === EDITOR_ROLE + ) { + this.setState({ + isOptionOverlayVisible: true, + isPreferencesOverlayVisible: false, + }) + } + }) } // @ts-ignore @@ -2413,6 +2545,7 @@ export class InventoryTopology extends PureComponent { isPinned, isOpenSensorData, fetchIntervalDataStatus, + topologyOption, } = this.state const [topSize, bottomSize] = bottomProportions return [ @@ -2437,8 +2570,16 @@ export class InventoryTopology extends PureComponent { {topologyStatus === RemoteDataState.Loading && ( )} - -
+ {fetchIntervalDataStatus === RemoteDataState.Loading && ( + + )} +
{ topologyId, topology, unsavedPreferenceTemperatureValues, + topologyOption, } = this.state this.setState({topologyStatus: RemoteDataState.Loading}) @@ -2813,7 +2955,8 @@ export class InventoryTopology extends PureComponent { links, topologyId, topology, - unsavedPreferenceTemperatureValues + unsavedPreferenceTemperatureValues, + topologyOption ) notify(notifyTopologySaved()) diff --git a/frontend/src/hosts/types/cloud.ts b/frontend/src/hosts/types/cloud.ts index 4a07cbeb..ba99bcab 100644 --- a/frontend/src/hosts/types/cloud.ts +++ b/frontend/src/hosts/types/cloud.ts @@ -87,3 +87,10 @@ export interface Instance { instanceid: string instancename: string } + +export interface TopologyOption { + hostStatusVisible: boolean + ipmiVisible: boolean + linkVisible: boolean + minimapVisible: boolean +} diff --git a/frontend/src/hosts/types/index.ts b/frontend/src/hosts/types/index.ts index b1c4f2b2..b95acaa7 100644 --- a/frontend/src/hosts/types/index.ts +++ b/frontend/src/hosts/types/index.ts @@ -4,6 +4,7 @@ import { CSPAccessObject, CSPFileWriteParam, Instance, + TopologyOption, } from 'src/hosts/types/cloud' import {PreferenceType} from 'src/hosts/types/preferences' @@ -14,4 +15,5 @@ export { CSPFileWriteParam, Instance, PreferenceType, + TopologyOption, } diff --git a/frontend/src/shared/copy/notifications.ts b/frontend/src/shared/copy/notifications.ts index c883b7c4..b64d9e0b 100644 --- a/frontend/src/shared/copy/notifications.ts +++ b/frontend/src/shared/copy/notifications.ts @@ -1440,3 +1440,17 @@ export const notifySetIpmiStatusFailed = ( duration: INFINITE, message: `Failed to Set IPMI Status : ${errorMessage}`, }) + +export const notifyTopologyOptionChangeFailed = ( + errorMessage: string +): Notification => ({ + ...defaultErrorNotification, + duration: INFINITE, + message: `Failed to Change Topology Options : ${errorMessage}`, +}) + +export const notifyMapReloadFailed = (errorMessage: string): Notification => ({ + ...defaultErrorNotification, + duration: INFINITE, + message: `Failed to Reload Map : ${errorMessage}`, +}) From c32c052304411498d4bea8252ce7e2ea47681078 Mon Sep 17 00:00:00 2001 From: jaeheesnet Date: Tue, 24 Sep 2024 08:17:50 +0000 Subject: [PATCH 09/28] At #528 Topology Cell Properties Status option change 'FALSE' to 'NONE' --- frontend/src/hosts/configurations/topology.ts | 12 ++++++------ frontend/src/hosts/containers/InventoryTopology.tsx | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/hosts/configurations/topology.ts b/frontend/src/hosts/configurations/topology.ts index ef98e235..c1206bc0 100644 --- a/frontend/src/hosts/configurations/topology.ts +++ b/frontend/src/hosts/configurations/topology.ts @@ -357,9 +357,9 @@ export const createTextField = function ( form.addOption( input, - 'FALSE', - false, - attribute.nodeValue === 'false' ? true : false + 'NONE', + 'none', + attribute.nodeValue === 'none' ? true : false ) } else if (attribute.nodeName === 'data-icon') { input = form.addCombo(nodeName, false) @@ -483,7 +483,7 @@ export const applyHandler = async function ( const childrenCell = cell.getChildAt(2) if (childrenCell.style.includes('status')) { - childrenCell.setVisible(newValue !== 'false' ? true : false) + childrenCell.setVisible(newValue !== 'none' ? true : false) shouldFetchData = newValue !== 'false' } } @@ -546,7 +546,7 @@ export const applyHandler = async function ( cell.value.match(/data-status="([^"]+)"/)[1].trim() ?? true if (childrenCell.style.includes('status')) { childrenCell.setVisible( - dataStatus !== 'false' && this.state.topologyOption.hostStatusVisible + dataStatus !== 'none' && this.state.topologyOption.hostStatusVisible ) } } @@ -780,7 +780,7 @@ export const dragCell = (node: Menu, self: any) => ( statusCell.geometry.offset = new mxPoint(-24, 6) statusCell.setConnectable(false) - const statusCheck = _.get(node, 'status') ? true : false + const statusCheck = _.get(node, 'status') !== 'none' ? true : false statusCell.setVisible( statusCheck && self.state.topologyOption.hostStatusVisible ) diff --git a/frontend/src/hosts/containers/InventoryTopology.tsx b/frontend/src/hosts/containers/InventoryTopology.tsx index 7c84151d..3f80aece 100644 --- a/frontend/src/hosts/containers/InventoryTopology.tsx +++ b/frontend/src/hosts/containers/InventoryTopology.tsx @@ -3879,7 +3879,7 @@ export class InventoryTopology extends PureComponent { const hostname = container.getAttribute('data-name') const dataGatherType = container.getAttribute('data-status') - if (isTooltipActiveHost === cellId || dataGatherType === 'false') { + if (isTooltipActiveHost === cellId || dataGatherType === 'none') { return } From 02c742071fc1e4fbf021dda5554b87361a0a78ce Mon Sep 17 00:00:00 2001 From: jaegeunha Date: Tue, 24 Sep 2024 17:19:15 +0900 Subject: [PATCH 10/28] At #528 Set default values when TopologyOptions is nil --- backend/kv/internal/internal.go | 11 +++++++++++ backend/server/topologys.go | 7 ------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/backend/kv/internal/internal.go b/backend/kv/internal/internal.go index 827783cd..8c841b3d 100644 --- a/backend/kv/internal/internal.go +++ b/backend/kv/internal/internal.go @@ -1000,11 +1000,22 @@ func UnmarshalTopology(data []byte, t *cloudhub.Topology) error { IPMIVisible: pb.TopologyOptions.IpmiVisible, LinkVisible: pb.TopologyOptions.LinkVisible, } + } else { + t.TopologyOptions = getDefaultTopologyOptions() } return nil } +func getDefaultTopologyOptions() cloudhub.TopologyOptions { + return cloudhub.TopologyOptions{ + MinimapVisible: true, + HostStatusVisible: true, + IPMIVisible: true, + LinkVisible: true, + } +} + // MarshalCSP encodes a mapping to binary protobuf format. func MarshalCSP(t *cloudhub.CSP) ([]byte, error) { return proto.Marshal(&CSP{ diff --git a/backend/server/topologys.go b/backend/server/topologys.go index 95970608..3332bb4c 100644 --- a/backend/server/topologys.go +++ b/backend/server/topologys.go @@ -20,13 +20,6 @@ type topologyResponse struct { TopologyOptions cloudhub.TopologyOptions `json:"topologyOptions,omitempty"` } -type topologyOptionsResponse struct { - MinimapVisible bool `json:"minimapVisible"` - HostStatusVisible bool `json:"hostStatusVisible"` - IPMIVisible bool `json:"ipmiVisible"` - LinkVisible bool `json:"linkVisible"` -} - // RequestBody represents the structure of the request payload // containing cells, user preferences, and topology options for the topology. type RequestBody struct { From c90303b566f57bd3b5aa6cca884d2914607771d5 Mon Sep 17 00:00:00 2001 From: jaeheesnet Date: Tue, 24 Sep 2024 08:39:01 +0000 Subject: [PATCH 11/28] At #528 Setting Modal Label Change --- .../components/TopologySettingOverlay.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/src/hosts/components/TopologySettingOverlay.tsx b/frontend/src/hosts/components/TopologySettingOverlay.tsx index d48625ba..0ac7461f 100644 --- a/frontend/src/hosts/components/TopologySettingOverlay.tsx +++ b/frontend/src/hosts/components/TopologySettingOverlay.tsx @@ -79,29 +79,29 @@ class TopologySettingOverlay extends PureComponent { this.onCheckClick('ipmiVisible', e.currentTarget.checked) } /> - +
- this.onCheckClick('linkVisible', e.currentTarget.checked) + this.onCheckClick('hostStatusVisible', e.currentTarget.checked) } /> - +
- this.onCheckClick('hostStatusVisible', e.currentTarget.checked) + this.onCheckClick('linkVisible', e.currentTarget.checked) } /> - +
) From 4ee51b6da97e93b0f71aefd53ff3277268eb20ab Mon Sep 17 00:00:00 2001 From: jaegeunha Date: Wed, 25 Sep 2024 13:19:36 +0900 Subject: [PATCH 12/28] At #528 Modify Host Health Status display to return as an array when no cell is available --- frontend/src/hosts/configurations/topology.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/hosts/configurations/topology.ts b/frontend/src/hosts/configurations/topology.ts index c1206bc0..3f708212 100644 --- a/frontend/src/hosts/configurations/topology.ts +++ b/frontend/src/hosts/configurations/topology.ts @@ -1556,11 +1556,11 @@ export const detectedHostsStatus = function ( hostsObject: {[x: string]: Host}, selectedTemperatureValue: string = 'type:inside,active:1,min:38,max:55' ) { - if (!this.graph) return + if (!this.graph) return [0, new Error('Graph object is missing or null')] const {cpu, memory, disk, temperature} = TOOLTIP_TYPE if (!cells || cells.length === 0) { - return + return [0, null] } let nodeCount = 0 From 913b83558e5ec6580705023c7ada6aeed9b681d5 Mon Sep 17 00:00:00 2001 From: jaeheesnet Date: Wed, 25 Sep 2024 05:03:25 +0000 Subject: [PATCH 13/28] At #528 Topology mxGraph Bug fix --- frontend/src/hosts/configurations/topology.ts | 32 +++++++++++-------- .../hosts/containers/InventoryTopology.tsx | 7 +--- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/frontend/src/hosts/configurations/topology.ts b/frontend/src/hosts/configurations/topology.ts index 3f708212..b7cb071c 100644 --- a/frontend/src/hosts/configurations/topology.ts +++ b/frontend/src/hosts/configurations/topology.ts @@ -354,7 +354,6 @@ export const createTextField = function ( 'ipmi', attribute.nodeValue === 'ipmi' ? true : false ) - form.addOption( input, 'NONE', @@ -464,7 +463,9 @@ export const applyHandler = async function ( childrenLink.setAttribute('href', newValue) childrenCell.setValue(childrenContainerElement.outerHTML) - childrenCell.setVisible(this.state.topologyOption.linkVisible) + childrenCell.setVisible( + !!newValue && this.state.topologyOption.linkVisible + ) } } } @@ -483,7 +484,10 @@ export const applyHandler = async function ( const childrenCell = cell.getChildAt(2) if (childrenCell.style.includes('status')) { - childrenCell.setVisible(newValue !== 'none' ? true : false) + childrenCell.setVisible( + (newValue !== 'none' ? true : false) && + this.state.topologyOption.hostStatusVisible + ) shouldFetchData = newValue !== 'false' } } @@ -519,13 +523,12 @@ export const applyHandler = async function ( if (cell.children) { const childrenCell = cell.getChildAt(1) const dataLink = cell.value.match(/data-link="([^"]+)"/) - if (childrenCell.style.includes('href') && !!dataLink) { + if (childrenCell?.style?.includes('href') && !!dataLink) { const childrenContainerElement = getContainerElement( childrenCell.value ) - const childrenLink = childrenContainerElement.querySelector('a') - childrenLink.setAttribute('href', newValue) + childrenLink.setAttribute('href', dataLink[1]) childrenCell.setValue(childrenContainerElement.outerHTML) childrenCell.setVisible(this.state.topologyOption.linkVisible) } @@ -541,13 +544,16 @@ export const applyHandler = async function ( } } if (attribute === 'data-status') { - const childrenCell = cell.getChildAt(2) - const dataStatus = - cell.value.match(/data-status="([^"]+)"/)[1].trim() ?? true - if (childrenCell.style.includes('status')) { - childrenCell.setVisible( - dataStatus !== 'none' && this.state.topologyOption.hostStatusVisible - ) + if (cell.children) { + const childrenCell = cell.getChildAt(2) + const dataStatus = + cell.value.match(/data-status="([^"]+)"/)[1].trim() ?? true + //check + if (childrenCell?.style?.includes('status')) { + childrenCell.setVisible( + dataStatus !== 'none' && this.state.topologyOption.hostStatusVisible + ) + } } } } diff --git a/frontend/src/hosts/containers/InventoryTopology.tsx b/frontend/src/hosts/containers/InventoryTopology.tsx index 3f80aece..a5344406 100644 --- a/frontend/src/hosts/containers/InventoryTopology.tsx +++ b/frontend/src/hosts/containers/InventoryTopology.tsx @@ -2566,13 +2566,8 @@ export class InventoryTopology extends PureComponent { {fetchIntervalDataStatus === RemoteDataState.Loading && ( )} + {topologyStatus === RemoteDataState.Loading && }
- {topologyStatus === RemoteDataState.Loading && ( - - )} - {fetchIntervalDataStatus === RemoteDataState.Loading && ( - - )}
Date: Wed, 25 Sep 2024 14:07:25 +0900 Subject: [PATCH 14/28] Update CloudHub Version from 1.4.8 to 1.4.9 --- Makefile | 2 +- backend/server/swagger.json | 2 +- frontend/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 2161d9be..42147a25 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -VERSION = 1.4.8 +VERSION = 1.4.9 ifeq ($(OS), Windows_NT) GOBINDATA := $(shell go-bindata.exe --version 2>nil) else diff --git a/backend/server/swagger.json b/backend/server/swagger.json index 5d975f3b..59df069c 100644 --- a/backend/server/swagger.json +++ b/backend/server/swagger.json @@ -3,7 +3,7 @@ "info": { "title": "CloudHub", "description": "API endpoints for CloudHub", - "version": "1.4.8" + "version": "1.4.9" }, "schemes": ["http"], "basePath": "/cloudhub/v1", diff --git a/frontend/package.json b/frontend/package.json index 595901f5..39c6236d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "1.4.8", + "version": "1.4.9", "private": true, "description": "This is CloudHub's User interface made in React & Typescript", "author": "Jack Kim", From 2129ce2f9beada88c470e9f0d352d4865e001967 Mon Sep 17 00:00:00 2001 From: jinhyeong Date: Wed, 25 Sep 2024 06:22:36 +0000 Subject: [PATCH 15/28] At #528 Rollback PageSpinner position --- frontend/src/hosts/containers/InventoryTopology.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/hosts/containers/InventoryTopology.tsx b/frontend/src/hosts/containers/InventoryTopology.tsx index a5344406..f4fb041b 100644 --- a/frontend/src/hosts/containers/InventoryTopology.tsx +++ b/frontend/src/hosts/containers/InventoryTopology.tsx @@ -2566,8 +2566,11 @@ export class InventoryTopology extends PureComponent { {fetchIntervalDataStatus === RemoteDataState.Loading && ( )} - {topologyStatus === RemoteDataState.Loading && } +
+ {topologyStatus === RemoteDataState.Loading && ( + + )}
Date: Thu, 26 Sep 2024 00:43:52 +0000 Subject: [PATCH 16/28] At #528 Host Health Status Rendering Sync to State When Mount --- frontend/src/hosts/configurations/topology.ts | 1 - frontend/src/hosts/containers/InventoryTopology.tsx | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/hosts/configurations/topology.ts b/frontend/src/hosts/configurations/topology.ts index b7cb071c..13e66928 100644 --- a/frontend/src/hosts/configurations/topology.ts +++ b/frontend/src/hosts/configurations/topology.ts @@ -548,7 +548,6 @@ export const applyHandler = async function ( const childrenCell = cell.getChildAt(2) const dataStatus = cell.value.match(/data-status="([^"]+)"/)[1].trim() ?? true - //check if (childrenCell?.style?.includes('status')) { childrenCell.setVisible( dataStatus !== 'none' && this.state.topologyOption.hostStatusVisible diff --git a/frontend/src/hosts/containers/InventoryTopology.tsx b/frontend/src/hosts/containers/InventoryTopology.tsx index f4fb041b..f60bc244 100644 --- a/frontend/src/hosts/containers/InventoryTopology.tsx +++ b/frontend/src/hosts/containers/InventoryTopology.tsx @@ -640,6 +640,8 @@ export class InventoryTopology extends PureComponent { await this.handleLoadCsps() } await this.getInventoryTopology() + this.handleChangeTopologyOption() + this.setTopologySetting() await this.fetchIntervalData() From 3c4431acd8f51742548db4b9a5577b1c2a1d8241 Mon Sep 17 00:00:00 2001 From: jaegeunha Date: Thu, 26 Sep 2024 10:25:35 +0900 Subject: [PATCH 17/28] At #528 Update to return Topology Options with true values when Topology is not created --- backend/server/topologys.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/server/topologys.go b/backend/server/topologys.go index 3332bb4c..b66c32c7 100644 --- a/backend/server/topologys.go +++ b/backend/server/topologys.go @@ -70,6 +70,12 @@ func (s *Service) Topology(w http.ResponseWriter, r *http.Request) { ID: "", Organization: "", Links: selfLinks{Self: ""}, + TopologyOptions: cloudhub.TopologyOptions{ + MinimapVisible: true, + HostStatusVisible: true, + IPMIVisible: true, + LinkVisible: true, + }, } encodeJSON(w, http.StatusOK, res, s.Logger) return From a3a00e8af158e6c18862e841bb173b18bd060044 Mon Sep 17 00:00:00 2001 From: jaegeunha Date: Thu, 26 Sep 2024 16:05:19 +0900 Subject: [PATCH 18/28] At #528 Update to determine Topology changes by comparing hash values of previous and update XML --- .../src/hosts/containers/InventoryTopology.tsx | 14 ++++++++++++-- frontend/src/hosts/utils/index.ts | 4 ++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/frontend/src/hosts/containers/InventoryTopology.tsx b/frontend/src/hosts/containers/InventoryTopology.tsx index f60bc244..8891850e 100644 --- a/frontend/src/hosts/containers/InventoryTopology.tsx +++ b/frontend/src/hosts/containers/InventoryTopology.tsx @@ -172,6 +172,7 @@ import { getNamespaceID, isGCPRequiredCheck, getAgentDetails, + generateSHA256Hash, } from 'src/hosts/utils' // error @@ -2483,9 +2484,18 @@ export class InventoryTopology extends PureComponent { } private handleGraphModel = (sender: mxGraphModelType) => { - const topology = this.xmlExport(sender) + const previousTopology = this.state.topology + const currentTopology = this.xmlExport(sender) - this.setState({topology, isTopologyChanged: true}) + const previousTopologyHash = generateSHA256Hash(previousTopology) + const currentTopologyHash = generateSHA256Hash(currentTopology) + + if (previousTopologyHash !== currentTopologyHash) { + this.setState({ + topology: currentTopology, + isTopologyChanged: true, + }) + } } private handleClose = () => { diff --git a/frontend/src/hosts/utils/index.ts b/frontend/src/hosts/utils/index.ts index 6f7ed0b6..8b62b837 100644 --- a/frontend/src/hosts/utils/index.ts +++ b/frontend/src/hosts/utils/index.ts @@ -512,6 +512,10 @@ export const updateCSPInstanceData = ( return cspInstanceData } +export const generateSHA256Hash = (data: string) => { + return CryptoJS.SHA256(data).toString(CryptoJS.enc.Hex) +} + export const cryptoJSAESencrypt = (key: string, encryptKey: string) => { const encryptedBytes = CryptoJS.AES.encrypt(key, encryptKey) const encryptedKey = encryptedBytes.toString() From 34fdffc54316c7829fe72f924adcda99ce942140 Mon Sep 17 00:00:00 2001 From: jaegeunha Date: Thu, 26 Sep 2024 16:07:13 +0900 Subject: [PATCH 19/28] At #528 Update to display IPMI Icon and Health Status only when DataType is 'Server' --- frontend/src/hosts/configurations/topology.ts | 27 ++++++++++++------- frontend/src/hosts/constants/tools.ts | 24 ++++++++--------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/frontend/src/hosts/configurations/topology.ts b/frontend/src/hosts/configurations/topology.ts index 13e66928..931dcfca 100644 --- a/frontend/src/hosts/configurations/topology.ts +++ b/frontend/src/hosts/configurations/topology.ts @@ -488,7 +488,8 @@ export const applyHandler = async function ( (newValue !== 'none' ? true : false) && this.state.topologyOption.hostStatusVisible ) - shouldFetchData = newValue !== 'false' + + shouldFetchData = newValue !== 'none' } } @@ -654,6 +655,8 @@ export const dragCell = (node: Menu, self: any) => ( model.beginUpdate() const cell = createHTMLValue(node, 'node') + const dataType = cell.getAttribute('data-type') + try { v1 = graph.insertVertex( parent, @@ -695,8 +698,6 @@ export const dragCell = (node: Menu, self: any) => ( ipmiStatus.geometry.offset = new mxPoint(-12, -12) ipmiStatus.setConnectable(false) - ipmiStatus.setVisible(self.state.topologyOption.ipmiVisible) - graph.setCellStyles(mxConstants.STYLE_STROKECOLOR, 'white', [ipmiStatus]) const linkBox = document.createElement('div') linkBox.setAttribute('btn-type', 'href') @@ -738,7 +739,10 @@ export const dragCell = (node: Menu, self: any) => ( const statusBox = document.createElement('div') statusBox.classList.add('vertex') - statusBox.setAttribute('data-status', 'agent') + statusBox.setAttribute( + 'data-status', + dataType === 'Server' ? 'agent' : 'none' + ) statusBox.setAttribute('btn-type', 'status') statusBox.style.display = 'flex' statusBox.style.alignItems = 'center' @@ -785,18 +789,21 @@ export const dragCell = (node: Menu, self: any) => ( statusCell.geometry.offset = new mxPoint(-24, 6) statusCell.setConnectable(false) - const statusCheck = _.get(node, 'status') !== 'none' ? true : false - statusCell.setVisible( - statusCheck && self.state.topologyOption.hostStatusVisible - ) + + if (dataType === 'Server') { + ipmiStatus.setVisible(self.state.topologyOption.ipmiVisible) + graph.setCellStyles(mxConstants.STYLE_STROKECOLOR, 'white', [ipmiStatus]) + statusCell.setVisible(self.state.topologyOption.hostStatusVisible) + } else { + ipmiStatus.setVisible(false) + statusCell.setVisible(false) + } } finally { model.endUpdate() } graph.setSelectionCell(v1) - const dataType = cell.getAttribute('data-type') - if (dataType === 'Server') { fetchIntervalData(graph, v1, self) } diff --git a/frontend/src/hosts/constants/tools.ts b/frontend/src/hosts/constants/tools.ts index 9529d642..9be29947 100644 --- a/frontend/src/hosts/constants/tools.ts +++ b/frontend/src/hosts/constants/tools.ts @@ -134,7 +134,7 @@ export const toolsMenu: Menu[] = [ name: 'server', label: 'Server', link: '', - status: false, + status: 'agent', detected: false, icon: 'Server', }, @@ -143,7 +143,7 @@ export const toolsMenu: Menu[] = [ name: 'database', label: 'Database', link: '', - status: false, + status: 'none', detected: false, icon: 'Database', }, @@ -152,7 +152,7 @@ export const toolsMenu: Menu[] = [ name: 'internet', label: 'Internet', link: '', - status: false, + status: 'none', detected: false, icon: 'Internet', }, @@ -161,7 +161,7 @@ export const toolsMenu: Menu[] = [ name: 'workstation', label: 'Workstation', link: '', - status: false, + status: 'none', detected: false, icon: 'Workstation', }, @@ -170,7 +170,7 @@ export const toolsMenu: Menu[] = [ name: 'virtual-machine', label: 'VirtualMachine', link: '', - status: false, + status: 'none', detected: false, icon: 'VirtualMachine', }, @@ -179,7 +179,7 @@ export const toolsMenu: Menu[] = [ name: 'email', label: 'Email', link: '', - status: false, + status: 'none', detected: false, icon: 'Email', }, @@ -188,7 +188,7 @@ export const toolsMenu: Menu[] = [ name: 'firewall', label: 'Firewall', link: '', - status: false, + status: 'none', detected: false, icon: 'Firewall', }, @@ -197,7 +197,7 @@ export const toolsMenu: Menu[] = [ name: 'router', label: 'Router', link: '', - status: false, + status: 'none', detected: false, icon: 'Router', }, @@ -206,7 +206,7 @@ export const toolsMenu: Menu[] = [ name: 'wireless-router', label: 'WirelessRouter', link: '', - status: false, + status: 'none', detected: false, icon: 'WirelessRouter', }, @@ -215,7 +215,7 @@ export const toolsMenu: Menu[] = [ name: 'switch', label: 'Switch', link: '', - status: false, + status: 'none', detected: false, icon: 'Switch', }, @@ -224,7 +224,7 @@ export const toolsMenu: Menu[] = [ name: 'cloud', label: 'Cloud', link: '', - status: false, + status: 'none', detected: false, icon: 'Cloud', }, @@ -233,7 +233,7 @@ export const toolsMenu: Menu[] = [ name: 'elastic-load-balancing', label: 'ELB', link: '', - status: false, + status: 'none', detected: false, icon: 'ELB', }, From ef3cbf1da935203ac250bb2165d6b33f9c42be75 Mon Sep 17 00:00:00 2001 From: jaeheesnet Date: Thu, 26 Sep 2024 08:08:55 +0000 Subject: [PATCH 20/28] At #528 Current Graph XML Save to ETCD --- frontend/src/hosts/configurations/topology.ts | 11 +++-- .../hosts/containers/InventoryTopology.tsx | 47 +++++++++++++------ 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/frontend/src/hosts/configurations/topology.ts b/frontend/src/hosts/configurations/topology.ts index 931dcfca..06c11118 100644 --- a/frontend/src/hosts/configurations/topology.ts +++ b/frontend/src/hosts/configurations/topology.ts @@ -530,29 +530,33 @@ export const applyHandler = async function ( ) const childrenLink = childrenContainerElement.querySelector('a') childrenLink.setAttribute('href', dataLink[1]) - childrenCell.setValue(childrenContainerElement.outerHTML) childrenCell.setVisible(this.state.topologyOption.linkVisible) + childrenCell.setValue(childrenContainerElement.outerHTML) } } } - if (attribute === 'data-ipmi_host') { if (cell.children) { const childrenCell = cell.getChildAt(0) - childrenCell.setVisible(this.state.topologyOption.ipmiVisible) + const childrenContainerElement = getContainerElement(childrenCell.value) + if (childrenCell.value.includes('ipmi')) { + childrenCell.setVisible(this.state.topologyOption.ipmiVisible) + childrenCell.setValue(childrenContainerElement.outerHTML) } } } if (attribute === 'data-status') { if (cell.children) { const childrenCell = cell.getChildAt(2) + const childrenContainerElement = getContainerElement(childrenCell.value) const dataStatus = cell.value.match(/data-status="([^"]+)"/)[1].trim() ?? true if (childrenCell?.style?.includes('status')) { childrenCell.setVisible( dataStatus !== 'none' && this.state.topologyOption.hostStatusVisible ) + childrenCell.setValue(childrenContainerElement.outerHTML) } } } @@ -1524,7 +1528,6 @@ const renderHostState = ( return } const hostValue = findHost[findKey] - const statusValue = dataStatusValue( statusKind, hostValue, diff --git a/frontend/src/hosts/containers/InventoryTopology.tsx b/frontend/src/hosts/containers/InventoryTopology.tsx index 8891850e..f6b333d3 100644 --- a/frontend/src/hosts/containers/InventoryTopology.tsx +++ b/frontend/src/hosts/containers/InventoryTopology.tsx @@ -641,7 +641,6 @@ export class InventoryTopology extends PureComponent { await this.handleLoadCsps() } await this.getInventoryTopology() - this.handleChangeTopologyOption() this.setTopologySetting() @@ -730,11 +729,7 @@ export class InventoryTopology extends PureComponent { topologyOption.linkVisible ) ) { - this.fetchIntervalData() - } - - if (!_.isEqual(prevState.topologyOption, topologyOption)) { - this.handleChangeTopologyOption() + this.onChangeTopologyOption() } } @@ -839,7 +834,6 @@ export class InventoryTopology extends PureComponent { } = this.state const {notify} = this.props const isExportXML = modalTitle === 'Export XML' - //modal return (
{!mxClient.isBrowserSupported() ? ( @@ -918,7 +912,10 @@ export class InventoryTopology extends PureComponent { state={RemoteDataState.Done} topologyOption={topologyOption} setTopologyOption={(value: TopologyOption) => - this.setState({topologyOption: value}) + this.setState(prevState => ({ + ...prevState, + topologyOption: value, + })) } /> )} @@ -1273,8 +1270,6 @@ export class InventoryTopology extends PureComponent { if (!this.graph) return const graph = this.graph - graph.getModel().beginUpdate() - try { _.forEach(graph.getModel().cells, (cell: mxCellType) => { const containerElement = getContainerElement(cell.value) @@ -1293,8 +1288,6 @@ export class InventoryTopology extends PureComponent { }) } catch (error) { notify(notifyTopologyOptionChangeFailed(error.message)) - } finally { - graph.getModel().endUpdate() } } @@ -1310,7 +1303,6 @@ export class InventoryTopology extends PureComponent { graph.getModel().beginUpdate() try { - graph.refresh() graph.getView().setTranslate(currentTranslate.x, currentTranslate.y) graph.getView().setScale(currentScale) } catch (error) { @@ -1320,6 +1312,31 @@ export class InventoryTopology extends PureComponent { } } + private onChangeTopologyOption = () => { + const {notify} = this.props + if (!this.graph) return + this.setState(preState => ({ + ...preState, + fetchIntervalDataStatus: RemoteDataState.Loading, + })) + try { + this.graph.model.beginUpdate() + this.handleChangeTopologyOption() + + this.fetchIpmiStatus() + this.getDetectedHostStatus() + } catch (error) { + notify(notifyTopologyOptionChangeFailed(error.message)) + } finally { + this.graph.model.endUpdate() + this.graph.refresh() + this.setState(preState => ({ + ...preState, + fetchIntervalDataStatus: RemoteDataState.Done, + })) + } + } + private handleCloseInstanceTypeModal = () => { this.setState({isInstanceTypeModalVisible: false}) } @@ -2948,7 +2965,7 @@ export class InventoryTopology extends PureComponent { if (_.isEmpty(topologyId) && !_.isEmpty(topology)) { const response = await createInventoryTopology( links, - topology, + this.xmlExport(this.graph.getModel()), unsavedPreferenceTemperatureValues ) const getTopologyId = _.get(response, 'data.id', null) @@ -2964,7 +2981,7 @@ export class InventoryTopology extends PureComponent { await updateInventoryTopology( links, topologyId, - topology, + this.xmlExport(this.graph.getModel()), unsavedPreferenceTemperatureValues, topologyOption ) From 5b848d8c5eed57e7945bfbd7681ddbb7e02c5462 Mon Sep 17 00:00:00 2001 From: jaeheesnet Date: Thu, 26 Sep 2024 08:32:23 +0000 Subject: [PATCH 21/28] At #528 Save Topology use State --- frontend/src/hosts/containers/InventoryTopology.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/hosts/containers/InventoryTopology.tsx b/frontend/src/hosts/containers/InventoryTopology.tsx index f6b333d3..cb617d8c 100644 --- a/frontend/src/hosts/containers/InventoryTopology.tsx +++ b/frontend/src/hosts/containers/InventoryTopology.tsx @@ -1333,6 +1333,7 @@ export class InventoryTopology extends PureComponent { this.setState(preState => ({ ...preState, fetchIntervalDataStatus: RemoteDataState.Done, + topology: this.xmlExport(this.graph.getModel()), })) } } @@ -2965,7 +2966,7 @@ export class InventoryTopology extends PureComponent { if (_.isEmpty(topologyId) && !_.isEmpty(topology)) { const response = await createInventoryTopology( links, - this.xmlExport(this.graph.getModel()), + topology, unsavedPreferenceTemperatureValues ) const getTopologyId = _.get(response, 'data.id', null) @@ -2981,7 +2982,7 @@ export class InventoryTopology extends PureComponent { await updateInventoryTopology( links, topologyId, - this.xmlExport(this.graph.getModel()), + topology, unsavedPreferenceTemperatureValues, topologyOption ) From dfa6faec8c3770e6138c59bfb57da8286d526c4d Mon Sep 17 00:00:00 2001 From: jaegeunha Date: Fri, 27 Sep 2024 09:51:19 +0900 Subject: [PATCH 22/28] At #528 Set isTopologyChanged state to true when topology options are updated --- frontend/src/hosts/containers/InventoryTopology.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/hosts/containers/InventoryTopology.tsx b/frontend/src/hosts/containers/InventoryTopology.tsx index cb617d8c..3ea4e353 100644 --- a/frontend/src/hosts/containers/InventoryTopology.tsx +++ b/frontend/src/hosts/containers/InventoryTopology.tsx @@ -1334,6 +1334,7 @@ export class InventoryTopology extends PureComponent { ...preState, fetchIntervalDataStatus: RemoteDataState.Done, topology: this.xmlExport(this.graph.getModel()), + isTopologyChanged: true, })) } } From dfc2eb144104fe2d830e51c58dfa79af0fe58698 Mon Sep 17 00:00:00 2001 From: jaegeunha Date: Fri, 27 Sep 2024 10:44:16 +0900 Subject: [PATCH 23/28] At #528 Show confirmation modal when originalTopology differs from topology state. --- .../hosts/containers/InventoryTopology.tsx | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/frontend/src/hosts/containers/InventoryTopology.tsx b/frontend/src/hosts/containers/InventoryTopology.tsx index 3ea4e353..fec17a0d 100644 --- a/frontend/src/hosts/containers/InventoryTopology.tsx +++ b/frontend/src/hosts/containers/InventoryTopology.tsx @@ -410,7 +410,7 @@ interface State { isPreferencesOverlayVisible: boolean isOptionOverlayVisible: boolean preferenceTemperatureValues: string[] - unsavedTopology: string + originalTopology: string preferencesStatus: RemoteDataState unsavedPreferenceTemperatureValues: string[] fetchIntervalDataStatus: RemoteDataState @@ -538,7 +538,7 @@ export class InventoryTopology extends PureComponent { isOptionOverlayVisible: false, preferenceTemperatureValues: [], unsavedPreferenceTemperatureValues: [], - unsavedTopology: '', + originalTopology: '', preferencesStatus: RemoteDataState.Done, fetchIntervalDataStatus: RemoteDataState.NotStarted, isTooltipActiveHost: null, @@ -793,7 +793,10 @@ export class InventoryTopology extends PureComponent { public componentWillUnmount() { const {isTopologyChanged} = this.state const view = this.graph.getView() - if (isTopologyChanged && window.confirm('Do you want to save changes?')) { + if ( + (isTopologyChanged || this.compareTopology()) && + window.confirm('Do you want to save changes?') + ) { this.handleTopologySave() } @@ -810,6 +813,16 @@ export class InventoryTopology extends PureComponent { this.isComponentMounted = false } + private compareTopology = () => { + const previousTopology = this.state.originalTopology + const currentTopology = this.state.topology + + const previousTopologyHash = generateSHA256Hash(previousTopology) + const currentTopologyHash = generateSHA256Hash(currentTopology) + + return previousTopologyHash !== currentTopologyHash + } + public render() { const { provider, @@ -1484,7 +1497,7 @@ export class InventoryTopology extends PureComponent { 'preferences', defaultPreferencesTemperature ), - unsavedTopology: _.get(topology, 'diagram'), + originalTopology: _.get(topology, 'diagram'), topology: _.get(topology, 'diagram'), topologyId: _.get(topology, 'id'), topologyStatus: RemoteDataState.Done, @@ -2975,7 +2988,7 @@ export class InventoryTopology extends PureComponent { notify(notifyTopologySaved()) this.setState({ - unsavedTopology: topology, + originalTopology: topology, topologyId: getTopologyId, topologyStatus: RemoteDataState.Done, }) @@ -2992,7 +3005,7 @@ export class InventoryTopology extends PureComponent { this.fetchIntervalData() this.setState({ - unsavedTopology: topology, + originalTopology: topology, topologyStatus: RemoteDataState.Done, isTopologyChanged: false, }) From 6273402909f6f62af0c0756bbc595ce3a5fa1392 Mon Sep 17 00:00:00 2001 From: jaeheesnet Date: Fri, 27 Sep 2024 02:26:37 +0000 Subject: [PATCH 24/28] At #528 Data Type Confirm before First Rendering --- frontend/src/hosts/configurations/topology.ts | 43 +++++++++++-------- .../hosts/containers/InventoryTopology.tsx | 11 +++-- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/frontend/src/hosts/configurations/topology.ts b/frontend/src/hosts/configurations/topology.ts index 06c11118..ddbcee7e 100644 --- a/frontend/src/hosts/configurations/topology.ts +++ b/frontend/src/hosts/configurations/topology.ts @@ -24,7 +24,6 @@ import { dataStatusValue, getContainerElement, getContainerTitle, - getIsHasString, getNotAvailableTitle, getParseHTML, getSelectedHostKey, @@ -416,6 +415,10 @@ export const createIPMIStatusIcon = function ( return } + if (!cell.value.includes('data-type="Server"')) { + return + } + const childrenCell = cell.getChildAt(0) if (!childrenCell || !childrenCell.style) { console.error('No valid children cell or missing style.') @@ -519,22 +522,15 @@ export const applyHandler = async function ( this.graphUpdateSave(cell) this.setState({fetchIntervalDataStatus: RemoteDataState.Done}) } - } else { - if (attribute === 'data-link') { - if (cell.children) { - const childrenCell = cell.getChildAt(1) - const dataLink = cell.value.match(/data-link="([^"]+)"/) - if (childrenCell?.style?.includes('href') && !!dataLink) { - const childrenContainerElement = getContainerElement( - childrenCell.value - ) - const childrenLink = childrenContainerElement.querySelector('a') - childrenLink.setAttribute('href', dataLink[1]) - childrenCell.setVisible(this.state.topologyOption.linkVisible) - childrenCell.setValue(childrenContainerElement.outerHTML) - } - } - } + } +} + +export const applyMultiHandler = async function ( + cell: mxCellType, + attribute: any, + dataType: string +) { + if (dataType === 'Server') { if (attribute === 'data-ipmi_host') { if (cell.children) { const childrenCell = cell.getChildAt(0) @@ -561,6 +557,19 @@ export const applyHandler = async function ( } } } + if (attribute === 'data-link') { + if (cell.children) { + const childrenCell = cell.getChildAt(1) + const dataLink = cell.value.match(/data-link="([^"]+)"/) + if (childrenCell?.style?.includes('href') && !!dataLink) { + const childrenContainerElement = getContainerElement(childrenCell.value) + const childrenLink = childrenContainerElement.querySelector('a') + childrenLink.setAttribute('href', dataLink[1]) + childrenCell.setVisible(this.state.topologyOption.linkVisible) + childrenCell.setValue(childrenContainerElement.outerHTML) + } + } + } } export const createHTMLValue = function (node: Menu, style: string) { diff --git a/frontend/src/hosts/containers/InventoryTopology.tsx b/frontend/src/hosts/containers/InventoryTopology.tsx index fec17a0d..44d366a7 100644 --- a/frontend/src/hosts/containers/InventoryTopology.tsx +++ b/frontend/src/hosts/containers/InventoryTopology.tsx @@ -214,6 +214,7 @@ import { refreshGraph, isCellMovable, createIPMIStatusIcon, + applyMultiHandler, } from 'src/hosts/configurations/topology' import {WindowResizeEventTrigger} from 'src/shared/utils/trigger' @@ -1282,20 +1283,20 @@ export class InventoryTopology extends PureComponent { } if (!this.graph) return const graph = this.graph + const cells = graph.getModel().cells try { - _.forEach(graph.getModel().cells, (cell: mxCellType) => { + _.forEach(cells, (cell: mxCellType) => { const containerElement = getContainerElement(cell.value) - if (containerElement && containerElement.hasAttribute('data-type')) { const dataType = containerElement.getAttribute('data-type') + const attrsKeys = _.map( _.keys(eachNodeTypeAttrs[dataType].attrs), attr => `data-${attr}` ) - _.forEach(attrsKeys, attr => { - applyHandler.bind(this)(graph, cell, attr) + applyMultiHandler.bind(this)(cell, attr, dataType) }) } }) @@ -1314,6 +1315,8 @@ export class InventoryTopology extends PureComponent { const currentTranslate = graph.getView().getTranslate() const currentScale = graph.getView().getScale() + this.setState(prevState => ({...prevState, isTopologyChanged: true})) + graph.getModel().beginUpdate() try { graph.getView().setTranslate(currentTranslate.x, currentTranslate.y) From 1863bfd41738e9e8fc785e6aad7c153ce0c3bcd2 Mon Sep 17 00:00:00 2001 From: jaeheesnet Date: Fri, 27 Sep 2024 04:12:37 +0000 Subject: [PATCH 25/28] At #528 Apply Topology Option during Import --- frontend/src/hosts/configurations/topology.ts | 2 +- frontend/src/hosts/containers/InventoryTopology.tsx | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/hosts/configurations/topology.ts b/frontend/src/hosts/configurations/topology.ts index ddbcee7e..4ff1e1cd 100644 --- a/frontend/src/hosts/configurations/topology.ts +++ b/frontend/src/hosts/configurations/topology.ts @@ -431,7 +431,7 @@ export const createIPMIStatusIcon = function ( return } - if (sepCellStyle[0] === 'ipmi') { + if (sepCellStyle[0] === 'ipmi' && this.state.topologyOption.ipmiVisible) { graph.setCellStyles(mxConstants.STYLE_STROKECOLOR, 'white', [childrenCell]) childrenCell.setVisible(true) } diff --git a/frontend/src/hosts/containers/InventoryTopology.tsx b/frontend/src/hosts/containers/InventoryTopology.tsx index 44d366a7..4106800b 100644 --- a/frontend/src/hosts/containers/InventoryTopology.tsx +++ b/frontend/src/hosts/containers/InventoryTopology.tsx @@ -990,7 +990,7 @@ export class InventoryTopology extends PureComponent { cell.setValue(containerElement.outerHTML) }) - createIPMIStatusIcon(graph, cell) + createIPMIStatusIcon.bind(this)(graph, cell) } }) } finally { @@ -1473,7 +1473,7 @@ export class InventoryTopology extends PureComponent { containerElement.removeAttribute(attr.nodeName) cell.setValue(containerElement.outerHTML) }) - createIPMIStatusIcon(graph, cell) + createIPMIStatusIcon.bind(this)(graph, cell) } }) } finally { @@ -2399,6 +2399,7 @@ export class InventoryTopology extends PureComponent { this.setState({ isImportTopologyOverlayVisible: true, }) + this.onChangeTopologyOption() }) this.editor.addAction('export', () => { From 6063c423b53285d89c50a0c300edeee45f0ee8d1 Mon Sep 17 00:00:00 2001 From: jaeheesnet Date: Fri, 27 Sep 2024 04:55:19 +0000 Subject: [PATCH 26/28] At #528 Apply bug fix --- frontend/src/hosts/containers/InventoryTopology.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/hosts/containers/InventoryTopology.tsx b/frontend/src/hosts/containers/InventoryTopology.tsx index 4106800b..e6b4d856 100644 --- a/frontend/src/hosts/containers/InventoryTopology.tsx +++ b/frontend/src/hosts/containers/InventoryTopology.tsx @@ -943,6 +943,8 @@ export class InventoryTopology extends PureComponent { importedTopology: string ) => { await this.importTopology(importedTopology) + this.onChangeTopologyOption() + this.fetchIntervalData() } @@ -993,6 +995,8 @@ export class InventoryTopology extends PureComponent { createIPMIStatusIcon.bind(this)(graph, cell) } }) + + this.onChangeTopologyOption() } finally { graph.getModel().endUpdate() } @@ -2399,7 +2403,6 @@ export class InventoryTopology extends PureComponent { this.setState({ isImportTopologyOverlayVisible: true, }) - this.onChangeTopologyOption() }) this.editor.addAction('export', () => { From d3c341bad7124d941c7dd74f84275c38e8562e11 Mon Sep 17 00:00:00 2001 From: jinhyeong Date: Fri, 27 Sep 2024 06:55:04 +0000 Subject: [PATCH 27/28] At #528 Fix Topology Tooltip legacy status bug --- frontend/src/hosts/containers/InventoryTopology.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/hosts/containers/InventoryTopology.tsx b/frontend/src/hosts/containers/InventoryTopology.tsx index e6b4d856..82deda46 100644 --- a/frontend/src/hosts/containers/InventoryTopology.tsx +++ b/frontend/src/hosts/containers/InventoryTopology.tsx @@ -3928,7 +3928,11 @@ export class InventoryTopology extends PureComponent { const hostname = container.getAttribute('data-name') const dataGatherType = container.getAttribute('data-status') - if (isTooltipActiveHost === cellId || dataGatherType === 'none') { + if ( + isTooltipActiveHost === cellId || + dataGatherType === 'none' || + dataGatherType === 'false' + ) { return } From 7293f379c75b54a47c9cb3a21b59ae214813c024 Mon Sep 17 00:00:00 2001 From: jaeheesnet Date: Fri, 27 Sep 2024 08:02:46 +0000 Subject: [PATCH 28/28] At #528 Topology Cell Status value 'NONE' to 'FALSE' --- frontend/src/hosts/configurations/topology.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/frontend/src/hosts/configurations/topology.ts b/frontend/src/hosts/configurations/topology.ts index 4ff1e1cd..5465bae5 100644 --- a/frontend/src/hosts/configurations/topology.ts +++ b/frontend/src/hosts/configurations/topology.ts @@ -356,8 +356,8 @@ export const createTextField = function ( form.addOption( input, 'NONE', - 'none', - attribute.nodeValue === 'none' ? true : false + false, + attribute.nodeValue === 'false' ? true : false ) } else if (attribute.nodeName === 'data-icon') { input = form.addCombo(nodeName, false) @@ -488,11 +488,11 @@ export const applyHandler = async function ( if (childrenCell.style.includes('status')) { childrenCell.setVisible( - (newValue !== 'none' ? true : false) && + (newValue !== 'false' ? true : false) && this.state.topologyOption.hostStatusVisible ) - shouldFetchData = newValue !== 'none' + shouldFetchData = newValue !== 'false' } } @@ -550,7 +550,8 @@ export const applyMultiHandler = async function ( cell.value.match(/data-status="([^"]+)"/)[1].trim() ?? true if (childrenCell?.style?.includes('status')) { childrenCell.setVisible( - dataStatus !== 'none' && this.state.topologyOption.hostStatusVisible + dataStatus !== 'false' && + this.state.topologyOption.hostStatusVisible ) childrenCell.setValue(childrenContainerElement.outerHTML) } @@ -754,7 +755,7 @@ export const dragCell = (node: Menu, self: any) => ( statusBox.classList.add('vertex') statusBox.setAttribute( 'data-status', - dataType === 'Server' ? 'agent' : 'none' + dataType === 'Server' ? 'agent' : 'false' ) statusBox.setAttribute('btn-type', 'status') statusBox.style.display = 'flex'