diff --git a/.changeset/rocket-lizards-congregate.md b/.changeset/rocket-lizards-congregate.md new file mode 100644 index 0000000000..f4cd50aa36 --- /dev/null +++ b/.changeset/rocket-lizards-congregate.md @@ -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. diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index ea99753c14..00be785754 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -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'; diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 4e63c6099b..4d97ea4334 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -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[] = [ diff --git a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx index fa5a6292af..8c48d69413 100644 --- a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx +++ b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx @@ -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, diff --git a/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx b/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx index bd2c71891d..ec1857fcf8 100644 --- a/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx +++ b/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx @@ -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 & { entity: Entity }; diff --git a/plugins/catalog/src/components/Router.tsx b/plugins/catalog/src/components/Router.tsx index 0a002ba907..fe1ba3a593 100644 --- a/plugins/catalog/src/components/Router.tsx +++ b/plugins/catalog/src/components/Router.tsx @@ -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 = () => ( @@ -56,6 +57,18 @@ const EntityPageSwitch = ({ EntityPage }: { EntityPage: ComponentType }) => { return ; }; +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 ( + + ); +}; + export const Router = ({ EntityPage = DefaultEntityPage, }: { @@ -71,5 +84,9 @@ export const Router = ({ } /> + } + /> ); diff --git a/plugins/catalog/src/components/useEntityCompoundName.ts b/plugins/catalog/src/components/useEntityCompoundName.ts index 2be23cb6d9..dd9c0d26ff 100644 --- a/plugins/catalog/src/components/useEntityCompoundName.ts +++ b/plugins/catalog/src/components/useEntityCompoundName.ts @@ -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 }; }; diff --git a/plugins/catalog/src/hooks/useEntity.ts b/plugins/catalog/src/hooks/useEntity.ts index 77ea8c1569..1c5b94b8c1 100644 --- a/plugins/catalog/src/hooks/useEntity.ts +++ b/plugins/catalog/src/hooks/useEntity.ts @@ -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({ }); 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); diff --git a/plugins/catalog/src/hooks/useStarredEntities.test.tsx b/plugins/catalog/src/hooks/useStarredEntities.test.tsx index 6c98690553..345bc9f04f 100644 --- a/plugins/catalog/src/hooks/useStarredEntities.test.tsx +++ b/plugins/catalog/src/hooks/useStarredEntities.test.tsx @@ -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, diff --git a/plugins/catalog/src/hooks/useStarredEntites.ts b/plugins/catalog/src/hooks/useStarredEntities.ts similarity index 100% rename from plugins/catalog/src/hooks/useStarredEntites.ts rename to plugins/catalog/src/hooks/useStarredEntities.ts diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog/src/routes.ts index 14e23ff73b..a39883e92b 100644 --- a/plugins/catalog/src/routes.ts +++ b/plugins/catalog/src/routes.ts @@ -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; +} diff --git a/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx index c75235c6cb..9631865ffb 100644 --- a/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx +++ b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx @@ -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; }; diff --git a/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx b/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx index c531e0ec2c..b97a19148e 100644 --- a/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx +++ b/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx @@ -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 && (