chore(catalog): clean up ComponentPage, use only Entity
This commit is contained in:
+5
-12
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user