Skip to content

Commit

Permalink
Merge pull request #7356 from TheThingsNetwork/fix/unclaim-gateway
Browse files Browse the repository at this point in the history
Unclaim gateway on delete
  • Loading branch information
PavelJankoski authored Oct 25, 2024
2 parents 1d21821 + cd1e2bc commit ea801ce
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 27 deletions.
103 changes: 103 additions & 0 deletions cypress/e2e/console/gateways/managed/delete.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright © 2024 The Things Network Foundation, The Things Industries B.V.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

describe('Delete Managed Gateway', () => {
const userId = 'managed-gateway-test-user'
const user = {
ids: { user_id: userId },
primary_email_address: '[email protected]',
password: 'ABCDefg123!',
password_confirm: 'ABCDefg123!',
}

const gatewayId = 'test-managed-gateway'
const gateway = { ids: { gateway_id: gatewayId } }

const gatewayVersionIds = {
hardware_version: 'v1.1',
firmware_version: 'v1.1',
model_id: 'Managed gateway',
}

beforeEach(() => {
cy.dropAndSeedDatabase()
cy.createUser(user)
cy.createGateway(gateway, userId)

cy.intercept('GET', `/api/v3/gcs/gateways/managed/${gatewayId}*`, {
statusCode: 200,
body: {
ids: {
gateway_id: `eui-${gateway.eui}`,
eui: gateway.eui,
},
version_ids: gatewayVersionIds,
},
}).as('get-is-gtw-managed')

cy.intercept('POST', `/api/v3/gcls/claim/info`, {
statusCode: 200,
body: {
supports_claiming: true,
},
}).as('get-is-gtw-claimable')

cy.intercept('DELETE', `/api/v3/gcls/claim/${gatewayId}`, {
statusCode: 200,
}).as('unclaim-gtw')

cy.loginConsole({ user_id: user.ids.user_id, password: user.password })
cy.visit(`${Cypress.config('consoleRootPath')}/gateways/${gatewayId}`)
cy.wait('@get-is-gtw-managed')
cy.findByRole('heading', { name: 'test-managed-gateway' })
cy.get('button').contains('Managed gateway').should('be.visible')
})

it('succeeds to trigger unclaiming when deleting the gateway from the overview header', () => {
cy.findByTestId('gateway-overview-menu').should('be.visible').click()
cy.findByText('Unclaim and delete gateway').should('be.visible').click()
cy.findByTestId('modal-window')
.should('be.visible')
.within(() => {
cy.findByText('Confirm deletion', { selector: 'h1' }).should('be.visible')
cy.findByRole('button', { name: 'Unclaim and delete gateway' }).click()
})
cy.wait('@unclaim-gtw')
cy.findByTestId('error-notification').should('not.exist')
cy.findByTestId('toast-notification-success')
.should('be.visible')
.and('contain', 'Gateway deleted')
})

it('succeeds to trigger unclaiming when deleting the gateway from the general settings', () => {
cy.get('a').contains('General settings').click()
cy.location('pathname').should(
'eq',
`${Cypress.config('consoleRootPath')}/gateways/${gatewayId}/general-settings`,
)
cy.findByText('Basic settings').should('be.visible')
cy.findByRole('button', { name: 'Unclaim and delete gateway' }).click()
cy.findByTestId('modal-window')
.should('be.visible')
.within(() => {
cy.findByText('Confirm deletion', { selector: 'h1' }).should('be.visible')
cy.findByRole('button', { name: 'Unclaim and delete gateway' }).click()
})
cy.wait('@unclaim-gtw')
cy.findByTestId('error-notification').should('not.exist')
cy.findByTestId('toast-notification-success')
.should('be.visible')
.and('contain', 'Gateway deleted')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ const ApplicationOverviewHeader = () => {
const webhooksCount = useSelector(selectWebhooksTotalCount)
const pubsubsCount = useSelector(selectPubsubsTotalCount)
const hasIntegrations = webhooksCount > 0 || pubsubsCount > 0
const additionalCondition = hasIntegrations
const mayViewDevices = useSelector(state => checkFromState(mayViewApplicationDevices, state))
const devicesTotalCount = useSelector(state =>
selectApplicationDeviceCount(state, application_id),
Expand Down Expand Up @@ -249,7 +248,7 @@ const ApplicationOverviewHeader = () => {
entityName={name}
setVisible={setDeleteApplicationModalVisible}
visible={deleteApplicationModalVisible}
additionalConditions={additionalCondition}
isPristine={hasIntegrations}
/>
</RequireRequest>
</div>
Expand Down
51 changes: 39 additions & 12 deletions pkg/webui/console/containers/delete-entity-header-modal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import {
import { getApiKeysList } from '@console/store/actions/api-keys'
import { getIsConfiguration } from '@console/store/actions/identity-server'
import { deleteApplication } from '@console/store/actions/applications'
import { deleteGateway } from '@console/store/actions/gateways'
import { deleteGateway, unclaimGateway } from '@console/store/actions/gateways'

import { selectApiKeysTotalCount } from '@console/store/selectors/api-keys'

Expand Down Expand Up @@ -79,11 +79,6 @@ const path = {
[GATEWAY]: '/gateways',
}

const deleteMessageMap = {
[APPLICATION]: sharedMessages.deleteApp,
[GATEWAY]: sharedMessages.deleteGateway,
}

const deletedMessageMap = {
[APPLICATION]: sharedMessages.deleteSuccess,
[GATEWAY]: sharedMessages.gatewayDeleted,
Expand All @@ -95,15 +90,32 @@ const deletedErrorMessageMap = {
}

const DeleteEntityHeaderModal = props => {
const { entity, entityId, entityName, visible, setVisible, setError, additionalConditions } =
props
const {
entity,
entityId,
entityName,
visible,
setVisible,
setError,
isPristine: isPristineProp,
supportsClaiming,
} = props

const lowerCaseEntity = entity.toLowerCase()
const isGateway = entity === GATEWAY
const [confirmId, setConfirmId] = React.useState('')
const [purgeEntity, setPurgeEntity] = React.useState(false)
const dispatch = useDispatch()
const navigate = useNavigate()

const deleteMessageMap = {
[APPLICATION]: sharedMessages.deleteApp,
[GATEWAY]:
supportsClaiming && isGateway
? sharedMessages.unclaimAndDeleteGateway
: sharedMessages.deleteGateway,
}

const mayPurgeEntity = useSelector(state => checkFromState(mayPurgeEntities, state))
const mayDeleteEntity = useSelector(state =>
checkFromState(mayDeleteEntitySelectorMap[entity], state),
Expand All @@ -114,7 +126,7 @@ const DeleteEntityHeaderModal = props => {
)
const hasApiKeys = apiKeysCount > 0
const hasAddedCollaborators = collaboratorsCount > 1
const isPristine = !hasAddedCollaborators && !hasApiKeys && !additionalConditions
const isPristine = !hasAddedCollaborators && !hasApiKeys && !isPristineProp
const mayViewCollaborators = useSelector(state =>
checkFromState(mayViewOrEditEntityCollaboratorsMap[entity], state),
)
Expand All @@ -138,6 +150,9 @@ const DeleteEntityHeaderModal = props => {
if (setError) {
setError(undefined)
}
if (supportsClaiming && isGateway) {
await dispatch(attachPromise(unclaimGateway(entityId)))
}
await dispatch(
attachPromise(
deleteEntityActionMap[entity](entityId, { purge: purgeEntity || false }),
Expand All @@ -163,7 +178,17 @@ const DeleteEntityHeaderModal = props => {
}
setVisible(false)
},
[dispatch, entityId, navigate, purgeEntity, setError, setVisible, entity],
[
setVisible,
setError,
supportsClaiming,
isGateway,
dispatch,
entity,
entityId,
purgeEntity,
navigate,
],
)

const loadData = useCallback(
Expand Down Expand Up @@ -266,19 +291,21 @@ const DeleteEntityHeaderModal = props => {
}

DeleteEntityHeaderModal.propTypes = {
additionalConditions: PropTypes.bool,
entity: PropTypes.string.isRequired,
entityId: PropTypes.string.isRequired,
entityName: PropTypes.string,
isPristine: PropTypes.bool,
setError: PropTypes.func,
setVisible: PropTypes.func.isRequired,
supportsClaiming: PropTypes.bool,
visible: PropTypes.bool.isRequired,
}

DeleteEntityHeaderModal.defaultProps = {
additionalConditions: false,
isPristine: false,
entityName: undefined,
setError: undefined,
supportsClaiming: false,
}

export default DeleteEntityHeaderModal
11 changes: 10 additions & 1 deletion pkg/webui/console/containers/gateway-overview-header/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {

import { selectUser } from '@console/store/selectors/user'
import { selectBookmarksList } from '@console/store/selectors/user-preferences'
import { selectSelectedGatewayClaimable } from '@console/store/selectors/gateways'

import style from './gateway-overview-header.styl'

Expand All @@ -69,6 +70,7 @@ const GatewayOverviewHeader = ({ gateway }) => {
selectFetchingEntry(state, DELETE_BOOKMARK_BASE),
)
const mayDeleteGtw = useSelector(state => checkFromState(mayDeleteGateway, state))
const supportsClaiming = useSelector(selectSelectedGatewayClaimable)

const isBookmarked = useMemo(
() => bookmarks.map(b => b.entity_ids?.gateway_ids?.gateway_id).some(b => b === gateway_id),
Expand Down Expand Up @@ -121,7 +123,12 @@ const GatewayOverviewHeader = ({ gateway }) => {
<Dropdown.Item title={sharedMessages.downloadGlobalConf} action={handleGlobalConfDownload} />
{/* <Dropdown.Item title={m.duplicateGateway} action={() => {}} />*/}
{mayDeleteGtw && (
<Dropdown.Item title={sharedMessages.deleteGateway} action={handleOpenDeleteGatewayModal} />
<Dropdown.Item
title={
supportsClaiming ? sharedMessages.unclaimAndDeleteGateway : sharedMessages.deleteGateway
}
action={handleOpenDeleteGatewayModal}
/>
)}
</>
)
Expand Down Expand Up @@ -156,6 +163,7 @@ const GatewayOverviewHeader = ({ gateway }) => {
noDropdownIcon
dropdownItems={menuDropdownItems}
dropdownPosition="below left"
data-test-id="gateway-overview-menu"
/>
</div>
<DeleteEntityHeaderModal
Expand All @@ -164,6 +172,7 @@ const GatewayOverviewHeader = ({ gateway }) => {
entityName={name}
setVisible={setDeleteGatewayVisible}
visible={deleteGatewayVisible}
supportsClaiming={supportsClaiming}
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,11 @@ const BasicSettingsForm = React.memo(props => {
<Require condition={mayDeleteGtw}>
<Button
onClick={handleOpenDeleteGatewayModal}
message={supportsClaiming ? m.unclaimAndDeleteGateway : sharedMessages.deleteGateway}
message={
supportsClaiming
? sharedMessages.unclaimAndDeleteGateway
: sharedMessages.deleteGateway
}
type="button"
icon={IconTrash}
naked
Expand All @@ -272,6 +276,7 @@ const BasicSettingsForm = React.memo(props => {
entityName={gateway.name}
setVisible={setDeleteGtwVisible}
visible={deleteGtwVisible}
supportsClaiming={supportsClaiming}
/>
</Require>
</SubmitBar>
Expand Down
8 changes: 2 additions & 6 deletions pkg/webui/console/views/gateway-general-settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {
mayViewOrEditGatewayCollaborators,
} from '@console/lib/feature-checks'

import { updateGateway, getGatewayClaimInfoByEui } from '@console/store/actions/gateways'
import { updateGateway } from '@console/store/actions/gateways'
import { getApiKeysList } from '@console/store/actions/api-keys'
import { getIsConfiguration } from '@console/store/actions/identity-server'

Expand Down Expand Up @@ -131,9 +131,6 @@ const GatewayGeneralSettingsInner = () => {

const GatewaySettings = () => {
const gtwId = useSelector(selectSelectedGatewayId)
const {
ids: { eui },
} = useSelector(selectSelectedGateway)
const mayDeleteGtw = useSelector(state => checkFromState(mayDeleteGateway, state))
const mayViewApiKeys = useSelector(state => checkFromState(mayViewOrEditGatewayApiKeys, state))
const mayViewCollaborators = useSelector(state =>
Expand All @@ -151,9 +148,8 @@ const GatewaySettings = () => {
}
}
dispatch(attachPromise(getIsConfiguration()))
dispatch(attachPromise(getGatewayClaimInfoByEui(eui, true)))
},
[mayDeleteGtw, eui, mayViewApiKeys, mayViewCollaborators, gtwId],
[mayDeleteGtw, mayViewApiKeys, mayViewCollaborators, gtwId],
)

useBreadcrumbs(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ const messages = defineMessages({
'Technical contact information for this gateway. Typically used to indicate who to contact with technical/security questions about the gateway.',
deleteGatewayDefaultMessage:
'This will <strong>PERMANENTLY DELETE THE ENTITY ITSELF AND ALL ASSOCIATED ENTITIES</strong>, including collaborator associations. It will also <strong>NOT BE POSSIBLE TO REUSE THE ENTITY ID</strong> until purged by an admin but the EUI can be reregistered later with a different ID.',
unclaimAndDeleteGateway: 'Unclaim and delete gateway',
})

export default messages
7 changes: 5 additions & 2 deletions pkg/webui/console/views/gateway/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
getGateway,
stopGatewayEventsStream,
getGatewaysRightsList,
getGatewayClaimInfoByEui,
} from '@console/store/actions/gateways'
import { getGsFrequencyPlans } from '@console/store/actions/configuration'
import { trackRecencyFrequencyItem } from '@console/store/actions/recency-frequency-items'
Expand Down Expand Up @@ -82,9 +83,11 @@ const Gateway = () => {
selector.push('lbs_lns_secret')
}

dispatch(getGsFrequencyPlans())
await dispatch(getGsFrequencyPlans())

return dispatch(attachPromise(getGateway(gtwId, selector)))
const { ids } = await dispatch(attachPromise(getGateway(gtwId, selector)))

await dispatch(attachPromise(getGatewayClaimInfoByEui(ids.eui, true)))
},
[gtwId],
)
Expand Down
1 change: 1 addition & 0 deletions pkg/webui/lib/shared-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export default defineMessages({
'Delay too short. The lower bound ({minimumValue}ms) will be used by the Gateway Server.',
deleteGateway: 'Delete gateway',
unclaimAndDeleteDevice: 'Unclaim and delete end device',
unclaimAndDeleteGateway: 'Unclaim and delete gateway',
deleteDevice: 'Delete end device',
deleteApp: 'Delete application',
deleteSuccess: 'Application deleted',
Expand Down
2 changes: 1 addition & 1 deletion pkg/webui/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -996,7 +996,6 @@
"console.views.gateway-general-settings.messages.adminContactDescription": "Administrative contact information for this gateway. Typically used to indicate who to contact with administrative questions about the gateway.",
"console.views.gateway-general-settings.messages.techContactDescription": "Technical contact information for this gateway. Typically used to indicate who to contact with technical/security questions about the gateway.",
"console.views.gateway-general-settings.messages.deleteGatewayDefaultMessage": "This will <strong>PERMANENTLY DELETE THE ENTITY ITSELF AND ALL ASSOCIATED ENTITIES</strong>, including collaborator associations. It will also <strong>NOT BE POSSIBLE TO REUSE THE ENTITY ID</strong> until purged by an admin but the EUI can be reregistered later with a different ID.",
"console.views.gateway-general-settings.messages.unclaimAndDeleteGateway": "Unclaim and delete gateway",
"console.views.organization-add.index.orgDescription": "Organizations are used to group multiple users and assigning collective rights for them. An organization can then be set as collaborator of applications or gateways. This makes it easy to grant or revoke rights to entities for a group of users.{break} Learn more in our guide on <Link>Organization Management</Link>.",
"console.views.user-settings-oauth-auth-settings.index.deleteButton": "Revoke authorization",
"console.views.user-settings-oauth-auth-settings.index.deleteSuccess": "This authorization was successfully revoked",
Expand Down Expand Up @@ -1312,6 +1311,7 @@
"lib.shared-messages.delayWarning": "Delay too short. The lower bound ({minimumValue}ms) will be used by the Gateway Server.",
"lib.shared-messages.deleteGateway": "Delete gateway",
"lib.shared-messages.unclaimAndDeleteDevice": "Unclaim and delete end device",
"lib.shared-messages.unclaimAndDeleteGateway": "Unclaim and delete gateway",
"lib.shared-messages.deleteDevice": "Delete end device",
"lib.shared-messages.deleteApp": "Delete application",
"lib.shared-messages.deleteSuccess": "Application deleted",
Expand Down
Loading

0 comments on commit ea801ce

Please sign in to comment.