From e1477fa635127efa6790827a8e8bb8480721c1e2 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Fri, 5 Jun 2020 11:36:52 +0200 Subject: [PATCH 1/7] feature: extract getLocationById method, wrap location fetching in useAsync --- plugins/catalog/src/api/CatalogClient.ts | 26 ++++++--- plugins/catalog/src/api/types.ts | 2 + .../components/CatalogPage/CatalogPage.tsx | 56 +++++++++---------- .../ComponentRemovalDialog.tsx | 3 + plugins/catalog/src/data/utils.ts | 14 ++++- 5 files changed, 59 insertions(+), 42 deletions(-) diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts index abba8a7523..804f2b3b6a 100644 --- a/plugins/catalog/src/api/CatalogClient.ts +++ b/plugins/catalog/src/api/CatalogClient.ts @@ -31,6 +31,22 @@ export class CatalogClient implements CatalogApi { this.apiOrigin = apiOrigin; this.basePath = basePath; } + async getLocationById(id: String): Promise { + const response = await fetch( + `${this.apiOrigin}${this.basePath}/locations/${id}`, + ); + if (response.ok) { + const location = await response.json(); + if (location) return location.data; + } + return undefined; + } + async getEntitiesByLocationId(id: string): Promise { + const response = await fetch( + `${this.apiOrigin}${this.basePath}/entities?backstage.io/managed-by-location=${id}`, + ); + return await response.json(); + } async getEntities(): Promise { const response = await fetch(`${this.apiOrigin}${this.basePath}/entities`); return await response.json(); @@ -50,14 +66,8 @@ export class CatalogClient implements CatalogApi { const locationId = findLocationIdInEntity(entity); if (!locationId) return undefined; - const response = await fetch( - `${this.apiOrigin}${this.basePath}/locations/${locationId}`, - ); - if (response.ok) { - const location = await response.json(); - if (location) return location.data; - } + const location = this.getLocationById(locationId); - return undefined; + return location; } } diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index 9e8dc5fbac..74184b8081 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -23,7 +23,9 @@ export const catalogApiRef = createApiRef({ }); export interface CatalogApi { + getLocationById(id: String): Promise; getEntities(): Promise; getEntityByName(name: string): Promise; + getEntitiesByLocationId(id: string): Promise; getLocationByEntity(entity: Entity): Promise; } diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 25d2618a9a..1597ae2135 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { FC, useCallback, useState, useEffect } from 'react'; +import React, { FC, useCallback, useState } from 'react'; import { Content, ContentHeader, @@ -47,13 +47,12 @@ const useStyles = makeStyles(theme => ({ })); import { catalogApiRef } from '../..'; -import { envelopeToComponent } from '../../data/utils'; +import { envelopeToComponent, findLocationForEntity } from '../../data/utils'; import { Component } from '../../data/component'; const CatalogPage: FC<{}> = () => { const catalogApi = useApi(catalogApiRef); const { value, error, loading } = useAsync(() => catalogApi.getEntities()); - const [locations, setLocations] = useState([]); const [selectedFilter, setSelectedFilter] = useState( defaultFilter, ); @@ -65,7 +64,7 @@ const CatalogPage: FC<{}> = () => { ); const styles = useStyles(); - useEffect(() => { + const { value: locations = [] } = useAsync(async () => { const getLocationDataForEntities = async (entities: Entity[]) => { return Promise.all( entities.map(entity => catalogApi.getLocationByEntity(entity)), @@ -76,12 +75,14 @@ const CatalogPage: FC<{}> = () => { getLocationDataForEntities(value) .then( (location): Location[] => - location.filter(l => !!l) as Array, + location.filter(loc => !!loc) as Array, ) .then(location => { - if (isMounted()) setLocations(location); + if (isMounted()) return [location]; + return []; }); } + return []; }, [value, catalogApi, isMounted]); const actions = [ @@ -97,15 +98,6 @@ const CatalogPage: FC<{}> = () => { }), ]; - const findLocationForEntity = ( - entity: Entity, - l: Location[], - ): Location | undefined => { - const entityLocationId = - entity.metadata.annotations?.['backstage.io/managed-by-location']; - return l.find(location => location.id === entityLocationId); - }; - return (
@@ -142,22 +134,24 @@ const CatalogPage: FC<{}> = () => { onSelectedChange={onFilterSelected} /> - - envelopeToComponent( - val, - findLocationForEntity(val, locations), - ), - )) || - [] - } - loading={loading} - error={error} - actions={actions} - /> + {locations && ( + + envelopeToComponent( + val, + findLocationForEntity(val, locations) ?? undefined, + ), + )) || + [] + } + loading={loading} + error={error} + actions={actions} + /> + )} diff --git a/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx b/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx index b5a863cef8..a25e761dbb 100644 --- a/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx +++ b/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx @@ -25,6 +25,9 @@ import { useTheme, } from '@material-ui/core'; import { Component } from '../../data/component'; +import { useAsync } from 'react-use'; +import { useApi, Progress } from '@backstage/core'; +import { catalogApiRef } from '../../api/types'; type ComponentRemovalDialogProps = { onConfirm: () => any; diff --git a/plugins/catalog/src/data/utils.ts b/plugins/catalog/src/data/utils.ts index fe2ec62493..a3ce0c43ee 100644 --- a/plugins/catalog/src/data/utils.ts +++ b/plugins/catalog/src/data/utils.ts @@ -16,14 +16,22 @@ import { Component } from './component'; import { Entity, Location } from '@backstage/catalog-model'; -export function envelopeToComponent( +export const envelopeToComponent = ( envelope: Entity, location?: Location, -): Component { +): Component => { return { name: envelope.metadata?.name ?? '', kind: envelope.kind ?? 'unknown', description: envelope.metadata?.annotations?.description ?? 'placeholder', location, }; -} +}; +export const findLocationForEntity = ( + entity: Entity, + l: Location[], +): Location | undefined => { + const entityLocationId = + entity.metadata.annotations?.['backstage.io/managed-by-location']; + return l.find(location => location.id === entityLocationId); +}; From 29f7f1c333d02dd400f9fe7598b17de12878aed2 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Fri, 5 Jun 2020 11:55:08 +0200 Subject: [PATCH 2/7] fix: make locations fetching work --- .../src/components/CatalogPage/CatalogPage.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 1597ae2135..ce983c991a 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -77,14 +77,13 @@ const CatalogPage: FC<{}> = () => { (location): Location[] => location.filter(loc => !!loc) as Array, ) - .then(location => { - if (isMounted()) return [location]; + .then(locs => { + if (isMounted()) return locs; return []; }); } return []; - }, [value, catalogApi, isMounted]); - + }, [value, catalogApi, isMounted, catalogApi]); const actions = [ (rowData: Component) => ({ icon: GitHub, @@ -139,12 +138,11 @@ const CatalogPage: FC<{}> = () => { titlePreamble={selectedFilter.label} components={ (value && - value.map(val => - envelopeToComponent( - val, - findLocationForEntity(val, locations) ?? undefined, - ), - )) || + value.map(val => { + const loc = + findLocationForEntity(val, locations) ?? undefined; + return envelopeToComponent(val, loc); + })) || [] } loading={loading} From 10980727a7b7aaae765ef9c5d88f483a9605de23 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Fri, 5 Jun 2020 14:14:07 +0200 Subject: [PATCH 3/7] fix: github actions --- packages/catalog-model/src/location/index.ts | 1 + .../src/database/CommonDatabase.ts | 9 ++++- .../src/ingestion/HigherOrderOperations.ts | 9 +++-- plugins/catalog/src/api/CatalogClient.ts | 13 +++--- .../components/CatalogPage/CatalogPage.tsx | 40 ++++++++++--------- .../ComponentPage/ComponentPage.tsx | 8 ++-- .../ComponentRemovalDialog.tsx | 21 +++++++--- plugins/catalog/src/data/component.ts | 1 + plugins/catalog/src/data/utils.ts | 19 +++++---- 9 files changed, 74 insertions(+), 47 deletions(-) diff --git a/packages/catalog-model/src/location/index.ts b/packages/catalog-model/src/location/index.ts index 60465bff9b..ff696524ab 100644 --- a/packages/catalog-model/src/location/index.ts +++ b/packages/catalog-model/src/location/index.ts @@ -16,3 +16,4 @@ export type { Location, LocationSpec } from './types'; export { locationSchema, locationSpecSchema } from './validation'; +export { LOCATION_ANNOTATION } from './annotation'; diff --git a/plugins/catalog-backend/src/database/CommonDatabase.ts b/plugins/catalog-backend/src/database/CommonDatabase.ts index dd965c89ef..bb5b95d9ba 100644 --- a/plugins/catalog-backend/src/database/CommonDatabase.ts +++ b/plugins/catalog-backend/src/database/CommonDatabase.ts @@ -19,7 +19,12 @@ import { InputError, NotFoundError, } from '@backstage/backend-common'; -import type { Entity, EntityMeta, Location } from '@backstage/catalog-model'; +import { + Entity, + EntityMeta, + Location, + LOCATION_ANNOTATION, +} from '@backstage/catalog-model'; import Knex from 'knex'; import lodash from 'lodash'; import { v4 as uuidv4 } from 'uuid'; @@ -167,7 +172,7 @@ export class CommonDatabase implements Database { annotations: { ...(newEntity.metadata?.annotations ?? {}), ...(request.locationId - ? { 'backstage.io/managed-by-location': request.locationId } + ? { [LOCATION_ANNOTATION]: request.locationId } : {}), }, }; diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts index e9051d1f78..3300df1640 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts @@ -15,7 +15,12 @@ */ import { InputError } from '@backstage/backend-common'; -import { Entity, Location, LocationSpec } from '@backstage/catalog-model'; +import { + Entity, + Location, + LocationSpec, + LOCATION_ANNOTATION, +} from '@backstage/catalog-model'; import lodash from 'lodash'; import { v4 as uuidv4 } from 'uuid'; import { EntitiesCatalog, LocationsCatalog } from '../catalog'; @@ -23,8 +28,6 @@ import { IngestionModel } from '../ingestion'; import { AddLocationResult, HigherOrderOperation } from './types'; import { Logger } from 'winston'; -const LOCATION_ANNOTATION = 'backstage.io/managed-by-location'; - /** * Placeholder for operations that span several catalogs and/or stretches out * in time. diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts index 804f2b3b6a..3f1d5a6090 100644 --- a/plugins/catalog/src/api/CatalogClient.ts +++ b/plugins/catalog/src/api/CatalogClient.ts @@ -16,7 +16,11 @@ import { CatalogApi } from './types'; import { DescriptorEnvelope } from '../types'; -import { Entity, Location } from '@backstage/catalog-model'; +import { + Entity, + Location, + LOCATION_ANNOTATION, +} from '@backstage/catalog-model'; export class CatalogClient implements CatalogApi { private apiOrigin: string; @@ -43,7 +47,7 @@ export class CatalogClient implements CatalogApi { } async getEntitiesByLocationId(id: string): Promise { const response = await fetch( - `${this.apiOrigin}${this.basePath}/entities?backstage.io/managed-by-location=${id}`, + `${this.apiOrigin}${this.basePath}/entities?${LOCATION_ANNOTATION}=${id}`, ); return await response.json(); } @@ -60,10 +64,7 @@ export class CatalogClient implements CatalogApi { throw new Error(`'Entity not found: ${name}`); } async getLocationByEntity(entity: Entity): Promise { - const findLocationIdInEntity = (e: Entity): string | undefined => - e.metadata.annotations?.['backstage.io/managed-by-location']; - - const locationId = findLocationIdInEntity(entity); + const locationId = entity.metadata.annotations?.[LOCATION_ANNOTATION]; if (!locationId) return undefined; const location = this.getLocationById(locationId); diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index ce983c991a..b23519a668 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -26,7 +26,7 @@ import { pageTheme, useApi, } from '@backstage/core'; -import { useAsync, useMountedState } from 'react-use'; +import { useAsync } from 'react-use'; import CatalogTable from '../CatalogTable/CatalogTable'; import { CatalogFilter, @@ -35,7 +35,11 @@ import { import { Button, makeStyles, Typography, Link } from '@material-ui/core'; import { filterGroups, defaultFilter } from '../../data/filters'; import GitHub from '@material-ui/icons/GitHub'; -import { Entity, Location } from '@backstage/catalog-model'; +import { + Entity, + Location, + LOCATION_ANNOTATION, +} from '@backstage/catalog-model'; const useStyles = makeStyles(theme => ({ contentWrapper: { @@ -56,7 +60,6 @@ const CatalogPage: FC<{}> = () => { const [selectedFilter, setSelectedFilter] = useState( defaultFilter, ); - const isMounted = useMountedState(); const onFilterSelected = useCallback( selected => setSelectedFilter(selected), @@ -64,26 +67,26 @@ const CatalogPage: FC<{}> = () => { ); const styles = useStyles(); - const { value: locations = [] } = useAsync(async () => { + const { value: locations } = useAsync(async () => { const getLocationDataForEntities = async (entities: Entity[]) => { return Promise.all( - entities.map(entity => catalogApi.getLocationByEntity(entity)), + entities.map(entity => { + const locationId = entity.metadata.annotations?.[LOCATION_ANNOTATION]; + if (!locationId) return undefined; + + return catalogApi.getLocationById(locationId); + }), ); }; if (value) { - getLocationDataForEntities(value) - .then( - (location): Location[] => - location.filter(loc => !!loc) as Array, - ) - .then(locs => { - if (isMounted()) return locs; - return []; - }); + return getLocationDataForEntities(value).then( + (location): Location[] => + location.filter(loc => !!loc) as Array, + ); } return []; - }, [value, catalogApi, isMounted, catalogApi]); + }, [value, catalogApi, catalogApi]); const actions = [ (rowData: Component) => ({ icon: GitHub, @@ -139,9 +142,10 @@ const CatalogPage: FC<{}> = () => { components={ (value && value.map(val => { - const loc = - findLocationForEntity(val, locations) ?? undefined; - return envelopeToComponent(val, loc); + return { + ...envelopeToComponent(val), + location: findLocationForEntity(val, locations), + }; })) || [] } diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx index 62f34f1620..481a5aeac9 100644 --- a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx @@ -54,9 +54,10 @@ const ComponentPage: FC = ({ match, history }) => { const errorApi = useApi(errorApiRef); const catalogApi = useApi(catalogApiRef); - const catalogRequest = useAsync(() => - catalogApi.getEntityByName(match.params.name), - ); + const catalogRequest = useAsync(async () => { + const entity = await catalogApi.getEntityByName(match.params.name); + return entity; + }); useEffect(() => { if (catalogRequest.error) { @@ -76,6 +77,7 @@ const ComponentPage: FC = ({ match, history }) => { setConfirmationDialogOpen(false); setRemovingPending(true); // await componentFactory.removeComponentByName(componentName); + await catalogApi; history.push('/catalog'); }; diff --git a/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx b/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx index a25e761dbb..7dde95c8f0 100644 --- a/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx +++ b/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx @@ -26,8 +26,9 @@ import { } from '@material-ui/core'; import { Component } from '../../data/component'; import { useAsync } from 'react-use'; -import { useApi, Progress } from '@backstage/core'; +import { useApi } from '@backstage/core'; import { catalogApiRef } from '../../api/types'; +import { Entity } from '@backstage/catalog-model'; type ComponentRemovalDialogProps = { onConfirm: () => any; @@ -41,18 +42,28 @@ const ComponentRemovalDialog: FC = ({ onClose, component, }) => { + const catalogApi = useApi(catalogApiRef); + const { value } = useAsync(async () => { + let colocatedEntities: Array = []; + const locationId = component.location?.id; + if (locationId) { + colocatedEntities = await catalogApi.getEntitiesByLocationId(locationId); + } + return colocatedEntities; + }); const theme = useTheme(); const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + const infoMessage = `This action will unregister ${ + value ? value.map(e => e.metadata.name).join(', ') : '' + } from location with target ${component.location?.target}. To undo, + just re-register the component in Backstage.`; return ( Are you sure you want to unregister this component? - - This action will unregister {component.name}. To undo, just - re-register the component in Backstage. - + {infoMessage}