Merge branch 'master' of https://github.com/backstage/backstage into pr-draft

This commit is contained in:
Lykke Axlin
2021-10-12 15:23:12 +02:00
342 changed files with 4728 additions and 2321 deletions
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-auth-backend': patch
---
AWS-ALB: update provider to the latest changes described [here](https://backstage.io/docs/auth/identity-resolver).
This removes the `ExperimentalIdentityResolver` type in favor of `SignInResolver` and `AuthHandler`.
The AWS ALB provider can now be configured in the same way as the Google provider in the example.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-api-docs': patch
---
Bump `swagger-ui-react` to `^4.0.0-rc.3`.
+19
View File
@@ -0,0 +1,19 @@
---
'@backstage/create-app': patch
---
The scaffolder plugin has just released the beta 3 version of software templates, which replaces the handlebars templating syntax. As part of this change, the template entity schema is no longer included in the core catalog-model as with previous versions. The decoupling of the template entities version will allow us to more easily make updates in the future.
In order to use the new beta 3 templates, the following changes are **required** for any existing installation, inside `packages/backend/src/plugins/catalog.ts`:
```diff
+import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend';
...
const builder = await CatalogBuilder.create(env);
+ builder.addProcessor(new ScaffolderEntitiesProcessor());
const { processingEngine, router } = await builder.build();
```
If you're interested in learning more about creating custom kinds, please check out the [extending the model](https://backstage.io/docs/features/software-catalog/extending-the-model) documentation.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/core-app-api': patch
'@backstage/core-components': patch
'@backstage/theme': patch
---
Internal refactor to avoid importing all of `@material-ui/core`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
update the null check to use the optional chaining operator in case of non-null assertion operator is not working in function extractInitials(values: string)
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-graphiql': patch
---
Add experimental `experimentalInstallationRecipe` to `package.json`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
Resolve a warning in `<Button>` related to not using `React.forwardRef`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
add a --from <location> option to the plugin install command
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': patch
---
Support `material-ui` overrides in SystemDiagramCard and EmptityLinksEmptyState components
+15
View File
@@ -0,0 +1,15 @@
---
'@backstage/plugin-home': patch
---
Adds a `<WelcomeTitle>` component that shows a playful greeting on the home page.
To use it, pass it to the home page header:
```typescript
<Page themeId="home">
<Header title={<WelcomeTitle />} pageTitleOverride="Home">
<HomepageTimer />
</Header>
</Page>
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-api-docs': patch
---
Remove unused dependency on material-icons/font
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/core-components': minor
'@backstage/plugin-catalog-graph': minor
---
Add documentation and more type safety around DependencyGraph
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/plugin-catalog-backend': minor
---
This continues the deprecation of classes used by the legacy catalog engine. New deprecations can be viewed in this [PR](https://github.com/backstage/backstage/pull/7500) or in the API reference documentation.
The `batchAddOrUpdateEntities` method of the `EntitiesCatalog` interface has been marked as optional and is being deprecated. It is still implemented and required to be implemented by the legacy catalog classes, but was never implemented in the new catalog.
This change is only relevant if you are consuming the `EntitiesCatalog` interface directly, in which case you will get a type error that you need to resolve. It can otherwise be ignored.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Bump sucrase to version 3.20.2
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Added extra configuration parameters for active directory file system identity
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Add semicolon in template to make prettier happy
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-azure-devops-backend': patch
---
Updates function for mapping RepoBuilds to handle undefined properties
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-code-coverage-backend': patch
---
check for existence of lines property in files
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search': patch
---
SearchBar component to accept optional placeholder prop
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': minor
---
The syntax highlighting library used by the `CodeSnippet` component is now lazy loaded. This most likely has no effect on existing code, but may break tests as the content of the `CodeSnippet` is now rendered asynchronously.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
This change refactors the internal package structure to remove the `next` catalog folder that was used during the implementation and testing phase of the new catalog engine. The implementation is now the default and is therefore restructured to no longer be packaged under `next/`. This refactor does not change catalog imports from other parts of the project.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Update default backend ESLint configuration to allow usage of `__dirname` in tests.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/cli': patch
---
Add an experimental `install <plugin>` command.
Given a `pluginId`, the command looks for NPM packages matching `@backstage/plugin-{pluginId}` or `backstage-plugin-{pluginId}` or `{pluginId}`. It looks for the `experimentalInstallationRecipe` in their `package.json` for the steps of installation. Detailed documentation and API Spec to follow (and to be decided as well).
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-kubernetes-backend': patch
'@backstage/plugin-kubernetes': patch
---
Refactor kubernetes fetcher to reduce boilerplate code
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': patch
---
Support `material-ui` overrides in plugin-catalog-react components
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-catalog': patch
'@backstage/plugin-catalog-react': patch
---
added sorting in entity `Name` column by `metadata.title` if present
+2
View File
@@ -54,3 +54,5 @@
| [CastDesk](https://castdesk.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe |
| [Santagostino](https://santagostino.it) | [@santagostino](https://github.com/santagostino) | Developer portal, gateway to our infrastructure, documentation, service catalog and internal tooling. |
| [Peak](https://peak.ai) | [Luke Beamish](https://github.com/lukebeamish-peak) | Developer portal for all internal engineers to access documentation and tooling. |
| [Gelato](https://gelato.com/) | [Dmitry Makarenko](https://github.com/dmitry-makarenko-gelato) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal and third-party systems🚀. |
| [GoCardless](https://gocardless.com/) | [James Turley](https://github.com/tragiclifestories) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal systems. |
+4 -3
View File
@@ -129,9 +129,10 @@ In general, changesets are not needed for the documentation, build utilities, co
1. Run `yarn changeset`
2. Select which packages you want to include a changeset for
3. Select impact of change that you're introducing, using `minor` for breaking changes and `patch` otherwise. We do not use `major` changes while packages are at version `0.x`.
4. Add generated changeset to Git
5. Push the commit with your changeset to the branch associated with your PR
6. Accept our gratitude for making the release process easier on the maintainers
4. Explain your changes in the generated changeset. See [examples of well written changesets](https://backstage.io/docs/getting-started/contributors#writing-changesets).
5. Add generated changeset to Git
6. Push the commit with your changeset to the branch associated with your PR
7. Accept our gratitude for making the release process easier on the maintainers
For more information, checkout [adding a changeset](https://github.com/atlassian/changesets/blob/master/docs/adding-a-changeset.md) documentation in the changesets repository.
+52 -24
View File
@@ -100,17 +100,16 @@ const app = createApp({
### Backend
When using ALB auth it is not possible to leverage the built-in auth config discovery mechanism implemented in the app created by default; bespoke logic needs to be implemented.
When using ALB auth you can configure it as described [here](https://backstage.io/docs/auth/identity-resolver).
- replace the content of `packages/backend/plugin/auth.ts` with the below
- replace the content of `packages/backend/plugin/auth.ts` with the below and tweak it according to your needs.
```ts
import {
createRouter,
AuthResponse,
AuthProviderFactoryOptions,
defaultAuthProviderFactories,
createAwsAlbProvider,
} from '@backstage/plugin-auth-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin({
@@ -118,30 +117,59 @@ export default async function createPlugin({
database,
config,
discovery,
}: PluginEnvironment) {
const identityResolver = (payload: any): Promise<AuthResponse<any>> => {
return Promise.resolve({
providerInfo: {},
profile: {
email: payload.email,
displayName: payload.name,
picture: payload.picture,
},
backstageIdentity: {
id: payload.email,
},
});
};
const providerFactories = {
awsalb: (options: AuthProviderFactoryOptions) =>
defaultAuthProviderFactories.awsalb({ ...options, identityResolver }),
};
}: PluginEnvironment): Promise<Router> {
return await createRouter({
logger,
config,
database,
discovery,
providerFactories,
providerFactories: {
awsalb: createAwsAlbProvider({
authHandler: async ({ fullProfile }) => {
let email: string | undefined = undefined;
if (fullProfile.emails && fullProfile.emails.length > 0) {
const [firstEmail] = fullProfile.emails;
email = firstEmail.value;
}
let picture: string | undefined = undefined;
if (fullProfile.photos && fullProfile.photos.length > 0) {
const [firstPhoto] = fullProfile.photos;
picture = firstPhoto.value;
}
const displayName: string | undefined =
fullProfile.displayName ?? fullProfile.username ?? fullProfile.id;
return {
profile: {
email,
picture,
displayName,
},
};
},
signIn: {
resolver: async ({ profile: { email } }, ctx) => {
const [id] = email?.split('@') ?? '';
// Fetch from an external system that returns entity claims like:
// ['user:default/breanna.davison', ...]
const ent = [`user:default/${id}`];
// Resolve group membership from the Backstage catalog
const fullEnt =
await ctx.catalogIdentityClient.resolveCatalogMembership({
entityRefs: [id].concat(ent),
logger: ctx.logger,
});
const token = await ctx.tokenIssuer.issueToken({
claims: { sub: id, ent: fullEnt },
});
return { id, token };
},
},
}),
},
});
}
```
@@ -0,0 +1,43 @@
---
id: adrs-adr012
title: ADR000: Use Luxon.toLocaleString and date/time presets
description: Architecture Decision Record (ADR) for using Luxon's toLocaleString method and date/time presets for displaying dates and times
---
## Context
User's locales will have their own style of reading dates. It's counter
intuitive to not have dates formatted in their familiar formats, it can cause
users to have to think harder about what the date is and could even lead to
interpreting dates incorrectly (e.g. 05/03/2021, this could be March 5th or May
3rd, depending on where the user is). At the moment, plugins are defining dates
and times using custom formats and the `toFormat` method, which leads to
inconsistent and unfamiliar formats.
## Decision
To keep the UI consistent and familiar to users, irrespective of their location,
we have decided that we use `toLocaleString` and Luxon's
[extensive list](https://github.com/moment/luxon/blob/master/docs/formatting.md#presets)
of Date and Time presets.
Here is an example:
```typescript
const date = new luxon.DateTime();
/* Avoid this: */
date.toFormat('yyyy LLL dd'); // 2014 Aug 06
date.toFormat('yyyy LLL dd hh:mm'); // 2014 Aug 06 12:01
/* Do this instead: */
date.toLocaleString(luxon.DateTime.DATE_MED); // US: Oct 14, 1983 | FR: 14 oct. 1983
date.toLocaleString(luxon.DateTime.DATETIME_MED); // US: Oct 14, 1983, 9:30 | FR: 14 oct. 1983 9:30
```
## Consequences
- We will need to audit the current places Date/Times are being displayed in the
UI and update them to follow this ADR.
- We will need to keep in mind for reviewing PRs going forward to follow this
ADR, or find/create a linting rule to automate this in the review process.
+1 -1
View File
@@ -76,7 +76,7 @@ CMD ["node", "packages/backend", "--config", "app-config.yaml"]
For more details on how the `backend:bundle` command and the `skeleton.tar.gz`
file works, see the
[`backend:bundle` command docs](../cli/commands.md#backendbundle).
[`backend:bundle` command docs](../local-dev/cli-commands.md#backendbundle).
The `Dockerfile` is located at `packages/backend/Dockerfile`, but needs to be
executed with the root of the repo as the build context, in order to get access
+11 -7
View File
@@ -102,20 +102,24 @@ more to come...
See [Backstage Search Architecture](architecture.md) to get an overview of how
the search engines are used.
| Search Engine | Support Status |
| ------------- | -------------- |
| Basic (lunr) | ✅ |
| ElasticSearch | Not yet ❌ |
| Search Engines | Support Status |
| -------------------------------------------------- | -------------- |
| [Lunr](./search-engines.md#lunr) | ✅ |
| [ElasticSearch](./search-engines.md#elasticsearch) | |
| [Postgres](./search-engines.md#postgres) | ✅ |
[Reach out to us](#feedback) if you want to chat about support for more search
engines.
[Reach out to us](#get-involved) if you want to chat about support for more
search engines.
## Plugins Integrated with Search
| Plugin | Support Status |
| -------- | -------------- |
| Catalog | ✅ |
| TechDocs | Not yet ❌ |
| TechDocs | |
[Reach out to us](#get-involved) if you want to chat about support for more
plugins integrated to search.
## Tech Stack
+46
View File
@@ -0,0 +1,46 @@
---
id: how-to-guides
title: Search "HOW TO" guides
sidebar_label: "HOW TO" guides
description: Search "HOW TO" guides
---
## How to implement your own Search API
The Search plugin provides implementation of one primary API by default: the
[SearchApi](https://github.com/backstage/backstage/blob/db2666b980853c281b8fe77905d7639c5d255f13/plugins/search/src/apis.ts#L35),
which is responsible for talking to the search-backend to query search results.
There may be occasions where you need to implement this API yourself, to
customize it to your own needs - for example if you have your own search backend
that you want to talk to. The purpose of this guide is to walk you through how
to do that in two steps.
1. Implement the `SearchApi`
[interface](https://github.com/backstage/backstage/blob/db2666b980853c281b8fe77905d7639c5d255f13/plugins/search/src/apis.ts#L31)
according to your needs.
```typescript
export class SearchClient implements SearchApi {
// your implementation
}
```
2. Override the API ref `searchApiRef` with your new implemented API in the
`App.tsx` using `ApiFactories`.
[Read more about App APIs](https://backstage.io/docs/api/utility-apis#app-apis).
```typescript
const app = createApp({
apis: [
// SearchApi
createApiFactory({
api: searchApiRef,
deps: { discovery: discoveryApiRef },
factory({ discovery }) {
return new SearchClient({ discoveryApi: discovery });
},
}),
],
});
```
@@ -429,3 +429,101 @@ from one environment to the other, do rollbacks, see their relative performance
metrics, and similar. This coherency and collection of tooling in one place is
where something like Backstage can offer the most value and effectiveness of
use. Splitting your entities apart into small islands makes this harder.
## Implementing custom model extensions
This section walks you through the steps involved extending the catalog model
with a new Entity type.
### Creating a custom entity definition
The first step of introducing a custom entity is to define what shape and schema
it has. We do this both using a TypeScript type, along with a JSONSchema schema.
Most of the time you will want to have at least the TypeScript type of your
extension available in both frontend and backend code, which means you likely
want to have an isomorphic package that houses these types. Within the Backstage
main repo the package naming pattern of `<plugin>-common` is used for isomorphic
packages, and you may choose to adopt this pattern as well.
There's at this point no existing templates for generating isomorphic plugins
using the `@backstage/cli`. Perhaps the simplest wat to get started right now is
to copy the contents of one of the existing packages in the main repository,
such as `plugins/scaffolder-common`, and rename the folder and file contents to
the desired name. This example uses _foobar_ as the plugin name so the plugin
will be named _foobar-common_.
Once you have a common package in place you can start adding your own entity
definitions. For the exact details on how to do that we defer to getting
inspired by the existing
[scaffolder-common](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-common/src/index.ts)
package. But in short you will need to declare a TypeScript type and a
JSONSchema for the new entity kind.
### Building a custom processor for the entity
The next step is to create a custom processor for your new entity kind. This
will be used within the catalog to make sure that it's able to ingest and
validate entities of our new kind. Just like with the definition package, you
can find inspiration in for example the existing
[ScaffolderEntitiesProcessor](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts).
We also provide a high-level example of what a catalog process for a custom
entity might look like:
```ts
import { entityKindSchemaValidator } from '@backstage/catalog-model';
export class FoobarEntitiesProcessor implements CatalogProcessor {
// You often end up wanting to support multiple versions of your kind as you
// iterate on the definition, so we keep each version inside this array.
private readonly validators = [
// This is where we use the JSONSchema that we export from our isomorphic package
entityKindSchemaValidator(foobarEntityV1alpha1Schema),
];
// validateEntityKind is responsible for signaling to the catalog processing engine
// that this entity is valid and should therefore be submitted for further processing.
async validateEntityKind(entity: Entity): Promise<boolean> {
for (const validator of this.validators) {
if (validator(entity)) {
return true;
}
}
return false;
}
async postProcessEntity(
entity: Entity,
_location: LocationSpec,
emit: CatalogProcessorEmit,
): Promise<Entity> {
if (
entity.apiVersion === 'example.com/v1alpha1' &&
entity.kind === 'Foobar'
) {
const foobarEntity = entity as FoobarEntityV1alpha1;
// Here we can modify the entity or emit results related to the entity
// Typically you will want to emit any relations associated with the entity here
emit(results.relation({ ... }))
}
return entity;
}
}
```
Once the processor is created it can be wired up to the catalog via the
`CatalogBuilder` in `packages/backend/src/plugins/catalog.ts`:
```diff
+ import { FoobarEntitiesProcessor implements CatalogProcessor {
} from '@internal/plugin-foobar-backend';
// ...
const builder = await CatalogBuilder.create(env);
+ builder.addProcessor(new FoobarEntitiesProcessor());
const { processingEngine, router } = await builder.build();
```
+89
View File
@@ -139,3 +139,92 @@ default app configs.
You can learn more about the local configuration in
[Static Configuration in Backstage](../conf/) section.
## Writing changesets
Changesets are an important part of the development process. They are used to
generate Changelog entries for all changes to the project. Ultimately they are
read by the end users to learn about important changes and fixes to the project.
Some of these fixes might require manual intervention from users so it's
important to write changesets that users understand and can take action on.
Here are some important do's and don'ts when writing changesets:
### Changeset should give a clear description to what has changed
#### Bad
```
---
'@backstage/catalog': patch
---
Fixed table layout
```
#### Good
```
---
'@backstage/catalog': patch
---
Fixed bug in EntityTable component where table layout did not readjust properly below 1080x768 pixels.
```
### Breaking changes not caught by the type checker should be clearly marked with bold **BREAKING** text
#### Bad
```
---
'@backstage/catalog': minor
---
getEntity is now a function that returns a Promise.
```
#### Good
```
---
'@backstage/catalog': minor
---
**BREAKING** The getEntity function now returns a Promise and **must** be awaited from now on.
```
### Changes to code should include a diff of the files that need updating
#### Bad
```
---
'@backstage/catalog': patch
---
**BREAKING** The catalogEngine now requires a flux capacitor to be passed.
```
#### Good
---
'@backstage/catalog': patch
---
**BREAKING** The catalog createRouter now requires that a `FluxCapacitor` is
passed to the router.
These changes are **required** to `packages/backend/src/plugins/catalog.ts`
```diff
+ import { FluxCapacitor } from '@backstage/time';
+ const fluxCapacitor = new FluxCapacitor();
return await createRouter({
entitiesCatalog,
locationAnalyzer,
locationService,
+ fluxCapacitor,
logger: env.logger,
config: env.config,
});
```
-53
View File
@@ -132,56 +132,3 @@ Now you're free to hack away on your own Backstage installation!
As you get more experienced with the app, in future you can run just the
frontend with `yarn start` in one window, and the backend with
`yarn start-backend` in a different window.
## Linking in local Backstage packages
It can often be useful to try out changes to the packages in the main Backstage
repo within your own app. For example if you want to make modifications to
`@backstage/core-plugin-api` and try them out in your app.
To link in external packages, add them to your `package.json` and `lerna.json`
workspace paths. These can be either relative or absolute paths with or without
globs. For example:
```json
"packages": [
"packages/*",
"plugins/*",
"../backstage/packages/core-plugin-api", // New path added to work on @backstage/core-plugin-api
],
```
Then reinstall packages to make yarn set up symlinks:
```bash
yarn install
```
With this in place you can now modify the `@backstage/core-plugin-api` package
within the main repo, and have those changes be reflected and tested in your
app. Simply run your app using `yarn dev` (or `yarn start` for just frontend) as
normal.
Note that for backend packages you need to make sure that linked packages are
not dependencies of any non-linked package. If you for example want to work on
`@backstage/backend-common`, you need to also link in other backend plugins and
packages that depend on `@backstage/backend-common`, or temporarily disable
those plugins in your backend. This is because the transformation of backend
module tree stops whenever a non-local package is encountered, and from that
point node will `require` packages directly for that entire module subtree.
Type checking can also have issues when linking in external packages, since the
linked in packages will use the types in the external project and dependency
version mismatches between the two projects may cause errors. To fix any of
those errors you need to sync versions of the dependencies in the two projects.
A simple way to do this can be to copy over `yarn.lock` from the external
project and run `yarn install`, although this is quite intrusive and can cause
other issues in existing projects, so use this method with care. It can often be
best to simply ignore the type errors, as app serving will work just fine
anyway.
Another issue with type checking is that the incremental type cache doesn't
invalidate correctly for the linked in packages, causing type checking to not
reflect changes made to types. You can work around this by either setting
`compilerOptions.incremental = false` in `tsconfig.json`, or by deleting the
types cache folder `dist-types` before running `yarn tsc`.
@@ -1,6 +1,6 @@
---
id: commands
title: Commands
id: cli-commands
title: CLI Commands
description: Descriptions of all commands available in the CLI.
---
@@ -1,6 +1,6 @@
---
id: index
title: Overview
id: cli-overview
title: CLI Overview
description: Overview of the Backstage CLI
---
@@ -20,7 +20,7 @@ Under the hood the CLI uses [Webpack](https://webpack.js.org/) for bundling,
linting. It also includes custom tooling for working within Backstage apps, for
example for keeping the app up to date and verifying static configuration.
For a full list of CLI commands, see the [commands](./commands.md) page.
For a full list of CLI commands, see the [commands](./cli-commands.md) page.
## Introduction
+56
View File
@@ -0,0 +1,56 @@
---
id: linking-local-packages
title: Linking in Local Packages
description: How to link in other local packages into your Backstage monorepo
---
It can often be useful to try out changes to the packages in the main Backstage
repo within your own app. For example if you want to make modifications to
`@backstage/core-plugin-api` and try them out in your app.
To link in external packages, add them to your `package.json` and `lerna.json`
workspace paths. These can be either relative or absolute paths with or without
globs. For example:
```json
"packages": [
"packages/*",
"plugins/*",
"../backstage/packages/core-plugin-api", // New path added to work on @backstage/core-plugin-api
],
```
Then reinstall packages to make yarn set up symlinks:
```bash
yarn install
```
With this in place you can now modify the `@backstage/core-plugin-api` package
within the main repo, and have those changes be reflected and tested in your
app. Simply run your app using `yarn dev` (or `yarn start` for just frontend) as
normal.
Note that for backend packages you need to make sure that linked packages are
not dependencies of any non-linked package. If you for example want to work on
`@backstage/backend-common`, you need to also link in other backend plugins and
packages that depend on `@backstage/backend-common`, or temporarily disable
those plugins in your backend. This is because the transformation of backend
module tree stops whenever a non-local package is encountered, and from that
point node will `require` packages directly for that entire module subtree.
Type checking can also have issues when linking in external packages, since the
linked in packages will use the types in the external project and dependency
version mismatches between the two projects may cause errors. To fix any of
those errors you need to sync versions of the dependencies in the two projects.
A simple way to do this can be to copy over `yarn.lock` from the external
project and run `yarn install`, although this is quite intrusive and can cause
other issues in existing projects, so use this method with care. It can often be
best to simply ignore the type errors, as app serving will work just fine
anyway.
Another issue with type checking is that the incremental type cache doesn't
invalidate correctly for the linked in packages, causing type checking to not
reflect changes made to types. You can work around this by either setting
`compilerOptions.incremental = false` in `tsconfig.json`, or by deleting the
types cache folder `dist-types` before running `yarn tsc`.
+2 -1
View File
@@ -10,7 +10,8 @@ A Backstage Plugin adds functionality to Backstage.
To create a new plugin, make sure you've run `yarn install` and installed
dependencies, then run the following on your command line (a shortcut to
invoking the [`backstage-cli create-plugin`](../cli/commands.md#create-plugin))
invoking the
[`backstage-cli create-plugin`](../local-dev/cli-commands.md#create-plugin))
from the root of your project.
```bash
+3 -3
View File
@@ -36,7 +36,7 @@ that we provide. This gives us a way to automate some of the work required to
create a GitHub app.
You can read more about the
[`backstage-cli create-github-app` method](../cli/commands.md#create-github-app).
[`backstage-cli create-github-app` method](../local-dev/cli-commands.md#create-github-app).
Once you've gone through the CLI command, it should produce a YAML file in the
root of the project which you can then use as an `include` in your
@@ -60,7 +60,7 @@ The YAML file must include the following information. Please note that the
indentation for the `privateKey` is required.
```yaml
appId: 1
appId: app id
clientId: client id
clientSecret: client secret
webhookSecret: webhook secret
@@ -95,7 +95,7 @@ If you want to limit the GitHub app installations visible to backstage you may
optionally include the `allowedInstallationOwners` option.
```yaml
appId: 1
appId: app id
allowedInstallationOwners: ['GlobexCorp']
clientId: client id
clientSecret: client secret
+13
View File
@@ -0,0 +1,13 @@
---
title: Grafana
author: K-Phoen
authorUrl: https://github.com/K-Phoen
category: Monitoring
description: Associate alerts and dashboards to components.
documentation: https://github.com/K-Phoen/backstage-plugin-grafana/
iconUrl: https://avatars.githubusercontent.com/u/7195757?s=200&v=4
npmPackageName: '@k-phoen/backstage-plugin-grafana'
tags:
- dashboards
- monitoring
- alerting
+11 -2
View File
@@ -29,7 +29,14 @@
"getting-started/contributors",
"getting-started/project-structure"
],
"CLI": ["cli/index", "cli/commands"],
"Local Development": [
{
"type": "subcategory",
"label": "CLI",
"ids": ["local-dev/cli-overview", "local-dev/cli-commands"]
},
"local-dev/linking-local-packages"
],
"Core Features": [
{
"type": "subcategory",
@@ -83,7 +90,8 @@
"features/search/getting-started",
"features/search/concepts",
"features/search/architecture",
"features/search/search-engines"
"features/search/search-engines",
"features/search/how-to-guides"
]
},
{
@@ -210,6 +218,7 @@
"label": "Included providers",
"ids": [
"auth/auth0/provider",
"auth/bitbucket/provider",
"auth/microsoft/provider",
"auth/github/provider",
"auth/gitlab/provider",
+4
View File
@@ -1217,3 +1217,7 @@ code {
.medium-zoom-image {
z-index: 10000;
}
h3.collapsible span.arrow {
margin-right: 4px;
}
+6 -3
View File
@@ -27,9 +27,11 @@ nav:
- Key Concepts: 'getting-started/concepts.md'
- Contributors: 'getting-started/contributors.md'
- Project Structure: 'getting-started/project-structure.md'
- CLI:
- Overview: 'cli/index.md'
- Commands: 'cli/commands.md'
- Local Development:
- CLI:
- Overview: 'local-dev/cli-overview.md'
- Commands: 'local-dev/cli-commands.md'
- Linking in Local Packages: 'local-dev/linking-local-packages.md'
- Core Features:
- Software Catalog:
- Overview: 'features/software-catalog/index.md'
@@ -66,6 +68,7 @@ nav:
- Concepts: 'features/search/concepts.md'
- Search Architecture: 'features/search/architecture.md'
- Search Engines: 'features/search/search-engines.md'
- HOW TO guides: 'features/search/how-to-guides.md'
- TechDocs:
- Overview: 'features/techdocs/README.md'
- Getting Started: 'features/techdocs/getting-started.md'
-2
View File
@@ -61,7 +61,6 @@
"devDependencies": {
"@types/webpack": "^5.28.0",
"@changesets/cli": "^2.14.0",
"@octokit/openapi-types": "^2.2.0",
"@spotify/prettier-config": "^11.0.0",
"command-exists": "^1.2.9",
"concurrently": "^6.0.0",
@@ -71,7 +70,6 @@
"lerna": "^4.0.0",
"lint-staged": "^11.1.2",
"prettier": "^2.2.1",
"recursive-readdir": "^2.2.2",
"shx": "^0.3.2",
"yarn-lock-check": "^1.0.5"
},
+11 -10
View File
@@ -14,25 +14,26 @@
* limitations under the License.
*/
import React from 'react';
import Grid from '@material-ui/core/Grid';
import {
HomePageRandomJoke,
ComponentAccordion,
ComponentTabs,
ComponentTab,
} from '@backstage/plugin-home';
import {
Content,
Header,
Page,
HomepageTimer,
Page,
} from '@backstage/core-components';
import {
ComponentAccordion,
ComponentTab,
ComponentTabs,
HomePageRandomJoke,
WelcomeTitle,
} from '@backstage/plugin-home';
import { HomePageSearchBar } from '@backstage/plugin-search';
import Grid from '@material-ui/core/Grid';
import React from 'react';
export const HomePage = () => (
<Page themeId="home">
<Header title="Home">
<Header title={<WelcomeTitle />} pageTitleOverride="Home">
<HomepageTimer />
</Header>
<Content>
@@ -135,13 +135,7 @@ describe('AwsS3UrlReader', () => {
'getObject',
Buffer.from(
require('fs').readFileSync(
path.resolve(
'src',
'reading',
'__fixtures__',
'awsS3',
'awsS3-mock-object.yaml',
),
path.resolve(__dirname, '__fixtures__/awsS3/awsS3-mock-object.yaml'),
),
),
);
@@ -187,13 +181,7 @@ describe('AwsS3UrlReader', () => {
'getObject',
Buffer.from(
require('fs').readFileSync(
path.resolve(
'src',
'reading',
'__fixtures__',
'awsS3',
'awsS3-mock-object.yaml',
),
path.resolve(__dirname, '__fixtures__/awsS3/awsS3-mock-object.yaml'),
),
),
);
@@ -249,13 +237,7 @@ describe('AwsS3UrlReader', () => {
'getObject',
Buffer.from(
require('fs').readFileSync(
path.resolve(
'src',
'reading',
'__fixtures__',
'awsS3',
'awsS3-mock-object.yaml',
),
path.resolve(__dirname, '__fixtures__/awsS3/awsS3-mock-object.yaml'),
),
),
);
@@ -152,7 +152,7 @@ describe('AzureUrlReader', () => {
describe('readTree', () => {
const repoBuffer = fs.readFileSync(
path.resolve('src', 'reading', '__fixtures__', 'mock-main.zip'),
path.resolve(__dirname, '__fixtures__/mock-main.zip'),
);
const processor = new AzureUrlReader(
@@ -264,7 +264,7 @@ describe('AzureUrlReader', () => {
describe('search', () => {
const repoBuffer = fs.readFileSync(
path.resolve('src', 'reading', '__fixtures__', 'mock-main.zip'),
path.resolve(__dirname, '__fixtures__/mock-main.zip'),
);
const processor = new AzureUrlReader(
@@ -104,20 +104,13 @@ describe('BitbucketUrlReader', () => {
describe('readTree', () => {
const repoBuffer = fs.readFileSync(
path.resolve(
'src',
'reading',
'__fixtures__',
'bitbucket-repo-with-commit-hash.tar.gz',
__dirname,
'__fixtures__/bitbucket-repo-with-commit-hash.tar.gz',
),
);
const privateBitbucketRepoBuffer = fs.readFileSync(
path.resolve(
'src',
'reading',
'__fixtures__',
'bitbucket-server-repo.tar.gz',
),
path.resolve(__dirname, '__fixtures__/bitbucket-server-repo.tar.gz'),
);
beforeEach(() => {
@@ -298,10 +291,8 @@ describe('BitbucketUrlReader', () => {
describe('search hosted', () => {
const repoBuffer = fs.readFileSync(
path.resolve(
'src',
'reading',
'__fixtures__',
'bitbucket-repo-with-commit-hash.tar.gz',
__dirname,
'__fixtures__/bitbucket-repo-with-commit-hash.tar.gz',
),
);
@@ -386,12 +377,7 @@ describe('BitbucketUrlReader', () => {
describe('search private', () => {
const privateBitbucketRepoBuffer = fs.readFileSync(
path.resolve(
'src',
'reading',
'__fixtures__',
'bitbucket-server-repo.tar.gz',
),
path.resolve(__dirname, '__fixtures__/bitbucket-server-repo.tar.gz'),
);
beforeEach(() => {
@@ -254,12 +254,7 @@ describe('GithubUrlReader', () => {
describe('readTree', () => {
const repoBuffer = fs.readFileSync(
path.resolve(
'src',
'reading',
'__fixtures__',
'backstage-mock-etag123.tar.gz',
),
path.resolve(__dirname, '__fixtures__/backstage-mock-etag123.tar.gz'),
);
const reposGithubApiResponse = {
@@ -540,12 +535,7 @@ describe('GithubUrlReader', () => {
describe('search', () => {
const repoBuffer = fs.readFileSync(
path.resolve(
'src',
'reading',
'__fixtures__',
'backstage-mock-etag123.tar.gz',
),
path.resolve(__dirname, '__fixtures__/backstage-mock-etag123.tar.gz'),
);
const githubTreeContents: GhTreeResponse['tree'] = [
@@ -223,7 +223,7 @@ describe('GitlabUrlReader', () => {
describe('readTree', () => {
const archiveBuffer = fs.readFileSync(
path.resolve('src', 'reading', '__fixtures__', 'gitlab-archive.tar.gz'),
path.resolve(__dirname, '__fixtures__/gitlab-archive.tar.gz'),
);
const projectGitlabApiResponse = {
@@ -495,7 +495,7 @@ describe('GitlabUrlReader', () => {
describe('search', () => {
const archiveBuffer = fs.readFileSync(
path.resolve('src', 'reading', '__fixtures__', 'gitlab-archive.tar.gz'),
path.resolve(__dirname, '__fixtures__/gitlab-archive.tar.gz'),
);
const projectGitlabApiResponse = {
+2
View File
@@ -15,6 +15,7 @@
*/
import { CatalogBuilder } from '@backstage/plugin-catalog-backend';
import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
@@ -22,6 +23,7 @@ export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const builder = await CatalogBuilder.create(env);
builder.addProcessor(new ScaffolderEntitiesProcessor());
const { processingEngine, router } = await builder.build();
await processingEngine.start();
return router;
+2
View File
@@ -46,6 +46,8 @@ describe('paths', () => {
const dir = resolvePath(__dirname, '..');
const root = resolvePath(__dirname, '../../..');
jest.spyOn(process, 'cwd').mockReturnValue(dir);
const paths = findPaths(__dirname);
expect(paths.ownDir).toBe(dir);
+11 -6
View File
@@ -14,6 +14,15 @@
* limitations under the License.
*/
const globalRestrictedSyntax = [
{
message:
'Default import from winston is not allowed, import `* as winston` instead.',
selector:
'ImportDeclaration[source.value="winston"] ImportDefaultSpecifier',
},
];
module.exports = {
extends: [
'@spotify/eslint-config-base',
@@ -69,17 +78,12 @@ module.exports = {
// Avoid default import from winston as it breaks at runtime
'no-restricted-syntax': [
'error',
{
message:
'Default import from winston is not allowed, import `* as winston` instead.',
selector:
'ImportDeclaration[source.value="winston"] ImportDefaultSpecifier',
},
{
message:
"`__dirname` doesn't refer to the same dir in production builds, try `resolvePackagePath()` from `@backstage/backend-common` instead.",
selector: 'Identifier[name="__dirname"]',
},
...globalRestrictedSyntax,
],
},
overrides: [
@@ -103,6 +107,7 @@ module.exports = {
bundledDependencies: true,
},
],
'no-restricted-syntax': ['error', ...globalRestrictedSyntax],
},
},
],
+3 -2
View File
@@ -18,7 +18,7 @@ const fs = require('fs-extra');
const path = require('path');
const glob = require('util').promisify(require('glob'));
async function getProjectConfig(targetPath) {
async function getProjectConfig(targetPath, displayName) {
const configJsPath = path.resolve(targetPath, 'jest.config.js');
const configTsPath = path.resolve(targetPath, 'jest.config.ts');
// If the package has it's own jest config, we use that instead.
@@ -73,6 +73,7 @@ async function getProjectConfig(targetPath) {
const transformModulePattern = transformModules && `(?!${transformModules})`;
const options = {
displayName,
rootDir: path.resolve(targetPath, 'src'),
coverageDirectory: path.resolve(targetPath, 'coverage'),
collectCoverageFrom: ['**/*.{js,jsx,ts,tsx}', '!**/*.d.ts'],
@@ -143,7 +144,7 @@ async function getRootConfig() {
const packageData = await fs.readJson(packagePath);
const testScript = packageData.scripts && packageData.scripts.test;
if (testScript && testScript.includes('backstage-cli test')) {
return await getProjectConfig(projectPath);
return await getProjectConfig(projectPath, packageData.name);
}
return undefined;
+1 -1
View File
@@ -104,7 +104,7 @@
"run-script-webpack-plugin": "^0.0.11",
"semver": "^7.3.2",
"style-loader": "^1.2.1",
"sucrase": "^3.20.1",
"sucrase": "^3.20.2",
"tar": "^6.1.2",
"terser-webpack-plugin": "^5.1.3",
"ts-loader": "^8.0.17",
+9
View File
@@ -230,6 +230,15 @@ export function registerCommands(program: CommanderStatic) {
.command('info')
.description('Show helpful information for debugging and reporting bugs')
.action(lazy(() => import('./info').then(m => m.default)));
program
.command('install [plugin-id]', { hidden: true })
.option(
'--from <packageJsonFilePath>',
'Install from a local package.json containing the installation recipe',
)
.description('Install a Backstage plugin [EXPERIMENTAL]')
.action(lazy(() => import('./install/install').then(m => m.default)));
}
// Wraps an action function so that it always exits and handles errors
@@ -0,0 +1,118 @@
/*
* Copyright 2021 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 { Step, PackageWithInstallRecipe } from './types';
import { fetchPackageInfo } from '../../lib/versioning';
import { NotFoundError } from '../../lib/errors';
import * as stepDefinitionMap from './steps';
import { Command } from 'commander';
import fs from 'fs-extra';
const stepDefinitions = Object.values(stepDefinitionMap);
async function fetchPluginPackage(
id: string,
): Promise<PackageWithInstallRecipe> {
const searchNames = [`@backstage/plugin-${id}`, `backstage-plugin-${id}`, id];
for (const name of searchNames) {
try {
const packageInfo = (await fetchPackageInfo(
name,
)) as PackageWithInstallRecipe;
return packageInfo;
} catch (error) {
if (error.name !== 'NotFoundError') {
throw error;
}
}
}
throw new NotFoundError(
`No matching package found for '${id}', tried ${searchNames.join(', ')}`,
);
}
type Steps = Array<{
type: string;
step: Step;
}>;
class PluginInstaller {
static async resolveSteps(pkg: PackageWithInstallRecipe) {
const steps: Steps = [];
// collectDependencies
// TODO: Deps mean the plugin package itself, and any other backstage plugins/packages it depends on, in its installation recipe.
const dependencies = [];
dependencies.push({
target: 'packages/app',
type: 'dependencies' as const,
name: pkg.name,
query: `^${pkg.version}`,
});
steps.push({
type: 'dependencies',
step: stepDefinitionMap.dependencies.create({ dependencies }),
});
for (const step of pkg.experimentalInstallationRecipe?.steps ?? []) {
const { type } = step;
const definition = stepDefinitions.find(d => d.type === type);
if (definition) {
steps.push({
type,
step: definition.deserialize(step, pkg),
});
} else {
throw new Error(`Unsupported step type: ${type}`);
}
}
return steps;
}
constructor(private readonly steps: Steps) {}
async run() {
for (const { type, step } of this.steps) {
// TODO(Rugvip): Add spinners, nicer message about the step.
console.log(`Running step ${type}`);
await step.run();
}
}
}
export default async (pluginId?: string, cmd?: Command) => {
// TODO(himanshu): If no plugin id is provided, it should list all plugins available. Maybe in some other command?
let pkg: PackageWithInstallRecipe;
if (pluginId) {
pkg = await fetchPluginPackage(pluginId);
} else if (cmd?.from) {
// TODO(himanshu): Also support reading directly from url
pkg = await fs.readJson(cmd.from);
} else {
throw new Error(
'Missing both <plugin-id> or a package.json file path in the --from flag.',
);
}
const steps = await PluginInstaller.resolveSteps(pkg);
const installer = new PluginInstaller(steps);
await installer.run();
};
@@ -0,0 +1,92 @@
/*
* Copyright 2021 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 fs from 'fs-extra';
import { paths } from '../../../lib/paths';
import { Step, createStepDefinition } from '../types';
type Data = {
path: string;
element: string;
packageName: string;
};
class AppRouteStep implements Step {
constructor(private readonly data: Data) {}
async run() {
const { path, element, packageName } = this.data;
const appTsxPath = paths.resolveTargetRoot('packages/app/src/App.tsx');
const contents = await fs.readFile(appTsxPath, 'utf-8');
let failed = false;
// Add a new route just above the end of the FlatRoutes block
const contentsWithRoute = contents.replace(
/(\s*)<\/FlatRoutes>/,
`$1 <Route path="${path}" element={${element}} />$1</FlatRoutes>`,
);
if (contentsWithRoute === contents) {
failed = true;
}
// Grab the component name from the element
const componentName = element.match(/[A-Za-z0-9]+/)?.[0];
if (!componentName) {
throw new Error(`Could not find component name in ${element}`);
}
// Add plugin import
// TODO(Rugvip): Attempt to add this among the other plugin imports
const contentsWithImport = contentsWithRoute.replace(
/^import /m,
`import { ${componentName} } from '${packageName}';\nimport `,
);
if (contentsWithImport === contentsWithRoute) {
failed = true;
}
if (failed) {
console.log(
'Failed to automatically add a route to package/app/src/App.tsx',
);
console.log(`Action needed, add the following:`);
console.log(`1. import { ${componentName} } from '${packageName}';`);
console.log(`2. <Route path="${path}" element={${element}} />`);
} else {
await fs.writeFile(appTsxPath, contentsWithImport);
}
}
}
export const appRoute = createStepDefinition<Data>({
type: 'app-route',
deserialize(obj, pkg) {
const { path, element } = obj;
if (!path || typeof path !== 'string') {
throw new Error("Invalid install step, 'path' must be a string");
}
if (!element || typeof element !== 'string') {
throw new Error("Invalid install step, 'element' must be a string");
}
return new AppRouteStep({ path, element, packageName: pkg.name });
},
create(data: Data) {
return new AppRouteStep(data);
},
});
@@ -0,0 +1,82 @@
/*
* Copyright 2021 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 fs from 'fs-extra';
import chalk from 'chalk';
import sortBy from 'lodash/sortBy';
import groupBy from 'lodash/groupBy';
import { paths } from '../../../lib/paths';
import { run } from '../../../lib/run';
import { Step, createStepDefinition } from '../types';
type Data = {
dependencies: Array<{
target: string;
type: 'dependencies';
name: string;
query: string;
}>;
};
class DependenciesStep implements Step {
constructor(private readonly data: Data) {}
async run() {
const { dependencies } = this.data;
// yarn --cwd packages/app add
const byTarget = groupBy(dependencies, 'target');
// Go through each target package and install the dependencies.
for (const [target, deps] of Object.entries(byTarget)) {
const pkgPath = paths.resolveTargetRoot(target, 'package.json');
const pkgJson = await fs.readJson(pkgPath);
// Populate each type of dependency object, dependencies, devDependencies, etc.
const depTypes = new Set<string>();
for (const dep of deps) {
depTypes.add(dep.type);
pkgJson[dep.type][dep.name] = dep.query;
}
// Be nice and sort the dependencies alphabetically
for (const depType of depTypes) {
pkgJson[depType] = Object.fromEntries(
sortBy(Object.entries(pkgJson[depType]), ([key]) => key),
);
}
await fs.writeJson(pkgPath, pkgJson, { spaces: 2 });
}
console.log();
console.log(
`Running ${chalk.blue('yarn install')} to install new versions`,
);
console.log();
await run('yarn', ['install']);
}
}
export const dependencies = createStepDefinition<Data>({
type: 'dependencies',
deserialize() {
throw new Error('The dependency step may not be defined in JSON');
},
create(data: Data) {
return new DependenciesStep(data);
},
});
@@ -14,10 +14,6 @@
* limitations under the License.
*/
export class ContextKey<T> {
constructor(readonly defaultValue: T) {}
}
export interface Context {
getContextValue<T>(key: ContextKey<T>): T;
}
export { appRoute } from './appRoute';
export { dependencies } from './dependencies';
export { message } from './message';
@@ -0,0 +1,48 @@
/*
* Copyright 2021 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 { Step, createStepDefinition } from '../types';
type Data = {
message: string;
};
class MessageStep implements Step {
constructor(private readonly data: Data) {}
async run() {
console.log(this.data.message);
}
}
export const message = createStepDefinition<Data>({
type: 'message',
deserialize(obj) {
const { message: msg } = obj;
if (!msg || (typeof msg !== 'string' && !Array.isArray(msg))) {
throw new Error(
"Invalid install step, 'message' must be a string or array",
);
}
return new MessageStep({ message: [msg].flat().join('') });
},
create(data: Data) {
return new MessageStep(data);
},
});
@@ -0,0 +1,69 @@
/*
* Copyright 2021 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 { YarnInfoInspectData } from '../../lib/versioning';
import { JsonObject } from '@backstage/config';
/**
* TODO: possible types
*
* frontend-deps: Install one or many frontend packages in a Backstage app
* backend-deps: Install one or many backend packages in a Backstage app
* app-config: Update app-config.yaml (and ask for inputs). E.g. Use local or docker for techdocs.builder
* frontend-route: Add a frontend route to the plugin homepage
* backend-route: Add a backend route to the plugin
* entity-page-tab: Add a tab on Catalogs entity page
* sidebar-item: Add a sidebar item
* frontend-api: Add a custom API
*/
/** A serialized install step as it appears in JSON */
export type SerializedStep = {
type: string;
} & unknown;
export type InstallationRecipe = {
type?: 'frontend' | 'backend';
steps: SerializedStep[];
};
/** package.json data */
export type PackageWithInstallRecipe = YarnInfoInspectData & {
version: string;
experimentalInstallationRecipe?: InstallationRecipe;
};
export interface Step {
run(): Promise<void>;
}
export interface StepDefinition<Options> {
/** The string identifying this type of step */
type: string;
/** Deserializes and validate a JSON description of the step data */
deserialize(obj: JsonObject, pkg: PackageWithInstallRecipe): Step;
/** Creates a step using known parameters */
create(options: Options): Step;
}
/** Creates a new step definition. Only used as a helper for type inference */
export function createStepDefinition<T>(
config: StepDefinition<T>,
): StepDefinition<T> {
return config;
}
+1
View File
@@ -16,3 +16,4 @@
export { Lockfile } from './Lockfile';
export { fetchPackageInfo, mapDependencies } from './packages';
export type { YarnInfoInspectData } from './packages';
+1 -1
View File
@@ -27,7 +27,7 @@ const DEP_TYPES = [
];
// Package data as returned by `yarn info`
type YarnInfoInspectData = {
export type YarnInfoInspectData = {
name: string;
'dist-tags': { latest: string };
versions: string[];
@@ -1,2 +1,2 @@
import '@testing-library/jest-dom';
import 'cross-fetch/polyfill'
import 'cross-fetch/polyfill';
@@ -15,7 +15,8 @@
*/
import React, { useMemo, useEffect, useState, PropsWithChildren } from 'react';
import { ThemeProvider, CssBaseline } from '@material-ui/core';
import { ThemeProvider } from '@material-ui/core/styles';
import CssBaseline from '@material-ui/core/CssBaseline';
import { useApi, appThemeApiRef, AppTheme } from '@backstage/core-plugin-api';
import { useObservable } from 'react-use';
+17
View File
@@ -1,8 +1,25 @@
const base = require('@backstage/cli/config/eslint');
const [, baseRestrictedImports] = base.rules['no-restricted-imports'];
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
rules: {
// TODO: add prop types to JS and remove
'react/prop-types': 0,
'jest/expect-expect': 0,
'no-restricted-imports': [
2,
{
...baseRestrictedImports,
paths: [
{
// Importing the entire MUI icons packages kills build performance as the list of icons is huge.
name: '@material-ui/core',
message: "Please import '@material-ui/core/...' instead.",
},
...baseRestrictedImports.paths,
],
},
],
},
};
+62 -86
View File
@@ -8,22 +8,21 @@
import { ApiRef } from '@backstage/core-plugin-api';
import { BackstageIdentityApi } from '@backstage/core-plugin-api';
import { BackstageTheme } from '@backstage/theme';
import { Breadcrumbs as Breadcrumbs_2 } from '@material-ui/core';
import { ButtonProps as ButtonProps_2 } from '@material-ui/core';
import { CardHeaderProps } from '@material-ui/core';
import { ButtonProps as ButtonProps_2 } from '@material-ui/core/Button';
import { CardHeaderProps } from '@material-ui/core/CardHeader';
import { Column } from '@material-table/core';
import { ComponentClass } from 'react';
import { ComponentProps } from 'react';
import { Context } from 'react';
import { default as CSS_2 } from 'csstype';
import { CSSProperties } from 'react';
import { default as dagre_2 } from 'dagre';
import { ElementType } from 'react';
import { ErrorInfo } from 'react';
import { IconComponent } from '@backstage/core-plugin-api';
import { LinearProgressProps } from '@material-ui/core';
import { LinkProps as LinkProps_2 } from '@material-ui/core';
import { LinearProgressProps } from '@material-ui/core/LinearProgress';
import { LinkProps as LinkProps_2 } from '@material-ui/core/Link';
import { LinkProps as LinkProps_3 } from 'react-router-dom';
import MaterialBreadcrumbs from '@material-ui/core/Breadcrumbs';
import { MaterialTableProps } from '@material-table/core';
import { NavLinkProps } from 'react-router-dom';
import { Overrides } from '@material-ui/core/styles/overrides';
@@ -37,14 +36,14 @@ import { SessionApi } from '@backstage/core-plugin-api';
import { SignInPageProps } from '@backstage/core-plugin-api';
import { SparklinesLineProps } from 'react-sparklines';
import { SparklinesProps } from 'react-sparklines';
import { StyledComponentProps } from '@material-ui/core';
import { StyledComponentProps } from '@material-ui/core/styles';
import { StyleRules } from '@material-ui/styles';
import { StyleRules as StyleRules_2 } from '@material-ui/core/styles/withStyles';
import { TabProps } from '@material-ui/core';
import { TabProps } from '@material-ui/core/Tab';
import { TextTruncateProps } from 'react-text-truncate';
import { Theme } from '@material-ui/core';
import { TooltipProps } from '@material-ui/core';
import { WithStyles } from '@material-ui/core';
import { Theme } from '@material-ui/core/styles';
import { TooltipProps } from '@material-ui/core/Tooltip';
import { WithStyles } from '@material-ui/core/styles';
// Warning: (ae-missing-release-tag) "AlertDisplay" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -53,15 +52,11 @@ export function AlertDisplay(_props: {}): JSX.Element | null;
// Warning: (ae-missing-release-tag) "Alignment" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public
enum Alignment {
// (undocumented)
DOWN_LEFT = 'DL',
// (undocumented)
DOWN_RIGHT = 'DR',
// (undocumented)
UP_LEFT = 'UL',
// (undocumented)
UP_RIGHT = 'UR',
}
@@ -264,11 +259,10 @@ export type CustomProviderClassKey = 'form' | 'button';
// @public (undocumented)
export function DashboardIcon(props: IconComponentProps): JSX.Element;
// Warning: (ae-forgotten-export) The symbol "CustomType" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "DependencyEdge" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
type DependencyEdge<T = CustomType> = T & {
// @public
type DependencyEdge<T = {}> = T & {
from: string;
to: string;
label?: string;
@@ -276,8 +270,10 @@ type DependencyEdge<T = CustomType> = T & {
// Warning: (ae-missing-release-tag) "DependencyGraph" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function DependencyGraph(props: DependencyGraphProps): JSX.Element;
// @public
export function DependencyGraph<NodeData, EdgeData>(
props: DependencyGraphProps<NodeData, EdgeData>,
): JSX.Element;
// Warning: (ae-missing-release-tag) "DependencyGraphDefaultLabelClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -301,40 +297,42 @@ export type DependencyGraphNodeClassKey = 'node';
// Warning: (ae-missing-release-tag) "DependencyGraphProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type DependencyGraphProps = React_2.SVGProps<SVGSVGElement> & {
edges: DependencyEdge[];
nodes: DependencyNode[];
direction?: Direction;
// @public
export interface DependencyGraphProps<NodeData, EdgeData>
extends React_2.SVGProps<SVGSVGElement> {
acyclicer?: 'greedy';
// Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver
align?: Alignment;
nodeMargin?: number;
defs?: SVGDefsElement | SVGDefsElement[];
// Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver
direction?: Direction;
edgeMargin?: number;
rankMargin?: number;
edgeRanks?: number;
edges: DependencyEdge<EdgeData>[];
edgeWeight?: number;
labelOffset?: number;
// Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver
labelPosition?: LabelPosition;
nodeMargin?: number;
nodes: DependencyNode<NodeData>[];
paddingX?: number;
paddingY?: number;
acyclicer?: 'greedy';
// Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver
ranker?: Ranker;
labelPosition?: LabelPosition;
labelOffset?: number;
edgeRanks?: number;
edgeWeight?: number;
renderNode?: RenderNodeFunction;
renderLabel?: RenderLabelFunction;
defs?: SVGDefsElement | SVGDefsElement[];
rankMargin?: number;
renderLabel?: RenderLabelFunction<EdgeData>;
renderNode?: RenderNodeFunction<NodeData>;
zoom?: 'enabled' | 'disabled' | 'enable-on-click';
};
}
declare namespace DependencyGraphTypes {
export {
DependencyEdge,
GraphEdge,
RenderLabelProps,
RenderLabelFunction,
DependencyNode,
GraphNode,
RenderNodeProps,
RenderNodeFunction,
EdgeProperties,
Direction,
Alignment,
Ranker,
@@ -345,22 +343,18 @@ export { DependencyGraphTypes };
// Warning: (ae-missing-release-tag) "DependencyNode" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
type DependencyNode<T = CustomType> = T & {
// @public
type DependencyNode<T = {}> = T & {
id: string;
};
// Warning: (ae-missing-release-tag) "Direction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public
enum Direction {
// (undocumented)
BOTTOM_TOP = 'BT',
// (undocumented)
LEFT_RIGHT = 'LR',
// (undocumented)
RIGHT_LEFT = 'RL',
// (undocumented)
TOP_BOTTOM = 'TB',
}
@@ -387,20 +381,6 @@ export type DismissbleBannerClassKey =
// @public (undocumented)
export function DocsIcon(props: IconComponentProps): JSX.Element;
// Warning: (ae-missing-release-tag) "EdgeProperties" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
type EdgeProperties = {
label?: string;
width?: number;
height?: number;
labeloffset?: number;
labelpos?: LabelPosition;
minlen?: number;
weight?: number;
[customKey: string]: any;
};
// Warning: (ae-missing-release-tag) "EmailIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -520,18 +500,6 @@ export type GaugeClassKey = 'root' | 'overlay' | 'circle' | 'colorUnknown';
// @public (undocumented)
export function GitHubIcon(props: IconComponentProps): JSX.Element;
// Warning: (ae-missing-release-tag) "GraphEdge" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
type GraphEdge<T = CustomType> = DependencyEdge<T> &
dagre_2.GraphEdge &
EdgeProperties;
// Warning: (ae-missing-release-tag) "GraphNode" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
type GraphNode<T = CustomType> = dagre_2.Node<DependencyNode<T>>;
// Warning: (ae-missing-release-tag) "GroupIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -738,7 +706,7 @@ export type ItemCardHeaderProps = Partial<WithStyles<typeof styles_2>> & {
// Warning: (ae-missing-release-tag) "LabelPosition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public
enum LabelPosition {
// (undocumented)
CENTER = 'c',
@@ -888,37 +856,43 @@ export function Progress(
// Warning: (ae-missing-release-tag) "Ranker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public
enum Ranker {
// (undocumented)
LONGEST_PATH = 'longest-path',
// (undocumented)
NETWORK_SIMPLEX = 'network-simplex',
// (undocumented)
TIGHT_TREE = 'tight-tree',
}
// Warning: (ae-missing-release-tag) "RenderLabelFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
type RenderLabelFunction = (props: RenderLabelProps<any>) => React.ReactNode;
// @public
type RenderLabelFunction<T = {}> = (
props: RenderLabelProps<T>,
) => React_2.ReactNode;
// Warning: (ae-missing-release-tag) "RenderLabelProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
// Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver
// Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver
//
// @public (undocumented)
type RenderLabelProps<T = CustomType> = {
// @public
type RenderLabelProps<T = unknown> = {
edge: DependencyEdge<T>;
};
// Warning: (ae-missing-release-tag) "RenderNodeFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
// Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver
//
// @public (undocumented)
type RenderNodeFunction = (props: RenderNodeProps<any>) => React.ReactNode;
// @public
type RenderNodeFunction<T = {}> = (
props: RenderNodeProps<T>,
) => React_2.ReactNode;
// Warning: (ae-missing-release-tag) "RenderNodeProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
// Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver
// Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver
//
// @public (undocumented)
type RenderNodeProps<T = CustomType> = {
// @public
type RenderNodeProps<T = unknown> = {
node: DependencyNode<T>;
};
@@ -2498,6 +2472,8 @@ export type WarningPanelClassKey =
// Warnings were encountered during analysis:
//
// src/components/DependencyGraph/types.d.ts:14:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "DependencyNode"
// src/components/DependencyGraph/types.d.ts:18:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "DependencyNode"
// src/components/TabbedLayout/RoutedTabs.d.ts:9:5 - (ae-forgotten-export) The symbol "SubRoute" needs to be exported by the entry point index.d.ts
// src/components/Table/Table.d.ts:19:5 - (ae-forgotten-export) The symbol "SelectedFilters" needs to be exported by the entry point index.d.ts
// src/layout/ErrorBoundary/ErrorBoundary.d.ts:7:5 - (ae-forgotten-export) The symbol "SlackChannel" needs to be exported by the entry point index.d.ts
+1 -1
View File
@@ -37,7 +37,6 @@
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
"@types/dagre": "^0.7.44",
"@types/react": "*",
"@types/react-sparklines": "^1.7.0",
"@types/react-text-truncate": "^0.14.0",
@@ -79,6 +78,7 @@
"@types/d3-selection": "^2.0.0",
"@types/d3-shape": "^3.0.1",
"@types/d3-zoom": "^2.0.0",
"@types/dagre": "^0.7.44",
"@types/google-protobuf": "^3.7.2",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32",
@@ -15,7 +15,8 @@
*/
import React, { useEffect, useState } from 'react';
import { Snackbar, IconButton } from '@material-ui/core';
import Snackbar from '@material-ui/core/Snackbar';
import IconButton from '@material-ui/core/IconButton';
import CloseIcon from '@material-ui/icons/Close';
import { Alert } from '@material-ui/lab';
import { AlertMessage, useApi, alertApiRef } from '@backstage/core-plugin-api';
@@ -14,12 +14,8 @@
* limitations under the License.
*/
import React, { CSSProperties } from 'react';
import {
Avatar as MaterialAvatar,
createStyles,
makeStyles,
Theme,
} from '@material-ui/core';
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles';
import MaterialAvatar from '@material-ui/core/Avatar';
import { extractInitials, stringToColor } from './utils';
export type AvatarClassKey = 'avatar';
@@ -28,5 +28,5 @@ export function stringToColor(str: string) {
}
export function extractInitials(value: string) {
return value.match(/\b\w/g)!.join('').substring(0, 2);
return value.match(/\b\w/g)?.join('').substring(0, 2);
}
@@ -17,15 +17,13 @@ import React, { ComponentType } from 'react';
import { Button } from './Button';
import { useLocation } from 'react-router-dom';
import { createRouteRef, useRouteRef } from '@backstage/core-plugin-api';
import {
Divider,
Link,
List,
ListItem,
ListItemText,
Typography,
Button as MaterialButton,
} from '@material-ui/core';
import Divider from '@material-ui/core/Divider';
import Link from '@material-ui/core/Link';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import Typography from '@material-ui/core/Typography';
import MaterialButton from '@material-ui/core/Button';
import { wrapInTestApp } from '@backstage/test-utils';
const routeRef = createRouteRef({
@@ -14,10 +14,9 @@
* limitations under the License.
*/
import {
Button as MaterialButton,
import MaterialButton, {
ButtonProps as MaterialButtonProps,
} from '@material-ui/core';
} from '@material-ui/core/Button';
import React from 'react';
import { Link, LinkProps } from '../Link';
@@ -43,7 +42,9 @@ declare function ButtonType(props: ButtonProps): JSX.Element;
/**
* This wrapper is here to reset the color of the Link and make typescript happy.
*/
const LinkWrapper = (props: LinkProps) => <Link {...props} color="initial" />;
const LinkWrapper = React.forwardRef<any, LinkProps>((props, ref) => (
<Link ref={ref} {...props} color="initial" />
));
const ActualButton = React.forwardRef<any, ButtonProps>((props, ref) => (
<MaterialButton ref={ref} component={LinkWrapper} {...props} />
@@ -15,7 +15,7 @@
*/
import React from 'react';
import { Chip } from '@material-ui/core';
import Chip from '@material-ui/core/Chip';
export default {
title: 'Data Display/Chip',
@@ -14,12 +14,56 @@
* limitations under the License.
*/
import React from 'react';
import SyntaxHighlighter from 'react-syntax-highlighter';
import { docco, dark } from 'react-syntax-highlighter/dist/cjs/styles/hljs';
import { useTheme } from '@material-ui/core';
import React, { lazy, Suspense } from 'react';
import { useTheme } from '@material-ui/core/styles';
import { BackstageTheme } from '@backstage/theme';
import { CopyTextButton } from '../CopyTextButton';
import { Progress } from '../Progress';
const LazySyntaxHighlighter = lazy(async () => {
const [{ default: SyntaxHighlighter }, { docco, dark }] = await Promise.all([
import('react-syntax-highlighter'),
import('react-syntax-highlighter/dist/cjs/styles/hljs'),
]);
function LazyHighlighter(props: CodeSnippetProps) {
const {
text,
language,
showLineNumbers = false,
highlightedNumbers,
customStyle,
} = props;
const theme = useTheme<BackstageTheme>();
const mode = theme.palette.type === 'dark' ? dark : docco;
const highlightColor =
theme.palette.type === 'dark' ? '#256bf3' : '#e6ffed';
return (
<SyntaxHighlighter
customStyle={customStyle}
language={language}
style={mode}
showLineNumbers={showLineNumbers}
wrapLines
lineNumberStyle={{ color: theme.palette.textVerySubtle }}
lineProps={(lineNumber: number) =>
highlightedNumbers?.includes(lineNumber)
? {
style: {
backgroundColor: highlightColor,
},
}
: {}
}
>
{text}
</SyntaxHighlighter>
);
}
return { default: LazyHighlighter };
});
/**
* Properties for {@link CodeSnippet}
@@ -68,38 +112,12 @@ export interface CodeSnippetProps {
* providing consistent theming and copy code button
*/
export function CodeSnippet(props: CodeSnippetProps) {
const {
text,
language,
showLineNumbers = false,
showCopyCodeButton = false,
highlightedNumbers,
customStyle,
} = props;
const theme = useTheme<BackstageTheme>();
const mode = theme.palette.type === 'dark' ? dark : docco;
const highlightColor = theme.palette.type === 'dark' ? '#256bf3' : '#e6ffed';
const { text, showCopyCodeButton = false } = props;
return (
<div style={{ position: 'relative' }}>
<SyntaxHighlighter
customStyle={customStyle}
language={language}
style={mode}
showLineNumbers={showLineNumbers}
wrapLines
lineNumberStyle={{ color: theme.palette.textVerySubtle }}
lineProps={(lineNumber: number) =>
highlightedNumbers?.includes(lineNumber)
? {
style: {
backgroundColor: highlightColor,
},
}
: {}
}
>
{text}
</SyntaxHighlighter>
<Suspense fallback={<Progress />}>
<LazySyntaxHighlighter {...props} />
</Suspense>
{showCopyCodeButton && (
<div style={{ position: 'absolute', top: 0, right: 0 }}>
<CopyTextButton text={text} />
@@ -15,7 +15,8 @@
*/
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
import { IconButton, Tooltip } from '@material-ui/core';
import IconButton from '@material-ui/core/IconButton';
import Tooltip from '@material-ui/core/Tooltip';
import CopyIcon from '@material-ui/icons/FileCopy';
import React, { MouseEventHandler, useEffect, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
@@ -15,7 +15,9 @@
*/
import { BackstageTheme } from '@backstage/theme';
import { Button, IconButton, useMediaQuery } from '@material-ui/core';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import React from 'react';
import { Link as RouterLink, LinkProps } from 'react-router-dom';
import AddCircleOutline from '@material-ui/icons/AddCircleOutline';
@@ -29,39 +29,148 @@ import {
Ranker,
RenderNodeFunction,
RenderLabelFunction,
GraphEdge,
GraphNode,
LabelPosition,
} from './types';
import { Node } from './Node';
import { Edge } from './Edge';
import { Edge, GraphEdge } from './Edge';
import { ARROW_MARKER_ID } from './constants';
export type DependencyGraphProps = React.SVGProps<SVGSVGElement> & {
edges: DependencyEdge[];
nodes: DependencyNode[];
/**
* Properties of {@link DependencyGraph}
*
* @remarks
* <NodeData> and <EdgeData> are useful when rendering custom or edge labels
*/
export interface DependencyGraphProps<NodeData, EdgeData>
extends React.SVGProps<SVGSVGElement> {
/**
* Edges of graph
*/
edges: DependencyEdge<EdgeData>[];
/**
* Nodes of Graph
*/
nodes: DependencyNode<NodeData>[];
/**
* Graph {@link DependencyGraphTypes.Direction | direction}
*
* @remarks
*
* Default: `DependencyGraphTypes.Direction.TOP_BOTTOM`
*/
direction?: Direction;
/**
* Node {@link DependencyGraphTypes.Alignment | alignment}
*/
align?: Alignment;
/**
* Margin between nodes on each rank
*
* @remarks
*
* Default: 50
*/
nodeMargin?: number;
/**
* Margin between edges
*
* @remarks
*
* Default: 10
*/
edgeMargin?: number;
/**
* Margin between each rank
*
* @remarks
*
* Default: 50
*/
rankMargin?: number;
/**
* Margin on left and right of whole graph
*
* @remarks
*
* Default: 0
*/
paddingX?: number;
/**
* Margin on top and bottom of whole graph
*
* @remarks
*
* Default: 0
*/
paddingY?: number;
/**
* Heuristic used to find set of edges that will make graph acyclic
*/
acyclicer?: 'greedy';
/**
* {@link DependencyGraphTypes.Ranker | Algorithm} used to rank nodes
*
* @remarks
*
* Default: `DependencyGraphTypes.Ranker.NETWORK_SIMPLEX`
*/
ranker?: Ranker;
/**
* {@link DependencyGraphTypes.LabelPosition | Position} of label in relation to edge
*
* @remarks
*
* Default: `DependencyGraphTypes.LabelPosition.RIGHT`
*/
labelPosition?: LabelPosition;
/**
* How much to move label away from edge
*
* @remarks
*
* Applies only when {@link DependencyGraphProps.labelPosition} is `DependencyGraphTypes.LabelPosition.LEFT` or
* `DependencyGraphTypes.LabelPosition.RIGHT`
*/
labelOffset?: number;
/**
* Minimum number of ranks to keep between connected nodes
*/
edgeRanks?: number;
/**
* Weight applied to edges in graph
*/
edgeWeight?: number;
renderNode?: RenderNodeFunction;
renderLabel?: RenderLabelFunction;
/**
* Custom node rendering component
*/
renderNode?: RenderNodeFunction<NodeData>;
/**
* Custom label rendering component
*/
renderLabel?: RenderLabelFunction<EdgeData>;
/**
* {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs | Defs} shared by rendered SVG to be used by
* {@link DependencyGraphProps.renderNode} and/or {@link DependencyGraphProps.renderLabel}
*/
defs?: SVGDefsElement | SVGDefsElement[];
/**
* Controls zoom behavior of graph
*
* @remarks
*
* Default: `enabled`
*/
zoom?: 'enabled' | 'disabled' | 'enable-on-click';
};
}
const WORKSPACE_ID = 'workspace';
export function DependencyGraph(props: DependencyGraphProps) {
/**
* Graph component used to visualize relations between entities
*/
export function DependencyGraph<NodeData, EdgeData>(
props: DependencyGraphProps<NodeData, EdgeData>,
) {
const {
edges,
nodes,
@@ -88,7 +197,7 @@ export function DependencyGraph(props: DependencyGraphProps) {
const [containerWidth, setContainerWidth] = React.useState<number>(100);
const [containerHeight, setContainerHeight] = React.useState<number>(100);
const graph = React.useRef<dagre.graphlib.Graph<{}>>(
const graph = React.useRef<dagre.graphlib.Graph<DependencyNode<NodeData>>>(
new dagre.graphlib.Graph(),
);
const [graphWidth, setGraphWidth] = React.useState<number>(
@@ -256,13 +365,13 @@ export function DependencyGraph(props: DependencyGraphProps) {
updateGraph,
]);
function setNode(id: string, node: DependencyNode) {
function setNode(id: string, node: DependencyNode<NodeData>) {
graph.current.setNode(id, node);
updateGraph();
return graph.current;
}
function setEdge(id: dagre.Edge, edge: DependencyEdge) {
function setEdge(id: dagre.Edge, edge: DependencyEdge<EdgeData>) {
graph.current.setEdge(id, edge);
updateGraph();
return graph.current;
@@ -303,7 +412,7 @@ export function DependencyGraph(props: DependencyGraphProps) {
viewBox={`0 0 ${graphWidth} ${graphHeight}`}
>
{graphEdges.map(e => {
const edge = graph.current.edge(e) as GraphEdge;
const edge = graph.current.edge(e) as GraphEdge<EdgeData>;
if (!edge) return null;
return (
<Edge
@@ -316,7 +425,7 @@ export function DependencyGraph(props: DependencyGraphProps) {
);
})}
{graphNodes.map((id: string) => {
const node = graph.current.node(id) as GraphNode;
const node = graph.current.node(id);
if (!node) return null;
return (
<Node
@@ -23,6 +23,10 @@ const fromNode = 'node';
const toNode = 'other-node';
const edge = {
points: [
{ x: 10, y: 20 },
{ x: 20, y: 20 },
],
from: fromNode,
to: toNode,
};
@@ -38,10 +42,6 @@ const renderElement = jest.fn((props: RenderLabelProps) => (
));
const minProps = {
points: [
{ x: 10, y: 20 },
{ x: 20, y: 20 },
],
id,
setEdge,
renderElement,
@@ -20,13 +20,26 @@ import isFinite from 'lodash/isFinite';
import makeStyles from '@material-ui/core/styles/makeStyles';
import { BackstageTheme } from '@backstage/theme';
import {
GraphEdge,
RenderLabelProps,
RenderLabelFunction,
DependencyEdge,
LabelPosition,
} from './types';
import { ARROW_MARKER_ID, EDGE_TEST_ID, LABEL_TEST_ID } from './constants';
import { DefaultLabel } from './DefaultLabel';
import dagre from 'dagre';
/* Based on: https://github.com/dagrejs/dagre/wiki#configuring-the-layout */
export type EdgeProperties = {
label?: string;
width?: number;
height?: number;
labeloffset?: number;
labelpos?: LabelPosition;
minlen?: number;
weight?: number;
};
export type GraphEdge<T> = DependencyEdge<T> & dagre.GraphEdge & EdgeProperties;
export type DependencyGraphEdgeClassKey = 'path' | 'label';
@@ -47,14 +60,19 @@ const useStyles = makeStyles(
type EdgePoint = dagre.GraphEdge['points'][0];
export type EdgeComponentProps<T = any> = {
export type EdgeComponentProps<T = unknown> = {
id: dagre.Edge;
edge: GraphEdge<T>;
render?: RenderLabelFunction;
setEdge: (id: dagre.Edge, edge: DependencyEdge) => dagre.graphlib.Graph<{}>;
render?: RenderLabelFunction<T>;
setEdge: (
id: dagre.Edge,
edge: DependencyEdge<T>,
) => dagre.graphlib.Graph<{}>;
};
const renderDefault = (props: RenderLabelProps) => <DefaultLabel {...props} />;
const renderDefault = (props: RenderLabelProps<unknown>) => (
<DefaultLabel {...props} />
);
const createPath = d3Shape
.line<EdgePoint>()
@@ -62,13 +80,14 @@ const createPath = d3Shape
.y(d => d.y)
.curve(d3Shape.curveMonotoneX);
export function Edge({
export function Edge<EdgeData>({
render = renderDefault,
setEdge,
id,
edge,
}: EdgeComponentProps) {
const { x = 0, y = 0, width, height, points, ...labelProps } = edge;
}: EdgeComponentProps<EdgeData>) {
const { x = 0, y = 0, width, height, points } = edge;
const labelProps: DependencyEdge<EdgeData> = edge;
const classes = useStyles();
const labelRef = React.useRef<SVGGElement>(null);
@@ -20,21 +20,16 @@ import { render } from '@testing-library/react';
import { Node } from './Node';
import { RenderNodeProps } from './types';
const node = { id: 'abc' };
const node = { id: 'abc', x: 0, y: 0, width: 0, height: 0 };
const setNode = jest.fn(() => new dagre.graphlib.Graph());
const renderElement = jest.fn((props: RenderNodeProps) => (
<text>{props.node.id}</text>
));
const minProps = {
id: node.id,
node,
setNode,
render: renderElement,
x: 0,
y: 0,
width: 0,
height: 0,
};
describe('<Node />', () => {
@@ -50,7 +45,7 @@ describe('<Node />', () => {
it('renders the supplied element', () => {
const { getByText } = render(<Node {...minProps} />);
expect(getByText(minProps.id)).toBeInTheDocument();
expect(getByText(minProps.node.id)).toBeInTheDocument();
});
it('passes down node properties to the render method', () => {
@@ -62,13 +57,13 @@ describe('<Node />', () => {
it('calls setNode with node ID and actual size after rendering', () => {
const { getByText } = render(<Node {...minProps} />);
expect(getByText(minProps.id)).toBeInTheDocument();
expect(getByText(minProps.node.id)).toBeInTheDocument();
// Updates the node in the graph
expect(setNode).toHaveBeenCalledWith(node.id, {
...node,
height: 100,
width: 100,
...node,
});
// Does not pass down width/height to node
@@ -17,8 +17,9 @@
import React from 'react';
import makeStyles from '@material-ui/core/styles/makeStyles';
import { DefaultNode } from './DefaultNode';
import { RenderNodeFunction, RenderNodeProps, GraphNode } from './types';
import { RenderNodeFunction, RenderNodeProps, DependencyNode } from './types';
import { NODE_TEST_ID } from './constants';
import dagre from 'dagre';
export type DependencyGraphNodeClassKey = 'node';
@@ -31,20 +32,23 @@ const useStyles = makeStyles(
{ name: 'BackstageDependencyGraphNode' },
);
export type NodeComponentProps<T = any> = {
export type GraphNode<T> = dagre.Node<DependencyNode<T>>;
export type NodeComponentProps<T> = {
node: GraphNode<T>;
render?: RenderNodeFunction;
render?: RenderNodeFunction<T>;
setNode: dagre.graphlib.Graph['setNode'];
};
const renderDefault = (props: RenderNodeProps) => <DefaultNode {...props} />;
export function Node({
export function Node<T>({
render = renderDefault,
setNode,
node,
}: NodeComponentProps) {
const { width, height, x = 0, y = 0, ...nodeProps } = node;
}: NodeComponentProps<T>) {
const { width, height, x = 0, y = 0 } = node;
const nodeProps: DependencyNode<T> = node;
const classes = useStyles();
const nodeRef = React.useRef<SVGGElement | null>(null);
@@ -14,73 +14,132 @@
* limitations under the License.
*/
import dagre from 'dagre';
/**
* Types used to customize and provide data to {@link DependencyGraph}
*
* @packageDocumentation
*/
type CustomType = { [customKey: string]: any };
import React from 'react';
/* Edges */
export type DependencyEdge<T = CustomType> = T & {
/**
* Edge of {@link DependencyGraph}
*/
export type DependencyEdge<T = {}> = T & {
/**
* ID of {@link DependencyNode} from where the Edge start
*/
from: string;
/**
* ID of {@link DependencyNode} to where the Edge goes to
*/
to: string;
/**
* Label assigned and rendered with the Edge
*/
label?: string;
};
export type GraphEdge<T = CustomType> = DependencyEdge<T> &
dagre.GraphEdge &
EdgeProperties;
/**
* Properties of {@link DependencyGraphTypes.RenderLabelFunction} for {@link DependencyGraphTypes.DependencyEdge}
*/
export type RenderLabelProps<T = unknown> = { edge: DependencyEdge<T> };
export type RenderLabelProps<T = CustomType> = { edge: DependencyEdge<T> };
export type RenderLabelFunction = (
props: RenderLabelProps<any>,
/**
* Custom React component for edge labels
*/
export type RenderLabelFunction<T = {}> = (
props: RenderLabelProps<T>,
) => React.ReactNode;
/* Nodes */
export type DependencyNode<T = CustomType> = T & {
/**
* Node of {@link DependencyGraph}
*/
export type DependencyNode<T = {}> = T & {
id: string;
};
export type GraphNode<T = CustomType> = dagre.Node<DependencyNode<T>>;
/**
* Properties of {@link DependencyGraphTypes.RenderNodeFunction} for {@link DependencyGraphTypes.DependencyNode}
*/
export type RenderNodeProps<T = unknown> = { node: DependencyNode<T> };
export type RenderNodeProps<T = CustomType> = { node: DependencyNode<T> };
export type RenderNodeFunction = (
props: RenderNodeProps<any>,
/**
* Custom React component for graph {@link DependencyGraphTypes.DependencyNode}
*/
export type RenderNodeFunction<T = {}> = (
props: RenderNodeProps<T>,
) => React.ReactNode;
/* Based on: https://github.com/dagrejs/dagre/wiki#configuring-the-layout */
export type EdgeProperties = {
label?: string;
width?: number;
height?: number;
labeloffset?: number;
labelpos?: LabelPosition;
minlen?: number;
weight?: number;
[customKey: string]: any;
};
/**
* Graph direction
*/
export enum Direction {
/**
* Top to Bottom
*/
TOP_BOTTOM = 'TB',
/**
* Bottom to Top
*/
BOTTOM_TOP = 'BT',
/**
* Left to Right
*/
LEFT_RIGHT = 'LR',
/**
* Right to Left
*/
RIGHT_LEFT = 'RL',
}
/**
* Node alignment
*/
export enum Alignment {
/**
* Up Left
*/
UP_LEFT = 'UL',
/**
* Up Right
*/
UP_RIGHT = 'UR',
/**
* Down Left
*/
DOWN_LEFT = 'DL',
/**
* Down Right
*/
DOWN_RIGHT = 'DR',
}
/**
* Algorithm used to rand nodes in graph
*/
export enum Ranker {
/**
* {@link https://en.wikipedia.org/wiki/Network_simplex_algorithm | Network Simplex} algorithm
*/
NETWORK_SIMPLEX = 'network-simplex',
/**
* Tight Tree algorithm
*/
TIGHT_TREE = 'tight-tree',
/**
* Longest path algorithm
*
* @remarks
*
* Simplest and fastest
*/
LONGEST_PATH = 'longest-path',
}
/**
* Position of label in relation to the edge
*/
export enum LabelPosition {
LEFT = 'l',
RIGHT = 'r',
@@ -14,15 +14,13 @@
* limitations under the License.
*/
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
Typography,
} from '@material-ui/core';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import IconButton from '@material-ui/core/IconButton';
import Typography from '@material-ui/core/Typography';
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';
import CloseIcon from '@material-ui/icons/Close';
import React, { useState } from 'react';
@@ -16,7 +16,8 @@
import React from 'react';
import { DismissableBanner } from './DismissableBanner';
import { Link, Typography } from '@material-ui/core';
import Link from '@material-ui/core/Link';
import Typography from '@material-ui/core/Typography';
import { ApiProvider, ApiRegistry, WebStorage } from '@backstage/core-app-api';
import {
ErrorApi,
@@ -18,7 +18,7 @@ import React, { ReactNode, useState, useEffect } from 'react';
import { useApi, storageApiRef } from '@backstage/core-plugin-api';
import { useObservable } from 'react-use';
import classNames from 'classnames';
import { makeStyles } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import { BackstageTheme } from '@backstage/theme';
import Snackbar from '@material-ui/core/Snackbar';
import SnackbarContent from '@material-ui/core/SnackbarContent';
@@ -15,15 +15,11 @@
*/
import React, { useState } from 'react';
import {
Drawer,
Button,
Typography,
makeStyles,
IconButton,
createStyles,
Theme,
} from '@material-ui/core';
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';
import Drawer from '@material-ui/core/Drawer';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
import IconButton from '@material-ui/core/IconButton';
import Close from '@material-ui/icons/Close';
export default {
@@ -16,7 +16,7 @@
import React from 'react';
import { EmptyState } from './EmptyState';
import { Button } from '@material-ui/core';
import Button from '@material-ui/core/Button';
import { MissingAnnotationEmptyState } from './MissingAnnotationEmptyState';
export default {
@@ -17,7 +17,7 @@
import React from 'react';
import { EmptyState } from './EmptyState';
import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils';
import { Button } from '@material-ui/core';
import Button from '@material-ui/core/Button';
describe('<EmptyState />', () => {
it('render EmptyState component with type annotaion is missing', async () => {
@@ -15,7 +15,9 @@
*/
import React from 'react';
import { makeStyles, Typography, Grid } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import Grid from '@material-ui/core/Grid';
import { EmptyStateImage } from './EmptyStateImage';
export type EmptyStateClassKey = 'root' | 'action' | 'imageContainer';
@@ -19,7 +19,7 @@ import missingAnnotation from './assets/missingAnnotation.svg';
import noInformation from './assets/noInformation.svg';
import createComponent from './assets/createComponent.svg';
import noBuild from './assets/noBuild.svg';
import { makeStyles } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
type Props = {
missing: 'field' | 'info' | 'content' | 'data';
@@ -15,7 +15,9 @@
*/
import React from 'react';
import { Button, makeStyles, Typography } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
import { BackstageTheme } from '@backstage/theme';
import { Link } from '../Link';
import { EmptyState } from './EmptyState';
@@ -14,7 +14,10 @@
* limitations under the License.
*/
import { List, ListItem, ListItemText, makeStyles } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import React, { PropsWithChildren } from 'react';
import { CopyTextButton } from '../CopyTextButton';
import { WarningPanel } from '../WarningPanel';
@@ -14,7 +14,9 @@
* limitations under the License.
*/
import { ClickAwayListener, makeStyles, Typography } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
import Typography from '@material-ui/core/Typography';
import React, {
PropsWithChildren,
useCallback,
@@ -15,7 +15,7 @@
*/
import React from 'react';
import { IconLinkVertical, IconLinkVerticalProps } from './IconLinkVertical';
import { makeStyles } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
export type HeaderIconLinkRowClassKey = 'links';
@@ -15,7 +15,8 @@
*/
import React from 'react';
import classnames from 'classnames';
import { makeStyles, Link } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import Link from '@material-ui/core/Link';
import LinkIcon from '@material-ui/icons/Link';
import { Link as RouterLink } from '../Link';
@@ -18,7 +18,7 @@ import React from 'react';
import { fireEvent } from '@testing-library/react';
import { renderInTestApp } from '@backstage/test-utils';
import { HorizontalScrollGrid } from './HorizontalScrollGrid';
import { Grid } from '@material-ui/core';
import Grid from '@material-ui/core/Grid';
describe('<HorizontalScrollGrid />', () => {
beforeEach(() => {

Some files were not shown because too many files have changed in this diff Show More