Documentation skeleton (#1617)
* Documentation skeleton * Added ADR list and more * rm material theme * Update mkdocs.yml * Add docs TOC * Work for plain MD and mkdocs * Update mkdocs.yml * Restructuring * Added roadmap * Add reference APIs * move publishing
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
# Create a Backstage Plugin
|
||||
|
||||
A Backstage Plugin adds functionality to Backstage.
|
||||
|
||||
## Create a Plugin
|
||||
|
||||
To create a new plugin, make sure you've run `yarn install` and installed
|
||||
dependencies, then run the following on your command line (invoking the
|
||||
`backstage-cli`).
|
||||
|
||||
```bash
|
||||
yarn create-plugin
|
||||
```
|
||||
|
||||
<p align='center'>
|
||||
<img src='https://github.com/spotify/backstage/raw/master/docs/getting-started/create-plugin_output.png' width='600' alt='create plugin'>
|
||||
</p>
|
||||
|
||||
This will create a new Backstage Plugin based on the ID that was provided. It
|
||||
will be built and added to the Backstage App automatically.
|
||||
|
||||
_If `yarn start` is already running you should be able to see the default page
|
||||
for your new plugin directly by navigating to
|
||||
`http://localhost:3000/my-plugin`._
|
||||
|
||||
<p align='center'>
|
||||
<img src='https://github.com/spotify/backstage/raw/master/docs/getting-started/my-plugin_screenshot.png' width='600' alt='my plugin'>
|
||||
</p>
|
||||
|
||||
You can also serve the plugin in isolation by running `yarn start` in the plugin
|
||||
directory. Or by using the yarn workspace command, for example:
|
||||
|
||||
```bash
|
||||
yarn workspace @backstage/plugin-welcome start # Also supports --check
|
||||
```
|
||||
|
||||
This method of serving the plugin provides quicker iteration speed and a faster
|
||||
startup and hot reloads. It is only meant for local development, and the setup
|
||||
for it can be found inside the plugin's `dev/` directory.
|
||||
|
||||
[Next Step - Structure of a plugin](structure-of-a-plugin.md)
|
||||
|
||||
[Back to Getting Started](README.md)
|
||||
@@ -0,0 +1,26 @@
|
||||
# Plugins
|
||||
|
||||
Backstage is a single-page application composed of a set of plugins.
|
||||
|
||||
Our goal for the plugin ecosystem is that the definition of a plugin is flexible
|
||||
enough to allow you to expose pretty much any kind of infrastructure or software
|
||||
development tool as a plugin in Backstage. By following strong
|
||||
[design guidelines](https://github.com/spotify/backstage/blob/master/docs/design.md)
|
||||
we ensure the the overall user experience stays consistent between plugins.
|
||||
|
||||

|
||||
|
||||
## Creating a plugin
|
||||
|
||||
To create a plugin, follow the steps outlined
|
||||
[here](https://github.com/spotify/backstage/blob/master/docs/getting-started/create-a-plugin.md).
|
||||
|
||||
## Suggesting a plugin
|
||||
|
||||
If you start developing a plugin that you aim to release as open source, we
|
||||
suggest that you create a new
|
||||
[new Issue](https://github.com/spotify/backstage/issues/new?template=plugin_template.md).
|
||||
This helps the community know what plugins are in development.
|
||||
|
||||
You can also use this process if you have an idea for a good plugin but you hope
|
||||
that someone else will pick up the work.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Publishing
|
||||
|
||||
## NPM
|
||||
|
||||
NPM packages are published through CI/CD in the
|
||||
[.github/workflows/master.yml](../.github/workflows/master.yml) workflow. Every
|
||||
commit that is merged to master will be checked for new versions of all public
|
||||
packages, and any new versions will automatically be published to NPM.
|
||||
|
||||
### Creating a new release
|
||||
|
||||
Version bumps are made through release PRs. To create a new release, checkout
|
||||
out a new branch that you will use for the release, e.g.
|
||||
|
||||
```sh
|
||||
$ git checkout -b new-release
|
||||
```
|
||||
|
||||
Then, from the root of the repo, run
|
||||
|
||||
```sh
|
||||
$ yarn release
|
||||
```
|
||||
|
||||
This will bring up the lerna release CLI where you choose what type of version
|
||||
bump you want to make, (major/minor/patch/prerelease). The CLI will take you
|
||||
through choosing a version, previewing all changes, and then approving the
|
||||
release. Once the release is approved, a new commit is created that you can
|
||||
submit as a PR. Push the branch to GitHub:
|
||||
|
||||
```sh
|
||||
$ git push origin -u new-release
|
||||
```
|
||||
|
||||
And then create a PR. Once the PR is approved and merged into master, the master
|
||||
build will publish new versions of all bumped packages.
|
||||
|
||||
[Back to Docs](README.md)
|
||||
@@ -0,0 +1,107 @@
|
||||
# Structure of a Plugin
|
||||
|
||||
Nice, you have a new plugin! We'll soon see how we can develop it into doing
|
||||
great things. But first off, let's look at what we get out of the box.
|
||||
|
||||
## Folder structure
|
||||
|
||||
The new plugin should look something like:
|
||||
|
||||
```
|
||||
new-plugin/
|
||||
dist/
|
||||
node_modules/
|
||||
src/
|
||||
components/
|
||||
ExampleComponent/
|
||||
ExampleComponent.test.tsx
|
||||
ExampleComponent.tsx
|
||||
index.ts
|
||||
ExampleFetchComponent/
|
||||
ExampleFetchComponent.test.tsx
|
||||
ExampleFetchComponent.tsx
|
||||
index.ts
|
||||
index.ts
|
||||
plugin.test.ts
|
||||
plugin.ts
|
||||
jest.config.js
|
||||
jest.setup.ts
|
||||
package.json
|
||||
README.md
|
||||
tsconfig.json
|
||||
```
|
||||
|
||||
You might note a thing or two. Yes, a plugin looks like a mini project on it's
|
||||
own with a `package.json` and a `src` folder. And this is because we want
|
||||
plugins to be separate packages. This makes it possible to ship plugins on npm
|
||||
and it lets you work on a plugin in isolation, without loading all the other
|
||||
plugins in a potentially big Backstage app.
|
||||
|
||||
The `index.ts` files are there to let us import from the folder path and not
|
||||
specific files. It's a way to have control over the exports in one file per
|
||||
folder.
|
||||
|
||||
## Base files
|
||||
|
||||
In the root folder you have some configuration for typescript and jest, the test
|
||||
runner. You get a readme to populate with info about your plugin and a
|
||||
package.json to declare the plugin dependencies, metadata and scripts.
|
||||
|
||||
## The plugin file
|
||||
|
||||
In the `src` folder we get to the interesting bits. Check out the `plugin.ts`:
|
||||
|
||||
```jsx
|
||||
import { createPlugin, createRouteRef } from '@backstage/core';
|
||||
import ExampleComponent from './components/ExampleComponent';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
path: '/new-plugin',
|
||||
title: 'New plugin',
|
||||
});
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'new-plugin',
|
||||
register({ router }) {
|
||||
router.addRoute(rootRouteRef, ExampleComponent);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
This is where the plugin is created and where it hooks into the app by declaring
|
||||
what component should be shown on what url. See reference docs for
|
||||
[createPlugin](../reference/createPlugin.md) or
|
||||
[router](../reference/createPlugin-router.md).
|
||||
|
||||
## Components
|
||||
|
||||
The generated plugin includes two example components to showcase how we
|
||||
structure our plugins. There are usually one or multiple page components and
|
||||
next to them you can split up the UI in as many components as you feel like.
|
||||
|
||||
We have the `ExamplePage` to show an example Backstage page component. The
|
||||
`ExampleFetchComponent` show cases the common task of making an async request to
|
||||
a public API and plot the response data in a table using Material-UI components.
|
||||
|
||||
You may tweak these components, rename them and/or replace them completely.
|
||||
|
||||
## Connecting the plugin to the Backstage app
|
||||
|
||||
There are two things needed for a Backstage app to start making use of a plugin.
|
||||
|
||||
1. Add plugin as dependency in `app/package.json`
|
||||
2. `import` plugin in `app/src/plugins.ts`
|
||||
|
||||
Luckily these two steps happen automatically when you create a plugin with the
|
||||
Backstage CLI.
|
||||
|
||||
## Talking to the outside world
|
||||
|
||||
If your plugin needs to communicate with services outside the backstage
|
||||
environment you will probably face challenges like CORS policies and/or
|
||||
backend-side authorization. To smooth this process out you can use proxy -
|
||||
either the one you already have (like nginx/haproxy/etc) or the proxy-backend
|
||||
plugin that we provide for the backstage backend.
|
||||
[Read more](../../plugins/proxy-backend/README.md)
|
||||
|
||||
[Back to Getting Started](README.md)
|
||||
@@ -0,0 +1,371 @@
|
||||
# Testing with Jest
|
||||
|
||||
Backstage uses [Jest](https://facebook.github.io/jest/) for all our unit testing
|
||||
needs.
|
||||
|
||||
Jest is a Facebook-built unit testing framework specifically built for React. It
|
||||
follows in the footsteps of other classic Node.js unit testing-related
|
||||
frameworks and libraries like [Mocha](https://mochajs.org/),
|
||||
[Jasmine](https://jasmine.github.io/), and [Chai](http://www.chaijs.com/).
|
||||
|
||||
## Running Tests
|
||||
|
||||
Running all tests:
|
||||
|
||||
yarn test-react
|
||||
|
||||
Running an individual test (e.g. `MyComponent.test.js`):
|
||||
|
||||
yarn test-react MyComponent
|
||||
|
||||
To run both `MyComponent.test.js` and `MyControl.test.js` suite of tests:
|
||||
|
||||
yarn test-react MyCo
|
||||
|
||||
Note: if `console.logs` are not appearing, run only the individual test you are
|
||||
working on.
|
||||
[This is a bug in Jest](https://github.com/facebook/jest/issues/2441).
|
||||
|
||||
## Naming Test Files
|
||||
|
||||
Tests should be name `[filename].test.js`.
|
||||
|
||||
For example, the tests for **`Link.js`** exist in the file **`Link.test.js`**.
|
||||
|
||||
## Third-Party Dependencies
|
||||
|
||||
Jest has its own built-in assertion library with `expect`, so there is no need
|
||||
to `import` a third-party library like some of the older frameworks required.
|
||||
However since assertion libraries simply throw errors, it would be feasible to
|
||||
import a third-party library if you needed (like Chai or
|
||||
[Sinon](http://sinonjs.org/)).
|
||||
|
||||
We use the light-weight
|
||||
[react-testing-library](https://github.com/kentcdodds/react-testing-library) to
|
||||
render React components.
|
||||
|
||||
## Testing Utilities
|
||||
|
||||
TODO.
|
||||
|
||||
# Writing Unit Tests
|
||||
|
||||
The following principles are good guides to determining if you are writing high
|
||||
quality frontend unit tests.
|
||||
|
||||
## Bad Unit Test Principle
|
||||
|
||||
> No unit test is better than a bad one.
|
||||
|
||||
Writing a poor unit test:
|
||||
|
||||
- Gives the illusion your code is more secure or reliable than it actually is.
|
||||
- Functions equivalent to a bad comment, in that it leads the next developer
|
||||
into erroneous assumptions.
|
||||
- Adds to future work by requiring updates to the unit test for irrelevant code
|
||||
changes.
|
||||
|
||||
## Input/Output Principle
|
||||
|
||||
> A unit test verifies an output matches an expected input.
|
||||
|
||||
For backend, this would be that when you provide configuration X, then the
|
||||
object responds with Y. For frontend, this would be that when you provide
|
||||
properties X to a component, then the visual functionality responds with Y.
|
||||
|
||||
## Blackbox Principle
|
||||
|
||||
> A good unit test does not tell the object how it should do its job but should
|
||||
> only compare inputs to outputs.
|
||||
|
||||
Consider a unit test for a form. A good unit test would not test the order of
|
||||
the form fields. Instead, it would verify that the inputs to the form fields
|
||||
lead to a certain backend call when submit is clicked.
|
||||
|
||||
## Scalability Principle
|
||||
|
||||
> Unit test quality is directly proportionate to how much code can change
|
||||
> without having to touch the unit test.
|
||||
|
||||
This is often overlooked! A unit test is not a test to verify the code never
|
||||
changes. Poor unit tests are written so that every time you make a tiny change
|
||||
to the code, you have to update the unit test. A good unit test suite allows a
|
||||
lot of flexibility in _how_ the code is written so that future refactoring can
|
||||
occur without having to touch the original unit tests.
|
||||
|
||||
## Increasing Complexity Principle
|
||||
|
||||
> The ordering of unit tests in a suite should proceed from least specific to
|
||||
> most specific.
|
||||
|
||||
Jest runs all tests in the order in which they are provided, regardless of the
|
||||
depth of `describe()` blocks you provide. We can use this to help us write tests
|
||||
that will help the next developer debug what they broke.
|
||||
|
||||
The idea here is that if they were to break a unit test, the next developer
|
||||
should be able to tell from the order in which the tests broke what they should
|
||||
do to fix things.
|
||||
|
||||
For example, good unit tests will verify the arguments to a function in a test
|
||||
prior to a test that validates the output. If you do not test this, then simply
|
||||
throwing an error saying that output was incorrect will lead the next developer
|
||||
into thinking they may have broken the entire functionality of the object rather
|
||||
than simply letting them know they had an invalid input.
|
||||
|
||||
## Broken Functionality Principle
|
||||
|
||||
> Generally, a unit test should not test exactly how the output appears, it
|
||||
> should test that the functionality has an expected _general_ response to an
|
||||
> input change.
|
||||
|
||||
This piggybacks the Scalability Principle and applies primarily to frontend
|
||||
development. As a general rule of thumb, frontends should be flexible enough so
|
||||
that the UX or design can change while touching the least amount of code
|
||||
possible. So for example, a poor unit test would verify the color of a button
|
||||
when it is hovered. This would be a poor unit test, because if you decide to
|
||||
test a slightly different color on the button the unit test will break. A better
|
||||
unit test would verify that the button's CSS classname is assigned properly on
|
||||
hover or test for something completely different.
|
||||
|
||||
## Example: Loading Indicator
|
||||
|
||||
A classic unit test on frontends is verifying a loading indicator displays when
|
||||
a backend request is being made.
|
||||
|
||||
**Here are some things we could test for _when data is loading_:**
|
||||
|
||||
> Did the internal `loading` state of the component change? (poor)
|
||||
|
||||
This is not a great test because it does not actually test that the
|
||||
functionality (displaying a message to the user) actually happens. It also
|
||||
breaks the Blackbox Principle by expecting the internals of the component must
|
||||
work a certain way. This could be a good test on its own right, but it does not
|
||||
actually achieve the goal of verifying that if our input (loading data) occurs,
|
||||
then the output (displaying a message to the user) has happened.
|
||||
|
||||
> Did the text `"Loading!"` appear in the DOM? (better)
|
||||
|
||||
This is a better test because it validates functionality, but it breaks the
|
||||
Scalability principle. By testing for `'Loading...'` we are linking our test
|
||||
code to the component's message. If we want to add internationalization or
|
||||
simply change the message to something more specific we will break our test and
|
||||
have to update code in two places.
|
||||
|
||||
> Did `<Loading />` get mounted? (best)
|
||||
|
||||
This is the best test of these examples (there could be more depending on your
|
||||
implementation).
|
||||
|
||||
Verifying that `<Loading />` is mounted when data is loading is the best test
|
||||
because it fulfills all the principles above:
|
||||
|
||||
✓ **Fulfills Input/Output Principle**: Verifies the output changes when the
|
||||
input changes
|
||||
|
||||
✓ **Fufills Blackbox Principle**: Does not verify _how_ the `<Loading />`
|
||||
component is mounted, just that it is mounted in response to the input.
|
||||
|
||||
✓ **Fulfills Scalability Principle**: If we decide to refactor the entire way
|
||||
the loading indicator is displayed the test still works without touching it.
|
||||
|
||||
✓ **Fulfills Broken Functionality Principle**: this test verifies the
|
||||
functionality (displaying an indicator) is working, rather than how it is
|
||||
working.
|
||||
|
||||
The increasing complexity principle does not really apply to this example, so it
|
||||
was excluded. However if you were to place this test in a suite of other tests,
|
||||
it would be best to test first that when the component is instructed to load
|
||||
data then it actually does it. this way both tests fail if the data loading part
|
||||
breaks and the next developer immediately know the problem is that the data
|
||||
loading is broken, not that the loading indicator is broken.
|
||||
|
||||
# Examples
|
||||
|
||||
## Utility Functions
|
||||
|
||||
A utility function is a function with no side effects. It takes in arguments and
|
||||
returns a result or displays an error or console message, like so:
|
||||
|
||||
**`StringUtil ellipsis`**
|
||||
|
||||
export function ellipsis(text, maxLength, midCharIx = 0, ellipsis = '...') {
|
||||
// Do something blackbox. We should not care about the internals, only inputs and outputs.
|
||||
...
|
||||
return someFinalValue;
|
||||
}
|
||||
|
||||
There are four things to test for in a utility function:
|
||||
|
||||
1. Handle Invalid Input
|
||||
2. Verify default input arguments
|
||||
3. Verify output for expected input arguments
|
||||
4. Handle thrown errors
|
||||
|
||||
> Handle Invalid Input (handle thrown errors):
|
||||
|
||||
it('Throws an error on improper arguments', () => {
|
||||
expect(() => {
|
||||
ellipsis();
|
||||
}).toThrowError('Expected \'text\' to be defined');
|
||||
});
|
||||
|
||||
> Verify default input arguments:
|
||||
|
||||
it('Works with defaults', () => {
|
||||
expect(ellipsis('Hello world', 3)).toBe('Hel...');
|
||||
expect(ellipsis('', 3)).toBe('');
|
||||
expect(ellipsis('H', 3)).toBe('H');
|
||||
expect(ellipsis('Hello', 5)).toBe('Hello');
|
||||
});
|
||||
|
||||
> Verify output for expected input arguments:
|
||||
|
||||
This is especially true for edge cases!
|
||||
|
||||
it('Works with midCharIx', () => {
|
||||
expect(ellipsis('Hello world', 3, 6)).toBe('...o w...');
|
||||
expect(ellipsis('', 3, 6)).toBe('');
|
||||
expect(ellipsis('Backstage is amazing', 4, 10)).toBe('...e is...');
|
||||
});
|
||||
|
||||
## Non-React Classes
|
||||
|
||||
Testing a Javascript object which is _not_ a React component follows a lot of
|
||||
the same principles as testing objects in other languages.
|
||||
|
||||
### API Testing Principles
|
||||
|
||||
Testing an API involves verifying four things:
|
||||
|
||||
1. Invalid inputs are caught before being sent to the server.
|
||||
2. Valid inputs translate into a valid browser request.
|
||||
3. Server response is translated into an expected Javascript object.
|
||||
4. Server errors are handled gracefully.
|
||||
|
||||
### Mocking API Calls
|
||||
|
||||
[Mocking in jest](https://facebook.github.io/jest/docs/en/mock-functions.html)
|
||||
involves wrapping existing functions (like an API call function) with an
|
||||
alternative.
|
||||
|
||||
For example:
|
||||
|
||||
**./Api.js**
|
||||
|
||||
```
|
||||
export {
|
||||
fetchSomethingFromServer: () => {
|
||||
// Live production call to a URI. Must be avoided during testing!
|
||||
return fetch('blah');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**./\_\_mocks\_\_/Api.js**
|
||||
|
||||
```
|
||||
export {
|
||||
fetchSomethingFromServer: () => {
|
||||
// Simulate a production call, but avoid jest and just use a promise
|
||||
return Promise.resolve('some result object simulating server data here');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**./Api.test.js**
|
||||
|
||||
```
|
||||
/* eslint-disable import/first */
|
||||
|
||||
jest.mock('./MyApi'); // Instruct Jest to swap all future imports of './MyApi.js' to './__mocks__/MyApi.js'
|
||||
|
||||
import MyApi from './MyApi'; // Will actually return the contents of the file in the __mocks__ folder now
|
||||
|
||||
it ('loads data', (done) => {
|
||||
MyApi.fetchSomethingFromServer().then(result => {
|
||||
expect(result).toBe('some result object simulating server data here');
|
||||
done();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Note: make sure you disable the eslint `'import/first'` rule at the top of the
|
||||
file since technically you are not allowed by the default settings to have an
|
||||
import after the `jest.mock` call.
|
||||
|
||||
## React Components
|
||||
|
||||
### Working with the React Lifecycle
|
||||
|
||||
The [React lifecycle](https://reactjs.org/docs/state-and-lifecycle.html) is
|
||||
asynchronous.
|
||||
|
||||
When you call `setState` or update the `props` of a component, there are several
|
||||
asynchronous stages that must occur before a rerender. Note the following
|
||||
example:
|
||||
|
||||
```jsx
|
||||
class MyComponent extends Component {
|
||||
load() {
|
||||
this.setState({loading: true});
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.state.loading ? <Loading /> : 'Finished!';
|
||||
}
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
// INCORRECT
|
||||
it('Test loading', () => {
|
||||
const wrapper = mount(<MyComponent />);
|
||||
wrapper.load();
|
||||
expect(wrapper.find('Loading').length).toEqual(1); // Will fail
|
||||
});
|
||||
|
||||
// CORRECT
|
||||
it('Test loading', () => {
|
||||
const wrapper = mount(<MyComponent />);
|
||||
wrapper.load();
|
||||
wrapper.update(); // This tells the components to run through a render cycle
|
||||
expect(wrapper.find('Loading').length).toEqual(1);
|
||||
});
|
||||
```
|
||||
|
||||
For more information:
|
||||
|
||||
- [React lifecycle](https://reactjs.org/docs/state-and-lifecycle.html)
|
||||
|
||||
### Accessing `store`, `theme`, routing, browser history, etc.
|
||||
|
||||
The Backstage application has several core providers at its root. To run your
|
||||
test wrapped in a "dummy" Backstage application, you can use our utility
|
||||
functions:
|
||||
|
||||
**`wrapInTestApp`**
|
||||
|
||||
import { wrapInTestApp } from '../../test-utils';
|
||||
...
|
||||
it('Definitely is not a coconut', () => {
|
||||
const mangoWrapper = mount(wrapInTestApp(<Mango />));
|
||||
|
||||
expect(mangoWrapper.context().store).toBeDefined();
|
||||
});
|
||||
|
||||
Note: wrapping in the test application **requires** you to do a `find()` or
|
||||
`dive()` since the wrapped component is now the application.
|
||||
|
||||
# Debugging Jest Tests
|
||||
|
||||
Currently, debugging Jest tests using IntelliJ or `node-debugger` is possible
|
||||
but can be
|
||||
[problematic to set up.](https://intellij-support.jetbrains.com/hc/en-us/community/posts/115000634564-Debugging-Jest-unit-tests)
|
||||
|
||||
It is possible, but you might spend a decent amount of time configuring your
|
||||
IDE.
|
||||
|
||||
In most cases, we have found that using `console.log` works well.
|
||||
|
||||
Note: if your console.logs are not being displayed, focus your specific unit
|
||||
test from the command line by running them like so `yarn test-react MyTest`.
|
||||
Reference in New Issue
Block a user