Merge remote-tracking branch 'upstream/master' into feat/relative-ref-tmp

This commit is contained in:
Dominik Henneke
2021-07-21 12:16:14 +02:00
118 changed files with 1736 additions and 939 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/techdocs-common': patch
---
Fix validation of mkdocs.yml docs_dir
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/catalog-client': patch
---
Export `CatalogRequestOptions` type
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
TokenIssuer is now exported so it may be used by auth providers that are not bundled with Backstage
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-jenkins-backend': patch
---
Update `@backstage/backend-common` to `^0.8.6`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-app-api': patch
---
Fix a bug in `FlatRoutes` that prevented outlets from working with the root route, as well as matching root routes too broadly.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-import': patch
---
Fix heading that wrongly implied catalog-import supports entity discovery for multiple integrations.
+15
View File
@@ -0,0 +1,15 @@
---
'@backstage/backend-common': patch
---
It's possible to customize the request logging handler when building the service. For example in your `backend`
```
const service = createServiceBuilder(module)
.loadConfig(config)
.setRequestLoggingHandler((logger?: Logger): RequestHandler => {
const actualLogger = (logger || getRootLogger()).child({
type: 'incomingRequest',
});
return expressWinston.logger({ ...
```
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/core-components': patch
'@backstage/test-utils': patch
'@backstage/plugin-api-docs': patch
'@backstage/plugin-catalog': patch
---
Updated the layout of catalog and API index pages to handle smaller screen sizes. This adds responsive wrappers to the entity tables, and switches filters to a drawer when width-constrained. If you have created a custom catalog or API index page, you will need to update the page structure to match the updated [catalog customization](https://backstage.io/docs/features/software-catalog/catalog-customization) documentation.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
chore: bump `eslint` to `7.30.0`
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-git-release-manager': minor
---
Enable users to add custom features
Add more metadata to success callbacks
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': patch
---
Export `CatalogClientWrapper` class
+15
View File
@@ -0,0 +1,15 @@
---
'@backstage/core-components': patch
'@backstage/create-app': patch
'@backstage/plugin-catalog': patch
'@backstage/plugin-techdocs': patch
---
Improve the responsiveness of the EntityPage UI. With this the Header component should scale with the screen size & wrapping should not cause overflowing/blocking of links. Additionally enforce the Pages using the Grid Layout to use it across all screen sizes & to wrap as intended.
To benefit from the improved responsive layout, the `EntityPage` in existing Backstage applications should be updated to set the `xs` column size on each grid item in the page, as this does not default. For example:
```diff
- <Grid item md={6}>
+ <Grid item xs={12} md={6}>
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
- Move out the `cookiecutter` templating to its own module that is depended on by the `scaffolder-backend` plugin. No breaking change yet, but we will drop first class support for `cookiecutter` in the future and it will become an opt-in feature.
@@ -0,0 +1,5 @@
---
'@backstage/techdocs-common': patch
---
Add link to https://backstage.io/docs/features/techdocs/configuration in the log warning message about updating techdocs.generate key.
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': patch
---
Handle error responses in `getTechDocsMetadata` and `getEntityMetadata` such that `<TechDocsPageHeader>` doesn't throw errors.
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/create-app': patch
'@backstage/plugin-scaffolder-backend': patch
---
Moved sample software templates to the [backstage/software-templates](https://github.com/backstage/software-templates) repository. If you previously referenced the sample templates straight from `scaffolder-backend` plugin in the main [backstage/backstage](https://github.com/backstage/backstage) repository in your `app-config.yaml`, these references will need to be updated.
See https://github.com/backstage/software-templates
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/dev-utils': patch
---
Allow custom theme for dev app.
+1
View File
@@ -212,6 +212,7 @@ repos
rerender
Reusability
reusability
roadmaps
rollbar
Rollbar
Rollup
@@ -27,27 +27,33 @@ default catalog page and create a component in a
```tsx
// imports, etc omitted for brevity. for full source see:
// https://github.com/backstage/backstage/blob/master/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx
export const CustomCatalogPage = () => {
export const CustomCatalogPage = ({
columns,
actions,
initiallySelectedFilter = 'owned',
}: CatalogPageProps) => {
return (
<CatalogLayout>
<PageWithHeader title={`${orgName} Catalog`} themeId="home">
<Content>
<ContentHeader title="Components">
<CreateComponentButton />
<SupportButton>All your software catalog entities</SupportButton>
</ContentHeader>
<div className={styles.contentWrapper}>
<EntityListProvider>
<div>
<EntityListProvider>
<FilteredEntityLayout>
<FilterContainer>
<EntityKindPicker initialFilter="component" hidden />
<EntityTypePicker />
<UserListPicker />
<UserListPicker initialFilter={initiallySelectedFilter} />
<EntityTagPicker />
</div>
<CatalogTable />
</EntityListProvider>
</div>
</FilterContainer>
<EntityListContainer>
<CatalogTable columns={columns} actions={actions} />
</EntityListContainer>
</FilteredEntityLayout>
</EntityListProvider>
</Content>
</CatalogLayout>
</PageWithHeader>
);
};
```
@@ -137,19 +143,27 @@ export const EntitySecurityTierPicker = () => {
Now we can add the component to `CustomCatalogPage`:
```diff
export const CustomCatalogPage = () => {
export const CustomCatalogPage = ({
columns,
actions,
initiallySelectedFilter = 'owned',
}: CatalogPageProps) => {
return (
...
<EntityListProvider>
<div>
<EntityListProvider>
<FilteredEntityLayout>
<FilterContainer>
<EntityKindPicker initialFilter="component" hidden />
<EntityTypePicker />
<UserListPicker />
+ <EntitySecurityTierPicker />
<UserListPicker initialFilter={initiallySelectedFilter} />
+ <EntitySecurityTierPicker />
<EntityTagPicker />
</div>
<CatalogTable />
</EntityListProvider>
<FilterContainer>
<EntityListContainer>
<CatalogTable columns={columns} actions={actions} />
</EntityListContainer>
</FilteredEntityLayout>
</EntityListProvider>
...
};
```
+5 -6
View File
@@ -8,12 +8,11 @@ description: The Backstage Software Catalog
## What is a Software Catalog?
The Backstage Software Catalog — actually, a software catalog, since it includes
more than just services — is a centralized system that keeps track of ownership
and metadata for all the software in your ecosystem (services, websites,
libraries, data pipelines, etc). The catalog is built around the concept of
[metadata YAML files](descriptor-format.md) stored together with the code, which
are then harvested and visualized in Backstage.
The Backstage Software Catalog is a centralized system that keeps track of
ownership and metadata for all the software in your ecosystem (services,
websites, libraries, data pipelines, etc). The catalog is built around the
concept of [metadata YAML files](descriptor-format.md) stored together with the
code, which are then harvested and visualized in Backstage.
![software-catalog](https://backstage.io/blog/assets/6/header.png)
+151 -82
View File
@@ -1,116 +1,185 @@
---
id: roadmap
title: Project roadmap
description: Roadmap of Backstage Project
title: Roadmap
description: Roadmap of Backstage
---
## Current status
## The Backstage Roadmap
> Backstage is currently under rapid development. This means that you can expect
> APIs and features to evolve. It is also recommended that teams who adopt
> Backstage today [upgrade their installation](../cli/commands.md#versionsbump)
> as new [releases](https://github.com/backstage/backstage/releases) become
> available, as Backwards compatibility is not yet guaranteed.
Backstage is currently under rapid development. This page details the projects
public roadmap, the result of ongoing collaboration between the core maintainers
and the broader Backstage community. Treat the roadmap as an ever-evolving guide
to keep us aligned as a community on:
## Phases
- Upcoming enhancements and benefits,
- Planning contributions and support,
- Planning the projects adoption,
- Understanding what things are coming soon,
- Avoiding duplication of work
We have divided the project into three high-level _phases_:
### How to influence the roadmap
- 🐣 **Phase 1:** Extensible frontend platform (Done ✅) - You will be able to
easily create a single consistent UI layer for your internal infrastructure
and tools. A set of reusable
[UX patterns and components](https://backstage.io/storybook) help ensure a
consistent experience between tools.
As we evolve Backstage, we want you to contribute actively in the journey to
define the most effective developer experience in the world.
- 🐢 **Phase 2:** Software Catalog
([alpha released](https://backstage.io/blog/2020/06/22/backstage-service-catalog-alpha)) -
With a single catalog, Backstage makes it easy for a team to manage ten
services — and makes it possible for your company to manage thousands of them.
A roadmap is only useful if it captures real needs. If you have success stories,
feedback, or ideas, we want to hear from you! If you plan to work (or are
already working) on a new or existing feature, please let us know, so that we
can update the roadmap accordingly. We are also happy to share knowledge and
context that will help your feature land successfully.
- 🐇 **Phase 3:** Ecosystem (ongoing, see
[Plugin Marketplace](https://backstage.io/plugins)) - Everyone's
infrastructure stack is different. By fostering a vibrant community of
contributors we hope to provide an ecosystem of Open Source
plugins/integrations that allows you to pick the tools that match your stack.
## Detailed roadmap
If you have questions about the roadmap or want to provide feedback, we would
love to hear from you! Please create an
[Issue](https://github.com/backstage/backstage/issues/new/choose), ping us on
[Discord](https://discord.gg/EBHEGzX) or reach out directly at
[backstage-interest@spotify.com](mailto:backstage-interest@spotify.com).
Want to help out? Awesome ❤️ Head over to
You can also head over to the
[CONTRIBUTING](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md)
guidelines to get started.
### Ongoing work 🚧
If you have specific questions about the roadmap, please create an
[issue](https://github.com/backstage/backstage/issues/new/choose), ping us on
[Discord](https://discord.gg/awD6SxgQ), or
[book time](http://calendly.com/spotify-backstage) with the Spotify team.
- **[Platform stabilization](https://github.com/backstage/backstage/milestone/19)** -
Stabilize the core of Backstage, including its core features, so that the
platform can be depended on for production use. After this, plugins will
require little-to-no maintenance.
### How to read the roadmap
- **[Kubernetes plugin for service owners](https://github.com/backstage/backstage/issues/2857)** -
Improve native support for Kubernetes, making it easier for service owners to
see and manage their services running in K8s, regardless if that's locally, in
AWS, GCS, Azure, or elsewhere.
The Backstage roadmap lays out both [“whats next”](#whats-next) and
[“future work”](#future-work). With "next" we mean features planned for release
within the ongoing quarter starting in July until September 2021 included. With
"future" we mean features in the radar, but not yet scheduled.
- **[Search platform](../features/search/README.md)** - Evolve the basic search
functionality currently available into a platform that **a)** enables search
across the software catalog, TechDocs, and any other information exposed by
plugins, and **b)** supports a variety of search engine technologies.
The long-term roadmap (12 - 36 months) is not detailed in the public roadmap.
Third-party contributions are also not currently included in the roadmap. Let us
know about any ongoing developments and were happy to include it here as well.
- **[Software Templates V2](https://github.com/backstage/backstage/issues/2771)** -
Expand the templates to make the steps more composable by adding the ability
to add more steps for custom logic, including webhooks and using authorization
from integrations.
### Roadmap evolution
### Future work 🔮
Will this roadmap change? Obviously!
- **Golden Path for Plugin Development** - Create an easy, standardized way for
developers to build plugins that will encourage contributions and lead to a
richer ecosystem for everyone.
Roadmap are always evolving and ours is no different; you can expect to see this
updated roughly every month.
- **[GraphQL API](https://github.com/backstage/backstage/milestone/13)** - A
GraphQL API will open up the rich metadata provided by Backstage in a single
query. Plugins can easily query this API as well as extend the model where
needed.
## Whats next
- **Inter-Plugin Communication** - **[Under consideration]** Establish more
clearly defined patterns for plugins to communicate.
The feature set below is planned for the ongoing quarter, and grouped by theme.
The list order doesnt necessarily reflect priority, and the development/release
cycle will vary based on maintainer schedules.
- **Improved Access Control** - **[Under consideration]** Provide finer grained
access controls and management for better control of the platform user
experience.
### Backstage Core
### Plugins
The following features are planned for release:
Building and maintaining [plugins](https://backstage.io/plugins) is the work of
the entire Backstage community.
- **Composable homepage:** Were seeing lots of interest from the community in
reusable components to build a homepage experience where users can easily
surface what they might find useful to start their tasks. Check out the
[milestone](https://github.com/backstage/backstage/milestone/34) for further
details.
- **Improved responsiveness:** Check out the
[RFC here](https://github.com/backstage/backstage/issues/6318) for further
details on how to improve the responsiveness for Backstage's UI.
A list of plugins that are in development is
[available here](https://github.com/backstage/backstage/issues?q=is%3Aissue+is%3Aopen+label%3Aplugin+sort%3Areactions-%2B1-desc).
We strongly recommend to upvote 👍 plugins you are interested in. This helps us
and the community prioritize what plugins to build.
### Software Templates
Are you missing a plugin for your favorite tool? Please
[suggest a new one](https://github.com/backstage/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME).
Chances are that someone will jump in and help build it.
The following features are planned for release::
### Community Initiatives 🧑‍🤝‍🧑
- **Re-creation/resubmission in case of failure:** Speed up productivity by
allowing developers to relaunch a project after a failure or any unexpected
problem. In the current version, this task requires retyping and a full
re-creation from scratch.
- **Performance and usability improvements for contributors:** Reach a relevant
improvement in templating's performance through the replacement of
[handlebars](https://handlebarsjs.com/). Other replacements will be considered
as part of this task (possibly
[cookiecutter](https://cookiecutter.readthedocs.io/)) for easier software
template creation, allowing more contributors to reach their goals without
having to learn new tooling.
- **Improved extensibility through inclusion:** Make software templates more
maintainable and extensible by adding `$include` support for parameters.
- **Authenticated job creation:** Created jobs will be able to run with an
authenticated user with all actions tracked for future consumption and
evidence. Track users creating jobs and make “jobs created by me” reporting
available.
- [**Backstage Community Sessions**](https://github.com/backstage/community#meetups) -
A monthly meetup for the community to come together to share and learn about
the latest happenings in Backstage.
### Software Catalog
- **Backstage Hackathons** - (Coming soon) Open to everyone in our Backstage
community, a celebration of you, the project and building awesome things
together
The following features are planned for release:
### Completed milestones ✅
- **Request For Comments (RFC) for composability improvements (routing):**
Enable plugins to be auto-added and make plugin installation and upgrades
easier for all Backstage users. This includes information card layouts, entity
pages containing content and hooking the external header, considering the
support of a separate deployment, and configuration for plugins.
- **Removing duplicated entities in catalog:** As any adopter knows, a software
catalog can contain thousands or more entities and it is very important to
avoid duplications in naming to prevent failures. With this development task,
two entities with the same name won't be allowed as described
[here](https://github.com/backstage/backstage/issues/4760).
- **Connecting identity to ownership to prepare for role-based access control
([RBAC](https://en.wikipedia.org/wiki/Role-based_access_control)):** This is a
first step to supporting RBAC for the software catalog (see the
[future work section](#future-work) for further details). Provide each entity
within the software catalog with a recognized owner.
- **Catalog performance improvements through improved caching:** Fix the
performance gaps in the catalog processor, which currently doesnt have a
strong caching mechanism. The current version often requires fetching a
relevant amount of data, especially at scale.
### Search
The following features are planned for release:
- ElasticSearch integration: Add ElasticSearch to the Search Platform as the
underlying search engine. Check out the
[milestone here](https://github.com/backstage/backstage/milestone/27) for
further details.
### TechDocs
The following features are planned for release:
- **TechDocs beta release:** Fix remaining bugs to get TechDocs to Beta. Check
out the [milestone here](https://github.com/backstage/backstage/milestone/29)
for further details.
## Future work
The following feature list doesnt represent a commitment to develop and the
list order doesnt reflect any priority or importance. But these features are on
the maintainers radar, with clear interest expressed by the community.
- **Improved UX design:** Provide a better Backstage user experience through
visual guidelines and templates, especially navigation across plug-ins and
portal functionalities.
- **Catalog composability (routing):** Follow up development after the RFC
planned for the ongoing quarter (see [whats next](#whats-next) for further
details).
- **Catalog-import improvements:** Provide a faster (scalability) and better
(more features like move/rename) way to import entities into the Software
Catalog. Importing items in the Software Catalog is crucial for creating a
Backstage proof-of-concept or testing/planning for broader organizational
adoption. This enhancement better supports getting developers to use Backstage
with less effort and customization.
- **Catalog improvements:** Add pagination and sourcing to Software Catalog.
- **[GraphQL](https://graphql.org/) support:** Introduce the ability to query
Backstage backend services with a standard query language for APIs.
- **Software templates performance improvements through decoupling a separate
worker:** Improve performance through decoupling resource-consuming services
and making them asynchronous. In the current version, project auto-creation
through the Software Templating system can consume a lot of resources and
bottleneck many concurrent projects created simultaneously.
- **API discovery and documentation:** Add better support for the
[gRPC](https://grpc.io/).
- **Adding TechDocs search to the Search Platform:** Having this capability in
place will provide a better and new major version of the Search Platform
(v3.0). You can refer to the
[milestone here](https://github.com/backstage/backstage/milestone/28) for
further details.
- **TechDocs GA release:** Work toward enhancements necessary to get TechDocs to
general availability. Check out the
[milestone here](https://github.com/backstage/backstage/milestone/30) for
further details.
## Completed milestones
Read more about the completed (and released) features for reference.
- [[Search] Out-of-the-Box Implementation (Alpha)](https://github.com/backstage/backstage/milestone/26)
- [Deploy a product demo at `demo.backstage.io`](https://demo.backstage.io)
- [Kubernetes plugin - v1](https://github.com/backstage/backstage/tree/master/plugins/kubernetes)
- [Helm charts](https://github.com/backstage/backstage/tree/master/contrib/chart/backstage)
+3 -4
View File
@@ -99,15 +99,14 @@ You may tweak these components, rename them and/or replace them completely.
## Connecting the plugin to the Backstage app
There are three things needed for a Backstage app to start making use of a
plugin.
There are two things needed for a Backstage app to start making use of a plugin.
1. Add plugin as dependency in `app/package.json`
2. Import and use one or more plugin extensions, for example in
`app/src/App.tsx`.
Luckily these three steps happen automatically when you create a plugin with the
Backstage CLI.
Luckily both of these steps happen automatically when you create a plugin with
the Backstage CLI.
## Talking to the outside world
+4
View File
@@ -44,6 +44,10 @@ class Footer extends React.Component {
<a href="https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md">
Contributing
</a>
<a href="https://backstage.spotify.com">Adopting</a>
<a href="https://github.com/backstage/community">
Community Sessions
</a>
<a href="https://mailchi.mp/spotify/backstage-community">
Subscribe to our newsletter
</a>
+1 -1
View File
@@ -16,7 +16,7 @@
"lock:check": "yarn-lock-check"
},
"devDependencies": {
"@spotify/prettier-config": "^10.0.0",
"@spotify/prettier-config": "^11.0.0",
"docusaurus": "^2.0.0-alpha.70",
"js-yaml": "^4.1.0",
"prettier": "^2.3.2",
+4 -4
View File
@@ -909,10 +909,10 @@
resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==
"@spotify/prettier-config@^10.0.0":
version "10.0.0"
resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-10.0.0.tgz#fa076d98d2e7e6c53dd3d86a696307a7010bd056"
integrity sha512-VYOdo8P7lIScAkl02nB9KpUAuOYMManryBIBuKJkAw5D3aVtLobfmdIKvdV6MqEmGMEQPbn7w/UpnjJYhUH+IA==
"@spotify/prettier-config@^11.0.0":
version "11.0.0"
resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-11.0.0.tgz#d91e0546a8c1c0f7299e2edc7e44306e9be210f6"
integrity sha512-dOI13j1uHMZkRxhZuge/ugOE7Aqcg7Nxki932lDZuXyY4G8CGxkc/66PeQ8pR4PCzThHORXo7Ptvau6bh101lQ==
"@types/cheerio@^0.22.8":
version "0.22.23"
+4 -4
View File
@@ -43,10 +43,10 @@
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.5.3",
"@roadiehq/backstage-plugin-buildkite": "^1.0.4",
"@roadiehq/backstage-plugin-github-insights": "^1.1.15",
"@roadiehq/backstage-plugin-github-pull-requests": "^1.0.8",
"@roadiehq/backstage-plugin-travis-ci": "^1.0.4",
"@roadiehq/backstage-plugin-buildkite": "^1.0.6",
"@roadiehq/backstage-plugin-github-insights": "^1.1.20",
"@roadiehq/backstage-plugin-github-pull-requests": "^1.0.10",
"@roadiehq/backstage-plugin-travis-ci": "^1.0.8",
"history": "^5.0.0",
"prop-types": "^15.7.2",
"react": "^16.12.0",
@@ -135,6 +135,13 @@ const EntityLayoutWrapper = (props: { children?: ReactNode }) => {
);
};
/**
* NOTE: This page is designed to work on small screens such as mobile devices.
* This is based on Material UI Grid. If breakpoints are used, each grid item must set the `xs` prop to a column size or to `true`,
* since this does not default. If no breakpoints are used, the items will equitably share the asvailable space.
* https://material-ui.com/components/grid/#basic-grid.
*/
export const cicdContent = (
<EntitySwitch>
<EntitySwitch.Case if={isJenkinsAvailable}>
@@ -292,10 +299,10 @@ const serviceEntityPage = (
<EntityLayout.Route path="/api" title="API">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<Grid item xs={12} md={6}>
<EntityProvidedApisCard />
</Grid>
<Grid item md={6}>
<Grid item xs={12} md={6}>
<EntityConsumedApisCard />
</Grid>
</Grid>
@@ -303,10 +310,10 @@ const serviceEntityPage = (
<EntityLayout.Route path="/dependencies" title="Dependencies">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<Grid item xs={12} md={6}>
<EntityDependsOnComponentsCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<Grid item xs={12} md={6}>
<EntityDependsOnResourcesCard variant="gridItem" />
</Grid>
</Grid>
@@ -431,15 +438,17 @@ const apiPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
<Grid item md={6}>
<Grid item xs={12}>
<EntityAboutCard />
</Grid>
<Grid container item md={12}>
<Grid item md={6}>
<EntityProvidingComponentsCard />
</Grid>
<Grid item md={6}>
<EntityConsumingComponentsCard />
<Grid item xs={12}>
<Grid container>
<Grid item xs={12} md={6}>
<EntityProvidingComponentsCard />
</Grid>
<Grid item xs={12} md={6}>
<EntityConsumingComponentsCard />
</Grid>
</Grid>
</Grid>
</Grid>
+11
View File
@@ -439,6 +439,13 @@ export type ReadTreeResponseFile = {
// @public
export function requestLoggingHandler(logger?: Logger_2): RequestHandler;
// Warning: (ae-missing-release-tag) "RequestLoggingHandlerFactory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type RequestLoggingHandlerFactory = (
logger?: Logger_2,
) => RequestHandler;
// Warning: (ae-missing-release-tag) "resolvePackagePath" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
@@ -492,6 +499,9 @@ export type ServiceBuilder = {
enableCors(options: cors.CorsOptions): ServiceBuilder;
setHttpsSettings(settings: HttpsSettings): ServiceBuilder;
addRouter(root: string, router: Router | RequestHandler): ServiceBuilder;
setRequestLoggingHandler(
requestLoggingHandler: RequestLoggingHandlerFactory,
): ServiceBuilder;
start(): Promise<Server>;
};
@@ -593,6 +603,7 @@ export function useHotMemoize<T>(_module: NodeModule, valueFactory: () => T): T;
// src/service/types.d.ts:57:5 - (ae-forgotten-export) The symbol "HttpsSettings" needs to be exported by the entry point index.d.ts
// src/service/types.d.ts:61:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/service/types.d.ts:62:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/service/types.d.ts:70:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// (No @packageDocumentation comment for this package)
```
+1 -1
View File
@@ -16,4 +16,4 @@
export { createServiceBuilder } from './createServiceBuilder';
export { createStatusCheckRouter } from './createStatusCheckRouter';
export type { ServiceBuilder } from './types';
export type { ServiceBuilder, RequestLoggingHandlerFactory } from './types';
@@ -27,9 +27,9 @@ import { getRootLogger } from '../../logging';
import {
errorHandler,
notFoundHandler,
requestLoggingHandler,
requestLoggingHandler as defaultRequestLoggingHandler,
} from '../../middleware';
import { ServiceBuilder } from '../types';
import { RequestLoggingHandlerFactory, ServiceBuilder } from '../types';
import {
CspOptions,
HttpsSettings,
@@ -65,6 +65,7 @@ export class ServiceBuilderImpl implements ServiceBuilder {
private cspOptions: Record<string, string[] | false> | undefined;
private httpsSettings: HttpsSettings | undefined;
private routers: [string, Router][];
private requestLoggingHandler: RequestLoggingHandlerFactory | undefined;
// Reference to the module where builder is created - needed for hot module
// reloading
private module: NodeModule;
@@ -144,6 +145,13 @@ export class ServiceBuilderImpl implements ServiceBuilder {
return this;
}
setRequestLoggingHandler(
requestLoggingHandler: RequestLoggingHandlerFactory,
) {
this.requestLoggingHandler = requestLoggingHandler;
return this;
}
async start(): Promise<http.Server> {
const app = express();
const {
@@ -160,7 +168,9 @@ export class ServiceBuilderImpl implements ServiceBuilder {
app.use(cors(corsOptions));
}
app.use(compression());
app.use(requestLoggingHandler(logger));
app.use(
(this.requestLoggingHandler ?? defaultRequestLoggingHandler)(logger),
);
for (const [root, route] of this.routers) {
app.use(root, route);
}
@@ -85,8 +85,21 @@ export type ServiceBuilder = {
*/
addRouter(root: string, router: Router | RequestHandler): ServiceBuilder;
/**
* Set the request logging handler
*
* If no handler is given the default one is used
*
* @param requestLoggingHandler a factory function that given a logger returns an handler
*/
setRequestLoggingHandler(
requestLoggingHandler: RequestLoggingHandlerFactory,
): ServiceBuilder;
/**
* Starts the server using the given settings.
*/
start(): Promise<Server>;
};
export type RequestLoggingHandlerFactory = (logger?: Logger) => RequestHandler;
+7 -2
View File
@@ -34,8 +34,6 @@ export interface CatalogApi {
location: AddLocationRequest,
options?: CatalogRequestOptions,
): Promise<AddLocationResponse>;
// Warning: (ae-forgotten-export) The symbol "CatalogRequestOptions" needs to be exported by the entry point index.d.ts
//
// (undocumented)
getEntities(
request?: CatalogEntitiesRequest,
@@ -138,6 +136,13 @@ export type CatalogListResponse<T> = {
items: T[];
};
// Warning: (ae-missing-release-tag) "CatalogRequestOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type CatalogRequestOptions = {
token?: string;
};
// Warning: (ae-missing-release-tag) "ENTITY_STATUS_CATALOG_PROCESSING_TYPE" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
@@ -20,5 +20,6 @@ export type {
CatalogApi,
CatalogEntitiesRequest,
CatalogListResponse,
CatalogRequestOptions,
} from './api';
export { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from './status';
+3 -3
View File
@@ -53,8 +53,8 @@
"@types/start-server-webpack-plugin": "^2.2.0",
"@types/webpack-env": "^1.15.2",
"@types/webpack-node-externals": "^2.5.0",
"@typescript-eslint/eslint-plugin": "^v4.27.0",
"@typescript-eslint/parser": "^v4.27.0",
"@typescript-eslint/eslint-plugin": "^v4.28.3",
"@typescript-eslint/parser": "^v4.28.3",
"@yarnpkg/lockfile": "^1.1.0",
"babel-plugin-dynamic-import-node": "^2.3.3",
"bfj": "^7.0.2",
@@ -65,7 +65,7 @@
"dashify": "^2.0.0",
"diff": "^5.0.0",
"esbuild": "^0.8.56",
"eslint": "^7.1.0",
"eslint": "^7.30.0",
"eslint-config-prettier": "^8.3.0",
"eslint-formatter-friendly": "^7.0.0",
"eslint-plugin-import": "^2.20.2",
@@ -100,7 +100,6 @@ describe('FlatRoutes', () => {
return <>Outlet: {useOutlet()}</>;
};
// The '/*' suffixes here are intentional and will be ignored by FlatRoutes
const routes = (
<>
<Route path="/a" element={<MyPage />}>
@@ -112,11 +111,15 @@ describe('FlatRoutes', () => {
<Route path="/b" element={<MyPage />}>
b
</Route>
<Route path="/" element={<MyPage />}>
c
</Route>
</>
);
const renderRoute = makeRouteRenderer(<FlatRoutes>{routes}</FlatRoutes>);
expect(renderRoute('/a').getByText('Outlet: a')).toBeInTheDocument();
expect(renderRoute('/a/b').getByText('Outlet: a-b')).toBeInTheDocument();
expect(renderRoute('/b').getByText('Outlet: b')).toBeInTheDocument();
expect(renderRoute('/').getByText('Outlet: c')).toBeInTheDocument();
});
});
@@ -49,8 +49,10 @@ export const FlatRoutes = (props: FlatRoutesProps): JSX.Element | null => {
element: child,
children: child.props.children
? [
// These are the children of each route, which we all add in under a catch-all
// subroute in order to make them available to `useOutlet`
{
path: '/*',
path: path === '/' ? '/' : '/*', // The root path must require an exact match
element: child.props.children,
},
]
+10
View File
@@ -1202,6 +1202,16 @@ export const Page: ({
children,
}: PropsWithChildren<Props_21>) => JSX.Element;
// Warning: (ae-forgotten-export) The symbol "PageWithHeaderProps" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "PageWithHeader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const PageWithHeader: ({
themeId,
children,
...props
}: PropsWithChildren<PageWithHeaderProps>) => JSX.Element;
// Warning: (ae-missing-release-tag) "Progress" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
+1 -1
View File
@@ -78,7 +78,7 @@
"@testing-library/user-event": "^13.1.8",
"@types/classnames": "^2.2.9",
"@types/d3-selection": "^2.0.0",
"@types/d3-shape": "^2.0.0",
"@types/d3-shape": "^3.0.1",
"@types/d3-zoom": "^2.0.0",
"@types/google-protobuf": "^3.7.2",
"@types/jest": "^26.0.7",
@@ -49,15 +49,17 @@ export const EmptyState = ({ title, description, missing, action }: Props) => {
className={classes.root}
spacing={2}
>
<Grid item container direction="column" xs={12} md={6}>
<Grid item>
<Typography variant="h5">{title}</Typography>
</Grid>
<Grid item>
<Typography variant="body1">{description}</Typography>
</Grid>
<Grid item className={classes.action}>
{action}
<Grid item xs={12} md={6}>
<Grid container direction="column">
<Grid item xs>
<Typography variant="h5">{title}</Typography>
</Grid>
<Grid item xs>
<Typography variant="body1">{description}</Typography>
</Grid>
<Grid item xs className={classes.action}>
{action}
</Grid>
</Grid>
</Grid>
<Grid item xs={12} md={6} className={classes.imageContainer}>
@@ -29,7 +29,7 @@ const useStyles = makeStyles({
generalImg: {
width: '95%',
zIndex: 2,
position: 'absolute',
position: 'relative',
left: '50%',
top: '50%',
transform: 'translate(-50%, 15%)',
@@ -28,7 +28,7 @@ import {
makeStyles,
Popover,
} from '@material-ui/core';
import React, { Fragment, MouseEventHandler, useState } from 'react';
import React, { MouseEventHandler, useState } from 'react';
import { SupportItem, SupportItemLink, useSupportConfig } from '../../hooks';
import { Link } from '../Link';
@@ -94,17 +94,17 @@ export const SupportButton = ({ title, children }: SupportButtonProps) => {
};
return (
<Fragment>
<Button
data-testid="support-button"
color="primary"
onClick={onClickHandler}
>
<Box marginRight={1}>
<HelpIcon />
</Box>
Support
</Button>
<>
<Box ml={1}>
<Button
data-testid="support-button"
color="primary"
onClick={onClickHandler}
startIcon={<HelpIcon />}
>
Support
</Button>
</Box>
<Popover
data-testid="support-button-popover"
open={popoverOpen}
@@ -141,6 +141,6 @@ export const SupportButton = ({ title, children }: SupportButtonProps) => {
</Button>
</DialogActions>
</Popover>
</Fragment>
</>
);
};
@@ -18,7 +18,7 @@
* TODO favoriteable capability
*/
import React, { ComponentType, Fragment, PropsWithChildren } from 'react';
import React, { ComponentType, PropsWithChildren } from 'react';
import { Typography, makeStyles } from '@material-ui/core';
import { Helmet } from 'react-helmet';
@@ -57,15 +57,15 @@ const useStyles = (props: ContentHeaderProps) =>
},
}));
type DefaultTitleProps = {
type ContentHeaderTitleProps = {
title?: string;
className: string;
className?: string;
};
const DefaultTitle = ({
const ContentHeaderTitle = ({
title = 'Unknown page',
className,
}: DefaultTitleProps) => (
}: ContentHeaderTitleProps) => (
<Typography
variant="h4"
component="h2"
@@ -77,7 +77,7 @@ const DefaultTitle = ({
);
type ContentHeaderProps = {
title?: DefaultTitleProps['title'];
title?: ContentHeaderTitleProps['title'];
titleComponent?: ComponentType;
description?: string;
textAlign?: 'left' | 'right' | 'center';
@@ -95,10 +95,10 @@ export const ContentHeader = ({
const renderedTitle = TitleComponent ? (
<TitleComponent />
) : (
<DefaultTitle title={title} className={classes.title} />
<ContentHeaderTitle title={title} className={classes.title} />
);
return (
<Fragment>
<>
<Helmet title={title} />
<div className={classes.container}>
<div className={classes.leftItemsBox}>
@@ -111,6 +111,6 @@ export const ContentHeader = ({
</div>
<div className={classes.rightItemsBox}>{children}</div>
</div>
</Fragment>
</>
);
};
@@ -16,17 +16,20 @@
import { useApi, configApiRef } from '@backstage/core-plugin-api';
import { BackstageTheme } from '@backstage/theme';
import { makeStyles, Tooltip, Typography } from '@material-ui/core';
import { Box, Grid, makeStyles, Tooltip, Typography } from '@material-ui/core';
import React, { CSSProperties, PropsWithChildren, ReactNode } from 'react';
import { Helmet } from 'react-helmet';
import { Link } from '../../components/Link';
import { Breadcrumbs } from '../Breadcrumbs';
const minHeaderHeight = 118;
const useStyles = makeStyles<BackstageTheme>(theme => ({
header: {
gridArea: 'pageHeader',
padding: theme.spacing(3),
minHeight: 118,
height: 'fit-content',
minHeight: minHeaderHeight,
width: '100%',
boxShadow: '0 0 8px 3px rgba(20, 20, 20, 0.3)',
position: 'relative',
@@ -34,26 +37,21 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'flex-end',
alignItems: 'center',
backgroundImage: theme.page.backgroundImage,
backgroundPosition: 'center',
backgroundSize: 'cover',
},
leftItemsBox: {
flex: '1 1 auto',
maxWidth: '100%',
flexGrow: 1,
marginBottom: theme.spacing(1),
},
rightItemsBox: {
flex: '0 1 auto',
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'center',
marginRight: theme.spacing(1),
width: 'auto',
},
title: {
color: theme.palette.bursts.fontColor,
lineHeight: '1.0em',
wordBreak: 'break-all',
fontSize: 'calc(24px + 6 * ((100vw - 320px) / 680))',
marginBottom: theme.spacing(1),
@@ -199,7 +197,7 @@ export const Header = ({
<>
<Helmet titleTemplate={titleTemplate} defaultTitle={defaultTitle} />
<header style={style} className={classes.header}>
<div className={classes.leftItemsBox}>
<Box className={classes.leftItemsBox}>
<TypeFragment
classes={classes}
type={type}
@@ -212,8 +210,10 @@ export const Header = ({
tooltip={tooltip}
/>
<SubtitleFragment classes={classes} subtitle={subtitle} />
</div>
<div className={classes.rightItemsBox}>{children}</div>
</Box>
<Grid container className={classes.rightItemsBox} spacing={4}>
{children}
</Grid>
</header>
</>
);
@@ -14,29 +14,25 @@
* limitations under the License.
*/
import { Link, makeStyles, Typography } from '@material-ui/core';
import { Link, makeStyles, Typography, Grid } from '@material-ui/core';
import React from 'react';
const useStyles = makeStyles(theme => ({
root: {
textAlign: 'left',
margin: theme.spacing(2),
display: 'inline-block',
},
label: {
color: '#FFFFFF',
color: theme.palette.common.white,
fontWeight: 'bold',
lineHeight: '16px',
letterSpacing: 0,
fontSize: 14,
height: '16px',
marginBottom: 2,
fontSize: theme.typography.fontSize,
marginBottom: theme.spacing(1) / 2,
lineHeight: 1,
},
value: {
color: 'rgba(255, 255, 255, 0.8)',
lineHeight: '16px',
fontSize: 14,
height: '16px',
fontSize: theme.typography.fontSize,
lineHeight: 1,
},
}));
@@ -64,9 +60,11 @@ export const HeaderLabel = ({ label, value, url }: HeaderLabelProps) => {
/>
);
return (
<span className={classes.root}>
<Typography className={classes.label}>{label}</Typography>
{url ? <Link href={url}>{content}</Link> : content}
</span>
<Grid item>
<span className={classes.root}>
<Typography className={classes.label}>{label}</Typography>
{url ? <Link href={url}>{content}</Link> : content}
</span>
</Grid>
);
};
@@ -14,27 +14,22 @@
* limitations under the License.
*/
import React from 'react';
import React, { PropsWithChildren, ComponentProps } from 'react';
import { Header, Page } from '@backstage/core-components';
import { useApi, configApiRef } from '@backstage/core-plugin-api';
import { Header } from '../Header';
import { Page } from './Page';
type Props = {
children?: React.ReactNode;
};
export const ApiExplorerLayout = ({ children }: Props) => {
const configApi = useApi(configApiRef);
const generatedSubtitle = `${
configApi.getOptionalString('organization.name') ?? 'Backstage'
} API Explorer`;
return (
<Page themeId="apis">
<Header
title="APIs"
subtitle={generatedSubtitle}
pageTitleOverride="APIs"
/>
{children}
</Page>
);
type PageWithHeaderProps = ComponentProps<typeof Header> & {
themeId: string;
};
export const PageWithHeader = ({
themeId,
children,
...props
}: PropsWithChildren<PageWithHeaderProps>) => (
<Page themeId={themeId}>
<Header {...props} />
{children}
</Page>
);
@@ -15,3 +15,4 @@
*/
export { Page } from './Page';
export { PageWithHeader } from './PageWithHeader';
@@ -100,11 +100,11 @@ catalog:
# Backstage example templates
- type: url
target: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml
target: https://github.com/backstage/software-templates/blob/master/scaffolder-templates/react-ssr-template/template.yaml
rules:
- allow: [Template]
- type: url
target: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/template.yaml
target: https://github.com/backstage/software-templates/blob/master/scaffolder-templates/springboot-grpc-template/template.yaml
rules:
- allow: [Template]
- type: url
@@ -112,6 +112,6 @@ catalog:
rules:
- allow: [Template]
- type: url
target: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml
target: https://github.com/backstage/software-templates/blob/master/scaffolder-templates/docs-template/template.yaml
rules:
- allow: [Template]
@@ -157,6 +157,13 @@ const websiteEntityPage = (
</EntityLayout>
);
/**
* NOTE: This page is designed to work on small screens such as mobile devices.
* This is based on Material UI Grid. If breakpoints are used, each grid item must set the `xs` prop to a column size or to `true`,
* since this does not default. If no breakpoints are used, the items will equitably share the asvailable space.
* https://material-ui.com/components/grid/#basic-grid.
*/
const defaultEntityPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
+1
View File
@@ -6,6 +6,7 @@
/// <reference types="react" />
import { ApiFactory } from '@backstage/core-plugin-api';
import { AppTheme } from '@backstage/core-plugin-api';
import { ComponentType } from 'react';
import { createPlugin } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
+11
View File
@@ -37,6 +37,7 @@ import {
import {
AnyApiFactory,
ApiFactory,
AppTheme,
attachComponentData,
configApiRef,
createApiFactory,
@@ -79,6 +80,7 @@ class DevAppBuilder {
private readonly sidebarItems = new Array<JSX.Element>();
private defaultPage?: string;
private themes?: Array<AppTheme>;
/**
* Register one or more plugins to render in the dev app
@@ -144,6 +146,14 @@ class DevAppBuilder {
return this;
}
/**
* Adds an array of themes to overide the default theme.
*/
addThemes(themes: AppTheme[]) {
this.themes = themes;
return this;
}
/**
* Build a DevApp component using the resources registered so far
*/
@@ -166,6 +176,7 @@ class DevAppBuilder {
const app = createApp({
apis,
plugins: this.plugins,
themes: this.themes,
bindRoutes: ({ bind }) => {
for (const plugin of this.plugins ?? []) {
const targets: Record<string, RouteRef<any>> = {};
@@ -0,0 +1,3 @@
site_name: Test site name
site_description: Test site description
docs_dir: docs/
@@ -47,6 +47,9 @@ const mkdocsYmlWithExtensions = fs.readFileSync(
const mkdocsYmlWithRepoUrl = fs.readFileSync(
resolvePath(__filename, '../__fixtures__/mkdocs_with_repo_url.yml'),
);
const mkdocsYmlWithValidDocDir = fs.readFileSync(
resolvePath(__filename, '../__fixtures__/mkdocs_valid_doc_dir.yml'),
);
const mkdocsYmlWithInvalidDocDir = fs.readFileSync(
resolvePath(__filename, '../__fixtures__/mkdocs_invalid_doc_dir.yml'),
);
@@ -336,6 +339,7 @@ describe('helpers', () => {
mockFs({
'/mkdocs.yml': mkdocsYml,
'/mkdocs_with_extensions.yml': mkdocsYmlWithExtensions,
'/mkdocs_valid_doc_dir.yml': mkdocsYmlWithValidDocDir,
'/mkdocs_invalid_doc_dir.yml': mkdocsYmlWithInvalidDocDir,
});
});
@@ -351,6 +355,12 @@ describe('helpers', () => {
).resolves.toBeUndefined();
});
it('should return true on when a valid docs_dir is present', async () => {
await expect(
validateMkdocsYaml(inputDir, '/mkdocs_valid_doc_dir.yml'),
).resolves.toBeUndefined();
});
it('should return false on absolute doc_dir path', async () => {
await expect(
validateMkdocsYaml(inputDir, '/mkdocs_invalid_doc_dir.yml'),
@@ -19,6 +19,7 @@ import { isChildPath } from '@backstage/backend-common';
import { spawn } from 'child_process';
import fs from 'fs-extra';
import yaml, { DEFAULT_SCHEMA, Type } from 'js-yaml';
import { resolve as resolvePath } from 'path';
import { PassThrough, Writable } from 'stream';
import { Logger } from 'winston';
import { ParsedLocationAnnotation } from '../../helpers';
@@ -178,7 +179,10 @@ export const validateMkdocsYaml = async (
schema: MKDOCS_SCHEMA,
});
if (mkdocsYml.docs_dir && !isChildPath(inputDir, mkdocsYml.docs_dir)) {
if (
mkdocsYml.docs_dir &&
!isChildPath(inputDir, resolvePath(inputDir, mkdocsYml.docs_dir))
) {
throw new Error(
`docs_dir configuration value in mkdocs can't be an absolute directory or start with ../ for security reasons.
Use relative paths instead which are resolved relative to your mkdocs.yml file location.`,
@@ -132,7 +132,8 @@ describe('readGeneratorConfig', () => {
runIn: 'local',
});
expect(logger.warn).toHaveBeenCalledWith(
`The 'techdocs.generators.techdocs' configuration key is deprecated and will be removed in the future. Please use 'techdocs.generator' instead.`,
`The 'techdocs.generators.techdocs' configuration key is deprecated and will be removed in the future. Please use 'techdocs.generator' instead. ` +
`See here https://backstage.io/docs/features/techdocs/configuration`,
);
});
});
@@ -168,7 +168,8 @@ export function readGeneratorConfig(
if (legacyGeneratorType) {
logger.warn(
`The 'techdocs.generators.techdocs' configuration key is deprecated and will be removed in the future. Please use 'techdocs.generator' instead.`,
`The 'techdocs.generators.techdocs' configuration key is deprecated and will be removed in the future. Please use 'techdocs.generator' instead. ` +
`See here https://backstage.io/docs/features/techdocs/configuration`,
);
}
+7 -8
View File
@@ -15,16 +15,15 @@ import { RouteRef } from '@backstage/core-plugin-api';
import { StorageApi } from '@backstage/core-plugin-api';
import { StorageValueChange } from '@backstage/core-plugin-api';
// Warning: (ae-forgotten-export) The symbol "Breakpoint" needs to be exported by the entry point index.d.ts
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// Warning: (ae-missing-release-tag) "mockBreakpoint" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function mockBreakpoint(
initialBreakpoint?: Breakpoint,
): {
set(breakpoint: Breakpoint): void;
remove(): void;
};
// @public
export function mockBreakpoint({
matches,
}: {
matches?: boolean | undefined;
}): void;
// Warning: (ae-missing-release-tag) "MockErrorApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -14,80 +14,30 @@
* limitations under the License.
*/
import { act } from '@testing-library/react';
type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
const queryToBreakpoint = {
'(min-width:1920px)': 'xl',
'(min-width:1280px)': 'lg',
'(min-width:960px)': 'md',
'(min-width:600px)': 'sm',
'(min-width:0px)': 'xs',
} as Record<string, Breakpoint>;
function toBreakpoint(query: string) {
const breakpoint = queryToBreakpoint[query];
if (!breakpoint) {
throw new Error(
`received unknown media query in breakpoint mock: '${query}'`,
);
}
return breakpoint;
}
type Listener = (event: { matches: boolean }) => void;
interface QueryList {
addListener(listener: Listener): void;
removeListener(listener: Listener): void;
matches: boolean;
}
interface Query {
query: string;
queryList: QueryList;
listeners: Set<Listener>;
}
export default function mockBreakpoint(initialBreakpoint: Breakpoint = 'xl') {
let currentBreakpoint = initialBreakpoint;
const queries = Array<Query>();
const previousMatchMedia: any = (window as any).matchMedia;
(window as any).matchMedia = (query: string): QueryList => {
const listeners = new Set<Listener>();
const queryList: QueryList = {
addListener(listener) {
listeners.add(listener);
},
removeListener(listener) {
listeners.delete(listener);
},
matches: toBreakpoint(query) === currentBreakpoint,
};
queries.push({ query, queryList, listeners });
return queryList;
};
return {
set(breakpoint: Breakpoint) {
currentBreakpoint = breakpoint;
act(() => {
queries.forEach(({ query, queryList, listeners }) => {
const matches = toBreakpoint(query) === breakpoint;
queryList.matches = matches;
listeners.forEach(listener => listener({ matches }));
});
});
},
remove() {
(window as any).matchMedia = previousMatchMedia;
},
};
/**
* This is a mocking method suggested in the Jest Doc's, as it is not implemented in JSDOM yet.
* It can be used to mock values when the MUI `useMediaQuery` hook if it is used in a tested component.
*
* For issues checkout the documentation:
* https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
*
* If there are any updates from MUI React on testing `useMediaQuery` this mock should be replaced
* https://material-ui.com/components/use-media-query/#testing
*
* @param matchMediaOptions
*/
export default function mockBreakpoint({ matches = false }) {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: matches,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
}
+9 -7
View File
@@ -59,15 +59,17 @@ const apiPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
<Grid item md={6}>
<Grid item xs={12} md={6}>
<EntityAboutCard />
</Grid>
<Grid container item md={12}>
<Grid item md={6}>
<EntityProvidingComponentsCard />
</Grid>
<Grid item md={6}>
<EntityConsumingComponentsCard />
<Grid container>
<Grid item md={12}>
<Grid item xs={12} md={6}>
<EntityProvidingComponentsCard />
</Grid>
<Grid item xs={12} md={6}>
<EntityConsumingComponentsCard />
</Grid>
</Grid>
</Grid>
</Grid>
+4 -2
View File
@@ -52,14 +52,16 @@ const apiDocsPlugin: BackstagePlugin<
export { apiDocsPlugin };
export { apiDocsPlugin as plugin };
// Warning: (ae-forgotten-export) The symbol "ApiExplorerPageProps" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "ApiExplorerPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const ApiExplorerPage: ({
initiallySelectedFilter,
columns,
}: ApiExplorerPageProps) => JSX.Element;
}: {
initiallySelectedFilter?: UserListFilterKind | undefined;
columns?: TableColumn<CatalogTableRow>[] | undefined;
}) => JSX.Element;
// Warning: (ae-missing-release-tag) "ApiTypeTitle" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -14,6 +14,21 @@
* limitations under the License.
*/
import {
Content,
ContentHeader,
PageWithHeader,
SupportButton,
TableColumn,
} from '@backstage/core-components';
import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
import {
CatalogTable,
CatalogTableRow,
FilteredEntityLayout,
EntityListContainer,
FilterContainer,
} from '@backstage/plugin-catalog';
import {
EntityKindPicker,
EntityLifecyclePicker,
@@ -24,29 +39,10 @@ import {
UserListFilterKind,
UserListPicker,
} from '@backstage/plugin-catalog-react';
import { CatalogTable, CatalogTableRow } from '@backstage/plugin-catalog';
import { Button, makeStyles } from '@material-ui/core';
import { Button } from '@material-ui/core';
import React from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { createComponentRouteRef } from '../../routes';
import { ApiExplorerLayout } from './ApiExplorerLayout';
import {
Content,
ContentHeader,
SupportButton,
TableColumn,
} from '@backstage/core-components';
import { useRouteRef } from '@backstage/core-plugin-api';
const useStyles = makeStyles(theme => ({
contentWrapper: {
display: 'grid',
gridTemplateAreas: "'filters' 'table'",
gridTemplateColumns: '250px 1fr',
gridColumnGap: theme.spacing(2),
},
}));
const defaultColumns: TableColumn<CatalogTableRow>[] = [
CatalogTable.columns.createNameColumn({ defaultKind: 'API' }),
@@ -58,7 +54,7 @@ const defaultColumns: TableColumn<CatalogTableRow>[] = [
CatalogTable.columns.createTagsColumn(),
];
export type ApiExplorerPageProps = {
type ApiExplorerPageProps = {
initiallySelectedFilter?: UserListFilterKind;
columns?: TableColumn<CatalogTableRow>[];
};
@@ -67,11 +63,19 @@ export const ApiExplorerPage = ({
initiallySelectedFilter = 'all',
columns,
}: ApiExplorerPageProps) => {
const styles = useStyles();
const createComponentLink = useRouteRef(createComponentRouteRef);
const configApi = useApi(configApiRef);
const generatedSubtitle = `${
configApi.getOptionalString('organization.name') ?? 'Backstage'
} API Explorer`;
return (
<ApiExplorerLayout>
<PageWithHeader
themeId="apis"
title="APIs"
subtitle={generatedSubtitle}
pageTitleOverride="APIs"
>
<Content>
<ContentHeader title="">
{createComponentLink && (
@@ -86,20 +90,22 @@ export const ApiExplorerPage = ({
)}
<SupportButton>All your APIs</SupportButton>
</ContentHeader>
<div className={styles.contentWrapper}>
<EntityListProvider>
<div>
<EntityListProvider>
<FilteredEntityLayout>
<FilterContainer>
<EntityKindPicker initialFilter="api" hidden />
<EntityTypePicker />
<UserListPicker initialFilter={initiallySelectedFilter} />
<EntityOwnerPicker />
<EntityLifecyclePicker />
<EntityTagPicker />
</div>
<CatalogTable columns={columns || defaultColumns} />
</EntityListProvider>
</div>
</FilterContainer>
<EntityListContainer>
<CatalogTable columns={columns || defaultColumns} />
</EntityListContainer>
</FilteredEntityLayout>
</EntityListProvider>
</Content>
</ApiExplorerLayout>
</PageWithHeader>
);
};
+12 -1
View File
@@ -346,6 +346,16 @@ export interface RouterOptions {
providerFactories?: ProviderFactories;
}
// Warning: (ae-missing-release-tag) "TokenIssuer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export type TokenIssuer = {
issueToken(params: TokenParams): Promise<string>;
listPublicKeys(): Promise<{
keys: AnyJWK[];
}>;
};
// Warning: (ae-missing-release-tag) "verifyNonce" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -366,9 +376,10 @@ export type WebMessageResponse =
// Warnings were encountered during analysis:
//
// src/identity/types.d.ts:25:5 - (ae-forgotten-export) The symbol "TokenParams" needs to be exported by the entry point index.d.ts
// src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts
// src/providers/google/provider.d.ts:36:5 - (ae-forgotten-export) The symbol "AuthHandler" needs to be exported by the entry point index.d.ts
// src/providers/types.d.ts:105:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts
// src/providers/types.d.ts:108:5 - (ae-forgotten-export) The symbol "TokenIssuer" needs to be exported by the entry point index.d.ts
// src/providers/types.d.ts:111:5 - (ae-forgotten-export) The symbol "ExperimentalIdentityResolver" needs to be exported by the entry point index.d.ts
// src/providers/types.d.ts:128:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative
+1
View File
@@ -16,6 +16,7 @@
export * from './service/router';
export { IdentityClient } from './identity';
export type { TokenIssuer } from './identity';
export * from './providers';
// flow package provides 2 functions
@@ -14,12 +14,12 @@
* limitations under the License.
*/
import { Grid, Typography } from '@material-ui/core';
import { Chip, Grid, Typography } from '@material-ui/core';
import React from 'react';
import { ImportStepper } from './ImportStepper';
import { StepperProviderOpts } from './ImportStepper/defaults';
import { ConfigApi, configApiRef, useApi } from '@backstage/core-plugin-api';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
import {
Content,
ContentHeader,
@@ -29,30 +29,12 @@ import {
SupportButton,
} from '@backstage/core-components';
function repositories(configApi: ConfigApi): string[] {
const integrations = configApi.getConfig('integrations');
const repos = [];
if (integrations.has('github')) {
repos.push('GitHub');
}
if (integrations.has('bitbucket')) {
repos.push('Bitbucket');
}
if (integrations.has('gitlab')) {
repos.push('GitLab');
}
if (integrations.has('azure')) {
repos.push('Azure');
}
return repos;
}
export const ImportComponentPage = (opts: StepperProviderOpts) => {
const configApi = useApi(configApiRef);
const appTitle = configApi.getOptional('app.title') || 'Backstage';
const repos = repositories(configApi);
const repositoryString = repos.join(', ').replace(/, (\w*)$/, ' or $1');
const integrations = configApi.getConfig('integrations');
const hasGithubIntegration = integrations.has('github');
return (
<Page themeId="home">
@@ -76,7 +58,8 @@ export const ImportComponentPage = (opts: StepperProviderOpts) => {
}}
>
<Typography variant="body2" paragraph>
Enter the URL to your SCM repository to add it to {appTitle}.
Enter the URL to your source code repository to add it to{' '}
{appTitle}.
</Typography>
<Typography variant="h6">
Link to an existing entity file
@@ -91,10 +74,11 @@ export const ImportComponentPage = (opts: StepperProviderOpts) => {
The wizard analyzes the file, previews the entities, and adds
them to the {appTitle} catalog.
</Typography>
{repos.length > 0 && (
{hasGithubIntegration && (
<>
<Typography variant="h6">
Link to a {repositoryString} repository
Link to a repository{' '}
<Chip label="GitHub only" variant="outlined" size="small" />
</Typography>
<Typography
variant="subtitle2"
@@ -52,8 +52,8 @@ export const MockEntityListContextProvider = ({
const defaultContext: EntityListContextProps = {
entities: [],
backendEntities: [],
updateFilters: updateFilters,
filters: filters,
updateFilters,
filters,
loading: false,
queryParameters: {},
};
+80 -9
View File
@@ -5,12 +5,21 @@
```ts
/// <reference types="react" />
import { AddLocationRequest } from '@backstage/catalog-client';
import { AddLocationResponse } from '@backstage/catalog-client';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { CatalogApi } from '@backstage/catalog-client';
import { CatalogClient } from '@backstage/catalog-client';
import { CatalogEntitiesRequest } from '@backstage/catalog-client';
import { CatalogListResponse } from '@backstage/catalog-client';
import { CatalogRequestOptions } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import { EntityName } from '@backstage/catalog-model';
import { ExternalRouteRef } from '@backstage/core-plugin-api';
import { IconComponent } from '@backstage/core-plugin-api';
import { IdentityApi } from '@backstage/core-plugin-api';
import { InfoCardVariants } from '@backstage/core-components';
import { Location as Location_2 } from '@backstage/catalog-model';
import { PropsWithChildren } from 'react';
import { default as React_2 } from 'react';
import { ReactNode } from 'react';
@@ -30,7 +39,7 @@ export function AboutCard({ variant }: AboutCardProps): JSX.Element;
// Warning: (ae-missing-release-tag) "AboutContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const AboutContent: ({ entity }: Props_2) => JSX.Element;
export const AboutContent: ({ entity }: Props) => JSX.Element;
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "AboutField" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -41,7 +50,54 @@ export const AboutField: ({
value,
gridSizes,
children,
}: Props_3) => JSX.Element;
}: Props_2) => JSX.Element;
// Warning: (ae-missing-release-tag) "CatalogClientWrapper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export class CatalogClientWrapper implements CatalogApi {
constructor(options: { client: CatalogClient; identityApi: IdentityApi });
// (undocumented)
addLocation(
request: AddLocationRequest,
options?: CatalogRequestOptions,
): Promise<AddLocationResponse>;
// (undocumented)
getEntities(
request?: CatalogEntitiesRequest,
options?: CatalogRequestOptions,
): Promise<CatalogListResponse<Entity>>;
// (undocumented)
getEntityByName(
compoundName: EntityName,
options?: CatalogRequestOptions,
): Promise<Entity | undefined>;
// (undocumented)
getLocationByEntity(
entity: Entity,
options?: CatalogRequestOptions,
): Promise<Location_2 | undefined>;
// (undocumented)
getLocationById(
id: string,
options?: CatalogRequestOptions,
): Promise<Location_2 | undefined>;
// (undocumented)
getOriginLocationByEntity(
entity: Entity,
options?: CatalogRequestOptions,
): Promise<Location_2 | undefined>;
// (undocumented)
removeEntityByUid(
uid: string,
options?: CatalogRequestOptions,
): Promise<void>;
// (undocumented)
removeLocationById(
id: string,
options?: CatalogRequestOptions,
): Promise<void>;
}
// Warning: (ae-missing-release-tag) "CatalogEntityPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -53,17 +109,11 @@ export const CatalogEntityPage: () => JSX.Element;
//
// @public (undocumented)
export const CatalogIndexPage: ({
initiallySelectedFilter,
columns,
actions,
initiallySelectedFilter,
}: CatalogPageProps) => JSX.Element;
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "CatalogLayout" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const CatalogLayout: ({ children }: Props) => JSX.Element;
// Warning: (ae-missing-release-tag) "catalogPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -249,6 +299,13 @@ export const EntityLinksCard: ({
variant?: 'gridItem' | undefined;
}) => JSX.Element;
// Warning: (ae-missing-release-tag) "EntityListContainer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const EntityListContainer: ({
children,
}: PropsWithChildren<{}>) => JSX.Element;
// Warning: (ae-missing-release-tag) "EntityOrphanWarning" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
@@ -287,6 +344,20 @@ export const EntitySwitch: {
// @public (undocumented)
export const EntitySystemDiagramCard: SystemDiagramCard;
// Warning: (ae-missing-release-tag) "FilterContainer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const FilterContainer: ({
children,
}: PropsWithChildren<{}>) => JSX.Element;
// Warning: (ae-missing-release-tag) "FilteredEntityLayout" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const FilteredEntityLayout: ({
children,
}: PropsWithChildren<{}>) => JSX.Element;
// Warning: (ae-missing-release-tag) "isComponentType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
+2 -5
View File
@@ -19,16 +19,13 @@ import {
AddLocationRequest,
AddLocationResponse,
CatalogApi,
CatalogClient,
CatalogEntitiesRequest,
CatalogListResponse,
CatalogClient,
CatalogRequestOptions,
} from '@backstage/catalog-client';
import { IdentityApi } from '@backstage/core-plugin-api';
type CatalogRequestOptions = {
token?: string;
};
/**
* CatalogClient wrapper that injects identity token for all requests
*/
@@ -1,42 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
import { Header, Page } from '@backstage/core-components';
type Props = {
children?: React.ReactNode;
};
export const CatalogLayout = ({ children }: Props) => {
const orgName =
useApi(configApiRef).getOptionalString('organization.name') ?? 'Backstage';
return (
<Page themeId="home">
<Header
title={`${orgName} Catalog`}
subtitle={`Catalog of software components at ${orgName}`}
pageTitleOverride="Home"
/>
{children}
</Page>
);
};
export default CatalogLayout;
@@ -26,6 +26,7 @@ import {
MockStorageApi,
renderWithEffects,
wrapInTestApp,
mockBreakpoint,
} from '@backstage/test-utils';
import { fireEvent, screen } from '@testing-library/react';
import React from 'react';
@@ -244,4 +245,13 @@ describe('CatalogPage', () => {
screen.findByText(/Starred \(1\)/),
).resolves.toBeInTheDocument();
});
it('should wrap filter in drawer on smaller screens', async () => {
mockBreakpoint({ matches: true });
const { getByRole } = await renderWrapped(<CatalogPage />);
const button = getByRole('button', { name: 'Filters' });
expect(getByRole('presentation', { hidden: true })).toBeInTheDocument();
fireEvent.click(button);
expect(getByRole('presentation')).toBeVisible();
});
});
@@ -14,8 +14,15 @@
* limitations under the License.
*/
import React from 'react';
import { Grid } from '@material-ui/core';
import {
Content,
ContentHeader,
PageWithHeader,
SupportButton,
TableColumn,
TableProps,
} from '@backstage/core-components';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
import {
EntityKindPicker,
EntityLifecyclePicker,
@@ -26,18 +33,15 @@ import {
UserListFilterKind,
UserListPicker,
} from '@backstage/plugin-catalog-react';
import React from 'react';
import { CatalogTable } from '../CatalogTable';
import { EntityRow } from '../CatalogTable/types';
import CatalogLayout from './CatalogLayout';
import { CreateComponentButton } from '../CreateComponentButton';
import {
Content,
ContentHeader,
SupportButton,
TableColumn,
TableProps,
} from '@backstage/core-components';
FilteredEntityLayout,
EntityListContainer,
FilterContainer,
} from '../FilteredEntityLayout';
export type CatalogPageProps = {
initiallySelectedFilter?: UserListFilterKind;
@@ -46,39 +50,36 @@ export type CatalogPageProps = {
};
export const CatalogPage = ({
initiallySelectedFilter = 'owned',
columns,
actions,
}: CatalogPageProps) => (
<CatalogLayout>
<Content>
<ContentHeader title="Components">
<CreateComponentButton />
<SupportButton>All your software catalog entities</SupportButton>
</ContentHeader>
<Grid container spacing={2}>
initiallySelectedFilter = 'owned',
}: CatalogPageProps) => {
const orgName =
useApi(configApiRef).getOptionalString('organization.name') ?? 'Backstage';
return (
<PageWithHeader title={`${orgName} Catalog`} themeId="home">
<Content>
<ContentHeader title="Components">
<CreateComponentButton />
<SupportButton>All your software catalog entities</SupportButton>
</ContentHeader>
<EntityListProvider>
<Grid item sm={12} lg={2} alignContent="flex-start">
<Grid container>
<Grid item xs={12} sm={4} lg={12}>
<EntityKindPicker initialFilter="component" hidden />
<EntityTypePicker />
</Grid>
<Grid item xs={12} sm={4} lg={12}>
<UserListPicker initialFilter={initiallySelectedFilter} />
</Grid>
<Grid item xs={12} sm={4} lg={12}>
<EntityOwnerPicker />
<EntityLifecyclePicker />
<EntityTagPicker />
</Grid>
</Grid>
</Grid>
<Grid item xs={12} sm={12} lg={10}>
<CatalogTable columns={columns} actions={actions} />
</Grid>
<FilteredEntityLayout>
<FilterContainer>
<EntityKindPicker initialFilter="component" hidden />
<EntityTypePicker />
<UserListPicker initialFilter={initiallySelectedFilter} />
<EntityOwnerPicker />
<EntityLifecyclePicker />
<EntityTagPicker />
</FilterContainer>
<EntityListContainer>
<CatalogTable columns={columns} actions={actions} />
</EntityListContainer>
</FilteredEntityLayout>
</EntityListProvider>
</Grid>
</Content>
</CatalogLayout>
);
</Content>
</PageWithHeader>
);
};
@@ -13,5 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { CatalogLayout } from './CatalogLayout';
export { CatalogPage } from './CatalogPage';
@@ -23,9 +23,7 @@ import { useRouteRef } from '@backstage/core-plugin-api';
export const CreateComponentButton = () => {
const createComponentLink = useRouteRef(createComponentRouteRef);
if (!createComponentLink) return null;
return (
return createComponentLink ? (
<Button
component={RouterLink}
variant="contained"
@@ -34,5 +32,5 @@ export const CreateComponentButton = () => {
>
Create Component
</Button>
);
) : null;
};
@@ -13,4 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { CreateComponentButton } from './CreateComponentButton';
@@ -20,9 +20,17 @@ import {
RELATION_OWNED_BY,
} from '@backstage/catalog-model';
import {
useElementFilter,
Content,
Header,
HeaderLabel,
Page,
Progress,
RoutedTabs,
} from '@backstage/core-components';
import {
attachComponentData,
IconComponent,
useElementFilter,
} from '@backstage/core-plugin-api';
import {
EntityContext,
@@ -37,14 +45,6 @@ import { useNavigate } from 'react-router';
import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu';
import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity';
import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog';
import {
Content,
Header,
HeaderLabel,
Page,
Progress,
RoutedTabs,
} from '@backstage/core-components';
type SubRoute = {
path: string;
@@ -68,12 +68,21 @@ const EntityLayoutTitle = ({
}: {
title: string;
entity: Entity | undefined;
}) => (
<Box display="inline-flex" alignItems="center" height="1em">
{title}
{entity && <FavouriteEntity entity={entity} />}
</Box>
);
}) => {
return (
<Box display="inline-flex" alignItems="center" height="1em" maxWidth="100%">
<Box
component="span"
textOverflow="ellipsis"
whiteSpace="nowrap"
overflow="hidden"
>
{title}
</Box>
{entity && <FavouriteEntity entity={entity} />}
</Box>
);
};
const headerProps = (
paramKind: string | undefined,
@@ -0,0 +1,24 @@
/*
* 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 { Grid } from '@material-ui/core';
import React, { PropsWithChildren } from 'react';
export const EntityListContainer = ({ children }: PropsWithChildren<{}>) => (
<Grid item xs={12} lg={10}>
{children}
</Grid>
);
@@ -0,0 +1,71 @@
/*
* 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 { BackstageTheme } from '@backstage/theme';
import {
Box,
Button,
Drawer,
Grid,
Typography,
useMediaQuery,
useTheme,
} from '@material-ui/core';
import FilterListIcon from '@material-ui/icons/FilterList';
import React, { useState, PropsWithChildren } from 'react';
export const FilterContainer = ({ children }: PropsWithChildren<{}>) => {
const isMidSizeScreen = useMediaQuery<BackstageTheme>(theme =>
theme.breakpoints.down('md'),
);
const theme = useTheme<BackstageTheme>();
const [filterDrawerOpen, setFilterDrawerOpen] = useState<boolean>(false);
return isMidSizeScreen ? (
<>
<Button
style={{ marginTop: theme.spacing(1), marginLeft: theme.spacing(1) }}
onClick={() => setFilterDrawerOpen(true)}
startIcon={<FilterListIcon />}
>
Filters
</Button>
<Drawer
open={filterDrawerOpen}
onClose={() => setFilterDrawerOpen(false)}
anchor="left"
disableAutoFocus
keepMounted
variant="temporary"
>
<Box m={2}>
<Typography
variant="h6"
component="h2"
style={{ marginBottom: theme.spacing(1) }}
>
Filters
</Typography>
{children}
</Box>
</Drawer>
</>
) : (
<Grid item lg={2}>
{children}
</Grid>
);
};
@@ -0,0 +1,24 @@
/*
* 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 { Grid } from '@material-ui/core';
import React, { PropsWithChildren } from 'react';
export const FilteredEntityLayout = ({ children }: PropsWithChildren<{}>) => (
<Grid container style={{ position: 'relative' }}>
{children}
</Grid>
);
@@ -0,0 +1,19 @@
/*
* 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.
*/
export { FilteredEntityLayout } from './FilteredEntityLayout';
export { FilterContainer } from './FilterContainer';
export { EntityListContainer } from './EntityListContainer';
+8 -7
View File
@@ -14,16 +14,18 @@
* limitations under the License.
*/
export { CatalogClientWrapper } from './CatalogClientWrapper';
export * from './components/AboutCard';
export { CatalogLayout } from './components/CatalogPage';
export { CatalogResultListItem } from './components/CatalogResultListItem';
export * from './components/CatalogResultListItem';
export { CatalogTable } from './components/CatalogTable';
export type { EntityRow as CatalogTableRow } from './components/CatalogTable';
export { CreateComponentButton } from './components/CreateComponentButton';
export { EntityLayout } from './components/EntityLayout';
export * from './components/CatalogTable/columns';
export * from './components/CreateComponentButton';
export * from './components/EntityLayout';
export * from './components/EntityOrphanWarning';
export { EntityPageLayout } from './components/EntityPageLayout';
export * from './components/EntityPageLayout';
export * from './components/EntitySwitch';
export * from './components/FilteredEntityLayout';
export { Router } from './components/Router';
export {
CatalogEntityPage,
@@ -31,8 +33,8 @@ export {
catalogPlugin,
catalogPlugin as plugin,
EntityAboutCard,
EntityDependsOnComponentsCard,
EntityDependencyOfComponentsCard,
EntityDependsOnComponentsCard,
EntityDependsOnResourcesCard,
EntityHasComponentsCard,
EntityHasResourcesCard,
@@ -41,4 +43,3 @@ export {
EntityLinksCard,
EntitySystemDiagramCard,
} from './plugin';
export * from './components/CatalogTable/columns';
@@ -7,6 +7,7 @@
import { ApiRef } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { default as React_2 } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
// Warning: (ae-forgotten-export) The symbol "GitReleaseApi" needs to be exported by the entry point index.d.ts
+60 -16
View File
@@ -16,7 +16,7 @@
import React from 'react';
import { createDevApp } from '@backstage/dev-utils';
import { Box, Typography } from '@material-ui/core';
import { Box, Button, Typography } from '@material-ui/core';
import { gitReleaseManagerPlugin, GitReleaseManagerPage } from '../src/plugin';
import { InfoCardPlus } from '../src/components/InfoCardPlus';
@@ -44,10 +44,16 @@ createDevApp()
<Box padding={4}>
<InfoCardPlus>
<Typography variant="h4">Dev notes</Typography>
<Typography>
Configure plugin statically by passing props to the
`GitHubReleaseManagerPage` component
</Typography>
<Typography variant="body2">
Note that the static configuration points towards private
repositories and will thus not work for everyone.
</Typography>
</InfoCardPlus>
<GitReleaseManagerPage
@@ -67,8 +73,10 @@ createDevApp()
<Box padding={4}>
<InfoCardPlus>
<Typography variant="h4">Dev notes</Typography>
<Typography>Each feature can be omitted</Typography>
<Typography>Success callbacks can also be added</Typography>
<Typography>
Each feature can be individually omitted as well as have success
callback attached to them
</Typography>
</InfoCardPlus>
<GitReleaseManagerPage
@@ -79,22 +87,12 @@ createDevApp()
}}
features={{
createRc: {
onSuccess: ({
comparisonUrl,
createdTag,
gitReleaseName,
gitReleaseUrl,
previousTag,
}) => {
onSuccess: args => {
// eslint-disable-next-line no-console
console.log(
'Custom success callback for Create RC',
comparisonUrl,
createdTag,
gitReleaseName,
gitReleaseUrl,
previousTag,
'Custom success callback for Create RC with the following args',
);
console.log(JSON.stringify(args, null, 2)); // eslint-disable-line no-console
},
},
promoteRc: {
@@ -108,4 +106,50 @@ createDevApp()
</Box>
),
})
.addPage({
title: 'Custom',
path: '/custom',
element: (
<Box padding={4}>
<InfoCardPlus>
<Typography variant="h4">Dev notes</Typography>
<Typography>
The custom feature's return value can either be a React Element or
an array of React Elements.
</Typography>
</InfoCardPlus>
<GitReleaseManagerPage
project={{
owner: 'eengervall-playground',
repo: 'playground-semver',
versioningStrategy: 'semver',
}}
features={{
custom: {
factory: args => {
return (
<InfoCardPlus>
<Typography variant="h4">I'm a custom feature</Typography>
<Button
variant="contained"
color="primary"
onClick={() => {
console.log(`Here's my args 🚀`); // eslint-disable-line no-console
console.log(JSON.stringify(args, null, 2)); // eslint-disable-line no-console
}}
>
View the arguments for this feature in the console by
pressing this button
</Button>
</InfoCardPlus>
);
},
},
}}
/>
</Box>
),
})
.render();
+3 -2
View File
@@ -28,12 +28,13 @@
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@octokit/rest": "^18.5.3",
"@types/react": "^16.9",
"luxon": "^1.26.0",
"qs": "^6.10.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router": "6.0.0-beta.0",
"react-use": "^17.2.4",
"react": "^16.13.1",
"recharts": "^1.8.5"
},
"devDependencies": {
@@ -42,8 +43,8 @@
"@backstage/dev-utils": "^0.2.2",
"@backstage/test-utils": "^0.1.14",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/react-hooks": "^3.4.2",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^13.1.8",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32",
@@ -18,12 +18,14 @@ import React from 'react';
import { useAsync } from 'react-use';
import { Alert } from '@material-ui/lab';
import { Box } from '@material-ui/core';
import { useApi } from '@backstage/core-plugin-api';
import { ContentHeader, Progress } from '@backstage/core-components';
import {
ComponentConfig,
ComponentConfigCreateRc,
ComponentConfigPatch,
ComponentConfigPromoteRc,
CreateRcOnSuccessArgs,
PatchOnSuccessArgs,
PromoteRcOnSuccessArgs,
} from './types/types';
import { Features } from './features/Features';
import { gitReleaseManagerApiRef } from './api/serviceApiRef';
@@ -33,18 +35,33 @@ import { ProjectContext, Project } from './contexts/ProjectContext';
import { RepoDetailsForm } from './features/RepoDetailsForm/RepoDetailsForm';
import { useQueryHandler } from './hooks/useQueryHandler';
import { UserContext } from './contexts/UserContext';
import { useApi } from '@backstage/core-plugin-api';
import { ContentHeader, Progress } from '@backstage/core-components';
import {
GetBranchResult,
GetLatestReleaseResult,
GetRepositoryResult,
} from './api/GitReleaseClient';
interface GitReleaseManagerProps {
project?: Omit<Project, 'isProvidedViaProps'>;
features?: {
info?: Pick<ComponentConfig<void>, 'omit'>;
stats?: Pick<ComponentConfig<void>, 'omit'>;
createRc?: ComponentConfigCreateRc;
promoteRc?: ComponentConfigPromoteRc;
patch?: ComponentConfigPatch;
createRc?: ComponentConfig<CreateRcOnSuccessArgs>;
promoteRc?: ComponentConfig<PromoteRcOnSuccessArgs>;
patch?: ComponentConfig<PatchOnSuccessArgs>;
custom?: {
factory: ({
latestRelease,
project,
releaseBranch,
repository,
}: {
latestRelease: GetLatestReleaseResult['latestRelease'] | null;
project: Project;
releaseBranch: GetBranchResult['branch'] | null;
repository: GetRepositoryResult['repository'];
}) => React.ReactElement | React.ReactElement[];
};
};
}
@@ -31,7 +31,7 @@ import {
GetLatestReleaseResult,
GetRepositoryResult,
} from '../../api/GitReleaseClient';
import { ComponentConfigCreateRc } from '../../types/types';
import { ComponentConfig, CreateRcOnSuccessArgs } from '../../types/types';
import { Differ } from '../../components/Differ';
import { getReleaseCandidateGitInfo } from '../../helpers/getReleaseCandidateGitInfo';
import { InfoCardPlus } from '../../components/InfoCardPlus';
@@ -45,7 +45,7 @@ interface CreateReleaseCandidateProps {
defaultBranch: GetRepositoryResult['repository']['defaultBranch'];
latestRelease: GetLatestReleaseResult['latestRelease'];
releaseBranch: GetBranchResult['branch'] | null;
onSuccess?: ComponentConfigCreateRc['onSuccess'];
onSuccess?: ComponentConfig<CreateRcOnSuccessArgs>['onSuccess'];
}
const InfoCardPlusWrapper = ({ children }: { children: React.ReactNode }) => {
@@ -21,7 +21,11 @@ import {
GetRepositoryResult,
} from '../../../api/GitReleaseClient';
import { CardHook, ComponentConfigCreateRc } from '../../../types/types';
import {
CardHook,
ComponentConfig,
CreateRcOnSuccessArgs,
} from '../../../types/types';
import { getReleaseCandidateGitInfo } from '../../../helpers/getReleaseCandidateGitInfo';
import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef';
import { GitReleaseManagerError } from '../../../errors/GitReleaseManagerError';
@@ -31,12 +35,12 @@ import { useResponseSteps } from '../../../hooks/useResponseSteps';
import { useUserContext } from '../../../contexts/UserContext';
import { useApi } from '@backstage/core-plugin-api';
interface UseCreateReleaseCandidate {
export interface UseCreateReleaseCandidate {
defaultBranch: GetRepositoryResult['repository']['defaultBranch'];
latestRelease: GetLatestReleaseResult['latestRelease'];
releaseCandidateGitInfo: ReturnType<typeof getReleaseCandidateGitInfo>;
project: Project;
onSuccess?: ComponentConfigCreateRc['onSuccess'];
onSuccess?: ComponentConfig<CreateRcOnSuccessArgs>['onSuccess'];
}
export function useCreateReleaseCandidate({
@@ -266,6 +270,12 @@ export function useCreateReleaseCandidate({
try {
await onSuccess({
input: {
defaultBranch,
latestRelease,
releaseCandidateGitInfo,
project,
},
comparisonUrl: getComparisonRes.value.htmlUrl,
createdTag: createReleaseRes.value.tagName,
gitReleaseName: createReleaseRes.value.name,
@@ -142,6 +142,14 @@ export function Features({
onSuccess={features?.patch?.onSuccess}
/>
)}
{features?.custom?.factory &&
features.custom.factory({
latestRelease: gitBatchInfo.value.latestRelease,
project,
releaseBranch: gitBatchInfo.value.releaseBranch,
repository: gitBatchInfo.value.repository,
})}
</ErrorBoundary>
</RefetchContext.Provider>
);
@@ -22,7 +22,7 @@ import {
GetBranchResult,
GetLatestReleaseResult,
} from '../../api/GitReleaseClient';
import { ComponentConfigPatch } from '../../types/types';
import { ComponentConfig, PatchOnSuccessArgs } from '../../types/types';
import { getBumpedTag } from '../../helpers/getBumpedTag';
import { InfoCardPlus } from '../../components/InfoCardPlus';
import { NoLatestRelease } from '../../components/NoLatestRelease';
@@ -32,7 +32,7 @@ import { useProjectContext } from '../../contexts/ProjectContext';
interface PatchProps {
latestRelease: GetLatestReleaseResult['latestRelease'];
releaseBranch: GetBranchResult['branch'] | null;
onSuccess?: ComponentConfigPatch['onSuccess'];
onSuccess?: ComponentConfig<PatchOnSuccessArgs>['onSuccess'];
}
export const Patch = ({
@@ -38,7 +38,7 @@ import {
GetLatestReleaseResult,
} from '../../api/GitReleaseClient';
import { CalverTagParts } from '../../helpers/tagParts/getCalverTagParts';
import { ComponentConfigPatch } from '../../types/types';
import { ComponentConfig, PatchOnSuccessArgs } from '../../types/types';
import { Differ } from '../../components/Differ';
import { getPatchCommitSuffix } from './helpers/getPatchCommitSuffix';
import { gitReleaseManagerApiRef } from '../../api/serviceApiRef';
@@ -56,7 +56,7 @@ interface PatchBodyProps {
bumpedTag: string;
latestRelease: NonNullable<GetLatestReleaseResult['latestRelease']>;
releaseBranch: GetBranchResult['branch'];
onSuccess?: ComponentConfigPatch['onSuccess'];
onSuccess?: ComponentConfig<PatchOnSuccessArgs>['onSuccess'];
tagParts: NonNullable<CalverTagParts | SemverTagParts>;
}
@@ -22,7 +22,11 @@ import {
} from '../../../api/GitReleaseClient';
import { CalverTagParts } from '../../../helpers/tagParts/getCalverTagParts';
import { ComponentConfigPatch, CardHook } from '../../../types/types';
import {
CardHook,
ComponentConfig,
PatchOnSuccessArgs,
} from '../../../types/types';
import { getPatchCommitSuffix } from '../helpers/getPatchCommitSuffix';
import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef';
import { Project } from '../../../contexts/ProjectContext';
@@ -32,12 +36,12 @@ import { useResponseSteps } from '../../../hooks/useResponseSteps';
import { useUserContext } from '../../../contexts/UserContext';
import { useApi } from '@backstage/core-plugin-api';
interface Patch {
export interface UsePatch {
bumpedTag: string;
latestRelease: NonNullable<GetLatestReleaseResult['latestRelease']>;
project: Project;
tagParts: NonNullable<CalverTagParts | SemverTagParts>;
onSuccess?: ComponentConfigPatch['onSuccess'];
onSuccess?: ComponentConfig<PatchOnSuccessArgs>['onSuccess'];
}
// Inspiration: https://stackoverflow.com/questions/53859199/how-to-cherry-pick-through-githubs-api
@@ -47,7 +51,7 @@ export function usePatch({
project,
tagParts,
onSuccess,
}: Patch): CardHook<GetRecentCommitsResultSingle> {
}: UsePatch): CardHook<GetRecentCommitsResultSingle> {
const pluginApiClient = useApi(gitReleaseManagerApiRef);
const { user } = useUserContext();
const {
@@ -337,13 +341,19 @@ ${selectedPatchCommit.commit.message}`,
try {
await onSuccess?.({
updatedReleaseUrl: updatedReleaseRes.value.htmlUrl,
updatedReleaseName: updatedReleaseRes.value.name,
previousTag: latestRelease.tagName,
patchedTag: updatedReleaseRes.value.tagName,
patchCommitUrl: releaseBranchRes.value.selectedPatchCommit.htmlUrl,
input: {
bumpedTag,
latestRelease,
project,
tagParts,
},
patchCommitMessage:
releaseBranchRes.value.selectedPatchCommit.commit.message,
patchCommitUrl: releaseBranchRes.value.selectedPatchCommit.htmlUrl,
patchedTag: updatedReleaseRes.value.tagName,
previousTag: latestRelease.tagName,
updatedReleaseName: updatedReleaseRes.value.name,
updatedReleaseUrl: updatedReleaseRes.value.htmlUrl,
});
} catch (error) {
asyncCatcher(error);
@@ -18,7 +18,7 @@ import React from 'react';
import { Alert, AlertTitle } from '@material-ui/lab';
import { Box, Typography } from '@material-ui/core';
import { ComponentConfigPromoteRc } from '../../types/types';
import { ComponentConfig, PromoteRcOnSuccessArgs } from '../../types/types';
import { GetLatestReleaseResult } from '../../api/GitReleaseClient';
import { InfoCardPlus } from '../../components/InfoCardPlus';
import { NoLatestRelease } from '../../components/NoLatestRelease';
@@ -27,7 +27,7 @@ import { TEST_IDS } from '../../test-helpers/test-ids';
interface PromoteRcProps {
latestRelease: GetLatestReleaseResult['latestRelease'];
onSuccess?: ComponentConfigPromoteRc['onSuccess'];
onSuccess?: ComponentConfig<PromoteRcOnSuccessArgs>['onSuccess'];
}
export const PromoteRc = ({ latestRelease, onSuccess }: PromoteRcProps) => {
@@ -17,7 +17,7 @@
import React from 'react';
import { Button, Typography, Box } from '@material-ui/core';
import { ComponentConfigPromoteRc } from '../../types/types';
import { ComponentConfig, PromoteRcOnSuccessArgs } from '../../types/types';
import { Differ } from '../../components/Differ';
import { GetLatestReleaseResult } from '../../api/GitReleaseClient';
import { ResponseStepDialog } from '../../components/ResponseStepDialog/ResponseStepDialog';
@@ -26,7 +26,7 @@ import { usePromoteRc } from './hooks/usePromoteRc';
interface PromoteRcBodyProps {
rcRelease: NonNullable<GetLatestReleaseResult['latestRelease']>;
onSuccess?: ComponentConfigPromoteRc['onSuccess'];
onSuccess?: ComponentConfig<PromoteRcOnSuccessArgs>['onSuccess'];
}
export const PromoteRcBody = ({ rcRelease, onSuccess }: PromoteRcBodyProps) => {
@@ -16,7 +16,11 @@
import { useState, useEffect } from 'react';
import { useAsync, useAsyncFn } from 'react-use';
import { CardHook, ComponentConfigPromoteRc } from '../../../types/types';
import {
CardHook,
ComponentConfig,
PromoteRcOnSuccessArgs,
} from '../../../types/types';
import { GetLatestReleaseResult } from '../../../api/GitReleaseClient';
import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef';
@@ -27,17 +31,17 @@ import { useResponseSteps } from '../../../hooks/useResponseSteps';
import { useUserContext } from '../../../contexts/UserContext';
import { useApi } from '@backstage/core-plugin-api';
interface PromoteRc {
export interface UsePromoteRc {
rcRelease: NonNullable<GetLatestReleaseResult['latestRelease']>;
releaseVersion: string;
onSuccess?: ComponentConfigPromoteRc['onSuccess'];
onSuccess?: ComponentConfig<PromoteRcOnSuccessArgs>['onSuccess'];
}
export function usePromoteRc({
rcRelease,
releaseVersion,
onSuccess,
}: PromoteRc): CardHook<void> {
}: UsePromoteRc): CardHook<void> {
const pluginApiClient = useApi(gitReleaseManagerApiRef);
const { user } = useUserContext();
const { project } = useProjectContext();
@@ -170,12 +174,16 @@ export function usePromoteRc({
try {
await onSuccess?.({
gitReleaseUrl: promotedReleaseRes.value.htmlUrl,
input: {
rcRelease,
releaseVersion,
},
gitReleaseName: promotedReleaseRes.value.name,
previousTagUrl: rcRelease.htmlUrl,
gitReleaseUrl: promotedReleaseRes.value.htmlUrl,
previousTag: rcRelease.tagName,
updatedTagUrl: promotedReleaseRes.value.htmlUrl,
previousTagUrl: rcRelease.htmlUrl,
updatedTag: promotedReleaseRes.value.tagName,
updatedTagUrl: promotedReleaseRes.value.htmlUrl,
});
} catch (error) {
asyncCatcher(error);
+15 -11
View File
@@ -14,21 +14,26 @@
* limitations under the License.
*/
export type ComponentConfig<Args> = {
import { UseCreateReleaseCandidate } from '../features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate';
import { UsePatch } from '../features/Patch/hooks/usePatch';
import { UsePromoteRc } from '../features/PromoteRc/hooks/usePromoteRc';
export type ComponentConfig<OnSuccessArgs> = {
omit?: boolean;
onSuccess?: (args: Args) => Promise<void> | void;
onSuccess?: (args: OnSuccessArgs) => Promise<void> | void;
};
interface CreateRcOnSuccessArgs {
gitReleaseUrl: string;
gitReleaseName: string | null;
export interface CreateRcOnSuccessArgs {
input: Omit<UseCreateReleaseCandidate, 'onSuccess'>;
comparisonUrl: string;
previousTag?: string;
createdTag: string;
gitReleaseName: string | null;
gitReleaseUrl: string;
previousTag?: string;
}
export type ComponentConfigCreateRc = ComponentConfig<CreateRcOnSuccessArgs>;
interface PromoteRcOnSuccessArgs {
export interface PromoteRcOnSuccessArgs {
input: Omit<UsePromoteRc, 'onSuccess'>;
gitReleaseUrl: string;
gitReleaseName: string | null;
previousTagUrl: string;
@@ -36,9 +41,9 @@ interface PromoteRcOnSuccessArgs {
updatedTagUrl: string;
updatedTag: string;
}
export type ComponentConfigPromoteRc = ComponentConfig<PromoteRcOnSuccessArgs>;
interface PatchOnSuccessArgs {
export interface PatchOnSuccessArgs {
input: Omit<UsePatch, 'onSuccess'>;
updatedReleaseUrl: string;
updatedReleaseName: string | null;
previousTag: string;
@@ -46,7 +51,6 @@ interface PatchOnSuccessArgs {
patchCommitUrl: string;
patchCommitMessage: string;
}
export type ComponentConfigPatch = ComponentConfig<PatchOnSuccessArgs>;
export interface ResponseStep {
message: string | React.ReactNode;
+1 -1
View File
@@ -21,7 +21,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.8.5s",
"@backstage/backend-common": "^0.8.6",
"@backstage/catalog-client": "^0.3.16",
"@backstage/catalog-model": "^0.9.0",
"@backstage/config": "^0.1.5",
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
@@ -0,0 +1,162 @@
# scaffolder-backend-module-cookiecutter
Welcome to the `fetch:cookiecutter` action for the `scaffolder-backend`.
## Getting started
You need to configure the action in your backend:
## From your Backstage root directory
```
cd packages/backend
yarn add @backstage/plugin-scaffolder-backend-module-cookiecutter
```
Configure the action:
(you can check the [docs](https://backstage.io/docs/features/software-templates/writing-custom-actions#registering-custom-actions) to see all options):
```typescript
// packages/backend/src/plugins/scaffolder.ts
const actions = [
createFetchCookiecutterAction({
integrations,
reader,
containerRunner,
}),
...createBuiltInActions({
...
})
];
return await createRouter({
containerRunner,
logger,
config,
database,
catalogClient,
reader,
actions,
});
```
After that you can use the action in your template:
```yaml
apiVersion: backstage.io/v1beta2
kind: Template
metadata:
name: cookiecutter-demo
title: Cookiecutter Test
description: Cookiecutter example
spec:
owner: backstage/techdocs-core
type: service
parameters:
- title: Fill in some steps
required:
- name
- owner
properties:
name:
title: Name
type: string
description: Unique name of the component
ui:autofocus: true
ui:options:
rows: 5
owner:
title: Owner
type: string
description: Owner of the component
ui:field: OwnerPicker
ui:options:
allowedKinds:
- Group
system:
title: System
type: string
description: System of the component
ui:field: EntityPicker
ui:options:
allowedKinds:
- System
defaultKind: System
- title: Choose a location
required:
- repoUrl
- dryRun
properties:
repoUrl:
title: Repository Location
type: string
ui:field: RepoUrlPicker
ui:options:
allowedHosts:
- github.com
dryRun:
title: Only perform a dry run, don't publish anything
type: boolean
default: false
steps:
- id: fetch-base
name: Fetch Base
action: fetch:cookiecutter
input:
url: ./template
values:
name: '{{ parameters.name }}'
owner: '{{ parameters.owner }}'
system: '{{ parameters.system }}'
destination: '{{ parseRepoUrl parameters.repoUrl }}'
- id: publish
if: '{{ not parameters.dryRun }}'
name: Publish
action: publish:github
input:
allowedHosts: ['github.com']
description: 'This is {{ parameters.name }}'
repoUrl: '{{ parameters.repoUrl }}'
- id: register
if: '{{ not parameters.dryRun }}'
name: Register
action: catalog:register
input:
repoContentsUrl: '{{ steps.publish.output.repoContentsUrl }}'
catalogInfoPath: '/catalog-info.yaml'
- name: Results
if: '{{ parameters.dryRun }}'
action: debug:log
input:
listWorkspace: true
output:
links:
- title: Repository
url: '{{ steps.publish.output.remoteUrl }}'
- title: Open in catalog
icon: 'catalog'
entityRef: '{{ steps.register.output.entityRef }}'
```
You can also visit the `/create/actions` route in your Backstage application to find out more about the parameters this action accepts when it's installed to configure how you like.
### Environment setup
The environment needs to have either `cookiecutter` installed and be available in the `PATH` or access to a `docker` daemon so it can spin up a docker container with `cookiecutter` available.
If you are running Backstage from a Docker container and you want to avoid calling a container inside a container, you can set up `cookiecutter` in your own image, this will use the local installation instead.
You can do so by including the following lines in the last step of your Dockerfile:
```dockerfile
RUN apt-get update && apt-get install -y python3 python3-pip
RUN pip3 install cookiecutter
```
@@ -0,0 +1,23 @@
## API Report File for "@backstage/plugin-scaffolder-backend-module-cookiecutter"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="node" />
import { ContainerRunner } from '@backstage/backend-common';
import { ScmIntegrations } from '@backstage/integration';
import { TemplateAction } from '@backstage/plugin-scaffolder-backend';
import { UrlReader } from '@backstage/backend-common';
// Warning: (ae-missing-release-tag) "createFetchCookiecutterAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function createFetchCookiecutterAction(options: {
reader: UrlReader;
integrations: ScmIntegrations;
containerRunner: ContainerRunner;
}): TemplateAction<any>;
// (No @packageDocumentation comment for this package)
```
@@ -0,0 +1,45 @@
{
"name": "@backstage/plugin-scaffolder-backend-module-cookiecutter",
"version": "0.1.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"scripts": {
"start": "backstage-cli backend:dev",
"build": "backstage-cli backend:build",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.8.6",
"@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.7",
"@backstage/plugin-scaffolder-backend": "^0.14.0",
"@backstage/config": "^0.1.5",
"command-exists": "^1.2.9",
"fs-extra": "10.0.0",
"winston": "^3.2.1",
"cross-fetch": "^3.0.6",
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.7.3",
"@types/fs-extra": "^9.0.1",
"@types/mock-fs": "^4.13.0",
"@types/jest": "^26.0.7",
"@types/command-exists": "^1.2.0",
"mock-fs": "^4.13.0",
"msw": "^0.29.0"
},
"files": [
"dist"
]
}
@@ -17,9 +17,12 @@ const runCommand = jest.fn();
const commandExists = jest.fn();
const fetchContents = jest.fn();
jest.mock('./helpers', () => ({ fetchContents }));
jest.mock('@backstage/plugin-scaffolder-backend', () => ({
...jest.requireActual('@backstage/plugin-scaffolder-backend'),
fetchContents,
runCommand,
}));
jest.mock('command-exists', () => commandExists);
jest.mock('../helpers', () => ({ runCommand }));
import {
getVoidLogger,
@@ -33,7 +36,7 @@ import os from 'os';
import { PassThrough } from 'stream';
import { createFetchCookiecutterAction } from './cookiecutter';
import { join } from 'path';
import { ActionContext } from '../../types';
import type { ActionContext } from '@backstage/plugin-scaffolder-backend';
describe('fetch:cookiecutter', () => {
const integrations = ScmIntegrations.fromConfig(
@@ -26,9 +26,11 @@ import commandExists from 'command-exists';
import fs from 'fs-extra';
import path, { resolve as resolvePath } from 'path';
import { Writable } from 'stream';
import { runCommand } from '../helpers';
import { createTemplateAction } from '../../createTemplateAction';
import { fetchContents } from './helpers';
import {
runCommand,
createTemplateAction,
fetchContents,
} from '@backstage/plugin-scaffolder-backend';
export class CookiecutterRunner {
private readonly containerRunner: ContainerRunner;
@@ -136,7 +138,7 @@ export function createFetchCookiecutterAction(options: {
}>({
id: 'fetch:cookiecutter',
description:
"Downloads a template from the given URL into the workspace, and runs cookiecutter on it. This action is deprecated in favor of 'fetch:template'. See https://backstage.io/docs/features/software-templates/builtin-actions#migrating-from-fetch-cookiecutter-to-fetch-template for more details.",
'Downloads a template from the given URL into the workspace, and runs cookiecutter on it.',
schema: {
input: {
type: 'object',
@@ -0,0 +1,16 @@
/*
* 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.
*/
export { createFetchCookiecutterAction } from './cookiecutter';

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