Merge pull request #9872 from backstage/blam/catalog-deprecations
🧹 tidy up the `useEntity` hook, and introduce `useAsyncEntity`
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,6 +18,7 @@ export {
|
||||
useEntityFromUrl,
|
||||
EntityProvider,
|
||||
AsyncEntityProvider,
|
||||
useAsyncEntity,
|
||||
} from './useEntity';
|
||||
export type {
|
||||
EntityLoadingStatus,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user