catalog-client: change entities interface, add fields support (#3296)

This commit is contained in:
Fredrik Adelöw
2020-11-18 19:58:36 +01:00
committed by GitHub
parent 16bb5a0c8e
commit 717e43de14
19 changed files with 298 additions and 235 deletions
@@ -26,24 +26,26 @@ import { ApiExplorerPage } from './ApiExplorerPage';
describe('ApiCatalogPage', () => {
const catalogApi: Partial<CatalogApi> = {
getEntities: () =>
Promise.resolve([
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'API',
metadata: {
name: 'Entity1',
Promise.resolve({
items: [
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'API',
metadata: {
name: 'Entity1',
},
spec: { type: 'openapi' },
},
spec: { type: 'openapi' },
},
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'API',
metadata: {
name: 'Entity2',
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'API',
metadata: {
name: 'Entity2',
},
spec: { type: 'openapi' },
},
spec: { type: 'openapi' },
},
] as Entity[]),
] as Entity[],
}),
getLocationByEntity: () =>
Promise.resolve({ id: 'id', type: 'github', target: 'url' }),
};
@@ -25,8 +25,8 @@ import { ApiExplorerLayout } from './ApiExplorerLayout';
export const ApiExplorerPage = () => {
const catalogApi = useApi(catalogApiRef);
const { loading, error, value: matchingEntities } = useAsync(() => {
return catalogApi.getEntities({ kind: 'API' });
const { loading, error, value: catalogResponse } = useAsync(() => {
return catalogApi.getEntities({ filter: { kind: 'API' } });
}, [catalogApi]);
return (
@@ -44,7 +44,7 @@ export const ApiExplorerPage = () => {
<SupportButton>All your APIs</SupportButton>
</ContentHeader>
<ApiExplorerTable
entities={matchingEntities!}
entities={catalogResponse?.items ?? []}
loading={loading}
error={error}
/>
@@ -33,30 +33,32 @@ import { ButtonGroup, CatalogFilter } from './CatalogFilter';
describe('Catalog Filter', () => {
const catalogApi: Partial<CatalogApi> = {
getEntities: () =>
Promise.resolve([
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'Entity1',
Promise.resolve({
items: [
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'Entity1',
},
spec: {
owner: 'tools@example.com',
type: 'service',
},
},
spec: {
owner: 'tools@example.com',
type: 'service',
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'Entity2',
},
spec: {
owner: 'not-tools@example.com',
type: 'service',
},
},
},
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'Entity2',
},
spec: {
owner: 'not-tools@example.com',
type: 'service',
},
},
] as Entity[]),
] as Entity[],
}),
};
const identityApi: Partial<IdentityApi> = {
@@ -34,37 +34,39 @@ import { CatalogPage } from './CatalogPage';
describe('CatalogPage', () => {
const catalogApi: Partial<CatalogApi> = {
getEntities: () =>
Promise.resolve([
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'Entity1',
Promise.resolve({
items: [
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'Entity1',
},
spec: {
owner: 'tools@example.com',
type: 'service',
},
},
spec: {
owner: 'tools@example.com',
type: 'service',
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'Entity2',
},
spec: {
owner: 'not-tools@example.com',
type: 'service',
},
},
},
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'Entity2',
},
spec: {
owner: 'not-tools@example.com',
type: 'service',
},
},
] as Entity[]),
] as Entity[],
}),
getLocationByEntity: () =>
Promise.resolve({ id: 'id', type: 'github', target: 'url' }),
};
const testProfile: Partial<ProfileInfo> = {
displayName: 'Display Name',
};
const indentityApi: Partial<IdentityApi> = {
const identityApi: Partial<IdentityApi> = {
getUserId: () => 'tools@example.com',
getProfile: () => testProfile,
};
@@ -75,7 +77,7 @@ describe('CatalogPage', () => {
<ApiProvider
apis={ApiRegistry.from([
[catalogApiRef, catalogApi],
[identityApiRef, indentityApi],
[identityApiRef, identityApi],
[storageApiRef, MockStorageApi.create()],
])}
>
@@ -33,46 +33,48 @@ import { ResultsFilter } from './ResultsFilter';
describe('Results Filter', () => {
const catalogApi: Partial<CatalogApi> = {
getEntities: () =>
Promise.resolve([
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'Entity1',
tags: ['java'],
Promise.resolve({
items: [
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'Entity1',
tags: ['java'],
},
spec: {
owner: 'tools@example.com',
type: 'service',
},
},
spec: {
owner: 'tools@example.com',
type: 'service',
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'Entity2',
},
spec: {
owner: 'not-tools@example.com',
type: 'service',
},
},
},
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'Entity2',
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'Entity3',
tags: ['java', 'test'],
},
spec: {
owner: 'tools@example.com',
type: 'service',
},
},
spec: {
owner: 'not-tools@example.com',
type: 'service',
},
},
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'Entity3',
tags: ['java', 'test'],
},
spec: {
owner: 'tools@example.com',
type: 'service',
},
},
] as Entity[]),
] as Entity[],
}),
};
const indentityApi: Partial<IdentityApi> = {
const identityApi: Partial<IdentityApi> = {
getUserId: () => 'tools@example.com',
};
@@ -82,7 +84,7 @@ describe('Results Filter', () => {
<ApiProvider
apis={ApiRegistry.from([
[catalogApiRef, catalogApi],
[identityApiRef, indentityApi],
[identityApiRef, identityApi],
[storageApiRef, MockStorageApi.create()],
])}
>
@@ -44,9 +44,13 @@ function useColocatedEntities(entity: Entity): AsyncState<Entity[]> {
const catalogApi = useApi(catalogApiRef);
return useAsync(async () => {
const myLocation = entity.metadata.annotations?.[LOCATION_ANNOTATION];
return myLocation
? await catalogApi.getEntities({ [LOCATION_ANNOTATION]: myLocation })
: [];
if (!myLocation) {
return [];
}
const response = await catalogApi.getEntities({
filter: { [LOCATION_ANNOTATION]: myLocation },
});
return response.items;
}, [catalogApi, entity]);
}
@@ -46,9 +46,12 @@ export const EntityFilterGroupsProvider = ({
// The hook that implements the actual context building
function useProvideEntityFilters(): FilterGroupsContext {
const catalogApi = useApi(catalogApiRef);
const [{ value: entities, error }, doReload] = useAsyncFn(() =>
catalogApi.getEntities({ kind: 'Component' }),
);
const [{ value: entities, error }, doReload] = useAsyncFn(async () => {
const response = await catalogApi.getEntities({
filter: { kind: 'Component' },
});
return response.items;
});
const filterGroups = useRef<{
[filterGroupId: string]: FilterGroup;
@@ -49,7 +49,7 @@ describe('useEntityFilterGroup', () => {
});
it('works for an empty set of filters', async () => {
catalogApi.getEntities.mockResolvedValue([]);
catalogApi.getEntities.mockResolvedValue({ items: [] });
const group: FilterGroup = { filters: {} };
const { result, waitFor } = renderHook(
() => useEntityFilterGroup('g1', group),
@@ -60,13 +60,15 @@ describe('useEntityFilterGroup', () => {
});
it('works for a single group', async () => {
catalogApi.getEntities.mockResolvedValue([
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: { name: 'n' },
},
]);
catalogApi.getEntities.mockResolvedValue({
items: [
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: { name: 'n' },
},
],
});
const group: FilterGroup = {
filters: {
f1: e => e.metadata.name === 'n',
@@ -52,7 +52,7 @@ describe('RollbarHome component', () => {
catalogApiRef,
({
async getEntities() {
return [];
return { items: [] };
},
} as Partial<CatalogApi>) as CatalogApi,
],
@@ -28,8 +28,8 @@ export function useRollbarEntities() {
configApi.getString('organization.name');
const { value, loading, error } = useAsync(async () => {
const entities = await catalogApi.getEntities();
return entities.filter(entity => {
const response = await catalogApi.getEntities();
return response.items.filter(entity => {
return !!entity.metadata.annotations?.[ROLLBAR_ANNOTATION];
});
}, [catalogApi]);
@@ -53,10 +53,12 @@ export const ScaffolderPage = () => {
const { data: templates, isValidating, error } = useStaleWhileRevalidate(
'templates/all',
async () =>
catalogApi.getEntities({ kind: 'Template' }) as Promise<
TemplateEntityV1alpha1[]
>,
async () => {
const response = await catalogApi.getEntities({
filter: { kind: 'Template' },
});
return response.items as TemplateEntityV1alpha1[];
},
);
useEffect(() => {
@@ -13,18 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { TemplatePage } from './TemplatePage';
import { wrapInTestApp, renderWithEffects } from '@backstage/test-utils';
import { ApiRegistry, errorApiRef, ApiProvider } from '@backstage/core';
import { scaffolderApiRef, ScaffolderApi } from '../../api';
import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog';
import { mutate } from 'swr';
import { act } from 'react-dom/test-utils';
import { Route, MemoryRouter } from 'react-router';
import { rootRoute } from '../../routes';
import { ThemeProvider } from '@material-ui/core';
import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog';
import { renderInTestApp, renderWithEffects } from '@backstage/test-utils';
import { lightTheme } from '@backstage/theme';
import { ThemeProvider } from '@material-ui/core';
import React from 'react';
import { act } from 'react-dom/test-utils';
import { MemoryRouter, Route } from 'react-router';
import { ScaffolderApi, scaffolderApiRef } from '../../api';
import { rootRoute } from '../../routes';
import { TemplatePage } from './TemplatePage';
const templateMock = {
apiVersion: 'backstage.io/v1alpha1',
@@ -90,48 +89,43 @@ const apis = ApiRegistry.from([
]);
describe('TemplatePage', () => {
afterEach(async () => {
// Cleaning up swr's cache
await act(async () => {
await mutate('templates/test');
});
});
beforeEach(() => jest.resetAllMocks());
it('renders correctly', async () => {
catalogApiMock.getEntities.mockResolvedValueOnce([templateMock]);
const rendered = await renderWithEffects(
wrapInTestApp(
<ApiProvider apis={apis}>
<TemplatePage />
</ApiProvider>,
),
catalogApiMock.getEntities.mockResolvedValueOnce({ items: [templateMock] });
const rendered = await renderInTestApp(
<ApiProvider apis={apis}>
<TemplatePage />
</ApiProvider>,
);
expect(rendered.queryByText('Create a new component')).toBeInTheDocument();
expect(rendered.queryByText('React SSR Template')).toBeInTheDocument();
// await act(async () => await mutate('templates/test'));
});
it('renders spinner while loading', async () => {
let resolve: Function;
const promise = new Promise<any>(res => {
resolve = res;
});
catalogApiMock.getEntities.mockResolvedValueOnce(promise);
const rendered = await renderWithEffects(
wrapInTestApp(
<ApiProvider apis={apis}>
<TemplatePage />
</ApiProvider>,
),
catalogApiMock.getEntities.mockReturnValueOnce(promise);
const rendered = await renderInTestApp(
<ApiProvider apis={apis}>
<TemplatePage />
</ApiProvider>,
);
expect(rendered.queryByText('Create a new component')).toBeInTheDocument();
expect(rendered.queryByTestId('loading-progress')).toBeInTheDocument();
// Need to cleanup the promise or will timeout
resolve!();
act(() => {
resolve!({ items: [] });
});
});
it('navigates away if no template was loaded', async () => {
catalogApiMock.getEntities.mockResolvedValueOnce([]);
catalogApiMock.getEntities.mockResolvedValueOnce({ items: [] });
const rendered = await renderWithEffects(
<ApiProvider apis={apis}>
@@ -27,28 +27,26 @@ import { catalogApiRef } from '@backstage/plugin-catalog';
import { LinearProgress } from '@material-ui/core';
import { IChangeEvent } from '@rjsf/core';
import React, { useState } from 'react';
import { useParams } from 'react-router-dom';
import useStaleWhileRevalidate from 'swr';
import { scaffolderApiRef } from '../../api';
import { JobStatusModal } from '../JobStatusModal';
import { Job } from '../../types';
import { MultistepJsonForm } from '../MultistepJsonForm';
import { Navigate } from 'react-router';
import { useParams } from 'react-router-dom';
import { useAsync } from 'react-use';
import { scaffolderApiRef } from '../../api';
import { rootRoute } from '../../routes';
import { Job } from '../../types';
import { JobStatusModal } from '../JobStatusModal';
import { MultistepJsonForm } from '../MultistepJsonForm';
const useTemplate = (
templateName: string,
catalogApi: typeof catalogApiRef.T,
) => {
const { data, error } = useStaleWhileRevalidate(
`templates/${templateName}`,
async () =>
catalogApi.getEntities({
kind: 'Template',
'metadata.name': templateName,
}) as Promise<TemplateEntityV1alpha1[]>,
);
return { template: data?.[0], loading: !error && !data, error };
const { value, loading, error } = useAsync(async () => {
const response = await catalogApi.getEntities({
filter: { kind: 'Template', 'metadata.name': templateName },
});
return response.items as TemplateEntityV1alpha1[];
});
return { template: value?.[0], loading, error };
};
const OWNER_REPO_SCHEMA = {
@@ -110,7 +108,7 @@ export const TemplatePage = () => {
const handleCreateComplete = async (job: Job) => {
const target = job.metadata.remoteUrl?.replace(
/\.git$/,
// TODO(Rugvip): This is not the location we want. As part of scaffodler v2 we
// TODO(Rugvip): This is not the location we want. As part of scaffolder v2 we
// want this to be more flexible, but before that we might want
// to update all templates to use catalog-info.yaml instead.
'/blob/master/component-info.yaml',
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core-api';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog';
import { wrapInTestApp } from '@backstage/test-utils';
@@ -24,7 +23,7 @@ import { TechDocsHome } from './TechDocsHome';
describe('TechDocs Home', () => {
const catalogApi: Partial<CatalogApi> = {
getEntities: () => Promise.resolve([] as Entity[]),
getEntities: () => Promise.resolve({ items: [] }),
};
const apiRegistry = ApiRegistry.from([[catalogApiRef, catalogApi]]);
@@ -14,19 +14,19 @@
* limitations under the License.
*/
import React from 'react';
import { useAsync } from 'react-use';
import { useNavigate, generatePath } from 'react-router-dom';
import { Grid } from '@material-ui/core';
import {
Content,
Header,
ItemCard,
Page,
Progress,
useApi,
Content,
Page,
Header,
} from '@backstage/core';
import { catalogApiRef } from '@backstage/plugin-catalog';
import { Grid } from '@material-ui/core';
import React from 'react';
import { generatePath, useNavigate } from 'react-router-dom';
import { useAsync } from 'react-use';
import { rootDocsRouteRef } from '../../plugin';
export const TechDocsHome = () => {
@@ -34,8 +34,8 @@ export const TechDocsHome = () => {
const navigate = useNavigate();
const { value, loading, error } = useAsync(async () => {
const entities = await catalogApi.getEntities();
return entities.filter(entity => {
const response = await catalogApi.getEntities();
return response.items.filter(entity => {
return !!entity.metadata.annotations?.['backstage.io/techdocs-ref'];
});
});