Merge pull request #7149 from backstage/rugvip/entity-context

catalog-react: cross-version compatibility for entity context
This commit is contained in:
Patrik Oldsberg
2021-09-14 22:53:13 +02:00
committed by GitHub
29 changed files with 504 additions and 347 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': patch
---
Deprecated `Router` in favor of using the plugin extensions.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': patch
---
Deprecated EntityContext in favor of using `useEntity`, `EntityProvider` and the new `AsyncEntityProvider` instead. This update also brings cross-version compatibility to `@backstage/catalog-react`, meaning that future versions can be used in parallel with this one.
+33 -8
View File
@@ -25,6 +25,29 @@ import { SystemEntity } from '@backstage/catalog-model';
import { TableColumn } from '@backstage/core-components';
import { UserEntity } from '@backstage/catalog-model';
// @public
export const AsyncEntityProvider: ({
children,
entity,
loading,
error,
refresh,
}: AsyncEntityProviderProps) => JSX.Element;
// @public
export interface AsyncEntityProviderProps {
// (undocumented)
children: ReactNode;
// (undocumented)
entity?: Entity;
// (undocumented)
error?: Error;
// (undocumented)
loading: boolean;
// (undocumented)
refresh?: VoidFunction;
}
export { CATALOG_FILTER_EXISTS };
export { CatalogApi };
@@ -109,9 +132,8 @@ export type DefaultEntityFilters = {
};
// Warning: (ae-forgotten-export) The symbol "EntityLoadingStatus" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "EntityContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public @deprecated (undocumented)
export const EntityContext: Context<EntityLoadingStatus>;
// Warning: (ae-missing-release-tag) "EntityFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -199,15 +221,20 @@ export class EntityOwnerFilter implements EntityFilter {
// @public (undocumented)
export const EntityOwnerPicker: () => JSX.Element | null;
// Warning: (ae-forgotten-export) The symbol "EntityProviderProps" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "EntityProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public
export const EntityProvider: ({
entity,
children,
}: EntityProviderProps) => JSX.Element;
// @public
export interface EntityProviderProps {
// (undocumented)
children: ReactNode;
// (undocumented)
entity?: Entity;
}
// Warning: (ae-forgotten-export) The symbol "EntityRefLinkProps" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "EntityRefLink" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -726,8 +753,6 @@ export const UnregisterEntityDialog: ({
entity,
}: Props_3) => JSX.Element;
// Warning: (ae-missing-release-tag) "useEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export function useEntity<T extends Entity = Entity>(): {
entity: T;
+1
View File
@@ -34,6 +34,7 @@
"@backstage/core-components": "^0.4.0",
"@backstage/core-plugin-api": "^0.1.6",
"@backstage/integration": "^0.6.3",
"@backstage/version-bridge": "^0.1.0",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -1,35 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import React, { ReactNode } from 'react';
import { EntityContext } from '../../hooks';
type EntityProviderProps = {
entity: Entity;
children: ReactNode;
};
export const EntityProvider = ({ entity, children }: EntityProviderProps) => (
<EntityContext.Provider
value={{
entity,
loading: !Boolean(entity),
error: undefined,
}}
>
{children}
</EntityContext.Provider>
);
@@ -1,16 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { EntityProvider } from './EntityProvider';
@@ -16,7 +16,6 @@
export * from './EntityKindPicker';
export * from './EntityLifecyclePicker';
export * from './EntityOwnerPicker';
export * from './EntityProvider';
export * from './EntityRefLink';
export * from './EntitySearchBar';
export * from './EntityTable';
+11 -1
View File
@@ -13,7 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { EntityContext, useEntity, useEntityFromUrl } from './useEntity';
export {
EntityContext,
useEntity,
useEntityFromUrl,
EntityProvider,
AsyncEntityProvider,
} from './useEntity';
export type {
EntityProviderProps,
AsyncEntityProviderProps,
} from './useEntity';
export { useEntityCompoundName } from './useEntityCompoundName';
export {
EntityListContext,
@@ -0,0 +1,140 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { renderHook } from '@testing-library/react-hooks';
import {
useEntity,
EntityProvider,
AsyncEntityProvider,
EntityContext,
} from './useEntity';
import { Entity } from '@backstage/catalog-model';
describe('EntityProvider', () => {
it('should provide no entity', async () => {
const { result } = renderHook(() => useEntity(), {
wrapper: ({ children }) => <EntityProvider children={children} />,
});
expect(result.current.entity).toBe(undefined);
expect(result.current.loading).toBe(true);
expect(result.current.error).toBe(undefined);
expect(result.current.refresh).toBe(undefined);
});
it('should provide an entity', async () => {
const entity = { kind: 'MyEntity' } as Entity;
const { result } = renderHook(() => useEntity(), {
wrapper: ({ children }) => (
<EntityProvider entity={entity} children={children} />
),
});
expect(result.current.entity).toBe(entity);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBe(undefined);
expect(result.current.refresh).toBe(undefined);
});
});
describe('AsyncEntityProvider', () => {
it('should provide no entity', async () => {
const { result } = renderHook(() => useEntity(), {
wrapper: ({ children }) => (
<AsyncEntityProvider loading={false} children={children} />
),
});
expect(result.current.entity).toBe(undefined);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBe(undefined);
expect(result.current.refresh).toBe(undefined);
});
it('should provide an entity', async () => {
const entity = { kind: 'MyEntity' } as Entity;
const refresh = () => {};
const { result } = renderHook(() => useEntity(), {
wrapper: ({ children }) => (
<AsyncEntityProvider
loading={false}
entity={entity}
refresh={refresh}
children={children}
/>
),
});
expect(result.current.entity).toBe(entity);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBe(undefined);
expect(result.current.refresh).toBe(refresh);
});
it('should provide an error', async () => {
const error = new Error('oh no');
const { result } = renderHook(() => useEntity(), {
wrapper: ({ children }) => (
<AsyncEntityProvider
loading={false}
error={error}
children={children}
/>
),
});
expect(result.current.entity).toBe(undefined);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBe(error);
expect(result.current.refresh).toBe(undefined);
});
});
describe('EntityContext.Provider', () => {
it('should provide no entity', async () => {
const { result } = renderHook(() => useEntity(), {
wrapper: ({ children }) => (
<EntityContext.Provider
value={{ loading: false }}
children={children}
/>
),
});
expect(result.current.entity).toBe(undefined);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBe(undefined);
expect(result.current.refresh).toBe(undefined);
});
it('should provide an entity', async () => {
const entity = { kind: 'MyEntity' } as Entity;
const { result } = renderHook(() => useEntity(), {
wrapper: ({ children }) => (
<EntityContext.Provider
value={{ entity, loading: false }}
children={children}
/>
),
});
expect(result.current.entity).toBe(entity);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBe(undefined);
expect(result.current.refresh).toBe(undefined);
});
});
@@ -1,70 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
import { createContext, useContext, useEffect } from 'react';
import { useNavigate } from 'react-router';
import { useAsyncRetry } from 'react-use';
import { catalogApiRef } from '../api';
import { useEntityCompoundName } from './useEntityCompoundName';
type EntityLoadingStatus = {
entity?: Entity;
loading: boolean;
error?: Error;
refresh?: VoidFunction;
};
export const EntityContext = createContext<EntityLoadingStatus>({
entity: undefined,
loading: true,
error: undefined,
refresh: () => {},
});
export const useEntityFromUrl = (): EntityLoadingStatus => {
const { kind, namespace, name } = useEntityCompoundName();
const navigate = useNavigate();
const errorApi = useApi(errorApiRef);
const catalogApi = useApi(catalogApiRef);
const {
value: entity,
error,
loading,
retry: refresh,
} = useAsyncRetry(
() => catalogApi.getEntityByName({ kind, namespace, name }),
[catalogApi, kind, namespace, name],
);
useEffect(() => {
if (!name) {
errorApi.post(new Error('No name provided!'));
navigate('/');
}
}, [errorApi, navigate, error, loading, entity, name]);
return { entity, loading, error, refresh };
};
/**
* Grab the current entity from the context and its current loading state.
*/
export function useEntity<T extends Entity = Entity>() {
const { entity, loading, error, refresh } = useContext(EntityContext);
return { entity: entity as T, loading, error, refresh };
}
@@ -0,0 +1,190 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
import {
createVersionedContext,
createVersionedValueMap,
useVersionedContext,
} from '@backstage/version-bridge';
import React, {
ReactNode,
useEffect,
createContext,
Provider,
Context,
} from 'react';
import { useNavigate } from 'react-router';
import { useAsyncRetry } from 'react-use';
import { catalogApiRef } from '../api';
import { useEntityCompoundName } from './useEntityCompoundName';
type EntityLoadingStatus = {
entity?: Entity;
loading: boolean;
error?: Error;
refresh?: VoidFunction;
};
/**
* @public
* @deprecated use `useEntity` and `EntityProvider` or `AsyncEntityProvider` instead.
*/
export const EntityContext: Context<EntityLoadingStatus> =
createContext<EntityLoadingStatus>({
entity: undefined,
loading: true,
error: undefined,
refresh: () => {},
});
// We grab this for use in the new provider, since we're overriding it later on
const OldEntityProvider = EntityContext.Provider;
// This context has support for multiple concurrent versions of this package.
// It is currently used in parallel with the old context in order to provide
// a smooth transition, but will eventually be the only context we use.
const NewEntityContext =
createVersionedContext<{ 1: EntityLoadingStatus }>('entity-context');
/**
* Properties for the AsyncEntityProvider component.
*
* @public
*/
export interface AsyncEntityProviderProps {
children: ReactNode;
entity?: Entity;
loading: boolean;
error?: Error;
refresh?: VoidFunction;
}
/**
* Provides a loaded entity to be picked up by the `useEntity` hook.
*
* @public
*/
export const AsyncEntityProvider = ({
children,
entity,
loading,
error,
refresh,
}: AsyncEntityProviderProps) => {
const value = { entity, loading, error, refresh };
// We provide both the old and the new context, since
// consumers might be doing things like `useContext(EntityContext)`
return (
<OldEntityProvider value={value}>
<NewEntityContext.Provider value={createVersionedValueMap({ 1: value })}>
{children}
</NewEntityContext.Provider>
</OldEntityProvider>
);
};
/**
* Properties for the EntityProvider component.
*
* @public
*/
export interface EntityProviderProps {
children: ReactNode;
entity?: Entity;
}
/**
* Provides an entity to be picked up by the `useEntity` hook.
*
* @public
*/
export const EntityProvider = ({ entity, children }: EntityProviderProps) => (
<AsyncEntityProvider
entity={entity}
loading={!Boolean(entity)}
error={undefined}
refresh={undefined}
children={children}
/>
);
// This is used for forwards compatibility with the new entity context
const CompatibilityProvider = ({
value,
children,
}: {
value: EntityLoadingStatus;
children: ReactNode;
}) => {
return <AsyncEntityProvider {...value} children={children} />;
};
EntityContext.Provider = CompatibilityProvider as Provider<EntityLoadingStatus>;
export const useEntityFromUrl = (): EntityLoadingStatus => {
const { kind, namespace, name } = useEntityCompoundName();
const navigate = useNavigate();
const errorApi = useApi(errorApiRef);
const catalogApi = useApi(catalogApiRef);
const {
value: entity,
error,
loading,
retry: refresh,
} = useAsyncRetry(
() => catalogApi.getEntityByName({ kind, namespace, name }),
[catalogApi, kind, namespace, name],
);
useEffect(() => {
if (!name) {
errorApi.post(new Error('No name provided!'));
navigate('/');
}
}, [errorApi, navigate, error, loading, entity, name]);
return { entity, loading, error, refresh };
};
/**
* Grab the current entity from the context and its current loading state.
*
* @public
*/
export function useEntity<T extends Entity = Entity>() {
const versionedHolder =
useVersionedContext<{ 1: EntityLoadingStatus }>('entity-context');
if (!versionedHolder) {
// TODO(Rugvip): Throw this once we fully migrate to the new context
// throw new Error('Entity context is not available');
return {
entity: undefined as unknown as T,
loading: true,
error: undefined,
refresh: () => {},
};
}
const value = versionedHolder.atVersion(1);
if (!value) {
throw new Error('EntityContext v1 not available');
}
const { entity, loading, error, refresh } = value;
return { entity: entity as T, loading, error, refresh };
}
+1 -1
View File
@@ -401,7 +401,7 @@ export const isOrphan: (entity: Entity) => boolean;
// Warning: (ae-missing-release-tag) "Router" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public @deprecated (undocumented)
export const Router: ({
EntityPage,
}: {
@@ -16,12 +16,13 @@
import React from 'react';
import { Outlet } from 'react-router';
import { EntityLoaderProvider } from '../EntityLoaderProvider';
import {
useEntityFromUrl,
AsyncEntityProvider,
} from '@backstage/plugin-catalog-react';
export const CatalogEntityPage = () => {
return (
<EntityLoaderProvider>
<Outlet />
</EntityLoaderProvider>
);
};
export const CatalogEntityPage = () => (
<AsyncEntityProvider {...useEntityFromUrl()}>
<Outlet />
</AsyncEntityProvider>
);
@@ -15,7 +15,11 @@
*/
import { CatalogApi } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import { catalogApiRef, EntityContext } from '@backstage/plugin-catalog-react';
import {
catalogApiRef,
EntityProvider,
AsyncEntityProvider,
} from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import { fireEvent } from '@testing-library/react';
import React from 'react';
@@ -26,16 +30,12 @@ import { EntityLayout } from './EntityLayout';
import { AlertApi, alertApiRef } from '@backstage/core-plugin-api';
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
const mockEntityData = {
loading: false,
error: undefined,
entity: {
kind: 'MyKind',
metadata: {
name: 'my-entity',
},
} as Entity,
};
const mockEntity = {
kind: 'MyKind',
metadata: {
name: 'my-entity',
},
} as Entity;
const mockApis = ApiRegistry.with(catalogApiRef, {} as CatalogApi).with(
alertApiRef,
@@ -46,13 +46,13 @@ describe('EntityLayout', () => {
it('renders simplest case', async () => {
const rendered = await renderInTestApp(
<ApiProvider apis={mockApis}>
<EntityContext.Provider value={mockEntityData}>
<EntityProvider entity={mockEntity}>
<EntityLayout>
<EntityLayout.Route path="/" title="tabbed-test-title">
<div>tabbed-test-content</div>
</EntityLayout.Route>
</EntityLayout>
</EntityContext.Provider>
</EntityProvider>
</ApiProvider>,
);
@@ -92,20 +92,15 @@ describe('EntityLayout', () => {
});
it('renders error message when entity is not found', async () => {
const noEntityData = {
...mockEntityData,
entity: undefined,
};
const rendered = await renderInTestApp(
<ApiProvider apis={mockApis}>
<EntityContext.Provider value={noEntityData}>
<AsyncEntityProvider loading={false}>
<EntityLayout>
<EntityLayout.Route path="/" title="tabbed-test-title">
<div>tabbed-test-content</div>
</EntityLayout.Route>
</EntityLayout>
</EntityContext.Provider>
</AsyncEntityProvider>
</ApiProvider>,
);
@@ -122,7 +117,7 @@ describe('EntityLayout', () => {
path="/*"
element={
<ApiProvider apis={mockApis}>
<EntityContext.Provider value={mockEntityData}>
<EntityProvider entity={mockEntity}>
<EntityLayout>
<EntityLayout.Route path="/" title="tabbed-test-title">
<div>tabbed-test-content</div>
@@ -134,7 +129,7 @@ describe('EntityLayout', () => {
<div>tabbed-test-content-2</div>
</EntityLayout.Route>
</EntityLayout>
</EntityContext.Provider>
</EntityProvider>
</ApiProvider>
}
/>
@@ -159,7 +154,7 @@ describe('EntityLayout', () => {
const rendered = await renderInTestApp(
<ApiProvider apis={mockApis}>
<EntityContext.Provider value={mockEntityData}>
<EntityProvider entity={mockEntity}>
<EntityLayout>
<EntityLayout.Route path="/" title="tabbed-test-title">
<div>tabbed-test-content</div>
@@ -179,7 +174,7 @@ describe('EntityLayout', () => {
<div>tabbed-test-content-3</div>
</EntityLayout.Route>
</EntityLayout>
</EntityContext.Provider>
</EntityProvider>
</ApiProvider>,
);
@@ -15,7 +15,7 @@
*/
import { Entity, EntityLink } from '@backstage/catalog-model';
import { EntityContext } from '@backstage/plugin-catalog-react';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils';
import React from 'react';
import { EntityLinksCard } from './EntityLinksCard';
@@ -40,17 +40,12 @@ describe('EntityLinksCard', () => {
it('should render a link', async () => {
const links: EntityLink[] = [createLink()];
const entityContextValue = {
entity: createEntity(links),
loading: false,
error: undefined,
};
const { queryByText } = await renderWithEffects(
wrapInTestApp(
<EntityContext.Provider value={entityContextValue}>
<EntityProvider entity={createEntity(links)}>
<EntityLinksCard />
</EntityContext.Provider>,
</EntityProvider>,
),
);
@@ -59,17 +54,11 @@ describe('EntityLinksCard', () => {
});
it('should show empty state', async () => {
const entityContextValue = {
entity: createEntity([]),
loading: false,
error: undefined,
};
const { queryByText } = await renderWithEffects(
wrapInTestApp(
<EntityContext.Provider value={entityContextValue}>
<EntityProvider entity={createEntity([])}>
<EntityLinksCard />
</EntityContext.Provider>,
</EntityProvider>,
),
);
@@ -1,30 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
EntityContext,
useEntityFromUrl,
} from '@backstage/plugin-catalog-react';
import React, { ReactNode } from 'react';
export const EntityLoaderProvider = ({ children }: { children: ReactNode }) => {
const { entity, loading, error, refresh } = useEntityFromUrl();
return (
<EntityContext.Provider value={{ entity, loading, error, refresh }}>
{children}
</EntityContext.Provider>
);
};
@@ -1,16 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { EntityLoaderProvider } from './EntityLoaderProvider';
@@ -15,7 +15,7 @@
*/
import { Entity } from '@backstage/catalog-model';
import { EntityContext } from '@backstage/plugin-catalog-react';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { render } from '@testing-library/react';
import React from 'react';
import { isKind } from './conditions';
@@ -46,15 +46,9 @@ describe('EntitySwitch', () => {
const rendered = render(
<Wrapper>
<EntityContext.Provider
value={{
entity: { kind: 'component' } as Entity,
loading: false,
error: undefined,
}}
>
<EntityProvider entity={{ kind: 'component' } as Entity}>
{content}
</EntityContext.Provider>
</EntityProvider>
</Wrapper>,
);
@@ -64,15 +58,9 @@ describe('EntitySwitch', () => {
rendered.rerender(
<Wrapper>
<EntityContext.Provider
value={{
entity: { kind: 'template' } as Entity,
loading: false,
error: undefined,
}}
>
<EntityProvider entity={{ kind: 'template' } as Entity}>
{content}
</EntityContext.Provider>
</EntityProvider>
</Wrapper>,
);
@@ -82,15 +70,9 @@ describe('EntitySwitch', () => {
rendered.rerender(
<Wrapper>
<EntityContext.Provider
value={{
entity: { kind: 'derp' } as Entity,
loading: false,
error: undefined,
}}
>
<EntityProvider entity={{ kind: 'derp' } as Entity}>
{content}
</EntityContext.Provider>
</EntityProvider>
</Wrapper>,
);
@@ -100,20 +82,16 @@ describe('EntitySwitch', () => {
});
it('should switch child when filters switch', () => {
const entityContextValue = {
entity: { kind: 'component' } as Entity,
loading: false,
error: undefined,
};
const entity = { kind: 'component' } as Entity;
const rendered = render(
<Wrapper>
<EntityContext.Provider value={entityContextValue}>
<EntityProvider entity={entity}>
<EntitySwitch>
<EntitySwitch.Case if={isKind('component')} children="A" />
<EntitySwitch.Case children="B" />
</EntitySwitch>
</EntityContext.Provider>
</EntityProvider>
</Wrapper>,
);
@@ -122,12 +100,12 @@ describe('EntitySwitch', () => {
rendered.rerender(
<Wrapper>
<EntityContext.Provider value={entityContextValue}>
<EntityProvider entity={entity}>
<EntitySwitch>
<EntitySwitch.Case if={isKind('template')} children="A" />
<EntitySwitch.Case children="B" />
</EntitySwitch>
</EntityContext.Provider>
</EntityProvider>
</Wrapper>,
);
+12 -4
View File
@@ -15,15 +15,16 @@
*/
import { ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model';
import {
AsyncEntityProvider,
entityRoute,
rootRoute,
useEntity,
useEntityFromUrl,
} from '@backstage/plugin-catalog-react';
import { Link, Typography } from '@material-ui/core';
import React, { ComponentType } from 'react';
import React, { ComponentType, ReactNode } from 'react';
import { Navigate, Route, Routes, useParams } from 'react-router';
import { CatalogPage } from './CatalogPage';
import { EntityLoaderProvider } from './EntityLoaderProvider';
import { EntityNotFound } from './EntityNotFound';
import { EntityPageLayout } from './EntityPageLayout';
import { Content } from '@backstage/core-components';
@@ -73,6 +74,13 @@ const OldEntityRouteRedirect = () => {
);
};
export const EntityLoader = (props: { children: ReactNode }) => (
<AsyncEntityProvider {...useEntityFromUrl()} {...props} />
);
/**
* @deprecated Use plugin extensions instead
* */
export const Router = ({
EntityPage = DefaultEntityPage,
}: {
@@ -83,9 +91,9 @@ export const Router = ({
<Route
path={`${entityRoute.path}`}
element={
<EntityLoaderProvider>
<EntityLoader>
<EntityPageSwitch EntityPage={EntityPage} />
</EntityLoaderProvider>
</EntityLoader>
}
/>
<Route
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { EntityContext } from '@backstage/plugin-catalog-react';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { renderHook } from '@testing-library/react-hooks';
import React, { PropsWithChildren } from 'react';
import { useConsumerGroupsForEntity } from './useConsumerGroupsForEntity';
@@ -23,11 +23,7 @@ describe('useConsumerGroupOffsets', () => {
let entity: Entity;
const wrapper = ({ children }: PropsWithChildren<{}>) => {
return (
<EntityContext.Provider value={{ entity: entity, loading: false }}>
{children}
</EntityContext.Provider>
);
return <EntityProvider entity={entity}>{children}</EntityProvider>;
};
const subject = () => renderHook(useConsumerGroupsForEntity, { wrapper });
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { EntityContext } from '@backstage/plugin-catalog-react';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { renderHook } from '@testing-library/react-hooks';
import { when } from 'jest-when';
import React, { PropsWithChildren } from 'react';
@@ -65,9 +65,7 @@ describe('useConsumerGroupOffsets', () => {
mockKafkaApi,
)}
>
<EntityContext.Provider value={{ entity: entity, loading: false }}>
{children}
</EntityContext.Provider>
<EntityProvider entity={entity}>{children}</EntityProvider>
</ApiProvider>
);
};
@@ -15,7 +15,7 @@
*/
import { Entity } from '@backstage/catalog-model';
import { EntityContext } from '@backstage/plugin-catalog-react';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { lightTheme } from '@backstage/theme';
import { ThemeProvider } from '@material-ui/core';
import { render } from '@testing-library/react';
@@ -77,16 +77,14 @@ describe('<AuditListTableForEntity />', () => {
},
};
const subject = (value = {}) =>
const subject = () =>
render(
<ThemeProvider theme={lightTheme}>
<MemoryRouter>
<ApiProvider apis={apis}>
<EntityContext.Provider
value={{ entity: entity, loading: false, ...value }}
>
<EntityProvider entity={entity}>
<AuditListForEntity />
</EntityContext.Provider>
</EntityProvider>
</ApiProvider>
</MemoryRouter>
</ThemeProvider>,
@@ -15,7 +15,7 @@
*/
import { Entity } from '@backstage/catalog-model';
import { EntityContext } from '@backstage/plugin-catalog-react';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { lightTheme } from '@backstage/theme';
import { ThemeProvider } from '@material-ui/core';
import { render } from '@testing-library/react';
@@ -64,15 +64,13 @@ describe('<LastLighthouseAuditCard />', () => {
},
};
const subject = (value = {}) =>
const subject = () =>
render(
<ThemeProvider theme={lightTheme}>
<MemoryRouter>
<EntityContext.Provider
value={{ entity: entity, loading: false, ...value }}
>
<EntityProvider entity={entity}>
<LastLighthouseAuditCard />
</EntityContext.Provider>
</EntityProvider>
</MemoryRouter>
</ThemeProvider>,
);
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { EntityContext } from '@backstage/plugin-catalog-react';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { renderHook } from '@testing-library/react-hooks';
import React, { PropsWithChildren } from 'react';
import { lighthouseApiRef, WebsiteListResponse } from '../api';
@@ -61,9 +61,7 @@ describe('useWebsiteForEntity', () => {
mockLighthouseApi,
)}
>
<EntityContext.Provider value={{ entity: entity, loading: false }}>
{children}
</EntityContext.Provider>
<EntityProvider entity={entity}>{children}</EntityProvider>
</ApiProvider>
);
};
@@ -15,7 +15,7 @@
*/
import { GroupEntity } from '@backstage/catalog-model';
import { EntityContext } from '@backstage/plugin-catalog-react';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { Grid } from '@material-ui/core';
import React from 'react';
import { MemoryRouter } from 'react-router';
@@ -57,12 +57,12 @@ const defaultEntity: GroupEntity = {
export const Default = () => (
<MemoryRouter>
<EntityContext.Provider value={{ entity: defaultEntity, loading: false }}>
<EntityProvider entity={defaultEntity}>
<Grid container spacing={4}>
<Grid item xs={12} md={4}>
<GroupProfileCard variant="gridItem" />
</Grid>
</Grid>
</EntityContext.Provider>
</EntityProvider>
</MemoryRouter>
);
@@ -16,7 +16,7 @@
import { Entity, GroupEntity } from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
import { catalogApiRef, EntityContext } from '@backstage/plugin-catalog-react';
import { catalogApiRef, EntityProvider } from '@backstage/plugin-catalog-react';
import { Grid } from '@material-ui/core';
import React from 'react';
import { MemoryRouter } from 'react-router';
@@ -105,13 +105,13 @@ const apiRegistry = (items: Entity[]) =>
export const Default = () => (
<MemoryRouter>
<ApiProvider apis={apiRegistry([alice, bob])}>
<EntityContext.Provider value={{ entity: defaultEntity, loading: false }}>
<EntityProvider entity={defaultEntity}>
<Grid container spacing={4}>
<Grid item xs={12} md={6}>
<MembersListCard />
</Grid>
</Grid>
</EntityContext.Provider>
</EntityProvider>
</ApiProvider>
</MemoryRouter>
);
@@ -119,13 +119,13 @@ export const Default = () => (
export const Empty = () => (
<MemoryRouter>
<ApiProvider apis={apiRegistry([])}>
<EntityContext.Provider value={{ entity: defaultEntity, loading: false }}>
<EntityProvider entity={defaultEntity}>
<Grid container spacing={4}>
<Grid item xs={12} md={6}>
<MembersListCard />
</Grid>
</Grid>
</EntityContext.Provider>
</EntityProvider>
</ApiProvider>
</MemoryRouter>
);
@@ -20,7 +20,7 @@ import {
CatalogApi,
catalogApiRef,
catalogRouteRef,
EntityContext,
EntityProvider,
} from '@backstage/plugin-catalog-react';
import { wrapInTestApp } from '@backstage/test-utils';
import {
@@ -91,13 +91,13 @@ const apiRegistry = ApiRegistry.from([[catalogApiRef, catalogApi]]);
export const Default = () =>
wrapInTestApp(
<ApiProvider apis={apiRegistry}>
<EntityContext.Provider value={{ entity: defaultEntity, loading: false }}>
<EntityProvider entity={defaultEntity}>
<Grid container spacing={4}>
<Grid item xs={12} md={6}>
<OwnershipCard />
</Grid>
</Grid>
</EntityContext.Provider>
</EntityProvider>
</ApiProvider>,
{
mountedRoutes: { '/catalog': catalogRouteRef },
@@ -124,15 +124,13 @@ export const Themed = () =>
wrapInTestApp(
<ThemeProvider theme={monochromeTheme}>
<ApiProvider apis={apiRegistry}>
<EntityContext.Provider
value={{ entity: defaultEntity, loading: false }}
>
<EntityProvider entity={defaultEntity}>
<Grid container spacing={4}>
<Grid item xs={12} md={6}>
<OwnershipCard />
</Grid>
</Grid>
</EntityContext.Provider>
</EntityProvider>
</ApiProvider>
</ThemeProvider>,
{
@@ -15,7 +15,7 @@
*/
import { UserEntity } from '@backstage/catalog-model';
import { EntityContext } from '@backstage/plugin-catalog-react';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { Grid } from '@material-ui/core';
import React from 'react';
import { MemoryRouter } from 'react-router';
@@ -55,13 +55,13 @@ const defaultEntity: UserEntity = {
export const Default = () => (
<MemoryRouter>
<EntityContext.Provider value={{ entity: defaultEntity, loading: false }}>
<EntityProvider entity={defaultEntity}>
<Grid container spacing={4}>
<Grid item xs={12} md={4}>
<UserProfileCard variant="gridItem" />
</Grid>
</Grid>
</EntityContext.Provider>
</EntityProvider>
</MemoryRouter>
);
@@ -83,12 +83,12 @@ const noImageEntity: UserEntity = {
export const NoImage = () => (
<MemoryRouter>
<EntityContext.Provider value={{ entity: noImageEntity, loading: false }}>
<EntityProvider entity={noImageEntity}>
<Grid container spacing={4}>
<Grid item xs={12} md={4}>
<UserProfileCard variant="gridItem" />
</Grid>
</Grid>
</EntityContext.Provider>
</EntityProvider>
</MemoryRouter>
);
@@ -16,7 +16,7 @@
import React from 'react';
import { act, fireEvent, render, waitFor } from '@testing-library/react';
import { Entity } from '@backstage/catalog-model';
import { EntityContext } from '@backstage/plugin-catalog-react';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { wrapInTestApp } from '@backstage/test-utils';
import {
splunkOnCallApiRef,
@@ -71,35 +71,27 @@ const apis = ApiRegistry.from([
],
]);
const mockEntityData = {
loading: false,
error: undefined,
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'splunkoncall-test',
annotations: {
'splunk.com/on-call-team': 'test',
},
const mockEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'splunkoncall-test',
annotations: {
'splunk.com/on-call-team': 'test',
},
} as Entity,
};
},
} as Entity;
const mockEntityDataNoIncidents = {
loading: false,
error: undefined,
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'splunkoncall-test',
annotations: {
'splunk.com/on-call-team': 'test-noincidents',
},
const mockEntityNoIncidents = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'splunkoncall-test',
annotations: {
'splunk.com/on-call-team': 'test-noincidents',
},
} as Entity,
};
},
} as Entity;
describe('SplunkOnCallCard', () => {
it('Render splunkoncall', async () => {
@@ -113,9 +105,9 @@ describe('SplunkOnCallCard', () => {
const { getByText, queryByTestId } = render(
wrapInTestApp(
<ApiProvider apis={apis}>
<EntityContext.Provider value={mockEntityDataNoIncidents}>
<EntityProvider entity={mockEntityNoIncidents}>
<EntitySplunkOnCallCard />
</EntityContext.Provider>
</EntityProvider>
</ApiProvider>,
),
);
@@ -136,9 +128,9 @@ describe('SplunkOnCallCard', () => {
const { getByText, queryByTestId } = render(
wrapInTestApp(
<ApiProvider apis={apis}>
<EntityContext.Provider value={mockEntityData}>
<EntityProvider entity={mockEntity}>
<EntitySplunkOnCallCard />
</EntityContext.Provider>
</EntityProvider>
</ApiProvider>,
),
);
@@ -155,9 +147,9 @@ describe('SplunkOnCallCard', () => {
const { getByText, queryByTestId } = render(
wrapInTestApp(
<ApiProvider apis={apis}>
<EntityContext.Provider value={mockEntityData}>
<EntityProvider entity={mockEntity}>
<EntitySplunkOnCallCard />
</EntityContext.Provider>
</EntityProvider>
</ApiProvider>,
),
);
@@ -181,9 +173,9 @@ describe('SplunkOnCallCard', () => {
const { getByText, queryByTestId } = render(
wrapInTestApp(
<ApiProvider apis={apis}>
<EntityContext.Provider value={mockEntityData}>
<EntityProvider entity={mockEntity}>
<EntitySplunkOnCallCard />
</EntityContext.Provider>
</EntityProvider>
</ApiProvider>,
),
);
@@ -204,9 +196,9 @@ describe('SplunkOnCallCard', () => {
const { getByText, queryByTestId, getByRole } = render(
wrapInTestApp(
<ApiProvider apis={apis}>
<EntityContext.Provider value={mockEntityData}>
<EntityProvider entity={mockEntity}>
<EntitySplunkOnCallCard />
</EntityContext.Provider>
</EntityProvider>
</ApiProvider>,
),
);