chore(catalog): rename all pages and components to use Entity nomenclature

This commit is contained in:
Fredrik Adelöw
2020-06-11 09:53:27 +02:00
parent 0b03318cea
commit 1d05fea3c1
22 changed files with 64 additions and 218 deletions
+2 -5
View File
@@ -20,7 +20,6 @@ import {
LOCATION_ANNOTATION,
} from '@backstage/catalog-model';
import Cache from 'node-cache';
import { DescriptorEnvelope } from '../types';
import { CatalogApi, EntityCompoundName } from './types';
export class CatalogClient implements CatalogApi {
@@ -77,10 +76,8 @@ export class CatalogClient implements CatalogApi {
return await this.getOptional(`/locations/${id}`);
}
async getEntities(
filter?: Record<string, string>,
): Promise<DescriptorEnvelope[]> {
const cachedValue = this.cache.get<DescriptorEnvelope[]>(
async getEntities(filter?: Record<string, string>): Promise<Entity[]> {
const cachedValue = this.cache.get<Entity[]>(
`get:${JSON.stringify(filter)}`,
);
if (cachedValue) return cachedValue;
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createApiRef } from '@backstage/core';
import { Entity, Location } from '@backstage/catalog-model';
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { useApi } from '@backstage/core';
import { catalogApiRef } from '../../api/types';
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import {
Card,
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { useStarredEntities } from '../../hooks/useStarredEntites';
@@ -189,7 +189,7 @@ export const CatalogPage: FC<{}> = () => {
>
Create Service
</Button>
<SupportButton>All your components</SupportButton>
<SupportButton>All your software catalog entities</SupportButton>
</ContentHeader>
<div className={styles.contentWrapper}>
<div>
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { wrapInTestApp } from '@backstage/test-utils';
import { render } from '@testing-library/react';
@@ -50,12 +51,12 @@ describe('CatalogTable component', () => {
),
);
const errorMessage = await rendered.findByText(
/Error encountered while fetching components./,
/Error encountered while fetching catalog entities./,
);
expect(errorMessage).toBeInTheDocument();
});
it('should display component names when loading has finished and no error occurred', async () => {
it('should display entity names when loading has finished and no error occurred', async () => {
const rendered = render(
wrapInTestApp(
<CatalogTable
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { Table, TableColumn } from '@backstage/core';
import { Link } from '@material-ui/core';
@@ -72,7 +73,7 @@ export const CatalogTable: FC<CatalogTableProps> = ({
return (
<div>
<Alert severity="error">
Error encountered while fetching components. {error.toString()}
Error encountered while fetching catalog entities. {error.toString()}
</Alert>
</div>
);
@@ -13,21 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentContextMenu } from './ComponentContextMenu';
import { render } from '@testing-library/react';
import { render, fireEvent } from '@testing-library/react';
import * as React from 'react';
import { act } from 'react-dom/test-utils';
import { EntityContextMenu } from './EntityContextMenu';
describe('ComponentContextMenu', () => {
it('should call onUnregisterComponent on button click', async () => {
it('should call onUnregisterEntity on button click', async () => {
await act(async () => {
const mockCallback = jest.fn();
const menu = render(
<ComponentContextMenu onUnregisterComponent={mockCallback} />,
<EntityContextMenu onUnregisterEntity={mockCallback} />,
);
const button = await menu.findByTestId('menu-button');
button.click();
const unregister = await menu.findByText('Unregister component');
fireEvent.click(button);
const unregister = await menu.findByText('Unregister entity');
expect(unregister).toBeInTheDocument();
});
});
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
IconButton,
ListItemIcon,
@@ -34,13 +35,11 @@ const useStyles = makeStyles({
},
});
type ComponentContextMenuProps = {
onUnregisterComponent: () => void;
type Props = {
onUnregisterEntity: () => void;
};
export const ComponentContextMenu: FC<ComponentContextMenuProps> = ({
onUnregisterComponent,
}) => {
export const EntityContextMenu: FC<Props> = ({ onUnregisterEntity }) => {
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement>();
const classes = useStyles();
@@ -53,7 +52,7 @@ export const ComponentContextMenu: FC<ComponentContextMenuProps> = ({
};
return (
<div>
<>
<IconButton
aria-label="more"
aria-controls="long-menu"
@@ -75,13 +74,13 @@ export const ComponentContextMenu: FC<ComponentContextMenuProps> = ({
<MenuItem
onClick={() => {
onClose();
onUnregisterComponent();
onUnregisterEntity();
}}
>
<ListItemIcon>
<Cancel fontSize="small" />
</ListItemIcon>
<Typography variant="inherit">Unregister component</Typography>
<Typography variant="inherit">Unregister entity</Typography>
</MenuItem>
<MenuItem>
<ListItemIcon>
@@ -91,6 +90,6 @@ export const ComponentContextMenu: FC<ComponentContextMenuProps> = ({
</MenuItem>
</MenuList>
</Popover>
</div>
</>
);
};
@@ -13,21 +13,20 @@
* 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 { EntityMetadataCard } from './EntityMetadataCard';
describe('ComponentMetadataCard component', () => {
it('should display component name if provided', async () => {
describe('EntityMetadataCard component', () => {
it('should display entity name if provided', async () => {
const testEntity: Entity = {
apiVersion: 'backstage.io/v1beta1',
kind: 'Component',
metadata: { name: 'test' },
};
const rendered = await render(
<ComponentMetadataCard entity={testEntity} />,
);
const rendered = await render(<EntityMetadataCard entity={testEntity} />);
expect(await rendered.findByText('test')).toBeInTheDocument();
});
});
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { InfoCard, StructuredMetadataTable } from '@backstage/core';
import React, { FC } from 'react';
@@ -21,7 +22,7 @@ type Props = {
entity: Entity;
};
export const ComponentMetadataCard: FC<Props> = ({ entity }) => (
export const EntityMetadataCard: FC<Props> = ({ entity }) => (
<InfoCard title="Metadata">
<StructuredMetadataTable metadata={entity.metadata} />
</InfoCard>
@@ -13,12 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentPage } from './ComponentPage';
import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core';
import { wrapInTestApp } from '@backstage/test-utils';
import { render, wait } from '@testing-library/react';
import * as React from 'react';
import { wrapInTestApp } from '@backstage/test-utils';
import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core';
import { catalogApiRef, CatalogApi } from '../../api/types';
import { CatalogApi, catalogApiRef } from '../../api/types';
import { EntityPage } from './EntityPage';
const getTestProps = (name: string) => {
return {
@@ -36,8 +37,8 @@ const getTestProps = (name: string) => {
const errorApi = { post: () => {} };
describe('ComponentPage', () => {
it('should redirect to component table page when name is not provided', async () => {
describe('EntityPage', () => {
it('should redirect to catalog page when name is not provided', async () => {
const props = getTestProps('');
render(
wrapInTestApp(
@@ -52,7 +53,7 @@ describe('ComponentPage', () => {
],
])}
>
<ComponentPage {...props} />
<EntityPage {...props} />
</ApiProvider>,
),
);
@@ -31,13 +31,13 @@ import { Alert } from '@material-ui/lab';
import React, { FC, useEffect, useState } from 'react';
import { useAsync } from 'react-use';
import { catalogApiRef } from '../..';
import { ComponentContextMenu } from '../ComponentContextMenu/ComponentContextMenu';
import { ComponentMetadataCard } from '../ComponentMetadataCard/ComponentMetadataCard';
import { ComponentRemovalDialog } from '../ComponentRemovalDialog/ComponentRemovalDialog';
import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu';
import { EntityMetadataCard } from '../EntityMetadataCard/EntityMetadataCard';
import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog';
const REDIRECT_DELAY = 1000;
type ComponentPageProps = {
type Props = {
match: {
params: {
optionalNamespaceAndName: string;
@@ -68,7 +68,7 @@ function headerProps(
};
}
export const ComponentPage: FC<ComponentPageProps> = ({ match, history }) => {
export const EntityPage: FC<Props> = ({ match, history }) => {
const { optionalNamespaceAndName, kind } = match.params;
const [name, namespace] = optionalNamespaceAndName.split(':').reverse();
@@ -83,7 +83,7 @@ export const ComponentPage: FC<ComponentPageProps> = ({ match, history }) => {
useEffect(() => {
if (!error && !loading && !entity) {
errorApi.post(new Error('Component not found!'));
errorApi.post(new Error('Entity not found!'));
setTimeout(() => {
history.push('/');
}, REDIRECT_DELAY);
@@ -141,9 +141,7 @@ export const ComponentPage: FC<ComponentPageProps> = ({ match, history }) => {
// TODO: Switch theme and type props based on component type (website, library, ...)
<Page theme={pageTheme.service}>
<Header title={headerTitle} type={headerType}>
{entity && (
<ComponentContextMenu onUnregisterComponent={showRemovalDialog} />
)}
{entity && <EntityContextMenu onUnregisterEntity={showRemovalDialog} />}
</Header>
{loading && <Progress />}
@@ -161,7 +159,7 @@ export const ComponentPage: FC<ComponentPageProps> = ({ match, history }) => {
<Content>
<Grid container spacing={3} direction="column">
<Grid item>
<ComponentMetadataCard entity={entity} />
<EntityMetadataCard entity={entity} />
</Grid>
<Grid item>
<SentryIssuesWidget
@@ -172,7 +170,7 @@ export const ComponentPage: FC<ComponentPageProps> = ({ match, history }) => {
</Grid>
</Content>
<ComponentRemovalDialog
<UnregisterEntityDialog
open={confirmationDialogOpen}
entity={entity}
onConfirm={cleanUpAfterRemoval}
@@ -33,7 +33,7 @@ import { useAsync } from 'react-use';
import { AsyncState } from 'react-use/lib/useAsync';
import { catalogApiRef } from '../../api/types';
type ComponentRemovalDialogProps = {
type Props = {
open: boolean;
onConfirm: () => any;
onClose: () => any;
@@ -50,7 +50,7 @@ function useColocatedEntities(entity: Entity): AsyncState<Entity[]> {
}, [catalogApi, entity]);
}
export const ComponentRemovalDialog: FC<ComponentRemovalDialogProps> = ({
export const UnregisterEntityDialog: FC<Props> = ({
open,
onConfirm,
onClose,
@@ -76,7 +76,7 @@ export const ComponentRemovalDialog: FC<ComponentRemovalDialogProps> = ({
return (
<Dialog fullScreen={fullScreen} open={open} onClose={onClose}>
<DialogTitle id="responsive-dialog-title">
Are you sure you want to unregister this component?
Are you sure you want to unregister this entity?
</DialogTitle>
<DialogContent>
{loading ? <Progress /> : null}
@@ -108,7 +108,7 @@ export const ComponentRemovalDialog: FC<ComponentRemovalDialogProps> = ({
</ul>
</Typography>
<DialogContentText>
To undo, just re-register the component in Backstage.
To undo, just re-register the entity in Backstage.
</DialogContentText>
</>
) : null}
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
CatalogFilterGroup,
CatalogFilterItem,
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
EntityMeta,
LocationSpec,
@@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState, useEffect, useCallback } from 'react';
import { useApi, storageApiRef } from '@backstage/core';
import { useObservable } from 'react-use';
import { Entity } from '@backstage/catalog-model';
import { storageApiRef, useApi } from '@backstage/core';
import { useCallback, useEffect, useState } from 'react';
import { useObservable } from 'react-use';
const buildEntityKey = (component: Entity) =>
`entity:${component.kind}:${component.metadata.namespace ?? 'default'}:${
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { renderHook, act } from '@testing-library/react-hooks';
import { useStarredEntities } from './useStarredEntites';
-1
View File
@@ -17,5 +17,4 @@
export { plugin } from './plugin';
export * from './api/CatalogClient';
export * from './api/types';
export * from './types';
export * from './routes';
+2 -2
View File
@@ -16,13 +16,13 @@
import { createPlugin } from '@backstage/core';
import { CatalogPage } from './components/CatalogPage/CatalogPage';
import { ComponentPage } from './components/ComponentPage/ComponentPage';
import { EntityPage } from './components/EntityPage/EntityPage';
import { entityRoute, rootRoute } from './routes';
export const plugin = createPlugin({
id: 'catalog',
register({ router }) {
router.addRoute(rootRoute, CatalogPage);
router.addRoute(entityRoute, ComponentPage);
router.addRoute(entityRoute, EntityPage);
},
});
-159
View File
@@ -1,159 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface ComponentDescriptorV1beta1 extends DescriptorEnvelope {
spec: {
type: string;
};
}
export type ComponentDescriptor = ComponentDescriptorV1beta1;
/**
* Metadata fields common to all versions/kinds of entity.
*
* @see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta
*/
export type EntityMeta = {
/**
* A globally unique ID for the entity.
*
* This field can not be set by the user at creation time, and the server
* will reject an attempt to do so. The field will be populated in read
* operations. The field can (optionally) be specified when performing
* update or delete operations, but the server is free to reject requests
* that do so in such a way that it breaks semantics.
*/
uid?: string;
/**
* An opaque string that changes for each update operation to any part of
* the entity, including metadata.
*
* This field can not be set by the user at creation time, and the server
* will reject an attempt to do so. The field will be populated in read
* operations. The field can (optionally) be specified when performing
* update or delete operations, and the server will then reject the
* operation if it does not match the current stored value.
*/
etag?: string;
/**
* A positive nonzero number that indicates the current generation of data
* for this entity; the value is incremented each time the spec changes.
*
* This field can not be set by the user at creation time, and the server
* will reject an attempt to do so. The field will be populated in read
* operations.
*/
generation?: number;
/**
* The name of the entity.
*
* Must be uniqe within the catalog at any given point in time, for any
* given namespace, for any given kind.
*/
name: string;
/**
* The short description of the entity.
*
* A a human readable string.
*/
description: string;
/**
* The namespace that the entity belongs to.
*/
namespace?: string;
/**
* Key/value pairs of identifying information attached to the entity.
*/
labels?: Record<string, string>;
/**
* Key/value pairs of non-identifying auxiliary information attached to the
* entity.
*/
annotations?: Record<string, string>;
};
/**
* The format envelope that's common to all versions/kinds.
*
* @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/
*/
export type DescriptorEnvelope = {
/**
* The version of specification format for this particular entity that
* this is written against.
*/
apiVersion: string;
/**
* The high level entity type being described.
*/
kind: string;
/**
* Optional metadata related to the entity.
*/
metadata: EntityMeta;
/**
* The specification data describing the entity itself.
*/
spec?: object;
};
/**
* Parses and validates descriptors.
*
* The output must be validated and well formed.
*/
export type DescriptorParser = {
/**
* Parses and validates a single raw descriptor.
*
* @param descriptor A raw descriptor object
* @returns A structure describing the parsed and validated descriptor
* @throws An Error if the descriptor was malformed
*/
parse(descriptor: object): Promise<DescriptorEnvelope>;
};
/**
* Parses and validates a single envelope into its materialized kind.
*
* These parsers may assume that the envelope is already validated and well
* formed.
*/
export type KindParser = {
/**
* Try to parse an envelope into a materialized kind.
*
* @param envelope A valid descriptor envelope
* @returns A materialized type, or undefined if the given version/kind is
* not meant to be handled by this parser
* @throws An Error if the type was handled and found to not be properly
* formatted
*/
tryParse(
envelope: DescriptorEnvelope,
): Promise<DescriptorEnvelope | undefined>;
};