Merge pull request #9872 from backstage/blam/catalog-deprecations

🧹 tidy up the `useEntity` hook, and introduce `useAsyncEntity`
This commit is contained in:
Ben Lambert
2022-03-02 12:57:58 +01:00
committed by GitHub
9 changed files with 104 additions and 30 deletions
@@ -23,6 +23,7 @@ import {
setupRequestMockHandlers,
renderInTestApp,
} from '@backstage/test-utils';
import { EntityProvider } from '@backstage/plugin-catalog-react';
describe('ExampleComponent', () => {
const server = setupServer();
@@ -39,7 +40,15 @@ describe('ExampleComponent', () => {
it('should render', async () => {
const rendered = await renderInTestApp(
<ThemeProvider theme={lightTheme}>
<AllureReportComponent />
<EntityProvider
entity={{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: { name: 'test' },
}}
>
<AllureReportComponent />
</EntityProvider>
</ThemeProvider>,
);
expect(rendered.getByText('Missing Annotation')).toBeInTheDocument();
+11 -6
View File
@@ -210,8 +210,8 @@ export const EntityListProvider: <EntityFilters extends DefaultEntityFilters>({
}: PropsWithChildren<{}>) => JSX.Element;
// @public (undocumented)
export type EntityLoadingStatus = {
entity?: Entity;
export type EntityLoadingStatus<TEntity extends Entity = Entity> = {
entity?: TEntity;
loading: boolean;
error?: Error;
refresh?: VoidFunction;
@@ -507,11 +507,16 @@ export type UnregisterEntityDialogProps = {
};
// @public
export function useEntity<T extends Entity = Entity>(): {
entity: T;
export function useAsyncEntity<
TEntity extends Entity = Entity,
>(): EntityLoadingStatus<TEntity>;
// @public
export function useEntity<TEntity extends Entity = Entity>(): {
entity: TEntity;
loading: boolean;
error: Error | undefined;
refresh: VoidFunction | undefined;
error?: Error;
refresh?: VoidFunction;
};
// @public @deprecated
+1
View File
@@ -18,6 +18,7 @@ export {
useEntityFromUrl,
EntityProvider,
AsyncEntityProvider,
useAsyncEntity,
} from './useEntity';
export type {
EntityLoadingStatus,
+49 -14
View File
@@ -27,8 +27,8 @@ import { catalogApiRef } from '../api';
import { useEntityCompoundName } from './useEntityCompoundName';
/** @public */
export type EntityLoadingStatus = {
entity?: Entity;
export type EntityLoadingStatus<TEntity extends Entity = Entity> = {
entity?: TEntity;
loading: boolean;
error?: Error;
refresh?: VoidFunction;
@@ -130,24 +130,25 @@ export const useEntityFromUrl = (): EntityLoadingStatus => {
};
/**
* Grab the current entity from the context and its current loading state.
* Grab the current entity from the context, throws if the entity has not yet been loaded
* or is not available.
*
* @public
*/
export function useEntity<T extends Entity = Entity>() {
export function useEntity<TEntity extends Entity = Entity>(): {
entity: TEntity;
/** @deprecated use {@link useAsyncEntity} instead */
loading: boolean;
/** @deprecated use {@link useAsyncEntity} instead */
error?: Error;
/** @deprecated use {@link useAsyncEntity} instead */
refresh?: VoidFunction;
} {
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: () => {},
};
throw new Error('Entity context is not available');
}
const value = versionedHolder.atVersion(1);
@@ -155,6 +156,40 @@ export function useEntity<T extends Entity = Entity>() {
throw new Error('EntityContext v1 not available');
}
if (!value.entity) {
// Once we have removed the additional fields from being returned we can drop this deprecation
// and move to the error instead.
// throw new Error('useEntity hook is being called outside of an EntityLayout where the entity has not been loaded. If this is intentional, please use useAsyncEntity instead.');
// eslint-disable-next-line no-console
console.warn(
'DEPRECATION: useEntity hook is being called outside of an EntityLayout where the entity has not been loaded. If this is intentional, please use useAsyncEntity instead. This warning will be replaced with an error in future releases.',
);
}
const { entity, loading, error, refresh } = value;
return { entity: entity as T, loading, error, refresh };
return { entity: entity as TEntity, loading, error, refresh };
}
/**
* Grab the current entity from the context, provides loading state and errors, and the ability to refresh.
*
* @public
*/
export function useAsyncEntity<
TEntity extends Entity = Entity,
>(): EntityLoadingStatus<TEntity> {
const versionedHolder =
useVersionedContext<{ 1: EntityLoadingStatus }>('entity-context');
if (!versionedHolder) {
throw new Error('Entity context is not available');
}
const value = versionedHolder.atVersion(1);
if (!value) {
throw new Error('EntityContext v1 not available');
}
const { entity, loading, error, refresh } = value;
return { entity: entity as TEntity, loading, error, refresh };
}
@@ -40,7 +40,7 @@ import {
getEntityRelations,
InspectEntityDialog,
UnregisterEntityDialog,
useEntity,
useAsyncEntity,
useEntityCompoundName,
} from '@backstage/plugin-catalog-react';
import { Box, TabProps } from '@material-ui/core';
@@ -177,7 +177,7 @@ export const EntityLayout = (props: EntityLayoutProps) => {
children,
} = props;
const { kind, namespace, name } = useEntityCompoundName();
const { entity, loading, error } = useEntity();
const { entity, loading, error } = useAsyncEntity();
const location = useLocation();
const routes = useElementFilter(
children,
@@ -190,7 +190,9 @@ export const EntityLayout = (props: EntityLayoutProps) => {
})
.getElements<EntityLayoutRouteProps>() // all nodes, element data, maintain structure or not?
.flatMap(({ props: elementProps }) => {
if (elementProps.if && entity && !elementProps.if(entity)) {
if (!entity) {
return [];
} else if (elementProps.if && !elementProps.if(entity)) {
return [];
}
@@ -15,7 +15,7 @@
*/
import { Entity } from '@backstage/catalog-model';
import { useEntity } from '@backstage/plugin-catalog-react';
import { useAsyncEntity } from '@backstage/plugin-catalog-react';
import React, { ReactNode, ReactElement } from 'react';
import {
attachComponentData,
@@ -63,7 +63,7 @@ export interface EntitySwitchProps {
/** @public */
export const EntitySwitch = (props: EntitySwitchProps) => {
const { entity } = useEntity();
const { entity } = useAsyncEntity();
const apis = useApiHolder();
const results = useElementFilter(
props.children,
@@ -75,6 +75,9 @@ export const EntitySwitch = (props: EntitySwitchProps) => {
})
.getElements()
.flatMap<SwitchCaseResult>((element: ReactElement) => {
if (!entity) {
return [];
}
const { if: condition, children: elementsChildren } =
element.props as EntitySwitchCase;
return [
@@ -27,7 +27,7 @@ function strCmp(a: string | undefined, b: string | undefined): boolean {
* @public
*/
export function isKind(kind: string) {
return (entity: Entity) => strCmp(entity?.kind, kind);
return (entity: Entity) => strCmp(entity.kind, kind);
}
/**
@@ -36,7 +36,7 @@ export function isKind(kind: string) {
*/
export function isComponentType(type: string) {
return (entity: Entity) => {
if (!strCmp(entity?.kind, 'component')) {
if (!strCmp(entity.kind, 'component')) {
return false;
}
const componentEntity = entity as ComponentEntity;
@@ -49,5 +49,5 @@ export function isComponentType(type: string) {
* @public
*/
export function isNamespace(namespace: string) {
return (entity: Entity) => strCmp(entity?.metadata?.namespace, namespace);
return (entity: Entity) => strCmp(entity.metadata?.namespace, namespace);
}
+10 -1
View File
@@ -19,6 +19,7 @@ import { Route } from 'react-router';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { todoPlugin, EntityTodoContent } from './plugin';
import { todoApiRef } from './api';
import { EntityProvider } from '@backstage/plugin-catalog-react';
describe('todo', () => {
it('should export plugin', () => {
@@ -47,7 +48,15 @@ describe('todo', () => {
],
]}
>
<Route path="/" element={<EntityTodoContent />} />
<EntityProvider
entity={{
apiVersion: 'backstage/v1alpha1',
kind: 'Component',
metadata: { name: 'Test TODO' },
}}
>
<Route path="/" element={<EntityTodoContent />} />
</EntityProvider>
</TestApiProvider>,
);