Skip to content

Commit

Permalink
[#1405] Shared network delete in the UI
Browse files Browse the repository at this point in the history
  • Loading branch information
msiodelski committed Jun 19, 2024
1 parent 71c1388 commit 89c65f9
Show file tree
Hide file tree
Showing 9 changed files with 263 additions and 24 deletions.
10 changes: 10 additions & 0 deletions webui/src/app/shared-network-tab/shared-network-tab.component.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<p-confirmDialog [baseZIndex]="10000" rejectButtonStyleClass="p-button-text"></p-confirmDialog>
<div *ngIf="sharedNetwork" class="mt-4 ml-2">
<div class="flex text-xl align-items-baseline font-normal text-primary mb-4">
<div class="fa fa-project-diagram mr-2"></div>
Expand Down Expand Up @@ -129,5 +130,14 @@
class="p-button-info ml-2"
(click)="onSharedNetworkEditBegin()"
></button>
<button
type="button"
pButton
[disabled]="sharedNetworkDeleting"
label="Delete"
icon="pi pi-times"
class="p-button-danger ml-2"
(click)="confirmDeleteSharedNetwork()"
></button>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'

import { SharedNetworkTabComponent } from './shared-network-tab.component'
import { FieldsetModule } from 'primeng/fieldset'
Expand All @@ -13,7 +13,6 @@ import { CheckboxModule } from 'primeng/checkbox'
import { FormsModule } from '@angular/forms'
import { NoopAnimationsModule } from '@angular/platform-browser/animations'
import { OverlayPanelModule } from 'primeng/overlaypanel'
import { RouterTestingModule } from '@angular/router/testing'
import { TableModule } from 'primeng/table'
import { TagModule } from 'primeng/tag'
import { TooltipModule } from 'primeng/tooltip'
Expand All @@ -29,10 +28,20 @@ import { PlaceholderPipe } from '../pipes/placeholder.pipe'
import { SubnetBarComponent } from '../subnet-bar/subnet-bar.component'
import { IPType } from '../iptype'
import { By } from '@angular/platform-browser'
import { ConfirmDialogModule } from 'primeng/confirmdialog'
import { RouterModule } from '@angular/router'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { ConfirmationService, MessageService } from 'primeng/api'
import { of, throwError } from 'rxjs'
import { DHCPService } from '../backend'
import { HttpErrorResponse } from '@angular/common/http'

describe('SharedNetworkTabComponent', () => {
let component: SharedNetworkTabComponent
let fixture: ComponentFixture<SharedNetworkTabComponent>
let dhcpApi: DHCPService
let msgService: MessageService
let confirmService: ConfirmationService

beforeEach(async () => {
await TestBed.configureTestingModule({
Expand All @@ -56,21 +65,27 @@ describe('SharedNetworkTabComponent', () => {
ButtonModule,
ChartModule,
CheckboxModule,
ConfirmDialogModule,
DividerModule,
FieldsetModule,
FormsModule,
HttpClientTestingModule,
NoopAnimationsModule,
OverlayPanelModule,
RouterTestingModule,
RouterModule.forRoot([{ path: 'dhcp/shared-networks/:id', component: SharedNetworkTabComponent }]),
TableModule,
TagModule,
TooltipModule,
TreeModule,
],
providers: [ConfirmationService, MessageService],
}).compileComponents()

fixture = TestBed.createComponent(SharedNetworkTabComponent)
component = fixture.componentInstance
dhcpApi = fixture.debugElement.injector.get(DHCPService)
confirmService = fixture.debugElement.injector.get(ConfirmationService)
msgService = fixture.debugElement.injector.get(MessageService)
fixture.detectChanges()
})

Expand Down Expand Up @@ -515,4 +530,75 @@ describe('SharedNetworkTabComponent', () => {
expect(fieldsets[5].nativeElement.innerText).toContain('DHCP Options')
expect(fieldsets[5].nativeElement.innerText).toContain('No options configured.')
})

it('should display shared network delete button', () => {
component.sharedNetwork = {
name: 'foo',
universe: IPType.IPv6,
addrUtilization: 30,
pdUtilization: 60,
pools: [
{
pool: '2001:db8:1::2-2001:db8:1::786',
},
],
localSharedNetworks: [
{
appId: 1,
appName: '[email protected]',
keaConfigSharedNetworkParameters: {
sharedNetworkLevelParameters: {},
},
},
],
}
fixture.detectChanges()
const deleteBtn = fixture.debugElement.query(By.css('[label=Delete]'))
expect(deleteBtn).toBeTruthy()

// Simulate clicking on the button and make sure that the confirm dialog
// has been displayed.
spyOn(confirmService, 'confirm')
deleteBtn.nativeElement.click()
expect(confirmService.confirm).toHaveBeenCalled()
})

it('should emit an event indicating successful shared network deletion', fakeAsync(() => {
const successResp: any = {}
spyOn(dhcpApi, 'deleteSharedNetwork').and.returnValue(of(successResp))
spyOn(msgService, 'add')
spyOn(component.sharedNetworkDelete, 'emit')

// Delete the subnet.
component.sharedNetwork = {
id: 1,
}
component.deleteSharedNetwork()
tick()
// Success message should be displayed.
expect(msgService.add).toHaveBeenCalled()
// An event should be called.
expect(component.sharedNetworkDelete.emit).toHaveBeenCalledWith(component.sharedNetwork)
// This flag should be cleared.
expect(component.sharedNetworkDeleting).toBeFalse()
}))

it('should not emit an event when shared network deletion fails', fakeAsync(() => {
spyOn(dhcpApi, 'deleteSharedNetwork').and.returnValue(throwError(() => new HttpErrorResponse({ status: 404 })))
spyOn(msgService, 'add')
spyOn(component.sharedNetworkDelete, 'emit')

// Delete the host and receive an error.
component.sharedNetwork = {
id: 1,
}
component.deleteSharedNetwork()
tick()
// Error message should be displayed.
expect(msgService.add).toHaveBeenCalled()
// The event shouldn't be emitted on error.
expect(component.sharedNetworkDelete.emit).not.toHaveBeenCalledWith(component.sharedNetwork)
// This flag should be cleared.
expect(component.sharedNetworkDeleting).toBeFalse()
}))
})
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ import { LocalNumberPipe } from '../pipes/local-number.pipe'
import { FieldsetModule } from 'primeng/fieldset'
import { DividerModule } from 'primeng/divider'
import { TableModule } from 'primeng/table'
import { NoopAnimationsModule } from '@angular/platform-browser/animations'
import { provideNoopAnimations } from '@angular/platform-browser/animations'
import { UtilizationStatsChartComponent } from '../utilization-stats-chart/utilization-stats-chart.component'
import { EntityLinkComponent } from '../entity-link/entity-link.component'
import { AddressPoolBarComponent } from '../address-pool-bar/address-pool-bar.component'
import { RouterTestingModule } from '@angular/router/testing'
import { DelegatedPrefixBarComponent } from '../delegated-prefix-bar/delegated-prefix-bar.component'
import { UtilizationStatsChartsComponent } from '../utilization-stats-charts/utilization-stats-charts.component'
import { CascadedParametersBoardComponent } from '../cascaded-parameters-board/cascaded-parameters-board.component'
Expand All @@ -27,27 +26,46 @@ import { FormsModule } from '@angular/forms'
import { IPType } from '../iptype'
import { PlaceholderPipe } from '../pipes/placeholder.pipe'
import { SubnetBarComponent } from '../subnet-bar/subnet-bar.component'
import { importProvidersFrom } from '@angular/core'
import { HttpClientModule } from '@angular/common/http'
import { ConfirmationService, MessageService } from 'primeng/api'
import { ConfirmDialogModule } from 'primeng/confirmdialog'
import { toastDecorator } from '../utils-stories'
import { MessageModule } from 'primeng/message'
import { ToastModule } from 'primeng/toast'
import { RouterModule, provideRouter } from '@angular/router'

export default {
title: 'App/SharedNetworkTab',
component: SharedNetworkTabComponent,
decorators: [
applicationConfig({
providers: [],
providers: [
ConfirmationService,
importProvidersFrom(HttpClientModule),
MessageService,
provideNoopAnimations(),
provideRouter([
{ path: 'dhcp/shared-networks/:id', component: SharedNetworkTabComponent },
{ path: 'iframe.html', component: SharedNetworkTabComponent },
]),
],
}),
moduleMetadata({
imports: [
ButtonModule,
ChartModule,
CheckboxModule,
ConfirmDialogModule,
DividerModule,
FieldsetModule,
FormsModule,
NoopAnimationsModule,
MessageModule,
OverlayPanelModule,
RouterTestingModule,
RouterModule,
TableModule,
TagModule,
ToastModule,
TooltipModule,
TreeModule,
],
Expand All @@ -67,6 +85,7 @@ export default {
UtilizationStatsChartsComponent,
],
}),
toastDecorator,
],
} as Meta

Expand All @@ -75,6 +94,7 @@ type Story = StoryObj<SharedNetworkTabComponent>
export const SharedNetwork4: Story = {
args: {
sharedNetwork: {
id: 1,
name: 'foo',
addrUtilization: 30,
pools: [
Expand Down Expand Up @@ -334,6 +354,7 @@ export const SharedNetwork4: Story = {
export const SharedNetwork6: Story = {
args: {
sharedNetwork: {
id: 2,
name: 'foo',
universe: IPType.IPv6,
addrUtilization: 30,
Expand Down
78 changes: 77 additions & 1 deletion webui/src/app/shared-network-tab/shared-network-tab.component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
import { SharedNetworkWithUniquePools, hasDifferentLocalSharedNetworkOptions } from '../subnets'
import { NamedCascadedParameters } from '../cascaded-parameters-board/cascaded-parameters-board.component'
import { DHCPOption, KeaConfigSubnetDerivedParameters } from '../backend'
import { DHCPOption, DHCPService, KeaConfigSubnetDerivedParameters, SharedNetwork } from '../backend'
import { ConfirmationService, MessageService } from 'primeng/api'
import { lastValueFrom } from 'rxjs'
import { getErrorMessage } from '../utils'

@Component({
selector: 'app-shared-network-tab',
Expand All @@ -20,6 +23,12 @@ export class SharedNetworkTabComponent implements OnInit {
*/
@Output() sharedNetworkEditBegin = new EventEmitter<any>()

/**
* An event emitter notifying a parent that user has clicked the
* Delete button to delete the shared network.
*/
@Output() sharedNetworkDelete = new EventEmitter<SharedNetwork>()

/**
* DHCP parameters structured for display by the @link CascadedParametersBoard.
*
Expand All @@ -35,6 +44,26 @@ export class SharedNetworkTabComponent implements OnInit {
*/
dhcpOptions: DHCPOption[][][] = []

/**
* Disables the button deleting a shared network after clicking this button.
*/
sharedNetworkDeleting = false

/**
* Component constructor.
*
* @param dhcpApi service used to communicate with the server over REST API.
* @param confirmService confirmation service displaying the confirm dialog when
* attempting to delete the shared network.
* @param msgService service displaying error messages upon a communication
* error with the server.
*/
constructor(
private dhcpApi: DHCPService,
private confirmService: ConfirmationService,
private msgService: MessageService
) {}

/**
* A component lifecycle hook invoked upon the component initialization.
*
Expand Down Expand Up @@ -80,4 +109,51 @@ export class SharedNetworkTabComponent implements OnInit {
onSharedNetworkEditBegin(): void {
this.sharedNetworkEditBegin.emit(this.sharedNetwork)
}

/**
* Displays a dialog to confirm shared network deletion.
*/
confirmDeleteSharedNetwork() {
this.confirmService.confirm({
message: 'Are you sure that you want to permanently delete this shared network and its subnets?',
header: 'Delete Shared Network',
icon: 'pi pi-exclamation-triangle',
accept: () => {
this.deleteSharedNetwork()
},
})
}

/**
* Sends a request to the server to delete the shared network.
*/
deleteSharedNetwork() {
// Disable the button for deleting the shared network to prevent pressing the
// button multiple times and sending multiple requests.
this.sharedNetworkDeleting = true
lastValueFrom(this.dhcpApi.deleteSharedNetwork(this.sharedNetwork.id))
.then((/* data */) => {
this.msgService.add({
severity: 'success',
summary: `Shared network ${this.sharedNetwork.name} successfully deleted`,
})
// Notify the parent that the shared network was deleted and the tab can be closed.
this.sharedNetworkDelete.emit(this.sharedNetwork)
})
.catch((err) => {
// Re-enable the delete button.
// Issues with deleting the host.
const msg = getErrorMessage(err)
this.msgService.add({
severity: 'error',
summary: 'Cannot delete the shared network',
detail: `Failed to delete the shared network ${this.sharedNetwork.name} : ${msg}`,
life: 10000,
})
})
.finally(() => {
// Re-enable the delete button.
this.sharedNetworkDeleting = false
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@
*ngSwitchCase="SharedNetworkTabType.Display"
[sharedNetwork]="openedTabs[activeTabIndex].tabSubject"
(sharedNetworkEditBegin)="onSharedNetworkEditBegin(openedTabs[activeTabIndex].tabSubject)"
(sharedNetworkDelete)="onSharedNetworkDelete(openedTabs[activeTabIndex].tabSubject)"
></app-shared-network-tab>
<app-shared-network-form
*ngSwitchCase="SharedNetworkTabType.New"
Expand Down
Loading

0 comments on commit 89c65f9

Please sign in to comment.