diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts index 42ef1c4da5..abba8a7523 100644 --- a/plugins/catalog/src/api/CatalogClient.ts +++ b/plugins/catalog/src/api/CatalogClient.ts @@ -16,6 +16,7 @@ import { CatalogApi } from './types'; import { DescriptorEnvelope } from '../types'; +import { Entity, Location } from '@backstage/catalog-model'; export class CatalogClient implements CatalogApi { private apiOrigin: string; @@ -42,4 +43,21 @@ export class CatalogClient implements CatalogApi { if (entity) return entity; 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); + 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; + } + + return undefined; + } } diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index eb2cdca562..9e8dc5fbac 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { createApiRef } from '@backstage/core'; -import { Entity } from '@backstage/catalog-model'; +import { Entity, Location } from '@backstage/catalog-model'; export const catalogApiRef = createApiRef({ id: 'plugin.catalog.service', @@ -25,4 +25,5 @@ export const catalogApiRef = createApiRef({ export interface CatalogApi { getEntities(): Promise; getEntityByName(name: string): Promise; + getLocationByEntity(entity: Entity): Promise; } diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index ec26ce5b28..9ebeb7f546 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -22,7 +22,10 @@ import { wrapInTestApp } from '@backstage/test-utils'; import { catalogApiRef } from '../..'; const errorApi = { post: () => {} }; -const catalogApi = { getEntities: () => Promise.resolve([{ kind: '' }]) }; +const catalogApi = { + getEntities: () => Promise.resolve([{ kind: '', metadata: {} }]), + getLocationByEntity: () => Promise.resolve({ data: {} }), +}; describe('CatalogPage', () => { // this test right now causes some red lines in the log output when running tests diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 8a2b448d97..25d2618a9a 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 } from 'react'; +import React, { FC, useCallback, useState, useEffect } from 'react'; import { Content, ContentHeader, @@ -26,7 +26,7 @@ import { pageTheme, useApi, } from '@backstage/core'; -import { useAsync } from 'react-use'; +import { useAsync, useMountedState } from 'react-use'; import CatalogTable from '../CatalogTable/CatalogTable'; import { CatalogFilter, @@ -34,6 +34,8 @@ import { } from '../CatalogFilter/CatalogFilter'; 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'; const useStyles = makeStyles(theme => ({ contentWrapper: { @@ -46,20 +48,64 @@ const useStyles = makeStyles(theme => ({ import { catalogApiRef } from '../..'; import { envelopeToComponent } from '../../data/utils'; +import { Component } from '../../data/component'; const CatalogPage: FC<{}> = () => { const catalogApi = useApi(catalogApiRef); const { value, error, loading } = useAsync(() => catalogApi.getEntities()); - const [selectedFilter, setSelectedFilter] = React.useState( + const [locations, setLocations] = useState([]); + const [selectedFilter, setSelectedFilter] = useState( defaultFilter, ); + const isMounted = useMountedState(); - const onFilterSelected = React.useCallback( + const onFilterSelected = useCallback( selected => setSelectedFilter(selected), [], ); const styles = useStyles(); + useEffect(() => { + const getLocationDataForEntities = async (entities: Entity[]) => { + return Promise.all( + entities.map(entity => catalogApi.getLocationByEntity(entity)), + ); + }; + + if (value) { + getLocationDataForEntities(value) + .then( + (location): Location[] => + location.filter(l => !!l) as Array, + ) + .then(location => { + if (isMounted()) setLocations(location); + }); + } + }, [value, catalogApi, isMounted]); + + const actions = [ + (rowData: Component) => ({ + icon: GitHub, + tooltip: 'View on GitHub', + onClick: () => { + if (!rowData || !rowData.location) return; + window.open(rowData.location.target, '_blank'); + }, + hidden: + rowData && rowData.location ? rowData.location.type !== 'github' : true, + }), + ]; + + 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 (
@@ -98,9 +144,19 @@ const CatalogPage: FC<{}> = () => { + envelopeToComponent( + val, + findLocationForEntity(val, locations), + ), + )) || + [] + } 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 f5fa45dc7e..015916c3f4 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -42,12 +42,14 @@ type CatalogTableProps = { titlePreamble: string; loading: boolean; error?: any; + actions?: any; }; const CatalogTable: FC = ({ components, loading, error, titlePreamble, + actions, }) => { if (loading) { return ; @@ -64,9 +66,10 @@ const CatalogTable: FC = ({ return ( ); }; diff --git a/plugins/catalog/src/data/component.ts b/plugins/catalog/src/data/component.ts index 57cba03a9a..1a7935a196 100644 --- a/plugins/catalog/src/data/component.ts +++ b/plugins/catalog/src/data/component.ts @@ -13,8 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Location } from '@backstage/catalog-model'; + export type Component = { name: string; kind: string; description: string; + location?: Location; }; diff --git a/plugins/catalog/src/data/utils.ts b/plugins/catalog/src/data/utils.ts index 05fb2fee46..fe2ec62493 100644 --- a/plugins/catalog/src/data/utils.ts +++ b/plugins/catalog/src/data/utils.ts @@ -14,12 +14,16 @@ * limitations under the License. */ import { Component } from './component'; -import { Entity } from '@backstage/catalog-model'; +import { Entity, Location } from '@backstage/catalog-model'; -export function envelopeToComponent(envelope: Entity): Component { +export function envelopeToComponent( + envelope: Entity, + location?: Location, +): Component { return { name: envelope.metadata?.name ?? '', kind: envelope.kind ?? 'unknown', description: envelope.metadata?.annotations?.description ?? 'placeholder', + location, }; }