Merge branch 'master' into ndudnik/unregister-entity
This commit is contained in:
@@ -14,14 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CatalogApi } from './types';
|
||||
import { DescriptorEnvelope } from '../types';
|
||||
import {
|
||||
Entity,
|
||||
Location,
|
||||
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 {
|
||||
// TODO(blam): This cache is just temporary until we have GraphQL.
|
||||
@@ -30,6 +30,7 @@ export class CatalogClient implements CatalogApi {
|
||||
private cache: Cache;
|
||||
private apiOrigin: string;
|
||||
private basePath: string;
|
||||
|
||||
constructor({
|
||||
apiOrigin,
|
||||
basePath,
|
||||
@@ -41,46 +42,41 @@ export class CatalogClient implements CatalogApi {
|
||||
this.basePath = basePath;
|
||||
this.cache = new Cache({ stdTTL: 10 });
|
||||
}
|
||||
|
||||
private async getRequired(path: string): Promise<any> {
|
||||
const url = `${this.apiOrigin}${this.basePath}${path}`;
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
private async getOptional(path: string): Promise<any | undefined> {
|
||||
const url = `${this.apiOrigin}${this.basePath}${path}`;
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const payload = await response.text();
|
||||
const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async getLocationById(id: String): Promise<Location | undefined> {
|
||||
const response = await fetch(
|
||||
`${this.apiOrigin}${this.basePath}/locations/${id}`,
|
||||
);
|
||||
if (response.ok) {
|
||||
const location = await response.json();
|
||||
if (location) return location.data;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
async removeEntityByUid(uid: string): Promise<void> {
|
||||
const response = await fetch(
|
||||
`${this.apiOrigin}${this.basePath}/entities/by-uid/${uid}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
throw new Error(
|
||||
`Request failed with ${response.status} ${response.statusText}, ${payload}`,
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
async removeLocationById(id: string): Promise<void> {
|
||||
const response = await fetch(
|
||||
`${this.apiOrigin}${this.basePath}/locations/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
throw new Error(
|
||||
`Request failed with ${response.status} ${response.statusText}, ${payload}`,
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
return await this.getOptional(`/locations/${id}`);
|
||||
}
|
||||
|
||||
async getEntities(
|
||||
filter?: Record<string, string>,
|
||||
): Promise<DescriptorEnvelope[]> {
|
||||
@@ -89,49 +85,25 @@ export class CatalogClient implements CatalogApi {
|
||||
);
|
||||
if (cachedValue) return cachedValue;
|
||||
|
||||
let url = `${this.apiOrigin}${this.basePath}/entities`;
|
||||
let path = `/entities`;
|
||||
if (filter) {
|
||||
url += '?';
|
||||
url += Object.entries(filter)
|
||||
path += '?';
|
||||
path += Object.entries(filter)
|
||||
.map(
|
||||
([key, value]) =>
|
||||
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
|
||||
)
|
||||
.join('&');
|
||||
}
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
throw new Error(
|
||||
`Request failed with ${response.status} ${response.statusText}, ${payload}`,
|
||||
);
|
||||
}
|
||||
const value = await response.json();
|
||||
|
||||
if (value?.length) {
|
||||
this.cache.set(`get:${JSON.stringify(filter)}`, value);
|
||||
}
|
||||
|
||||
return value;
|
||||
return await this.getRequired(path);
|
||||
}
|
||||
|
||||
async getEntity({
|
||||
name,
|
||||
namespace,
|
||||
kind,
|
||||
}: {
|
||||
name: string;
|
||||
namespace?: string;
|
||||
kind: string;
|
||||
}): Promise<DescriptorEnvelope> {
|
||||
const response = await fetch(
|
||||
`${this.apiOrigin}${this.basePath}/entities/by-name/${kind}/${
|
||||
namespace ?? 'default'
|
||||
}/${name}`,
|
||||
);
|
||||
const entity = await response.json();
|
||||
if (entity) return entity;
|
||||
throw new Error(`'Entity not found: ${name}`);
|
||||
async getEntityByName(
|
||||
compoundName: EntityCompoundName,
|
||||
): Promise<Entity | undefined> {
|
||||
const { kind, namespace = 'default', name } = compoundName;
|
||||
return this.getOptional(`/entities/by-name/${kind}/${namespace}/${name}`);
|
||||
}
|
||||
|
||||
async addLocation(type: string, target: string) {
|
||||
@@ -162,11 +134,10 @@ export class CatalogClient implements CatalogApi {
|
||||
}
|
||||
|
||||
async getLocationByEntity(entity: Entity): Promise<Location | undefined> {
|
||||
const locationId = entity.metadata.annotations?.[LOCATION_ANNOTATION];
|
||||
if (!locationId) return undefined;
|
||||
|
||||
const location = this.getLocationById(locationId);
|
||||
|
||||
return location;
|
||||
const locationCompound = entity.metadata.annotations?.[LOCATION_ANNOTATION];
|
||||
const all: { data: Location }[] = await this.getRequired('/locations');
|
||||
return all
|
||||
.map(r => r.data)
|
||||
.find(l => locationCompound === `${l.type}:${l.target}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,13 +22,17 @@ export const catalogApiRef = createApiRef<CatalogApi>({
|
||||
'Used by the Catalog plugin to make requests to accompanying backend',
|
||||
});
|
||||
|
||||
export type EntityCompoundName = {
|
||||
kind: string;
|
||||
namespace?: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export interface CatalogApi {
|
||||
getEntity(params: {
|
||||
name: string;
|
||||
namespace?: string;
|
||||
kind: string;
|
||||
}): Promise<Entity>;
|
||||
getLocationById(id: String): Promise<Location | undefined>;
|
||||
getEntityByName(
|
||||
compoundName: EntityCompoundName,
|
||||
): Promise<Entity | undefined>;
|
||||
getEntities(filter?: Record<string, string>): Promise<Entity[]>;
|
||||
addLocation(type: string, target: string): Promise<AddLocationResponse>;
|
||||
getLocationByEntity(entity: Entity): Promise<Location | undefined>;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { LocationSpec, Entity } from '@backstage/catalog-model';
|
||||
import { Entity, LocationSpec } from '@backstage/catalog-model';
|
||||
import {
|
||||
Content,
|
||||
ContentHeader,
|
||||
@@ -27,21 +27,18 @@ import {
|
||||
SupportButton,
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
|
||||
import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder';
|
||||
import { Button, makeStyles, Typography, Link } from '@material-ui/core';
|
||||
import GitHub from '@material-ui/icons/GitHub';
|
||||
import StarOutline from '@material-ui/icons/StarBorder';
|
||||
import Star from '@material-ui/icons/Star';
|
||||
|
||||
import { Button, Link, makeStyles, Typography } from '@material-ui/core';
|
||||
import Edit from '@material-ui/icons/Edit';
|
||||
|
||||
import GitHub from '@material-ui/icons/GitHub';
|
||||
import Star from '@material-ui/icons/Star';
|
||||
import StarOutline from '@material-ui/icons/StarBorder';
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { useAsync } from 'react-use';
|
||||
import { catalogApiRef } from '../..';
|
||||
import { defaultFilter, filterGroups, entityFilters } from '../../data/filters';
|
||||
import { entityToComponent, findLocationForEntityMeta } from '../../data/utils';
|
||||
import { findLocationForEntityMeta } from '../../data/utils';
|
||||
import { useStarredEntities } from '../../hooks/useStarredEntites';
|
||||
import {
|
||||
CatalogFilter,
|
||||
@@ -102,7 +99,7 @@ export const CatalogPage: FC<{}> = () => {
|
||||
if (!location) return;
|
||||
window.open(location.target, '_blank');
|
||||
},
|
||||
hidden: location ? location?.type !== 'github' : true,
|
||||
hidden: location?.type !== 'github',
|
||||
};
|
||||
},
|
||||
(rowData: Entity) => {
|
||||
@@ -125,7 +122,7 @@ export const CatalogPage: FC<{}> = () => {
|
||||
if (!location) return;
|
||||
window.open(createEditLink(location), '_blank');
|
||||
},
|
||||
hidden: location ? location?.type !== 'github' : true,
|
||||
hidden: location?.type !== 'github',
|
||||
};
|
||||
},
|
||||
(rowData: Entity) => {
|
||||
@@ -204,16 +201,7 @@ export const CatalogPage: FC<{}> = () => {
|
||||
</div>
|
||||
<CatalogTable
|
||||
titlePreamble={selectedFilter.label}
|
||||
components={
|
||||
(value &&
|
||||
value.map(val => {
|
||||
return {
|
||||
...entityToComponent(val),
|
||||
locationSpec: findLocationForEntityMeta(val.metadata),
|
||||
};
|
||||
})) ||
|
||||
[]
|
||||
}
|
||||
entities={value || []}
|
||||
loading={loading}
|
||||
error={error}
|
||||
actions={actions}
|
||||
|
||||
@@ -13,30 +13,27 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { render } from '@testing-library/react';
|
||||
import * as React from 'react';
|
||||
import { CatalogTable } from './CatalogTable';
|
||||
import { Component } from '../../data/component';
|
||||
|
||||
const components: Component[] = [
|
||||
const entites: Entity[] = [
|
||||
{
|
||||
name: 'component1',
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'Component',
|
||||
metadata: { name: 'component1' },
|
||||
description: 'Placeholder',
|
||||
},
|
||||
{
|
||||
name: 'component2',
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'Component',
|
||||
metadata: { name: 'component2' },
|
||||
description: 'Placeholder',
|
||||
},
|
||||
{
|
||||
name: 'component3',
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'Component',
|
||||
metadata: { name: 'component3' },
|
||||
description: 'Placeholder',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -46,7 +43,7 @@ describe('CatalogTable component', () => {
|
||||
wrapInTestApp(
|
||||
<CatalogTable
|
||||
titlePreamble="Owned"
|
||||
components={[]}
|
||||
entities={[]}
|
||||
loading={false}
|
||||
error={{ code: 'error' }}
|
||||
/>,
|
||||
@@ -63,13 +60,13 @@ describe('CatalogTable component', () => {
|
||||
wrapInTestApp(
|
||||
<CatalogTable
|
||||
titlePreamble="Owned"
|
||||
components={components}
|
||||
entities={entites}
|
||||
loading={false}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
expect(
|
||||
await rendered.findByText(`Owned (${components.length})`),
|
||||
await rendered.findByText(`Owned (${entites.length})`),
|
||||
).toBeInTheDocument();
|
||||
expect(await rendered.findByText('component1')).toBeInTheDocument();
|
||||
expect(await rendered.findByText('component2')).toBeInTheDocument();
|
||||
|
||||
@@ -13,33 +13,33 @@
|
||||
* 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';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import React, { FC } from 'react';
|
||||
import { generatePath, Link as RouterLink } from 'react-router-dom';
|
||||
import { Component } from '../../data/component';
|
||||
import { entityRoute } from '../../routes';
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{
|
||||
title: 'Name',
|
||||
field: 'name',
|
||||
field: 'metadata.name',
|
||||
highlight: true,
|
||||
render: (componentData: any) => (
|
||||
render: (entity: any) => (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={generatePath(entityRoute.path, {
|
||||
optionalNamespaceAndName: [
|
||||
componentData.namespace,
|
||||
componentData.name,
|
||||
entity.metadata.namespace,
|
||||
entity.metadata.name,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(':'),
|
||||
kind: componentData.kind,
|
||||
kind: entity.kind,
|
||||
})}
|
||||
>
|
||||
{componentData.name}
|
||||
{entity.metadata.name}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
@@ -49,12 +49,12 @@ const columns: TableColumn[] = [
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
field: 'description',
|
||||
field: 'metadata.description',
|
||||
},
|
||||
];
|
||||
|
||||
type CatalogTableProps = {
|
||||
components: Component[];
|
||||
entities: Entity[];
|
||||
titlePreamble: string;
|
||||
loading: boolean;
|
||||
error?: any;
|
||||
@@ -62,7 +62,7 @@ type CatalogTableProps = {
|
||||
};
|
||||
|
||||
export const CatalogTable: FC<CatalogTableProps> = ({
|
||||
components,
|
||||
entities,
|
||||
loading,
|
||||
error,
|
||||
titlePreamble,
|
||||
@@ -88,8 +88,8 @@ export const CatalogTable: FC<CatalogTableProps> = ({
|
||||
loadingType: 'linear',
|
||||
showEmptyDataSourceMessage: !loading,
|
||||
}}
|
||||
title={`${titlePreamble} (${(components && components.length) || 0})`}
|
||||
data={components}
|
||||
title={`${titlePreamble} (${(entities && entities.length) || 0})`}
|
||||
data={entities}
|
||||
actions={actions}
|
||||
/>
|
||||
);
|
||||
|
||||
+6
-14
@@ -13,29 +13,21 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import ComponentMetadataCard from './ComponentMetadataCard';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { render } from '@testing-library/react';
|
||||
import { Entity } from '../../../../../packages/catalog-model/src/entity/Entity';
|
||||
import React from 'react';
|
||||
import { ComponentMetadataCard } from './ComponentMetadataCard';
|
||||
|
||||
describe('ComponentMetadataCard component', () => {
|
||||
it('should display component name if provided', async () => {
|
||||
const testEntity: Entity = {
|
||||
apiVersion: '',
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'test',
|
||||
},
|
||||
metadata: { name: 'test' },
|
||||
};
|
||||
const rendered = await render(
|
||||
<ComponentMetadataCard loading={false} entity={testEntity} />,
|
||||
<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 entity={undefined} />,
|
||||
);
|
||||
expect(await rendered.findByRole('progressbar')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,32 +13,16 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { FC } from 'react';
|
||||
import { Progress, InfoCard, StructuredMetadataTable } from '@backstage/core';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { InfoCard, StructuredMetadataTable } from '@backstage/core';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
type ComponentMetadataCardProps = {
|
||||
loading: boolean;
|
||||
entity: Entity | undefined;
|
||||
type Props = {
|
||||
entity: Entity;
|
||||
};
|
||||
const ComponentMetadataCard: FC<ComponentMetadataCardProps> = ({
|
||||
loading,
|
||||
entity,
|
||||
}) => {
|
||||
if (loading) {
|
||||
return (
|
||||
<InfoCard title="Metadata">
|
||||
<Progress />
|
||||
</InfoCard>
|
||||
);
|
||||
}
|
||||
if (!entity) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<InfoCard title="Metadata">
|
||||
<StructuredMetadataTable metadata={entity} />
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
export default ComponentMetadataCard;
|
||||
|
||||
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,26 +13,27 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import ComponentMetadataCard from '../ComponentMetadataCard/ComponentMetadataCard';
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
Content,
|
||||
Header,
|
||||
pageTheme,
|
||||
Page,
|
||||
useApi,
|
||||
ErrorApi,
|
||||
errorApiRef,
|
||||
Header,
|
||||
HeaderTabs,
|
||||
Page,
|
||||
pageTheme,
|
||||
Progress,
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
import ComponentContextMenu from '../ComponentContextMenu/ComponentContextMenu';
|
||||
import ComponentRemovalDialog from '../ComponentRemovalDialog/ComponentRemovalDialog';
|
||||
|
||||
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 { Entity } from '@backstage/catalog-model';
|
||||
import { ComponentContextMenu } from '../ComponentContextMenu/ComponentContextMenu';
|
||||
import { ComponentMetadataCard } from '../ComponentMetadataCard/ComponentMetadataCard';
|
||||
import { ComponentRemovalDialog } from '../ComponentRemovalDialog/ComponentRemovalDialog';
|
||||
|
||||
const REDIRECT_DELAY = 1000;
|
||||
|
||||
@@ -48,42 +49,59 @@ type ComponentPageProps = {
|
||||
};
|
||||
};
|
||||
|
||||
const ComponentPage: FC<ComponentPageProps> = ({ match, history }) => {
|
||||
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
||||
const [removingPending, setRemovingPending] = useState(false);
|
||||
const showRemovalDialog = () => setConfirmationDialogOpen(true);
|
||||
const hideRemovalDialog = () => setConfirmationDialogOpen(false);
|
||||
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 { 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.getEntity({ name, namespace, kind });
|
||||
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 cleanUpAfterRemoval = async () => {
|
||||
setConfirmationDialogOpen(false);
|
||||
setRemovingPending(true);
|
||||
history.push('/');
|
||||
};
|
||||
|
||||
const showRemovalDialog = () => setConfirmationDialogOpen(true);
|
||||
|
||||
// TODO - Replace with proper tabs implementation
|
||||
const tabs = [
|
||||
{
|
||||
@@ -112,39 +130,56 @@ 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={entity?.metadata.name || 'Catalog'}>
|
||||
<ComponentContextMenu onUnregisterComponent={showRemovalDialog} />
|
||||
<Header title={headerTitle} type={headerType}>
|
||||
{entity && (
|
||||
<ComponentContextMenu onUnregisterComponent={showRemovalDialog} />
|
||||
)}
|
||||
</Header>
|
||||
<HeaderTabs tabs={tabs} />
|
||||
|
||||
{confirmationDialogOpen && entity && (
|
||||
<ComponentRemovalDialog
|
||||
entity={entity}
|
||||
onClose={hideRemovalDialog}
|
||||
onConfirm={cleanUpAfterRemoval}
|
||||
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}
|
||||
onConfirm={cleanUpAfterRemoval}
|
||||
onClose={() => setConfirmationDialogOpen(false)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Content>
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<ComponentMetadataCard
|
||||
loading={loading || removingPending}
|
||||
entity={entity}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<SentryIssuesWidget
|
||||
sentryProjectId="sample-sentry-project-id"
|
||||
statsFor="24h"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
export default ComponentPage;
|
||||
|
||||
@@ -26,19 +26,17 @@ import {
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
} from '@material-ui/core';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import React, { FC } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { AsyncState } from 'react-use/lib/useAsync';
|
||||
import { catalogApiRef } from '../../api/types';
|
||||
import { alertApiRef } from '../../../../../packages/core-api/src/apis/definitions/AlertApi';
|
||||
|
||||
type ComponentRemovalDialogProps = {
|
||||
open: boolean;
|
||||
onConfirm: () => any;
|
||||
onCancel: () => any;
|
||||
onClose: () => any;
|
||||
entity: Entity;
|
||||
};
|
||||
@@ -54,51 +52,51 @@ function useColocatedEntities(entity: Entity): AsyncState<Entity[]> {
|
||||
}
|
||||
|
||||
export const ComponentRemovalDialog: FC<ComponentRemovalDialogProps> = ({
|
||||
open,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
onClose,
|
||||
entity,
|
||||
}) => {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const { value: entities, loading, error } = useColocatedEntities(entity);
|
||||
const theme = useTheme();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const alertApi = useApi(alertApiRef);
|
||||
|
||||
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>
|
||||
<DialogContent>
|
||||
{loading ? <Progress /> : null}
|
||||
{error ? (
|
||||
<DialogContentText>{error.toString()}</DialogContentText>
|
||||
<Alert severity="error" style={{ wordBreak: 'break-word' }}>
|
||||
{error.toString()}
|
||||
</Alert>
|
||||
) : null}
|
||||
{entities ? (
|
||||
<>
|
||||
<DialogContentText>
|
||||
This action will unregister the following entities:
|
||||
</DialogContentText>
|
||||
<List dense>
|
||||
{entities.map(e => (
|
||||
<ListItem key={e.metadata.name}>
|
||||
<ListItemText primary={e.metadata.name} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
<Typography component="div">
|
||||
<ul>
|
||||
{entities.map(e => (
|
||||
<li key={e.metadata.name}>{e.metadata.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Typography>
|
||||
<DialogContentText>
|
||||
That are located at the following location:
|
||||
</DialogContentText>
|
||||
<List dense>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={
|
||||
entities[0]?.metadata?.annotations?.[LOCATION_ANNOTATION]
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
<Typography component="div">
|
||||
<ul>
|
||||
<li>
|
||||
{entities[0]?.metadata?.annotations?.[LOCATION_ANNOTATION]}
|
||||
</li>
|
||||
</ul>
|
||||
</Typography>
|
||||
<DialogContentText>
|
||||
To undo, just re-register the component in Backstage.
|
||||
</DialogContentText>
|
||||
@@ -106,7 +104,7 @@ export const ComponentRemovalDialog: FC<ComponentRemovalDialogProps> = ({
|
||||
) : null}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel} color="primary">
|
||||
<Button onClick={onClose} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
@@ -125,7 +123,7 @@ export const ComponentRemovalDialog: FC<ComponentRemovalDialogProps> = ({
|
||||
|
||||
onConfirm();
|
||||
}}
|
||||
color="primary"
|
||||
color="secondary"
|
||||
>
|
||||
Unregister
|
||||
</Button>
|
||||
|
||||
+2
@@ -24,6 +24,7 @@ import { catalogApiRef } from '@backstage/plugin-catalog';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
const errorApi = { post: () => {} };
|
||||
|
||||
const catalogApi: jest.Mocked<typeof catalogApiRef.T> = {
|
||||
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
|
||||
addLocation: jest.fn((_a, _b) => new Promise(() => {})),
|
||||
@@ -49,6 +50,7 @@ const setup = () => ({
|
||||
</MemoryRouter>,
|
||||
),
|
||||
});
|
||||
|
||||
describe('RegisterComponentPage', () => {
|
||||
afterEach(() => cleanup());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user