catalog-client: change entities interface, add fields support (#3296)
This commit is contained in:
@@ -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'];
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user