diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts index 42ef1c4da5..d6d717a085 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 } from '@backstage/catalog-model'; export class CatalogClient implements CatalogApi { private apiOrigin: string; @@ -42,4 +43,23 @@ 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) => { + return e.metadata.annotations + ? e.metadata.annotations['backstage.io/managed-by-location'] + : null; + }; + + const locationId = findLocationIdInEntity(entity); + if (!locationId) return null; + + const response = await fetch( + `${this.apiOrigin}${this.basePath}/locations/${locationId}`, + ); + if (response.ok) { + const location = await response.json(); + if (location) return location; + } + throw new Error(`'Location not found: ${locationId}`); + } } diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index eb2cdca562..845dbe8e83 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -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.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 8a2b448d97..4242f89fe1 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, @@ -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 } from '@backstage/catalog-model'; const useStyles = makeStyles(theme => ({ contentWrapper: { @@ -50,16 +52,52 @@ import { envelopeToComponent } from '../../data/utils'; 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 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.map(l => l.data)) + .then(setLocations); + } + }, [value, catalogApi]); + + const actions = [ + (rowData: any) => ({ + icon: GitHub, + tooltop: 'View on GitHub', + onClick: () => { + if (!rowData || !rowData.location) return; + window.open(rowData.location.target, '_blank'); + }, + isHidden: + rowData && rowData.location + ? rowData.location.type === 'github' + : false, + }), + ]; + + const findLocationForEntity = (entity: Entity, l: any) => { + const entityLocationId = + entity.metadata.annotations?.['backstage.io/managed-by-location']; + return l.find((location: any) => location.id === entityLocationId); + }; + return (
@@ -98,9 +136,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..59359fab16 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..5da962911e 100644 --- a/plugins/catalog/src/data/component.ts +++ b/plugins/catalog/src/data/component.ts @@ -17,4 +17,5 @@ export type Component = { name: string; kind: string; description: string; + location: any; }; diff --git a/plugins/catalog/src/data/utils.ts b/plugins/catalog/src/data/utils.ts index 05fb2fee46..e54b39c7c0 100644 --- a/plugins/catalog/src/data/utils.ts +++ b/plugins/catalog/src/data/utils.ts @@ -16,10 +16,14 @@ import { Component } from './component'; import { Entity } from '@backstage/catalog-model'; -export function envelopeToComponent(envelope: Entity): Component { +export function envelopeToComponent( + envelope: Entity, + location: any, +): Component { return { name: envelope.metadata?.name ?? '', kind: envelope.kind ?? 'unknown', description: envelope.metadata?.annotations?.description ?? 'placeholder', + location, }; }