diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts index eb9cb2439c..30b480f348 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts @@ -28,6 +28,7 @@ describe('DatabaseEntitiesCatalog', () => { updateEntity: jest.fn(), entities: jest.fn(), entity: jest.fn(), + entityByUid: jest.fn(), removeEntity: jest.fn(), addLocation: jest.fn(), removeLocation: jest.fn(), diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts index 1ec1ebf6e5..6ab660a587 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts @@ -15,6 +15,9 @@ */ import type { Entity } from '@backstage/catalog-model'; +import { LOCATION_ANNOTATION } from '@backstage/catalog-model'; +import { NotFoundError } from '@backstage/backend-common'; + import type { Database, DbEntityResponse, EntityFilters } from '../database'; import type { EntitiesCatalog } from './types'; @@ -78,7 +81,28 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { async removeEntityByUid(uid: string): Promise { return await this.database.transaction(async tx => { - await this.database.removeEntity(tx, uid); + const entityResponse = await this.database.entityByUid(tx, uid); + if (!entityResponse) { + throw new NotFoundError(`Entity with ID ${uid} was not found`); + } + const location = + entityResponse.entity.metadata.annotations?.[LOCATION_ANNOTATION]; + const colocatedEntities = location + ? await this.database.entities(tx, [ + { + key: LOCATION_ANNOTATION, + values: [location], + }, + ]) + : [entityResponse]; + for (const dbResponse of colocatedEntities) { + await this.database.removeEntity(tx, dbResponse?.entity.metadata.uid!); + } + + if (entityResponse.locationId) { + await this.database.removeLocation(tx, entityResponse?.locationId!); + } + return undefined; }); } diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts index a1e76679f7..f515829b91 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts @@ -31,7 +31,7 @@ export class DatabaseLocationsCatalog implements LocationsCatalog { } async removeLocation(id: string): Promise { - await this.database.removeLocation(id); + await this.database.transaction(tx => this.database.removeLocation(tx, id)); } async locations(): Promise { diff --git a/plugins/catalog-backend/src/database/CommonDatabase.test.ts b/plugins/catalog-backend/src/database/CommonDatabase.test.ts index f1723e3733..1802ea5e7d 100644 --- a/plugins/catalog-backend/src/database/CommonDatabase.test.ts +++ b/plugins/catalog-backend/src/database/CommonDatabase.test.ts @@ -105,8 +105,7 @@ describe('CommonDatabase', () => { expect(locations).toEqual([output]); const location = await db.location(locations[0].id); expect(location).toEqual(output); - - await db.removeLocation(locations[0].id); + await db.transaction(tx => db.removeLocation(tx, locations[0].id)); await expect(db.locations()).resolves.toEqual([]); await expect(db.location(locations[0].id)).rejects.toThrow( diff --git a/plugins/catalog-backend/src/database/CommonDatabase.ts b/plugins/catalog-backend/src/database/CommonDatabase.ts index 882995c142..2a1b33bbbe 100644 --- a/plugins/catalog-backend/src/database/CommonDatabase.ts +++ b/plugins/catalog-backend/src/database/CommonDatabase.ts @@ -319,6 +319,21 @@ export class CommonDatabase implements Database { return toEntityResponse(rows[0]); } + async entityByUid( + txOpaque: unknown, + id: string, + ): Promise { + const tx = txOpaque as Knex.Transaction; + + const rows = await tx('entities').where({ id }).select(); + + if (rows.length !== 1) { + return undefined; + } + + return toEntityResponse(rows[0]); + } + async removeEntity(txOpaque: unknown, uid: string): Promise { const tx = txOpaque as Knex.Transaction; @@ -341,10 +356,10 @@ export class CommonDatabase implements Database { }); } - async removeLocation(id: string): Promise { - const result = await this.database('locations') - .where({ id }) - .del(); + async removeLocation(txOpaque: unknown, id: string): Promise { + const tx = txOpaque as Knex.Transaction; + + const result = await tx('locations').where({ id }).del(); if (!result) { throw new NotFoundError(`Found no location with ID ${id}`); diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts index 17da373ae9..fc81e124ba 100644 --- a/plugins/catalog-backend/src/database/types.ts +++ b/plugins/catalog-backend/src/database/types.ts @@ -130,11 +130,13 @@ export type Database = { namespace?: string, ): Promise; + entityByUid(tx: unknown, uid: string): Promise; + removeEntity(tx: unknown, uid: string): Promise; addLocation(location: Location): Promise; - removeLocation(id: string): Promise; + removeLocation(tx: unknown, id: string): Promise; location(id: string): Promise; diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts index dcaab53307..86ea6d0624 100644 --- a/plugins/catalog/src/api/CatalogClient.ts +++ b/plugins/catalog/src/api/CatalogClient.ts @@ -134,4 +134,20 @@ export class CatalogClient implements CatalogApi { .map(r => r.data) .find(l => locationCompound === `${l.type}:${l.target}`); } + + async removeEntityByUid(uid: string): Promise { + const response = await fetch( + `${this.apiOrigin}${this.basePath}/entities/by-uid/${uid}`, + { + method: 'DELETE', + }, + ); + if (!response.ok) { + const payload = await response.text(); + throw new Error( + `Request failed with ${response.status} ${response.statusText}, ${payload}`, + ); + } + return undefined; + } } diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index 9edf0358d1..3bd2c6de97 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -36,6 +36,7 @@ export interface CatalogApi { getEntities(filter?: Record): Promise; addLocation(type: string, target: string): Promise; getLocationByEntity(entity: Entity): Promise; + removeEntityByUid(uid: string): Promise; } export type AddLocationResponse = { location: Location; entities: Entity[] }; diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx index 6246726fab..b78450d549 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; import { CatalogFilter, CatalogFilterGroup } from './CatalogFilter'; -import { FilterGroupItem } from '../../types'; +import { EntityFilterType } from '../../data/filters'; describe('Catalog Filter', () => { it('should render the different groups', async () => { @@ -41,11 +41,11 @@ describe('Catalog Filter', () => { name: 'Test Group 1', items: [ { - id: FilterGroupItem.ALL, + id: EntityFilterType.ALL, label: 'First Label', }, { - id: FilterGroupItem.STARRED, + id: EntityFilterType.STARRED, label: 'Second Label', }, ], @@ -68,12 +68,12 @@ describe('Catalog Filter', () => { name: 'Test Group 1', items: [ { - id: FilterGroupItem.ALL, + id: EntityFilterType.ALL, label: 'First Label', count: 100, }, { - id: FilterGroupItem.STARRED, + id: EntityFilterType.STARRED, label: 'Second Label', count: 400, }, @@ -97,12 +97,12 @@ describe('Catalog Filter', () => { name: 'Test Group 1', items: [ { - id: FilterGroupItem.ALL, + id: EntityFilterType.ALL, label: 'First Label', count: 100, }, { - id: FilterGroupItem.STARRED, + id: EntityFilterType.STARRED, label: 'Second Label', count: 400, }, @@ -136,12 +136,12 @@ describe('Catalog Filter', () => { name: 'Test Group 1', items: [ { - id: FilterGroupItem.ALL, + id: EntityFilterType.ALL, label: 'First Label', count: () => BACKSTAGE!, }, { - id: FilterGroupItem.STARRED, + id: EntityFilterType.STARRED, label: 'Second Label', count: 400, }, diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx index 6541a4df8d..2551ebd4d1 100644 --- a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx @@ -25,9 +25,9 @@ import { makeStyles, } from '@material-ui/core'; import type { IconComponent } from '@backstage/core'; -import { FilterGroupItem } from '../../types'; +import { EntityFilterType } from '../../data/filters'; export type CatalogFilterItem = { - id: FilterGroupItem; + id: EntityFilterType; label: string; icon?: IconComponent; count?: number | React.FC; diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index d995556546..d30361d0e7 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry, @@ -28,6 +27,7 @@ import React from 'react'; import { catalogApiRef } from '../..'; import { CatalogApi } from '../../api/types'; import { CatalogPage } from './CatalogPage'; +import { Entity } from '@backstage/catalog-model'; describe('CatalogPage', () => { const mockErrorApi = new MockErrorApi(); diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index eac753d9fa..c5af3220c0 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -37,7 +37,7 @@ import React, { FC, useCallback, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import { useAsync } from 'react-use'; import { catalogApiRef } from '../..'; -import { dataResolvers, defaultFilter, filterGroups } from '../../data/filters'; +import { defaultFilter, filterGroups, entityFilters } from '../../data/filters'; import { findLocationForEntityMeta } from '../../data/utils'; import { useStarredEntities } from '../../hooks/useStarredEntites'; import { @@ -71,7 +71,14 @@ export const CatalogPage: FC<{}> = () => { ); const { value, error, loading } = useAsync( - () => dataResolvers[selectedFilter.id]({ catalogApi, isStarredEntity }), + () => + catalogApi + .getEntities() + .then(entities => + entities.filter( + entityFilters[selectedFilter.id]({ isStarredEntity }), + ), + ), [selectedFilter.id, starredEntities.size], ); diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx index 17ad2d880d..247a13247f 100644 --- a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx @@ -95,14 +95,12 @@ export const ComponentPage: FC = ({ match, history }) => { return null; } - const removeComponent = async () => { + const cleanUpAfterRemoval = async () => { setConfirmationDialogOpen(false); - // await componentFactory.removeComponentByName(componentName); history.push('/'); }; const showRemovalDialog = () => setConfirmationDialogOpen(true); - const hideRemovalDialog = () => setConfirmationDialogOpen(false); // TODO - Replace with proper tabs implementation const tabs = [ @@ -177,8 +175,8 @@ export const ComponentPage: FC = ({ match, history }) => { setConfirmationDialogOpen(false)} /> )} diff --git a/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx b/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx index aa8f99e01e..6ad507a603 100644 --- a/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx +++ b/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx @@ -15,7 +15,7 @@ */ import { Entity, LOCATION_ANNOTATION } from '@backstage/catalog-model'; -import { Progress, useApi } from '@backstage/core'; +import { Progress, useApi, alertApiRef } from '@backstage/core'; import { Button, Dialog, @@ -59,6 +59,19 @@ export const ComponentRemovalDialog: FC = ({ const { value: entities, loading, error } = useColocatedEntities(entity); const theme = useTheme(); const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + const catalogApi = useApi(catalogApiRef); + const alertApi = useApi(alertApiRef); + + const removeEntity = async () => { + const uid = entity.metadata.uid; + try { + await catalogApi.removeEntityByUid(uid!); + } catch (err) { + alertApi.post({ message: err.message }); + } + + onConfirm(); + }; return ( @@ -90,7 +103,7 @@ export const ComponentRemovalDialog: FC = ({
  • - {entities[0]?.metadata?.annotations?.[LOCATION_ANNOTATION]} + {entities[0]?.metadata.annotations?.[LOCATION_ANNOTATION]}
@@ -101,10 +114,12 @@ export const ComponentRemovalDialog: FC = ({ ) : null} - +