Merge remote-tracking branch 'upstream/master' into feat/relative-ref-tmp
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/techdocs-common': patch
|
||||
---
|
||||
|
||||
Fix validation of mkdocs.yml docs_dir
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/catalog-client': patch
|
||||
---
|
||||
|
||||
Export `CatalogRequestOptions` type
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-jenkins-backend': patch
|
||||
---
|
||||
|
||||
Update `@backstage/backend-common` to `^0.8.6`
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-import': patch
|
||||
---
|
||||
|
||||
Fix heading that wrongly implied catalog-import supports entity discovery for multiple integrations.
|
||||
@@ -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({ ...
|
||||
```
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
chore: bump `eslint` to `7.30.0`
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-git-release-manager': minor
|
||||
---
|
||||
|
||||
Enable users to add custom features
|
||||
|
||||
Add more metadata to success callbacks
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Export `CatalogClientWrapper` class
|
||||
@@ -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}>
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/dev-utils': patch
|
||||
---
|
||||
|
||||
Allow custom theme for dev app.
|
||||
@@ -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>
|
||||
...
|
||||
};
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
|
||||
+151
-82
@@ -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 project’s
|
||||
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 project’s 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 [“what’s 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 we’re 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.
|
||||
## What’s 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 doesn’t 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:** We’re 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 doesn’t 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 doesn’t represent a commitment to develop and the
|
||||
list order doesn’t 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 [what’s 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+16
-21
@@ -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]
|
||||
|
||||
+7
@@ -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">
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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: {},
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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';
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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 }) => {
|
||||
|
||||
+13
-3
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
+6
-3
@@ -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(
|
||||
+6
-4
@@ -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
Reference in New Issue
Block a user