Merge pull request #1199 from spotify/freben/unregister
Unbreak the unregister dialog after catalog changes
This commit is contained in:
@@ -57,11 +57,14 @@ const SidebarLogo: FC<{}> = () => {
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<NavLink to="/">
|
||||
<Link underline="none" className={classes.link}>
|
||||
{isOpen ? <LogoFull /> : <LogoIcon />}
|
||||
</Link>
|
||||
</NavLink>
|
||||
<Link
|
||||
component={NavLink}
|
||||
to="/"
|
||||
underline="none"
|
||||
className={classes.link}
|
||||
>
|
||||
{isOpen ? <LogoFull /> : <LogoIcon />}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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, findLocationForEntityMeta } 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,37 +68,19 @@ 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,
|
||||
tooltip: 'View on GitHub',
|
||||
onClick: () => {
|
||||
if (!rowData || !rowData.location) return;
|
||||
window.open(rowData.location.target, '_blank');
|
||||
},
|
||||
hidden:
|
||||
rowData && rowData.location ? rowData.location.type !== 'github' : true,
|
||||
}),
|
||||
(rowData: Component) => {
|
||||
const location = findLocationForEntityMeta(rowData.metadata);
|
||||
return {
|
||||
icon: GitHub,
|
||||
tooltip: 'View on GitHub',
|
||||
onClick: () => {
|
||||
if (!location) return;
|
||||
window.open(location.target, '_blank');
|
||||
},
|
||||
hidden: location ? location?.type !== 'github' : true,
|
||||
};
|
||||
},
|
||||
];
|
||||
|
||||
// TODO: replace me with the proper tabs implemntation
|
||||
@@ -171,24 +147,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: findLocationForEntityMeta(val.metadata),
|
||||
};
|
||||
})) ||
|
||||
[]
|
||||
}
|
||||
loading={loading}
|
||||
error={error}
|
||||
actions={actions}
|
||||
/>
|
||||
</div>
|
||||
</Content>
|
||||
</Page>
|
||||
|
||||
@@ -20,9 +20,24 @@ import CatalogTable from './CatalogTable';
|
||||
import { Component } from '../../data/component';
|
||||
|
||||
const components: Component[] = [
|
||||
{ name: 'component1', kind: 'Component', description: 'Placeholder' },
|
||||
{ name: 'component2', kind: 'Component', description: 'Placeholder' },
|
||||
{ name: 'component3', kind: 'Component', description: 'Placeholder' },
|
||||
{
|
||||
name: 'component1',
|
||||
kind: 'Component',
|
||||
metadata: { name: 'component1' },
|
||||
description: 'Placeholder',
|
||||
},
|
||||
{
|
||||
name: 'component2',
|
||||
kind: 'Component',
|
||||
metadata: { name: 'component2' },
|
||||
description: 'Placeholder',
|
||||
},
|
||||
{
|
||||
name: 'component3',
|
||||
kind: 'Component',
|
||||
metadata: { name: 'component3' },
|
||||
description: 'Placeholder',
|
||||
},
|
||||
];
|
||||
|
||||
describe('CatalogTable component', () => {
|
||||
@@ -48,7 +63,7 @@ describe('CatalogTable component', () => {
|
||||
),
|
||||
);
|
||||
const errorMessage = await rendered.findByText(
|
||||
'Error encountered while fetching components.',
|
||||
/Error encountered while fetching components./,
|
||||
);
|
||||
expect(errorMessage).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Progress, 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 { InfoCard, Progress, Table, TableColumn } from '@backstage/core';
|
||||
import { Typography, Link } from '@material-ui/core';
|
||||
import { Link as RouterLink, generatePath } from 'react-router-dom';
|
||||
import { entityRoute } from '../../routes';
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
@@ -51,6 +52,7 @@ type CatalogTableProps = {
|
||||
error?: any;
|
||||
actions?: any;
|
||||
};
|
||||
|
||||
const CatalogTable: FC<CatalogTableProps> = ({
|
||||
components,
|
||||
loading,
|
||||
@@ -60,16 +62,16 @@ 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>
|
||||
</InfoCard>
|
||||
<div>
|
||||
<Alert severity="error">
|
||||
Error encountered while fetching components. {error.toString()}
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
@@ -80,4 +82,5 @@ const CatalogTable: FC<CatalogTableProps> = ({
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CatalogTable;
|
||||
|
||||
@@ -13,22 +13,24 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { FC, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
IconButton,
|
||||
ListItemIcon,
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Popover,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import Cancel from '@material-ui/icons/Cancel';
|
||||
import MoreVert from '@material-ui/icons/MoreVert';
|
||||
import SwapHoriz from '@material-ui/icons/SwapHoriz';
|
||||
import React, { FC, useState } from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
// TODO(freben): It should probably instead be the case that Header sets the theme text color to white inside itself unconditionally instead
|
||||
const useStyles = makeStyles({
|
||||
menu: {
|
||||
marginTop: 52,
|
||||
button: {
|
||||
color: 'white',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -39,52 +41,58 @@ type ComponentContextMenuProps = {
|
||||
const ComponentContextMenu: FC<ComponentContextMenuProps> = ({
|
||||
onUnregisterComponent,
|
||||
}) => {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const menuAnchor = useRef<HTMLDivElement>(null);
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement>();
|
||||
const classes = useStyles();
|
||||
|
||||
useEffect(() => {
|
||||
const globalCloseHandler = (event: any) => {
|
||||
const menu = menuAnchor.current;
|
||||
if (menu !== null && !menu.contains(event.target)) {
|
||||
setMenuOpen(false);
|
||||
}
|
||||
};
|
||||
const onOpen = (event: React.SyntheticEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
window.addEventListener('click', globalCloseHandler);
|
||||
return () => window.removeEventListener('click', globalCloseHandler);
|
||||
}, [menuOpen]);
|
||||
const onClose = () => {
|
||||
setAnchorEl(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={menuAnchor}>
|
||||
<div>
|
||||
<IconButton
|
||||
aria-label="more"
|
||||
aria-controls="long-menu"
|
||||
aria-haspopup="true"
|
||||
onClick={() => setMenuOpen(!menuOpen)}
|
||||
onClick={onOpen}
|
||||
data-testid="menu-button"
|
||||
className={classes.button}
|
||||
>
|
||||
<MoreVert />
|
||||
</IconButton>
|
||||
<Menu
|
||||
open={menuOpen}
|
||||
anchorEl={menuAnchor.current}
|
||||
className={classes.menu}
|
||||
<Popover
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={onClose}
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
>
|
||||
<MenuItem onClick={onUnregisterComponent}>
|
||||
<ListItemIcon>
|
||||
<Cancel fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<Typography variant="inherit">Unregister component</Typography>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<ListItemIcon>
|
||||
<SwapHoriz fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<Typography variant="inherit">More repository</Typography>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onClose();
|
||||
onUnregisterComponent();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Cancel fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<Typography variant="inherit">Unregister component</Typography>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<ListItemIcon>
|
||||
<SwapHoriz fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<Typography variant="inherit">Move repository</Typography>
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComponentContextMenu;
|
||||
|
||||
@@ -23,6 +23,7 @@ describe('ComponentMetadataCard component', () => {
|
||||
const testComponent: Component = {
|
||||
name: 'test',
|
||||
kind: 'Component',
|
||||
metadata: { name: 'test' },
|
||||
description: 'Placeholder',
|
||||
};
|
||||
const rendered = await render(
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
* 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,
|
||||
@@ -21,14 +23,16 @@ import {
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from '@material-ui/core';
|
||||
import { Component } from '../../data/component';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
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,79 @@ 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 ? (
|
||||
<Alert severity="error" style={{ wordBreak: 'break-word' }}>
|
||||
{error.toString()}
|
||||
</Alert>
|
||||
) : null}
|
||||
{entities ? (
|
||||
<>
|
||||
<DialogContentText>
|
||||
This action will unregister the following entities:
|
||||
</DialogContentText>
|
||||
<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>
|
||||
<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>
|
||||
</>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={onConfirm} color="primary">
|
||||
<Button onClick={onCancel}>Cancel</Button>
|
||||
<Button
|
||||
disabled={!!(loading || error)}
|
||||
onClick={onConfirm}
|
||||
color="secondary"
|
||||
>
|
||||
Unregister
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComponentRemovalDialog;
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { EntityMeta } 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;
|
||||
};
|
||||
|
||||
@@ -13,23 +13,24 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Component } from './component';
|
||||
import {
|
||||
Entity,
|
||||
Location,
|
||||
LocationSpec,
|
||||
LOCATION_ANNOTATION,
|
||||
EntityMeta,
|
||||
} from '@backstage/catalog-model';
|
||||
import Edit from '@material-ui/icons/Edit';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import { styled } from '@material-ui/core/styles';
|
||||
import Edit from '@material-ui/icons/Edit';
|
||||
import React from 'react';
|
||||
import { Component } from './component';
|
||||
|
||||
const DescriptionWrapper = styled('span')({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
const createEditLink = (location: Location): string => {
|
||||
const createEditLink = (location: LocationSpec): string => {
|
||||
switch (location.type) {
|
||||
case 'github':
|
||||
return location.target.replace('/blob/', '/edit/');
|
||||
@@ -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 = findLocationForEntityMeta(envelope.metadata);
|
||||
return {
|
||||
name: envelope.metadata?.name ?? '',
|
||||
kind: envelope.kind ?? 'unknown',
|
||||
metadata: envelope.metadata,
|
||||
description: (
|
||||
<DescriptionWrapper>
|
||||
{envelope.metadata?.annotations?.description ?? 'placeholder'}
|
||||
@@ -57,18 +57,28 @@ export function entityToComponent(
|
||||
) : null}
|
||||
</DescriptionWrapper>
|
||||
),
|
||||
location,
|
||||
};
|
||||
}
|
||||
|
||||
export function findLocationForEntity(
|
||||
entity: Entity,
|
||||
locations: Location[],
|
||||
): Location | undefined {
|
||||
for (const loc of locations) {
|
||||
if (loc.id === entity.metadata.annotations?.[LOCATION_ANNOTATION]) {
|
||||
return loc;
|
||||
}
|
||||
export function findLocationForEntityMeta(
|
||||
meta: EntityMeta,
|
||||
): LocationSpec | undefined {
|
||||
if (!meta) {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
|
||||
const annotation = meta.annotations?.[LOCATION_ANNOTATION];
|
||||
if (!annotation) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const separatorIndex = annotation.indexOf(':');
|
||||
if (separatorIndex === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
type: annotation.substring(0, separatorIndex),
|
||||
target: annotation.substring(separatorIndex + 1),
|
||||
};
|
||||
}
|
||||
|
||||
-1
@@ -31,7 +31,6 @@ const catalogApi: jest.Mocked<typeof catalogApiRef.T> = {
|
||||
getEntityByName: jest.fn(),
|
||||
getLocationByEntity: jest.fn(),
|
||||
getLocationById: jest.fn(),
|
||||
getEntitiesByLocationId: jest.fn(),
|
||||
};
|
||||
|
||||
const setup = () => ({
|
||||
|
||||
Reference in New Issue
Block a user