Merge pull request #1112 from hooloovooo/catalog-github-link
Catalog GitHub link
This commit is contained in:
@@ -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<Location | undefined> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<CatalogApi>({
|
||||
id: 'plugin.catalog.service',
|
||||
@@ -25,4 +25,5 @@ export const catalogApiRef = createApiRef<CatalogApi>({
|
||||
export interface CatalogApi {
|
||||
getEntities(): Promise<Entity[]>;
|
||||
getEntityByName(name: string): Promise<Entity>;
|
||||
getLocationByEntity(entity: Entity): Promise<Location | undefined>;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<CatalogFilterItem>(
|
||||
const [locations, setLocations] = useState<Location[]>([]);
|
||||
const [selectedFilter, setSelectedFilter] = useState<CatalogFilterItem>(
|
||||
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<Location>,
|
||||
)
|
||||
.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 (
|
||||
<Page theme={pageTheme.home}>
|
||||
<Header title="Service Catalog" subtitle="Keep track of your software">
|
||||
@@ -98,9 +144,19 @@ const CatalogPage: FC<{}> = () => {
|
||||
</div>
|
||||
<CatalogTable
|
||||
titlePreamble={selectedFilter.label}
|
||||
components={(value && value.map(envelopeToComponent)) || []}
|
||||
components={
|
||||
(value &&
|
||||
value.map(val =>
|
||||
envelopeToComponent(
|
||||
val,
|
||||
findLocationForEntity(val, locations),
|
||||
),
|
||||
)) ||
|
||||
[]
|
||||
}
|
||||
loading={loading}
|
||||
error={error}
|
||||
actions={actions}
|
||||
/>
|
||||
</div>
|
||||
</Content>
|
||||
|
||||
@@ -42,12 +42,14 @@ type CatalogTableProps = {
|
||||
titlePreamble: string;
|
||||
loading: boolean;
|
||||
error?: any;
|
||||
actions?: any;
|
||||
};
|
||||
const CatalogTable: FC<CatalogTableProps> = ({
|
||||
components,
|
||||
loading,
|
||||
error,
|
||||
titlePreamble,
|
||||
actions,
|
||||
}) => {
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
@@ -64,9 +66,10 @@ const CatalogTable: FC<CatalogTableProps> = ({
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
options={{ paging: false }}
|
||||
options={{ paging: false, actionsColumnIndex: -1 }}
|
||||
title={`${titlePreamble} (${(components && components.length) || 0})`}
|
||||
data={components}
|
||||
actions={actions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user