Catalog: Add refresh button to AboutCard
Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com> Signed-off-by: Johan Haals <johan.haals@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Updates the `AboutCard` with a refresh button that allows the entity to be scheduled for refresh.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/catalog-client': minor
|
||||
'@backstage/plugin-catalog-react': minor
|
||||
---
|
||||
|
||||
Extends the `CatalogClient` interface with a `refreshEntity` method.
|
||||
@@ -136,6 +136,24 @@ export class CatalogClient implements CatalogApi {
|
||||
);
|
||||
}
|
||||
|
||||
async refreshEntity(entityRef: string, options?: CatalogRequestOptions) {
|
||||
const response = await fetch(
|
||||
`${await this.discoveryApi.getBaseUrl('catalog')}/refresh`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(options?.token && { Authorization: `Bearer ${options?.token}` }),
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ entityRef }),
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
}
|
||||
|
||||
async addLocation(
|
||||
{ type = 'url', target, dryRun, presence }: AddLocationRequest,
|
||||
options?: CatalogRequestOptions,
|
||||
|
||||
@@ -53,6 +53,10 @@ export interface CatalogApi {
|
||||
uid: string,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<void>;
|
||||
refreshEntity(
|
||||
entityRef: string,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<void>;
|
||||
|
||||
// Locations
|
||||
getLocationById(
|
||||
|
||||
@@ -33,6 +33,7 @@ describe('CatalogIdentityClient', () => {
|
||||
getOriginLocationByEntity: jest.fn(),
|
||||
getLocationByEntity: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
};
|
||||
const tokenIssuer: jest.Mocked<TokenIssuer> = {
|
||||
issueToken: jest.fn(),
|
||||
|
||||
@@ -68,7 +68,6 @@ beforeEach(() => {
|
||||
|
||||
describe('AwsALBAuthProvider', () => {
|
||||
const catalogApi = {
|
||||
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
|
||||
addLocation: jest.fn(),
|
||||
removeLocationById: jest.fn(),
|
||||
getEntities: jest.fn(),
|
||||
@@ -77,6 +76,7 @@ describe('AwsALBAuthProvider', () => {
|
||||
getLocationById: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
getEntityByName: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
};
|
||||
|
||||
const mockRequest = {
|
||||
|
||||
@@ -66,6 +66,7 @@ describe('createRouter', () => {
|
||||
getLocationById: jest.fn(),
|
||||
removeLocationById: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
};
|
||||
|
||||
config = new ConfigReader({
|
||||
|
||||
@@ -104,6 +104,7 @@ describe('CatalogImportClient', () => {
|
||||
getLocationByEntity: jest.fn(),
|
||||
getLocationById: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
};
|
||||
|
||||
let catalogImportClient: CatalogImportClient;
|
||||
|
||||
+1
@@ -42,6 +42,7 @@ describe('<StepPrepareCreatePullRequest />', () => {
|
||||
getLocationById: jest.fn(),
|
||||
removeLocationById: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
};
|
||||
|
||||
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
@@ -109,4 +109,13 @@ export class CatalogClientWrapper implements CatalogApi {
|
||||
token: options?.token ?? (await this.identityApi.getIdToken()),
|
||||
});
|
||||
}
|
||||
|
||||
async refreshEntity(
|
||||
entityRef: string,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<void> {
|
||||
return await this.client.refreshEntity(entityRef, {
|
||||
token: options?.token ?? (await this.identityApi.getIdToken()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,13 +24,28 @@ import {
|
||||
ScmIntegrationsApi,
|
||||
scmIntegrationsApiRef,
|
||||
} from '@backstage/integration-react';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
catalogApiRef,
|
||||
EntityProvider,
|
||||
CatalogApi,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { viewTechDocRouteRef } from '../../routes';
|
||||
import { AboutCard } from './AboutCard';
|
||||
|
||||
describe('<AboutCard />', () => {
|
||||
const catalogApi: jest.Mocked<CatalogApi> = {
|
||||
getLocationById: jest.fn(),
|
||||
getEntityByName: jest.fn(),
|
||||
getEntities: jest.fn(),
|
||||
addLocation: jest.fn(),
|
||||
getLocationByEntity: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
} as any;
|
||||
|
||||
it('renders info', async () => {
|
||||
const entity = {
|
||||
apiVersion: 'v1',
|
||||
@@ -62,7 +77,7 @@ describe('<AboutCard />', () => {
|
||||
integrations: {},
|
||||
}),
|
||||
),
|
||||
);
|
||||
).with(catalogApiRef, catalogApi);
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
@@ -109,7 +124,7 @@ describe('<AboutCard />', () => {
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
).with(catalogApiRef, catalogApi);
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
@@ -155,7 +170,7 @@ describe('<AboutCard />', () => {
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
).with(catalogApiRef, catalogApi);
|
||||
|
||||
const { getByTitle } = await renderInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
@@ -188,7 +203,7 @@ describe('<AboutCard />', () => {
|
||||
const apis = ApiRegistry.with(
|
||||
scmIntegrationsApiRef,
|
||||
ScmIntegrationsApi.fromConfig(new ConfigReader({})),
|
||||
);
|
||||
).with(catalogApiRef, catalogApi);
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
@@ -200,6 +215,43 @@ describe('<AboutCard />', () => {
|
||||
expect(getByText('View Source').closest('a')).not.toHaveAttribute('href');
|
||||
});
|
||||
|
||||
it('triggers a refresh', async () => {
|
||||
const entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'software',
|
||||
},
|
||||
spec: {
|
||||
owner: 'guest',
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
},
|
||||
};
|
||||
const apis = ApiRegistry.with(
|
||||
scmIntegrationsApiRef,
|
||||
ScmIntegrationsApi.fromConfig(new ConfigReader({})),
|
||||
).with(catalogApiRef, catalogApi);
|
||||
|
||||
const { getByTitle } = await renderInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entity}>
|
||||
<AboutCard />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
expect(catalogApi.refreshEntity).not.toHaveBeenCalledWith(
|
||||
'component:default/software',
|
||||
);
|
||||
|
||||
userEvent.click(getByTitle('Schedule entity refresh'));
|
||||
|
||||
expect(catalogApi.refreshEntity).toHaveBeenCalledWith(
|
||||
'component:default/software',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders techdocs link', async () => {
|
||||
const entity = {
|
||||
apiVersion: 'v1',
|
||||
@@ -230,7 +282,7 @@ describe('<AboutCard />', () => {
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
).with(catalogApiRef, catalogApi);
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
@@ -278,7 +330,7 @@ describe('<AboutCard />', () => {
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
).with(catalogApiRef, catalogApi);
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
@@ -321,7 +373,7 @@ describe('<AboutCard />', () => {
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
).with(catalogApiRef, catalogApi);
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
ENTITY_DEFAULT_NAMESPACE,
|
||||
RELATION_CONSUMES_API,
|
||||
RELATION_PROVIDES_API,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import {
|
||||
HeaderIconLinkRow,
|
||||
@@ -26,12 +27,13 @@ import {
|
||||
InfoCardVariants,
|
||||
Link,
|
||||
} from '@backstage/core-components';
|
||||
import { useApi, useRouteRef } from '@backstage/core-plugin-api';
|
||||
import { alertApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
ScmIntegrationIcon,
|
||||
scmIntegrationsApiRef,
|
||||
} from '@backstage/integration-react';
|
||||
import {
|
||||
catalogApiRef,
|
||||
getEntityMetadataEditUrl,
|
||||
getEntityRelations,
|
||||
getEntitySourceLocation,
|
||||
@@ -45,10 +47,11 @@ import {
|
||||
IconButton,
|
||||
makeStyles,
|
||||
} from '@material-ui/core';
|
||||
import CachedIcon from '@material-ui/icons/Cached';
|
||||
import DocsIcon from '@material-ui/icons/Description';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import ExtensionIcon from '@material-ui/icons/Extension';
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { viewTechDocRouteRef } from '../../routes';
|
||||
import { AboutContent } from './AboutContent';
|
||||
|
||||
@@ -82,6 +85,8 @@ export function AboutCard({ variant }: AboutCardProps) {
|
||||
const classes = useStyles();
|
||||
const { entity } = useEntity();
|
||||
const scmIntegrationsApi = useApi(scmIntegrationsApiRef);
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const viewTechdocLink = useRouteRef(viewTechDocRouteRef);
|
||||
|
||||
const entitySourceLocation = getEntitySourceLocation(
|
||||
@@ -142,20 +147,34 @@ export function AboutCard({ variant }: AboutCardProps) {
|
||||
cardContentClass = classes.fullHeightCardContent;
|
||||
}
|
||||
|
||||
const refreshEntity = useCallback(async () => {
|
||||
await catalogApi.refreshEntity(stringifyEntityRef(entity));
|
||||
alertApi.post({ message: 'Refresh scheduled', severity: 'info' });
|
||||
}, [catalogApi, alertApi, entity]);
|
||||
|
||||
return (
|
||||
<Card className={cardClass}>
|
||||
<CardHeader
|
||||
title="About"
|
||||
action={
|
||||
<IconButton
|
||||
component={Link}
|
||||
aria-label="Edit"
|
||||
disabled={!entityMetadataEditUrl}
|
||||
title="Edit Metadata"
|
||||
to={entityMetadataEditUrl ?? '#'}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="Refresh"
|
||||
title="Schedule entity refresh"
|
||||
onClick={refreshEntity}
|
||||
>
|
||||
<CachedIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
component={Link}
|
||||
aria-label="Edit"
|
||||
disabled={!entityMetadataEditUrl}
|
||||
title="Edit Metadata"
|
||||
to={entityMetadataEditUrl ?? '#'}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
}
|
||||
subheader={
|
||||
<HeaderIconLinkRow links={[viewInSource, viewInTechDocs, viewApi]} />
|
||||
|
||||
@@ -31,6 +31,7 @@ describe('<DefaultExplorePage />', () => {
|
||||
removeLocationById: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
getEntityByName: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
};
|
||||
|
||||
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
@@ -33,6 +33,7 @@ describe('<DomainExplorerContent />', () => {
|
||||
removeLocationById: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
getEntityByName: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
};
|
||||
|
||||
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
@@ -32,6 +32,7 @@ describe('<GroupsExplorerContent />', () => {
|
||||
removeLocationById: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
getEntityByName: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
};
|
||||
|
||||
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
@@ -32,6 +32,7 @@ describe('<FossaPage />', () => {
|
||||
getOriginLocationByEntity: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
removeLocationById: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
};
|
||||
const fossaApi: jest.Mocked<FossaApi> = {
|
||||
getFindingSummary: jest.fn(),
|
||||
|
||||
@@ -50,6 +50,7 @@ function mockCatalogClient(entity?: Entity): jest.Mocked<CatalogApi> {
|
||||
getLocationById: jest.fn(),
|
||||
removeLocationById: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
};
|
||||
if (entity) {
|
||||
mock.getEntityByName.mockReturnValue(entity);
|
||||
|
||||
Reference in New Issue
Block a user