refactor: address PR comments

This commit is contained in:
Ivan Shmidt
2020-09-02 15:56:14 +02:00
parent 9a99a92f84
commit 9988479c61
16 changed files with 86 additions and 78 deletions
@@ -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>
}
/>
+12 -17
View File
@@ -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 };
};
+1 -1
View File
@@ -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';
-5
View File
@@ -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,