Merge remote-tracking branch 'origin/master' into cost-insight-trending-line
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
---
|
||||
'@backstage/plugin-events-backend': minor
|
||||
---
|
||||
|
||||
**BREAKING:** Remove required field `router` at `HttpPostIngressEventPublisher.fromConfig`
|
||||
and replace it with `bind(router: Router)`.
|
||||
Additionally, the path prefix `/http` will be added inside `HttpPostIngressEventPublisher`.
|
||||
|
||||
```diff
|
||||
// at packages/backend/src/plugins/events.ts
|
||||
const eventsRouter = Router();
|
||||
- const httpRouter = Router();
|
||||
- eventsRouter.use('/http', httpRouter);
|
||||
|
||||
const http = HttpPostIngressEventPublisher.fromConfig({
|
||||
config: env.config,
|
||||
logger: env.logger,
|
||||
- router: httpRouter,
|
||||
});
|
||||
+ http.bind(eventsRouter);
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Use Json types from @backstage/types
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
Create a variable for minimum height and add a prop named 'fit' for determining if the graph height should grow or be contained.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-explore': patch
|
||||
---
|
||||
|
||||
Adds styling to graph forcing it to always fill out the available space.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
'@backstage/plugin-events-backend': patch
|
||||
'@backstage/plugin-events-node': minor
|
||||
---
|
||||
|
||||
Introduce a new interface `RequestDetails` to abstract `Request`
|
||||
providing access to request body and headers.
|
||||
|
||||
**BREAKING:** Replace `request: Request` with `request: RequestDetails` at `RequestValidator`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-app-api': patch
|
||||
---
|
||||
|
||||
Added `lifecycleFactory` implementation.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
'@backstage/plugin-codescene': patch
|
||||
'@backstage/plugin-sonarqube': patch
|
||||
---
|
||||
|
||||
Updated dependency `rc-progress` to `3.4.1`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Registered shutdown hook in experimental catalog plugin.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Fixed Entity kind pluralisation in the `CatalogKindHeader` component.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-vault-backend': patch
|
||||
---
|
||||
|
||||
Use `express-promise-router` to catch errors properly.
|
||||
Add `403` error as a known one. It will now return a `NotAllowed` error.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-defaults': patch
|
||||
---
|
||||
|
||||
Added `lifecycleFactory` to default service factories.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-plugin-api': patch
|
||||
---
|
||||
|
||||
Added initial support for registering shutdown hooks via `lifecycleServiceRef`.
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Automate merge message
|
||||
on:
|
||||
pull_request:
|
||||
branches: ['master']
|
||||
types: ['closed']
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: none
|
||||
checks: none
|
||||
contents: none
|
||||
deployments: none
|
||||
issues: none
|
||||
packages: none
|
||||
pages: none
|
||||
repository-projects: none
|
||||
security-events: none
|
||||
statuses: none
|
||||
|
||||
jobs:
|
||||
message:
|
||||
# prevent running towards forks, and only run on merged PRs
|
||||
if: github.repository == 'backstage/backstage' && github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: '${{ github.event.pull_request.merge_commit_sha }}'
|
||||
|
||||
- name: fetch base
|
||||
run: git fetch --depth 1 origin ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
# We avoid using the in-source script since this workflow has elevated permissions that we don't want to expose
|
||||
- name: Generate Message
|
||||
id: generate-message
|
||||
run: |
|
||||
rm -f generate.js
|
||||
wget -O generate.js https://raw.githubusercontent.com/backstage/backstage/master/scripts/generate-merge-message.js 1>&2
|
||||
node generate.js FETCH_HEAD > message.txt
|
||||
|
||||
- name: Post Message
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
ISSUE_NUMBER: ${{ github.event.pull_request.number }}
|
||||
with:
|
||||
script: |
|
||||
const owner = "backstage";
|
||||
const repo = "backstage";
|
||||
const body = require('fs').readFileSync('message.txt', 'utf8').trim();
|
||||
const issue_number = Number(process.env.ISSUE_NUMBER);
|
||||
|
||||
if (!body) {
|
||||
console.log(`skipping comment for #${issue_number}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`creating comment for #${issue_number}`);
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
body,
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -159,7 +159,12 @@ Check out the numbered markings - let's go through them one by one.
|
||||
the outcome of that. This example issues a `fetch` to the right service and
|
||||
issues a full refresh of its entity bucket based on that.
|
||||
5. The method translates the foreign data model to the native `Entity` form, as
|
||||
expected by the catalog.
|
||||
expected by the catalog. The `Entity` must include the
|
||||
`backstage.io/managed-by-location` and
|
||||
`backstage.io/managed-by-origin-location annotations`; otherwise, it will not
|
||||
appear in the Catalog and will generate warning logs. The
|
||||
[Well-known Annotations](./well-known-annotations.md#backstageiomanaged-by-location)
|
||||
documentation has guidance on what values to use for these.
|
||||
6. Finally, we issue a "mutation" to the catalog. This persists the entities in
|
||||
our own bucket, along with an optional `locationKey` that's used for conflict
|
||||
checks. But this is a bigger topic - mutations warrant their own explanatory
|
||||
|
||||
@@ -393,6 +393,98 @@ You can add more icons, if the [default icons](https://github.com/backstage/back
|
||||
|
||||
Note: If the icon is not available as one of the default icons or one you've added then it will fall back to Material UI's `LanguageIcon`
|
||||
|
||||
## Custom Sidebar
|
||||
|
||||
As you've seen there are many ways that you can customize your Backstage app. The following section will show you how you can customize the sidebar.
|
||||
|
||||
### Sidebar Sub-menu
|
||||
|
||||
For this example we'll show you how you can expand the sidebar with a sub-menu:
|
||||
|
||||
1. Open the `Root.tsx` file located in `packages/app/src/components/Root` as this is where the sidebar code lives
|
||||
2. Then we want to add the following imports for the icons:
|
||||
|
||||
```ts
|
||||
import ApiIcon from '@material-ui/icons/Extension';
|
||||
import ComponentIcon from '@material-ui/icons/Memory';
|
||||
import DomainIcon from '@material-ui/icons/Apartment';
|
||||
import ResourceIcon from '@material-ui/icons/Work';
|
||||
import SystemIcon from '@material-ui/icons/Category';
|
||||
import UserIcon from '@material-ui/icons/Person';
|
||||
```
|
||||
|
||||
3. Then update the `@backstage/core-components` import like this:
|
||||
|
||||
```diff
|
||||
import {
|
||||
Sidebar,
|
||||
sidebarConfig,
|
||||
SidebarDivider,
|
||||
SidebarGroup,
|
||||
SidebarItem,
|
||||
SidebarPage,
|
||||
SidebarScrollWrapper,
|
||||
SidebarSpace,
|
||||
useSidebarOpenState,
|
||||
Link,
|
||||
+ GroupIcon,
|
||||
+ SidebarSubmenu,
|
||||
+ SidebarSubmenuItem,
|
||||
} from '@backstage/core-components';
|
||||
```
|
||||
|
||||
4. Finally replace `<SidebarItem icon={HomeIcon} to="catalog" text="Home" />` with this:
|
||||
|
||||
```ts
|
||||
<SidebarItem icon={HomeIcon} to="catalog" text="Home">
|
||||
<SidebarSubmenu title="Catalog">
|
||||
<SidebarSubmenuItem
|
||||
title="Domains"
|
||||
to="catalog?filters[kind]=domain"
|
||||
icon={DomainIcon}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="Systems"
|
||||
to="catalog?filters[kind]=system"
|
||||
icon={SystemIcon}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="Components"
|
||||
to="catalog?filters[kind]=component"
|
||||
icon={ComponentIcon}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="APIs"
|
||||
to="catalog?filters[kind]=api"
|
||||
icon={ApiIcon}
|
||||
/>
|
||||
<SidebarDivider />
|
||||
<SidebarSubmenuItem
|
||||
title="Resources"
|
||||
to="catalog?filters[kind]=resource"
|
||||
icon={ResourceIcon}
|
||||
/>
|
||||
<SidebarDivider />
|
||||
<SidebarSubmenuItem
|
||||
title="Groups"
|
||||
to="catalog?filters[kind]=group"
|
||||
icon={GroupIcon}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="Users"
|
||||
to="catalog?filters[kind]=user"
|
||||
icon={UserIcon}
|
||||
/>
|
||||
</SidebarSubmenu>
|
||||
</SidebarItem>
|
||||
```
|
||||
|
||||
When you startup your Backstage app and hover over the Home option on the sidebar you'll now see a nice sub-menu appear with links to the various Kinds in your Catalog. It would look like this:
|
||||
|
||||

|
||||
|
||||
You can see more ways to use this in the [Storybook Sidebar examples](https://backstage.io/storybook/?path=/story/layout-sidebar--sample-scalable-sidebar)
|
||||
|
||||
## Custom Homepage
|
||||
|
||||
In addition to a custom theme, a custom logo, you can also customize the
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: Cloudsmith
|
||||
author: Roadie
|
||||
authorUrl: https://roadie.io
|
||||
category: CI/CD
|
||||
description: Show Cloudsmith Repository stats on your backstage homepage.
|
||||
documentation: https://github.com/RoadieHQ/roadie-backstage-plugins/tree/main/plugins/frontend/backstage-plugin-cloudsmith
|
||||
iconUrl: https://cloudsmith.com/img/cloudsmith-mini-dark.svg
|
||||
npmPackageName: '@roadiehq/backstage-plugin-cloudsmith'
|
||||
tags:
|
||||
- dashboards
|
||||
- monitoring
|
||||
addedDate: '2022-11-18'
|
||||
@@ -47,12 +47,21 @@ import {
|
||||
SidebarSpace,
|
||||
Link,
|
||||
useSidebarOpenState,
|
||||
SidebarSubmenu,
|
||||
SidebarSubmenuItem,
|
||||
} from '@backstage/core-components';
|
||||
import { MyGroupsSidebarItem } from '@backstage/plugin-org';
|
||||
import GroupIcon from '@material-ui/icons/People';
|
||||
import { SearchModal } from '../search/SearchModal';
|
||||
import Score from '@material-ui/icons/Score';
|
||||
|
||||
import ApiIcon from '@material-ui/icons/Extension';
|
||||
import ComponentIcon from '@material-ui/icons/Memory';
|
||||
import DomainIcon from '@material-ui/icons/Apartment';
|
||||
import ResourceIcon from '@material-ui/icons/Work';
|
||||
import SystemIcon from '@material-ui/icons/Category';
|
||||
import UserIcon from '@material-ui/icons/Person';
|
||||
|
||||
const useSidebarLogoStyles = makeStyles({
|
||||
root: {
|
||||
width: sidebarConfig.drawerWidthClosed,
|
||||
@@ -93,7 +102,47 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
|
||||
<SidebarDivider />
|
||||
<SidebarGroup label="Menu" icon={<MenuIcon />}>
|
||||
{/* Global nav, not org-specific */}
|
||||
<SidebarItem icon={HomeIcon} to="catalog" text="Home" />
|
||||
<SidebarItem icon={HomeIcon} to="catalog" text="Home">
|
||||
<SidebarSubmenu title="Catalog">
|
||||
<SidebarSubmenuItem
|
||||
title="Domains"
|
||||
to="catalog?filters[kind]=domain"
|
||||
icon={DomainIcon}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="Systems"
|
||||
to="catalog?filters[kind]=system"
|
||||
icon={SystemIcon}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="Components"
|
||||
to="catalog?filters[kind]=component"
|
||||
icon={ComponentIcon}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="APIs"
|
||||
to="catalog?filters[kind]=api"
|
||||
icon={ApiIcon}
|
||||
/>
|
||||
<SidebarDivider />
|
||||
<SidebarSubmenuItem
|
||||
title="Resources"
|
||||
to="catalog?filters[kind]=resource"
|
||||
icon={ResourceIcon}
|
||||
/>
|
||||
<SidebarDivider />
|
||||
<SidebarSubmenuItem
|
||||
title="Groups"
|
||||
to="catalog?filters[kind]=group"
|
||||
icon={GroupIcon}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="Users"
|
||||
to="catalog?filters[kind]=user"
|
||||
icon={UserIcon}
|
||||
/>
|
||||
</SidebarSubmenu>
|
||||
</SidebarItem>
|
||||
<MyGroupsSidebarItem
|
||||
singularTitle="My Squad"
|
||||
pluralTitle="My Squads"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
```ts
|
||||
import { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
import { BackendLifecycle } from '@backstage/backend-plugin-api';
|
||||
import { Config } from '@backstage/config';
|
||||
import { ExtensionPoint } from '@backstage/backend-plugin-api';
|
||||
import { HttpRouterService } from '@backstage/backend-plugin-api';
|
||||
@@ -66,6 +67,11 @@ export type HttpRouterFactoryOptions = {
|
||||
indexPlugin?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export const lifecycleFactory: (
|
||||
options?: undefined,
|
||||
) => ServiceFactory<BackendLifecycle>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const loggerFactory: (options?: undefined) => ServiceFactory<Logger>;
|
||||
|
||||
|
||||
@@ -25,4 +25,5 @@ export { schedulerFactory } from './schedulerService';
|
||||
export { tokenManagerFactory } from './tokenManagerService';
|
||||
export { urlReaderFactory } from './urlReaderService';
|
||||
export { httpRouterFactory } from './httpRouterService';
|
||||
export { lifecycleFactory } from './lifecycleService';
|
||||
export type { HttpRouterFactoryOptions } from './httpRouterService';
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2022 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 { getVoidLogger } from '@backstage/backend-common';
|
||||
import { BackendLifecycleImpl } from './lifecycleService';
|
||||
|
||||
describe('lifecycleService', () => {
|
||||
it('should execute registered shutdown hook', async () => {
|
||||
const service = new BackendLifecycleImpl(getVoidLogger());
|
||||
const hook = jest.fn();
|
||||
service.addShutdownHook({
|
||||
pluginId: 'test',
|
||||
fn: async () => {
|
||||
hook();
|
||||
},
|
||||
});
|
||||
// should not execute the hook more than once.
|
||||
await service.shutdown();
|
||||
await service.shutdown();
|
||||
await service.shutdown();
|
||||
expect(hook).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not throw errors', async () => {
|
||||
const service = new BackendLifecycleImpl(getVoidLogger());
|
||||
service.addShutdownHook({
|
||||
pluginId: 'test',
|
||||
fn: async () => {
|
||||
throw new Error('oh no');
|
||||
},
|
||||
});
|
||||
await expect(service.shutdown()).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2022 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 {
|
||||
BackendLifecycle,
|
||||
createServiceFactory,
|
||||
lifecycleServiceRef,
|
||||
loggerToWinstonLogger,
|
||||
pluginMetadataServiceRef,
|
||||
rootLoggerServiceRef,
|
||||
BackendLifecycleShutdownHook,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
const CALLBACKS = ['SIGTERM', 'SIGINT', 'beforeExit'];
|
||||
export class BackendLifecycleImpl {
|
||||
constructor(private readonly logger: Logger) {
|
||||
CALLBACKS.map(signal => process.on(signal, () => this.shutdown()));
|
||||
}
|
||||
|
||||
#isCalled = false;
|
||||
#shutdownTasks: Array<BackendLifecycleShutdownHook & { pluginId: string }> =
|
||||
[];
|
||||
|
||||
addShutdownHook(
|
||||
options: BackendLifecycleShutdownHook & { pluginId: string },
|
||||
): void {
|
||||
this.#shutdownTasks.push(options);
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
if (this.#isCalled) {
|
||||
return;
|
||||
}
|
||||
this.#isCalled = true;
|
||||
|
||||
this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`);
|
||||
await Promise.all(
|
||||
this.#shutdownTasks.map(hook =>
|
||||
Promise.resolve()
|
||||
.then(() => hook.fn())
|
||||
.catch(e => {
|
||||
this.logger.error(
|
||||
`Shutdown hook registered by plugin '${hook.pluginId}' failed with: ${e}`,
|
||||
);
|
||||
})
|
||||
.then(() =>
|
||||
this.logger.info(
|
||||
`Successfully ran shutdown hook registered by plugin ${hook.pluginId}`,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PluginScopedLifecycleImpl implements BackendLifecycle {
|
||||
constructor(
|
||||
private readonly lifecycle: BackendLifecycleImpl,
|
||||
private readonly pluginId: string,
|
||||
) {}
|
||||
addShutdownHook(options: BackendLifecycleShutdownHook): void {
|
||||
this.lifecycle.addShutdownHook({ ...options, pluginId: this.pluginId });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows plugins to register shutdown hooks that are run when the process is about to exit.
|
||||
* @public */
|
||||
export const lifecycleFactory = createServiceFactory({
|
||||
service: lifecycleServiceRef,
|
||||
deps: {
|
||||
logger: rootLoggerServiceRef,
|
||||
plugin: pluginMetadataServiceRef,
|
||||
},
|
||||
async factory({ logger }) {
|
||||
const rootLifecycle = new BackendLifecycleImpl(
|
||||
loggerToWinstonLogger(logger),
|
||||
);
|
||||
return async ({ plugin }) => {
|
||||
return new PluginScopedLifecycleImpl(rootLifecycle, plugin.getId());
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
databaseFactory,
|
||||
discoveryFactory,
|
||||
httpRouterFactory,
|
||||
lifecycleFactory,
|
||||
loggerFactory,
|
||||
permissionsFactory,
|
||||
rootLoggerFactory,
|
||||
@@ -43,6 +44,7 @@ export const defaultServiceFactories = [
|
||||
tokenManagerFactory,
|
||||
urlReaderFactory,
|
||||
httpRouterFactory,
|
||||
lifecycleFactory,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,6 +24,16 @@ export interface BackendFeature {
|
||||
register(reg: BackendRegistrationPoints): void;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface BackendLifecycle {
|
||||
addShutdownHook(options: BackendLifecycleShutdownHook): void;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type BackendLifecycleShutdownHook = {
|
||||
fn: () => void | Promise<void>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export interface BackendModuleConfig<TOptions> {
|
||||
// (undocumented)
|
||||
@@ -158,6 +168,9 @@ export interface HttpRouterService {
|
||||
// @public (undocumented)
|
||||
export const httpRouterServiceRef: ServiceRef<HttpRouterService, 'plugin'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const lifecycleServiceRef: ServiceRef<BackendLifecycle, 'plugin'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface Logger {
|
||||
// (undocumented)
|
||||
|
||||
@@ -28,4 +28,9 @@ export { permissionsServiceRef } from './permissionsServiceRef';
|
||||
export { schedulerServiceRef } from './schedulerServiceRef';
|
||||
export { rootLoggerServiceRef } from './rootLoggerServiceRef';
|
||||
export { pluginMetadataServiceRef } from './pluginMetadataServiceRef';
|
||||
export { lifecycleServiceRef } from './lifecycleServiceRef';
|
||||
export type {
|
||||
BackendLifecycle,
|
||||
BackendLifecycleShutdownHook,
|
||||
} from './lifecycleServiceRef';
|
||||
export type { PluginMetadata } from './pluginMetadataServiceRef';
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2022 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 { createServiceRef } from '../system/types';
|
||||
|
||||
/**
|
||||
* @public
|
||||
**/
|
||||
export type BackendLifecycleShutdownHook = {
|
||||
fn: () => void | Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
**/
|
||||
export interface BackendLifecycle {
|
||||
/**
|
||||
* Register a function to be called when the backend is shutting down.
|
||||
*/
|
||||
addShutdownHook(options: BackendLifecycleShutdownHook): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const lifecycleServiceRef = createServiceRef<BackendLifecycle>({
|
||||
id: 'core.lifecycle',
|
||||
scope: 'plugin',
|
||||
});
|
||||
@@ -27,14 +27,12 @@ export default async function createPlugin(
|
||||
subscribers: EventSubscriber[],
|
||||
): Promise<Router> {
|
||||
const eventsRouter = Router();
|
||||
const httpRouter = Router();
|
||||
eventsRouter.use('/http', httpRouter);
|
||||
|
||||
const http = HttpPostIngressEventPublisher.fromConfig({
|
||||
config: env.config,
|
||||
logger: env.logger,
|
||||
router: httpRouter,
|
||||
});
|
||||
http.bind(eventsRouter);
|
||||
|
||||
await new EventsBackend(env.logger)
|
||||
.addPublishers(http)
|
||||
|
||||
@@ -250,6 +250,7 @@ export interface DependencyGraphProps<NodeData, EdgeData>
|
||||
edgeRanks?: number;
|
||||
edges: DependencyEdge<EdgeData>[];
|
||||
edgeWeight?: number;
|
||||
fit?: 'grow' | 'contain';
|
||||
labelOffset?: number;
|
||||
// Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver
|
||||
labelPosition?: LabelPosition;
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"pluralize": "^8.0.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"qs": "^6.9.4",
|
||||
"rc-progress": "3.4.0",
|
||||
"rc-progress": "3.4.1",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-hook-form": "^7.12.2",
|
||||
"react-markdown": "^8.0.0",
|
||||
|
||||
@@ -170,6 +170,14 @@ export interface DependencyGraphProps<NodeData, EdgeData>
|
||||
* Default: 'curveMonotoneX'
|
||||
*/
|
||||
curve?: 'curveStepBefore' | 'curveMonotoneX';
|
||||
/**
|
||||
* Controls if the graph should be contained or grow
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Default: 'grow'
|
||||
*/
|
||||
fit?: 'grow' | 'contain';
|
||||
}
|
||||
|
||||
const WORKSPACE_ID = 'workspace';
|
||||
@@ -203,6 +211,7 @@ export function DependencyGraph<NodeData, EdgeData>(
|
||||
defs,
|
||||
zoom = 'enabled',
|
||||
curve = 'curveMonotoneX',
|
||||
fit = 'grow',
|
||||
...svgProps
|
||||
} = props;
|
||||
const theme: BackstageTheme = useTheme();
|
||||
@@ -223,6 +232,9 @@ export function DependencyGraph<NodeData, EdgeData>(
|
||||
|
||||
const maxWidth = Math.max(graphWidth, containerWidth);
|
||||
const maxHeight = Math.max(graphHeight, containerHeight);
|
||||
const minHeight = Math.min(graphHeight, containerHeight);
|
||||
|
||||
const scalableHeight = fit === 'grow' ? maxHeight : minHeight;
|
||||
|
||||
const containerRef = React.useMemo(
|
||||
() =>
|
||||
@@ -394,7 +406,7 @@ export function DependencyGraph<NodeData, EdgeData>(
|
||||
ref={containerRef}
|
||||
{...svgProps}
|
||||
width="100%"
|
||||
height={maxHeight}
|
||||
height={scalableHeight}
|
||||
viewBox={`0 0 ${maxWidth} ${maxHeight}`}
|
||||
>
|
||||
<defs>
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
permissionsServiceRef,
|
||||
urlReaderServiceRef,
|
||||
httpRouterServiceRef,
|
||||
lifecycleServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { CatalogBuilder } from './CatalogBuilder';
|
||||
import {
|
||||
@@ -78,6 +79,7 @@ export const catalogPlugin = createBackendPlugin({
|
||||
permissions: permissionsServiceRef,
|
||||
database: databaseServiceRef,
|
||||
httpRouter: httpRouterServiceRef,
|
||||
lifecycle: lifecycleServiceRef,
|
||||
},
|
||||
async init({
|
||||
logger,
|
||||
@@ -86,6 +88,7 @@ export const catalogPlugin = createBackendPlugin({
|
||||
database,
|
||||
permissions,
|
||||
httpRouter,
|
||||
lifecycle,
|
||||
}) {
|
||||
const winstonLogger = loggerToWinstonLogger(logger);
|
||||
const builder = await CatalogBuilder.create({
|
||||
@@ -100,7 +103,11 @@ export const catalogPlugin = createBackendPlugin({
|
||||
const { processingEngine, router } = await builder.build();
|
||||
|
||||
await processingEngine.start();
|
||||
|
||||
lifecycle.addShutdownHook({
|
||||
fn: async () => {
|
||||
await processingEngine.stop();
|
||||
},
|
||||
});
|
||||
httpRouter.use(router);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"@material-ui/lab": "4.0.0-alpha.57",
|
||||
"history": "^5.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"pluralize": "^8.0.0",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-use": "^17.2.4",
|
||||
"zen-observable": "^0.9.0"
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from '@backstage/test-utils';
|
||||
import { CatalogKindHeader } from './CatalogKindHeader';
|
||||
import { errorApiRef } from '@backstage/core-plugin-api';
|
||||
import pluralize from 'pluralize';
|
||||
|
||||
const entities: Entity[] = [
|
||||
{
|
||||
@@ -96,7 +97,7 @@ describe('<CatalogKindHeader />', () => {
|
||||
|
||||
entities.map(entity => {
|
||||
expect(
|
||||
screen.getByRole('option', { name: `${entity.kind}s` }),
|
||||
screen.getByRole('option', { name: `${pluralize(entity.kind)}` }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import pluralize from 'pluralize';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
@@ -132,7 +133,7 @@ export function CatalogKindHeader(props: CatalogKindHeaderProps) {
|
||||
>
|
||||
{Object.keys(options).map(kind => (
|
||||
<MenuItem value={kind} key={kind}>
|
||||
{`${options[kind]}s`}
|
||||
{`${pluralize(options[kind])}`}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"@material-ui/core": "^4.9.10",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.57",
|
||||
"rc-progress": "3.4.0",
|
||||
"rc-progress": "3.4.1",
|
||||
"react-use": "^17.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -170,8 +170,8 @@ const http = HttpPostIngressEventPublisher.fromConfig({
|
||||
},
|
||||
},
|
||||
logger: env.logger,
|
||||
router: httpRouter,
|
||||
});
|
||||
http.bind(router);
|
||||
|
||||
await new EventsBackend(env.logger)
|
||||
.addPublishers(http)
|
||||
|
||||
@@ -33,6 +33,8 @@ export const eventsPlugin: (options?: undefined) => BackendFeature;
|
||||
|
||||
// @public
|
||||
export class HttpPostIngressEventPublisher implements EventPublisher {
|
||||
// (undocumented)
|
||||
bind(router: express.Router): void;
|
||||
// (undocumented)
|
||||
static fromConfig(env: {
|
||||
config: Config;
|
||||
@@ -40,7 +42,6 @@ export class HttpPostIngressEventPublisher implements EventPublisher {
|
||||
[topic: string]: Omit<HttpPostIngressOptions, 'topic'>;
|
||||
};
|
||||
logger: Logger;
|
||||
router: express.Router;
|
||||
}): HttpPostIngressEventPublisher;
|
||||
// (undocumented)
|
||||
setEventBroker(eventBroker: EventBroker): Promise<void>;
|
||||
|
||||
@@ -90,14 +90,11 @@ export const eventsPlugin = createBackendPlugin({
|
||||
env.registerInit({
|
||||
deps: {
|
||||
config: configServiceRef,
|
||||
httpRouter: httpRouterServiceRef,
|
||||
logger: loggerServiceRef,
|
||||
router: httpRouterServiceRef,
|
||||
},
|
||||
async init({ config, httpRouter, logger }) {
|
||||
async init({ config, logger, router }) {
|
||||
const winstonLogger = loggerToWinstonLogger(logger);
|
||||
const eventsRouter = Router();
|
||||
const router = Router();
|
||||
eventsRouter.use('/http', router);
|
||||
|
||||
const ingresses = Object.fromEntries(
|
||||
extensionPoint.httpPostIngresses.map(ingress => [
|
||||
@@ -108,23 +105,20 @@ export const eventsPlugin = createBackendPlugin({
|
||||
|
||||
const http = HttpPostIngressEventPublisher.fromConfig({
|
||||
config,
|
||||
logger: winstonLogger,
|
||||
router,
|
||||
ingresses,
|
||||
logger: winstonLogger,
|
||||
});
|
||||
const eventsRouter = Router();
|
||||
http.bind(eventsRouter);
|
||||
router.use(eventsRouter);
|
||||
|
||||
if (!extensionPoint.eventBroker) {
|
||||
extensionPoint.setEventBroker(new InMemoryEventBroker(winstonLogger));
|
||||
}
|
||||
const eventBroker =
|
||||
extensionPoint.eventBroker ?? new InMemoryEventBroker(winstonLogger);
|
||||
|
||||
extensionPoint.eventBroker!.subscribe(extensionPoint.subscribers);
|
||||
eventBroker.subscribe(extensionPoint.subscribers);
|
||||
[extensionPoint.publishers, http]
|
||||
.flat()
|
||||
.forEach(publisher =>
|
||||
publisher.setEventBroker(extensionPoint.eventBroker!),
|
||||
);
|
||||
|
||||
httpRouter.use(eventsRouter);
|
||||
.forEach(publisher => publisher.setEventBroker(eventBroker));
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { errorHandler, getVoidLogger } from '@backstage/backend-common';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils';
|
||||
import express from 'express';
|
||||
@@ -35,37 +35,35 @@ describe('HttpPostIngressEventPublisher', () => {
|
||||
});
|
||||
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
router.use(errorHandler());
|
||||
const app = express().use(router);
|
||||
|
||||
const publisher = HttpPostIngressEventPublisher.fromConfig({
|
||||
config,
|
||||
logger,
|
||||
router,
|
||||
ingresses: {
|
||||
testB: {},
|
||||
},
|
||||
logger,
|
||||
});
|
||||
publisher.bind(router);
|
||||
|
||||
const eventBroker = new TestEventBroker();
|
||||
await publisher.setEventBroker(eventBroker);
|
||||
|
||||
const notFoundResponse = await request(app)
|
||||
.post('/unknown')
|
||||
.post('/http/unknown')
|
||||
.timeout(100)
|
||||
.send({ test: 'data' });
|
||||
expect(notFoundResponse.status).toBe(404);
|
||||
|
||||
const response1 = await request(app)
|
||||
.post('/testA')
|
||||
.post('/http/testA')
|
||||
.set('X-Custom-Header', 'test-value')
|
||||
.timeout(100)
|
||||
.send({ testA: 'data' });
|
||||
expect(response1.status).toBe(202);
|
||||
|
||||
const response2 = await request(app)
|
||||
.post('/testB')
|
||||
.post('/http/testB')
|
||||
.set('X-Custom-Header', 'test-value')
|
||||
.timeout(100)
|
||||
.send({ testB: 'data' });
|
||||
@@ -100,14 +98,10 @@ describe('HttpPostIngressEventPublisher', () => {
|
||||
});
|
||||
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
router.use(errorHandler());
|
||||
const app = express().use(router);
|
||||
|
||||
const publisher = HttpPostIngressEventPublisher.fromConfig({
|
||||
config,
|
||||
logger,
|
||||
router,
|
||||
ingresses: {
|
||||
testB: {
|
||||
validator: async (req, context) => {
|
||||
@@ -148,26 +142,28 @@ describe('HttpPostIngressEventPublisher', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
logger,
|
||||
});
|
||||
publisher.bind(router);
|
||||
|
||||
const eventBroker = new TestEventBroker();
|
||||
await publisher.setEventBroker(eventBroker);
|
||||
|
||||
const response1 = await request(app)
|
||||
.post('/testA')
|
||||
.post('/http/testA')
|
||||
.timeout(100)
|
||||
.send({ test: 'data' });
|
||||
expect(response1.status).toBe(202);
|
||||
|
||||
const response2 = await request(app)
|
||||
.post('/testB')
|
||||
.post('/http/testB')
|
||||
.timeout(100)
|
||||
.send({ test: 'data' });
|
||||
expect(response2.status).toBe(400);
|
||||
expect(response2.body).toEqual({ message: 'wrong signature' });
|
||||
|
||||
const response3 = await request(app)
|
||||
.post('/testB')
|
||||
.post('/http/testB')
|
||||
.set('X-Test-Signature', 'wrong')
|
||||
.timeout(100)
|
||||
.send({ test: 'data' });
|
||||
@@ -175,21 +171,21 @@ describe('HttpPostIngressEventPublisher', () => {
|
||||
expect(response3.body).toEqual({ message: 'wrong signature' });
|
||||
|
||||
const response4 = await request(app)
|
||||
.post('/testB')
|
||||
.post('/http/testB')
|
||||
.set('X-Test-Signature', 'testB-signature')
|
||||
.timeout(100)
|
||||
.send({ test: 'data' });
|
||||
expect(response4.status).toBe(202);
|
||||
|
||||
const response5 = await request(app)
|
||||
.post('/testC')
|
||||
.post('/http/testC')
|
||||
.timeout(100)
|
||||
.send({ test: 'data' });
|
||||
expect(response5.status).toBe(404);
|
||||
expect(response5.body).toEqual({});
|
||||
|
||||
const response6 = await request(app)
|
||||
.post('/testD')
|
||||
.post('/http/testD')
|
||||
.timeout(100)
|
||||
.send({ test: 'data' });
|
||||
expect(response6.status).toBe(403);
|
||||
@@ -210,15 +206,10 @@ describe('HttpPostIngressEventPublisher', () => {
|
||||
it('without configuration', async () => {
|
||||
const config = new ConfigReader({});
|
||||
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
router.use(errorHandler());
|
||||
|
||||
expect(() =>
|
||||
HttpPostIngressEventPublisher.fromConfig({
|
||||
config,
|
||||
logger,
|
||||
router,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
@@ -41,7 +41,6 @@ export class HttpPostIngressEventPublisher implements EventPublisher {
|
||||
config: Config;
|
||||
ingresses?: { [topic: string]: Omit<HttpPostIngressOptions, 'topic'> };
|
||||
logger: Logger;
|
||||
router: express.Router;
|
||||
}): HttpPostIngressEventPublisher {
|
||||
const topics =
|
||||
env.config.getOptionalStringArray('events.http.topics') ?? [];
|
||||
@@ -55,15 +54,18 @@ export class HttpPostIngressEventPublisher implements EventPublisher {
|
||||
}
|
||||
});
|
||||
|
||||
return new HttpPostIngressEventPublisher(env.logger, env.router, ingresses);
|
||||
return new HttpPostIngressEventPublisher(env.logger, ingresses);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
private logger: Logger,
|
||||
router: express.Router,
|
||||
ingresses: { [topic: string]: Omit<HttpPostIngressOptions, 'topic'> },
|
||||
) {
|
||||
router.use(this.createRouter(ingresses));
|
||||
private readonly logger: Logger,
|
||||
private readonly ingresses: {
|
||||
[topic: string]: Omit<HttpPostIngressOptions, 'topic'>;
|
||||
},
|
||||
) {}
|
||||
|
||||
bind(router: express.Router): void {
|
||||
router.use('/http', this.createRouter(this.ingresses));
|
||||
}
|
||||
|
||||
async setEventBroker(eventBroker: EventBroker): Promise<void> {
|
||||
@@ -92,8 +94,12 @@ export class HttpPostIngressEventPublisher implements EventPublisher {
|
||||
const path = `/${topic}`;
|
||||
|
||||
router.post(path, async (request, response) => {
|
||||
const requestDetails = {
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
};
|
||||
const context = new RequestValidationContextImpl();
|
||||
await validator?.(request, context);
|
||||
await validator?.(requestDetails, context);
|
||||
if (context.wasRejected()) {
|
||||
response
|
||||
.status(context.rejectionDetails!.status)
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
```ts
|
||||
import { ExtensionPoint } from '@backstage/backend-plugin-api';
|
||||
import { Request as Request_2 } from 'express';
|
||||
|
||||
// @public
|
||||
export interface EventBroker {
|
||||
@@ -74,6 +73,12 @@ export interface HttpPostIngressOptions {
|
||||
validator?: RequestValidator;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface RequestDetails {
|
||||
body: unknown;
|
||||
headers: Record<string, string | string[] | undefined>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface RequestRejectionDetails {
|
||||
// (undocumented)
|
||||
@@ -89,7 +94,7 @@ export interface RequestValidationContext {
|
||||
|
||||
// @public
|
||||
export type RequestValidator = (
|
||||
request: Request_2,
|
||||
request: RequestDetails,
|
||||
context: RequestValidationContext,
|
||||
) => Promise<void>;
|
||||
|
||||
|
||||
@@ -24,9 +24,7 @@
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@types/express": "^4.17.6",
|
||||
"express": "^4.17.1"
|
||||
"@backstage/backend-plugin-api": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2022 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface RequestDetails {
|
||||
/**
|
||||
* Request body. JSON payloads have been parsed already.
|
||||
*/
|
||||
body: unknown;
|
||||
/**
|
||||
* Key-value pairs of header names and values. Header names are lower-cased.
|
||||
*/
|
||||
headers: Record<string, string | string[] | undefined>;
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Request } from 'express';
|
||||
import { RequestDetails } from './RequestDetails';
|
||||
import { RequestValidationContext } from './RequestValidationContext';
|
||||
|
||||
/**
|
||||
@@ -29,6 +29,6 @@ import { RequestValidationContext } from './RequestValidationContext';
|
||||
* @public
|
||||
*/
|
||||
export type RequestValidator = (
|
||||
request: Request,
|
||||
request: RequestDetails,
|
||||
context: RequestValidationContext,
|
||||
) => Promise<void>;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type { RequestDetails } from './RequestDetails';
|
||||
export type { RequestRejectionDetails } from './RequestRejectionDetails';
|
||||
export type { RequestValidationContext } from './RequestValidationContext';
|
||||
export type { RequestValidator } from './RequestValidator';
|
||||
|
||||
@@ -43,8 +43,10 @@ import useAsync from 'react-use/lib/useAsync';
|
||||
|
||||
const useStyles = makeStyles((theme: BackstageTheme) => ({
|
||||
graph: {
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
minHeight: '100%',
|
||||
},
|
||||
graphWrapper: {
|
||||
height: '100%',
|
||||
},
|
||||
organizationNode: {
|
||||
fill: theme.palette.secondary.light,
|
||||
@@ -62,6 +64,15 @@ const useStyles = makeStyles((theme: BackstageTheme) => ({
|
||||
justifyContent: 'center',
|
||||
color: 'black',
|
||||
},
|
||||
legend: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
padding: theme.spacing(1),
|
||||
'& .icon': {
|
||||
verticalAlign: 'bottom',
|
||||
},
|
||||
},
|
||||
textOrganization: {
|
||||
color: theme.palette.secondary.contrastText,
|
||||
},
|
||||
@@ -221,7 +232,7 @@ export function GroupsDiagram() {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.graphWrapper}>
|
||||
<DependencyGraph
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
@@ -229,14 +240,18 @@ export function GroupsDiagram() {
|
||||
direction={DependencyGraphTypes.Direction.RIGHT_LEFT}
|
||||
renderNode={RenderNode}
|
||||
className={classes.graph}
|
||||
fit="contain"
|
||||
/>
|
||||
|
||||
<Typography
|
||||
variant="caption"
|
||||
style={{ display: 'block', textAlign: 'right' }}
|
||||
color="textSecondary"
|
||||
display="block"
|
||||
className={classes.legend}
|
||||
>
|
||||
<ZoomOutMap style={{ verticalAlign: 'bottom' }} /> Use pinch & zoom
|
||||
to move around the diagram.
|
||||
<ZoomOutMap className="icon" /> Use pinch & zoom to move around the
|
||||
diagram.
|
||||
</Typography>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,9 +23,10 @@ import {
|
||||
stringifyEntityRef,
|
||||
UserEntity,
|
||||
} from '@backstage/catalog-model';
|
||||
import { Config, JsonObject, JsonValue } from '@backstage/config';
|
||||
import { Config } from '@backstage/config';
|
||||
import { InputError, NotFoundError, stringifyError } from '@backstage/errors';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { JsonObject, JsonValue } from '@backstage/types';
|
||||
import {
|
||||
TaskSpec,
|
||||
TemplateEntityV1beta3,
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"@material-ui/lab": "4.0.0-alpha.57",
|
||||
"@material-ui/styles": "^4.10.0",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"rc-progress": "3.4.0",
|
||||
"rc-progress": "3.4.1",
|
||||
"react-use": "^17.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
import { Config } from '@backstage/config';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { Logger } from 'winston';
|
||||
import express, { Router } from 'express';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { VaultClient } from './vaultApi';
|
||||
import { TaskRunner, PluginTaskScheduler } from '@backstage/backend-tasks';
|
||||
import { errorHandler } from '@backstage/backend-common';
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import { NotAllowedError, NotFoundError } from '@backstage/errors';
|
||||
import fetch from 'node-fetch';
|
||||
import plimit from 'p-limit';
|
||||
import { getVaultConfig, VaultConfig } from '../config';
|
||||
@@ -103,6 +103,8 @@ export class VaultClient implements VaultApi {
|
||||
return (await response.json()) as T;
|
||||
} else if (response.status === 404) {
|
||||
throw new NotFoundError(`No secrets found in path '${path}'`);
|
||||
} else if (response.status === 403) {
|
||||
throw new NotAllowedError(response.statusText);
|
||||
}
|
||||
throw new Error(
|
||||
`Unexpected error while fetching secrets from path '${path}'`,
|
||||
|
||||
Executable
+115
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* Copyright 2022 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.
|
||||
*/
|
||||
|
||||
const { execFile: execFileCb } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const { resolve: resolvePath } = require('path');
|
||||
|
||||
const execFile = promisify(execFileCb);
|
||||
|
||||
async function hasNewChangesets(ref) {
|
||||
if (!ref) {
|
||||
throw new Error('ref is required');
|
||||
}
|
||||
|
||||
const { stdout } = await execFile('git', [
|
||||
'diff',
|
||||
'--compact-summary',
|
||||
ref,
|
||||
'.changeset/*.md',
|
||||
]);
|
||||
return stdout.includes('(new)');
|
||||
}
|
||||
|
||||
function getReleaseOfMonth(year, month) {
|
||||
const base = new Date(Date.UTC(year, month));
|
||||
const wednesdayOffset =
|
||||
base.getUTCDay() > 3 ? 10 - base.getUTCDay() : 3 - base.getUTCDay();
|
||||
const thirdWednesdayOffset = wednesdayOffset + 7 * 2;
|
||||
const releaseOffset = thirdWednesdayOffset - 1;
|
||||
const releaseDay = new Date(
|
||||
Date.UTC(base.getUTCFullYear(), base.getUTCMonth(), releaseOffset + 1),
|
||||
);
|
||||
return releaseDay;
|
||||
}
|
||||
|
||||
function getReleaseSchedule() {
|
||||
const firstReleaseYear = 2022;
|
||||
const firstReleaseMonth = 2;
|
||||
|
||||
return Array(100)
|
||||
.fill(0)
|
||||
.map((_, i) => {
|
||||
const date = getReleaseOfMonth(firstReleaseYear, firstReleaseMonth + i);
|
||||
return { version: `1.${i}.0`, date };
|
||||
});
|
||||
}
|
||||
|
||||
function getCurrentRelease() {
|
||||
const { version: releaseVersion } = require(resolvePath('package.json'));
|
||||
|
||||
const match = releaseVersion.match(/^(\d+\.\d+\.\d+)/);
|
||||
if (!match) {
|
||||
throw new Error(`Failed to parse release version, '${releaseVersion}'`);
|
||||
}
|
||||
const [versionStr] = match;
|
||||
if (versionStr === releaseVersion) {
|
||||
return releaseVersion;
|
||||
}
|
||||
const [major, minor] = versionStr.split('.').map(Number);
|
||||
return `${major}.${minor - 1}.0`;
|
||||
}
|
||||
|
||||
function findNextRelease(currentRelease, releaseSchedule) {
|
||||
const currentIndex = releaseSchedule.findIndex(
|
||||
r => r.version === currentRelease,
|
||||
);
|
||||
if (currentIndex === -1) {
|
||||
throw new Error(
|
||||
`Failed to find current release '${currentRelease}' in release schedule`,
|
||||
);
|
||||
}
|
||||
|
||||
return releaseSchedule[currentIndex + 1];
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [diffRef = 'origin/master'] = process.argv.slice(2);
|
||||
const needsMessage = await hasNewChangesets(diffRef);
|
||||
if (!needsMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentRelease = getCurrentRelease();
|
||||
const releaseSchedule = getReleaseSchedule();
|
||||
const nextRelease = findNextRelease(currentRelease, releaseSchedule);
|
||||
|
||||
const scheduledDate = nextRelease.date
|
||||
.toUTCString()
|
||||
.replace(/\s*\d+:\d+:\d+.*/, '');
|
||||
process.stdout.write(
|
||||
[
|
||||
'Thank you for contributing to Backstage! The changes in this pull request will be part',
|
||||
`of the \`${nextRelease.version}\` release, scheduled for ${scheduledDate}.`,
|
||||
].join(' '),
|
||||
);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
+43
-43
@@ -2966,90 +2966,90 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-darwin-arm64@npm:1.3.16":
|
||||
version: 1.3.16
|
||||
resolution: "@swc/core-darwin-arm64@npm:1.3.16"
|
||||
"@swc/core-darwin-arm64@npm:1.3.19":
|
||||
version: 1.3.19
|
||||
resolution: "@swc/core-darwin-arm64@npm:1.3.19"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-darwin-x64@npm:1.3.16":
|
||||
version: 1.3.16
|
||||
resolution: "@swc/core-darwin-x64@npm:1.3.16"
|
||||
"@swc/core-darwin-x64@npm:1.3.19":
|
||||
version: 1.3.19
|
||||
resolution: "@swc/core-darwin-x64@npm:1.3.19"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-linux-arm-gnueabihf@npm:1.3.16":
|
||||
version: 1.3.16
|
||||
resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.16"
|
||||
"@swc/core-linux-arm-gnueabihf@npm:1.3.19":
|
||||
version: 1.3.19
|
||||
resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.19"
|
||||
conditions: os=linux & cpu=arm
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-linux-arm64-gnu@npm:1.3.16":
|
||||
version: 1.3.16
|
||||
resolution: "@swc/core-linux-arm64-gnu@npm:1.3.16"
|
||||
"@swc/core-linux-arm64-gnu@npm:1.3.19":
|
||||
version: 1.3.19
|
||||
resolution: "@swc/core-linux-arm64-gnu@npm:1.3.19"
|
||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-linux-arm64-musl@npm:1.3.16":
|
||||
version: 1.3.16
|
||||
resolution: "@swc/core-linux-arm64-musl@npm:1.3.16"
|
||||
"@swc/core-linux-arm64-musl@npm:1.3.19":
|
||||
version: 1.3.19
|
||||
resolution: "@swc/core-linux-arm64-musl@npm:1.3.19"
|
||||
conditions: os=linux & cpu=arm64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-linux-x64-gnu@npm:1.3.16":
|
||||
version: 1.3.16
|
||||
resolution: "@swc/core-linux-x64-gnu@npm:1.3.16"
|
||||
"@swc/core-linux-x64-gnu@npm:1.3.19":
|
||||
version: 1.3.19
|
||||
resolution: "@swc/core-linux-x64-gnu@npm:1.3.19"
|
||||
conditions: os=linux & cpu=x64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-linux-x64-musl@npm:1.3.16":
|
||||
version: 1.3.16
|
||||
resolution: "@swc/core-linux-x64-musl@npm:1.3.16"
|
||||
"@swc/core-linux-x64-musl@npm:1.3.19":
|
||||
version: 1.3.19
|
||||
resolution: "@swc/core-linux-x64-musl@npm:1.3.19"
|
||||
conditions: os=linux & cpu=x64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-win32-arm64-msvc@npm:1.3.16":
|
||||
version: 1.3.16
|
||||
resolution: "@swc/core-win32-arm64-msvc@npm:1.3.16"
|
||||
"@swc/core-win32-arm64-msvc@npm:1.3.19":
|
||||
version: 1.3.19
|
||||
resolution: "@swc/core-win32-arm64-msvc@npm:1.3.19"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-win32-ia32-msvc@npm:1.3.16":
|
||||
version: 1.3.16
|
||||
resolution: "@swc/core-win32-ia32-msvc@npm:1.3.16"
|
||||
"@swc/core-win32-ia32-msvc@npm:1.3.19":
|
||||
version: 1.3.19
|
||||
resolution: "@swc/core-win32-ia32-msvc@npm:1.3.19"
|
||||
conditions: os=win32 & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-win32-x64-msvc@npm:1.3.16":
|
||||
version: 1.3.16
|
||||
resolution: "@swc/core-win32-x64-msvc@npm:1.3.16"
|
||||
"@swc/core-win32-x64-msvc@npm:1.3.19":
|
||||
version: 1.3.19
|
||||
resolution: "@swc/core-win32-x64-msvc@npm:1.3.19"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core@npm:^1.3.9":
|
||||
version: 1.3.16
|
||||
resolution: "@swc/core@npm:1.3.16"
|
||||
version: 1.3.19
|
||||
resolution: "@swc/core@npm:1.3.19"
|
||||
dependencies:
|
||||
"@swc/core-darwin-arm64": 1.3.16
|
||||
"@swc/core-darwin-x64": 1.3.16
|
||||
"@swc/core-linux-arm-gnueabihf": 1.3.16
|
||||
"@swc/core-linux-arm64-gnu": 1.3.16
|
||||
"@swc/core-linux-arm64-musl": 1.3.16
|
||||
"@swc/core-linux-x64-gnu": 1.3.16
|
||||
"@swc/core-linux-x64-musl": 1.3.16
|
||||
"@swc/core-win32-arm64-msvc": 1.3.16
|
||||
"@swc/core-win32-ia32-msvc": 1.3.16
|
||||
"@swc/core-win32-x64-msvc": 1.3.16
|
||||
"@swc/core-darwin-arm64": 1.3.19
|
||||
"@swc/core-darwin-x64": 1.3.19
|
||||
"@swc/core-linux-arm-gnueabihf": 1.3.19
|
||||
"@swc/core-linux-arm64-gnu": 1.3.19
|
||||
"@swc/core-linux-arm64-musl": 1.3.19
|
||||
"@swc/core-linux-x64-gnu": 1.3.19
|
||||
"@swc/core-linux-x64-musl": 1.3.19
|
||||
"@swc/core-win32-arm64-msvc": 1.3.19
|
||||
"@swc/core-win32-ia32-msvc": 1.3.19
|
||||
"@swc/core-win32-x64-msvc": 1.3.19
|
||||
dependenciesMeta:
|
||||
"@swc/core-darwin-arm64":
|
||||
optional: true
|
||||
@@ -3073,7 +3073,7 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
swcx: run_swcx.js
|
||||
checksum: 4361252c928c487a02f526aecd8f3072b923244234c2701916944cf13c252b6d5ce2466caf3e0797d3e92e71a89d4a044f8577e51e8f7fe3af0fe30d94e94b13
|
||||
checksum: 752499e18f81df789a9737936b2a83ea1db34bb2f983c80b394766796d853fa7d30701b52b9cb30ba943ad15286d8316603149683f4c553a5493aa78d89a76e0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user