Skip to content

Commit

Permalink
Introduce Storage Connections management dialog
Browse files Browse the repository at this point in the history
Storage Connections management dialog is opened by clicking the
Connections button in the Storage Domains table when iSCSI Storage
Domain is selected.

The dialog displays storage connections, attached to the domain, and
also has a switch to show all the iSCSI connections. It allows 5
actions to be performed on the connections:
1. Add new connection
2. Edit connection
3. Remove connection
4. Attach connection to the domain
5. Detach connection from the domain
Which provides UI for the existing storage connections API
Live changes are not supported, thus the domain should be in
maintenance mode to perform actions (4),(5), and also (2),(3) for
attached connections.
  • Loading branch information
mkemel committed Mar 28, 2022
1 parent 8d1aa0c commit 3eee1bc
Show file tree
Hide file tree
Showing 10 changed files with 917 additions and 4 deletions.
1 change: 1 addition & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const entityTypes = {
vm: 'VirtualMachine',
hostDevices: 'HostDevice',
vmDevices: 'VirtualMachineDevice',
storage: 'Storage',
}

export const heatMapThresholds = {
Expand Down
23 changes: 23 additions & 0 deletions src/integrations/buttons.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { showVmMigrateModal } from './showVmMigrate'
import { showClusterUpgradeWizard } from './showClusterUpgrade'
import { showVmExportModal } from './showVmExport'
import { showHostCopyNetworksModal } from './showHostCopyNetworks'
import { showStorageConnectionsModal } from './showStorageConnectionsModal'

function isVmUp (vm) {
return vmUpStates.includes(vm.status)
Expand Down Expand Up @@ -160,6 +161,27 @@ function addHostCpuPinningButton () {
})
}

/**
* "Connections" button in the Storage Domains list. Enabled when exactly 1 iSCSI domain is selected
*/
function addManageStorageConnectionsButton () {
getPluginApi().addMenuPlaceActionButton(entityTypes.storage, msg.storageConnectionsManageButton(), {
onClick: function ([selectedDomain]) {
showStorageConnectionsModal(selectedDomain)
},

isEnabled: function (selectedDomains) {
return (
selectedDomains.length === 1 &&
selectedDomains[0]?.type?.toLowerCase() === 'iscsi'
)
},

index: 4,
id: 'StorageConnectionsButton',
})
}

export function addButtons () {
addVmManageGpuButton()
addVmCpuPinningButton()
Expand All @@ -169,4 +191,5 @@ export function addButtons () {
addClusterUpgradeButton()
addHostCopyNetworksButton()
addHostCpuPinningButton()
addManageStorageConnectionsButton()
}
17 changes: 17 additions & 0 deletions src/integrations/showStorageConnectionsModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react'
import { renderComponent } from '_/utils/react-modals'

import StorageConnectionsModal from '_/modals/storage-connections/StorageConnectionsModal'
import StorageConnectionsDataProvider from '_/modals/storage-connections/StorageConnectionsDataProvider'

export function showStorageConnectionsModal (storageDomain) {
renderComponent(
({ unmountComponent }) => (
<StorageConnectionsDataProvider storageDomain={storageDomain}>
<StorageConnectionsModal
onClose={unmountComponent}
/>
</StorageConnectionsDataProvider>
)
)
}
134 changes: 134 additions & 0 deletions src/intl/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,140 @@ const messageDescriptors = {
defaultMessage: 'Core {id, number, ::.}',
description: 'core',
},

// Storage Connections modal dialog related strings

storageConnectionsManageButton: {
id: 'storage.domains.connections.buttonLabel',
defaultMessage: 'Connections',
description: 'label for Storage Connections management dialog button',
},

storageConnectionsDataError: {
id: 'storage.domains.connections.dataError',
defaultMessage: 'Could not fetch data needed for showing Storage Connections',
description: 'notification shown when Storage Connections dialog failed to load its data',
},

storageConnectionsTitle: {
id: 'storage.domains.connections.title',
defaultMessage: 'Manage Storage Connections',
description: 'title of Storage Connections dialog',
},

storageConnectionsTitleWithName: {
id: 'storage.domains.connections.title.withName',
defaultMessage: 'Manage Storage Connections - {sdName}',
description: 'title of Storage Connections dialog with name',
},

storageConnectionsTableColAddress: {
id: 'storage.domains.connections.table.column.address',
defaultMessage: 'Address',
description: 'address column of Storage Connections Table',
},

storageConnectionsTableColPort: {
id: 'storage.domains.connections.table.column.port',
defaultMessage: 'Port',
description: 'port column of Storage Connections Table',
},

storageConnectionsTableColTarget: {
id: 'storage.domains.connections.table.column.target',
defaultMessage: 'Target',
description: 'target column of Storage Connections Table',
},

storageConnectionsTableColPath: {
id: 'storage.domains.connections.table.column.path',
defaultMessage: 'Path',
description: 'path column of Storage Connections Table',
},

storageConnectionsTableColAttached: {
id: 'storage.domains.connections.table.column.attached',
defaultMessage: 'Attached',
description: 'attached column of Storage Connections Table',
},

storageConnectionsTableAttachedStr: {
id: 'storage.domains.connections.table.connection.attached',
defaultMessage: 'ATTACHED',
description: 'attached string in Storage Connections Table',
},

storageConnectionsRemoveConnectionButton: {
id: 'storage.domains.connections.connection.remove.button',
defaultMessage: 'Remove',
description: 'remove Connection button',
},

storageConnectionsAddConnectionButton: {
id: 'storage.domains.connections.connection.add.button',
defaultMessage: 'Add',
description: 'add Connection button',
},

storageConnectionsAttachConnectionButton: {
id: 'storage.domains.connections.connection.attach.button',
defaultMessage: 'Attach',
description: 'attach Connection button',
},

storageConnectionsDetachConnectionButton: {
id: 'storage.domains.connections.connection.detach.button',
defaultMessage: 'Detach',
description: 'detach Connection button',
},

storageConnectionsDomainNotInMaintenanceWarning: {
id: 'storage.domains.connections.domain.maintenance.warning',
defaultMessage: 'Storage Domain is not in Maintenance mode',
description: 'storage Domain is not in Maintenance mode Warning',
},

storageConnectionsDomainNotInMaintenanceWarningDetail: {
id: 'storage.domains.connections.domain.maintenance.warning.detail',
defaultMessage: 'Connections cannot be attached or detached, cannot edit attached connections',
description: 'storage Domain is not in Maintenance mode Warning Detail',
},

storageConnectionsShowAllConnectionsSwitchOn: {
id: 'storage.domains.connections.showAll.switch.on',
defaultMessage: 'On',
description: 'show all connections switch on',
},

storageConnectionsShowAllConnectionsSwitchOff: {
id: 'storage.domains.connections.showAll.switch.off',
defaultMessage: 'Off',
description: 'show all connections switch off',
},

storageConnectionsShowAllConnectionsLabel: {
id: 'storage.domains.connections.showAll.label',
defaultMessage: 'Show all connections',
description: 'show all connections label',
},

storageConnectionsOperationFailedTitle: {
id: 'storage.domains.connections.operation.failed.title',
defaultMessage: 'Operation Failed',
description: 'storage Connection operation failed title',
},

storageConnectionsFieldRequiredError: {
id: 'storage.domains.connections.field.required.error',
defaultMessage: 'This field is required',
description: 'field required error message',
},

storageConnectionsFieldPortError: {
id: 'storage.domains.connections.field.port.error',
defaultMessage: 'Invalid port value',
description: 'invalid port field value error',
},
}

module.exports = exports = messageDescriptors
118 changes: 118 additions & 0 deletions src/modals/storage-connections/StorageConnectionsDataProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import React from 'react'
import PropTypes from 'prop-types'
import getPluginApi from '_/plugin-api'
import { engineGet, enginePost, enginePut, engineDelete } from '_/utils/fetch'
import DataProvider from '_/components/helper/DataProvider'
import { webadminToastTypes } from '_/constants'
import { msg } from '_/intl-messages'

const createConnection = async (connection) => {
return await enginePost(
'api/storageconnections',
JSON.stringify(connection)
)
}

const editConnection = async (connection, connectionId) => {
return await enginePut(
`api/storageconnections/${connectionId}`,
JSON.stringify(connection)
)
}

const deleteConnection = async (connectionId) => {
return await engineDelete(`api/storageconnections/${connectionId}`)
}

const attachConnection = async (connectionId, domainId) => {
return await enginePost(
`api/storagedomains/${domainId}/storageconnections`,
JSON.stringify({ id: connectionId })
)
}

const detachConnection = async (connectionId, domainId) => {
return await engineDelete(`api/storagedomains/${domainId}/storageconnections/${connectionId}`)
}

const fetchData = async (stgDomain) => {
if (!stgDomain?.id || !stgDomain?.dataCenterId) {
throw new Error('StorageConnectionsDataProvider: invalid Storage Domain')
}
const [allConnectionsJson, storageDomain, domainConnectionsJson] = await Promise.all([
engineGet('api/storageconnections'),
engineGet(`api/datacenters/${stgDomain.dataCenterId}/storagedomains/${stgDomain.id}`),
engineGet(`api/storagedomains/${stgDomain.id}/storageconnections`),
])

if (!storageDomain) {
throw new Error('StorageConnectionsDataProvider: failed to fetch storage domain')
}

const allConnections = allConnectionsJson?.storage_connection
const domainConnections = domainConnectionsJson?.storage_connection

if (!allConnections || allConnections.error) {
throw new Error('StorageConnectionsDataProvider: failed to fetch storage connections')
}

if (!domainConnections || domainConnections.error) {
throw new Error('StorageConnectionsDataProvider: failed to fetch storage connections' +
'for storage domain ' + stgDomain.id)
}

const domainConnectionsIds = new Set(domainConnections.map(connection => connection.id))
const allConnectionsByTypeSorted = allConnections
.filter(connection => connection.type === storageDomain.storage?.type)
.map((conn) => {
return { ...conn, isAttachedToDomain: domainConnectionsIds.has(conn.id) }
})
.sort((conn1, conn2) => {
return (conn1.isAttachedToDomain === conn2.isAttachedToDomain) ? 0 : conn1.isAttachedToDomain ? -1 : 1
})

return {
storageDomain: storageDomain,
connections: allConnectionsByTypeSorted,
}
}

const StorageConnectionsDataProvider = ({ children, storageDomain }) => (
<DataProvider fetchData={() => fetchData(storageDomain)}>
{({ data, fetchError, lastUpdated, fetchAndUpdateData }) => {
// expecting single child component
const child = React.Children.only(children)

// handle data loading and error scenarios
if (fetchError) {
getPluginApi().showToast(webadminToastTypes.danger, msg.storageConnectionsDataError())
return null
}

if (!data) {
return React.cloneElement(child, { isLoading: true })
}

const { storageDomain, connections } = data

return React.cloneElement(child, {
storageDomain,
connections,
lastUpdated,
doConnectionCreate: createConnection,
doConnectionEdit: editConnection,
doConnectionDelete: deleteConnection,
doConnectionAttach: attachConnection,
doConnectionDetach: detachConnection,
doRefreshConnections: () => { fetchAndUpdateData() },
})
}}
</DataProvider>
)

StorageConnectionsDataProvider.propTypes = {
children: PropTypes.element.isRequired,
storageDomain: PropTypes.object.isRequired,
}

export default StorageConnectionsDataProvider
Loading

0 comments on commit 3eee1bc

Please sign in to comment.