refactor: address PR comments
This commit is contained in:
@@ -13,10 +13,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useContext } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router';
|
||||
|
||||
import { useEntity } from '../../hooks/useEntity';
|
||||
import { EntityContext } from '../../hooks/useEntity';
|
||||
import {
|
||||
pageTheme,
|
||||
PageTheme,
|
||||
@@ -74,7 +74,7 @@ function headerProps(
|
||||
export const EntityPageLayout = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
}) => {
|
||||
const { optionalNamespaceAndName, kind } = useParams() as {
|
||||
optionalNamespaceAndName: string;
|
||||
@@ -82,7 +82,7 @@ export const EntityPageLayout = ({
|
||||
};
|
||||
const [name, namespace] = optionalNamespaceAndName.split(':').reverse();
|
||||
|
||||
const { entity, loading, error } = useEntity();
|
||||
const { entity, loading, error } = useContext(EntityContext);
|
||||
const { headerTitle, headerType } = headerProps(
|
||||
kind,
|
||||
namespace,
|
||||
@@ -139,5 +139,4 @@ export const EntityPageLayout = ({
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
EntityPageLayout.Content = Tabbed.Content;
|
||||
|
||||
@@ -36,6 +36,21 @@ describe('Tabbed layout', () => {
|
||||
expect(rendered.getByText('tabbed-test-content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('throws if any other component is a child of Tabbed.Layout', async () => {
|
||||
await expect(
|
||||
renderInTestApp(
|
||||
<Tabbed.Layout>
|
||||
<Tabbed.Content
|
||||
title="tabbed-test-title"
|
||||
path="*"
|
||||
element={<div>tabbed-test-content</div>}
|
||||
/>
|
||||
<div>This will cause app to throw</div>
|
||||
</Tabbed.Layout>,
|
||||
),
|
||||
).rejects.toThrow(/EntityPageLayout component only accepts/);
|
||||
});
|
||||
|
||||
it('navigates when user clicks different tab', async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Routes>
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
RouteMatch,
|
||||
} from 'react-router';
|
||||
import { Tab, HeaderTabs, Content } from '@backstage/core';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const getSelectedIndexOrDefault = (
|
||||
@@ -67,6 +66,11 @@ export const Tabbed = {
|
||||
// Skip conditionals resolved to falses/nulls/undefineds etc
|
||||
return;
|
||||
}
|
||||
if (child.type !== Tabbed.Content) {
|
||||
throw new Error(
|
||||
'This component only accepts Content elements as direct children. Check the code of the EntityPage.',
|
||||
);
|
||||
}
|
||||
const pathAndId = (child as JSX.Element).props.path;
|
||||
|
||||
// Child here must be then always a functional component without any wrappers
|
||||
@@ -92,7 +96,7 @@ export const Tabbed = {
|
||||
matchRoutes(routes as RouteObject[], `/${params['*']}`) ?? [];
|
||||
const selectedIndex = getSelectedIndexOrDefault(matchedRoute, tabs);
|
||||
const currentTab = tabs[selectedIndex];
|
||||
const title = currentTab.label;
|
||||
const title = currentTab?.label;
|
||||
|
||||
const onTabChange = (index: number) =>
|
||||
// Remove trailing /*
|
||||
@@ -103,6 +107,7 @@ export const Tabbed = {
|
||||
|
||||
const currentRouteElement = useRoutes(routes);
|
||||
|
||||
if (!currentTab) return null;
|
||||
return (
|
||||
<>
|
||||
<HeaderTabs
|
||||
@@ -111,10 +116,8 @@ export const Tabbed = {
|
||||
onChange={onTabChange}
|
||||
/>
|
||||
<Content>
|
||||
<Grid container spacing={3}>
|
||||
<Helmet title={title} />
|
||||
{currentRouteElement}
|
||||
</Grid>
|
||||
<Helmet title={title} />
|
||||
{currentRouteElement}
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -14,13 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { ComponentType } from 'react';
|
||||
import { CatalogPage } from './components/CatalogPage';
|
||||
import { EntityPageLayout } from './components/EntityPageLayout';
|
||||
import { CatalogPage } from './CatalogPage';
|
||||
import { EntityPageLayout } from './EntityPageLayout';
|
||||
import { Route, Routes } from 'react-router';
|
||||
import { entityRoute, rootRoute, entityRouteDefault } from './routes';
|
||||
import { entityRoute, rootRoute } from '../routes';
|
||||
import { Content } from '@backstage/core';
|
||||
import { Typography, Link } from '@material-ui/core';
|
||||
import { EntityProvider } from './components/EntityProvider';
|
||||
import { EntityProvider } from './EntityProvider';
|
||||
import { useEntity } from '../hooks/useEntity';
|
||||
|
||||
const DefaultEntityPage = () => (
|
||||
<EntityPageLayout>
|
||||
@@ -43,7 +44,17 @@ const DefaultEntityPage = () => (
|
||||
</EntityPageLayout>
|
||||
);
|
||||
|
||||
export const CatalogRouter = ({
|
||||
const EntityPageSwitch = ({ EntityPage }: { EntityPage: ComponentType }) => {
|
||||
const { entity } = useEntity();
|
||||
// Loading and error states
|
||||
if (!entity) return <EntityPageLayout />;
|
||||
|
||||
// Otherwise EntityPage provided from the App
|
||||
// Note that EntityPage will include EntityPageLayout already
|
||||
return <EntityPage />;
|
||||
};
|
||||
|
||||
export const Router = ({
|
||||
EntityPage = DefaultEntityPage,
|
||||
}: {
|
||||
EntityPage?: ComponentType;
|
||||
@@ -54,15 +65,7 @@ export const CatalogRouter = ({
|
||||
path={`/${entityRoute.path}`}
|
||||
element={
|
||||
<EntityProvider>
|
||||
<EntityPage />
|
||||
</EntityProvider>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={`/${entityRouteDefault.path}`}
|
||||
element={
|
||||
<EntityProvider>
|
||||
<EntityPage />
|
||||
<EntityPageSwitch EntityPage={EntityPage} />
|
||||
</EntityProvider>
|
||||
}
|
||||
/>
|
||||
@@ -24,13 +24,13 @@ const REDIRECT_DELAY = 2000;
|
||||
|
||||
type EntityLoadingStatus = {
|
||||
entity?: Entity;
|
||||
loading: boolean | null;
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
};
|
||||
|
||||
export const EntityContext = createContext<EntityLoadingStatus>({
|
||||
entity: undefined as any,
|
||||
loading: null,
|
||||
entity: undefined,
|
||||
loading: true,
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
@@ -53,25 +53,20 @@ export const useEntityFromUrl = (): EntityLoadingStatus => {
|
||||
navigate('/');
|
||||
}, REDIRECT_DELAY);
|
||||
}
|
||||
}, [errorApi, navigate, error, loading, entity]);
|
||||
|
||||
if (!name) {
|
||||
navigate('/catalog');
|
||||
return {
|
||||
entity: undefined,
|
||||
loading: null,
|
||||
error: new Error('No name in url'),
|
||||
} as never;
|
||||
}
|
||||
if (!name) {
|
||||
errorApi.post(new Error('No name provided!'));
|
||||
navigate('/');
|
||||
}
|
||||
}, [errorApi, navigate, error, loading, entity, name]);
|
||||
|
||||
return { entity, loading, error };
|
||||
};
|
||||
|
||||
/**
|
||||
* Always going to return an entity, or throw an error if not a descendant of a EntityProvider.
|
||||
* Otherwise the useEntityFromUrl will take care of the `undefined` entity
|
||||
*/
|
||||
export const useEntity = () =>
|
||||
useContext<Omit<EntityLoadingStatus, 'entity'> & { entity: Entity }>(
|
||||
EntityContext as any,
|
||||
);
|
||||
export const useEntity = () => {
|
||||
const { entity } = useContext<{ entity: Entity }>(EntityContext as any);
|
||||
return { entity };
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ export * from './api/CatalogClient';
|
||||
export * from './api/types';
|
||||
export * from './routes';
|
||||
export { useEntityCompoundName } from './components/useEntityCompoundName';
|
||||
export * from './Router';
|
||||
export { Router } from './components/Router';
|
||||
export { useEntity } from './hooks/useEntity';
|
||||
export { AboutCard } from './components/AboutCard';
|
||||
export { EntityPageLayout } from './components/EntityPageLayout';
|
||||
|
||||
@@ -28,8 +28,3 @@ export const entityRoute = createRouteRef({
|
||||
path: ':kind/:optionalNamespaceAndName/*',
|
||||
title: 'Entity',
|
||||
});
|
||||
export const entityRouteDefault = createRouteRef({
|
||||
icon: NoIcon,
|
||||
path: ':kind/:optionalNamespaceAndName/*',
|
||||
title: 'Entity',
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ import { useJobPolling } from './useJobPolling';
|
||||
import { Job } from '../../types';
|
||||
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import { Button } from '@backstage/core';
|
||||
import { entityRouteDefault } from '@backstage/plugin-catalog';
|
||||
import { entityRoute } from '@backstage/plugin-catalog';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
|
||||
type Props = {
|
||||
@@ -72,7 +72,7 @@ export const JobStatusModal = ({
|
||||
{entity && (
|
||||
<DialogActions>
|
||||
<Button
|
||||
to={generatePath(entityRouteDefault.path, {
|
||||
to={generatePath(entityRoute.path, {
|
||||
kind: entity.kind,
|
||||
optionalNamespaceAndName: [
|
||||
entity.metadata.namespace,
|
||||
|
||||
Reference in New Issue
Block a user