diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts index 2f9ffe302c..9ea59aab48 100644 --- a/plugins/catalog/src/api/CatalogClient.ts +++ b/plugins/catalog/src/api/CatalogClient.ts @@ -45,14 +45,27 @@ export class CatalogClient implements CatalogApi { } return undefined; } - async getEntitiesByLocationId(id: string): Promise { - const response = await fetch( - `${this.apiOrigin}${this.basePath}/entities?${LOCATION_ANNOTATION}=${id}`, - ); - return await response.json(); - } - async getEntities(): Promise { - const response = await fetch(`${this.apiOrigin}${this.basePath}/entities`); + async getEntities( + filter?: Record, + ): Promise { + let url = `${this.apiOrigin}${this.basePath}/entities`; + if (filter) { + url += '?'; + url += Object.entries(filter) + .map( + ([key, value]) => + `${encodeURIComponent(key)}=${encodeURIComponent(value)}`, + ) + .join('&'); + } + const response = await fetch(url); + if (!response.ok) { + const payload = await response.text(); + throw new Error( + `Request failed with ${response.status} ${response.statusText}, ${payload}`, + ); + } + return await response.json(); } async getEntityByName(name: string): Promise { diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index 7d6c1d2b47..1cfe5736fb 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -24,9 +24,8 @@ export const catalogApiRef = createApiRef({ export interface CatalogApi { getLocationById(id: String): Promise; - getEntities(): Promise; + getEntities(filter?: Record): Promise; getEntityByName(name: string): Promise; - getEntitiesByLocationId(id: string): Promise; addLocation(type: string, target: 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 ac8de59bd9..0a8468efef 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -14,35 +14,33 @@ * limitations under the License. */ -import React, { FC, useCallback, useState } from 'react'; import { Content, ContentHeader, DismissableBanner, Header, + HeaderTabs, HomepageTimer, - SupportButton, Page, pageTheme, + SupportButton, useApi, - HeaderTabs, } from '@backstage/core'; +import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder'; +import { Button, Link, makeStyles, Typography } from '@material-ui/core'; +import GitHub from '@material-ui/icons/GitHub'; +import React, { FC, useCallback, useState } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; import { useAsync } from 'react-use'; -import CatalogTable from '../CatalogTable/CatalogTable'; +import { catalogApiRef } from '../..'; +import { Component } from '../../data/component'; +import { defaultFilter, filterGroups } from '../../data/filters'; +import { entityToComponent, findLocationForEntity } from '../../data/utils'; import { CatalogFilter, CatalogFilterItem, } from '../CatalogFilter/CatalogFilter'; -import { Button, makeStyles, Typography, Link } from '@material-ui/core'; -import { filterGroups, defaultFilter } from '../../data/filters'; -import { Link as RouterLink } from 'react-router-dom'; -import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder'; -import GitHub from '@material-ui/icons/GitHub'; -import { - Entity, - Location, - LOCATION_ANNOTATION, -} from '@backstage/catalog-model'; +import CatalogTable from '../CatalogTable/CatalogTable'; const useStyles = makeStyles(theme => ({ contentWrapper: { @@ -57,10 +55,6 @@ const useStyles = makeStyles(theme => ({ }, })); -import { catalogApiRef } from '../..'; -import { entityToComponent, findLocationForEntity } from '../../data/utils'; -import { Component } from '../../data/component'; - const CatalogPage: FC<{}> = () => { const catalogApi = useApi(catalogApiRef); const { value, error, loading } = useAsync(() => catalogApi.getEntities()); @@ -74,26 +68,6 @@ const CatalogPage: FC<{}> = () => { ); const styles = useStyles(); - const { value: locations } = useAsync(async () => { - const getLocationDataForEntities = async (entities: Entity[]) => { - return Promise.all( - entities.map(entity => { - const locationId = entity.metadata.annotations?.[LOCATION_ANNOTATION]; - if (!locationId) return undefined; - - return catalogApi.getLocationById(locationId); - }), - ); - }; - - if (value) { - return getLocationDataForEntities(value).then( - (location): Location[] => - location.filter(loc => !!loc) as Array, - ); - } - return []; - }, [value, catalogApi, catalogApi]); const actions = [ (rowData: Component) => ({ icon: GitHub, @@ -171,24 +145,22 @@ const CatalogPage: FC<{}> = () => { onSelectedChange={onFilterSelected} /> - {locations && ( - { - return { - ...entityToComponent(val), - location: findLocationForEntity(val, locations), - }; - })) || - [] - } - loading={loading} - error={error} - actions={actions} - /> - )} + { + return { + ...entityToComponent(val), + locationSpec: findLocationForEntity(val), + }; + })) || + [] + } + loading={loading} + error={error} + actions={actions} + /> diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 7ff521ee67..704fc68eff 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { FC } from 'react'; -import { Component } from '../../data/component'; import { InfoCard, Progress, Table, TableColumn } from '@backstage/core'; -import { Typography, Link } from '@material-ui/core'; -import { Link as RouterLink, generatePath } from 'react-router-dom'; +import { Link, Typography } from '@material-ui/core'; +import React, { FC } from 'react'; +import { generatePath, Link as RouterLink } from 'react-router-dom'; +import { Component } from '../../data/component'; import { entityRoute } from '../../routes'; const columns: TableColumn[] = [ @@ -51,6 +51,7 @@ type CatalogTableProps = { error?: any; actions?: any; }; + const CatalogTable: FC = ({ components, loading, @@ -60,16 +61,17 @@ const CatalogTable: FC = ({ }) => { if (loading) { return ; - } - if (error) { + } else if (error) { return ( Error encountered while fetching components. + {error} ); } + return ( = ({ /> ); }; + export default CatalogTable; diff --git a/plugins/catalog/src/components/ComponentContextMenu/ComponentContextMenu.tsx b/plugins/catalog/src/components/ComponentContextMenu/ComponentContextMenu.tsx index 674d979f83..7b3729f841 100644 --- a/plugins/catalog/src/components/ComponentContextMenu/ComponentContextMenu.tsx +++ b/plugins/catalog/src/components/ComponentContextMenu/ComponentContextMenu.tsx @@ -81,7 +81,7 @@ const ComponentContextMenu: FC = ({ - More repository + Move repository diff --git a/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx b/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx index 7dde95c8f0..2c9ccc3eea 100644 --- a/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx +++ b/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { FC } from 'react'; +import { Entity, LOCATION_ANNOTATION } from '@backstage/catalog-model'; +import { Progress, useApi } from '@backstage/core'; import { Button, Dialog, @@ -23,12 +24,15 @@ import { DialogTitle, useMediaQuery, useTheme, + List, + ListItem, + ListItemText, } from '@material-ui/core'; -import { Component } from '../../data/component'; +import React, { FC } from 'react'; import { useAsync } from 'react-use'; -import { useApi } from '@backstage/core'; +import { AsyncState } from 'react-use/lib/useAsync'; import { catalogApiRef } from '../../api/types'; -import { Entity } from '@backstage/catalog-model'; +import { Component } from '../../data/component'; type ComponentRemovalDialogProps = { onConfirm: () => any; @@ -36,44 +40,81 @@ type ComponentRemovalDialogProps = { onClose: () => any; component: Component; }; + +function useColocatedEntities(component: Component): AsyncState { + const catalogApi = useApi(catalogApiRef); + return useAsync(async () => { + const myLocation = component.metadata.annotations?.[LOCATION_ANNOTATION]; + return myLocation + ? await catalogApi.getEntities({ [LOCATION_ANNOTATION]: myLocation }) + : []; + }, [catalogApi, component]); +} + const ComponentRemovalDialog: FC = ({ onConfirm, onCancel, 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 { value: entities, loading, error } = useColocatedEntities(component); 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? - {infoMessage} + {loading ? : null} + {error ? ( + {error.toString()} + ) : null} + {entities ? ( + <> + + This action will unregister the following entities: + + + {entities.map(e => ( + + + + ))} + + + That are located at the following location: + + + + + + + + To undo, just re-register the component in Backstage. + + + ) : null} - ); }; + export default ComponentRemovalDialog; diff --git a/plugins/catalog/src/data/component.ts b/plugins/catalog/src/data/component.ts index 73b349391b..5827c05930 100644 --- a/plugins/catalog/src/data/component.ts +++ b/plugins/catalog/src/data/component.ts @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { EntityMeta, Location } from '@backstage/catalog-model'; import { ReactNode } from 'react'; -import { Location } from '@backstage/catalog-model'; export type Component = { name: string; kind: string; + metadata: EntityMeta; description: ReactNode; location?: Location; }; diff --git a/plugins/catalog/src/data/utils.tsx b/plugins/catalog/src/data/utils.tsx index 93d28f5ca2..a3dca7b6e1 100644 --- a/plugins/catalog/src/data/utils.tsx +++ b/plugins/catalog/src/data/utils.tsx @@ -19,6 +19,7 @@ import { Entity, Location, LOCATION_ANNOTATION, + LocationSpec, } from '@backstage/catalog-model'; import Edit from '@material-ui/icons/Edit'; import IconButton from '@material-ui/core/IconButton'; @@ -38,13 +39,12 @@ const createEditLink = (location: Location): string => { } }; -export function entityToComponent( - envelope: Entity, - location?: Location, -): Component { +export function entityToComponent(envelope: Entity): Component { + const location = findLocationForEntity(envelope); return { name: envelope.metadata?.name ?? '', kind: envelope.kind ?? 'unknown', + metadata: envelope.metadata, description: ( {envelope.metadata?.annotations?.description ?? 'placeholder'} @@ -57,18 +57,25 @@ export function entityToComponent( ) : null} ), - location, + location: findLocationForEntity(envelope), }; } export function findLocationForEntity( entity: Entity, - locations: Location[], -): Location | undefined { - for (const loc of locations) { - if (loc.id === entity.metadata.annotations?.[LOCATION_ANNOTATION]) { - return loc; - } +): LocationSpec | undefined { + const annotation = entity.metadata.annotations?.[LOCATION_ANNOTATION]; + if (!annotation) { + return undefined; } - return undefined; + + const separatorIndex = annotation.indexOf(':'); + if (separatorIndex === -1) { + return undefined; + } + + return { + type: annotation.substring(0, separatorIndex), + target: annotation.substring(separatorIndex + 1), + }; }