fix(catalog): change route pattern to entities (#3133)
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': minor
|
||||
---
|
||||
|
||||
The URL path for a catalog entity has changed,
|
||||
|
||||
- from: `/catalog/:kind/:optionalNamespaceAndName`
|
||||
- to: `/catalog/:namespace/:kind/:name`
|
||||
|
||||
Redirects are in place, so disruptions for users should not happen.
|
||||
@@ -31,7 +31,7 @@ import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { catalogApiRef } from '../../api/types';
|
||||
import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter';
|
||||
import { useStarredEntities } from '../../hooks/useStarredEntites';
|
||||
import { useStarredEntities } from '../../hooks/useStarredEntities';
|
||||
import { ButtonGroup, CatalogFilter } from '../CatalogFilter/CatalogFilter';
|
||||
import { CatalogTable } from '../CatalogTable/CatalogTable';
|
||||
import { ResultsFilter } from '../ResultsFilter/ResultsFilter';
|
||||
|
||||
@@ -22,8 +22,8 @@ import { Alert } from '@material-ui/lab';
|
||||
import React from 'react';
|
||||
import { generatePath, Link as RouterLink } from 'react-router-dom';
|
||||
import { findLocationForEntityMeta } from '../../data/utils';
|
||||
import { useStarredEntities } from '../../hooks/useStarredEntites';
|
||||
import { entityRoute } from '../../routes';
|
||||
import { useStarredEntities } from '../../hooks/useStarredEntities';
|
||||
import { entityRoute, entityRouteParams } from '../../routes';
|
||||
import {
|
||||
favouriteEntityIcon,
|
||||
favouriteEntityTooltip,
|
||||
@@ -38,13 +38,7 @@ const columns: TableColumn<Entity>[] = [
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={generatePath(entityRoute.path, {
|
||||
optionalNamespaceAndName: [
|
||||
entity.metadata.namespace,
|
||||
entity.metadata.name,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(':'),
|
||||
kind: entity.kind,
|
||||
...entityRouteParams(entity),
|
||||
selectedTabId: 'overview',
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -13,17 +13,17 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { useState, useContext } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router';
|
||||
|
||||
import { EntityContext } from '../../hooks/useEntity';
|
||||
import { Page, Header, HeaderLabel, Content, Progress } from '@backstage/core';
|
||||
import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model';
|
||||
import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity';
|
||||
import { Content, Header, HeaderLabel, Page, Progress } from '@backstage/core';
|
||||
import { Box } from '@material-ui/core';
|
||||
import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu';
|
||||
import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import React, { PropsWithChildren, useContext, useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { EntityContext } from '../../hooks/useEntity';
|
||||
import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu';
|
||||
import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity';
|
||||
import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog';
|
||||
import { useEntityCompoundName } from '../useEntityCompoundName';
|
||||
import { Tabbed } from './Tabbed';
|
||||
|
||||
const EntityPageTitle = ({
|
||||
@@ -62,17 +62,8 @@ function headerProps(
|
||||
};
|
||||
}
|
||||
|
||||
export const EntityPageLayout = ({
|
||||
children,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
}) => {
|
||||
const { optionalNamespaceAndName, kind } = useParams() as {
|
||||
optionalNamespaceAndName: string;
|
||||
kind: string;
|
||||
};
|
||||
const [name, namespace] = optionalNamespaceAndName.split(':').reverse();
|
||||
|
||||
export const EntityPageLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
const { kind, namespace, name } = useEntityCompoundName();
|
||||
const { entity, loading, error } = useContext(EntityContext);
|
||||
const { headerTitle, headerType } = headerProps(
|
||||
kind,
|
||||
|
||||
@@ -18,7 +18,7 @@ import React, { ComponentProps } from 'react';
|
||||
import { IconButton, Tooltip, withStyles } from '@material-ui/core';
|
||||
import StarBorder from '@material-ui/icons/StarBorder';
|
||||
import Star from '@material-ui/icons/Star';
|
||||
import { useStarredEntities } from '../../hooks/useStarredEntites';
|
||||
import { useStarredEntities } from '../../hooks/useStarredEntities';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
type Props = ComponentProps<typeof IconButton> & { entity: Entity };
|
||||
|
||||
@@ -13,16 +13,17 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model';
|
||||
import { Content } from '@backstage/core';
|
||||
import { Link, Typography } from '@material-ui/core';
|
||||
import React, { ComponentType } from 'react';
|
||||
import { Navigate, Route, Routes, useParams } from 'react-router';
|
||||
import { useEntity } from '../hooks/useEntity';
|
||||
import { entityRoute, rootRoute } from '../routes';
|
||||
import { CatalogPage } from './CatalogPage';
|
||||
import { EntityNotFound } from './EntityNotFound';
|
||||
import { EntityPageLayout } from './EntityPageLayout';
|
||||
import { Route, Routes } from 'react-router';
|
||||
import { entityRoute, rootRoute } from '../routes';
|
||||
import { Content } from '@backstage/core';
|
||||
import { Typography, Link } from '@material-ui/core';
|
||||
import { EntityProvider } from './EntityProvider';
|
||||
import { useEntity } from '../hooks/useEntity';
|
||||
|
||||
const DefaultEntityPage = () => (
|
||||
<EntityPageLayout>
|
||||
@@ -56,6 +57,18 @@ const EntityPageSwitch = ({ EntityPage }: { EntityPage: ComponentType }) => {
|
||||
return <EntityPage />;
|
||||
};
|
||||
|
||||
const OldEntityRouteRedirect = () => {
|
||||
const { optionalNamespaceAndName, '*': rest } = useParams() as any;
|
||||
const [name, namespace] = optionalNamespaceAndName.split(':').reverse();
|
||||
const namespaceLower = namespace?.toLowerCase() ?? ENTITY_DEFAULT_NAMESPACE;
|
||||
const restWithSlash = rest ? `/${rest}` : '';
|
||||
return (
|
||||
<Navigate
|
||||
to={`../../${namespaceLower}/component/${name}${restWithSlash}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Router = ({
|
||||
EntityPage = DefaultEntityPage,
|
||||
}: {
|
||||
@@ -71,5 +84,9 @@ export const Router = ({
|
||||
</EntityProvider>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="Component/:optionalNamespaceAndName/*"
|
||||
element={<OldEntityRouteRedirect />}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
|
||||
@@ -16,11 +16,9 @@
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
/**
|
||||
* Grabs entity kind and name + optional namespace from location
|
||||
* Grabs entity kind, namespace, and name from the location
|
||||
*/
|
||||
export const useEntityCompoundName = () => {
|
||||
const params = useParams();
|
||||
const { kind, optionalNamespaceAndName = '' } = params;
|
||||
const [name, namespace] = optionalNamespaceAndName.split(':').reverse();
|
||||
return { kind, name, namespace };
|
||||
const { kind, namespace, name } = useParams();
|
||||
return { kind, namespace, name };
|
||||
};
|
||||
|
||||
@@ -13,12 +13,13 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { useEffect, createContext, useContext } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import { useApi, errorApiRef } from '@backstage/core';
|
||||
import { catalogApiRef } from '../api/types';
|
||||
import { useAsync } from 'react-use';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { errorApiRef, useApi } from '@backstage/core';
|
||||
import { createContext, useContext, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useAsync } from 'react-use';
|
||||
import { catalogApiRef } from '../api/types';
|
||||
import { useEntityCompoundName } from '../components/useEntityCompoundName';
|
||||
|
||||
type EntityLoadingStatus = {
|
||||
entity?: Entity;
|
||||
@@ -33,8 +34,7 @@ export const EntityContext = createContext<EntityLoadingStatus>({
|
||||
});
|
||||
|
||||
export const useEntityFromUrl = (): EntityLoadingStatus => {
|
||||
const { optionalNamespaceAndName, kind } = useParams();
|
||||
const [name, namespace] = optionalNamespaceAndName.split(':').reverse();
|
||||
const { kind, namespace, name } = useEntityCompoundName();
|
||||
const navigate = useNavigate();
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useStarredEntities } from './useStarredEntites';
|
||||
import { useStarredEntities } from './useStarredEntities';
|
||||
import {
|
||||
ApiProvider,
|
||||
ApiRegistry,
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model';
|
||||
import { createRouteRef } from '@backstage/core';
|
||||
|
||||
const NoIcon = () => null;
|
||||
@@ -25,6 +26,17 @@ export const rootRoute = createRouteRef({
|
||||
});
|
||||
export const entityRoute = createRouteRef({
|
||||
icon: NoIcon,
|
||||
path: ':kind/:optionalNamespaceAndName/*',
|
||||
path: ':namespace/:kind/:name/*',
|
||||
title: 'Entity',
|
||||
});
|
||||
|
||||
// Utility function to get suitable route params for entityRoute, given an
|
||||
// entity instance
|
||||
export function entityRouteParams(entity: Entity) {
|
||||
return {
|
||||
kind: entity.kind.toLowerCase(),
|
||||
namespace:
|
||||
entity.metadata.namespace?.toLowerCase() ?? ENTITY_DEFAULT_NAMESPACE,
|
||||
name: entity.metadata.name,
|
||||
} as const;
|
||||
}
|
||||
|
||||
+14
-20
@@ -14,23 +14,23 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { RouteRef, StructuredMetadataTable } from '@backstage/core';
|
||||
import { entityRoute, entityRouteParams } from '@backstage/plugin-catalog';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
Link,
|
||||
List,
|
||||
ListItem,
|
||||
Link,
|
||||
Divider,
|
||||
DialogActions,
|
||||
Button,
|
||||
} from '@material-ui/core';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { StructuredMetadataTable, RouteRef } from '@backstage/core';
|
||||
import React from 'react';
|
||||
import { generatePath, resolvePath } from 'react-router';
|
||||
import { entityRoute } from '@backstage/plugin-catalog';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
|
||||
type Props = {
|
||||
@@ -47,22 +47,16 @@ const getEntityCatalogPath = ({
|
||||
entity: Entity;
|
||||
catalogRouteRef: RouteRef;
|
||||
}) => {
|
||||
const optionalNamespaceAndName = [
|
||||
entity.metadata.namespace,
|
||||
entity.metadata.name,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(':');
|
||||
|
||||
const relativeEntityPathInsideCatalog = generatePath(entityRoute.path, {
|
||||
optionalNamespaceAndName,
|
||||
kind: entity.kind,
|
||||
});
|
||||
const relativeEntityPathInsideCatalog = generatePath(
|
||||
entityRoute.path,
|
||||
entityRouteParams(entity),
|
||||
);
|
||||
|
||||
const resolvedAbsolutePath = resolvePath(
|
||||
relativeEntityPathInsideCatalog,
|
||||
catalogRouteRef.path,
|
||||
)?.pathname;
|
||||
|
||||
return resolvedAbsolutePath;
|
||||
};
|
||||
|
||||
|
||||
@@ -13,21 +13,21 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
LinearProgress,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
} from '@material-ui/core';
|
||||
import { JobStage } from '../JobStage/JobStage';
|
||||
import { useJobPolling } from './useJobPolling';
|
||||
import { Job } from '../../types';
|
||||
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import { Button } from '@backstage/core';
|
||||
import { entityRoute } from '@backstage/plugin-catalog';
|
||||
import { entityRoute, entityRouteParams } from '@backstage/plugin-catalog';
|
||||
import {
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
LinearProgress,
|
||||
} from '@material-ui/core';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { Job } from '../../types';
|
||||
import { JobStage } from '../JobStage/JobStage';
|
||||
import { useJobPolling } from './useJobPolling';
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
@@ -75,15 +75,10 @@ export const JobStatusModal = ({
|
||||
{entity && (
|
||||
<DialogActions>
|
||||
<Button
|
||||
to={generatePath(`/catalog/${entityRoute.path}`, {
|
||||
kind: entity.kind,
|
||||
optionalNamespaceAndName: [
|
||||
entity.metadata.namespace,
|
||||
entity.metadata.name,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(':'),
|
||||
})}
|
||||
to={generatePath(
|
||||
`/catalog/${entityRoute.path}`,
|
||||
entityRouteParams(entity),
|
||||
)}
|
||||
>
|
||||
View in catalog
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user