Skip to content

Commit

Permalink
bug(web): Inform user when deleted topology is still used
Browse files Browse the repository at this point in the history
This change fixes #135 which showed that trying to delete a topology
used by a scenario would result in nothing happening in the UI and a 500
error being returned by the server. We check whether a scenario still
references the topology and show an error to the user if that happens.

Fixes #135
  • Loading branch information
fabianishere committed Mar 26, 2023
1 parent 6bc9b99 commit a9da766
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 43 deletions.
Expand Up @@ -22,10 +22,12 @@

package org.opendc.web.server.rest.user;

import io.quarkus.hibernate.orm.panache.Panache;
import io.quarkus.security.identity.SecurityIdentity;
import java.time.Instant;
import java.util.List;
import javax.annotation.security.RolesAllowed;
import javax.persistence.PersistenceException;
import javax.transaction.Transactional;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
Expand Down Expand Up @@ -193,6 +195,14 @@ public org.opendc.web.proto.user.Topology delete(

entity.updatedAt = Instant.now();
entity.delete();

try {
// Flush the results, so we can check whether the constraints are not violated
Panache.flush();
} catch (PersistenceException e) {
throw new WebApplicationException("Topology is still in use", 403);
}

return UserProtocol.toDto(entity, auth);
}
}
Expand Up @@ -355,4 +355,20 @@ public void testDelete() {
.statusCode(200)
.contentType(ContentType.JSON);
}

/**
* Test to delete a topology that is still being used by a scenario.
*/
@Test
@TestSecurity(
user = "owner",
roles = {"openid"})
public void testDeleteUsed() {
given().pathParam("project", "1")
.when()
.delete("/1") // Topology 1 is still used by scenario 1 and 2
.then()
.statusCode(403)
.contentType(ContentType.JSON);
}
}
2 changes: 1 addition & 1 deletion opendc-web/opendc-web-ui/src/api/index.js
Expand Up @@ -49,7 +49,7 @@ export async function request(auth, path, method = 'GET', body) {
const json = await response.json()

if (!response.ok) {
throw response.message
throw json.message
}

return json
Expand Down
104 changes: 64 additions & 40 deletions opendc-web/opendc-web-ui/src/components/projects/TopologyTable.js
Expand Up @@ -20,18 +20,22 @@
* SOFTWARE.
*/

import { Bullseye } from '@patternfly/react-core'
import { Bullseye, AlertGroup, Alert, AlertVariant, AlertActionCloseButton } from '@patternfly/react-core'
import PropTypes from 'prop-types'
import Link from 'next/link'
import { Tr, Th, Thead, Td, ActionsColumn, Tbody, TableComposable } from '@patternfly/react-table'
import React from 'react'
import React, { useState } from 'react'
import TableEmptyState from '../util/TableEmptyState'
import { parseAndFormatDateTime } from '../../util/date-time'
import { useTopologies, useDeleteTopology } from '../../data/topology'

function TopologyTable({ projectId }) {
const [error, setError] = useState('')

const { status, data: topologies = [] } = useTopologies(projectId)
const { mutate: deleteTopology } = useDeleteTopology()
const { mutate: deleteTopology } = useDeleteTopology({
onError: (error) => setError(error),
})

const actions = ({ number }) => [
{
Expand All @@ -42,45 +46,65 @@ function TopologyTable({ projectId }) {
]

return (
<TableComposable aria-label="Topology List" variant="compact">
<Thead>
<Tr>
<Th>Name</Th>
<Th>Rooms</Th>
<Th>Last Edited</Th>
</Tr>
</Thead>
<Tbody>
{topologies.map((topology) => (
<Tr key={topology.id}>
<Td dataLabel="Name">
<Link href={`/projects/${projectId}/topologies/${topology.number}`}>{topology.name}</Link>
</Td>
<Td dataLabel="Rooms">
{topology.rooms.length === 1 ? '1 room' : `${topology.rooms.length} rooms`}
</Td>
<Td dataLabel="Last Edited">{parseAndFormatDateTime(topology.updatedAt)}</Td>
<Td isActionCell>
<ActionsColumn items={actions(topology)} />
</Td>
</Tr>
))}
{topologies.length === 0 && (
<>
<AlertGroup isToast>
{error && (
<Alert
isLiveRegion
variant={AlertVariant.danger}
title={error}
actionClose={
<AlertActionCloseButton
title={error}
variantLabel="danger alert"
onClose={() => setError(null)}
/>
}
/>
)}
</AlertGroup>
<TableComposable aria-label="Topology List" variant="compact">
<Thead>
<Tr>
<Td colSpan={3}>
<Bullseye>
<TableEmptyState
status={status}
loadingTitle="Loading topologies"
emptyTitle="No topologies"
emptyText="You have not created any topology for this project yet. Click the New Topology button to create one."
/>
</Bullseye>
</Td>
<Th>Name</Th>
<Th>Rooms</Th>
<Th>Last Edited</Th>
</Tr>
)}
</Tbody>
</TableComposable>
</Thead>
<Tbody>
{topologies.map((topology) => (
<Tr key={topology.id}>
<Td dataLabel="Name">
<Link href={`/projects/${projectId}/topologies/${topology.number}`}>
{topology.name}
</Link>
</Td>
<Td dataLabel="Rooms">
{topology.rooms.length === 1 ? '1 room' : `${topology.rooms.length} rooms`}
</Td>
<Td dataLabel="Last Edited">{parseAndFormatDateTime(topology.updatedAt)}</Td>
<Td isActionCell>
<ActionsColumn items={actions(topology)} />
</Td>
</Tr>
))}
{topologies.length === 0 && (
<Tr>
<Td colSpan={3}>
<Bullseye>
<TableEmptyState
status={status}
loadingTitle="Loading topologies"
emptyTitle="No topologies"
emptyText="You have not created any topology for this project yet. Click the New Topology button to create one."
/>
</Bullseye>
</Td>
</Tr>
)}
</Tbody>
</TableComposable>
</>
)
}

Expand Down
4 changes: 2 additions & 2 deletions opendc-web/opendc-web-ui/src/data/topology.js
Expand Up @@ -83,6 +83,6 @@ export function useNewTopology() {
/**
* Create a mutation for deleting a topology.
*/
export function useDeleteTopology() {
return useMutation('deleteTopology')
export function useDeleteTopology(options = {}) {
return useMutation('deleteTopology', options)
}

0 comments on commit a9da766

Please sign in to comment.