chore(catalog): clean up ComponentPage, use only Entity

This commit is contained in:
Fredrik Adelöw
2020-06-10 10:25:22 +02:00
parent 36d4dfcfa7
commit 5a128f3f40
5 changed files with 105 additions and 94 deletions
@@ -13,28 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { render } from '@testing-library/react';
import React from 'react';
import { ComponentMetadataCard } from './ComponentMetadataCard';
import { Component } from '../../data/component';
import { render } from '@testing-library/react';
describe('ComponentMetadataCard component', () => {
it('should display component name if provided', async () => {
const testComponent: Component = {
name: 'test',
const testEntity: Entity = {
apiVersion: 'backstage.io/v1beta1',
kind: 'Component',
metadata: { name: 'test' },
description: 'Placeholder',
};
const rendered = await render(
<ComponentMetadataCard loading={false} component={testComponent} />,
<ComponentMetadataCard entity={testEntity} />,
);
expect(await rendered.findByText('test')).toBeInTheDocument();
});
it('should display loader when loading is set to true', async () => {
const rendered = await render(
<ComponentMetadataCard loading component={undefined} />,
);
expect(await rendered.findByRole('progressbar')).toBeInTheDocument();
});
});
@@ -13,29 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InfoCard, Progress, StructuredMetadataTable } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { InfoCard, StructuredMetadataTable } from '@backstage/core';
import React, { FC } from 'react';
import { Component } from '../../data/component';
type Props = {
loading: boolean;
component: Component | undefined;
entity: Entity;
};
export const ComponentMetadataCard: FC<Props> = ({ loading, component }) => {
if (loading) {
return (
<InfoCard title="Metadata">
<Progress />
</InfoCard>
);
}
if (!component) {
return null;
}
return (
<InfoCard title="Metadata">
<StructuredMetadataTable metadata={component} />
</InfoCard>
);
};
export const ComponentMetadataCard: FC<Props> = ({ entity }) => (
<InfoCard title="Metadata">
<StructuredMetadataTable metadata={entity.metadata} />
</InfoCard>
);
@@ -47,8 +47,8 @@ describe('ComponentPage', () => {
[
catalogApiRef,
({
async getEntity() {},
} as unknown) as CatalogApi,
async getEntityByName() {},
} as Partial<CatalogApi>) as CatalogApi,
],
])}
>
@@ -13,23 +13,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import {
Content,
ErrorApi,
errorApiRef,
Header,
HeaderTabs,
Page,
pageTheme,
Progress,
useApi,
} from '@backstage/core';
import { SentryIssuesWidget } from '@backstage/plugin-sentry';
import { Grid } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import React, { FC, useEffect, useState } from 'react';
import { useAsync } from 'react-use';
import { catalogApiRef } from '../..';
import { Component } from '../../data/component';
import { entityToComponent } from '../../data/utils';
import { ComponentContextMenu } from '../ComponentContextMenu/ComponentContextMenu';
import { ComponentMetadataCard } from '../ComponentMetadataCard/ComponentMetadataCard';
import { ComponentRemovalDialog } from '../ComponentRemovalDialog/ComponentRemovalDialog';
@@ -48,48 +49,61 @@ type ComponentPageProps = {
};
};
function headerProps(
kind: string,
namespace: string | undefined,
name: string,
entity: Entity | undefined,
): { headerTitle: string; headerType: string } {
return {
headerTitle: `${name}${namespace ? ` in ${namespace}` : ''}`,
headerType: (() => {
let t = kind.toLowerCase();
if (entity && entity.spec && 'type' in entity.spec) {
t += ' — ';
t += (entity.spec as { type: string }).type.toLowerCase();
}
return t;
})(),
};
}
export const ComponentPage: FC<ComponentPageProps> = ({ match, history }) => {
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
const [removingPending, setRemovingPending] = useState(false);
const showRemovalDialog = () => setConfirmationDialogOpen(true);
const hideRemovalDialog = () => setConfirmationDialogOpen(false);
const { optionalNamespaceAndName, kind } = match.params;
const [name, namespace] = optionalNamespaceAndName.split(':').reverse();
const errorApi = useApi<ErrorApi>(errorApiRef);
const errorApi = useApi(errorApiRef);
const catalogApi = useApi(catalogApiRef);
const { value: component, error, loading } = useAsync<Component>(async () => {
const entity = await catalogApi.getEntityByName({ name, namespace, kind });
if (!entity) {
throw new Error(`No entity found with that name`);
}
const location = await catalogApi.getLocationByEntity(entity);
return { ...entityToComponent(entity), location };
});
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
const { value: entity, error, loading } = useAsync<Entity | undefined>(
() => catalogApi.getEntityByName({ kind, namespace, name }),
[catalogApi, kind, namespace, name],
);
useEffect(() => {
if (error) {
if (!error && !loading && !entity) {
errorApi.post(new Error('Component not found!'));
setTimeout(() => {
history.push('/');
}, REDIRECT_DELAY);
}
}, [error, errorApi, history]);
}, [errorApi, history, error, loading, entity]);
if (name === '') {
if (!name) {
history.push('/catalog');
return null;
}
const removeComponent = async () => {
setConfirmationDialogOpen(false);
setRemovingPending(true);
// await componentFactory.removeComponentByName(componentName);
await catalogApi;
history.push('/');
};
const showRemovalDialog = () => setConfirmationDialogOpen(true);
const hideRemovalDialog = () => setConfirmationDialogOpen(false);
// TODO - Replace with proper tabs implementation
const tabs = [
{
@@ -118,38 +132,56 @@ export const ComponentPage: FC<ComponentPageProps> = ({ match, history }) => {
},
];
const { headerTitle, headerType } = headerProps(
kind,
namespace,
name,
entity,
);
return (
// TODO: Switch theme and type props based on component type (website, library, ...)
<Page theme={pageTheme.service}>
<Header title={component?.name || 'Catalog'} type="Service">
<ComponentContextMenu onUnregisterComponent={showRemovalDialog} />
<Header title={headerTitle} type={headerType}>
{entity && (
<ComponentContextMenu onUnregisterComponent={showRemovalDialog} />
)}
</Header>
<HeaderTabs tabs={tabs} />
{confirmationDialogOpen && component && (
<ComponentRemovalDialog
component={component}
onClose={hideRemovalDialog}
onConfirm={removeComponent}
onCancel={hideRemovalDialog}
/>
{loading && <Progress />}
{error && (
<Content>
<Alert severity="error">{error.toString()}</Alert>
</Content>
)}
{entity && (
<>
<HeaderTabs tabs={tabs} />
<Content>
<Grid container spacing={3} direction="column">
<Grid item>
<ComponentMetadataCard entity={entity} />
</Grid>
<Grid item>
<SentryIssuesWidget
sentryProjectId="sample-sentry-project-id"
statsFor="24h"
/>
</Grid>
</Grid>
</Content>
<ComponentRemovalDialog
open={confirmationDialogOpen}
entity={entity}
onClose={hideRemovalDialog}
onConfirm={removeComponent}
/>
</>
)}
<Content>
<Grid container spacing={3} direction="column">
<Grid item>
<ComponentMetadataCard
loading={loading || removingPending}
component={component}
/>
</Grid>
<Grid item>
<SentryIssuesWidget
sentryProjectId="sample-sentry-project-id"
statsFor="24h"
/>
</Grid>
</Grid>
</Content>
</Page>
);
};
@@ -32,37 +32,36 @@ import React, { FC } from 'react';
import { useAsync } from 'react-use';
import { AsyncState } from 'react-use/lib/useAsync';
import { catalogApiRef } from '../../api/types';
import { Component } from '../../data/component';
type ComponentRemovalDialogProps = {
open: boolean;
onConfirm: () => any;
onCancel: () => any;
onClose: () => any;
component: Component;
entity: Entity;
};
function useColocatedEntities(component: Component): AsyncState<Entity[]> {
function useColocatedEntities(entity: Entity): AsyncState<Entity[]> {
const catalogApi = useApi(catalogApiRef);
return useAsync(async () => {
const myLocation = component.metadata.annotations?.[LOCATION_ANNOTATION];
const myLocation = entity.metadata.annotations?.[LOCATION_ANNOTATION];
return myLocation
? await catalogApi.getEntities({ [LOCATION_ANNOTATION]: myLocation })
: [];
}, [catalogApi, component]);
}, [catalogApi, entity]);
}
export const ComponentRemovalDialog: FC<ComponentRemovalDialogProps> = ({
open,
onConfirm,
onCancel,
onClose,
component,
entity,
}) => {
const { value: entities, loading, error } = useColocatedEntities(component);
const { value: entities, loading, error } = useColocatedEntities(entity);
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
return (
<Dialog fullScreen={fullScreen} open onClose={onClose}>
<Dialog fullScreen={fullScreen} open={open} onClose={onClose}>
<DialogTitle id="responsive-dialog-title">
Are you sure you want to unregister this component?
</DialogTitle>
@@ -102,7 +101,7 @@ export const ComponentRemovalDialog: FC<ComponentRemovalDialogProps> = ({
) : null}
</DialogContent>
<DialogActions>
<Button onClick={onCancel}>Cancel</Button>
<Button onClick={onClose}>Cancel</Button>
<Button
disabled={!!(loading || error)}
onClick={onConfirm}