Merge branch 'master' of https://github.com/backstage/backstage into feat/reference-external-docs

This commit is contained in:
jrwpatterson
2023-08-28 08:00:59 +10:00
291 changed files with 4458 additions and 789 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-playlist': patch
---
change color of delete icon to secondary in playlist table
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Changed the processing loop task pipeline implementation from recursive to iterative
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-tech-radar': patch
---
Fixed the width of the tech radar dialog so that the content fits
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Update OpenAPI schema to relax the encoding validation of all request parameters.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': patch
---
Added delete alert popup when user delete the entity
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-org': patch
---
Ensure direct relations are shown for User entities while keeping support for aggregating closest parent group ownership
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-home': patch
---
Align zod dependency to align with other packages.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-search-react': patch
---
Optionally initializes the search context with default settings for search queries only when the config is defined, rather than always overriding it.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes-backend': patch
---
fix "undefined" kind for custom resources
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes': patch
---
fix logs dialog min height
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Fixed validation of the `fullTextFilterFields` query parameter.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Replace `passport-saml` with `@node-saml/passport-saml` (since 4.0.0).
@@ -0,0 +1,5 @@
---
'@backstage/repo-tools': patch
---
Fixed a bug with the `generate-catalog-info` command that could cause `metadata.description` values to be overwritten by `package.json` description values only because unrelated attributes were being changed.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs-module-addons-contrib': minor
---
Enable zoom icon for techdocs images inside lightbox
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Add examples for `github:actions:dispatch` scaffolder actions.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Fix issue with `catalogFileName` not being a required property for `/analyze-location`
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-techdocs-backend': minor
'@backstage/plugin-techdocs-node': minor
'@techdocs/cli': minor
---
Add optional config and cli option for techdocs to specify default mkdocs plugins.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-home': patch
---
Allow specifying static widgets to custom home page
+2 -2
View File
@@ -58,8 +58,8 @@ yarn.lock @backstage/maintainers @backst
/plugins/home-* @backstage/discoverability-maintainers
/plugins/kafka @backstage/maintainers @backstage/reviewers @andrewthauer
/plugins/kafka-backend @backstage/maintainers @backstage/reviewers @andrewthauer
/plugins/kubernetes @backstage/maintainers @backstage/reviewers @backstage/kubernetes-maintainers
/plugins/kubernetes-* @backstage/maintainers @backstage/reviewers @backstage/kubernetes-maintainers
/plugins/kubernetes @backstage/kubernetes-maintainers
/plugins/kubernetes-* @backstage/kubernetes-maintainers
/plugins/linguist @backstage/maintainers @backstage/reviewers @awanlin
/plugins/linguist-backend @backstage/maintainers @backstage/reviewers @awanlin
/plugins/linguist-common @backstage/maintainers @backstage/reviewers @awanlin
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [16.x]
node-version: [18.x]
steps:
- name: checkout
+2
View File
@@ -94,11 +94,13 @@ Scope: The TechDocs plugin and related tooling
| ------------------------ | ------------ | ------- | ------------------------------------------------- | ------------------- |
| Alex Lorenzi | Spotify | ProTean | [AlexLorenzi](https://github.com/AlexLorenzi) | `alexlorenzi` |
| Bond Yan | Spotify | ProTean | [byan1197](https://github.com/byan1197) | `bondify` |
| Daniel Lochrie | Spotify | ProTean | [dlochrie](https://github.com/dlochrie) | - |
| Jackson Chen | Spotify | ProTean | [PeaWarrior](https://github.com/PeaWarrior) | `jacksonc#3322` |
| John Philip | Spotify | ProTean | [johnphilip283](https://github.com/johnphilip283) | `john_philip#2399` |
| Morgan Bentell | Spotify | ProTean | [agentbellnorm](http://github.com/agentbellnorm) | `morganbentell` |
| Raghunandan Balachandran | Spotify | BUX | [soapraj](http://github.com/soapraj) | `raghunandanb#1114` |
| Sydney Achinger | Spotify | ProTean | [squid-ney](https://github.com/squid-ney) | - |
| Vojta Burian | Spotify | ProTean | [sankaritan](https://github.com/sankaritan) | `vojtabu` |
## Incubating Project Areas
+1 -1
View File
@@ -6,7 +6,7 @@
"features": {
"ghcr.io/devcontainers/features/common-utils:1": {},
"ghcr.io/devcontainers/features/docker-from-docker:1": {},
"ghcr.io/devcontainers-contrib/features/mkdocs:1": {}
"ghcr.io/devcontainers-contrib/features/mkdocs:2": {}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
+4 -1
View File
@@ -1,3 +1,6 @@
#!/bin/bash
yarn install
pip install mkdocs-techdocs-core
export VIRTUAL_ENV=$HOME/venv
python3 -m venv $VIRTUAL_ENV
export PATH="$VIRTUAL_ENV/bin:$PATH"
python3 -m pip install mkdocs-techdocs-core
@@ -40,7 +40,7 @@ export const fooServiceRef = createServiceRef<FooService>({
The `fooServiceRef` that we create above should be exported, and can then be used to declare a dependency on the `FooService` interface and receive an implementation of it at runtime.
When creating a service reference you need to give it an ID. This ID needs to be globally unique, and should generally be of the format `'<pluginId>.<serviceName>'`. For more naming patters surrounding services, see the [naming patterns](./07-naming-patterns.md#services) page.
When creating a service reference you need to give it an ID. This ID needs to be globally unique, and should generally be of the format `'<pluginId>.<serviceName>'`. For more naming patterns surrounding services, see the [naming patterns](./07-naming-patterns.md#services) page.
A note on naming: the frontend and backend systems intentionally use the separate names "APIs" and "Services" for concepts that are quite similar. This is to avoid confusion between the two, both in documentation and discussion, but also in code. While the two systems are quite similar, they are not identical, and they can't be used interchangeably.
@@ -123,3 +123,14 @@ source that should be mirrored into Backstage. To make Backstage a mirror of
this remote source, users cannot also register new entities with e.g. the
[catalog-import](https://github.com/backstage/backstage/tree/master/plugins/catalog-import)
plugin.
## Clean up orphaned entities
In short entities can become orphaned through multiple means, such as when a catalog-info YAML file is moved from one place to another in the version control system without updating the registration in the catalog. For safety reasons the default behavior is to just tag the orphaned entities, and keep them around. You can read more about orphaned entities [here](life-of-an-entity.md#orphaning).
However, if you do with to automatically remove the orphaned entities, you can use the following configuration, and everything with an orphaned entity tag will be eventually deleted.
```
catalog:
orphanStrategy: delete
```
@@ -222,7 +222,8 @@ either, it becomes _orphaned_. The end result is as follows:
- The stitching process injects a `backstage.io/orphan: 'true'` annotation on
the child entity.
- The child entity is _not_ removed from the catalog, but stays around until
explicitly deleted via the catalog API, or "reclaimed" by the original parent
explicitly deleted via the catalog API, implicitly if `orphanStrategy: delete`
configuration is set, or until it is "reclaimed" by the original parent
or another parent starting to reference it.
- The catalog page in Backstage for the child entity detects the new annotation
and informs users about the orphan status.
@@ -259,9 +260,13 @@ entities without explicit owner consent. The catalog therefore takes the stance
that entities that often were added by direct user action should also be deleted
only by direct user action.
It is possible to use the catalog API to build automated "reaper" systems that
finally delete entities that are orphaned. This is however not something that's
provided out of the box.
However, if you want to delete orphaned entities automatically anyway, you can
enable the automated clean up with the following app-config option.
```
catalog:
orphanStrategy: delete
```
## Implicit Deletion
+6 -6
View File
@@ -122,12 +122,12 @@ page header, TechDocs Addons whose location is `Header` will not be rendered.
Addons can, in principle, be provided by any plugin! To make it easier to
discover available Addons, we've compiled a list of them here:
| Addon | Package/Plugin | Description |
| ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`<ExpandableNavigation />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.expandablenavigation) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to expand or collapse the entire TechDocs main navigation, and keeps the user's preferred state between documentation sites. |
| [`<ReportIssue />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.reportissue) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to select a portion of text on a TechDocs page and open an issue against the repository that contains the documentation, populating the issue description with the selected text according to a configurable template. |
| [`<TextSize />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.textsize) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to customize text size on documentation pages, they can select how much they want to increase or decrease the font size via slider or buttons. The default value for font size is 100% and this setting is kept in the browser's local storage whenever it is changed. |
| [`<LightBox />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.lightbox) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to open images in a light-box on documentation pages, they can navigate between images if there are several on one page. The image size of the light-box image is the same as the image size on the document page. |
| Addon | Package/Plugin | Description |
| ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`<ExpandableNavigation />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.expandablenavigation) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to expand or collapse the entire TechDocs main navigation, and keeps the user's preferred state between documentation sites. |
| [`<ReportIssue />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.reportissue) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to select a portion of text on a TechDocs page and open an issue against the repository that contains the documentation, populating the issue description with the selected text according to a configurable template. |
| [`<TextSize />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.textsize) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to customize text size on documentation pages, they can select how much they want to increase or decrease the font size via slider or buttons. The default value for font size is 100% and this setting is kept in the browser's local storage whenever it is changed. |
| [`<LightBox />`](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.lightbox) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to open images in a light-box on documentation pages, they can navigate between images if there are several on one page. The image size of the light-box image is the same as the image size on the document page. When clicking on the zoom icon it zooms the image to fit in the screen (similar to `background-size: contain`). |
Got an Addon to contribute? Feel free to add a row above!
+1
View File
@@ -141,6 +141,7 @@ Options:
if not found.
--etag <ETAG> A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored
in techdocs_metadata.json.
--defaultPlugin <PLUGIN_NAME> Plugins which should be added automatically to the mkdocs.yaml file. (default: [])
--omitTechdocsCoreMkdocsPlugin An option to disable automatic addition of techdocs-core plugin to the mkdocs.yaml files.
Defaults to false, which means that the techdocs-core plugin is always added to the mkdocs file.
--legacyCopyReadmeMdToIndexMd Attempt to ensure an index.md exists falling back to using <docs-dir>/README.md or README.md
+9
View File
@@ -49,6 +49,15 @@ techdocs:
# will be broken in these scenarios.
legacyCopyReadmeMdToIndexMd: false
# (Optional) Configures the default plugins which should be added
# automatically to every mkdocs.yaml file. This simplifies the usage as
# e.g. styling plugins can be added once for all.
# Make sure that the defined plugins are installed locally / in the Docker
# image.
# By default, only the techdocs-core plugin will be added (except if
# omitTechdocsCorePlugin: true).
defaultPlugins: ['techdocs-core']
# techdocs.builder can be either 'local' or 'external'.
# Using the default build strategy, if builder is set to 'local' and you open a TechDocs page,
# techdocs-backend will try to generate the docs, publish to storage and show the generated docs afterwards.
+28
View File
@@ -13,6 +13,34 @@ npm.
Releases are handled by changesets and trigger whenever the "Version Packages"
PR is merged. This is typically done every Tuesday around noon CET.
## Next Line Release Process
- PR Checks: Ensure there are no outstanding PRs pending to be merged for this version. If there are any, reach out to maintainers and relevant owners of the affected code reminding them of the deadline for the release.
- [optional] Lock main branch
- Lock the main branch to prevent any new merges.
- Note: Admin rights are required to lock the branch. If you lack the necessary permissions, contact a core maintainer to perform this action on your behalf.
- Check [`Version Packages (next)` Pull Request](https://github.com/backstage/backstage/pulls?q=is%3Aopen+is%3Apr+in%3Atitle+%22Version+Packages+%28next%29%22)
- Verify the version we are shipping is correct, by looking at the version packages PR title. It should be "Version Packages (next)"
- Check [`.changeset/pre.json`](https://github.com/backstage/backstage/blob/master/.changeset/pre.json) if the `mode` is set to `pre`. If you encounter `mode: "exit"` or if it's not defined, it indicates a mainline release.
- Verify that there are no active/unfinished `sync_version-packages` actions running (https://github.com/backstage/backstage/actions/workflows/sync_version-packages.yml)
- Locking the main branch will prevent new ones to be created, but be sure to check for running actions again after unlocking, since it may cause pending auto-merge PRs to be merged.
- Check [`Version Packages (next)` Pull Request](https://github.com/backstage/backstage/pulls?q=is%3Aopen+is%3Apr+in%3Atitle+%22Version+Packages+%28next%29%22) for sufficient approval to be merged
- Check generated `changelog` in the changed files of the pull request under `docs/releases` to see if there are any unexpected major bumps e.g. by searching the file
- Review & approve changes
- Reach out to core maintainer to merge the pull request
- Heads-up: The microsite building step can be skipped as long as the `prettier` task passes & everything else looks green
Merging the `Version Packages (next)` Pull Request will trigger the deployment workflows. Follow along the [deployment workflow](https://github.com/backstage/backstage/actions/workflows/deploy_packages.yml). If you notice flakiness (e.g. if the build is flaky or if the release step runs into an error with releasing to npm) just restart the workflow.
Congratulations on the release! There should be now a post in the [`#announcements` channel](https://discord.com/channels/687207715902193673/705123584468582400) in Discord linking to the release tag - check if links & tag look as expected. Finally unlock the main branch again. Merging PRs in master directly after release should be done with caution as it potential complicates fixing issues introduced in the release.
## Switching Release Modes
- To enter pre-release mode: `yarn changeset pre enter next` & create PR + merge changes
- To exit pre-release mode: `yarn changeset pre exit` & create PR + merge changes
- Has to be done before the mainline release
- It's not time critical; Affects the next release happening
## Emergency Release Process
**This emergency release process is intended only for the Backstage
+12
View File
@@ -0,0 +1,12 @@
---
title: Artifacts Plugin for JFrog Artifactory
author: StageCentral
authorUrl: https://stagecentral.io
category: Artifacts
description: |
Connect Backstage entities to software artifacts stored in JFrog Artifactory.
Display information about artifacts, such as their version, size, and download links.
documentation: https://github.com/StageCentral/backstage-artifactory-plugin/tree/main#readme
iconUrl: https://raw.githubusercontent.com/StageCentral/backstage-artifactory-plugin/main/logo/logo.png
npmPackageName: '@stagecentral/plugin-artifactory'
addedDate: '2023-08-23'
+10
View File
@@ -0,0 +1,10 @@
---
title: Flux Plugins
author: Weaveworks
authorUrl: https://weave.works
category: GitOps
description: View the status of fluxcd.io resources
documentation: https://github.com/weaveworks/weaveworks-backstage/tree/main/plugins/backstage-plugin-flux#readme
iconUrl: https://raw.githubusercontent.com/fluxcd/website/main/assets/icons/logo.svg
npmPackageName: '@weaveworksoss/backstage-plugin-flux'
addedDate: '2023-08-23'
+4 -4
View File
@@ -5028,14 +5028,14 @@ __metadata:
linkType: hard
"docusaurus-plugin-sass@npm:^0.2.3":
version: 0.2.4
resolution: "docusaurus-plugin-sass@npm:0.2.4"
version: 0.2.5
resolution: "docusaurus-plugin-sass@npm:0.2.5"
dependencies:
sass-loader: ^10.1.1
peerDependencies:
"@docusaurus/core": ^2.0.0-beta
"@docusaurus/core": ^2.0.0-beta || ^3.0.0-alpha
sass: ^1.30.0
checksum: 10e0c9f519dd7ef4666b1fb82d120738feab03ea451c18bab7f785627887f65b8cb72457065a9b2a2664af1490c20b38df9adf525c1b58ce3e324d2b50412734
checksum: db17a8c87a344330717962fe195c94089e32281dc7f79b66bf99587254ebad71353b8588c5b74feb1c2841c6183e59b20259f5fb1a5c74026903557cbb113a68
languageName: node
linkType: hard
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-app-defaults
title: '@backstage/app-defaults'
description: Provides the default wiring of a Backstage App
spec:
lifecycle: production
type: backstage-web-library
owner: maintainers
+9
View File
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: example-app-next
title: example-app-next
spec:
lifecycle: experimental
type: backstage-frontend
owner: maintainers
@@ -14,11 +14,11 @@
* limitations under the License.
*/
import React from 'react';
import {
createExtension,
createPlugin,
coreExtensionData,
createSchemaFromZod,
} from '@backstage/frontend-plugin-api';
import { Router as GraphiQLPage } from '@backstage/plugin-graphiql';
@@ -27,12 +27,12 @@ export const GraphiqlPageExtension = createExtension({
component: coreExtensionData.reactComponent,
path: coreExtensionData.routePath,
},
configSchema: createSchemaFromZod(z =>
z.object({ path: z.string().default('/graphiql') }),
),
factory({ bind, config }) {
bind.component(() => {
return <GraphiQLPage />;
});
// TODO: In need of schemas and type safety
bind.path((config as { path: string }).path);
bind.component(GraphiQLPage);
bind.path(config.path);
},
});
@@ -43,7 +43,6 @@ export const graphiqlPlugin = createPlugin({
id: 'graphiql.page',
at: 'core.router/routes',
extension: GraphiqlPageExtension,
config: { path: '/graphiql' },
},
],
});
+9
View File
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: example-app
title: example-app
spec:
lifecycle: experimental
type: backstage-frontend
owner: maintainers
@@ -15,14 +15,14 @@
*/
import {
HomePageRandomJoke,
WelcomeTitle,
HeaderWorldClock,
ClockConfig,
HomePageStarredEntities,
CustomHomepageGrid,
HomePageToolkit,
HeaderWorldClock,
HomePageCompanyLogo,
HomePageRandomJoke,
HomePageStarredEntities,
HomePageToolkit,
WelcomeTitle,
} from '@backstage/plugin-home';
import { Content, Header, Page } from '@backstage/core-components';
import { HomePageSearchBar } from '@backstage/plugin-search';
@@ -64,6 +64,9 @@ const defaultConfig = [
y: 0,
width: 12,
height: 1,
movable: false,
resizable: false,
deletable: false,
},
{
component: 'WelcomeTitle',
@@ -77,7 +80,7 @@ const defaultConfig = [
x: 0,
y: 2,
width: 12,
height: 1,
height: 2,
},
];
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-backend-app-api
title: '@backstage/backend-app-api'
description: Core API used by Backstage backend apps
spec:
lifecycle: experimental
type: backstage-node-library
owner: maintainers
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-backend-common
title: '@backstage/backend-common'
description: Common functionality library for Backstage backends
spec:
lifecycle: experimental
type: backstage-node-library
owner: maintainers
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-backend-defaults
title: '@backstage/backend-defaults'
description: Backend defaults used by Backstage backend apps
spec:
lifecycle: experimental
type: backstage-node-library
owner: maintainers
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-backend-dev-utils
title: '@backstage/backend-dev-utils'
spec:
lifecycle: experimental
type: backstage-node-library
owner: maintainers
+9
View File
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: example-backend-next
title: example-backend-next
spec:
lifecycle: experimental
type: backstage-backend
owner: maintainers
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-backend-openapi-utils
title: '@backstage/backend-openapi-utils'
description: OpenAPI typescript support.
spec:
lifecycle: experimental
type: backstage-node-library
owner: maintainers
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-backend-plugin-api
title: '@backstage/backend-plugin-api'
description: Core API used by Backstage backend plugins
spec:
lifecycle: experimental
type: backstage-node-library
owner: maintainers
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-backend-tasks
title: '@backstage/backend-tasks'
description: Common distributed task management library for Backstage backends
spec:
lifecycle: experimental
type: backstage-node-library
owner: maintainers
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-backend-test-utils
title: '@backstage/backend-test-utils'
description: Test helpers library for Backstage backends
spec:
lifecycle: experimental
type: backstage-node-library
owner: maintainers
+9
View File
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: example-backend
title: example-backend
spec:
lifecycle: experimental
type: backstage-backend
owner: maintainers
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-catalog-client
title: '@backstage/catalog-client'
description: An isomorphic client for the catalog backend
spec:
lifecycle: production
type: backstage-common-library
owner: maintainers
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-catalog-model
title: '@backstage/catalog-model'
description: Types and validators that help describe the model of a Backstage Catalog
spec:
lifecycle: production
type: backstage-common-library
owner: maintainers
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-cli-common
title: '@backstage/cli-common'
description: Common functionality used by cli, backend, and create-app
spec:
lifecycle: experimental
type: backstage-node-library
owner: maintainers
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-cli-node
title: '@backstage/cli-node'
description: Node.js library for Backstage CLIs
spec:
lifecycle: experimental
type: backstage-node-library
owner: maintainers
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-cli
title: '@backstage/cli'
description: CLI for developing Backstage plugins and apps
spec:
lifecycle: experimental
type: backstage-cli
owner: maintainers
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-codemods
title: '@backstage/codemods'
description: A collection of codemods for Backstage projects
spec:
lifecycle: experimental
type: backstage-cli
owner: maintainers
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-config-loader
title: '@backstage/config-loader'
description: Config loading functionality used by Backstage backend, and CLI
spec:
lifecycle: production
type: backstage-node-library
owner: maintainers
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-config
title: '@backstage/config'
description: Config API used by Backstage core, backend, and CLI
spec:
lifecycle: production
type: backstage-common-library
owner: maintainers
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-core-app-api
title: '@backstage/core-app-api'
description: Core app API used by Backstage apps
spec:
lifecycle: production
type: backstage-web-library
owner: maintainers
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-core-components
title: '@backstage/core-components'
description: Core components used by Backstage plugins and apps
spec:
lifecycle: experimental
type: backstage-web-library
owner: maintainers
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-core-plugin-api
title: '@backstage/core-plugin-api'
description: Core API used by Backstage plugins
spec:
lifecycle: production
type: backstage-web-library
owner: maintainers
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-create-app
title: '@backstage/create-app'
description: A CLI that helps you create your own Backstage app
spec:
lifecycle: experimental
type: backstage-cli
owner: maintainers
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-dev-utils
title: '@backstage/dev-utils'
description: Utilities for developing Backstage plugins.
spec:
lifecycle: production
type: backstage-web-library
owner: maintainers
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: e2e-test
title: e2e-test
description: E2E test for verifying Backstage packages
spec:
lifecycle: experimental
type: backstage-cli
owner: maintainers
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-errors
title: '@backstage/errors'
description: Common utilities for error handling within Backstage
spec:
lifecycle: production
type: backstage-common-library
owner: maintainers
+4 -7
View File
@@ -1,12 +1,9 @@
# @backstage/frontend-app-api
_This package was created through the Backstage CLI_.
**This package is EXPERIMENTAL, we recommend against using it for production deployments**
## Installation
This package provides the core API used by Backstage frontend apps. It implements the design outlined in [RFC: Frontend System Evolution](https://github.com/backstage/backstage/issues/18372).
Install the package via Yarn:
## Documentation
```sh
cd <package-dir> # if within a monorepo
yarn add @backstage/frontend-app-api
```
- [Backstage Documentation](https://backstage.io/docs)
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-frontend-app-api
title: '@backstage/frontend-app-api'
spec:
lifecycle: experimental
type: backstage-web-library
owner: maintainers
+1
View File
@@ -35,6 +35,7 @@
"@backstage/config": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/plugin-graphiql": "workspace:^",
"@backstage/types": "workspace:^",
"lodash": "^4.17.21"
},
"peerDependencies": {
+31 -103
View File
@@ -15,9 +15,9 @@
*/
import React from 'react';
import { Config, ConfigReader } from '@backstage/config';
import { ConfigReader } from '@backstage/config';
import {
ExtensionInstanceConfig,
ExtensionInstanceParameters,
BackstagePlugin,
coreExtensionData,
} from '@backstage/frontend-plugin-api';
@@ -26,65 +26,10 @@ import {
createExtensionInstance,
ExtensionInstance,
} from './createExtensionInstance';
// Since we'll never merge arrays in config the config reader context
// isn't too much of a help. Fall back to manual config reading logic
// as the Config interface makes it quite hard for us otherwise.
function readAppExtensionConfigs(
rootConfig: Config,
): Partial<ExtensionInstanceConfig>[] {
const arr = rootConfig.getOptional('app.extensions');
if (!Array.isArray(arr)) {
if (arr === undefined) {
return [];
}
// This will throw, and show which part of config had the wrong type
rootConfig.getConfigArray('app.extensions');
return [];
}
return arr.map((value, index) => {
function errorMsg(msg: string, key?: string, prop?: string) {
return `Invalid extension configuration at app.extensions[${index}]${
key ? `[${key}]` : ''
}${prop ? `.${prop}` : ''}, ${msg}`;
}
if (typeof value === 'string') {
return { id: value };
} else if (
typeof value !== 'object' ||
value === null ||
Array.isArray(value)
) {
throw new Error(errorMsg('must be a string or an object'));
}
const keys = Object.keys(value);
if (keys.length !== 1) {
const joinedKeys = `"${keys.join('", "')}"`;
throw new Error(errorMsg(`must have exactly one key, got ${joinedKeys}`));
}
const key = keys[0];
const obj = value[key];
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
throw new Error(errorMsg('must be an object', key));
}
const at = obj.at;
if (at !== undefined && typeof at !== 'string') {
throw new Error(errorMsg('must be a string', key, 'at'));
}
const extension = obj.extension;
if (extension !== undefined && typeof extension !== 'string') {
throw new Error(errorMsg('must be a string', key, 'extension'));
}
if (extension) {
throw new Error('TODO: implement extension resolution');
}
return { id: key, at, config: obj.config /* validate later */ };
});
}
import {
mergeExtensionParameters,
readAppExtensionParameters,
} from './wiring/parameters';
/** @public */
export function createApp(options: { plugins: BackstagePlugin[] }): {
@@ -92,10 +37,7 @@ export function createApp(options: { plugins: BackstagePlugin[] }): {
} {
const appConfig = ConfigReader.fromConfigs(process.env.APP_CONFIG as any);
// pull in default extension instance from discovered packages
// apply config to adjust default extension instances and add more
const extensionInstanceConfigs = [
...options.plugins.flatMap(plugin => plugin.defaultExtensionInstances),
const builtinExtensionInstanceParams = [
{
id: 'core.router',
at: 'root/default',
@@ -104,40 +46,26 @@ export function createApp(options: { plugins: BackstagePlugin[] }): {
},
];
const appExtensionConfigs = readAppExtensionConfigs(appConfig);
for (const appExtensionConfig of appExtensionConfigs) {
const existingConfig = extensionInstanceConfigs.find(
e => e.id === appExtensionConfig.id,
);
if (existingConfig) {
if (appExtensionConfig.at) {
existingConfig.at = appExtensionConfig.at;
}
if (appExtensionConfig.extension) {
// TODO: do we want to reset config here? it might be completely
// unrelated to the previous one
existingConfig.extension = appExtensionConfig.extension;
}
if (appExtensionConfig.config) {
// TODO: merge config?
existingConfig.config = appExtensionConfig.config;
}
} else if (appExtensionConfig.id) {
const { id, at, extension, config } = appExtensionConfig;
if (!at || !extension) {
throw new Error(`Extension ${appExtensionConfig.id} is incomplete`);
}
extensionInstanceConfigs.push({ id, at, extension, config });
}
}
// pull in default extension instance from discovered packages
// apply config to adjust default extension instances and add more
const extensionInstanceParams = mergeExtensionParameters(
[
...options.plugins.flatMap(plugin => plugin.defaultExtensionInstances),
...builtinExtensionInstanceParams,
],
readAppExtensionParameters(appConfig),
);
// TODO: validate the config of all extension instances
// We do it at this point to ensure that merging (if any) of config has already happened
// Create attachment map so that we can look attachments up during instance creation
const attachmentMap = new Map<
string,
Map<string, ExtensionInstanceConfig[]>
Map<string, ExtensionInstanceParameters[]>
>();
for (const instanceConfig of extensionInstanceConfigs) {
const [extensionId, pointId = 'default'] = instanceConfig.at.split('/');
for (const instanceParams of extensionInstanceParams) {
const [extensionId, pointId = 'default'] = instanceParams.at.split('/');
let pointMap = attachmentMap.get(extensionId);
if (!pointMap) {
@@ -151,21 +79,21 @@ export function createApp(options: { plugins: BackstagePlugin[] }): {
pointMap.set(pointId, instances);
}
instances.push(instanceConfig);
instances.push(instanceParams);
}
const instances = new Map<string, ExtensionInstance>();
function createInstance(
instanceConfig: ExtensionInstanceConfig,
instanceParams: ExtensionInstanceParameters,
): ExtensionInstance {
const existingInstance = instances.get(instanceConfig.id);
const existingInstance = instances.get(instanceParams.id);
if (existingInstance) {
return existingInstance;
}
const attachments = Object.fromEntries(
Array.from(attachmentMap.get(instanceConfig.id)?.entries() ?? []).map(
Array.from(attachmentMap.get(instanceParams.id)?.entries() ?? []).map(
([inputName, attachmentConfigs]) => [
inputName,
attachmentConfigs.map(createInstance),
@@ -174,16 +102,16 @@ export function createApp(options: { plugins: BackstagePlugin[] }): {
);
return createExtensionInstance({
id: instanceConfig.id,
config: instanceConfig.config,
extension: instanceConfig.extension,
id: instanceParams.id,
config: instanceParams.config,
extension: instanceParams.extension,
attachments,
});
}
const rootConfigs = attachmentMap.get('root')?.get('default') ?? [];
const rootInstances = rootConfigs.map(instanceConfig =>
createInstance(instanceConfig),
const rootInstances = rootConfigs.map(instanceParams =>
createInstance(instanceParams),
);
return {
@@ -27,26 +27,45 @@ export interface ExtensionInstance {
/** @internal */
export function createExtensionInstance(options: {
id: string;
extension: Extension;
extension: Extension<unknown>;
config: unknown;
attachments: Record<string, ExtensionInstance[]>;
}): ExtensionInstance {
const { extension, config, attachments } = options;
const { id, extension, config, attachments } = options;
const extensionData = new Map<string, unknown>();
extension.factory({
config,
bind: mapValues(extension.output, ref => {
return (value: unknown) => extensionData.set(ref.id, value);
}),
inputs: mapValues(
extension.inputs,
({ extensionData: pointData }, inputName) => {
// TODO: validation
return (attachments[inputName] ?? []).map(attachment =>
mapValues(pointData, ref => attachment.data.get(ref.id)),
);
},
),
});
return { id: options.id, data: extensionData, $$type: 'extension-instance' };
let parsedConfig: unknown;
try {
parsedConfig = extension.configSchema?.parse(config ?? {});
} catch (e) {
throw new Error(
`Invalid configuration for extension instance '${id}', ${e}`,
);
}
try {
extension.factory({
config: parsedConfig,
bind: mapValues(extension.output, ref => {
return (value: unknown) => extensionData.set(ref.id, value);
}),
inputs: mapValues(
extension.inputs,
({ extensionData: pointData }, inputName) => {
// TODO: validation
return (attachments[inputName] ?? []).map(attachment =>
mapValues(pointData, ref => attachment.data.get(ref.id)),
);
},
),
});
} catch (e) {
throw new Error(`Failed to instantiate extension instance '${id}', ${e}`);
}
return {
id: options.id,
data: extensionData,
$$type: 'extension-instance',
};
}
@@ -0,0 +1,436 @@
/*
* Copyright 2023 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 { ConfigReader } from '@backstage/config';
import { Extension } from '@backstage/frontend-plugin-api';
import { JsonValue } from '@backstage/types';
import {
expandShorthandExtensionParameters,
mergeExtensionParameters,
readAppExtensionParameters,
} from './parameters';
function makeExt(id: string, status: 'disabled' | 'enabled' = 'enabled') {
return {
id,
at: 'foo/bar',
extension: {} as Extension<unknown>,
disabled: status === 'disabled',
};
}
describe('mergeExtensionParameters', () => {
it('should filter out disabled extension instances', () => {
expect(mergeExtensionParameters([makeExt('a', 'disabled')], [])).toEqual(
[],
);
});
it('should pass through extension instances', () => {
expect(mergeExtensionParameters([makeExt('a'), makeExt('b')], [])).toEqual([
makeExt('a'),
makeExt('b'),
]);
});
it('should override extension instances', () => {
expect(
mergeExtensionParameters(
[makeExt('a'), makeExt('b')],
[
{
id: 'b',
extension: { ext: 'other' } as unknown as Extension<unknown>,
},
],
),
).toEqual([makeExt('a'), { ...makeExt('b'), extension: { ext: 'other' } }]);
});
it('should override attachment points', () => {
expect(
mergeExtensionParameters(
[makeExt('a'), makeExt('b')],
[
{
id: 'b',
at: 'derp',
},
],
),
).toEqual([makeExt('a'), { ...makeExt('b'), at: 'derp' }]);
});
it('should fully override configuration', () => {
expect(
mergeExtensionParameters(
[
{ ...makeExt('a'), config: { foo: { bar: 1 } } },
{ ...makeExt('b'), config: { foo: { bar: 2 } } },
],
[
{
id: 'b',
config: { foo: { qux: 3 } },
},
],
),
).toEqual([
{ ...makeExt('a'), config: { foo: { bar: 1 } } },
{ ...makeExt('b'), config: { foo: { qux: 3 } } },
]);
});
it('should place enabled instances in the order that they were enabled', () => {
expect(
mergeExtensionParameters(
[makeExt('a', 'disabled'), makeExt('b', 'disabled')],
[
{
id: 'b',
disabled: false,
},
{
id: 'a',
disabled: false,
},
],
),
).toEqual([makeExt('b'), makeExt('a')]);
});
});
describe('readAppExtensionParameters', () => {
it('should disable extension with shorthand notation', () => {
expect(
readAppExtensionParameters(
new ConfigReader({ app: { extensions: [{ 'core.router': false }] } }),
),
).toEqual([
{
id: 'core.router',
disabled: true,
},
]);
expect(
readAppExtensionParameters(
new ConfigReader({
app: { extensions: [{ 'core.router': { disabled: true } }] },
}),
),
).toEqual([
{
at: undefined,
config: undefined,
disabled: true,
id: 'core.router',
},
]);
});
it('should enable extension with shorthand notation', () => {
expect(
readAppExtensionParameters(
new ConfigReader({ app: { extensions: ['core.router'] } }),
),
).toEqual([
{
id: 'core.router',
},
]);
expect(
readAppExtensionParameters(
new ConfigReader({ app: { extensions: [{ 'core.router': true }] } }),
),
).toEqual([
{
id: 'core.router',
disabled: false,
},
]);
expect(
readAppExtensionParameters(
new ConfigReader({
app: { extensions: [{ 'core.router': { disabled: false } }] },
}),
),
).toEqual([
{
id: 'core.router',
disabled: false,
},
]);
});
it('should support extension implementation shorthand', () => {
expect(
readAppExtensionParameters(
new ConfigReader({
app: {
extensions: [{ 'core.router': 'example-package#CustomRouter' }],
},
}),
ref => ({ ref } as unknown as Extension<unknown>),
),
).toEqual([
{
id: 'core.router',
extension: { ref: 'example-package#CustomRouter' },
},
]);
});
it('should support attachment shorthand', () => {
expect(
readAppExtensionParameters(
new ConfigReader({
app: {
extensions: [
{
'core.router/routes': {
extension: 'example-package#MyPage',
config: { foo: 'bar' },
},
},
],
},
}),
ref => ({ ref } as unknown as Extension<unknown>),
),
).toEqual([
{
id: 'generated.1',
at: 'core.router/routes',
extension: { ref: 'example-package#MyPage' },
config: { foo: 'bar' },
},
]);
});
it('should support attachment with extension shorthand', () => {
expect(
readAppExtensionParameters(
new ConfigReader({
app: {
extensions: [{ 'core.router/routes': 'example-package#MyPage' }],
},
}),
ref => ({ ref } as unknown as Extension<unknown>),
),
).toEqual([
{
id: 'generated.1',
at: 'core.router/routes',
extension: { ref: 'example-package#MyPage' },
},
]);
});
it('should reject attachment shorthand with explicit attachment', () => {
expect(() =>
readAppExtensionParameters(
new ConfigReader({
app: {
extensions: [
{
'core.router/routes': {
at: 'other/input',
},
},
],
},
}),
),
).toThrow(
`Invalid extension configuration at app.extensions[0][core.router/routes], must not redundantly specify 'at' when the extension input ID form of the key is used (with a slash); the 'at' is already implicitly 'core.router/routes'`,
);
});
});
describe('expandShorthandExtensionParameters', () => {
const resolveExtensionRef = jest.fn(
ref => ({ ref } as unknown as Extension<unknown>),
);
const generateExtensionId = jest.fn(() => 'generated.1');
const run = (value: JsonValue) => {
return expandShorthandExtensionParameters(
value,
1,
resolveExtensionRef,
generateExtensionId,
);
};
it('rejects unknown keys', () => {
expect(() => run(null)).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], must be a string or an object"`,
);
expect(() => run(1)).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], must be a string or an object"`,
);
expect(() => run([])).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], must be a string or an object"`,
);
});
it('rejects the wrong number of keys', () => {
expect(() => run({})).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], must have exactly one key, got none"`,
);
expect(() => run({ a: {}, b: {} })).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], must have exactly one key, got 'a', 'b'"`,
);
});
it('rejects unknown values', () => {
expect(() => run({ a: null })).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][a], value must be a boolean, string, or object"`,
);
expect(() => run({ a: 1 })).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][a], value must be a boolean, string, or object"`,
);
expect(() => run({ a: [] })).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][a], value must be a boolean, string, or object"`,
);
});
it('supports string key', () => {
expect(run('core.router')).toEqual({
id: 'core.router',
});
expect(() => run('')).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], string shorthand cannot be the empty string"`,
);
expect(() => run('core.router/routes')).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], cannot target an extension instance input with the string shorthand (key cannot contain slashes; did you mean 'core.router'?)"`,
);
});
it('supports boolean value', () => {
expect(run({ 'core.router': true })).toEqual({
id: 'core.router',
disabled: false,
});
expect(run({ 'core.router': false })).toEqual({
id: 'core.router',
disabled: true,
});
expect(() =>
run({ 'core.router/routes': false }),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][core.router/routes], cannot target an extension instance input (key cannot contain slashes; did you mean 'core.router'?)"`,
);
});
it('supports string values', () => {
expect(run({ 'core.router': 'example-package#MyRouter' })).toEqual({
id: 'core.router',
extension: { ref: 'example-package#MyRouter' },
});
expect(run({ 'core.router/routes': 'example-package#MyRouter' })).toEqual({
id: 'generated.1',
at: 'core.router/routes',
extension: { ref: 'example-package#MyRouter' },
});
});
it('supports object id only in the key', () => {
expect(() =>
run({ 'core.router/routes': { id: 'some.id' } }),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][core.router/routes].id, must not specify 'id' when the extension input ID form of the key is used (with a slash); please replace the key 'core.router/routes' with the id instead, and put that key in the 'at' field"`,
);
expect(() =>
run({
'core.router/routes': {
id: 'example-package#MyRouter',
at: 'core.router/routes',
},
}),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][core.router/routes], must not redundantly specify 'at' when the extension input ID form of the key is used (with a slash); the 'at' is already implicitly 'core.router/routes'"`,
);
});
it('supports object at', () => {
expect(run({ 'core.router': { at: 'other.root/inputs' } })).toEqual({
id: 'core.router',
at: 'other.root/inputs',
});
expect(() =>
run({
'core.router': {
id: 'other-id',
},
}),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][core.router], must not redundantly specify 'id' when the extension instance ID form of the key is used (without a slash); the 'id' is already implicitly 'core.router'"`,
);
});
it('supports object disabled', () => {
expect(run({ 'core.router': { disabled: true } })).toEqual({
id: 'core.router',
disabled: true,
});
expect(run({ 'core.router': { disabled: false } })).toEqual({
id: 'core.router',
disabled: false,
});
expect(() =>
run({ 'core.router': { disabled: 0 } }),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][core.router].disabled, must be a boolean"`,
);
});
it('supports object extension', () => {
expect(
run({ 'core.router/routes': { extension: 'example-package#MyRouter' } }),
).toEqual({
id: 'generated.1',
at: 'core.router/routes',
extension: { ref: 'example-package#MyRouter' },
});
expect(() =>
run({ 'core.router/routes': { extension: 0 } }),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][core.router/routes].extension, must be a string"`,
);
});
it('supports object config', () => {
expect(
run({ 'core.router': { config: { disableRedirects: true } } }),
).toEqual({
id: 'core.router',
config: { disableRedirects: true },
});
expect(() =>
run({ 'core.router': { config: 0 } }),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][core.router].config, must be an object"`,
);
});
it('rejects unknown object keys', () => {
expect(() =>
run({ 'core.router': { foo: { settings: true } } }),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1][core.router].foo, unknown parameter; expected one of 'at', 'disabled', 'extension', 'config'"`,
);
});
});
@@ -0,0 +1,299 @@
/*
* Copyright 2023 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 { Config } from '@backstage/config';
import {
Extension,
ExtensionInstanceParameters,
} from '@backstage/frontend-plugin-api';
import { JsonValue } from '@backstage/types';
import omitBy from 'lodash/omitBy';
const knownExtensionInstanceParameters = [
'at',
'disabled',
'extension',
'config',
];
// Since we'll never merge arrays in config the config reader context
// isn't too much of a help. Fall back to manual config reading logic
// as the Config interface makes it quite hard for us otherwise.
/** @internal */
export function readAppExtensionParameters(
rootConfig: Config,
resolveExtensionRef: (ref: string) => Extension<unknown> = () => {
throw new Error('Not Implemented');
},
): Partial<ExtensionInstanceParameters>[] {
const arr = rootConfig.getOptional('app.extensions');
if (!Array.isArray(arr)) {
if (arr === undefined) {
return [];
}
// This will throw, and show which part of config had the wrong type
rootConfig.getConfigArray('app.extensions');
return [];
}
const generateExtensionId = (() => {
let index = 1;
return () => `generated.${index++}`;
})();
return arr.map((arrayEntry, arrayIndex) =>
expandShorthandExtensionParameters(
arrayEntry,
arrayIndex,
resolveExtensionRef,
generateExtensionId,
),
);
}
/** @internal */
export function expandShorthandExtensionParameters(
arrayEntry: JsonValue,
arrayIndex: number,
resolveExtensionRef: (ref: string) => Extension<unknown>,
generateExtensionId: () => string,
): Partial<ExtensionInstanceParameters> {
function errorMsg(msg: string, key?: string, prop?: string) {
return `Invalid extension configuration at app.extensions[${arrayIndex}]${
key ? `[${key}]` : ''
}${prop ? `.${prop}` : ''}, ${msg}`;
}
// Example YAML:
// - entity.card.about
if (typeof arrayEntry === 'string') {
if (arrayEntry.includes('/')) {
const suggestion = arrayEntry.split('/')[0];
throw new Error(
errorMsg(
`cannot target an extension instance input with the string shorthand (key cannot contain slashes; did you mean '${suggestion}'?)`,
),
);
}
if (!arrayEntry) {
throw new Error(errorMsg('string shorthand cannot be the empty string'));
}
return {
id: arrayEntry,
};
}
// All remaining cases are single-key objects
if (
typeof arrayEntry !== 'object' ||
arrayEntry === null ||
Array.isArray(arrayEntry)
) {
throw new Error(errorMsg('must be a string or an object'));
}
const keys = Object.keys(arrayEntry);
if (keys.length !== 1) {
const joinedKeys = keys.length ? `'${keys.join("', '")}'` : 'none';
throw new Error(errorMsg(`must have exactly one key, got ${joinedKeys}`));
}
const key = keys[0];
const value = arrayEntry[key];
// Example YAML:
// - catalog.page.cicd: false
if (typeof value === 'boolean') {
if (key.includes('/')) {
const suggestion = key.split('/')[0];
throw new Error(
errorMsg(
`cannot target an extension instance input (key cannot contain slashes; did you mean '${suggestion}'?)`,
key,
),
);
}
return {
id: key,
disabled: !value,
};
}
// Example YAML:
// - core.router/routes: '@backstage/plugin-some-plugin#MyPage'
// - some-plugin.page: '@internal/frontend-customizations#MyModifiedPage'
if (typeof value === 'string') {
if (key.includes('/')) {
return {
id: generateExtensionId(),
at: key,
extension: resolveExtensionRef(value),
};
}
return {
id: key,
extension: resolveExtensionRef(value),
};
}
// The remaining case is the generic object. Example YAML:
// - core.router/routes:
// extension: '@backstage/core-app-api#Redirect'
// config:
// path: /
// to: /catalog
// - tech-radar.page:
// at: core.router/routes
// extension: '@backstage/plugin-tech-radar#TechRadarPage'
// config:
// path: /tech-radar
// width: 1500
// height: 800
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new Error(
errorMsg('value must be a boolean, string, or object', key),
);
}
let id = value.id;
let at = value.at;
const disabled = value.disabled;
const extensionRef = value.extension;
const config = value.config;
if (key.includes('/')) {
if (at !== undefined) {
throw new Error(
errorMsg(
`must not redundantly specify 'at' when the extension input ID form of the key is used (with a slash); the 'at' is already implicitly '${key}'`,
key,
),
);
}
if (id !== undefined) {
throw new Error(
errorMsg(
`must not specify 'id' when the extension input ID form of the key is used (with a slash); please replace the key '${key}' with the id instead, and put that key in the 'at' field`,
key,
'id',
),
);
}
id = generateExtensionId();
at = key;
} else {
if (id !== undefined) {
throw new Error(
errorMsg(
`must not redundantly specify 'id' when the extension instance ID form of the key is used (without a slash); the 'id' is already implicitly '${key}'`,
key,
),
);
}
id = key;
}
if (id !== undefined && typeof id !== 'string') {
throw new Error(errorMsg('must be a string', key, 'id'));
} else if (at !== undefined && typeof at !== 'string') {
throw new Error(errorMsg('must be a string', key, 'at'));
} else if (disabled !== undefined && typeof disabled !== 'boolean') {
throw new Error(errorMsg('must be a boolean', key, 'disabled'));
} else if (extensionRef !== undefined && typeof extensionRef !== 'string') {
throw new Error(errorMsg('must be a string', key, 'extension'));
} else if (
config !== undefined &&
(typeof config !== 'object' || config === null || Array.isArray(config))
) {
throw new Error(errorMsg('must be an object', key, 'config'));
}
const unknownKeys = Object.keys(value).filter(
k => !knownExtensionInstanceParameters.includes(k),
);
if (unknownKeys.length > 0) {
throw new Error(
errorMsg(
`unknown parameter; expected one of '${knownExtensionInstanceParameters.join(
"', '",
)}'`,
key,
unknownKeys.join(', '),
),
);
}
const extension: Extension<unknown> | undefined = extensionRef
? resolveExtensionRef(extensionRef)
: undefined;
return omitBy(
{
id,
at,
disabled,
extension,
config,
},
v => v === undefined,
);
}
/** @internal */
export function mergeExtensionParameters(
base: ExtensionInstanceParameters[],
overrides: Array<Partial<ExtensionInstanceParameters>>,
): ExtensionInstanceParameters[] {
const extensionInstanceParams = base.slice();
for (const overrideParam of overrides) {
const existingParamIndex = extensionInstanceParams.findIndex(
e => e.id === overrideParam.id,
);
if (existingParamIndex !== -1) {
const existingParam = extensionInstanceParams[existingParamIndex];
if (overrideParam.at) {
existingParam.at = overrideParam.at;
}
if (overrideParam.extension) {
// TODO: do we want to reset config here? it might be completely
// unrelated to the previous one
existingParam.extension = overrideParam.extension;
}
if (overrideParam.config) {
// TODO: merge config?
existingParam.config = overrideParam.config;
}
if (Boolean(existingParam.disabled) !== Boolean(overrideParam.disabled)) {
existingParam.disabled = Boolean(overrideParam.disabled);
if (!existingParam.disabled) {
// bump
extensionInstanceParams.splice(existingParamIndex, 1);
extensionInstanceParams.push(existingParam);
}
}
} else if (overrideParam.id) {
const { id, at, extension, config } = overrideParam;
if (!at || !extension) {
throw new Error(`Extension ${overrideParam.id} is incomplete`);
}
extensionInstanceParams.push({ id, at, extension, config });
}
}
return extensionInstanceParams.filter(param => !param.disabled);
}
+4 -7
View File
@@ -1,12 +1,9 @@
# @backstage/frontend-plugin-api
_This package was created through the Backstage CLI_.
**This package is EXPERIMENTAL, we recommend against using it for production deployments**
## Installation
This package provides the core API used by Backstage frontend plugins. It implements the design outlined in [RFC: Frontend System Evolution](https://github.com/backstage/backstage/issues/18372).
Install the package via Yarn:
## Documentation
```sh
cd <package-dir> # if within a monorepo
yarn add @backstage/frontend-plugin-api
```
- [Backstage Documentation](https://backstage.io/docs)
+32 -9
View File
@@ -4,6 +4,10 @@
```ts
import { ComponentType } from 'react';
import { JsonObject } from '@backstage/types';
import { z } from 'zod';
import { ZodSchema } from 'zod';
import { ZodTypeDef } from 'zod';
// @public (undocumented)
export type AnyExtensionDataMap = Record<string, ExtensionDataRef<any>>;
@@ -13,7 +17,7 @@ export interface BackstagePlugin {
// (undocumented)
$$type: 'backstage-plugin';
// (undocumented)
defaultExtensionInstances: ExtensionInstanceConfig[];
defaultExtensionInstances: ExtensionInstanceParameters[];
// (undocumented)
id: string;
}
@@ -21,7 +25,7 @@ export interface BackstagePlugin {
// @public (undocumented)
export interface BackstagePluginOptions {
// (undocumented)
defaultExtensionInstances?: ExtensionInstanceConfig[];
defaultExtensionInstances?: ExtensionInstanceParameters[];
// (undocumented)
id: string;
}
@@ -41,7 +45,8 @@ export function createExtension<
extensionData: AnyExtensionDataMap;
}
>,
>(options: CreateExtensionOptions<TData, TPoint>): Extension;
TConfig = never,
>(options: CreateExtensionOptions<TData, TPoint, TConfig>): Extension<TConfig>;
// @public (undocumented)
export interface CreateExtensionOptions<
@@ -52,11 +57,14 @@ export interface CreateExtensionOptions<
extensionData: AnyExtensionDataMap;
}
>,
TConfig,
> {
// (undocumented)
configSchema?: PortableSchema<TConfig>;
// (undocumented)
factory(options: {
bind: ExtensionDataBind<TData>;
config?: unknown;
config: TConfig;
inputs: {
[pointName in keyof TPoint]: ExtensionDataValue<
TPoint[pointName]['extensionData']
@@ -73,13 +81,20 @@ export interface CreateExtensionOptions<
export function createPlugin(options: BackstagePluginOptions): BackstagePlugin;
// @public (undocumented)
export interface Extension {
export function createSchemaFromZod<TOutput, TInput>(
schemaCreator: (zImpl: typeof z) => ZodSchema<TOutput, ZodTypeDef, TInput>,
): PortableSchema<TOutput>;
// @public (undocumented)
export interface Extension<TConfig> {
// (undocumented)
$$type: 'extension';
// (undocumented)
configSchema?: PortableSchema<TConfig>;
// (undocumented)
factory(options: {
bind: ExtensionDataBind<AnyExtensionDataMap>;
config?: unknown;
config: TConfig;
inputs: Record<string, Array<Record<string, unknown>>>;
}): void;
// (undocumented)
@@ -111,14 +126,22 @@ export type ExtensionDataValue<TData extends AnyExtensionDataMap> = {
};
// @public (undocumented)
export interface ExtensionInstanceConfig {
export interface ExtensionInstanceParameters {
// (undocumented)
at: string;
// (undocumented)
config: unknown;
config?: unknown;
// (undocumented)
extension: Extension;
disabled?: boolean;
// (undocumented)
extension: Extension<unknown>;
// (undocumented)
id: string;
}
// @public (undocumented)
export type PortableSchema<TOutput> = {
parse: (input: unknown) => TOutput;
schema: JsonObject;
};
```
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-frontend-plugin-api
title: '@backstage/frontend-plugin-api'
spec:
lifecycle: experimental
type: backstage-web-library
owner: maintainers
+4 -1
View File
@@ -33,6 +33,9 @@
"react": "^16.13.1 || ^17.0.0"
},
"dependencies": {
"lodash": "^4.17.21"
"@backstage/types": "workspace:^",
"lodash": "^4.17.21",
"zod": "^3.21.4",
"zod-to-json-schema": "^3.21.4"
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2023 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 { createSchemaFromZod } from './createSchemaFromZod';
describe('createSchemaFromZod', () => {
it('should provide nice parse errors', () => {
const { parse } = createSchemaFromZod(z =>
z.object({
foo: z.union([z.string(), z.number()]),
derp: z.object({ bar: z.number() }),
}),
);
expect(() => parse({ derp: { bar: 'derp' } })).toThrow(
`Missing required value at 'foo'; Expected number, received string at 'derp.bar'`,
);
expect(() => parse(undefined)).toThrow(`Missing required value`);
expect(() => parse('derp')).toThrow(`Expected object, received string`);
});
});
@@ -0,0 +1,59 @@
/*
* Copyright 2023 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 { JsonObject } from '@backstage/types';
import { z, ZodSchema, ZodTypeDef } from 'zod';
import zodToJsonSchema from 'zod-to-json-schema';
/** @public */
export type PortableSchema<TOutput> = {
parse: (input: unknown) => TOutput;
schema: JsonObject;
};
/** @public */
export function createSchemaFromZod<TOutput, TInput>(
schemaCreator: (zImpl: typeof z) => ZodSchema<TOutput, ZodTypeDef, TInput>,
): PortableSchema<TOutput> {
const schema = schemaCreator(z);
return {
// TODO: Types allow z.array etc here but it will break stuff
parse: input => {
const result = schema.safeParse(input);
if (result.success) {
return result.data;
}
throw new Error(result.error.issues.map(formatIssue).join('; '));
},
// TODO: Verify why we are not compatible with the latest zodToJsonSchema.
schema: zodToJsonSchema(schema) as JsonObject,
};
}
function formatIssue(issue: z.ZodIssue): string {
if (issue.code === 'invalid_union') {
return formatIssue(issue.unionErrors[0].issues[0]);
}
let message = issue.message;
if (message === 'Required') {
message = `Missing required value`;
}
if (issue.path.length) {
message += ` at '${issue.path.join('.')}'`;
}
return message;
}
+5 -1
View File
@@ -24,7 +24,7 @@ export {
createExtension,
coreExtensionData,
createPlugin,
type ExtensionInstanceConfig,
type ExtensionInstanceParameters,
type BackstagePlugin,
type Extension,
type AnyExtensionDataMap,
@@ -34,3 +34,7 @@ export {
type ExtensionDataRef,
type ExtensionDataValue,
} from './types';
export {
createSchemaFromZod,
type PortableSchema,
} from './createSchemaFromZod';
+16 -9
View File
@@ -15,6 +15,7 @@
*/
import { ComponentType } from 'react';
import { PortableSchema } from './createSchemaFromZod';
/** @public */
export type ExtensionDataRef<T> = {
@@ -51,12 +52,14 @@ export type ExtensionDataValue<TData extends AnyExtensionDataMap> = {
export interface CreateExtensionOptions<
TData extends AnyExtensionDataMap,
TPoint extends Record<string, { extensionData: AnyExtensionDataMap }>,
TConfig,
> {
inputs?: TPoint;
output: TData;
configSchema?: PortableSchema<TConfig>;
factory(options: {
bind: ExtensionDataBind<TData>;
config?: unknown;
config: TConfig;
inputs: {
[pointName in keyof TPoint]: ExtensionDataValue<
TPoint[pointName]['extensionData']
@@ -66,13 +69,15 @@ export interface CreateExtensionOptions<
}
/** @public */
export interface Extension {
export interface Extension<TConfig> {
$$type: 'extension';
// TODO: will extensions have a default "at" as part of their contract, making it optional in the instance config?
inputs: Record<string, { extensionData: AnyExtensionDataMap }>;
output: AnyExtensionDataMap;
configSchema?: PortableSchema<TConfig>;
factory(options: {
bind: ExtensionDataBind<AnyExtensionDataMap>;
config?: unknown;
config: TConfig;
inputs: Record<string, Array<Record<string, unknown>>>;
}): void;
}
@@ -81,29 +86,31 @@ export interface Extension {
export function createExtension<
TData extends AnyExtensionDataMap,
TPoint extends Record<string, { extensionData: AnyExtensionDataMap }>,
>(options: CreateExtensionOptions<TData, TPoint>): Extension {
TConfig = never,
>(options: CreateExtensionOptions<TData, TPoint, TConfig>): Extension<TConfig> {
return { ...options, $$type: 'extension', inputs: options.inputs ?? {} };
}
/** @public */
export interface ExtensionInstanceConfig {
export interface ExtensionInstanceParameters {
id: string;
at: string;
extension: Extension;
config: unknown;
extension: Extension<unknown>;
config?: unknown;
disabled?: boolean;
}
/** @public */
export interface BackstagePluginOptions {
id: string;
defaultExtensionInstances?: ExtensionInstanceConfig[];
defaultExtensionInstances?: ExtensionInstanceParameters[];
}
/** @public */
export interface BackstagePlugin {
$$type: 'backstage-plugin';
id: string;
defaultExtensionInstances: ExtensionInstanceConfig[];
defaultExtensionInstances: ExtensionInstanceParameters[];
}
/** @public */
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-integration-aws-node
title: '@backstage/integration-aws-node'
description: Helpers for fetching AWS account credentials
spec:
lifecycle: experimental
type: backstage-node-library
owner: maintainers
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-integration-react
title: '@backstage/integration-react'
description: Frontend package for managing integrations towards external systems
spec:
lifecycle: production
type: backstage-web-library
owner: maintainers
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-integration
title: '@backstage/integration'
description: Helpers for managing integrations towards external systems
spec:
lifecycle: production
type: backstage-common-library
owner: maintainers
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-release-manifests
title: '@backstage/release-manifests'
description: Helper library for receiving release manifests
spec:
lifecycle: experimental
type: backstage-common-library
owner: maintainers
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-repo-tools
title: '@backstage/repo-tools'
description: 'CLI for Backstage repo tooling '
spec:
lifecycle: experimental
type: backstage-cli
owner: maintainers
@@ -253,7 +253,7 @@ function createOrMergeEntity(
// Provide default name/title/description values.
name: safeEntityName,
title: packageJson.name,
...(packageJson.description
...(packageJson.description && !existingEntity.metadata?.description
? { description: packageJson.description }
: undefined),
},
+1
View File
@@ -37,6 +37,7 @@ Options:
-v --verbose
--omitTechdocsCoreMkdocsPlugin
--legacyCopyReadmeMdToIndexMd
--defaultPlugin [defaultPlugins...]
-h, --help
```
@@ -47,6 +47,7 @@ export default async function generate(opts: OptionValues) {
const dockerImage = opts.dockerImage;
const pullImage = opts.pull;
const legacyCopyReadmeMdToIndexMd = opts.legacyCopyReadmeMdToIndexMd;
const defaultPlugins = opts.defaultPlugin;
logger.info(`Using source dir ${sourceDir}`);
logger.info(`Will output generated files in ${outputDir}`);
@@ -68,6 +69,7 @@ export default async function generate(opts: OptionValues) {
mkdocs: {
legacyCopyReadmeMdToIndexMd,
omitTechdocsCorePlugin,
defaultPlugins,
},
},
},
@@ -70,6 +70,11 @@ export function registerCommands(program: Command) {
'Attempt to ensure an index.md exists falling back to using <docs-dir>/README.md or README.md in case a default <docs-dir>/index.md is not provided.',
false,
)
.option(
'--defaultPlugin [defaultPlugins...]',
'Plugins which should be added automatically to the mkdocs.yaml file',
[],
)
.alias('build')
.action(lazy(() => import('./generate/generate').then(m => m.default)));
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-test-utils
title: '@backstage/test-utils'
description: Utilities to test Backstage plugins and apps.
spec:
lifecycle: production
type: backstage-web-library
owner: maintainers
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-theme
title: '@backstage/theme'
description: material-ui theme for use with Backstage.
spec:
lifecycle: experimental
type: backstage-web-library
owner: maintainers
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-types
title: '@backstage/types'
description: Common TypeScript types used within Backstage
spec:
lifecycle: production
type: backstage-common-library
owner: maintainers
+12
View File
@@ -0,0 +1,12 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-version-bridge
title: '@backstage/version-bridge'
description: >-
Utilities used by @backstage packages to support multiple concurrent
versions
spec:
lifecycle: production
type: backstage-web-library
owner: maintainers
+9
View File
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-adr-backend
title: '@backstage/plugin-adr-backend'
spec:
lifecycle: experimental
type: backstage-backend-plugin
owner: kuangp
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-adr-common
title: '@backstage/plugin-adr-common'
description: Common functionalities for the adr plugin
spec:
lifecycle: experimental
type: backstage-common-library
owner: kuangp
+9
View File
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-adr
title: '@backstage/plugin-adr'
spec:
lifecycle: experimental
type: backstage-frontend-plugin
owner: kuangp
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-airbrake-backend
title: '@backstage/plugin-airbrake-backend'
spec:
lifecycle: experimental
type: backstage-backend-plugin
owner: maintainers
+9
View File
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-airbrake
title: '@backstage/plugin-airbrake'
spec:
lifecycle: experimental
type: backstage-frontend-plugin
owner: maintainers
+10
View File
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-allure
title: '@backstage/plugin-allure'
description: A Backstage plugin that integrates with Allure
spec:
lifecycle: experimental
type: backstage-frontend-plugin
owner: maintainers
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-analytics-module-ga
title: '@backstage/plugin-analytics-module-ga'
spec:
lifecycle: experimental
type: backstage-frontend-plugin-module
owner: maintainers
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-analytics-module-ga4
title: '@backstage/plugin-analytics-module-ga4'
spec:
lifecycle: experimental
type: backstage-frontend-plugin-module
owner: maintainers
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-analytics-module-newrelic-browser
title: '@backstage/plugin-analytics-module-newrelic-browser'
spec:
lifecycle: experimental
type: backstage-frontend-plugin-module
owner: maintainers

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