Unbreak the unregister dialog after catalog changes
This commit is contained in:
committed by
Nikita Nek Dudnik
parent
920f496a7f
commit
c617b1cf33
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user