Merge pull request #9554 from backstage/jhaals/remove-router
catalog: remove Router and EntityPageLayout
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': minor
|
||||
---
|
||||
|
||||
Removed deprecated `Router` and `EntityPageLayout` exports.
|
||||
`Router` is replaced by plugin extensions and `EntityPageLayout` is replaced by `CatalogEntityPage`.
|
||||
@@ -349,22 +349,6 @@ export const EntityListContainer: ({
|
||||
// @public
|
||||
export const EntityOrphanWarning: () => JSX.Element;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "EntityPageLayout" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public @deprecated
|
||||
export const EntityPageLayout: {
|
||||
({
|
||||
children,
|
||||
UNSTABLE_extraContextMenuItems,
|
||||
UNSTABLE_contextMenuOptions,
|
||||
}: EntityPageLayoutProps): JSX.Element;
|
||||
Content: (_props: {
|
||||
path: string;
|
||||
title: string;
|
||||
element: JSX.Element;
|
||||
}) => null;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "EntityProcessingErrorsPanel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
@@ -452,15 +436,6 @@ export const RelatedEntitiesCard: <T extends Entity>(props: {
|
||||
asRenderableEntities: (entities: Entity[]) => T[];
|
||||
}) => JSX.Element;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "Router" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public @deprecated (undocumented)
|
||||
export const Router: ({
|
||||
EntityPage,
|
||||
}: {
|
||||
EntityPage?: React_2.ComponentType<{}> | undefined;
|
||||
}) => JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export type SystemDiagramCardClassKey =
|
||||
| 'domainNode'
|
||||
@@ -475,6 +450,5 @@ export type SystemDiagramCardClassKey =
|
||||
// src/components/CatalogTable/CatalogTable.d.ts:11:5 - (ae-forgotten-export) The symbol "columnFactories" needs to be exported by the entry point index.d.ts
|
||||
// src/components/EntityLayout/EntityLayout.d.ts:43:5 - (ae-forgotten-export) The symbol "EntityLayoutProps" needs to be exported by the entry point index.d.ts
|
||||
// src/components/EntityLayout/EntityLayout.d.ts:44:5 - (ae-forgotten-export) The symbol "SubRoute" needs to be exported by the entry point index.d.ts
|
||||
// src/components/EntityPageLayout/EntityPageLayout.d.ts:22:5 - (ae-forgotten-export) The symbol "EntityPageLayoutProps" needs to be exported by the entry point index.d.ts
|
||||
// src/plugin.d.ts:22:5 - (ae-forgotten-export) The symbol "ColumnBreakpoints" needs to be exported by the entry point index.d.ts
|
||||
```
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
Entity,
|
||||
DEFAULT_NAMESPACE,
|
||||
RELATION_OWNED_BY,
|
||||
} from '@backstage/catalog-model';
|
||||
import {
|
||||
EntityContext,
|
||||
EntityRefLinks,
|
||||
FavoriteEntity,
|
||||
getEntityRelations,
|
||||
InspectEntityDialog,
|
||||
UnregisterEntityDialog,
|
||||
useEntityCompoundName,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { Box } from '@material-ui/core';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu';
|
||||
import { Tabbed } from './Tabbed';
|
||||
|
||||
import {
|
||||
Content,
|
||||
Header,
|
||||
HeaderLabel,
|
||||
Link,
|
||||
Page,
|
||||
Progress,
|
||||
ResponseErrorPanel,
|
||||
WarningPanel,
|
||||
} from '@backstage/core-components';
|
||||
|
||||
import { IconComponent } from '@backstage/core-plugin-api';
|
||||
|
||||
const EntityPageTitle = ({
|
||||
entity,
|
||||
title,
|
||||
}: {
|
||||
title: string;
|
||||
entity: Entity | undefined;
|
||||
}) => (
|
||||
<Box display="inline-flex" alignItems="center" height="1em">
|
||||
{title}
|
||||
{entity && <FavoriteEntity entity={entity} />}
|
||||
</Box>
|
||||
);
|
||||
|
||||
const EntityLabels = ({ entity }: { entity: Entity }) => {
|
||||
const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
|
||||
return (
|
||||
<>
|
||||
{ownedByRelations.length > 0 && (
|
||||
<HeaderLabel
|
||||
label="Owner"
|
||||
value={
|
||||
<EntityRefLinks
|
||||
entityRefs={ownedByRelations}
|
||||
defaultKind="Group"
|
||||
color="inherit"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{entity.spec?.lifecycle && (
|
||||
<HeaderLabel label="Lifecycle" value={entity.spec.lifecycle} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const headerProps = (
|
||||
kind: string,
|
||||
namespace: string | undefined,
|
||||
name: string,
|
||||
entity: Entity | undefined,
|
||||
): { headerTitle: string; headerType: string } => {
|
||||
return {
|
||||
headerTitle: `${name}${
|
||||
namespace && namespace !== DEFAULT_NAMESPACE ? ` in ${namespace}` : ''
|
||||
}`,
|
||||
headerType: (() => {
|
||||
let t = kind.toLocaleLowerCase('en-US');
|
||||
if (entity && entity.spec && 'type' in entity.spec) {
|
||||
t += ' — ';
|
||||
t += (entity.spec as { type: string }).type.toLocaleLowerCase('en-US');
|
||||
}
|
||||
return t;
|
||||
})(),
|
||||
};
|
||||
};
|
||||
|
||||
// NOTE(freben): Intentionally not exported at this point, since it's part of
|
||||
// the unstable extra context menu items concept below
|
||||
type ExtraContextMenuItem = {
|
||||
title: string;
|
||||
Icon: IconComponent;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
// unstable context menu option, eg: disable the unregister entity menu
|
||||
type contextMenuOptions = {
|
||||
disableUnregister: boolean;
|
||||
};
|
||||
|
||||
type EntityPageLayoutProps = {
|
||||
UNSTABLE_extraContextMenuItems?: ExtraContextMenuItem[];
|
||||
UNSTABLE_contextMenuOptions?: contextMenuOptions;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Old entity page, only used by the old router based hierarchies.
|
||||
*
|
||||
* @deprecated Please use CatalogEntityPage instead
|
||||
*/
|
||||
export const EntityPageLayout = ({
|
||||
children,
|
||||
UNSTABLE_extraContextMenuItems,
|
||||
UNSTABLE_contextMenuOptions,
|
||||
}: EntityPageLayoutProps) => {
|
||||
const { kind, namespace, name } = useEntityCompoundName();
|
||||
const { entity, loading, error } = useContext(EntityContext);
|
||||
const { headerTitle, headerType } = headerProps(
|
||||
kind,
|
||||
namespace,
|
||||
name,
|
||||
entity!,
|
||||
);
|
||||
|
||||
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
||||
const [inspectionDialogOpen, setInspectionDialogOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const cleanUpAfterRemoval = async () => {
|
||||
setConfirmationDialogOpen(false);
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<Page themeId={entity?.spec?.type?.toString() ?? 'home'}>
|
||||
<Header
|
||||
title={<EntityPageTitle title={headerTitle} entity={entity!} />}
|
||||
pageTitleOverride={headerTitle}
|
||||
type={headerType}
|
||||
>
|
||||
{/* TODO: Make entity labels configurable for entity kind / type */}
|
||||
{entity && (
|
||||
<>
|
||||
<EntityLabels entity={entity} />
|
||||
<EntityContextMenu
|
||||
UNSTABLE_extraContextMenuItems={UNSTABLE_extraContextMenuItems}
|
||||
UNSTABLE_contextMenuOptions={UNSTABLE_contextMenuOptions}
|
||||
onUnregisterEntity={() => setConfirmationDialogOpen(true)}
|
||||
onInspectEntity={() => setInspectionDialogOpen(true)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Header>
|
||||
|
||||
{loading && (
|
||||
<Content>
|
||||
<Progress />
|
||||
</Content>
|
||||
)}
|
||||
|
||||
{entity && <Tabbed.Layout>{children}</Tabbed.Layout>}
|
||||
|
||||
{error && (
|
||||
<Content>
|
||||
<ResponseErrorPanel error={error} />
|
||||
</Content>
|
||||
)}
|
||||
|
||||
{!loading && !error && !entity && (
|
||||
<Content>
|
||||
<WarningPanel title="Entity not found">
|
||||
There is no {kind} with the requested{' '}
|
||||
<Link to="https://backstage.io/docs/features/software-catalog/references">
|
||||
kind, namespace, and name
|
||||
</Link>
|
||||
.
|
||||
</WarningPanel>
|
||||
</Content>
|
||||
)}
|
||||
|
||||
<UnregisterEntityDialog
|
||||
open={confirmationDialogOpen}
|
||||
entity={entity!}
|
||||
onConfirm={cleanUpAfterRemoval}
|
||||
onClose={() => setConfirmationDialogOpen(false)}
|
||||
/>
|
||||
<InspectEntityDialog
|
||||
open={inspectionDialogOpen}
|
||||
entity={entity!}
|
||||
onClose={() => setInspectionDialogOpen(false)}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
EntityPageLayout.Content = Tabbed.Content;
|
||||
@@ -1,197 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Tabbed } from './Tabbed';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { act, fireEvent } from '@testing-library/react';
|
||||
import { Routes, Route } from 'react-router';
|
||||
|
||||
describe('Tabbed layout', () => {
|
||||
it('renders simplest case', async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Tabbed.Layout>
|
||||
<Tabbed.Content
|
||||
title="tabbed-test-title"
|
||||
path="*"
|
||||
element={<div>tabbed-test-content</div>}
|
||||
/>
|
||||
</Tabbed.Layout>,
|
||||
);
|
||||
|
||||
expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument();
|
||||
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(/This component only accepts/);
|
||||
});
|
||||
|
||||
it('navigates when user clicks different tab', async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Routes>
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<Tabbed.Layout>
|
||||
<Tabbed.Content
|
||||
title="tabbed-test-title"
|
||||
path="/"
|
||||
element={<div>tabbed-test-content</div>}
|
||||
/>
|
||||
<Tabbed.Content
|
||||
title="tabbed-test-title-2"
|
||||
path="/some-other-path"
|
||||
element={<div>tabbed-test-content-2</div>}
|
||||
/>
|
||||
</Tabbed.Layout>
|
||||
}
|
||||
/>
|
||||
</Routes>,
|
||||
);
|
||||
|
||||
const secondTab = rendered.queryAllByRole('tab')[1];
|
||||
act(() => {
|
||||
fireEvent.click(secondTab);
|
||||
});
|
||||
|
||||
expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument();
|
||||
expect(rendered.queryByText('tabbed-test-content')).not.toBeInTheDocument();
|
||||
|
||||
expect(rendered.getByText('tabbed-test-title-2')).toBeInTheDocument();
|
||||
expect(rendered.queryByText('tabbed-test-content-2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('correctly delegates nested links', () => {
|
||||
const renderRoute = (route: string) =>
|
||||
renderInTestApp(
|
||||
<Routes>
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<Tabbed.Layout>
|
||||
<Tabbed.Content
|
||||
title="tabbed-test-title"
|
||||
path="/"
|
||||
element={<div>tabbed-test-content</div>}
|
||||
/>
|
||||
<Tabbed.Content
|
||||
title="tabbed-test-title-2"
|
||||
path="/some-other-path/*"
|
||||
element={
|
||||
<div>
|
||||
tabbed-test-content-2
|
||||
<Routes>
|
||||
<Route
|
||||
path="/nested"
|
||||
element={<div>tabbed-test-nested-content-2</div>}
|
||||
/>
|
||||
</Routes>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Tabbed.Layout>
|
||||
}
|
||||
/>
|
||||
</Routes>,
|
||||
{ routeEntries: [route] },
|
||||
);
|
||||
|
||||
it('works for nested content', async () => {
|
||||
const rendered = await renderRoute('/some-other-path/nested');
|
||||
|
||||
expect(
|
||||
rendered.queryByText('tabbed-test-content'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(rendered.queryByText('tabbed-test-content-2')).toBeInTheDocument();
|
||||
expect(
|
||||
rendered.queryByText('tabbed-test-nested-content-2'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('works for non-nested content', async () => {
|
||||
const rendered = await renderRoute('/some-other-path/');
|
||||
|
||||
expect(
|
||||
rendered.queryByText('tabbed-test-content'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(rendered.queryByText('tabbed-test-content-2')).toBeInTheDocument();
|
||||
expect(
|
||||
rendered.queryByText('tabbed-test-nested-content-2'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows only one tab contents at a time', async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Tabbed.Layout>
|
||||
<Tabbed.Content
|
||||
title="tabbed-test-title"
|
||||
path="/"
|
||||
element={<div>tabbed-test-content</div>}
|
||||
/>
|
||||
<Tabbed.Content
|
||||
title="tabbed-test-title-2"
|
||||
path="/some-other-path"
|
||||
element={<div>tabbed-test-content-2</div>}
|
||||
/>
|
||||
</Tabbed.Layout>,
|
||||
{ routeEntries: ['/some-other-path'] },
|
||||
);
|
||||
|
||||
expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument();
|
||||
expect(rendered.queryByText('tabbed-test-content')).not.toBeInTheDocument();
|
||||
|
||||
expect(rendered.getByText('tabbed-test-title-2')).toBeInTheDocument();
|
||||
expect(rendered.queryByText('tabbed-test-content-2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('redirects to the top level when no route is matching the url', async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<Tabbed.Layout>
|
||||
<Tabbed.Content
|
||||
title="tabbed-test-title"
|
||||
path="/"
|
||||
element={<div>tabbed-test-content</div>}
|
||||
/>
|
||||
<Tabbed.Content
|
||||
title="tabbed-test-title-2"
|
||||
path="/some-other-path"
|
||||
element={<div>tabbed-test-content-2</div>}
|
||||
/>
|
||||
</Tabbed.Layout>,
|
||||
{ routeEntries: ['/non-existing-path'] },
|
||||
);
|
||||
|
||||
expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument();
|
||||
expect(rendered.getByText('tabbed-test-content')).toBeInTheDocument();
|
||||
expect(rendered.getByText('tabbed-test-title-2')).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
rendered.queryByText('tabbed-test-content-2'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,127 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {
|
||||
useParams,
|
||||
useNavigate,
|
||||
PartialRouteObject,
|
||||
matchRoutes,
|
||||
RouteObject,
|
||||
useRoutes,
|
||||
Navigate,
|
||||
RouteMatch,
|
||||
} from 'react-router';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Tab, HeaderTabs, Content } from '@backstage/core-components';
|
||||
|
||||
const getSelectedIndexOrDefault = (
|
||||
matchedRoute: RouteMatch,
|
||||
tabs: Tab[],
|
||||
defaultIndex = 0,
|
||||
) => {
|
||||
if (!matchedRoute) return defaultIndex;
|
||||
const tabIndex = tabs.findIndex(t => t.id === matchedRoute.route.path);
|
||||
return ~tabIndex ? tabIndex : defaultIndex;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compound component, which allows you to define layout
|
||||
* for EntityPage using Tabs as a sub-navigation mechanism
|
||||
* Consists of 2 parts: Tabbed.Layout and Tabbed.Content.
|
||||
* Takes care of: tabs, routes, document titles, spacing around content
|
||||
*
|
||||
* @example
|
||||
* ```jsx
|
||||
* <Tabbed.Layout>
|
||||
* <Tabbed.Content
|
||||
* title="Example tab"
|
||||
* route="/example/*"
|
||||
* element={<div>This is rendered under /example/anything-here route</div>}
|
||||
* />
|
||||
* </TabbedLayout>
|
||||
* ```
|
||||
*/
|
||||
export const Tabbed = {
|
||||
Layout: ({ children }: { children: React.ReactNode }) => {
|
||||
const routes: PartialRouteObject[] = [];
|
||||
const tabs: Tab[] = [];
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
React.Children.forEach(children, child => {
|
||||
if (!React.isValidElement(child)) {
|
||||
// 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
|
||||
tabs.push({
|
||||
id: pathAndId,
|
||||
label: (child as JSX.Element).props.title,
|
||||
});
|
||||
|
||||
routes.push({
|
||||
path: pathAndId,
|
||||
element: child.props.element,
|
||||
});
|
||||
});
|
||||
|
||||
// Add catch-all for incorrect sub-routes
|
||||
if ((routes?.[0]?.path ?? '') !== '')
|
||||
routes.push({
|
||||
path: '/*',
|
||||
element: <Navigate to={routes[0].path!} />,
|
||||
});
|
||||
|
||||
const [matchedRoute] =
|
||||
matchRoutes(routes as RouteObject[], `/${params['*']}`) ?? [];
|
||||
const selectedIndex = getSelectedIndexOrDefault(matchedRoute, tabs);
|
||||
const currentTab = tabs[selectedIndex];
|
||||
const title = currentTab?.label;
|
||||
|
||||
const onTabChange = (index: number) =>
|
||||
// Remove trailing /*
|
||||
// And remove leading / for relative navigation
|
||||
// Note! route resolves relative to the position in the React tree,
|
||||
// not relative to current location
|
||||
navigate(tabs[index].id.replace(/\/\*$/, '').replace(/^\//, ''));
|
||||
|
||||
const currentRouteElement = useRoutes(routes);
|
||||
|
||||
if (!currentTab) return null;
|
||||
return (
|
||||
<>
|
||||
<HeaderTabs
|
||||
tabs={tabs}
|
||||
selectedIndex={selectedIndex}
|
||||
onChange={onTabChange}
|
||||
/>
|
||||
<Content>
|
||||
<Helmet title={title} />
|
||||
{currentRouteElement}
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
},
|
||||
Content: (_props: { path: string; title: string; element: JSX.Element }) =>
|
||||
null,
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { Tabbed } from './Tabbed';
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { EntityPageLayout } from './EntityPageLayout';
|
||||
@@ -1,100 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { DEFAULT_NAMESPACE } from '@backstage/catalog-model';
|
||||
import {
|
||||
AsyncEntityProvider,
|
||||
useEntity,
|
||||
useEntityFromUrl,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { Typography } from '@material-ui/core';
|
||||
import React, { ComponentType, ReactNode } from 'react';
|
||||
import { Navigate, Route, Routes, useParams } from 'react-router';
|
||||
import { CatalogPage } from './CatalogPage';
|
||||
import { EntityNotFound } from './EntityNotFound';
|
||||
import { EntityPageLayout } from './EntityPageLayout';
|
||||
import { Content, Link } from '@backstage/core-components';
|
||||
|
||||
const DefaultEntityPage = () => (
|
||||
<EntityPageLayout>
|
||||
<EntityPageLayout.Content
|
||||
path="/"
|
||||
title="Overview"
|
||||
element={
|
||||
<Content>
|
||||
<Typography variant="h2">This is the default entity page.</Typography>
|
||||
<Typography variant="body1">
|
||||
To override this component with your custom implementation, read
|
||||
docs on{' '}
|
||||
<Link to="https://backstage.io/docs">backstage.io/docs</Link>
|
||||
</Typography>
|
||||
</Content>
|
||||
}
|
||||
/>
|
||||
</EntityPageLayout>
|
||||
);
|
||||
|
||||
const EntityPageSwitch = ({ EntityPage }: { EntityPage: ComponentType }) => {
|
||||
const { entity, loading, error } = useEntity();
|
||||
// Loading and error states
|
||||
if (loading) return <EntityPageLayout />;
|
||||
if (error || !entity) return <EntityNotFound />;
|
||||
|
||||
// Otherwise EntityPage provided from the App
|
||||
// Note that EntityPage will include EntityPageLayout already
|
||||
return <EntityPage />;
|
||||
};
|
||||
|
||||
const OldEntityRouteRedirect = () => {
|
||||
const { optionalNamespaceAndName, '*': rest } = useParams() as any;
|
||||
const [name, namespace] = optionalNamespaceAndName.split(':').reverse();
|
||||
const namespaceLower =
|
||||
namespace?.toLocaleLowerCase('en-US') ?? DEFAULT_NAMESPACE;
|
||||
const restWithSlash = rest ? `/${rest}` : '';
|
||||
return (
|
||||
<Navigate
|
||||
to={`../../${namespaceLower}/component/${name}${restWithSlash}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const EntityLoader = (props: { children: ReactNode }) => (
|
||||
<AsyncEntityProvider {...useEntityFromUrl()} {...props} />
|
||||
);
|
||||
|
||||
/**
|
||||
* @deprecated Use plugin extensions instead
|
||||
* */
|
||||
export const Router = ({
|
||||
EntityPage = DefaultEntityPage,
|
||||
}: {
|
||||
EntityPage?: ComponentType;
|
||||
}) => (
|
||||
<Routes>
|
||||
<Route path="/" element={<CatalogPage />} />
|
||||
<Route
|
||||
path="/:namespace/:kind/:name"
|
||||
element={
|
||||
<EntityLoader>
|
||||
<EntityPageSwitch EntityPage={EntityPage} />
|
||||
</EntityLoader>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="Component/:optionalNamespaceAndName/*"
|
||||
element={<OldEntityRouteRedirect />}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
@@ -30,11 +30,9 @@ export * from './components/CatalogTable/columns';
|
||||
export * from './components/EntityLayout';
|
||||
export * from './components/EntityOrphanWarning';
|
||||
export * from './components/EntityProcessingErrorsPanel';
|
||||
export * from './components/EntityPageLayout';
|
||||
export * from './components/EntitySwitch';
|
||||
export * from './components/FilteredEntityLayout';
|
||||
export * from './overridableComponents';
|
||||
export { Router } from './components/Router';
|
||||
export {
|
||||
CatalogEntityPage,
|
||||
CatalogIndexPage,
|
||||
|
||||
Reference in New Issue
Block a user