Merge pull request #1128 from spotify/shmidt-i/links-docs

NavLink, NavButton and RouteRefs enforcing
This commit is contained in:
Ivan Shmidt
2020-06-04 09:53:03 +02:00
committed by GitHub
20 changed files with 476 additions and 67 deletions
+23 -17
View File
@@ -39,14 +39,19 @@ Each plugin is responsible for registering its components to corresponding route
The app will call the `createPlugin` method on each plugin, passing in a `router` object with a set
of methods on it.
```typescript
import { createPlugin } from '@backstage/core';
```jsx
import { createPlugin, createRouteRef } from '@backstage/core';
import ExampleComponent from './components/ExampleComponent';
export default createPlugin({
id: 'my-plugin',
export const rootRouteRef = createRouteRef({
path: '/new-plugin',
title: 'New plugin',
});
export const plugin = createPlugin({
id: 'new-plugin',
register({ router }) {
router.registerRoute('/my-plugin', ExampleComponent);
router.addRoute(rootRouteRef, ExampleComponent);
},
});
```
@@ -54,17 +59,18 @@ export default createPlugin({
#### `router` API
```typescript
type RouterHooks = {
registerRoute(
path: RoutePath,
Component: ComponentType<any>,
options?: RouteOptions,
): void;
addRoute(
target: RouteRef,
Component: ComponentType<any>,
options?: RouteOptions,
): void;
registerRedirect(
path: RoutePath,
target: RoutePath,
options?: RouteOptions,
): void;
};
/**
* @deprecated See the `addRoute` method
*/
registerRoute(
path: RoutePath,
Component: ComponentType<any>,
options?: RouteOptions,
): void;
```
@@ -43,13 +43,18 @@ In the root folder you have some configuration for typescript and jest, the test
In the `src` folder we get to the interesting bits. Check out the `plugin.ts`:
```jsx
import { createPlugin } from '@backstage/core';
import { createPlugin, createRouteRef } from '@backstage/core';
import ExampleComponent from './components/ExampleComponent';
export default createPlugin({
export const rootRouteRef = createRouteRef({
path: '/new-plugin',
title: 'New plugin',
});
export const plugin = createPlugin({
id: 'new-plugin',
register({ router }) {
router.registerRoute('/new-plugin', ExampleComponent);
router.addRoute(rootRouteRef, ExampleComponent);
},
});
```
+28 -13
View File
@@ -1,21 +1,36 @@
# createPlugin - router
The router that is passed to the `register` function includes makes it possible for plugins to hook into routing of the Backstage app and provide the end users with new views to navigate to.
The router that is passed to the `register` function makes it possible for plugins to hook into routing of the Backstage app and provide the end users with new views to navigate to.
This is done by utilising the following methods on the `router`:
```typescript
type RouterHooks = {
registerRoute(
path: RoutePath,
Component: ComponentType<any>,
options?: RouteOptions,
): void;
addRoute(
target: RouteRef,
Component: ComponentType<any>,
options?: RouteOptions,
): void;
registerRedirect(
path: RoutePath,
target: RoutePath,
options?: RouteOptions,
): void;
};
/**
* @deprecated See the `addRoute` method
*/
registerRoute(
path: RoutePath,
Component: ComponentType<any>,
options?: RouteOptions,
): void;
```
## RouteRef
`addRoute` method is using mutable RouteRefs, which can be created as following:
```ts
import { createRouteRef } from '@backstage/core';
const myPluginRouteRef = createRouteRef({
path: '/my-plugin',
title: 'My Plugin',
});
```
[Back to References](README.md)
@@ -1,25 +1,30 @@
/*
* Copyright 2020 Spotify AB
*
* 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.
*/
* Copyright 2020 Spotify AB
*
* 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 { createPlugin } from '@backstage/core';
import { createPlugin, createRouteRef } from '@backstage/core';
import ExampleComponent from './components/ExampleComponent';
export const plugin = createPlugin({
id: '{{ id }}',
register({ router }) {
router.registerRoute('/{{ id }}', ExampleComponent);
},
export const rootRouteRef = createRouteRef({
path: '/{{ id }}',
title: '{{ id }}',
});
export const plugin = createPlugin({
id: '{{ id }}',
register({ router }) {
router.addRoute(rootRouteRef, ExampleComponent);
},
});
+14 -1
View File
@@ -110,7 +110,7 @@ export class PrivateAppImpl implements BackstageApp {
);
break;
}
case 'redirect-route': {
case 'legacy-redirect-route': {
const { path, target, options = {} } = output;
const { exact = true } = options;
routes.push(
@@ -118,6 +118,19 @@ export class PrivateAppImpl implements BackstageApp {
);
break;
}
case 'redirect-route': {
const { from, to, options = {} } = output;
const { exact = true } = options;
routes.push(
<Redirect
key={from.path}
path={from.path}
to={to.path}
exact={exact}
/>,
);
break;
}
case 'feature-flag': {
registeredFeatureFlags.push({
pluginId: plugin.getId(),
+3 -9
View File
@@ -42,17 +42,14 @@ export type RouterHooks = {
options?: RouteOptions,
): void;
/**
* @deprecated See the `addRoute` method
*/
registerRoute(
path: RoutePath,
Component: ComponentType<any>,
options?: RouteOptions,
): void;
registerRedirect(
path: RoutePath,
target: RoutePath,
options?: RouteOptions,
): void;
};
export type FeatureFlagsHooks = {
@@ -91,9 +88,6 @@ export class PluginImpl {
registerRoute(path, component, options) {
outputs.push({ type: 'legacy-route', path, component, options });
},
registerRedirect(path, target, options) {
outputs.push({ type: 'redirect-route', path, target, options });
},
},
featureFlags: {
register(name) {
+8
View File
@@ -41,6 +41,13 @@ export type RouteOutput = {
export type RedirectRouteOutput = {
type: 'redirect-route';
from: RouteRef;
to: RouteRef;
options?: RouteOptions;
};
export type LegacyRedirectRouteOutput = {
type: 'legacy-redirect-route';
path: RoutePath;
target: RoutePath;
options?: RouteOptions;
@@ -56,6 +63,7 @@ export type FeatureFlagOutput = {
export type PluginOutput =
| LegacyRouteOutput
| RouteOutput
| LegacyRedirectRouteOutput
| RedirectRouteOutput
| FeatureFlagOutput;
+2 -2
View File
@@ -18,13 +18,13 @@ import { IconComponent } from '../icons';
export type RouteRef = {
path: string;
icon: IconComponent;
icon?: IconComponent;
title: string;
};
export type RouteRefConfig = {
path: string;
icon: IconComponent;
icon?: IconComponent;
title: string;
};
+1
View File
@@ -34,6 +34,7 @@
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@types/react": "^16.9",
"@types/react-router-dom": "^5.1.5",
"@types/react-sparklines": "^1.7.0",
"classnames": "^2.2.6",
"clsx": "^1.1.0",
@@ -0,0 +1,93 @@
/*
* Copyright 2020 Spotify AB
*
* 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, { FunctionComponentFactory } from 'react';
import { Button } from './Button';
import {
MemoryRouter,
Route,
useLocation,
Link as RouterLink,
} from 'react-router-dom';
import { createRouteRef } from '@backstage/core-api';
const Location = () => {
const location = useLocation();
return <pre>Current location: {location.pathname}</pre>;
};
export default {
title: 'Button',
component: Button,
decorators: [
(storyFn: FunctionComponentFactory<{}>) => (
<MemoryRouter>
<div>
<div>
<Location />
</div>
{storyFn()}
</div>
</MemoryRouter>
),
],
};
export const Default = () => {
const routeRef = createRouteRef({
path: '/hello',
title: 'Hi there!',
});
return (
<>
<Button to={routeRef.path}>This button</Button>&nbsp;will utilise the
react-router MemoryRouter's navigation
<Route path={routeRef.path}>
<h1>{routeRef.title}</h1>
</Route>
</>
);
};
export const PassProps = () => {
const routeRef = createRouteRef({
path: '/hello',
title: 'Hi there!',
});
return (
<>
<Button
to={routeRef.path}
/** react-router-dom related prop */
component={RouterLink}
/** material-ui related prop */
color="secondary"
variant="outlined"
>
This link
</Button>
&nbsp;has props for both material-ui's component as well as for
react-router-dom's
<Route path={routeRef.path}>
<h1>{routeRef.title}</h1>
</Route>
</>
);
};
PassProps.story = {
name: `Accepts material-ui Button's and react-router-dom Link's props`,
};
@@ -0,0 +1,40 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { render, fireEvent } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { Button } from './Button';
import { MemoryRouter, Route } from 'react-router';
import { act } from 'react-dom/test-utils';
describe('<Button />', () => {
it('navigates using react-router', async () => {
const testString = 'This is test string';
const buttonLabel = 'Navigate!';
const { getByText } = render(
wrapInTestApp(
<MemoryRouter>
<Button to="/test">{buttonLabel}</Button>
<Route path="/test">{testString}</Route>{' '}
</MemoryRouter>,
),
);
expect(() => getByText(testString)).toThrow();
await act(async () => fireEvent.click(getByText(buttonLabel)));
expect(getByText(testString)).toBeInTheDocument();
});
});
@@ -0,0 +1,30 @@
/*
* Copyright 2020 Spotify AB
*
* 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, { ComponentProps } from 'react';
import { Button as MaterialButton } from '@material-ui/core';
import { Link as RouterLink } from 'react-router-dom';
type Props = ComponentProps<typeof MaterialButton> &
ComponentProps<typeof RouterLink>;
/**
* Thin wrapper on top of material-ui's Button component
* Makes the Button to utilise react-router
*/
export const Button = React.forwardRef<any, Props>((props, ref) => (
<MaterialButton ref={ref} component={RouterLink} {...props} />
));
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { Button } from './Button';
@@ -0,0 +1,92 @@
/*
* Copyright 2020 Spotify AB
*
* 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, { FunctionComponentFactory } from 'react';
import { Link } from './Link';
import {
MemoryRouter,
Route,
useLocation,
NavLink as RouterNavLink,
} from 'react-router-dom';
import { createRouteRef } from '@backstage/core-api';
const Location = () => {
const location = useLocation();
return <pre>Current location: {location.pathname}</pre>;
};
export default {
title: 'Link',
component: Link,
decorators: [
(storyFn: FunctionComponentFactory<{}>) => (
<MemoryRouter>
<div>
<div>
<Location />
</div>
{storyFn()}
</div>
</MemoryRouter>
),
],
};
export const Default = () => {
const routeRef = createRouteRef({
path: '/hello',
title: 'Hi there!',
});
return (
<>
<Link to={routeRef.path}>This link</Link>&nbsp;will utilise the
react-router MemoryRouter's navigation
<Route path={routeRef.path}>
<h1>{routeRef.title}</h1>
</Route>
</>
);
};
export const PassProps = () => {
const routeRef = createRouteRef({
path: '/hello',
title: 'Hi there!',
});
return (
<>
<Link
to={routeRef.path}
/** react-router-dom related prop */
component={RouterNavLink}
/** material-ui related prop */
color="secondary"
>
This link
</Link>
&nbsp;has props for both material-ui's component as well as for
react-router-dom's
<Route path={routeRef.path}>
<h1>{routeRef.title}</h1>
</Route>
</>
);
};
PassProps.story = {
name: `Accepts material-ui Link's and react-router-dom Link's props`,
};
@@ -0,0 +1,40 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { render, fireEvent } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { Link } from './Link';
import { MemoryRouter, Route } from 'react-router';
import { act } from 'react-dom/test-utils';
describe('<Link />', () => {
it('navigates using react-router', async () => {
const testString = 'This is test string';
const linkText = 'Navigate!';
const { getByText } = render(
wrapInTestApp(
<MemoryRouter>
<Link to="/test">{linkText}</Link>
<Route path="/test">{testString}</Route>
</MemoryRouter>,
),
);
expect(() => getByText(testString)).toThrow();
await act(async () => fireEvent.click(getByText(linkText)));
expect(getByText(testString)).toBeInTheDocument();
});
});
@@ -0,0 +1,30 @@
/*
* Copyright 2020 Spotify AB
*
* 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, { ComponentProps } from 'react';
import { Link as MaterialLink } from '@material-ui/core';
import { Link as RouterLink } from 'react-router-dom';
type Props = ComponentProps<typeof MaterialLink> &
ComponentProps<typeof RouterLink>;
/**
* Thin wrapper on top of material-ui's Link component
* Makes the Link to utilise react-router
*/
export const Link = React.forwardRef<any, Props>((props, ref) => (
<MaterialLink ref={ref} component={RouterLink} {...props} />
));
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { Link } from './Link';
+2
View File
@@ -38,4 +38,6 @@ export { default as StructuredMetadataTable } from './components/StructuredMetad
export { default as TrendLine } from './components/TrendLine';
export { FeatureCalloutCircular } from './components/FeatureDiscovery/FeatureCalloutCircular';
export * from './components/Status';
export * from './components/Button';
export * from './components/Link';
export { default as WarningPanel } from './components/WarningPanel';
+2 -1
View File
@@ -33,6 +33,7 @@ import {
OAuthRequestDialog,
} from '@backstage/core';
import * as defaultApiFactories from './apiFactories';
import SentimentDissatisfiedIcon from '@material-ui/icons/SentimentDissatisfied';
// TODO(rugvip): export proper plugin type from core that isn't the plugin class
type BackstagePlugin = ReturnType<typeof createPlugin>;
@@ -148,7 +149,7 @@ class DevAppBuilder {
key={target.path}
to={target.path}
text={target.title}
icon={target.icon}
icon={target.icon ?? SentimentDissatisfiedIcon}
/>,
);
break;
+1 -1
View File
@@ -3896,7 +3896,7 @@
"@types/react" "*"
immutable ">=3.8.2"
"@types/react-router-dom@^5.1.3":
"@types/react-router-dom@^5.1.3", "@types/react-router-dom@^5.1.5":
version "5.1.5"
resolved "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.5.tgz#7c334a2ea785dbad2b2dcdd83d2cf3d9973da090"
integrity sha512-ArBM4B1g3BWLGbaGvwBGO75GNFbLDUthrDojV2vHLih/Tq8M+tgvY1DSwkuNrPSwdp/GUL93WSEpTZs8nVyJLw==