Unbreak the unregister dialog after catalog changes

This commit is contained in:
Fredrik Adelöw
2020-06-08 16:31:29 +02:00
committed by Nikita Nek Dudnik
parent 920f496a7f
commit c617b1cf33
8 changed files with 141 additions and 105 deletions
+21 -8
View File
@@ -45,14 +45,27 @@ export class CatalogClient implements CatalogApi {
}
return undefined;
}
async getEntitiesByLocationId(id: string): Promise<Entity[]> {
const response = await fetch(
`${this.apiOrigin}${this.basePath}/entities?${LOCATION_ANNOTATION}=${id}`,
);
return await response.json();
}
async getEntities(): Promise<DescriptorEnvelope[]> {
const response = await fetch(`${this.apiOrigin}${this.basePath}/entities`);
async getEntities(
filter?: Record<string, string>,
): Promise<DescriptorEnvelope[]> {
let url = `${this.apiOrigin}${this.basePath}/entities`;
if (filter) {
url += '?';
url += 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}`,
);
}
return await response.json();
}
async getEntityByName(name: string): Promise<DescriptorEnvelope> {
+1 -2
View File
@@ -24,9 +24,8 @@ export const catalogApiRef = createApiRef<CatalogApi>({
export interface CatalogApi {
getLocationById(id: String): Promise<Location | undefined>;
getEntities(): Promise<Entity[]>;
getEntities(filter?: Record<string, string>): Promise<Entity[]>;
getEntityByName(name: string): Promise<Entity>;
getEntitiesByLocationId(id: string): Promise<Entity[]>;
addLocation(type: string, target: string): Promise<AddLocationResponse>;
getLocationByEntity(entity: Entity): Promise<Location | undefined>;
}
@@ -14,35 +14,33 @@
* limitations under the License.
*/
import React, { FC, useCallback, useState } from 'react';
import {
Content,
ContentHeader,
DismissableBanner,
Header,
HeaderTabs,
HomepageTimer,
SupportButton,
Page,
pageTheme,
SupportButton,
useApi,
HeaderTabs,
} from '@backstage/core';
import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder';
import { Button, Link, makeStyles, Typography } from '@material-ui/core';
import GitHub from '@material-ui/icons/GitHub';
import React, { FC, useCallback, useState } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { useAsync } from 'react-use';
import CatalogTable from '../CatalogTable/CatalogTable';
import { catalogApiRef } from '../..';
import { Component } from '../../data/component';
import { defaultFilter, filterGroups } from '../../data/filters';
import { entityToComponent, findLocationForEntity } from '../../data/utils';
import {
CatalogFilter,
CatalogFilterItem,
} from '../CatalogFilter/CatalogFilter';
import { Button, makeStyles, Typography, Link } from '@material-ui/core';
import { filterGroups, defaultFilter } from '../../data/filters';
import { Link as RouterLink } from 'react-router-dom';
import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder';
import GitHub from '@material-ui/icons/GitHub';
import {
Entity,
Location,
LOCATION_ANNOTATION,
} from '@backstage/catalog-model';
import CatalogTable from '../CatalogTable/CatalogTable';
const useStyles = makeStyles(theme => ({
contentWrapper: {
@@ -57,10 +55,6 @@ const useStyles = makeStyles(theme => ({
},
}));
import { catalogApiRef } from '../..';
import { entityToComponent, findLocationForEntity } from '../../data/utils';
import { Component } from '../../data/component';
const CatalogPage: FC<{}> = () => {
const catalogApi = useApi(catalogApiRef);
const { value, error, loading } = useAsync(() => catalogApi.getEntities());
@@ -74,26 +68,6 @@ const CatalogPage: FC<{}> = () => {
);
const styles = useStyles();
const { value: locations } = useAsync(async () => {
const getLocationDataForEntities = async (entities: Entity[]) => {
return Promise.all(
entities.map(entity => {
const locationId = entity.metadata.annotations?.[LOCATION_ANNOTATION];
if (!locationId) return undefined;
return catalogApi.getLocationById(locationId);
}),
);
};
if (value) {
return getLocationDataForEntities(value).then(
(location): Location[] =>
location.filter(loc => !!loc) as Array<Location>,
);
}
return [];
}, [value, catalogApi, catalogApi]);
const actions = [
(rowData: Component) => ({
icon: GitHub,
@@ -171,24 +145,22 @@ const CatalogPage: FC<{}> = () => {
onSelectedChange={onFilterSelected}
/>
</div>
{locations && (
<CatalogTable
titlePreamble={selectedFilter.label}
components={
(value &&
value.map(val => {
return {
...entityToComponent(val),
location: findLocationForEntity(val, locations),
};
})) ||
[]
}
loading={loading}
error={error}
actions={actions}
/>
)}
<CatalogTable
titlePreamble={selectedFilter.label}
components={
(value &&
value.map(val => {
return {
...entityToComponent(val),
locationSpec: findLocationForEntity(val),
};
})) ||
[]
}
loading={loading}
error={error}
actions={actions}
/>
</div>
</Content>
</Page>
@@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC } from 'react';
import { Component } from '../../data/component';
import { InfoCard, Progress, Table, TableColumn } from '@backstage/core';
import { Typography, Link } from '@material-ui/core';
import { Link as RouterLink, generatePath } from 'react-router-dom';
import { Link, Typography } from '@material-ui/core';
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[] = [
@@ -51,6 +51,7 @@ type CatalogTableProps = {
error?: any;
actions?: any;
};
const CatalogTable: FC<CatalogTableProps> = ({
components,
loading,
@@ -60,16 +61,17 @@ const CatalogTable: FC<CatalogTableProps> = ({
}) => {
if (loading) {
return <Progress />;
}
if (error) {
} else if (error) {
return (
<InfoCard>
<Typography variant="subtitle1" paragraph>
Error encountered while fetching components.
</Typography>
<Typography>{error}</Typography>
</InfoCard>
);
}
return (
<Table
columns={columns}
@@ -80,4 +82,5 @@ const CatalogTable: FC<CatalogTableProps> = ({
/>
);
};
export default CatalogTable;
@@ -81,7 +81,7 @@ const ComponentContextMenu: FC<ComponentContextMenuProps> = ({
<ListItemIcon>
<SwapHoriz fontSize="small" />
</ListItemIcon>
<Typography variant="inherit">More repository</Typography>
<Typography variant="inherit">Move repository</Typography>
</MenuItem>
</Menu>
</div>
@@ -13,7 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC } from 'react';
import { Entity, LOCATION_ANNOTATION } from '@backstage/catalog-model';
import { Progress, useApi } from '@backstage/core';
import {
Button,
Dialog,
@@ -23,12 +24,15 @@ import {
DialogTitle,
useMediaQuery,
useTheme,
List,
ListItem,
ListItemText,
} from '@material-ui/core';
import { Component } from '../../data/component';
import React, { FC } from 'react';
import { useAsync } from 'react-use';
import { useApi } from '@backstage/core';
import { AsyncState } from 'react-use/lib/useAsync';
import { catalogApiRef } from '../../api/types';
import { Entity } from '@backstage/catalog-model';
import { Component } from '../../data/component';
type ComponentRemovalDialogProps = {
onConfirm: () => any;
@@ -36,44 +40,81 @@ type ComponentRemovalDialogProps = {
onClose: () => any;
component: Component;
};
function useColocatedEntities(component: Component): AsyncState<Entity[]> {
const catalogApi = useApi(catalogApiRef);
return useAsync(async () => {
const myLocation = component.metadata.annotations?.[LOCATION_ANNOTATION];
return myLocation
? await catalogApi.getEntities({ [LOCATION_ANNOTATION]: myLocation })
: [];
}, [catalogApi, component]);
}
const ComponentRemovalDialog: FC<ComponentRemovalDialogProps> = ({
onConfirm,
onCancel,
onClose,
component,
}) => {
const catalogApi = useApi(catalogApiRef);
const { value } = useAsync(async () => {
let colocatedEntities: Array<Entity> = [];
const locationId = component.location?.id;
if (locationId) {
colocatedEntities = await catalogApi.getEntitiesByLocationId(locationId);
}
return colocatedEntities;
});
const { value: entities, loading, error } = useColocatedEntities(component);
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const infoMessage = `This action will unregister ${
value ? value.map(e => e.metadata.name).join(', ') : ''
} from location with target ${component.location?.target}. To undo,
just re-register the component in Backstage.`;
return (
<Dialog fullScreen={fullScreen} open onClose={onClose}>
<DialogTitle id="responsive-dialog-title">
Are you sure you want to unregister this component?
</DialogTitle>
<DialogContent>
<DialogContentText>{infoMessage}</DialogContentText>
{loading ? <Progress /> : null}
{error ? (
<DialogContentText>{error.toString()}</DialogContentText>
) : 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>
<DialogContentText>
That are located at the following location:
</DialogContentText>
<List dense>
<ListItem>
<ListItemText
primary={
entities[0]?.metadata?.annotations?.[LOCATION_ANNOTATION]
}
/>
</ListItem>
</List>
<DialogContentText>
To undo, just re-register the component in Backstage.
</DialogContentText>
</>
) : null}
</DialogContent>
<DialogActions>
<Button onClick={onCancel} color="primary">
Cancel
</Button>
<Button onClick={onConfirm} color="primary">
<Button
disabled={!!(loading || error)}
onClick={onConfirm}
color="primary"
>
Unregister
</Button>
</DialogActions>
</Dialog>
);
};
export default ComponentRemovalDialog;
+2 -1
View File
@@ -13,12 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EntityMeta, Location } from '@backstage/catalog-model';
import { ReactNode } from 'react';
import { Location } from '@backstage/catalog-model';
export type Component = {
name: string;
kind: string;
metadata: EntityMeta;
description: ReactNode;
location?: Location;
};
+19 -12
View File
@@ -19,6 +19,7 @@ import {
Entity,
Location,
LOCATION_ANNOTATION,
LocationSpec,
} from '@backstage/catalog-model';
import Edit from '@material-ui/icons/Edit';
import IconButton from '@material-ui/core/IconButton';
@@ -38,13 +39,12 @@ const createEditLink = (location: Location): string => {
}
};
export function entityToComponent(
envelope: Entity,
location?: Location,
): Component {
export function entityToComponent(envelope: Entity): Component {
const location = findLocationForEntity(envelope);
return {
name: envelope.metadata?.name ?? '',
kind: envelope.kind ?? 'unknown',
metadata: envelope.metadata,
description: (
<DescriptionWrapper>
{envelope.metadata?.annotations?.description ?? 'placeholder'}
@@ -57,18 +57,25 @@ export function entityToComponent(
) : null}
</DescriptionWrapper>
),
location,
location: findLocationForEntity(envelope),
};
}
export function findLocationForEntity(
entity: Entity,
locations: Location[],
): Location | undefined {
for (const loc of locations) {
if (loc.id === entity.metadata.annotations?.[LOCATION_ANNOTATION]) {
return loc;
}
): LocationSpec | undefined {
const annotation = entity.metadata.annotations?.[LOCATION_ANNOTATION];
if (!annotation) {
return undefined;
}
return undefined;
const separatorIndex = annotation.indexOf(':');
if (separatorIndex === -1) {
return undefined;
}
return {
type: annotation.substring(0, separatorIndex),
target: annotation.substring(separatorIndex + 1),
};
}