Merge remote-tracking branch 'upstream/master' into add-external-id-to-s3-integration
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Remove beta labels
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Added new `github:issues:label` action to apply labels to issues, and also output `pullRequestNumber` from `publish:github:pull-request`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
The `ScaffolderPage` now uses the `CatalogFilterLayout`, which means the filters are put in a drawer on smaller screens.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
**DEPRECATION**: The `FilteredEntityLayout` and related components have been moved to `@backstage/plugin-catalog-react` and renamed. The original components are now deprecated and should be replaced as follows:
|
||||
|
||||
- `FilteredEntityLayout` -> `CatalogFilterLayout`
|
||||
- `FilterContainer` -> `CatalogFilterLayout.Filters`
|
||||
- `EntityListContainer` -> `CatalogFilterLayout.Content`
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
---
|
||||
|
||||
Added `CatalogFilterLayout`, which replaces `FilteredEntityLayout` from `@backstage/plugin-catalog`, as well as `FilterContainer` and `EntityListContainer`. It is used like this:
|
||||
|
||||
```tsx
|
||||
<CatalogFilterLayout>
|
||||
<CatalogFilterLayout.Filters>
|
||||
{/* filter drawer, for example <EntityTypePicker /> and friends */}
|
||||
</CatalogFilterLayout.Filters>
|
||||
<CatalogFilterLayout.Content>
|
||||
{/* content view, for example a <CatalogTable /> */}
|
||||
</CatalogFilterLayout.Content>
|
||||
</CatalogFilterLayout>
|
||||
```
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-msgraph': minor
|
||||
---
|
||||
|
||||
**BREAKING**: `MicrosoftGraphOrgEntityProvider.fromConfig` now requires a `schedule` field in its options, which simplifies scheduling. If you want to retain the old behavior of calling its `run()` method manually, you can set the new field value to the string `'manual'`. But you may prefer to instead give it a scheduled task runner from the backend tasks package:
|
||||
|
||||
```diff
|
||||
// packages/backend/src/plugins/catalog.ts
|
||||
+import { Duration } from 'luxon';
|
||||
+import { MicrosoftGraphOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-msgraph';
|
||||
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
|
||||
+ // The target parameter below needs to match one of the providers' target
|
||||
+ // value specified in your app-config.
|
||||
+ builder.addEntityProvider(
|
||||
+ MicrosoftGraphOrgEntityProvider.fromConfig(env.config, {
|
||||
+ id: 'production',
|
||||
+ target: 'https://graph.microsoft.com/v1.0',
|
||||
+ logger: env.logger,
|
||||
+ schedule: env.scheduler.createScheduledTaskRunner({
|
||||
+ frequency: Duration.fromObject({ minutes: 5 }),
|
||||
+ timeout: Duration.fromObject({ minutes: 3 }),
|
||||
+ }),
|
||||
+ }),
|
||||
+ );
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-api-docs': patch
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
Switched to using `CatalogFilterLayout` from `@backstage/plugin-catalog-react`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-badges': patch
|
||||
---
|
||||
|
||||
Fixed an error caused by the `EntityBadgesDialog` trying to access the entity before it is loaded.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Fix for `EntitySwitch` not properly falling back to render the default entity page when the entity is missing.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
'@backstage/plugin-catalog-backend-module-github': patch
|
||||
'@backstage/plugin-catalog-backend-module-ldap': patch
|
||||
---
|
||||
|
||||
Made sure to move the catalog-related github and ldap config into their right places
|
||||
@@ -27,6 +27,7 @@ Billett
|
||||
bitbucket
|
||||
Bitrise
|
||||
Blackbox
|
||||
Brex
|
||||
bool
|
||||
boolean
|
||||
builtins
|
||||
@@ -151,6 +152,7 @@ Leasot
|
||||
lerna
|
||||
Lerna
|
||||
LocalStack
|
||||
lockdown
|
||||
lockfile
|
||||
lunr
|
||||
Luxon
|
||||
@@ -268,6 +270,7 @@ seb
|
||||
semlas
|
||||
semver
|
||||
Serverless
|
||||
shoutout
|
||||
siloed
|
||||
Sinon
|
||||
Snyk
|
||||
|
||||
@@ -41,16 +41,16 @@ export const CustomCatalogPage = ({
|
||||
<CreateButton title="Create Component" to={createComponentLink()} />
|
||||
<SupportButton>All your software catalog entities</SupportButton>
|
||||
</ContentHeader>
|
||||
<FilteredEntityLayout>
|
||||
<FilterContainer>
|
||||
<CatalogFilterLayout>
|
||||
<CatalogFilterLayout.Filters>
|
||||
<EntityTypePicker />
|
||||
<UserListPicker initialFilter={initiallySelectedFilter} />
|
||||
<EntityTagPicker />
|
||||
</FilterContainer>
|
||||
<EntityListContainer>
|
||||
</CatalogFilterLayout.Filters>
|
||||
<CatalogFilterLayout.Content>
|
||||
<CatalogTable columns={columns} actions={actions} />
|
||||
</EntityListContainer>
|
||||
</FilteredEntityLayout>
|
||||
</CatalogFilterLayout.Content>
|
||||
</CatalogFilterLayout>
|
||||
</Content>
|
||||
</EntityListProvider>
|
||||
</PageWithHeader>
|
||||
@@ -151,18 +151,18 @@ export const CustomCatalogPage = ({
|
||||
return (
|
||||
...
|
||||
<EntityListProvider>
|
||||
<FilteredEntityLayout>
|
||||
<FilterContainer>
|
||||
<CatalogFilterLayout>
|
||||
<CatalogFilterLayout.Filters>
|
||||
<EntityKindPicker initialFilter="component" hidden />
|
||||
<EntityTypePicker />
|
||||
<UserListPicker initialFilter={initiallySelectedFilter} />
|
||||
+ <EntitySecurityTierPicker />
|
||||
<EntityTagPicker />
|
||||
<FilterContainer>
|
||||
<EntityListContainer>
|
||||
<CatalogFilterLayout.Filters>
|
||||
<CatalogFilterLayout.Content>
|
||||
<CatalogTable columns={columns} actions={actions} />
|
||||
</EntityListContainer>
|
||||
</FilteredEntityLayout>
|
||||
</CatalogFilterLayout.Content>
|
||||
</CatalogFilterLayout>
|
||||
</EntityListProvider>
|
||||
...
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
id: software-catalog-overview
|
||||
title: Backstage Software Catalog (alpha)
|
||||
title: Backstage Software Catalog
|
||||
sidebar_label: Overview
|
||||
# prettier-ignore
|
||||
description: The Backstage Software Catalog
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: Backstage Turns Two!
|
||||
author: Lee Mills, Spotify
|
||||
authorURL: https://github.com/leemills83
|
||||
authorImageURL: https://avatars1.githubusercontent.com/u/1236238?s=460&v=4
|
||||
---
|
||||
|
||||

|
||||
|
||||
**TLDR:** Backstage has turned two. Of course, this is software, so we expect things to move fast. But when you have a passionate, worldwide community building together, we’ve now seen firsthand just how fast that can be. We’re marking our second year as a community by becoming a CNCF incubating project, reaching 100 public adopters, releasing version 1.0 of the core app, and other very nice birthday gifts. Rather than a toddler finding their feet, Backstage today is beginning to look awfully grown-up for a two-year-old.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
## Out of the Sandbox and into Incubation
|
||||
|
||||
Two years ago, a small team at Spotify launched Backstage into open source during our internal Hack Week. We knew we had something special on our hands and that Backstage could potentially be the most ambitious open source project in Spotify’s history (you can listen to me wax philosophical on this topic more in [Episode 2 of the new NerdOut@Spotify podcast](https://open.spotify.com/episode/332yTwGiILGKTS7dsHCj2P?si=pX9xTzB2SgmsROdlLLr24A).)
|
||||
|
||||
When Spotify donated the project to the CNCF, we saw it as the first promising step toward making Backstage an industry standard. As the home to Kubernetes, Envoy, and so many other game-changing open technologies, the CNCF and its community has had a wide-ranging impact on the daily lives of developers everywhere. And that’s what we hoped for Backstage, even back then.
|
||||
|
||||
But we couldn't have envisioned where we’d be two years later — part of an ever-growing community of people that feel as passionate as we do about unlocking better developer experience for every engineering organization.
|
||||
|
||||
Yesterday, the CNCF announced that its [Technical Oversight Committee](https://github.com/cncf/toc) (TOC) voted to accept Backstage as a [CNCF incubating project](https://www.cncf.io/blog/2022/03/15/backstage-project-joins-the-cncf-incubator/). The move from the CNCF Sandbox to Incubation is an important step forward for the project’s technical governance.
|
||||
|
||||
It’s also recognition for all the work the Backstage open source community has done to advance and grow this project since we launched the repo on GitHub just a few weeks after that Hack Week. We still have a long way to go to becoming the standard, but moving out of the CNCF Sandbox and beginning the Incubation phase is further validation of those aspirations.
|
||||
|
||||
## What do retailers, gaming studios, banks, airlines, streamers, startups, and enterprise software makers all have in common?
|
||||
|
||||
Developers, developers, developers — that’s what all companies have in common. Developers dealing with [more complexity](https://youtu.be/85TQEpNCaU0) than they’ve ever had to deal with before. Developers who want to be able to go from idea to bug fix or feature faster.
|
||||
|
||||
Two years in, the Backstage ecosystem has grown by leaps and bounds. More than 5,000 contributors have joined the project and are submitting bugs, adding new features, and building valuable plugins that benefit the greater community. From [Expedia Group](https://backstage.spotify.com/blog/measuring-backstage-proof-of-value-at-expedia/) (adopter No.18) to Unity (adopter No.67), Backstage now has [100 publicly listed adopters](https://github.com/backstage/backstage/blob/master/ADOPTERS.md) across a variety of industries, including retail, gaming, financial services, manufacturing, and government.
|
||||
|
||||
We have companies like [Roadie](http://roadie.io) providing hosting solutions and ThoughtWorks [providing technology and consulting services](https://www.thoughtworks.com/en-us/about-us/news/2021/thoughtworks-is-collaborating-with-spotify-to-deliver-better-dev) to teams looking to stand up Backstage in their own orgs. We’re starting to see more and more job listings for Backstage developers which tells us that companies of all shapes and sizes are going all-in on Backstage and developer effectiveness.
|
||||
|
||||
Also, I’ll pause now to give a special shoutout to our early adopters. They stuck with us through all the bumps and breaking changes. But because of their early commitment, we now see instances of Backstage up and running in so many different kinds of engineering organizations. From [Zalando](https://youtu.be/6sg5uMCLxTA) to [American Airlines](https://backstage.spotify.com/blog/adopter-spotlight/american-airlines-runway/) and [Brex](https://backstage.spotify.com/blog/community-session/11-brex-backstage-upgrade-helper/), the growing pains of our earliest adopters helped pave the way for smoother adoptions for all who follow them.
|
||||
|
||||
A few more numbers that make us proud:
|
||||
|
||||
- 2,000+ project forks
|
||||
- 20,000+ commits
|
||||
- 10,000+ PRs & issues
|
||||
- 15,000+ stars on GitHub
|
||||
- 60+ open source plugins in the [Backstage Plugin Marketplace](https://backstage.io/plugins)
|
||||
|
||||
## A global community of friendly, helpful contributors
|
||||
|
||||
We open sourced Backstage in March 2020 and then almost immediately went into lockdown. When you think about this project in the context of the pandemic, it's really remarkable how much this community has been able to achieve.
|
||||
|
||||
The Backstage community is truly global with adopters and contributors from almost every continent (sorry, Antarctica!) coming together to share what they’ve worked on and demo cool stuff in our monthly Community Sessions. The repo and the Discord server truly never sleep as a growing group of dedicated Backstage experts outside Spotify support one another and offer help 24/7.
|
||||
|
||||
We asked a few contributors and maintainers to share their thoughts around this two-year milestone.
|
||||
|
||||
<iframe width="780" height="440" src="https://www.youtube.com/embed/-j-waaybcH0" frameBorder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe>
|
||||
|
||||
Warms the heart, doesn’t it? Thanks again to all our contributors for making this possible. Seriously. We’re proud of what the community has accomplished and how we’ve accomplished it. All along, it’s your enthusiasm and warm/collaborative/positive vibes that have carried us and this project through.
|
||||
|
||||
## Version 1.0 and beyond
|
||||
|
||||
Tomorrow, we’ll be releasing version 1.0 of the Backstage core framework, including Software Catalog, Software Templates, TechDocs, and API Reference. With Backstage 1.0, the project’s core components are coming out of beta and into production with regular versioning and release cycle commitments (check back on this blog for more details!).
|
||||
|
||||
v1.0 paves the way for more adoption, a bigger community, more contributions — and with this continued growth in the ecosystem, we expect even greater innovation on the platform just on the horizon with exchange of features and plugins.
|
||||
|
||||
What comes next for Backstage is — in many ways — entirely up to you. We are looking to this community to [help us shape the next year](https://backstage.io/docs/overview/roadmap).
|
||||
|
||||
If you’re already a part of this community, thank you for your hard work and dedication. Backstage wouldn’t be where it is today without you. We’ve come a long way together, but there’s still so much to do.
|
||||
|
||||
And if you haven’t signed up yet, there’s plenty of room on this party boat! And there’s never been a better time to hop on board. As Irma ([@Irma12](https://github.com/Irma12)) said so beautifully: where else is there an opportunity to have so many people look at your code, hear you out, and help you out?
|
||||
|
||||
Take a look at how to [start contributing](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md) and [join the community](https://github.com/backstage/community)!
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
@@ -109,15 +109,7 @@ class Index extends React.Component {
|
||||
className="Block__GIF"
|
||||
src={`${baseUrl}animations/backstage-software-catalog-icon-1.gif`}
|
||||
/>
|
||||
<Block.Subtitle>
|
||||
Backstage Software Catalog{' '}
|
||||
<a
|
||||
title="Submit feedback for this feature. Click to learn more about this release."
|
||||
href="https://backstage.io/blog/2020/06/22/backstage-service-catalog-alpha"
|
||||
>
|
||||
(alpha)
|
||||
</a>
|
||||
</Block.Subtitle>
|
||||
<Block.Subtitle>Backstage Software Catalog</Block.Subtitle>
|
||||
<Block.Title half>
|
||||
Build an ecosystem, not a wilderness
|
||||
</Block.Title>
|
||||
@@ -200,7 +192,7 @@ class Index extends React.Component {
|
||||
Learn more about the software catalog
|
||||
</ActionBlock.Title>
|
||||
<ActionBlock.Link
|
||||
href={`https://backstage.io/blog/2020/06/22/backstage-service-catalog-alpha`}
|
||||
href={`https://backstage.io/docs/features/software-catalog/software-catalog-overview`}
|
||||
>
|
||||
Read
|
||||
</ActionBlock.Link>
|
||||
@@ -213,15 +205,7 @@ class Index extends React.Component {
|
||||
className="Block__GIF"
|
||||
src={`${baseUrl}animations/backstage-software-templates-icon-5.gif`}
|
||||
/>
|
||||
<Block.Subtitle>
|
||||
Backstage Software Templates{' '}
|
||||
<a
|
||||
title="Submit feedback for this feature. Click to learn more about this release."
|
||||
href="https://backstage.io/blog/2021/07/26/software-templates-are-now-in-beta"
|
||||
>
|
||||
(beta)
|
||||
</a>
|
||||
</Block.Subtitle>
|
||||
<Block.Subtitle>Backstage Software Templates</Block.Subtitle>
|
||||
<Block.Title small>Standards can set you free</Block.Title>
|
||||
</Block.TextBox>
|
||||
<Breakpoint
|
||||
@@ -311,15 +295,7 @@ class Index extends React.Component {
|
||||
src={`${baseUrl}animations/backstage-techdocs-icon-1.gif`}
|
||||
/>
|
||||
|
||||
<Block.Subtitle>
|
||||
Backstage TechDocs{' '}
|
||||
<a
|
||||
title="Submit feedback for this feature. Click to learn more about this release."
|
||||
href="https://backstage.io/blog/2021/09/16/the-techdocs-beta-has-landed"
|
||||
>
|
||||
(beta)
|
||||
</a>
|
||||
</Block.Subtitle>
|
||||
<Block.Subtitle>Backstage TechDocs</Block.Subtitle>
|
||||
<Block.Title small>Docs like code</Block.Title>
|
||||
</Block.TextBox>
|
||||
<Breakpoint
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
Entity,
|
||||
RELATION_API_CONSUMED_BY,
|
||||
RELATION_API_PROVIDED_BY,
|
||||
RELATION_CONSUMES_API,
|
||||
@@ -179,7 +180,7 @@ export const cicdContent = (
|
||||
<EntityJenkinsContent />
|
||||
</EntitySwitch.Case>
|
||||
|
||||
<EntitySwitch.Case if={isBuildkiteAvailable}>
|
||||
<EntitySwitch.Case if={isBuildkiteAvailable as (e: Entity) => boolean}>
|
||||
<EntityBuildkiteContent />
|
||||
</EntitySwitch.Case>
|
||||
|
||||
@@ -191,7 +192,7 @@ export const cicdContent = (
|
||||
<EntityCloudbuildContent />
|
||||
</EntitySwitch.Case>
|
||||
|
||||
<EntitySwitch.Case if={isTravisciAvailable}>
|
||||
<EntitySwitch.Case if={isTravisciAvailable as (e: Entity) => boolean}>
|
||||
<EntityTravisCIContent />
|
||||
</EntitySwitch.Case>
|
||||
|
||||
@@ -234,7 +235,7 @@ const cicdCard = (
|
||||
</Grid>
|
||||
</EntitySwitch.Case>
|
||||
|
||||
<EntitySwitch.Case if={isTravisciAvailable}>
|
||||
<EntitySwitch.Case if={isTravisciAvailable as (e: Entity) => boolean}>
|
||||
<Grid item sm={6}>
|
||||
<EntityTravisCIOverviewCard />
|
||||
</Grid>
|
||||
@@ -326,7 +327,9 @@ const overviewContent = (
|
||||
{cicdCard}
|
||||
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case if={e => Boolean(isGithubInsightsAvailable(e))}>
|
||||
<EntitySwitch.Case
|
||||
if={isGithubInsightsAvailable as (e: Entity) => boolean}
|
||||
>
|
||||
<Grid item md={6}>
|
||||
<EntityGithubInsightsLanguagesCard />
|
||||
<EntityGithubInsightsReleasesCard />
|
||||
@@ -346,7 +349,9 @@ const overviewContent = (
|
||||
</EntitySwitch>
|
||||
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case if={e => Boolean(isGithubPullRequestsAvailable(e))}>
|
||||
<EntitySwitch.Case
|
||||
if={isGithubPullRequestsAvailable as (e: Entity) => boolean}
|
||||
>
|
||||
<Grid item sm={4}>
|
||||
<EntityGithubPullRequestsOverviewCard />
|
||||
</Grid>
|
||||
|
||||
@@ -24,13 +24,7 @@ import {
|
||||
TableProps,
|
||||
} from '@backstage/core-components';
|
||||
import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
CatalogTable,
|
||||
CatalogTableRow,
|
||||
FilteredEntityLayout,
|
||||
EntityListContainer,
|
||||
FilterContainer,
|
||||
} from '@backstage/plugin-catalog';
|
||||
import { CatalogTable, CatalogTableRow } from '@backstage/plugin-catalog';
|
||||
import {
|
||||
EntityKindPicker,
|
||||
EntityLifecyclePicker,
|
||||
@@ -40,6 +34,7 @@ import {
|
||||
EntityTypePicker,
|
||||
UserListFilterKind,
|
||||
UserListPicker,
|
||||
CatalogFilterLayout,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import React from 'react';
|
||||
import { registerComponentRouteRef } from '../../routes';
|
||||
@@ -95,22 +90,22 @@ export const DefaultApiExplorerPage = ({
|
||||
<SupportButton>All your APIs</SupportButton>
|
||||
</ContentHeader>
|
||||
<EntityListProvider>
|
||||
<FilteredEntityLayout>
|
||||
<FilterContainer>
|
||||
<CatalogFilterLayout>
|
||||
<CatalogFilterLayout.Filters>
|
||||
<EntityKindPicker initialFilter="api" hidden />
|
||||
<EntityTypePicker />
|
||||
<UserListPicker initialFilter={initiallySelectedFilter} />
|
||||
<EntityOwnerPicker />
|
||||
<EntityLifecyclePicker />
|
||||
<EntityTagPicker />
|
||||
</FilterContainer>
|
||||
<EntityListContainer>
|
||||
</CatalogFilterLayout.Filters>
|
||||
<CatalogFilterLayout.Content>
|
||||
<CatalogTable
|
||||
columns={columns || defaultColumns}
|
||||
actions={actions}
|
||||
/>
|
||||
</EntityListContainer>
|
||||
</FilteredEntityLayout>
|
||||
</CatalogFilterLayout.Content>
|
||||
</CatalogFilterLayout>
|
||||
</EntityListProvider>
|
||||
</Content>
|
||||
</PageWithHeader>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { useAsyncEntity } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -44,7 +44,7 @@ type Props = {
|
||||
|
||||
export const EntityBadgesDialog = ({ open, onClose }: Props) => {
|
||||
const theme = useTheme();
|
||||
const { entity } = useEntity();
|
||||
const { entity } = useAsyncEntity();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const badgesApi = useApi(badgesApiRef);
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
catalog?: {
|
||||
processors?: {
|
||||
/**
|
||||
* GithubMultiOrgReaderProcessor configuration
|
||||
*/
|
||||
githubMultiOrg?: {
|
||||
/**
|
||||
* The configuration parameters for each GitHub org to process.
|
||||
*/
|
||||
orgs: Array<{
|
||||
/**
|
||||
* The name of the GitHub org to process.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The namespace of the group created for this org.
|
||||
*
|
||||
* Defaults to org name if omitted.
|
||||
*/
|
||||
groupNamespace?: string;
|
||||
|
||||
/**
|
||||
* The namespace of the users created from this org.
|
||||
*
|
||||
* Defaults to empty string if omitted.
|
||||
*/
|
||||
userNamespace?: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -52,6 +52,8 @@
|
||||
"@types/lodash": "^4.14.151"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
"dist",
|
||||
"config.d.ts"
|
||||
],
|
||||
"configSchema": "config.d.ts"
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export class LdapOrgReaderProcessor implements CatalogProcessor {
|
||||
});
|
||||
// (undocumented)
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
configRoot: Config,
|
||||
options: {
|
||||
logger: Logger;
|
||||
groupTransformer?: GroupTransformer;
|
||||
|
||||
+215
@@ -17,8 +17,223 @@
|
||||
import { JsonValue } from '@backstage/types';
|
||||
|
||||
export interface Config {
|
||||
/**
|
||||
* LdapOrgEntityProvider / LdapOrgReaderProcessor configuration
|
||||
*/
|
||||
ldap?: {
|
||||
/**
|
||||
* The configuration parameters for each single LDAP provider.
|
||||
*/
|
||||
providers: Array<{
|
||||
/**
|
||||
* The prefix of the target that this matches on, e.g.
|
||||
* "ldaps://ds.example.net", with no trailing slash.
|
||||
*/
|
||||
target: string;
|
||||
|
||||
/**
|
||||
* The settings to use for the bind command. If none are specified,
|
||||
* the bind command is not issued.
|
||||
*/
|
||||
bind?: {
|
||||
/**
|
||||
* The DN of the user to auth as.
|
||||
*
|
||||
* E.g. "uid=ldap-robot,ou=robots,ou=example,dc=example,dc=net"
|
||||
*/
|
||||
dn: string;
|
||||
/**
|
||||
* The secret of the user to auth as (its password).
|
||||
*
|
||||
* @visibility secret
|
||||
*/
|
||||
secret: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The settings that govern the reading and interpretation of users.
|
||||
*/
|
||||
users: {
|
||||
/**
|
||||
* The DN under which users are stored.
|
||||
*
|
||||
* E.g. "ou=people,ou=example,dc=example,dc=net"
|
||||
*/
|
||||
dn: string;
|
||||
/**
|
||||
* The search options to use. The default is scope "one" and
|
||||
* attributes "*" and "+".
|
||||
*
|
||||
* It is common to want to specify a filter, to narrow down the set
|
||||
* of matching items.
|
||||
*/
|
||||
options: {
|
||||
scope?: 'base' | 'one' | 'sub';
|
||||
filter?: string;
|
||||
attributes?: string | string[];
|
||||
sizeLimit?: number;
|
||||
timeLimit?: number;
|
||||
derefAliases?: number;
|
||||
typesOnly?: boolean;
|
||||
paged?:
|
||||
| boolean
|
||||
| {
|
||||
pageSize?: number;
|
||||
pagePause?: boolean;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* JSON paths (on a.b.c form) and hard coded values to set on those
|
||||
* paths.
|
||||
*
|
||||
* This can be useful for example if you want to hard code a
|
||||
* namespace or similar on the generated entities.
|
||||
*/
|
||||
set?: { [key: string]: JsonValue };
|
||||
/**
|
||||
* Mappings from well known entity fields, to LDAP attribute names
|
||||
*/
|
||||
map?: {
|
||||
/**
|
||||
* The name of the attribute that holds the relative
|
||||
* distinguished name of each entry. Defaults to "uid".
|
||||
*/
|
||||
rdn?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the value of
|
||||
* the metadata.name field of the entity. Defaults to "uid".
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the value of
|
||||
* the metadata.description field of the entity.
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the value of
|
||||
* the spec.profile.displayName field of the entity. Defaults to
|
||||
* "cn".
|
||||
*/
|
||||
displayName?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the value of
|
||||
* the spec.profile.email field of the entity. Defaults to
|
||||
* "mail".
|
||||
*/
|
||||
email?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the value of
|
||||
* the spec.profile.picture field of the entity.
|
||||
*/
|
||||
picture?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the values of
|
||||
* the spec.memberOf field of the entity. Defaults to "memberOf".
|
||||
*/
|
||||
memberOf?: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* The settings that govern the reading and interpretation of groups.
|
||||
*/
|
||||
groups: {
|
||||
/**
|
||||
* The DN under which groups are stored.
|
||||
*
|
||||
* E.g. "ou=people,ou=example,dc=example,dc=net"
|
||||
*/
|
||||
dn: string;
|
||||
/**
|
||||
* The search options to use. The default is scope "one" and
|
||||
* attributes "*" and "+".
|
||||
*
|
||||
* It is common to want to specify a filter, to narrow down the set
|
||||
* of matching items.
|
||||
*/
|
||||
options: {
|
||||
scope?: 'base' | 'one' | 'sub';
|
||||
filter?: string;
|
||||
attributes?: string | string[];
|
||||
sizeLimit?: number;
|
||||
timeLimit?: number;
|
||||
derefAliases?: number;
|
||||
typesOnly?: boolean;
|
||||
paged?:
|
||||
| boolean
|
||||
| {
|
||||
pageSize?: number;
|
||||
pagePause?: boolean;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* JSON paths (on a.b.c form) and hard coded values to set on those
|
||||
* paths.
|
||||
*
|
||||
* This can be useful for example if you want to hard code a
|
||||
* namespace or similar on the generated entities.
|
||||
*/
|
||||
set?: { [key: string]: JsonValue };
|
||||
/**
|
||||
* Mappings from well known entity fields, to LDAP attribute names
|
||||
*/
|
||||
map?: {
|
||||
/**
|
||||
* The name of the attribute that holds the relative
|
||||
* distinguished name of each entry. Defaults to "cn".
|
||||
*/
|
||||
rdn?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the value of
|
||||
* the metadata.name field of the entity. Defaults to "cn".
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the value of
|
||||
* the metadata.description field of the entity. Defaults to
|
||||
* "description".
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the value of
|
||||
* the spec.type field of the entity. Defaults to "groupType".
|
||||
*/
|
||||
type?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the value of
|
||||
* the spec.profile.displayName field of the entity. Defaults to
|
||||
* "cn".
|
||||
*/
|
||||
displayName?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the value of
|
||||
* the spec.profile.email field of the entity.
|
||||
*/
|
||||
email?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the value of
|
||||
* the spec.profile.picture field of the entity.
|
||||
*/
|
||||
picture?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the values of
|
||||
* the spec.parent field of the entity. Defaults to "memberOf".
|
||||
*/
|
||||
memberOf?: string;
|
||||
/**
|
||||
* The name of the attribute that shall be used for the values of
|
||||
* the spec.children field of the entity. Defaults to "member".
|
||||
*/
|
||||
members?: string;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Configuration options for the catalog plugin.
|
||||
*
|
||||
* TODO(freben): Deprecate this entire block
|
||||
*/
|
||||
catalog?: {
|
||||
/**
|
||||
|
||||
@@ -114,17 +114,11 @@ export class LdapOrgEntityProvider implements EntityProvider {
|
||||
const config =
|
||||
configRoot.getOptionalConfig('ldap') ||
|
||||
configRoot.getOptionalConfig('catalog.processors.ldapOrg');
|
||||
if (!config) {
|
||||
throw new TypeError(
|
||||
`There is no LDAP configuration. Please add it as "ldap.providers".`,
|
||||
);
|
||||
}
|
||||
|
||||
const providers = readLdapConfig(config);
|
||||
const providers = config ? readLdapConfig(config) : [];
|
||||
const provider = providers.find(p => options.target === p.target);
|
||||
if (!provider) {
|
||||
throw new TypeError(
|
||||
`There is no LDAP configuration that matches ${options.target}. Please add a configuration entry for it under "ldap.providers".`,
|
||||
`There is no LDAP configuration that matches "${options.target}". Please add a configuration entry for it under "ldap.providers".`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,17 +43,20 @@ export class LdapOrgReaderProcessor implements CatalogProcessor {
|
||||
private readonly userTransformer?: UserTransformer;
|
||||
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
configRoot: Config,
|
||||
options: {
|
||||
logger: Logger;
|
||||
groupTransformer?: GroupTransformer;
|
||||
userTransformer?: UserTransformer;
|
||||
},
|
||||
) {
|
||||
const c = config.getOptionalConfig('catalog.processors.ldapOrg');
|
||||
// TODO(freben): Deprecate the old catalog.processors.ldapOrg config
|
||||
const config =
|
||||
configRoot.getOptionalConfig('ldap') ||
|
||||
configRoot.getOptionalConfig('catalog.processors.ldapOrg');
|
||||
return new LdapOrgReaderProcessor({
|
||||
...options,
|
||||
providers: c ? readLdapConfig(c) : [],
|
||||
providers: config ? readLdapConfig(config) : [],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,7 +88,7 @@ export class LdapOrgReaderProcessor implements CatalogProcessor {
|
||||
const provider = this.providers.find(p => location.target === p.target);
|
||||
if (!provider) {
|
||||
throw new Error(
|
||||
`There is no LDAP Org provider that matches ${location.target}. Please add a configuration entry for it under catalog.processors.ldapOrg.providers.`,
|
||||
`There is no LDAP configuration that matches "${location.target}". Please add a configuration entry for it under "ldap.providers".`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -92,26 +92,31 @@ yarn add @backstage/plugin-catalog-backend-module-msgraph
|
||||
|
||||
4. The `MicrosoftGraphOrgEntityProvider` is not registered by default, so you
|
||||
have to register it in the catalog plugin. Pass the target to reference a
|
||||
provider from the configuration. As entity providers are not part of the
|
||||
entity refresh loop, you have to run them manually.
|
||||
provider from the configuration.
|
||||
|
||||
```typescript
|
||||
// packages/backend/src/plugins/catalog.ts
|
||||
const msGraphOrgEntityProvider = MicrosoftGraphOrgEntityProvider.fromConfig(
|
||||
env.config,
|
||||
{
|
||||
id: 'https://graph.microsoft.com/v1.0',
|
||||
target: 'https://graph.microsoft.com/v1.0',
|
||||
logger: env.logger,
|
||||
},
|
||||
);
|
||||
builder.addEntityProvider(msGraphOrgEntityProvider);
|
||||
```diff
|
||||
// packages/backend/src/plugins/catalog.ts
|
||||
+import { Duration } from 'luxon';
|
||||
+import { MicrosoftGraphOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-msgraph';
|
||||
|
||||
// Trigger a read every 5 minutes
|
||||
useHotCleanup(
|
||||
module,
|
||||
runPeriodically(() => msGraphOrgEntityProvider.read(), 5 * 60 * 1000),
|
||||
);
|
||||
export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
|
||||
+ // The target parameter below needs to match one of the providers' target
|
||||
+ // value specified in your app-config (see above).
|
||||
+ builder.addEntityProvider(
|
||||
+ MicrosoftGraphOrgEntityProvider.fromConfig(env.config, {
|
||||
+ id: 'production',
|
||||
+ target: 'https://graph.microsoft.com/v1.0',
|
||||
+ logger: env.logger,
|
||||
+ schedule: env.scheduler.createScheduledTaskRunner({
|
||||
+ frequency: Duration.fromObject({ minutes: 5 }),
|
||||
+ timeout: Duration.fromObject({ minutes: 3 }),
|
||||
+ }),
|
||||
+ }),
|
||||
+ );
|
||||
```
|
||||
|
||||
### Using the Processor
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Logger } from 'winston';
|
||||
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||
import * as msal from '@azure/msal-node';
|
||||
import { Response as Response_2 } from 'node-fetch';
|
||||
import { TaskRunner } from '@backstage/backend-tasks';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
|
||||
// @public
|
||||
@@ -119,19 +120,23 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
|
||||
connect(connection: EntityProviderConnection): Promise<void>;
|
||||
// (undocumented)
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
options: {
|
||||
id: string;
|
||||
target: string;
|
||||
logger: Logger;
|
||||
userTransformer?: UserTransformer;
|
||||
groupTransformer?: GroupTransformer;
|
||||
organizationTransformer?: OrganizationTransformer;
|
||||
},
|
||||
configRoot: Config,
|
||||
options: MicrosoftGraphOrgEntityProviderOptions,
|
||||
): MicrosoftGraphOrgEntityProvider;
|
||||
// (undocumented)
|
||||
getProviderName(): string;
|
||||
read(): Promise<void>;
|
||||
read(options?: { logger?: Logger }): Promise<void>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface MicrosoftGraphOrgEntityProviderOptions {
|
||||
groupTransformer?: GroupTransformer;
|
||||
id: string;
|
||||
logger: Logger;
|
||||
organizationTransformer?: OrganizationTransformer;
|
||||
schedule: 'manual' | TaskRunner;
|
||||
target: string;
|
||||
userTransformer?: UserTransformer;
|
||||
}
|
||||
|
||||
// @public
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/msal-node": "^1.1.0",
|
||||
"@backstage/backend-tasks": "^0.2.0",
|
||||
"@backstage/catalog-model": "^0.13.0",
|
||||
"@backstage/config": "^0.1.15",
|
||||
"@backstage/plugin-catalog-backend": "^0.24.0",
|
||||
@@ -42,6 +43,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"node-fetch": "^2.6.7",
|
||||
"p-limit": "^3.0.2",
|
||||
"uuid": "^8.0.0",
|
||||
"winston": "^3.2.1",
|
||||
"qs": "^6.9.4"
|
||||
},
|
||||
|
||||
+106
-18
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TaskRunner } from '@backstage/backend-tasks';
|
||||
import {
|
||||
ANNOTATION_LOCATION,
|
||||
ANNOTATION_ORIGIN_LOCATION,
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
EntityProviderConnection,
|
||||
} from '@backstage/plugin-catalog-backend';
|
||||
import { merge } from 'lodash';
|
||||
import * as uuid from 'uuid';
|
||||
import { Logger } from 'winston';
|
||||
import {
|
||||
GroupTransformer,
|
||||
@@ -39,6 +41,62 @@ import {
|
||||
UserTransformer,
|
||||
} from '../microsoftGraph';
|
||||
|
||||
/**
|
||||
* Options for {@link MicrosoftGraphOrgEntityProvider}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface MicrosoftGraphOrgEntityProviderOptions {
|
||||
/**
|
||||
* A unique, stable identifier for this provider.
|
||||
*
|
||||
* @example "production"
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The target that this provider should consume.
|
||||
*
|
||||
* Should exactly match the "target" field of one of the providers
|
||||
* configuration entries.
|
||||
*/
|
||||
target: string;
|
||||
|
||||
/**
|
||||
* The logger to use.
|
||||
*/
|
||||
logger: Logger;
|
||||
|
||||
/**
|
||||
* The refresh schedule to use.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* If you pass in 'manual', you are responsible for calling the `read` method
|
||||
* manually at some interval.
|
||||
*
|
||||
* But more commonly you will pass in the result of
|
||||
* {@link @backstage/backend-tasks#PluginTaskScheduler.createScheduledTaskRunner}
|
||||
* to enable automatic scheduling of tasks.
|
||||
*/
|
||||
schedule: 'manual' | TaskRunner;
|
||||
|
||||
/**
|
||||
* The function that transforms a user entry in msgraph to an entity.
|
||||
*/
|
||||
userTransformer?: UserTransformer;
|
||||
|
||||
/**
|
||||
* The function that transforms a group entry in msgraph to an entity.
|
||||
*/
|
||||
groupTransformer?: GroupTransformer;
|
||||
|
||||
/**
|
||||
* The function that transforms an organization entry in msgraph to an entity.
|
||||
*/
|
||||
organizationTransformer?: OrganizationTransformer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads user and group entries out of Microsoft Graph, and provides them as
|
||||
* User and Group entities for the catalog.
|
||||
@@ -47,25 +105,21 @@ import {
|
||||
*/
|
||||
export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
|
||||
private connection?: EntityProviderConnection;
|
||||
private scheduleFn?: () => Promise<void>;
|
||||
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
options: {
|
||||
id: string;
|
||||
target: string;
|
||||
logger: Logger;
|
||||
userTransformer?: UserTransformer;
|
||||
groupTransformer?: GroupTransformer;
|
||||
organizationTransformer?: OrganizationTransformer;
|
||||
},
|
||||
configRoot: Config,
|
||||
options: MicrosoftGraphOrgEntityProviderOptions,
|
||||
) {
|
||||
const c = config.getOptionalConfig('catalog.processors.microsoftGraphOrg');
|
||||
const providers = c ? readMicrosoftGraphConfig(c) : [];
|
||||
const config = configRoot.getOptionalConfig(
|
||||
'catalog.processors.microsoftGraphOrg',
|
||||
);
|
||||
const providers = config ? readMicrosoftGraphConfig(config) : [];
|
||||
const provider = providers.find(p => options.target.startsWith(p.target));
|
||||
|
||||
if (!provider) {
|
||||
throw new Error(
|
||||
`There is no Microsoft Graph Org provider that matches ${options.target}. Please add a configuration entry for it under catalog.processors.microsoftGraphOrg.providers.`,
|
||||
`There is no Microsoft Graph Org provider that matches "${options.target}". Please add a configuration entry for it under "catalog.processors.microsoftGraphOrg.providers".`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -73,7 +127,7 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
|
||||
target: options.target,
|
||||
});
|
||||
|
||||
return new MicrosoftGraphOrgEntityProvider({
|
||||
const result = new MicrosoftGraphOrgEntityProvider({
|
||||
id: options.id,
|
||||
userTransformer: options.userTransformer,
|
||||
groupTransformer: options.groupTransformer,
|
||||
@@ -81,6 +135,10 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
|
||||
logger,
|
||||
provider,
|
||||
});
|
||||
|
||||
result.schedule(options.schedule);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
constructor(
|
||||
@@ -102,19 +160,21 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
|
||||
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.connect} */
|
||||
async connect(connection: EntityProviderConnection) {
|
||||
this.connection = connection;
|
||||
await this.scheduleFn?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs one complete ingestion loop. Call this method regularly at some
|
||||
* appropriate cadence.
|
||||
*/
|
||||
async read() {
|
||||
async read(options?: { logger?: Logger }) {
|
||||
if (!this.connection) {
|
||||
throw new Error('Not initialized');
|
||||
}
|
||||
|
||||
const logger = options?.logger ?? this.options.logger;
|
||||
const provider = this.options.provider;
|
||||
const { markReadComplete } = trackProgress(this.options.logger);
|
||||
const { markReadComplete } = trackProgress(logger);
|
||||
const client = MicrosoftGraphClient.create(this.options.provider);
|
||||
|
||||
const { users, groups } = await readMicrosoftGraphOrg(
|
||||
@@ -130,7 +190,7 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
|
||||
groupTransformer: this.options.groupTransformer,
|
||||
userTransformer: this.options.userTransformer,
|
||||
organizationTransformer: this.options.organizationTransformer,
|
||||
logger: this.options.logger,
|
||||
logger: logger,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -146,6 +206,34 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider {
|
||||
|
||||
markCommitComplete();
|
||||
}
|
||||
|
||||
private schedule(
|
||||
schedule: MicrosoftGraphOrgEntityProviderOptions['schedule'],
|
||||
) {
|
||||
if (schedule === 'manual') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.scheduleFn = async () => {
|
||||
const id = `${this.getProviderName()}:refresh`;
|
||||
await schedule.run({
|
||||
id,
|
||||
fn: async () => {
|
||||
const logger = this.options.logger.child({
|
||||
class: MicrosoftGraphOrgEntityProvider.prototype.constructor.name,
|
||||
taskId: id,
|
||||
taskInstanceId: uuid.v4(),
|
||||
});
|
||||
|
||||
try {
|
||||
await this.read({ logger });
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Helps wrap the timing and logging behaviors
|
||||
@@ -173,12 +261,12 @@ function trackProgress(logger: Logger) {
|
||||
|
||||
// Makes sure that emitted entities have a proper location based on their uuid
|
||||
export function withLocations(providerId: string, entity: Entity): Entity {
|
||||
const uuid =
|
||||
const uid =
|
||||
entity.metadata.annotations?.[MICROSOFT_GRAPH_USER_ID_ANNOTATION] ||
|
||||
entity.metadata.annotations?.[MICROSOFT_GRAPH_GROUP_ID_ANNOTATION] ||
|
||||
entity.metadata.annotations?.[MICROSOFT_GRAPH_TENANT_ID_ANNOTATION] ||
|
||||
entity.metadata.name;
|
||||
const location = `msgraph:${providerId}/${encodeURIComponent(uuid)}`;
|
||||
const location = `msgraph:${providerId}/${encodeURIComponent(uid)}`;
|
||||
return merge(
|
||||
{
|
||||
metadata: {
|
||||
|
||||
@@ -15,4 +15,5 @@
|
||||
*/
|
||||
|
||||
export { MicrosoftGraphOrgEntityProvider } from './MicrosoftGraphOrgEntityProvider';
|
||||
export type { MicrosoftGraphOrgEntityProviderOptions } from './MicrosoftGraphOrgEntityProvider';
|
||||
export { MicrosoftGraphOrgReaderProcessor } from './MicrosoftGraphOrgReaderProcessor';
|
||||
|
||||
Vendored
-33
@@ -105,38 +105,5 @@ export interface Config {
|
||||
allow: Array<string>;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* List of processor-specific options and attributes
|
||||
*/
|
||||
processors?: {
|
||||
/**
|
||||
* GithubMultiOrgReaderProcessor configuration
|
||||
*/
|
||||
githubMultiOrg?: {
|
||||
/**
|
||||
* The configuration parameters for each GitHub org to process.
|
||||
*/
|
||||
orgs: Array<{
|
||||
/**
|
||||
* The name of the GitHub org to process.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The namespace of the group created for this org.
|
||||
*
|
||||
* Defaults to org name if omitted.
|
||||
*/
|
||||
groupNamespace?: string;
|
||||
|
||||
/**
|
||||
* The namespace of the users created from this org.
|
||||
*
|
||||
* Defaults to empty string if omitted.
|
||||
*/
|
||||
userNamespace?: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -63,6 +63,13 @@ export { CatalogApi };
|
||||
// @public
|
||||
export const catalogApiRef: ApiRef<CatalogApi>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const CatalogFilterLayout: {
|
||||
(props: { children: React_2.ReactNode }): JSX.Element;
|
||||
Filters: (props: { children: React_2.ReactNode }) => JSX.Element;
|
||||
Content: (props: { children: React_2.ReactNode }) => JSX.Element;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type CatalogReactComponentsNameToClassKey = {
|
||||
CatalogReactUserListPicker: CatalogReactUserListPickerClassKey;
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"@backstage/integration": "^0.8.0",
|
||||
"@backstage/plugin-permission-common": "^0.5.2",
|
||||
"@backstage/plugin-permission-react": "^0.3.3",
|
||||
"@backstage/theme": "^0.2.15",
|
||||
"@backstage/types": "^0.1.3",
|
||||
"@backstage/version-bridge": "^0.1.2",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
|
||||
+25
-4
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -25,10 +25,10 @@ import {
|
||||
useTheme,
|
||||
} from '@material-ui/core';
|
||||
import FilterListIcon from '@material-ui/icons/FilterList';
|
||||
import React, { useState } from 'react';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
|
||||
/** @public */
|
||||
export function FilterContainer(props: { children: React.ReactNode }) {
|
||||
export const Filters = (props: { children: React.ReactNode }) => {
|
||||
const isMidSizeScreen = useMediaQuery<BackstageTheme>(theme =>
|
||||
theme.breakpoints.down('md'),
|
||||
);
|
||||
@@ -69,4 +69,25 @@ export function FilterContainer(props: { children: React.ReactNode }) {
|
||||
{props.children}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export const Content = (props: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<Grid item xs={12} lg={10}>
|
||||
{props.children}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export const CatalogFilterLayout = (props: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<Grid container style={{ position: 'relative' }}>
|
||||
{props.children}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
CatalogFilterLayout.Filters = Filters;
|
||||
CatalogFilterLayout.Content = Content;
|
||||
+1
-11
@@ -14,14 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Grid } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
|
||||
/** @public */
|
||||
export function EntityListContainer(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Grid item xs={12} lg={10}>
|
||||
{props.children}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
export { CatalogFilterLayout } from './CatalogFilterLayout';
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './CatalogFilterLayout';
|
||||
export * from './EntityKindPicker';
|
||||
export * from './EntityLifecyclePicker';
|
||||
export * from './EntityOwnerPicker';
|
||||
|
||||
@@ -283,10 +283,10 @@ export interface EntityLinksCardProps {
|
||||
// @public (undocumented)
|
||||
export type EntityLinksEmptyStateClassKey = 'code';
|
||||
|
||||
// @public (undocumented)
|
||||
export function EntityListContainer(props: {
|
||||
children: React_2.ReactNode;
|
||||
}): JSX.Element;
|
||||
// @public @deprecated (undocumented)
|
||||
export const EntityListContainer: (props: {
|
||||
children: ReactNode;
|
||||
}) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export function EntityOrphanWarning(): JSX.Element;
|
||||
@@ -319,15 +319,13 @@ export interface EntitySwitchProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export function FilterContainer(props: {
|
||||
children: React_2.ReactNode;
|
||||
}): JSX.Element;
|
||||
// @public @deprecated (undocumented)
|
||||
export const FilterContainer: (props: { children: ReactNode }) => JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export function FilteredEntityLayout(props: {
|
||||
children: React_2.ReactNode;
|
||||
}): JSX.Element;
|
||||
// @public @deprecated (undocumented)
|
||||
export const FilteredEntityLayout: (props: {
|
||||
children: React.ReactNode;
|
||||
}) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export function hasCatalogProcessingErrors(
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from '@backstage/core-components';
|
||||
import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
CatalogFilterLayout,
|
||||
EntityLifecyclePicker,
|
||||
EntityListProvider,
|
||||
EntityOwnerPicker,
|
||||
@@ -36,11 +37,6 @@ import {
|
||||
import React from 'react';
|
||||
import { createComponentRouteRef } from '../../routes';
|
||||
import { CatalogTable, CatalogTableRow } from '../CatalogTable';
|
||||
import {
|
||||
FilteredEntityLayout,
|
||||
EntityListContainer,
|
||||
FilterContainer,
|
||||
} from '../FilteredEntityLayout';
|
||||
import { CatalogKindHeader } from '../CatalogKindHeader';
|
||||
|
||||
/**
|
||||
@@ -71,18 +67,18 @@ export function DefaultCatalogPage(props: DefaultCatalogPageProps) {
|
||||
/>
|
||||
<SupportButton>All your software catalog entities</SupportButton>
|
||||
</ContentHeader>
|
||||
<FilteredEntityLayout>
|
||||
<FilterContainer>
|
||||
<CatalogFilterLayout>
|
||||
<CatalogFilterLayout.Filters>
|
||||
<EntityTypePicker />
|
||||
<UserListPicker initialFilter={initiallySelectedFilter} />
|
||||
<EntityOwnerPicker />
|
||||
<EntityLifecyclePicker />
|
||||
<EntityTagPicker />
|
||||
</FilterContainer>
|
||||
<EntityListContainer>
|
||||
</CatalogFilterLayout.Filters>
|
||||
<CatalogFilterLayout.Content>
|
||||
<CatalogTable columns={columns} actions={actions} />
|
||||
</EntityListContainer>
|
||||
</FilteredEntityLayout>
|
||||
</CatalogFilterLayout.Content>
|
||||
</CatalogFilterLayout>
|
||||
</Content>
|
||||
</EntityListProvider>
|
||||
</PageWithHeader>
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
AsyncEntityProvider,
|
||||
EntityProvider,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { isKind } from './conditions';
|
||||
@@ -76,6 +79,18 @@ describe('EntitySwitch', () => {
|
||||
expect(rendered.queryByText('A')).not.toBeInTheDocument();
|
||||
expect(rendered.queryByText('B')).not.toBeInTheDocument();
|
||||
expect(rendered.queryByText('C')).toBeInTheDocument();
|
||||
|
||||
rendered.rerender(
|
||||
<Wrapper>
|
||||
<AsyncEntityProvider entity={undefined} loading={false}>
|
||||
{content}
|
||||
</AsyncEntityProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(rendered.queryByText('A')).not.toBeInTheDocument();
|
||||
expect(rendered.queryByText('B')).not.toBeInTheDocument();
|
||||
expect(rendered.queryByText('C')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should switch child when filters switch', () => {
|
||||
|
||||
@@ -63,7 +63,7 @@ export interface EntitySwitchProps {
|
||||
|
||||
/** @public */
|
||||
export const EntitySwitch = (props: EntitySwitchProps) => {
|
||||
const { entity } = useAsyncEntity();
|
||||
const { entity, loading } = useAsyncEntity();
|
||||
const apis = useApiHolder();
|
||||
const results = useElementFilter(
|
||||
props.children,
|
||||
@@ -75,11 +75,23 @@ export const EntitySwitch = (props: EntitySwitchProps) => {
|
||||
})
|
||||
.getElements()
|
||||
.flatMap<SwitchCaseResult>((element: ReactElement) => {
|
||||
if (!entity) {
|
||||
// Nothing is rendered while loading
|
||||
if (loading) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { if: condition, children: elementsChildren } =
|
||||
element.props as EntitySwitchCase;
|
||||
|
||||
// If the entity is missing or there is an error, render the default page
|
||||
if (!entity) {
|
||||
return [
|
||||
{
|
||||
if: condition === undefined,
|
||||
children: elementsChildren,
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
if: condition?.(entity, { apis }) ?? true,
|
||||
@@ -87,7 +99,7 @@ export const EntitySwitch = (props: EntitySwitchProps) => {
|
||||
},
|
||||
];
|
||||
}),
|
||||
[apis, entity],
|
||||
[apis, entity, loading],
|
||||
);
|
||||
const hasAsyncCases = results.some(
|
||||
r => typeof r.if === 'object' && 'then' in r.if,
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
/** @public */
|
||||
export function FilteredEntityLayout(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Grid container style={{ position: 'relative' }}>
|
||||
{props.children}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,24 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { FilteredEntityLayout } from './FilteredEntityLayout';
|
||||
export { FilterContainer } from './FilterContainer';
|
||||
export { EntityListContainer } from './EntityListContainer';
|
||||
import { CatalogFilterLayout } from '@backstage/plugin-catalog-react';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use `FilteredCatalogLayout` from `@backstage/plugin-catalog-react` instead.
|
||||
*/
|
||||
export const FilteredEntityLayout = CatalogFilterLayout as (props: {
|
||||
children: React.ReactNode;
|
||||
}) => JSX.Element;
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use `FilteredCatalogLayout.Filters` from `@backstage/plugin-catalog-react` instead.
|
||||
*/
|
||||
export const FilterContainer = CatalogFilterLayout.Filters;
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use `FilteredCatalogLayout.Content` from `@backstage/plugin-catalog-react` instead.
|
||||
*/
|
||||
export const EntityListContainer = CatalogFilterLayout.Content;
|
||||
|
||||
@@ -137,6 +137,17 @@ export function createGithubActionsDispatchAction(options: {
|
||||
token?: string | undefined;
|
||||
}>;
|
||||
|
||||
// @public
|
||||
export function createGithubIssuesLabelAction(options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
}): TemplateAction<{
|
||||
repoUrl: string;
|
||||
number: number;
|
||||
labels: string[];
|
||||
token?: string | undefined;
|
||||
}>;
|
||||
|
||||
// @public
|
||||
export interface CreateGithubPullRequestActionOptions {
|
||||
clientFactory?: (
|
||||
@@ -368,6 +379,7 @@ export interface OctokitWithPullRequestPluginClient {
|
||||
createPullRequest(options: createPullRequest.Options): Promise<{
|
||||
data: {
|
||||
html_url: string;
|
||||
number: number;
|
||||
};
|
||||
} | null>;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
import {
|
||||
createGithubActionsDispatchAction,
|
||||
createGithubWebhookAction,
|
||||
createGithubIssuesLabelAction,
|
||||
} from './github';
|
||||
import { TemplateFilter } from '../../../lib';
|
||||
import { TemplateAction } from '../types';
|
||||
@@ -145,6 +146,10 @@ export const createBuiltinActions = (
|
||||
integrations,
|
||||
githubCredentialsProvider,
|
||||
}),
|
||||
createGithubIssuesLabelAction({
|
||||
integrations,
|
||||
githubCredentialsProvider,
|
||||
}),
|
||||
];
|
||||
|
||||
return actions as TemplateAction<JsonObject>[];
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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 { createGithubIssuesLabelAction } from './githubIssuesLabel';
|
||||
import {
|
||||
ScmIntegrations,
|
||||
DefaultGithubCredentialsProvider,
|
||||
GithubCredentialsProvider,
|
||||
} from '@backstage/integration';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { PassThrough } from 'stream';
|
||||
import { TemplateAction } from '../..';
|
||||
|
||||
const mockOctokit = {
|
||||
rest: {
|
||||
issues: {
|
||||
addLabels: jest.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
jest.mock('octokit', () => ({
|
||||
Octokit: class {
|
||||
constructor() {
|
||||
return mockOctokit;
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
describe('github:issues:label', () => {
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
github: [
|
||||
{ host: 'github.com', token: 'tokenlols' },
|
||||
{ host: 'ghe.github.com' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
let githubCredentialsProvider: GithubCredentialsProvider;
|
||||
let action: TemplateAction<any>;
|
||||
|
||||
const mockContext = {
|
||||
input: {
|
||||
repoUrl: 'github.com?repo=repo&owner=owner',
|
||||
number: '1',
|
||||
labels: ['label1', 'label2'],
|
||||
},
|
||||
workspacePath: 'lol',
|
||||
logger: getVoidLogger(),
|
||||
logStream: new PassThrough(),
|
||||
output: jest.fn(),
|
||||
createTemporaryDirectory: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
githubCredentialsProvider =
|
||||
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
|
||||
action = createGithubIssuesLabelAction({
|
||||
integrations,
|
||||
githubCredentialsProvider,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call the githubApi for adding labels', async () => {
|
||||
await action.handler(mockContext);
|
||||
expect(mockOctokit.rest.issues.addLabels).toHaveBeenCalledWith({
|
||||
owner: 'owner',
|
||||
repo: 'repo',
|
||||
issue_number: '1',
|
||||
labels: ['label1', 'label2'],
|
||||
});
|
||||
});
|
||||
});
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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 {
|
||||
GithubCredentialsProvider,
|
||||
ScmIntegrationRegistry,
|
||||
} from '@backstage/integration';
|
||||
import { createTemplateAction } from '../../createTemplateAction';
|
||||
import { assertError, InputError } from '@backstage/errors';
|
||||
import { Octokit } from 'octokit';
|
||||
import { getOctokitOptions } from './helpers';
|
||||
import { parseRepoUrl } from '../publish/util';
|
||||
|
||||
/**
|
||||
* Adds labels to a pull request or issue on GitHub
|
||||
* @public
|
||||
*/
|
||||
export function createGithubIssuesLabelAction(options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
}) {
|
||||
const { integrations, githubCredentialsProvider } = options;
|
||||
|
||||
return createTemplateAction<{
|
||||
repoUrl: string;
|
||||
number: number;
|
||||
labels: string[];
|
||||
token?: string;
|
||||
}>({
|
||||
id: 'github:issues:label',
|
||||
description: 'Adds labels to a pull request or issue on GitHub.',
|
||||
schema: {
|
||||
input: {
|
||||
type: 'object',
|
||||
required: ['repoUrl', 'number', 'labels'],
|
||||
properties: {
|
||||
repoUrl: {
|
||||
title: 'Repository Location',
|
||||
description: `Accepts the format 'github.com?repo=reponame&owner=owner' where 'reponame' is the repository name and 'owner' is an organization or username`,
|
||||
type: 'string',
|
||||
},
|
||||
number: {
|
||||
title: 'Pull Request or issue number',
|
||||
description: 'The pull request or issue number to add labels to',
|
||||
type: 'number',
|
||||
},
|
||||
labels: {
|
||||
title: 'Labels',
|
||||
description: 'The labels to add to the pull request or issue',
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
token: {
|
||||
title: 'Authentication Token',
|
||||
type: 'string',
|
||||
description: 'The GITHUB_TOKEN to use for authorization to GitHub',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async handler(ctx) {
|
||||
const { repoUrl, number, labels, token: providedToken } = ctx.input;
|
||||
|
||||
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
|
||||
ctx.logger.info(`Adding labels to ${number} issue on repo ${repo}`);
|
||||
|
||||
if (!owner) {
|
||||
throw new InputError('Invalid repository owner provided in repoUrl');
|
||||
}
|
||||
|
||||
const client = new Octokit(
|
||||
await getOctokitOptions({
|
||||
integrations,
|
||||
credentialsProvider: githubCredentialsProvider,
|
||||
repoUrl: repoUrl,
|
||||
token: providedToken,
|
||||
}),
|
||||
);
|
||||
|
||||
try {
|
||||
await client.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: number,
|
||||
labels,
|
||||
});
|
||||
} catch (e) {
|
||||
assertError(e);
|
||||
ctx.logger.warn(
|
||||
`Failed: adding labels to issue: '${number}' on repo: '${repo}', ${e.message}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -16,3 +16,4 @@
|
||||
|
||||
export { createGithubActionsDispatchAction } from './githubActionsDispatch';
|
||||
export { createGithubWebhookAction } from './githubWebhook';
|
||||
export { createGithubIssuesLabelAction } from './githubIssuesLabel';
|
||||
|
||||
+9
-4
@@ -58,6 +58,7 @@ describe('createPublishGithubPullRequestAction', () => {
|
||||
status: 201,
|
||||
data: {
|
||||
html_url: 'https://github.com/myorg/myrepo/pull/123',
|
||||
number: 123,
|
||||
},
|
||||
};
|
||||
}),
|
||||
@@ -123,13 +124,14 @@ describe('createPublishGithubPullRequestAction', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('creates outputs for the url', async () => {
|
||||
it('creates outputs for the pull request url and number', async () => {
|
||||
await instance.handler(ctx);
|
||||
|
||||
expect(ctx.output).toHaveBeenCalledWith(
|
||||
'remoteUrl',
|
||||
'https://github.com/myorg/myrepo/pull/123',
|
||||
);
|
||||
expect(ctx.output).toHaveBeenCalledWith('pullRequestNumber', 123);
|
||||
});
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
@@ -254,13 +256,14 @@ describe('createPublishGithubPullRequestAction', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('creates outputs for the url', async () => {
|
||||
it('creates outputs for the pull request url and number', async () => {
|
||||
await instance.handler(ctx);
|
||||
|
||||
expect(ctx.output).toHaveBeenCalledWith(
|
||||
'remoteUrl',
|
||||
'https://github.com/myorg/myrepo/pull/123',
|
||||
);
|
||||
expect(ctx.output).toHaveBeenCalledWith('pullRequestNumber', 123);
|
||||
});
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
@@ -322,13 +325,14 @@ describe('createPublishGithubPullRequestAction', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('creates outputs for the url', async () => {
|
||||
it('creates outputs for the pull request url and number', async () => {
|
||||
await instance.handler(ctx);
|
||||
|
||||
expect(ctx.output).toHaveBeenCalledWith(
|
||||
'remoteUrl',
|
||||
'https://github.com/myorg/myrepo/pull/123',
|
||||
);
|
||||
expect(ctx.output).toHaveBeenCalledWith('pullRequestNumber', 123);
|
||||
});
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
@@ -390,13 +394,14 @@ describe('createPublishGithubPullRequestAction', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('creates outputs for the url', async () => {
|
||||
it('creates outputs for the pull request url and number', async () => {
|
||||
await instance.handler(ctx);
|
||||
|
||||
expect(ctx.output).toHaveBeenCalledWith(
|
||||
'remoteUrl',
|
||||
'https://github.com/myorg/myrepo/pull/123',
|
||||
);
|
||||
expect(ctx.output).toHaveBeenCalledWith('pullRequestNumber', 123);
|
||||
});
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
|
||||
+10
-1
@@ -37,7 +37,10 @@ class GithubResponseError extends CustomErrorBase {}
|
||||
/** @public */
|
||||
export interface OctokitWithPullRequestPluginClient {
|
||||
createPullRequest(options: createPullRequest.Options): Promise<{
|
||||
data: { html_url: string };
|
||||
data: {
|
||||
html_url: string;
|
||||
number: number;
|
||||
};
|
||||
} | null>;
|
||||
}
|
||||
|
||||
@@ -169,6 +172,11 @@ export const createPublishGithubPullRequestAction = ({
|
||||
title: 'Pull Request URL',
|
||||
description: 'Link to the pull request in Github',
|
||||
},
|
||||
pullRequestNumber: {
|
||||
type: 'number',
|
||||
title: 'Pull Request Number',
|
||||
description: 'The pull request number',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -263,6 +271,7 @@ export const createPublishGithubPullRequestAction = ({
|
||||
}
|
||||
|
||||
ctx.output('remoteUrl', response.data.html_url);
|
||||
ctx.output('pullRequestNumber', response.data.number);
|
||||
} catch (e) {
|
||||
throw new GithubResponseError('Pull request creation failed', e);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
ContentHeader,
|
||||
CreateButton,
|
||||
Header,
|
||||
Lifecycle,
|
||||
Page,
|
||||
SupportButton,
|
||||
} from '@backstage/core-components';
|
||||
@@ -27,13 +26,13 @@ import { Entity } from '@backstage/catalog-model';
|
||||
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
|
||||
import { useRouteRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
CatalogFilterLayout,
|
||||
EntityKindPicker,
|
||||
EntityListProvider,
|
||||
EntitySearchBar,
|
||||
EntityTagPicker,
|
||||
UserListPicker,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
import React, { ComponentType } from 'react';
|
||||
import { registerComponentRouteRef } from '../../routes';
|
||||
import { TemplateList } from '../TemplateList';
|
||||
@@ -41,15 +40,6 @@ import { TemplateTypePicker } from '../TemplateTypePicker';
|
||||
import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common';
|
||||
import { usePermission } from '@backstage/plugin-permission-react';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
contentWrapper: {
|
||||
display: 'grid',
|
||||
gridTemplateAreas: "'filters' 'grid'",
|
||||
gridTemplateColumns: '250px 1fr',
|
||||
gridColumnGap: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
export type ScaffolderPageProps = {
|
||||
TemplateCardComponent?:
|
||||
| ComponentType<{ template: TemplateEntityV1beta3 }>
|
||||
@@ -66,7 +56,6 @@ export const ScaffolderPageContents = ({
|
||||
TemplateCardComponent,
|
||||
groups,
|
||||
}: ScaffolderPageProps) => {
|
||||
const styles = useStyles();
|
||||
const registerComponentLink = useRouteRef(registerComponentRouteRef);
|
||||
const otherTemplatesGroup = {
|
||||
title: groups ? 'Other Templates' : 'Templates',
|
||||
@@ -82,11 +71,7 @@ export const ScaffolderPageContents = ({
|
||||
<Page themeId="home">
|
||||
<Header
|
||||
pageTitleOverride="Create a New Component"
|
||||
title={
|
||||
<>
|
||||
Create a New Component <Lifecycle shorthand />
|
||||
</>
|
||||
}
|
||||
title="Create a New Component"
|
||||
subtitle="Create new software components using standard templates"
|
||||
/>
|
||||
<Content>
|
||||
@@ -104,8 +89,8 @@ export const ScaffolderPageContents = ({
|
||||
</SupportButton>
|
||||
</ContentHeader>
|
||||
|
||||
<div className={styles.contentWrapper}>
|
||||
<div>
|
||||
<CatalogFilterLayout>
|
||||
<CatalogFilterLayout.Filters>
|
||||
<EntitySearchBar />
|
||||
<EntityKindPicker initialFilter="template" hidden />
|
||||
<UserListPicker
|
||||
@@ -114,8 +99,8 @@ export const ScaffolderPageContents = ({
|
||||
/>
|
||||
<TemplateTypePicker />
|
||||
<EntityTagPicker />
|
||||
</div>
|
||||
<div>
|
||||
</CatalogFilterLayout.Filters>
|
||||
<CatalogFilterLayout.Content>
|
||||
{groups &&
|
||||
groups.map((group, index) => (
|
||||
<TemplateList
|
||||
@@ -129,8 +114,8 @@ export const ScaffolderPageContents = ({
|
||||
TemplateCardComponent={TemplateCardComponent}
|
||||
group={otherTemplatesGroup}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CatalogFilterLayout.Content>
|
||||
</CatalogFilterLayout>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
Content,
|
||||
ErrorPage,
|
||||
Header,
|
||||
Lifecycle,
|
||||
Page,
|
||||
LogViewer,
|
||||
Progress,
|
||||
@@ -315,11 +314,7 @@ export const TaskPage = ({ loadingText }: TaskPageProps) => {
|
||||
<Page themeId="home">
|
||||
<Header
|
||||
pageTitleOverride={`Task ${taskId}`}
|
||||
title={
|
||||
<>
|
||||
Task Activity <Lifecycle alpha shorthand />
|
||||
</>
|
||||
}
|
||||
title="Task Activity"
|
||||
subtitle={`Activity for task: ${taskId}`}
|
||||
/>
|
||||
<Content>
|
||||
|
||||
@@ -27,13 +27,7 @@ import { SecretsContext } from '../secrets/SecretsContext';
|
||||
import { rootRouteRef, scaffolderTaskRouteRef } from '../../routes';
|
||||
import { MultistepJsonForm } from '../MultistepJsonForm';
|
||||
|
||||
import {
|
||||
Content,
|
||||
Header,
|
||||
InfoCard,
|
||||
Lifecycle,
|
||||
Page,
|
||||
} from '@backstage/core-components';
|
||||
import { Content, Header, InfoCard, Page } from '@backstage/core-components';
|
||||
import {
|
||||
ApiHolder,
|
||||
errorApiRef,
|
||||
@@ -183,11 +177,7 @@ export const TemplatePage = ({
|
||||
<Page themeId="home">
|
||||
<Header
|
||||
pageTitleOverride="Create a New Component"
|
||||
title={
|
||||
<>
|
||||
Create a New Component <Lifecycle shorthand />
|
||||
</>
|
||||
}
|
||||
title="Create a New Component"
|
||||
subtitle="Create new software components using standard templates"
|
||||
/>
|
||||
<Content>
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
"@backstage/errors": "^0.2.2",
|
||||
"@backstage/integration": "^0.8.0",
|
||||
"@backstage/integration-react": "^0.1.25",
|
||||
"@backstage/plugin-catalog": "^0.10.0",
|
||||
"@backstage/plugin-catalog-react": "^0.9.0",
|
||||
"@backstage/plugin-search": "^0.7.3",
|
||||
"@backstage/theme": "^0.2.15",
|
||||
|
||||
@@ -23,11 +23,7 @@ import {
|
||||
TableProps,
|
||||
} from '@backstage/core-components';
|
||||
import {
|
||||
EntityListContainer,
|
||||
FilterContainer,
|
||||
FilteredEntityLayout,
|
||||
} from '@backstage/plugin-catalog';
|
||||
import {
|
||||
CatalogFilterLayout,
|
||||
EntityListProvider,
|
||||
EntityOwnerPicker,
|
||||
EntityTagPicker,
|
||||
@@ -65,17 +61,17 @@ export const DefaultTechDocsHome = (props: DefaultTechDocsHomeProps) => {
|
||||
</SupportButton>
|
||||
</ContentHeader>
|
||||
<EntityListProvider>
|
||||
<FilteredEntityLayout>
|
||||
<FilterContainer>
|
||||
<CatalogFilterLayout>
|
||||
<CatalogFilterLayout.Filters>
|
||||
<TechDocsPicker />
|
||||
<UserListPicker initialFilter={initialFilter} />
|
||||
<EntityOwnerPicker />
|
||||
<EntityTagPicker />
|
||||
</FilterContainer>
|
||||
<EntityListContainer>
|
||||
</CatalogFilterLayout.Filters>
|
||||
<CatalogFilterLayout.Content>
|
||||
<EntityListDocsTable actions={actions} columns={columns} />
|
||||
</EntityListContainer>
|
||||
</FilteredEntityLayout>
|
||||
</CatalogFilterLayout.Content>
|
||||
</CatalogFilterLayout>
|
||||
</EntityListProvider>
|
||||
</Content>
|
||||
</TechDocsPageWrapper>
|
||||
|
||||
@@ -2010,13 +2010,13 @@
|
||||
tslib "~2.3.0"
|
||||
|
||||
"@graphql-codegen/typescript@^2.4.2":
|
||||
version "2.4.5"
|
||||
resolved "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-2.4.5.tgz#80ca7a343511a27cd587c0b220b0a0dd99a4dc4b"
|
||||
integrity sha512-Ytb8phNHKl/v/wxudsMOAV1dmzIbckWHm2J83PLNOvnu9CGEhgsd67vfe3ZoF95VU2BKSG8BXGa6uL9z2xDmuA==
|
||||
version "2.4.7"
|
||||
resolved "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-2.4.7.tgz#e72e87cd47a51b2bc232293762573a86182de469"
|
||||
integrity sha512-1YAK+m9SZRS1Uy8hdXVQAaAG/WF2jdFD76wbMRQ8Lf5e9YbmtJuy1RpI13nZJTZbhFopd9zqoWFJHsU/oy4i7g==
|
||||
dependencies:
|
||||
"@graphql-codegen/plugin-helpers" "^2.4.0"
|
||||
"@graphql-codegen/schema-ast" "^2.4.1"
|
||||
"@graphql-codegen/visitor-plugin-common" "2.7.1"
|
||||
"@graphql-codegen/visitor-plugin-common" "2.7.3"
|
||||
auto-bind "~4.0.0"
|
||||
tslib "~2.3.0"
|
||||
|
||||
@@ -2052,6 +2052,22 @@
|
||||
parse-filepath "^1.0.2"
|
||||
tslib "~2.3.0"
|
||||
|
||||
"@graphql-codegen/visitor-plugin-common@2.7.3":
|
||||
version "2.7.3"
|
||||
resolved "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-2.7.3.tgz#f374e8974ae4ab7b1059a7f8748db6393f20c138"
|
||||
integrity sha512-rODPGkrHEf9aHD8SgVWS0xie8VmJoYXLWgQR8PFKrwCUFjf7nvDuATAL2lTFiHATlVBWg1meXVPdUlkKeJj/0Q==
|
||||
dependencies:
|
||||
"@graphql-codegen/plugin-helpers" "^2.4.0"
|
||||
"@graphql-tools/optimize" "^1.0.1"
|
||||
"@graphql-tools/relay-operation-optimizer" "^6.3.7"
|
||||
"@graphql-tools/utils" "^8.3.0"
|
||||
auto-bind "~4.0.0"
|
||||
change-case-all "1.0.14"
|
||||
dependency-graph "^0.11.0"
|
||||
graphql-tag "^2.11.0"
|
||||
parse-filepath "^1.0.2"
|
||||
tslib "~2.3.0"
|
||||
|
||||
"@graphql-tools/apollo-engine-loader@^7.0.5":
|
||||
version "7.2.1"
|
||||
resolved "https://registry.npmjs.org/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-7.2.1.tgz#14e5d0b1032a7d882d22a7533c8969ee3fa797f2"
|
||||
@@ -5105,13 +5121,13 @@
|
||||
pretty-format "^27.0.2"
|
||||
|
||||
"@testing-library/jest-dom@^5.10.1":
|
||||
version "5.14.1"
|
||||
resolved "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.14.1.tgz#8501e16f1e55a55d675fe73eecee32cdaddb9766"
|
||||
integrity sha512-dfB7HVIgTNCxH22M1+KU6viG5of2ldoA5ly8Ar8xkezKHKXjRvznCdbMbqjYGgO2xjRbwnR+rR8MLUIqF3kKbQ==
|
||||
version "5.16.2"
|
||||
resolved "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.2.tgz#f329b36b44aa6149cd6ced9adf567f8b6aa1c959"
|
||||
integrity sha512-6ewxs1MXWwsBFZXIk4nKKskWANelkdUehchEOokHsN8X7c2eKXGw+77aRV63UU8f/DTSVUPLaGxdrj4lN7D/ug==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.9.2"
|
||||
"@types/testing-library__jest-dom" "^5.9.1"
|
||||
aria-query "^4.2.2"
|
||||
aria-query "^5.0.0"
|
||||
chalk "^3.0.0"
|
||||
css "^3.0.0"
|
||||
css.escape "^1.5.1"
|
||||
@@ -5140,9 +5156,9 @@
|
||||
"@types/react-dom" "*"
|
||||
|
||||
"@testing-library/user-event@^13.1.8":
|
||||
version "13.1.8"
|
||||
resolved "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.1.8.tgz#9cbf342b88d837ee188f9f9f4df6d1beaaf179c2"
|
||||
integrity sha512-M04HgOlJvxILf5xyrkJaEQfFOtcvhy3usLldQIEg9zgFIYQofSmFGVfFlS7BWowqlBGLrItwGMlPXCoBgoHSiw==
|
||||
version "13.5.0"
|
||||
resolved "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz#69d77007f1e124d55314a2b73fd204b333b13295"
|
||||
integrity sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
|
||||
@@ -17391,10 +17407,10 @@ miller-rabin@^4.0.0:
|
||||
bn.js "^4.0.0"
|
||||
brorand "^1.0.1"
|
||||
|
||||
mime-db@1.51.0, "mime-db@>= 1.43.0 < 2":
|
||||
version "1.51.0"
|
||||
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c"
|
||||
integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==
|
||||
mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
|
||||
version "1.52.0"
|
||||
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-db@~1.33.0:
|
||||
version "1.33.0"
|
||||
@@ -17409,11 +17425,11 @@ mime-types@2.1.18:
|
||||
mime-db "~1.33.0"
|
||||
|
||||
mime-types@^2.0.8, mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34:
|
||||
version "2.1.34"
|
||||
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24"
|
||||
integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==
|
||||
version "2.1.35"
|
||||
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
dependencies:
|
||||
mime-db "1.51.0"
|
||||
mime-db "1.52.0"
|
||||
|
||||
mime@1.6.0, mime@^1.3.4:
|
||||
version "1.6.0"
|
||||
|
||||
Reference in New Issue
Block a user