Merge pull request #7390 from backstage/blam/feat/all-the-errroz
Display errors from Ancestor entities
This commit is contained in:
@@ -34,6 +34,7 @@ describe('CatalogIdentityClient', () => {
|
||||
getLocationByEntity: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
};
|
||||
const tokenIssuer: jest.Mocked<TokenIssuer> = {
|
||||
issueToken: jest.fn(),
|
||||
|
||||
@@ -77,6 +77,7 @@ describe('AwsALBAuthProvider', () => {
|
||||
removeEntityByUid: jest.fn(),
|
||||
getEntityByName: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
};
|
||||
|
||||
const mockRequest = {
|
||||
|
||||
@@ -67,6 +67,7 @@ describe('createRouter', () => {
|
||||
removeLocationById: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
};
|
||||
|
||||
config = new ConfigReader({
|
||||
|
||||
@@ -57,6 +57,7 @@ describe('<CatalogGraphCard/>', () => {
|
||||
addLocation: jest.fn(),
|
||||
removeLocationById: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
};
|
||||
apis = ApiRegistry.with(catalogApiRef, catalog);
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ describe('<CatalogGraphPage/>', () => {
|
||||
addLocation: jest.fn(),
|
||||
removeLocationById: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
};
|
||||
const apis = ApiRegistry.with(catalogApiRef, catalog);
|
||||
|
||||
|
||||
+1
@@ -156,6 +156,7 @@ describe('<EntityRelationsGraph/>', () => {
|
||||
addLocation: jest.fn(),
|
||||
removeLocationById: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
};
|
||||
const apis = ApiRegistry.with(catalogApiRef, catalog);
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ describe('useEntityStore', () => {
|
||||
addLocation: jest.fn(),
|
||||
removeLocationById: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
};
|
||||
|
||||
useApi.mockReturnValue(catalogApi);
|
||||
|
||||
@@ -105,6 +105,7 @@ describe('CatalogImportClient', () => {
|
||||
getLocationById: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
};
|
||||
|
||||
let catalogImportClient: CatalogImportClient;
|
||||
|
||||
+1
@@ -45,6 +45,7 @@ describe('<StepPrepareCreatePullRequest />', () => {
|
||||
removeLocationById: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
};
|
||||
|
||||
const errorApi: jest.Mocked<typeof errorApiRef.T> = {
|
||||
|
||||
@@ -7,10 +7,13 @@
|
||||
|
||||
import { AddLocationRequest } from '@backstage/catalog-client';
|
||||
import { AddLocationResponse } from '@backstage/catalog-client';
|
||||
import { ApiHolder } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
import { CatalogEntitiesRequest } from '@backstage/catalog-client';
|
||||
import { CatalogEntityAncestorsRequest } from '@backstage/catalog-client';
|
||||
import { CatalogEntityAncestorsResponse } from '@backstage/catalog-client';
|
||||
import { CatalogListResponse } from '@backstage/catalog-client';
|
||||
import { CatalogRequestOptions } from '@backstage/catalog-client';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
@@ -68,6 +71,11 @@ export class CatalogClientWrapper implements CatalogApi {
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<CatalogListResponse<Entity>>;
|
||||
// (undocumented)
|
||||
getEntityAncestors(
|
||||
request: CatalogEntityAncestorsRequest,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<CatalogEntityAncestorsResponse>;
|
||||
// (undocumented)
|
||||
getEntityByName(
|
||||
compoundName: EntityName,
|
||||
options?: CatalogRequestOptions,
|
||||
@@ -346,7 +354,7 @@ export const EntityPageLayout: {
|
||||
// Warning: (ae-missing-release-tag) "EntityProcessingErrorsPanel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export const EntityProcessingErrorsPanel: () => JSX.Element;
|
||||
export const EntityProcessingErrorsPanel: () => JSX.Element | null;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "EntitySwitch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
@@ -354,7 +362,14 @@ export const EntityProcessingErrorsPanel: () => JSX.Element;
|
||||
export const EntitySwitch: {
|
||||
({ children }: PropsWithChildren<{}>): JSX.Element | null;
|
||||
Case: (_: {
|
||||
if?: ((entity: Entity) => boolean) | undefined;
|
||||
if?:
|
||||
| ((
|
||||
entity: Entity,
|
||||
context: {
|
||||
apis: ApiHolder;
|
||||
},
|
||||
) => boolean | Promise<boolean>)
|
||||
| undefined;
|
||||
children: ReactNode;
|
||||
}) => null;
|
||||
};
|
||||
@@ -382,7 +397,12 @@ export const FilteredEntityLayout: ({
|
||||
// Warning: (ae-missing-release-tag) "hasCatalogProcessingErrors" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const hasCatalogProcessingErrors: (entity: Entity) => boolean;
|
||||
export const hasCatalogProcessingErrors: (
|
||||
entity: Entity,
|
||||
context: {
|
||||
apis: ApiHolder;
|
||||
},
|
||||
) => Promise<boolean>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "isComponentType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"@backstage/core-plugin-api": "^0.1.9",
|
||||
"@backstage/integration-react": "^0.1.11",
|
||||
"@backstage/plugin-catalog-react": "^0.5.1",
|
||||
"@backstage/errors": "^0.1.2",
|
||||
"@backstage/theme": "^0.2.10",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
CatalogEntitiesRequest,
|
||||
CatalogListResponse,
|
||||
CatalogRequestOptions,
|
||||
CatalogEntityAncestorsRequest,
|
||||
CatalogEntityAncestorsResponse,
|
||||
} from '@backstage/catalog-client';
|
||||
import { IdentityApi } from '@backstage/core-plugin-api';
|
||||
|
||||
@@ -118,4 +120,13 @@ export class CatalogClientWrapper implements CatalogApi {
|
||||
token: options?.token ?? (await this.identityApi.getIdToken()),
|
||||
});
|
||||
}
|
||||
|
||||
async getEntityAncestors(
|
||||
request: CatalogEntityAncestorsRequest,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<CatalogEntityAncestorsResponse> {
|
||||
return await this.client.getEntityAncestors(request, {
|
||||
token: options?.token ?? (await this.identityApi.getIdToken()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+132
-5
@@ -14,14 +14,25 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
CatalogApi,
|
||||
catalogApiRef,
|
||||
EntityProvider,
|
||||
entityRouteRef,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import React from 'react';
|
||||
import { EntityProcessingErrorsPanel } from './EntityProcessingErrorsPanel';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Entity, getEntityName } from '@backstage/catalog-model';
|
||||
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
|
||||
|
||||
describe('<EntityProcessErrors />', () => {
|
||||
const catalogClient: jest.Mocked<CatalogApi> = {
|
||||
getEntityAncestors: jest.fn(),
|
||||
} as any;
|
||||
const apis = ApiRegistry.with(catalogApiRef, catalogClient);
|
||||
|
||||
it('renders EntityProcessErrors if the entity has errors', async () => {
|
||||
const entity: Entity = {
|
||||
apiVersion: 'v1',
|
||||
@@ -86,10 +97,16 @@ describe('<EntityProcessErrors />', () => {
|
||||
},
|
||||
};
|
||||
|
||||
catalogClient.getEntityAncestors.mockResolvedValue({
|
||||
root: getEntityName(entity),
|
||||
items: [{ entity, parents: [] }],
|
||||
});
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<EntityProvider entity={entity}>
|
||||
<EntityProcessingErrorsPanel />
|
||||
</EntityProvider>,
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entity}>
|
||||
<EntityProcessingErrorsPanel />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
expect(
|
||||
@@ -99,5 +116,115 @@ describe('<EntityProcessErrors />', () => {
|
||||
).toBeInTheDocument();
|
||||
expect(getByText('Error: Foo')).toBeInTheDocument();
|
||||
expect(queryByText('Error: This should not be rendered')).toBeNull();
|
||||
expect(
|
||||
queryByText('The error below originates from'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders EntityProcessErrors if the parent entity has errors', async () => {
|
||||
const entity: Entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'software',
|
||||
description: 'This is the description',
|
||||
},
|
||||
spec: {
|
||||
owner: 'guest',
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
},
|
||||
};
|
||||
|
||||
const parent: Entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'parent',
|
||||
description: 'This is the description',
|
||||
},
|
||||
|
||||
spec: {
|
||||
owner: 'guest',
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
},
|
||||
status: {
|
||||
items: [
|
||||
{
|
||||
type: 'backstage.io/catalog-processing',
|
||||
level: 'error',
|
||||
message:
|
||||
'InputError: Policy check failed; caused by Error: Malformed envelope, /metadata/labels should be object',
|
||||
error: {
|
||||
name: 'InputError',
|
||||
message:
|
||||
'Policy check failed; caused by Error: Malformed envelope, /metadata/labels should be object',
|
||||
cause: {
|
||||
name: 'Error',
|
||||
message:
|
||||
'Malformed envelope, /metadata/labels should be object',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'foo',
|
||||
level: 'error',
|
||||
message: 'InputError: This should not be rendered',
|
||||
error: {
|
||||
name: 'InputError',
|
||||
message: 'Foo',
|
||||
cause: {
|
||||
name: 'Error',
|
||||
message:
|
||||
'Malformed envelope, /metadata/labels should be object',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'backstage.io/catalog-processing',
|
||||
level: 'error',
|
||||
message: 'InputError: Foo',
|
||||
error: {
|
||||
name: 'InputError',
|
||||
message: 'Foo',
|
||||
cause: {
|
||||
name: 'Error',
|
||||
message:
|
||||
'Malformed envelope, /metadata/labels should be object',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
catalogClient.getEntityAncestors.mockResolvedValue({
|
||||
root: getEntityName(entity),
|
||||
items: [
|
||||
{ entity, parents: [getEntityName(parent)] },
|
||||
{ entity: parent, parents: [] },
|
||||
],
|
||||
});
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityProvider entity={entity}>
|
||||
<EntityProcessingErrorsPanel />
|
||||
</EntityProvider>
|
||||
</ApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
getByText(
|
||||
'Error: Policy check failed; caused by Error: Malformed envelope, /metadata/labels should be object',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(getByText('Error: Foo')).toBeInTheDocument();
|
||||
expect(queryByText('Error: This should not be rendered')).toBeNull();
|
||||
expect(queryByText('The error below originates from')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
+90
-13
@@ -14,36 +14,113 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity, UNSTABLE_EntityStatusItem } from '@backstage/catalog-model';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
Entity,
|
||||
stringifyEntityRef,
|
||||
UNSTABLE_EntityStatusItem,
|
||||
compareEntityToRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import {
|
||||
catalogApiRef,
|
||||
EntityRefLink,
|
||||
useEntity,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { Box } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { ResponseErrorPanel } from '@backstage/core-components';
|
||||
import { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from '@backstage/catalog-client';
|
||||
import {
|
||||
CatalogApi,
|
||||
ENTITY_STATUS_CATALOG_PROCESSING_TYPE,
|
||||
} from '@backstage/catalog-client';
|
||||
import { useApi, ApiHolder } from '@backstage/core-plugin-api';
|
||||
import { useAsync } from 'react-use';
|
||||
import { SerializedError } from '@backstage/errors';
|
||||
|
||||
const errorfilter = (i: UNSTABLE_EntityStatusItem) =>
|
||||
const errorFilter = (i: UNSTABLE_EntityStatusItem) =>
|
||||
i.error &&
|
||||
i.level === 'error' &&
|
||||
i.type === ENTITY_STATUS_CATALOG_PROCESSING_TYPE;
|
||||
|
||||
export const hasCatalogProcessingErrors = (entity: Entity) =>
|
||||
entity?.status?.items?.filter(errorfilter).length! > 0;
|
||||
type GetOwnAndAncestorsErrorsResponse = {
|
||||
items: {
|
||||
errors: SerializedError[];
|
||||
entity: Entity;
|
||||
}[];
|
||||
};
|
||||
|
||||
async function getOwnAndAncestorsErrors(
|
||||
entityRef: string,
|
||||
catalogApi: CatalogApi,
|
||||
): Promise<GetOwnAndAncestorsErrorsResponse> {
|
||||
const ancestors = await catalogApi.getEntityAncestors({ entityRef });
|
||||
const items = ancestors.items
|
||||
.map(item => {
|
||||
const statuses = item.entity.status?.items ?? [];
|
||||
const errors = statuses
|
||||
.filter(errorFilter)
|
||||
.map(e => e.error)
|
||||
.filter((e): e is SerializedError => Boolean(e));
|
||||
return { errors: errors, entity: item.entity };
|
||||
})
|
||||
.filter(item => item.errors.length > 0);
|
||||
return { items };
|
||||
}
|
||||
|
||||
export const hasCatalogProcessingErrors = async (
|
||||
entity: Entity,
|
||||
context: { apis: ApiHolder },
|
||||
) => {
|
||||
const catalogApi = context.apis.get(catalogApiRef);
|
||||
if (!catalogApi) {
|
||||
throw new Error(`No implementation available for ${catalogApiRef}`);
|
||||
}
|
||||
|
||||
const errors = await getOwnAndAncestorsErrors(
|
||||
stringifyEntityRef(entity),
|
||||
catalogApi,
|
||||
);
|
||||
return errors.items.length > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays a list of errors if the entity is invalid.
|
||||
* Displays a list of errors from the ancestors of the current entity.
|
||||
*/
|
||||
export const EntityProcessingErrorsPanel = () => {
|
||||
const { entity } = useEntity();
|
||||
const catalogProcessingErrors =
|
||||
(entity?.status?.items?.filter(
|
||||
errorfilter,
|
||||
) as Required<UNSTABLE_EntityStatusItem>[]) || [];
|
||||
const entityRef = stringifyEntityRef(entity);
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const { loading, error, value } = useAsync(async () => {
|
||||
return getOwnAndAncestorsErrors(entityRef, catalogApi);
|
||||
}, [entityRef, catalogApi]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Box mb={1}>
|
||||
<ResponseErrorPanel error={error} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading || !value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{catalogProcessingErrors.map(({ error }, index) => (
|
||||
{value.items.map((ancestorError, index) => (
|
||||
<Box key={index} mb={1}>
|
||||
<ResponseErrorPanel error={error} />
|
||||
{!compareEntityToRef(
|
||||
entity,
|
||||
stringifyEntityRef(ancestorError.entity),
|
||||
) && (
|
||||
<Box p={1}>
|
||||
The error below originates from{' '}
|
||||
<EntityRefLink entityRef={ancestorError.entity} />
|
||||
</Box>
|
||||
)}
|
||||
{ancestorError.errors.map((e, i) => (
|
||||
<ResponseErrorPanel key={i} error={e} />
|
||||
))}
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -112,4 +112,63 @@ describe('EntitySwitch', () => {
|
||||
expect(rendered.queryByText('A')).not.toBeInTheDocument();
|
||||
expect(rendered.queryByText('B')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should switch with async condition that is true', async () => {
|
||||
const entity = { kind: 'component' } as Entity;
|
||||
|
||||
const shouldRender = () => Promise.resolve(true);
|
||||
const rendered = render(
|
||||
<Wrapper>
|
||||
<EntityProvider entity={entity}>
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case if={shouldRender} children="A" />
|
||||
<EntitySwitch.Case children="B" />
|
||||
</EntitySwitch>
|
||||
</EntityProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
await expect(rendered.findByText('A')).resolves.toBeInTheDocument();
|
||||
expect(rendered.queryByText('B')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should switch with sync condition that is false', async () => {
|
||||
const entity = { kind: 'component' } as Entity;
|
||||
|
||||
const shouldRender = () => Promise.resolve(false);
|
||||
const rendered = render(
|
||||
<Wrapper>
|
||||
<EntityProvider entity={entity}>
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case if={shouldRender} children="A" />
|
||||
<EntitySwitch.Case if={() => true} children="B" />
|
||||
</EntitySwitch>
|
||||
</EntityProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
await expect(rendered.findByText('B')).resolves.toBeInTheDocument();
|
||||
expect(rendered.queryByText('A')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should switch with sync condition that throws', async () => {
|
||||
const entity = { kind: 'component' } as Entity;
|
||||
|
||||
const shouldRender = () => Promise.reject();
|
||||
const rendered = render(
|
||||
<Wrapper>
|
||||
<EntityProvider entity={entity}>
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case if={shouldRender} children="A" />
|
||||
<EntitySwitch.Case if={() => false} children="B" />
|
||||
<EntitySwitch.Case children="C" />
|
||||
</EntitySwitch>
|
||||
</EntityProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
await expect(rendered.findByText('C')).resolves.toBeInTheDocument();
|
||||
expect(rendered.queryByText('A')).not.toBeInTheDocument();
|
||||
expect(rendered.queryByText('B')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,45 +16,98 @@
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { PropsWithChildren, ReactNode } from 'react';
|
||||
import React, { PropsWithChildren, ReactNode } from 'react';
|
||||
import {
|
||||
attachComponentData,
|
||||
useApiHolder,
|
||||
useElementFilter,
|
||||
ApiHolder,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
const ENTITY_SWITCH_KEY = 'core.backstage.entitySwitch';
|
||||
|
||||
const EntitySwitchCase = (_: {
|
||||
if?: (entity: Entity) => boolean;
|
||||
if?: (
|
||||
entity: Entity,
|
||||
context: { apis: ApiHolder },
|
||||
) => boolean | Promise<boolean>;
|
||||
children: ReactNode;
|
||||
}) => null;
|
||||
|
||||
attachComponentData(EntitySwitchCase, ENTITY_SWITCH_KEY, true);
|
||||
|
||||
type SwitchCase = {
|
||||
if?: (entity: Entity) => boolean;
|
||||
if?: (
|
||||
entity: Entity,
|
||||
context: { apis: ApiHolder },
|
||||
) => boolean | Promise<boolean>;
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
type SwitchCaseResult = {
|
||||
if: boolean | Promise<boolean>;
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
export const EntitySwitch = ({ children }: PropsWithChildren<{}>) => {
|
||||
const { entity } = useEntity();
|
||||
const switchCases = useElementFilter(children, collection =>
|
||||
collection
|
||||
.selectByComponentData({
|
||||
key: ENTITY_SWITCH_KEY,
|
||||
withStrictError: 'Child of EntitySwitch is not an EntitySwitch.Case',
|
||||
})
|
||||
.getElements()
|
||||
.flatMap<SwitchCase>((element: React.ReactElement) => {
|
||||
const { if: condition, children: elementsChildren } = element.props;
|
||||
return [{ if: condition, children: elementsChildren }];
|
||||
}),
|
||||
const apis = useApiHolder();
|
||||
const results = useElementFilter(
|
||||
children,
|
||||
collection =>
|
||||
collection
|
||||
.selectByComponentData({
|
||||
key: ENTITY_SWITCH_KEY,
|
||||
withStrictError: 'Child of EntitySwitch is not an EntitySwitch.Case',
|
||||
})
|
||||
.getElements()
|
||||
.flatMap<SwitchCaseResult>((element: React.ReactElement) => {
|
||||
const { if: condition, children: elementsChildren } =
|
||||
element.props as SwitchCase;
|
||||
return [
|
||||
{
|
||||
if: condition?.(entity, { apis }) ?? true,
|
||||
children: elementsChildren,
|
||||
},
|
||||
];
|
||||
}),
|
||||
[apis, entity],
|
||||
);
|
||||
const hasAsyncCases = results.some(
|
||||
r => typeof r.if === 'object' && 'then' in r.if,
|
||||
);
|
||||
|
||||
const matchingCase = switchCases.find(switchCase =>
|
||||
switchCase.if ? switchCase.if(entity) : true,
|
||||
);
|
||||
return matchingCase?.children ?? null;
|
||||
if (hasAsyncCases) {
|
||||
return <AsyncEntitySwitch results={results} />;
|
||||
}
|
||||
|
||||
return results.find(r => r.if)?.children ?? null;
|
||||
};
|
||||
|
||||
function AsyncEntitySwitch({ results }: { results: SwitchCaseResult[] }) {
|
||||
const { loading, value } = useAsync(async () => {
|
||||
const promises = results.map(
|
||||
async ({ if: condition, children: output }) => {
|
||||
try {
|
||||
if (await condition) {
|
||||
return output;
|
||||
}
|
||||
} catch {
|
||||
/* ignored */
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
);
|
||||
return (await Promise.all(promises)).find(Boolean) ?? null;
|
||||
}, [results]);
|
||||
|
||||
if (loading || !value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
EntitySwitch.Case = EntitySwitchCase;
|
||||
|
||||
@@ -32,6 +32,7 @@ describe('<DefaultExplorePage />', () => {
|
||||
removeEntityByUid: jest.fn(),
|
||||
getEntityByName: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
};
|
||||
|
||||
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
@@ -34,6 +34,7 @@ describe('<DomainExplorerContent />', () => {
|
||||
removeEntityByUid: jest.fn(),
|
||||
getEntityByName: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
};
|
||||
|
||||
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
@@ -33,6 +33,7 @@ describe('<GroupsExplorerContent />', () => {
|
||||
removeEntityByUid: jest.fn(),
|
||||
getEntityByName: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
};
|
||||
|
||||
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
@@ -37,6 +37,7 @@ describe('<FossaPage />', () => {
|
||||
removeEntityByUid: jest.fn(),
|
||||
removeLocationById: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
};
|
||||
const fossaApi: jest.Mocked<FossaApi> = {
|
||||
getFindingSummary: jest.fn(),
|
||||
|
||||
@@ -51,6 +51,7 @@ function mockCatalogClient(entity?: Entity): jest.Mocked<CatalogApi> {
|
||||
removeLocationById: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
};
|
||||
if (entity) {
|
||||
mock.getEntityByName.mockReturnValue(entity);
|
||||
|
||||
Reference in New Issue
Block a user