From 8e554c649a9bfe447894f14efe9f152a479f7734 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 6 Apr 2021 13:53:17 +0100 Subject: [PATCH 01/95] support GitHub enterprise and hosted GitHub on GithubDeployments plugin Signed-off-by: Andrew Johnson --- plugins/github-deployments/package.json | 3 + plugins/github-deployments/src/api/index.ts | 32 +++++++++++ .../components/GithubDeploymentsCard.test.tsx | 57 ++++++++++++++++++- .../src/components/GithubDeploymentsCard.tsx | 13 ++++- plugins/github-deployments/src/plugin.ts | 10 +++- 5 files changed, 108 insertions(+), 7 deletions(-) diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index b8761c1385..528d660ea8 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -22,6 +22,9 @@ "dependencies": { "@backstage/catalog-model": "^0.7.4", "@backstage/core": "^0.7.3", + "@backstage/errors": "^0.1.1", + "@backstage/integration": "^0.5.1", + "@backstage/integration-react": "^0.1.1", "@backstage/plugin-catalog-react": "^0.1.3", "@backstage/theme": "^0.2.5", "@material-ui/core": "^4.11.0", diff --git a/plugins/github-deployments/src/api/index.ts b/plugins/github-deployments/src/api/index.ts index ca69ac3853..f239af9ef8 100644 --- a/plugins/github-deployments/src/api/index.ts +++ b/plugins/github-deployments/src/api/index.ts @@ -14,8 +14,33 @@ * limitations under the License. */ import { createApiRef, OAuthApi } from '@backstage/core'; +import { InputError } from '@backstage/errors'; +import { ScmIntegrationRegistry } from '@backstage/integration'; import { graphql } from '@octokit/graphql'; +const getBaseUrl = ( + scmIntegrationsApi: ScmIntegrationRegistry, + host?: string, +): string => { + if (!host) { + return 'https://api.github.com'; + } + + const integrationConfig = scmIntegrationsApi.github.byHost(host); + + if (!integrationConfig) { + throw new InputError( + `No matching GitHub integration configuration for host ${host}, please check your integrations config`, + ); + } + + if (!integrationConfig.config.apiBaseUrl) { + throw new InputError(`No apiBaseUrl available for host ${host}`); + } + + return integrationConfig.config.apiBaseUrl; +}; + export type GithubDeployment = { environment: string; state: string; @@ -28,6 +53,7 @@ export type GithubDeployment = { export interface GithubDeploymentsApi { listDeployments(options: { + host?: string; owner: string; repo: string; last: number; @@ -41,6 +67,7 @@ export const githubDeploymentsApiRef = createApiRef({ export type Options = { githubAuthApi: OAuthApi; + scmIntegrationsApi: ScmIntegrationRegistry; }; const deploymentsQuery = ` @@ -71,19 +98,24 @@ export type QueryResponse = { export class GithubDeploymentsApiClient implements GithubDeploymentsApi { private readonly githubAuthApi: OAuthApi; + private readonly scmIntegrationsApi: ScmIntegrationRegistry; constructor(options: Options) { this.githubAuthApi = options.githubAuthApi; + this.scmIntegrationsApi = options.scmIntegrationsApi; } async listDeployments(options: { owner: string; repo: string; last: number; + host?: string; }): Promise { + const baseUrl = getBaseUrl(this.scmIntegrationsApi, options.host); const token = await this.githubAuthApi.getAccessToken(['repo']); const graphQLWithAuth = graphql.defaults({ + baseUrl, headers: { authorization: `token ${token}`, }, diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index e20183f1d6..db0092caa8 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -39,6 +39,7 @@ import { import { setupServer } from 'msw/node'; import { graphql } from 'msw'; +import { ScmIntegrations } from '@backstage/integration'; jest.mock('@backstage/plugin-catalog-react', () => ({ useEntity: () => { @@ -48,7 +49,20 @@ jest.mock('@backstage/plugin-catalog-react', () => ({ const errorApiMock = { post: jest.fn(), error$: jest.fn() }; -const configApi: ConfigApi = new ConfigReader({}); +const configApi: ConfigApi = new ConfigReader({ + integrations: { + github: [ + { + host: 'missing-api-base-url.com', + }, + { + host: 'my-github.com', + apiBaseUrl: 'https://api.my-github.com', + }, + ], + }, +}); +const scmIntegrationsApi = ScmIntegrations.fromConfig(configApi); const githubAuthApi: OAuthApi = { getAccessToken: async _ => 'access_token', }; @@ -56,7 +70,10 @@ const githubAuthApi: OAuthApi = { const apis = ApiRegistry.from([ [configApiRef, configApi], [errorApiRef, errorApiMock], - [githubDeploymentsApiRef, new GithubDeploymentsApiClient({ githubAuthApi })], + [ + githubDeploymentsApiRef, + new GithubDeploymentsApiClient({ scmIntegrationsApi, githubAuthApi }), + ], ]); describe('github-deployments', () => { @@ -159,5 +176,41 @@ describe('github-deployments', () => { ).toBeInTheDocument(); expect(await rendered.findByText('failure')).toBeInTheDocument(); }); + + it('shows error when host does not exist', async () => { + worker.use( + graphql.query('deployments', (_, res, ctx) => + res(ctx.data(responseStub)), + ), + ); + + const rendered = await renderInTestApp( + + + , + ); + + expect(await rendered.findByText( + 'Warning: No matching GitHub integration configuration for host unknown-host.com, please check your integrations config', + )).toBeInTheDocument(); + }); + + it('shows error when baseApiURL does not exist forr host', async () => { + worker.use( + graphql.query('deployments', (_, res, ctx) => + res(ctx.data(responseStub)), + ), + ); + + const rendered = await renderInTestApp( + + + , + ); + + expect(await rendered.findByText( + 'Warning: No apiBaseUrl available for host missing-api-base-url.com', + )).toBeInTheDocument(); + }); }); }); diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx index 99dfba560a..691c63fcc3 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx @@ -29,9 +29,11 @@ import { import GithubDeploymentsTable from './GithubDeploymentsTable/GithubDeploymentsTable'; const GithubDeploymentsComponent = ({ + host, projectSlug, last, }: { + host?: string; projectSlug: string; last: number; }) => { @@ -39,7 +41,7 @@ const GithubDeploymentsComponent = ({ const [owner, repo] = projectSlug.split('/'); const { loading, value, error, retry: reload } = useAsyncRetry( - async () => await api.listDeployments({ owner, repo, last }), + async () => await api.listDeployments({ host, owner, repo, last }), ); if (error) { @@ -55,7 +57,13 @@ const GithubDeploymentsComponent = ({ ); }; -export const GithubDeploymentsCard = ({ last }: { last?: number }) => { +export const GithubDeploymentsCard = ({ + last, + host, +}: { + last?: number; + host?: string; +}) => { const { entity } = useEntity(); return !isGithubDeploymentsAvailable(entity) ? ( @@ -66,6 +74,7 @@ export const GithubDeploymentsCard = ({ last }: { last?: number }) => { entity?.metadata.annotations?.[GITHUB_PROJECT_SLUG_ANNOTATION] || '' } last={last || 10} + host={host} /> ); }; diff --git a/plugins/github-deployments/src/plugin.ts b/plugins/github-deployments/src/plugin.ts index 3707b1cf68..943d5e0bc6 100644 --- a/plugins/github-deployments/src/plugin.ts +++ b/plugins/github-deployments/src/plugin.ts @@ -19,6 +19,7 @@ import { createPlugin, githubAuthApiRef, } from '@backstage/core'; +import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { githubDeploymentsApiRef, GithubDeploymentsApiClient } from './api'; export const githubDeploymentsPlugin = createPlugin({ @@ -26,9 +27,12 @@ export const githubDeploymentsPlugin = createPlugin({ apis: [ createApiFactory({ api: githubDeploymentsApiRef, - deps: { githubAuthApi: githubAuthApiRef }, - factory: ({ githubAuthApi }) => - new GithubDeploymentsApiClient({ githubAuthApi }), + deps: { + scmIntegrationsApi: scmIntegrationsApiRef, + githubAuthApi: githubAuthApiRef, + }, + factory: ({ scmIntegrationsApi, githubAuthApi }) => + new GithubDeploymentsApiClient({ scmIntegrationsApi, githubAuthApi }), }), ], }); From f5a916c497671f9f47b44eb6ff56a3fa018c1104 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 6 Apr 2021 14:00:58 +0100 Subject: [PATCH 02/95] changeset and typo Signed-off-by: Andrew Johnson --- .changeset/smooth-vans-travel.md | 5 +++++ .../src/components/GithubDeploymentsCard.test.tsx | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/smooth-vans-travel.md diff --git a/.changeset/smooth-vans-travel.md b/.changeset/smooth-vans-travel.md new file mode 100644 index 0000000000..0038c35e8e --- /dev/null +++ b/.changeset/smooth-vans-travel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-github-deployments': patch +--- + +Support for GitHub Enterprise with GithubDeploymentsPlugin diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index db0092caa8..a671e6a847 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -195,7 +195,7 @@ describe('github-deployments', () => { )).toBeInTheDocument(); }); - it('shows error when baseApiURL does not exist forr host', async () => { + it('shows error when baseApiURL does not exist for host', async () => { worker.use( graphql.query('deployments', (_, res, ctx) => res(ctx.data(responseStub)), From 612a9c03399b74b9cad0f6d0c444f21548b78e19 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 6 Apr 2021 14:06:57 +0100 Subject: [PATCH 03/95] update readme Signed-off-by: Andrew Johnson --- plugins/github-deployments/README.md | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/plugins/github-deployments/README.md b/plugins/github-deployments/README.md index 174d1bc8c7..64ae9c66ca 100644 --- a/plugins/github-deployments/README.md +++ b/plugins/github-deployments/README.md @@ -60,3 +60,33 @@ spec: owner: CNCF lifecycle: experimental ``` + +### Self-hosted / Enterprise GitHub + +1. Add the `host` and `apiBaseUrl` to your `app-config.yaml` + +```yaml +# app-config.yaml + +integrations: + github: + - host: 'your-github-host.com' + apiBaseUrl: 'https://api.your-github-host.com' +``` + +2. Pass the host into the `EntityGithubDeploymentsCard` + +```typescript +// packages/app/src/components/catalog/EntityPage.tsx + +import { EntityGithubDeploymentsCard } from '@backstage/plugin-github-deployments'; + +const OverviewContent = () => ( + + // ... + + + + // ... + +); \ No newline at end of file From b4c269777b383e096ed7daae2559dcf234c95dde Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 6 Apr 2021 14:11:19 +0100 Subject: [PATCH 04/95] prettier Signed-off-by: Andrew Johnson --- plugins/github-deployments/README.md | 5 +++-- .../components/GithubDeploymentsCard.test.tsx | 16 ++++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/plugins/github-deployments/README.md b/plugins/github-deployments/README.md index 64ae9c66ca..42f550059c 100644 --- a/plugins/github-deployments/README.md +++ b/plugins/github-deployments/README.md @@ -85,8 +85,9 @@ const OverviewContent = () => ( // ... - + // ... -); \ No newline at end of file +); +``` diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index a671e6a847..d7bc0dfcc8 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -190,9 +190,11 @@ describe('github-deployments', () => { , ); - expect(await rendered.findByText( - 'Warning: No matching GitHub integration configuration for host unknown-host.com, please check your integrations config', - )).toBeInTheDocument(); + expect( + await rendered.findByText( + 'Warning: No matching GitHub integration configuration for host unknown-host.com, please check your integrations config', + ), + ).toBeInTheDocument(); }); it('shows error when baseApiURL does not exist for host', async () => { @@ -208,9 +210,11 @@ describe('github-deployments', () => { , ); - expect(await rendered.findByText( - 'Warning: No apiBaseUrl available for host missing-api-base-url.com', - )).toBeInTheDocument(); + expect( + await rendered.findByText( + 'Warning: No apiBaseUrl available for host missing-api-base-url.com', + ), + ).toBeInTheDocument(); }); }); }); From c05da9a5d9bb07ee0c21483d0c3960f8c23f2be9 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Thu, 8 Apr 2021 12:18:34 +0100 Subject: [PATCH 05/95] use location from entity Signed-off-by: Andrew Johnson --- plugins/github-deployments/src/api/index.ts | 49 +++-- .../components/GithubDeploymentsCard.test.tsx | 208 ++++++++++++------ .../src/components/GithubDeploymentsCard.tsx | 23 +- plugins/github-deployments/src/mocks/mocks.ts | 7 +- 4 files changed, 188 insertions(+), 99 deletions(-) diff --git a/plugins/github-deployments/src/api/index.ts b/plugins/github-deployments/src/api/index.ts index f239af9ef8..31e3defa47 100644 --- a/plugins/github-deployments/src/api/index.ts +++ b/plugins/github-deployments/src/api/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { parseLocationReference } from '@backstage/catalog-model'; import { createApiRef, OAuthApi } from '@backstage/core'; import { InputError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; @@ -20,25 +21,29 @@ import { graphql } from '@octokit/graphql'; const getBaseUrl = ( scmIntegrationsApi: ScmIntegrationRegistry, - host?: string, + location?: string, ): string => { - if (!host) { + if (!location) { return 'https://api.github.com'; } - const integrationConfig = scmIntegrationsApi.github.byHost(host); + const config = scmIntegrationsApi.github.byUrl( + parseLocationReference(location).target, + ); - if (!integrationConfig) { + if (!config) { throw new InputError( - `No matching GitHub integration configuration for host ${host}, please check your integrations config`, + `No matching GitHub integration configuration for location ${location}, please check your integrations config`, ); } - if (!integrationConfig.config.apiBaseUrl) { - throw new InputError(`No apiBaseUrl available for host ${host}`); + if (!config.config.apiBaseUrl) { + throw new InputError( + `No apiBaseUrl available for location ${location}, please check your integrations config`, + ); } - return integrationConfig.config.apiBaseUrl; + return config?.config.apiBaseUrl; }; export type GithubDeployment = { @@ -51,13 +56,17 @@ export type GithubDeployment = { }; }; +type QueryOptions = { + owner: string; + repo: string; + last: number; +}; + export interface GithubDeploymentsApi { - listDeployments(options: { - host?: string; - owner: string; - repo: string; - last: number; - }): Promise; + listDeployments( + options: QueryOptions, + location?: string, + ): Promise; } export const githubDeploymentsApiRef = createApiRef({ @@ -105,13 +114,11 @@ export class GithubDeploymentsApiClient implements GithubDeploymentsApi { this.scmIntegrationsApi = options.scmIntegrationsApi; } - async listDeployments(options: { - owner: string; - repo: string; - last: number; - host?: string; - }): Promise { - const baseUrl = getBaseUrl(this.scmIntegrationsApi, options.host); + async listDeployments( + options: QueryOptions, + location?: string, + ): Promise { + const baseUrl = getBaseUrl(this.scmIntegrationsApi, location); const token = await this.githubAuthApi.getAccessToken(['repo']); const graphQLWithAuth = graphql.defaults({ diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index d7bc0dfcc8..040a9b6571 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -40,10 +40,13 @@ import { import { setupServer } from 'msw/node'; import { graphql } from 'msw'; import { ScmIntegrations } from '@backstage/integration'; +import { Entity } from '@backstage/catalog-model'; + +let entity: { entity: Entity }; jest.mock('@backstage/plugin-catalog-react', () => ({ useEntity: () => { - return entityStub; + return entity; }, })); @@ -53,15 +56,24 @@ const configApi: ConfigApi = new ConfigReader({ integrations: { github: [ { - host: 'missing-api-base-url.com', + host: 'my-github-1.com', + apiBaseUrl: 'https://api.my-github-1.com', }, { - host: 'my-github.com', - apiBaseUrl: 'https://api.my-github.com', + host: 'my-github-2.com', + apiBaseUrl: 'https://api.my-github-2.com', + }, + { + host: 'my-github-3.com', }, ], }, }); + +const GRAPHQL_GITHUB_API = graphql.link('https://api.github.com/graphql'); +const GRAPHQL_CUSTOM_API_1 = graphql.link('https://api.github-1.com/graphql'); +const GRAPHQL_CUSTOM_API_2 = graphql.link('https://api.github-2.com/graphql'); + const scmIntegrationsApi = ScmIntegrations.fromConfig(configApi); const githubAuthApi: OAuthApi = { getAccessToken: async _ => 'access_token', @@ -76,11 +88,36 @@ const apis = ApiRegistry.from([ ], ]); +const assertFetchedData = async () => { + const rendered = await renderInTestApp( + + + , + ); + + expect(await rendered.findByText('GitHub Deployments')).toBeInTheDocument(); + + expect(await rendered.findByText('active')).toBeInTheDocument(); + expect(await rendered.findByText('prd')).toBeInTheDocument(); + expect(await rendered.findByText('12345')).toHaveAttribute( + 'href', + 'https://exampleapi.com/123456789', + ); + + expect(await rendered.findByText('pending')).toBeInTheDocument(); + expect(await rendered.findByText('lab')).toBeInTheDocument(); + expect(await rendered.findByText('54321')).toHaveAttribute( + 'href', + 'https://exampleapi.com/543212345', + ); +}; + describe('github-deployments', () => { const worker = setupServer(); msw.setupDefaultHandlers(worker); beforeEach(() => { + worker.resetHandlers(); jest.resetAllMocks(); }); @@ -91,40 +128,27 @@ describe('github-deployments', () => { }); describe('GithubDeploymentsCard', () => { - it('should display fetched data', async () => { + beforeEach(() => { + entity = entityStub; + entity.entity.metadata.annotations = { + 'github.com/project-slug': 'org/repo', + }; + }); + + it('displays fetched data using default github api', async () => { worker.use( - graphql.query('deployments', (_, res, ctx) => + GRAPHQL_GITHUB_API.query('deployments', (_, res, ctx) => res(ctx.data(responseStub)), ), ); - const rendered = await renderInTestApp( - - - , - ); - - expect( - await rendered.findByText('GitHub Deployments'), - ).toBeInTheDocument(); - expect(await rendered.findByText('active')).toBeInTheDocument(); - expect(await rendered.findByText('prd')).toBeInTheDocument(); - expect(await rendered.findByText('12345')).toHaveAttribute( - 'href', - 'https://exampleapi.com/123456789', - ); - - expect(await rendered.findByText('pending')).toBeInTheDocument(); - expect(await rendered.findByText('lab')).toBeInTheDocument(); - expect(await rendered.findByText('54321')).toHaveAttribute( - 'href', - 'https://exampleapi.com/543212345', - ); + await assertFetchedData(); + expect.assertions(7); }); it('should display empty state when no data', async () => { worker.use( - graphql.query('deployments', (_, res, ctx) => + GRAPHQL_GITHUB_API.query('deployments', (_, res, ctx) => res(ctx.data(noDataResponseStub)), ), ); @@ -145,7 +169,7 @@ describe('github-deployments', () => { it('should shows new data on reload', async () => { worker.use( - graphql.query('deployments', (_, res, ctx) => + GRAPHQL_GITHUB_API.query('deployments', (_, res, ctx) => res(ctx.data(responseStub)), ), ); @@ -163,7 +187,7 @@ describe('github-deployments', () => { worker.resetHandlers(); worker.use( - graphql.query('deployments', (_, res, ctx) => + GRAPHQL_GITHUB_API.query('deployments', (_, res, ctx) => res(ctx.data(refreshedResponseStub)), ), ); @@ -177,44 +201,102 @@ describe('github-deployments', () => { expect(await rendered.findByText('failure')).toBeInTheDocument(); }); - it('shows error when host does not exist', async () => { - worker.use( - graphql.query('deployments', (_, res, ctx) => - res(ctx.data(responseStub)), - ), - ); + describe('entity with source location', () => { + beforeEach(() => { + entity = entityStub; + entity.entity.metadata.annotations = { + 'github.com/project-slug': 'org/repo', + 'backstage.io/source-location': + 'url:https://my-github-1.com/org/repo', + }; + }); - const rendered = await renderInTestApp( - - - , - ); + it('should fetch data using custom api', async () => { + const customAPI = graphql.link('https://api.my-github-1.com/graphql'); - expect( - await rendered.findByText( - 'Warning: No matching GitHub integration configuration for host unknown-host.com, please check your integrations config', - ), - ).toBeInTheDocument(); + worker.use( + customAPI.query('deployments', (_, res, ctx) => + res(ctx.data(responseStub)), + ), + ); + + await assertFetchedData(); + expect.assertions(7); + }); }); - it('shows error when baseApiURL does not exist for host', async () => { - worker.use( - graphql.query('deployments', (_, res, ctx) => - res(ctx.data(responseStub)), - ), - ); + describe('entity with managed by location', () => { + beforeEach(() => { + entity = entityStub; + entity.entity.metadata.annotations = { + 'github.com/project-slug': 'org/repo', + 'backstage.io/managed-by-location': + 'url:https://my-github-2.com/org/repo', + }; + }); - const rendered = await renderInTestApp( - - - , - ); + it('should fetch data using custom api', async () => { + const customAPI = graphql.link('https://api.my-github-2.com/graphql'); - expect( - await rendered.findByText( - 'Warning: No apiBaseUrl available for host missing-api-base-url.com', - ), - ).toBeInTheDocument(); + worker.use( + customAPI.query('deployments', (_, res, ctx) => + res(ctx.data(responseStub)), + ), + ); + + await assertFetchedData(); + expect.assertions(7); + }); + }); + + describe('entity with location without baseApiURL', () => { + beforeEach(() => { + entity = entityStub; + entity.entity.metadata.annotations = { + 'github.com/project-slug': 'org/repo', + 'backstage.io/managed-by-location': + 'url:https://my-github-3.com/org/repo', + }; + }); + + it('shows error when no matching github host', async () => { + const rendered = await renderInTestApp( + + + , + ); + + expect( + await rendered.findByText( + 'Warning: No apiBaseUrl available for location url:https://my-github-3.com/org/repo, please check your integrations config', + ), + ).toBeInTheDocument(); + }); + }); + + describe('entity with location without matching host', () => { + beforeEach(() => { + entity = entityStub; + entity.entity.metadata.annotations = { + 'github.com/project-slug': 'org/repo', + 'backstage.io/managed-by-location': + 'url:https://my-github-unknown.com/org/repo', + }; + }); + + it('shows error when no matching github host', async () => { + const rendered = await renderInTestApp( + + + , + ); + + expect( + await rendered.findByText( + 'Warning: No matching GitHub integration configuration for location url:https://my-github-unknown.com/org/repo, please check your integrations config', + ), + ).toBeInTheDocument(); + }); }); }); }); diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx index 691c63fcc3..f7ba6293ee 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx @@ -27,21 +27,25 @@ import { isGithubDeploymentsAvailable, } from '../Router'; import GithubDeploymentsTable from './GithubDeploymentsTable/GithubDeploymentsTable'; +import { + LOCATION_ANNOTATION, + SOURCE_LOCATION_ANNOTATION, +} from '@backstage/catalog-model'; const GithubDeploymentsComponent = ({ - host, projectSlug, last, + location, }: { - host?: string; projectSlug: string; last: number; + location?: string; }) => { const api = useApi(githubDeploymentsApiRef); const [owner, repo] = projectSlug.split('/'); const { loading, value, error, retry: reload } = useAsyncRetry( - async () => await api.listDeployments({ host, owner, repo, last }), + async () => await api.listDeployments({ owner, repo, last }, location), ); if (error) { @@ -57,14 +61,11 @@ const GithubDeploymentsComponent = ({ ); }; -export const GithubDeploymentsCard = ({ - last, - host, -}: { - last?: number; - host?: string; -}) => { +export const GithubDeploymentsCard = ({ last }: { last?: number }) => { const { entity } = useEntity(); + const location = + entity?.metadata.annotations?.[SOURCE_LOCATION_ANNOTATION] || + entity?.metadata.annotations?.[LOCATION_ANNOTATION]; return !isGithubDeploymentsAvailable(entity) ? ( @@ -74,7 +75,7 @@ export const GithubDeploymentsCard = ({ entity?.metadata.annotations?.[GITHUB_PROJECT_SLUG_ANNOTATION] || '' } last={last || 10} - host={host} + location={location} /> ); }; diff --git a/plugins/github-deployments/src/mocks/mocks.ts b/plugins/github-deployments/src/mocks/mocks.ts index f636a08558..9e10e6c289 100644 --- a/plugins/github-deployments/src/mocks/mocks.ts +++ b/plugins/github-deployments/src/mocks/mocks.ts @@ -13,15 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Entity } from '@backstage/catalog-model'; import { QueryResponse } from '../api'; -export const entityStub = { +export const entityStub: { entity: Entity } = { entity: { metadata: { namespace: 'default', - annotations: { - 'github.com/project-slug': 'org/repo', - }, + annotations: {}, name: 'sample-service', description: 'Sample service', uid: 'g0h33dd9-56h7-835b-b63v-7x5da3j64851', From ebce9fdbeef552544540a0c304c6aa85860d5524 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Thu, 8 Apr 2021 12:30:13 +0100 Subject: [PATCH 06/95] update test Signed-off-by: Andrew Johnson --- plugins/github-deployments/src/api/index.ts | 20 +++++--- .../components/GithubDeploymentsCard.test.tsx | 50 ++++++++++++++----- .../src/components/GithubDeploymentsCard.tsx | 15 +++--- 3 files changed, 57 insertions(+), 28 deletions(-) diff --git a/plugins/github-deployments/src/api/index.ts b/plugins/github-deployments/src/api/index.ts index 31e3defa47..221eb3be8a 100644 --- a/plugins/github-deployments/src/api/index.ts +++ b/plugins/github-deployments/src/api/index.ts @@ -21,15 +21,19 @@ import { graphql } from '@octokit/graphql'; const getBaseUrl = ( scmIntegrationsApi: ScmIntegrationRegistry, - location?: string, + locations: string[], ): string => { - if (!location) { + const targets = locations + .map(parseLocationReference) + .filter(location => location.type === 'url') + .map(location => location.target); + + if (targets.length === 0) { return 'https://api.github.com'; } - const config = scmIntegrationsApi.github.byUrl( - parseLocationReference(location).target, - ); + const location = targets[0]; + const config = scmIntegrationsApi.github.byUrl(location); if (!config) { throw new InputError( @@ -65,7 +69,7 @@ type QueryOptions = { export interface GithubDeploymentsApi { listDeployments( options: QueryOptions, - location?: string, + location: string[], ): Promise; } @@ -116,9 +120,9 @@ export class GithubDeploymentsApiClient implements GithubDeploymentsApi { async listDeployments( options: QueryOptions, - location?: string, + locations: string[], ): Promise { - const baseUrl = getBaseUrl(this.scmIntegrationsApi, location); + const baseUrl = getBaseUrl(this.scmIntegrationsApi, locations); const token = await this.githubAuthApi.getAccessToken(['repo']); const graphQLWithAuth = graphql.defaults({ diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index 040a9b6571..d2f6b1c4b6 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -71,8 +71,12 @@ const configApi: ConfigApi = new ConfigReader({ }); const GRAPHQL_GITHUB_API = graphql.link('https://api.github.com/graphql'); -const GRAPHQL_CUSTOM_API_1 = graphql.link('https://api.github-1.com/graphql'); -const GRAPHQL_CUSTOM_API_2 = graphql.link('https://api.github-2.com/graphql'); +const GRAPHQL_CUSTOM_API_1 = graphql.link( + 'https://api.my-github-1.com/graphql', +); +const GRAPHQL_CUSTOM_API_2 = graphql.link( + 'https://api.my-github-2.com/graphql', +); const scmIntegrationsApi = ScmIntegrations.fromConfig(configApi); const githubAuthApi: OAuthApi = { @@ -167,7 +171,7 @@ describe('github-deployments', () => { ).toBeInTheDocument(); }); - it('should shows new data on reload', async () => { + it('should show new data on reload', async () => { worker.use( GRAPHQL_GITHUB_API.query('deployments', (_, res, ctx) => res(ctx.data(responseStub)), @@ -212,10 +216,8 @@ describe('github-deployments', () => { }); it('should fetch data using custom api', async () => { - const customAPI = graphql.link('https://api.my-github-1.com/graphql'); - worker.use( - customAPI.query('deployments', (_, res, ctx) => + GRAPHQL_CUSTOM_API_1.query('deployments', (_, res, ctx) => res(ctx.data(responseStub)), ), ); @@ -236,10 +238,8 @@ describe('github-deployments', () => { }); it('should fetch data using custom api', async () => { - const customAPI = graphql.link('https://api.my-github-2.com/graphql'); - worker.use( - customAPI.query('deployments', (_, res, ctx) => + GRAPHQL_CUSTOM_API_2.query('deployments', (_, res, ctx) => res(ctx.data(responseStub)), ), ); @@ -259,7 +259,7 @@ describe('github-deployments', () => { }; }); - it('shows error when no matching github host', async () => { + it('shows no apiBaseUrl error', async () => { const rendered = await renderInTestApp( @@ -268,7 +268,7 @@ describe('github-deployments', () => { expect( await rendered.findByText( - 'Warning: No apiBaseUrl available for location url:https://my-github-3.com/org/repo, please check your integrations config', + 'Warning: No apiBaseUrl available for location https://my-github-3.com/org/repo, please check your integrations config', ), ).toBeInTheDocument(); }); @@ -284,7 +284,7 @@ describe('github-deployments', () => { }; }); - it('shows error when no matching github host', async () => { + it('shows no matching host error', async () => { const rendered = await renderInTestApp( @@ -293,10 +293,34 @@ describe('github-deployments', () => { expect( await rendered.findByText( - 'Warning: No matching GitHub integration configuration for location url:https://my-github-unknown.com/org/repo, please check your integrations config', + 'Warning: No matching GitHub integration configuration for location https://my-github-unknown.com/org/repo, please check your integrations config', ), ).toBeInTheDocument(); }); }); + + describe('entity with non url location', () => { + beforeEach(() => { + entity = entityStub; + entity.entity.metadata.annotations = { + 'github.com/project-slug': 'org/repo', + 'backstage.io/source-location': + 'some-other-managed-location:my-favourite-location/org/repo', + 'backstage.io/managed-by-location': + 'file:my-favourite-file/org/repo.yaml', + }; + }); + + it('displays fetched data using default github api', async () => { + worker.use( + GRAPHQL_GITHUB_API.query('deployments', (_, res, ctx) => + res(ctx.data(responseStub)), + ), + ); + + await assertFetchedData(); + expect.assertions(7); + }); + }); }); }); diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx index f7ba6293ee..eccab4089a 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx @@ -35,17 +35,17 @@ import { const GithubDeploymentsComponent = ({ projectSlug, last, - location, + locations, }: { projectSlug: string; last: number; - location?: string; + locations: string[]; }) => { const api = useApi(githubDeploymentsApiRef); const [owner, repo] = projectSlug.split('/'); const { loading, value, error, retry: reload } = useAsyncRetry( - async () => await api.listDeployments({ owner, repo, last }, location), + async () => await api.listDeployments({ owner, repo, last }, locations), ); if (error) { @@ -63,9 +63,10 @@ const GithubDeploymentsComponent = ({ export const GithubDeploymentsCard = ({ last }: { last?: number }) => { const { entity } = useEntity(); - const location = - entity?.metadata.annotations?.[SOURCE_LOCATION_ANNOTATION] || - entity?.metadata.annotations?.[LOCATION_ANNOTATION]; + const locations = [ + entity?.metadata.annotations?.[SOURCE_LOCATION_ANNOTATION], + entity?.metadata.annotations?.[LOCATION_ANNOTATION], + ].filter(location => location !== undefined) as string[]; return !isGithubDeploymentsAvailable(entity) ? ( @@ -75,7 +76,7 @@ export const GithubDeploymentsCard = ({ last }: { last?: number }) => { entity?.metadata.annotations?.[GITHUB_PROJECT_SLUG_ANNOTATION] || '' } last={last || 10} - location={location} + locations={locations} /> ); }; From 2842e953f42d2df618c80aa8281221692b484f93 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Thu, 8 Apr 2021 16:37:22 +0100 Subject: [PATCH 07/95] as per feedback Signed-off-by: Andrew Johnson --- .changeset/smooth-vans-travel.md | 2 +- plugins/github-deployments/README.md | 21 +++---------------- plugins/github-deployments/package.json | 8 +++---- plugins/github-deployments/src/api/index.ts | 14 ++++++++----- .../src/components/GithubDeploymentsCard.tsx | 2 +- 5 files changed, 18 insertions(+), 29 deletions(-) diff --git a/.changeset/smooth-vans-travel.md b/.changeset/smooth-vans-travel.md index 0038c35e8e..eb074c8ecf 100644 --- a/.changeset/smooth-vans-travel.md +++ b/.changeset/smooth-vans-travel.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-github-deployments': patch +'@backstage/plugin-github-deployments': minor --- Support for GitHub Enterprise with GithubDeploymentsPlugin diff --git a/plugins/github-deployments/README.md b/plugins/github-deployments/README.md index 42f550059c..e1f3d7c2c0 100644 --- a/plugins/github-deployments/README.md +++ b/plugins/github-deployments/README.md @@ -63,6 +63,9 @@ spec: ### Self-hosted / Enterprise GitHub +The plugin will try to use `backstage.io/source-location` or `backstage.io/managed-by-location` +annotations to figure out the location of the source code. + 1. Add the `host` and `apiBaseUrl` to your `app-config.yaml` ```yaml @@ -73,21 +76,3 @@ integrations: - host: 'your-github-host.com' apiBaseUrl: 'https://api.your-github-host.com' ``` - -2. Pass the host into the `EntityGithubDeploymentsCard` - -```typescript -// packages/app/src/components/catalog/EntityPage.tsx - -import { EntityGithubDeploymentsCard } from '@backstage/plugin-github-deployments'; - -const OverviewContent = () => ( - - // ... - - - - // ... - -); -``` diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index 528d660ea8..39c7577bb5 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.4", - "@backstage/core": "^0.7.3", + "@backstage/catalog-model": "^0.7.6", + "@backstage/core": "^0.7.4", "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.1", "@backstage/integration-react": "^0.1.1", @@ -37,9 +37,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.6", + "@backstage/cli": "^0.6.7", "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.9", + "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/github-deployments/src/api/index.ts b/plugins/github-deployments/src/api/index.ts index 221eb3be8a..0deb586e6d 100644 --- a/plugins/github-deployments/src/api/index.ts +++ b/plugins/github-deployments/src/api/index.ts @@ -60,16 +60,20 @@ export type GithubDeployment = { }; }; -type QueryOptions = { +type QueryParams = { owner: string; repo: string; last: number; }; +type QueryOptions = { + locations: string[]; +}; + export interface GithubDeploymentsApi { listDeployments( + params: QueryParams, options: QueryOptions, - location: string[], ): Promise; } @@ -119,10 +123,10 @@ export class GithubDeploymentsApiClient implements GithubDeploymentsApi { } async listDeployments( + params: QueryParams, options: QueryOptions, - locations: string[], ): Promise { - const baseUrl = getBaseUrl(this.scmIntegrationsApi, locations); + const baseUrl = getBaseUrl(this.scmIntegrationsApi, options.locations); const token = await this.githubAuthApi.getAccessToken(['repo']); const graphQLWithAuth = graphql.defaults({ @@ -134,7 +138,7 @@ export class GithubDeploymentsApiClient implements GithubDeploymentsApi { const response: QueryResponse = await graphQLWithAuth( deploymentsQuery, - options, + params, ); return response.repository?.deployments?.nodes?.reverse() || []; } diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx index eccab4089a..fb433338fa 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx @@ -45,7 +45,7 @@ const GithubDeploymentsComponent = ({ const [owner, repo] = projectSlug.split('/'); const { loading, value, error, retry: reload } = useAsyncRetry( - async () => await api.listDeployments({ owner, repo, last }, locations), + async () => await api.listDeployments({ owner, repo, last }, { locations }), ); if (error) { From 6b4b3c89e30c7452ce00c0aa1e41c8b17b38ff5f Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 16 Apr 2021 17:39:29 +0100 Subject: [PATCH 08/95] prettier Signed-off-by: Andrew Johnson --- .../src/components/GithubDeploymentsCard.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index f68e57ed73..cae8313422 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -210,7 +210,7 @@ describe('github-deployments', () => { ).toBeInTheDocument(); expect(await rendered.findByText('failure')).toBeInTheDocument(); }); - + it('should display extra columns', async () => { worker.use( graphql.query('deployments', (_, res, ctx) => From 2d3ee7572eb6f7816dd725c1b6230ad9c4171c9d Mon Sep 17 00:00:00 2001 From: yacut Date: Wed, 14 Apr 2021 17:29:23 +0200 Subject: [PATCH 09/95] add iLert plugin Signed-off-by: yacut --- .github/CODEOWNERS | 1 + microsite/data/plugins/ilert.yaml | 13 + plugins/ilert/.eslintrc.js | 6 + plugins/ilert/README.md | 132 +++++ plugins/ilert/dev/index.tsx | 27 + plugins/ilert/package.json | 54 ++ plugins/ilert/src/api/client.ts | 500 ++++++++++++++++++ plugins/ilert/src/api/index.ts | 23 + plugins/ilert/src/api/types.ts | 110 ++++ plugins/ilert/src/assets/ilert.icon.svg | 11 + .../AlertSource/AlertSourceLink.tsx | 71 +++ .../MissingAuthorizationHeaderError.tsx | 35 ++ plugins/ilert/src/components/Errors/index.ts | 17 + .../EscalationPolicy/EscalationPolicyLink.tsx | 49 ++ .../src/components/ILertCard/ILertCard.tsx | 145 +++++ .../ILertCard/ILertCardActionsHeader.tsx | 203 +++++++ .../ILertCard/ILertCardEmptyState.tsx | 86 +++ .../ILertCard/ILertCardHeaderStatus.tsx | 46 ++ .../ILertCard/ILertCardMaintenanceModal.tsx | 138 +++++ .../ilert/src/components/ILertCard/index.ts | 16 + .../src/components/ILertPage/ILertPage.tsx | 68 +++ .../ilert/src/components/ILertPage/index.ts | 16 + .../Incident/IncidentActionsMenu.tsx | 148 ++++++ .../Incident/IncidentAssignModal.tsx | 183 +++++++ .../src/components/Incident/IncidentLink.tsx | 45 ++ .../components/Incident/IncidentNewModal.tsx | 230 ++++++++ .../components/Incident/IncidentStatus.tsx | 48 ++ .../ilert/src/components/Incident/index.ts | 17 + .../IncidentsPage/IncidentsPage.tsx | 93 ++++ .../IncidentsPage/IncidentsTable.tsx | 253 +++++++++ .../components/IncidentsPage/StatusChip.tsx | 57 ++ .../components/IncidentsPage/TableTitle.tsx | 97 ++++ .../src/components/IncidentsPage/index.ts | 17 + .../OnCallSchedulesGrid.tsx | 150 ++++++ .../OnCallSchedulesPage.tsx | 56 ++ .../OnCallSchedulesPage/OnCallShiftItem.tsx | 105 ++++ .../components/OnCallSchedulesPage/index.ts | 17 + .../components/Shift/ShiftOverrideModal.tsx | 196 +++++++ .../UptimeMonitorActionsMenu.tsx | 134 +++++ .../UptimeMonitor/UptimeMonitorLink.tsx | 49 ++ .../src/components/UptimeMonitor/index.ts | 16 + .../UptimeMonitorsPage/StatusChip.tsx | 73 +++ .../UptimeMonitorCheckType.tsx | 39 ++ .../UptimeMonitorsPage/UptimeMonitorsPage.tsx | 59 +++ .../UptimeMonitorsTable.tsx | 173 ++++++ .../components/UptimeMonitorsPage/index.ts | 17 + plugins/ilert/src/components/index.ts | 18 + plugins/ilert/src/constants.ts | 16 + plugins/ilert/src/hooks/index.ts | 28 + plugins/ilert/src/hooks/useAlertSource.ts | 103 ++++ plugins/ilert/src/hooks/useAssignIncident.ts | 73 +++ plugins/ilert/src/hooks/useIncidents.ts | 151 ++++++ plugins/ilert/src/hooks/useNewIncident.ts | 77 +++ plugins/ilert/src/hooks/useOnCallSchedules.ts | 60 +++ plugins/ilert/src/hooks/useShiftOverride.ts | 78 +++ plugins/ilert/src/hooks/useUptimeMonitors.ts | 88 +++ plugins/ilert/src/index.ts | 33 ++ plugins/ilert/src/plugin.test.ts | 22 + plugins/ilert/src/plugin.ts | 60 +++ plugins/ilert/src/route-refs.tsx | 24 + plugins/ilert/src/setupTests.ts | 17 + plugins/ilert/src/types.ts | 312 +++++++++++ 62 files changed, 5199 insertions(+) create mode 100644 microsite/data/plugins/ilert.yaml create mode 100644 plugins/ilert/.eslintrc.js create mode 100644 plugins/ilert/README.md create mode 100644 plugins/ilert/dev/index.tsx create mode 100644 plugins/ilert/package.json create mode 100644 plugins/ilert/src/api/client.ts create mode 100644 plugins/ilert/src/api/index.ts create mode 100644 plugins/ilert/src/api/types.ts create mode 100644 plugins/ilert/src/assets/ilert.icon.svg create mode 100644 plugins/ilert/src/components/AlertSource/AlertSourceLink.tsx create mode 100644 plugins/ilert/src/components/Errors/MissingAuthorizationHeaderError.tsx create mode 100644 plugins/ilert/src/components/Errors/index.ts create mode 100644 plugins/ilert/src/components/EscalationPolicy/EscalationPolicyLink.tsx create mode 100644 plugins/ilert/src/components/ILertCard/ILertCard.tsx create mode 100644 plugins/ilert/src/components/ILertCard/ILertCardActionsHeader.tsx create mode 100644 plugins/ilert/src/components/ILertCard/ILertCardEmptyState.tsx create mode 100644 plugins/ilert/src/components/ILertCard/ILertCardHeaderStatus.tsx create mode 100644 plugins/ilert/src/components/ILertCard/ILertCardMaintenanceModal.tsx create mode 100644 plugins/ilert/src/components/ILertCard/index.ts create mode 100644 plugins/ilert/src/components/ILertPage/ILertPage.tsx create mode 100644 plugins/ilert/src/components/ILertPage/index.ts create mode 100644 plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx create mode 100644 plugins/ilert/src/components/Incident/IncidentAssignModal.tsx create mode 100644 plugins/ilert/src/components/Incident/IncidentLink.tsx create mode 100644 plugins/ilert/src/components/Incident/IncidentNewModal.tsx create mode 100644 plugins/ilert/src/components/Incident/IncidentStatus.tsx create mode 100644 plugins/ilert/src/components/Incident/index.ts create mode 100644 plugins/ilert/src/components/IncidentsPage/IncidentsPage.tsx create mode 100644 plugins/ilert/src/components/IncidentsPage/IncidentsTable.tsx create mode 100644 plugins/ilert/src/components/IncidentsPage/StatusChip.tsx create mode 100644 plugins/ilert/src/components/IncidentsPage/TableTitle.tsx create mode 100644 plugins/ilert/src/components/IncidentsPage/index.ts create mode 100644 plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx create mode 100644 plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesPage.tsx create mode 100644 plugins/ilert/src/components/OnCallSchedulesPage/OnCallShiftItem.tsx create mode 100644 plugins/ilert/src/components/OnCallSchedulesPage/index.ts create mode 100644 plugins/ilert/src/components/Shift/ShiftOverrideModal.tsx create mode 100644 plugins/ilert/src/components/UptimeMonitor/UptimeMonitorActionsMenu.tsx create mode 100644 plugins/ilert/src/components/UptimeMonitor/UptimeMonitorLink.tsx create mode 100644 plugins/ilert/src/components/UptimeMonitor/index.ts create mode 100644 plugins/ilert/src/components/UptimeMonitorsPage/StatusChip.tsx create mode 100644 plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorCheckType.tsx create mode 100644 plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsPage.tsx create mode 100644 plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsTable.tsx create mode 100644 plugins/ilert/src/components/UptimeMonitorsPage/index.ts create mode 100644 plugins/ilert/src/components/index.ts create mode 100644 plugins/ilert/src/constants.ts create mode 100644 plugins/ilert/src/hooks/index.ts create mode 100644 plugins/ilert/src/hooks/useAlertSource.ts create mode 100644 plugins/ilert/src/hooks/useAssignIncident.ts create mode 100644 plugins/ilert/src/hooks/useIncidents.ts create mode 100644 plugins/ilert/src/hooks/useNewIncident.ts create mode 100644 plugins/ilert/src/hooks/useOnCallSchedules.ts create mode 100644 plugins/ilert/src/hooks/useShiftOverride.ts create mode 100644 plugins/ilert/src/hooks/useUptimeMonitors.ts create mode 100644 plugins/ilert/src/index.ts create mode 100644 plugins/ilert/src/plugin.test.ts create mode 100644 plugins/ilert/src/plugin.ts create mode 100644 plugins/ilert/src/route-refs.tsx create mode 100644 plugins/ilert/src/setupTests.ts create mode 100644 plugins/ilert/src/types.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4e21603c63..d939d529dc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -14,6 +14,7 @@ /plugins/search-* @backstage/techdocs-core /plugins/techdocs @backstage/techdocs-core /plugins/techdocs-backend @backstage/techdocs-core +/plugins/ilert @yacut /packages/techdocs-common @backstage/techdocs-core /.changeset/cost-insights-* @backstage/silver-lining /.changeset/techdocs-* @backstage/techdocs-core diff --git a/microsite/data/plugins/ilert.yaml b/microsite/data/plugins/ilert.yaml new file mode 100644 index 0000000000..99cfca01bc --- /dev/null +++ b/microsite/data/plugins/ilert.yaml @@ -0,0 +1,13 @@ +--- +title: iLert +author: iLert +authorUrl: https://github.com/iLert +category: Monitoring +description: iLert is a platform for alerting, on-call management and uptime monitoring. +documentation: https://github.com/backstage/backstage/tree/master/plugins/ilert +iconUrl: https://avatars.githubusercontent.com/u/13230510?s=200&v=4 +npmPackageName: '@backstage/plugin-ilert' +tags: + - monitoring + - errors + - alerting diff --git a/plugins/ilert/.eslintrc.js b/plugins/ilert/.eslintrc.js new file mode 100644 index 0000000000..4d51616e98 --- /dev/null +++ b/plugins/ilert/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], + rules: { + quotes: ['error', 'single'], + }, +}; diff --git a/plugins/ilert/README.md b/plugins/ilert/README.md new file mode 100644 index 0000000000..55e41ba5bd --- /dev/null +++ b/plugins/ilert/README.md @@ -0,0 +1,132 @@ +# @backstage/plugin-ilert + +## Introduction + +[iLert](https://www.ilert.com) is a platform for alerting, on-call management and uptime monitoring. It helps teams to reduce response times to critical incidents by extending monitoring tools with reliable alerting, automatic escalations, on-call schedules and other features to support the incident response process, such as [informing stakeholders](https://docs.ilert.com/getting-started/stakeholder-engagement) or creating tickets in external incident management tools. + +## Overview + +This plugin displays iLert information about an entity such as if there are any active incidents, wo is on-call now and uptime monitor status. + +There is also an easy way to trigger an incident directly to the person who is currently on-call. + +This plugin provides: + +- Information details about the persons on-call +- A way to override current person on-call +- A list of incidents +- A way to trigger a new incident +- A way to reassign/acknowledge/resolve an incident +- A list of uptime monitors + +## Setup instructions + +Install the plugin: + +```bash +yarn add @backstage/plugin-ilert +``` + +Then make sure to export the plugin in your app's [`plugins.ts`](https://github.com/backstage/backstage/blob/master/packages/app/src/plugins.ts) +to enable the plugin: + +```js +export { plugin as ILert } from '@backstage/plugin-ilert'; +``` + +Add it to the `EntityPage.tsx`: + +```ts +import { + isPluginApplicableToEntity as isILertAvailable, + EntityILertCard, +} from '@backstage/plugin-ilert'; +// add to code +{ + isILertAvailable(entity) && ( + + + + ); +} +``` + +> To force iLert card for each entity just add the `` component, so an instruction card will appears if no integration key is set. + +## Add iLert explorer to the App sidebar + +Modify your app routes in [`App.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/App.tsx) to include the Router component exported from the plugin, for example: + +```tsx +// At the top imports +import { ILertPage } from '@backstage/plugin-ilert'; + +// Inside App component + + // ... + } /> + // ... +; +``` + +Modify your sidebar in [`Root.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/components/Root/Root.tsx) to include the icon component exported from the plugin, for example: + +```tsx +// At the top imports +import { ILertIcon } from '@backstage/plugin-ilert'; + +// Inside Sidebar component + + // ... + + // ... +; +``` + +## Client configuration + +If you want to override the default URL for api calls and detail pages, you can add it to `app-config.yaml`. + +In `app-config.yaml`: + +```yaml +ilert: + domain: https://my-org.ilert.com/ +``` + +## Providing the Authorization Header + +In order to make the API calls, you need to provide a new proxy config which will redirect to the [iLert API](https://api.ilert.com/api-docs/) endpoint it needs an [Authorization Header](https://api.ilert.com/api-docs/#section/Authentication). + +Add the proxy configuration in `app-config.yaml` + +```yaml +proxy: + ... + '/ilert': + target: https://api.ilert.com + allowedMethods: ['GET', 'POST', 'PUT'] + allowedHeaders: ['Authorization'] + headers: + Authorization: + $env: ILERT_AUTH_HEADER +``` + +Then start the backend passing the token as an environment variable: + +```bash +$ ILERT_AUTH_HEADER='Basic ' yarn start +``` + +## Integration Key + +The information displayed for each entity is based on the [alert source integration key](https://docs.ilert.com/integrations/backstage). + +### Adding the integration key to the entity annotation + +If you want to use this plugin for an entity, you need to label it with the below annotation: + +```yml +annotations: + ilert.com/integration-key: [INTEGRATION_KEY] +``` diff --git a/plugins/ilert/dev/index.tsx b/plugins/ilert/dev/index.tsx new file mode 100644 index 0000000000..c05e1da3d9 --- /dev/null +++ b/plugins/ilert/dev/index.tsx @@ -0,0 +1,27 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { createDevApp } from '@backstage/dev-utils'; +import { ilertPlugin } from '../src/plugin'; +import { IlertPage } from '../src'; + +createDevApp() + .registerPlugin(ilertPlugin) + .addPage({ + element: , + title: 'Root Page', + }) + .render(); diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json new file mode 100644 index 0000000000..b31af34753 --- /dev/null +++ b/plugins/ilert/package.json @@ -0,0 +1,54 @@ +{ + "name": "@backstage/plugin-ilert", + "version": "0.1.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/catalog-model": "^0.7.6", + "@backstage/core": "^0.7.4", + "@backstage/plugin-catalog-react": "^0.1.4", + "@backstage/theme": "^0.2.5", + "@date-io/date-fns": "1.x", + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "@material-ui/pickers": "^3.3.10", + "date-fns": "^2.20.2", + "moment": "^2.29.1", + "moment-timezone": "^0.5.33", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-use": "^15.3.3" + }, + "devDependencies": { + "@backstage/cli": "^0.6.7", + "@backstage/dev-utils": "^0.1.13", + "@backstage/test-utils": "^0.1.10", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32", + "cross-fetch": "^3.0.6", + "msw": "^0.21.2" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/ilert/src/api/client.ts b/plugins/ilert/src/api/client.ts new file mode 100644 index 0000000000..259502eff0 --- /dev/null +++ b/plugins/ilert/src/api/client.ts @@ -0,0 +1,500 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { ConfigApi, createApiRef, DiscoveryApi } from '@backstage/core'; +import { + AlertSource, + EscalationPolicy, + Incident, + IncidentResponder, + Schedule, + UptimeMonitor, + User, +} from '../types'; +import { + ILertApi, + GetIncidentsOpts, + GetIncidentsCountOpts, + Options, + EventRequest, +} from './types'; +import moment from 'moment'; +import momentTimezone from 'moment-timezone'; + +export const ilertApiRef = createApiRef({ + id: 'plugin.ilert.service', + description: 'Used to make requests towards iLert API', +}); + +const DEFAULT_PROXY_PATH = '/ilert'; +export class UnauthorizedError extends Error {} + +export class ILertClient implements ILertApi { + private readonly discoveryApi: DiscoveryApi; + private readonly proxyPath: string; + private readonly domain: string; + + static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) { + const domainUrl: string = + configApi.getOptionalString('domainUrl') ?? 'https://api.ilert.com'; + + return new ILertClient({ + discoveryApi: discoveryApi, + domain: domainUrl, + proxyPath: configApi.getOptionalString('ilert.proxyPath'), + }); + } + + constructor(opts: Options) { + this.discoveryApi = opts.discoveryApi; + this.domain = opts.domain; + this.proxyPath = opts.proxyPath ?? DEFAULT_PROXY_PATH; + } + + private async fetch(input: string, init?: RequestInit): Promise { + const apiUrl = await this.apiUrl(); + + const response = await fetch(`${apiUrl}${input}`, init); + if (response.status === 401) { + throw new UnauthorizedError(''); + } + if (!response.ok) { + throw new Error( + `Request failed with ${response.status} ${response.statusText}`, + ); + } + + return await response.json(); + } + + async fetchIncidents(opts?: GetIncidentsOpts): Promise { + const init = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + const query = new URLSearchParams(); + if (opts && opts.maxResults) { + query.append('max-results', `${opts.maxResults}`); + } + if (opts && opts.startIndex) { + query.append('start-index', `${opts.startIndex}`); + } + if (opts && opts.alertSources) { + opts.alertSources.forEach((a: string | number) => { + if (a) { + query.append('alert-source', `${a}`); + } + }); + } + + if (opts && opts.states && Array.isArray(opts.states)) { + opts.states.forEach(state => { + query.append('state', state); + }); + } + const response = await this.fetch( + `/api/v1/incidents?${query.toString()}`, + init, + ); + + return response; + } + + async fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise { + const init = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + const query = new URLSearchParams(); + if (opts && opts.states && Array.isArray(opts.states)) { + opts.states.forEach(state => { + query.append('state', state); + }); + } + const response = await this.fetch( + `/api/v1/incidents/count?${query.toString()}`, + init, + ); + + return response && response.count ? response.count : 0; + } + + async fetchIncidentResponders( + incident: Incident, + ): Promise { + const init = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + const response = await this.fetch( + `/api/v1/incidents/${incident.id}/responders`, + init, + ); + + return response; + } + + async acceptIncident(incident: Incident): Promise { + const init = { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ summary: 'Backstage — iLert plugin' }), + }; + + const response = await this.fetch( + `/api/v1/incidents/${incident.id}/accept`, + init, + ); + + return response; + } + + async resolveIncident(incident: Incident): Promise { + const init = { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ summary: 'Backstage — iLert plugin' }), + }; + + const response = await this.fetch( + `/api/v1/incidents/${incident.id}/resolve`, + init, + ); + + return response; + } + + async assignIncident( + incident: Incident, + responder: IncidentResponder, + ): Promise { + const init = { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + const query = new URLSearchParams(); + switch (responder.group) { + case 'ESCALATION_POLICY': + query.append('policy-id', `${responder.id}`); + break; + case 'ON_CALL_SCHEDULE': + query.append('schedule-id', `${responder.id}`); + break; + default: + query.append('user-id', `${responder.id}`); + break; + } + + const response = await this.fetch( + `/api/v1/incidents/${incident.id}/assign?${query.toString()}`, + init, + ); + + return response; + } + + async createIncident(eventRequest: EventRequest): Promise { + const init = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ + apiKey: eventRequest.integrationKey, + summary: eventRequest.summary, + details: eventRequest.details, + eventType: 'ALERT', + links: [ + { + href: eventRequest.source, + text: 'Backstage Url', + }, + ], + customDetails: { + userName: eventRequest.userName, + }, + }), + }; + + const response = await this.fetch('/api/v1/events', init); + return response.responseCode === 'NEW_INCIDENT_CREATED'; + } + + async fetchUptimeMonitors(): Promise { + const init = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + const response = await this.fetch('/api/v1/uptime-monitors', init); + + return response; + } + + async fetchUptimeMonitor(id: number): Promise { + const init = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + const response: UptimeMonitor = await this.fetch( + `/api/v1/uptime-monitors/${id}`, + init, + ); + + return response; + } + + async pauseUptimeMonitor( + uptimeMonitor: UptimeMonitor, + ): Promise { + const init = { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ ...uptimeMonitor, paused: true }), + }; + + const response = await this.fetch( + `/api/v1/uptime-monitors/${uptimeMonitor.id}`, + init, + ); + + return response; + } + + async resumeUptimeMonitor( + uptimeMonitor: UptimeMonitor, + ): Promise { + const init = { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ ...uptimeMonitor, paused: false }), + }; + + const response = await this.fetch( + `/api/v1/uptime-monitors/${uptimeMonitor.id}`, + init, + ); + + return response; + } + + async fetchAlertSources(): Promise { + const init = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + const response = await this.fetch('/api/v1/alert-sources', init); + + return response; + } + + async fetchAlertSource( + idOrIntegrationKey: number | string, + ): Promise { + const init = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + const response = await this.fetch( + `/api/v1/alert-sources/${idOrIntegrationKey}`, + init, + ); + + return response; + } + + async enableAlertSource(alertSource: AlertSource): Promise { + const init = { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ ...alertSource, active: true }), + }; + + const response = await this.fetch( + `/api/v1/alert-sources/${alertSource.id}`, + init, + ); + + return response; + } + + async disableAlertSource(alertSource: AlertSource): Promise { + const init = { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ ...alertSource, active: false }), + }; + + const response = await this.fetch( + `/api/v1/alert-sources/${alertSource.id}`, + init, + ); + + return response; + } + + async addImmediateMaintenance( + alertSourceId: number, + minutes: number, + ): Promise { + const init = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ + start: moment().utc().toISOString(), + end: moment().add(minutes, 'minutes').utc().toISOString(), + description: `Immediate maintenance window for ${minutes} minutes. Backstage — iLert plugin.`, + createdBy: 'Backstage', + timezone: momentTimezone.tz.guess(), + alertSources: [{ id: alertSourceId }], + }), + }; + + const response = await this.fetch('/api/v1/maintenance-windows', init); + + return response; + } + + async fetchOnCallSchedules(): Promise { + const init = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + const response = await this.fetch('/api/v1/schedules', init); + + return response; + } + + async fetchUsers(): Promise { + const init = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + const response = await this.fetch('/api/v1/users', init); + + return response; + } + + async overrideShift( + scheduleId: number, + userId: number, + start: string, + end: string, + ): Promise { + const init = { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ user: { id: userId }, start, end }), + }; + + const response = await this.fetch( + `/api/v1/schedules/${scheduleId}/overrides`, + init, + ); + + return response; + } + + getIncidentDetailsURL(incident: Incident): string { + return `${this.domain}/incident/view.jsf?id=${incident.id}`; + } + + getAlertSourceDetailsURL(alertSource: AlertSource | null): string { + if (!alertSource) { + return ''; + } + return `${this.domain}/source/view.jsf?id=${alertSource.id}`; + } + + getEscalationPolicyDetailsURL(escalationPolicy: EscalationPolicy): string { + return `${this.domain}/policy/view.jsf?id=${escalationPolicy.id}`; + } + + getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string { + return `${this.domain}/uptime/view.jsf?id=${uptimeMonitor.id}`; + } + + getScheduleDetailsURL(schedule: Schedule): string { + return `${this.domain}/schedule/view.jsf?id=${schedule.id}`; + } + + getUserInitials(assignedTo: User | null) { + if (!assignedTo) { + return ''; + } + if (!assignedTo.firstName && !assignedTo.lastName) { + return assignedTo.username; + } + return `${assignedTo.firstName} ${assignedTo.lastName} (${assignedTo.username})`; + } + + private async apiUrl() { + const proxyUrl = await this.discoveryApi.getBaseUrl('proxy'); + return proxyUrl + this.proxyPath; + } +} diff --git a/plugins/ilert/src/api/index.ts b/plugins/ilert/src/api/index.ts new file mode 100644 index 0000000000..128856ebdf --- /dev/null +++ b/plugins/ilert/src/api/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { ILertClient, ilertApiRef, UnauthorizedError } from './client'; +export type { + ILertApi, + GetIncidentsCountOpts, + GetIncidentsOpts, + TableState, +} from './types'; diff --git a/plugins/ilert/src/api/types.ts b/plugins/ilert/src/api/types.ts new file mode 100644 index 0000000000..4c33455d86 --- /dev/null +++ b/plugins/ilert/src/api/types.ts @@ -0,0 +1,110 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { DiscoveryApi } from '@backstage/core'; +import { + AlertSource, + Incident, + User, + IncidentStatus, + UptimeMonitor, + EscalationPolicy, + Schedule, + IncidentResponder, +} from '../types'; + +export type TableState = { + page: number; + pageSize: number; +}; + +export type GetIncidentsOpts = { + maxResults?: number; + startIndex?: number; + states?: IncidentStatus[]; + alertSources?: number[] | string[]; +}; + +export type GetIncidentsCountOpts = { + states?: IncidentStatus[]; +}; + +export type EventRequest = { + integrationKey: string; + summary: string; + details: string; + userName: string; + source: string; +}; + +export interface ILertApi { + fetchIncidents(opts?: GetIncidentsOpts): Promise; + fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise; + fetchIncidentResponders(incident: Incident): Promise; + acceptIncident(incident: Incident): Promise; + resolveIncident(incident: Incident): Promise; + assignIncident( + incident: Incident, + responder: IncidentResponder, + ): Promise; + createIncident(eventRequest: EventRequest): Promise; + + fetchUptimeMonitors(): Promise; + pauseUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise; + resumeUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise; + fetchUptimeMonitor(id: number): Promise; + + fetchAlertSources(): Promise; + fetchAlertSource(idOrIntegrationKey: number | string): Promise; + enableAlertSource(alertSource: AlertSource): Promise; + disableAlertSource(alertSource: AlertSource): Promise; + + addImmediateMaintenance( + alertSourceId: number, + minutes: number, + ): Promise; + + fetchOnCallSchedules(): Promise; + fetchUsers(): Promise; + + overrideShift( + scheduleId: number, + userId: number, + start: string, + end: string, + ): Promise; + + getIncidentDetailsURL(incident: Incident): string; + getAlertSourceDetailsURL(alertSource: AlertSource | null): string; + getEscalationPolicyDetailsURL(escalationPolicy: EscalationPolicy): string; + getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string; + getScheduleDetailsURL(schedule: Schedule): string; + getUserInitials(assignedTo: User | null): string; +} + +export type Options = { + discoveryApi: DiscoveryApi; + + /** + * Domain used by users to access iLert web UI. + * Example: https://my-org.ilert.com/ + */ + domain: string; + + /** + * Path to use for requests via the proxy, defaults to /ilert/api + */ + proxyPath?: string; +}; diff --git a/plugins/ilert/src/assets/ilert.icon.svg b/plugins/ilert/src/assets/ilert.icon.svg new file mode 100644 index 0000000000..e93f1e80ca --- /dev/null +++ b/plugins/ilert/src/assets/ilert.icon.svg @@ -0,0 +1,11 @@ + + + + diff --git a/plugins/ilert/src/components/AlertSource/AlertSourceLink.tsx b/plugins/ilert/src/components/AlertSource/AlertSourceLink.tsx new file mode 100644 index 0000000000..d94a9b0039 --- /dev/null +++ b/plugins/ilert/src/components/AlertSource/AlertSourceLink.tsx @@ -0,0 +1,71 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { useApi } from '@backstage/core'; +import Link from '@material-ui/core/Link'; +import Grid from '@material-ui/core/Grid'; +import { AlertSource } from '../../types'; +import { ilertApiRef } from '../../api'; +import { makeStyles } from '@material-ui/core/styles'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; + +const useStyles = makeStyles({ + root: { + display: 'flex', + maxWidth: '100%', + }, + image: { + height: 22, + paddingRight: 4, + }, + link: { + lineHeight: '22px', + }, +}); + +export const AlertSourceLink = ({ + alertSource, +}: { + alertSource: AlertSource | null; +}) => { + const ilertApi = useApi(ilertApiRef); + const classes = useStyles(); + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); + + if (!alertSource) { + return null; + } + + return ( + + + {alertSource.name} + + + + {alertSource.name} + + + + ); +}; diff --git a/plugins/ilert/src/components/Errors/MissingAuthorizationHeaderError.tsx b/plugins/ilert/src/components/Errors/MissingAuthorizationHeaderError.tsx new file mode 100644 index 0000000000..6b3d463d2a --- /dev/null +++ b/plugins/ilert/src/components/Errors/MissingAuthorizationHeaderError.tsx @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { EmptyState } from '@backstage/core'; +import { Button } from '@material-ui/core'; + +export const MissingAuthorizationHeaderError = () => ( + + Read More + + } + /> +); diff --git a/plugins/ilert/src/components/Errors/index.ts b/plugins/ilert/src/components/Errors/index.ts new file mode 100644 index 0000000000..f20de48b90 --- /dev/null +++ b/plugins/ilert/src/components/Errors/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { MissingAuthorizationHeaderError } from './MissingAuthorizationHeaderError'; diff --git a/plugins/ilert/src/components/EscalationPolicy/EscalationPolicyLink.tsx b/plugins/ilert/src/components/EscalationPolicy/EscalationPolicyLink.tsx new file mode 100644 index 0000000000..7f1e3524ff --- /dev/null +++ b/plugins/ilert/src/components/EscalationPolicy/EscalationPolicyLink.tsx @@ -0,0 +1,49 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { useApi } from '@backstage/core'; +import Link from '@material-ui/core/Link'; +import { makeStyles } from '@material-ui/core/styles'; +import { EscalationPolicy } from '../../types'; +import { ilertApiRef } from '../../api'; + +const useStyles = makeStyles({ + link: { + lineHeight: '22px', + }, +}); + +export const EscalationPolicyLink = ({ + escalationPolicy, +}: { + escalationPolicy: EscalationPolicy | null; +}) => { + const ilertApi = useApi(ilertApiRef); + const classes = useStyles(); + + if (!escalationPolicy) { + return null; + } + + return ( + + {escalationPolicy.name} + + ); +}; diff --git a/plugins/ilert/src/components/ILertCard/ILertCard.tsx b/plugins/ilert/src/components/ILertCard/ILertCard.tsx new file mode 100644 index 0000000000..b376b319b5 --- /dev/null +++ b/plugins/ilert/src/components/ILertCard/ILertCard.tsx @@ -0,0 +1,145 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { Entity } from '@backstage/catalog-model'; +import { UnauthorizedError } from '../../api'; +import Alert from '@material-ui/lab/Alert'; +import Card from '@material-ui/core/Card'; +import CardContent from '@material-ui/core/CardContent'; +import CardHeader from '@material-ui/core/CardHeader'; +import Divider from '@material-ui/core/Divider'; +import { makeStyles } from '@material-ui/core/styles'; +import { ILERT_INTEGRATION_KEY } from '../../constants'; +import { MissingAuthorizationHeaderError } from '../Errors'; +import { useIncidents } from '../../hooks/useIncidents'; +import { IncidentsTable } from '../IncidentsPage'; +import { IncidentNewModal } from '../Incident/IncidentNewModal'; +import { ILertCardActionsHeader } from './ILertCardActionsHeader'; +import { useAlertSource } from '../../hooks/useAlertSource'; +import { useILertEntity } from '../../hooks'; +import { ILertCardHeaderStatus } from './ILertCardHeaderStatus'; +import { ILertCardMaintenanceModal } from './ILertCardMaintenanceModal'; +import { ILertCardEmptyState } from './ILertCardEmptyState'; + +export const isPluginApplicableToEntity = (entity: Entity) => + Boolean(entity.metadata.annotations?.[ILERT_INTEGRATION_KEY]); + +const useStyles = makeStyles({ + content: { + paddingLeft: '0 !important', + paddingRight: '0 !important', + paddingBottom: '0 !important', + paddingTop: '0 !important', + '& div div': { + boxShadow: 'none !important', + }, + }, +}); + +export const ILertCard = () => { + const classes = useStyles(); + const { integrationKey, name } = useILertEntity(); + const [ + { alertSource, uptimeMonitor }, + { setAlertSource, refetchAlertSource }, + ] = useAlertSource(integrationKey); + const alertSourcesFilter = alertSource ? [alertSource.id] : [integrationKey]; + const [ + { tableState, states, incidents, incidentsCount, isLoading, error }, + { + onIncidentStatesChange, + onChangePage, + onChangeRowsPerPage, + onIncidentChanged, + refetchIncidents, + setIsLoading, + }, + ] = useIncidents(false, alertSourcesFilter); + + const [ + isNewIncidentModalOpened, + setIsNewIncidentModalOpened, + ] = React.useState(false); + const [ + isMaintenanceModalOpened, + setIsMaintenanceModalOpened, + ] = React.useState(false); + + if (error) { + if (error instanceof UnauthorizedError) { + return ; + } + + return ( + + {error.message} + + ); + } + + if (!integrationKey) { + return ; + } + + return ( + <> + + + } + action={} + /> + + + + + + + + + ); +}; diff --git a/plugins/ilert/src/components/ILertCard/ILertCardActionsHeader.tsx b/plugins/ilert/src/components/ILertCard/ILertCardActionsHeader.tsx new file mode 100644 index 0000000000..c19c31839a --- /dev/null +++ b/plugins/ilert/src/components/ILertCard/ILertCardActionsHeader.tsx @@ -0,0 +1,203 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { + HeaderIconLinkRow, + IconLinkVerticalProps, + useApi, + alertApiRef, +} from '@backstage/core'; +import Alert from '@material-ui/lab/Alert'; +import Button from '@material-ui/core/Button'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import AlarmAddIcon from '@material-ui/icons/AlarmAdd'; +import BuildIcon from '@material-ui/icons/Build'; +import PauseIcon from '@material-ui/icons/Pause'; +import PlayArrowIcon from '@material-ui/icons/PlayArrow'; +import TimelineIcon from '@material-ui/icons/Timeline'; +import WebIcon from '@material-ui/icons/Web'; +import Typography from '@material-ui/core/Typography'; +import { ilertApiRef } from '../../api'; +import { AlertSource, UptimeMonitor } from '../../types'; + +export const ILertCardActionsHeader = ({ + alertSource, + setAlertSource, + setIsNewIncidentModalOpened, + setIsMaintenanceModalOpened, + uptimeMonitor, +}: { + alertSource: AlertSource | null; + setAlertSource: (alertSource: AlertSource) => void; + setIsNewIncidentModalOpened: (isOpen: boolean) => void; + setIsMaintenanceModalOpened: (isOpen: boolean) => void; + uptimeMonitor: UptimeMonitor | null; +}) => { + const ilertApi = useApi(ilertApiRef); + const alertApi = useApi(alertApiRef); + const [isLoading, setIsLoading] = React.useState(false); + const [isDisableModalOpened, setIsDisableModalOpened] = React.useState(false); + + const handleCreateNewIncident = () => { + setIsNewIncidentModalOpened(true); + }; + + const handleEnableAlertSource = async () => { + try { + if (!alertSource) { + return; + } + setIsLoading(true); + const newAlertSource = await ilertApi.enableAlertSource(alertSource); + alertApi.post({ message: 'Alert source enabled.' }); + setIsLoading(false); + setAlertSource(newAlertSource); + } catch (err) { + setIsLoading(false); + alertApi.post({ message: err, severity: 'error' }); + } + }; + const handleDisableAlertSource = async () => { + try { + if (!alertSource) { + return; + } + setIsDisableModalOpened(false); + setIsLoading(true); + const newAlertSource = await ilertApi.disableAlertSource(alertSource); + alertApi.post({ message: 'Alert source disabled.' }); + setIsLoading(false); + setAlertSource(newAlertSource); + } catch (err) { + setIsLoading(false); + alertApi.post({ message: err, severity: 'error' }); + } + }; + + const handleDisableAlertSourceWarningOpen = () => { + setIsDisableModalOpened(true); + }; + + const handleDisableAlertSourceWarningClose = () => { + setIsDisableModalOpened(false); + }; + + const handleMaintenanceAlertSource = () => { + setIsMaintenanceModalOpened(true); + }; + + const alertSourceLink: IconLinkVerticalProps = { + label: 'Alert Source', + href: ilertApi.getAlertSourceDetailsURL(alertSource), + icon: , + }; + + const createIncidentLink: IconLinkVerticalProps = { + label: 'Create Incident', + onClick: handleCreateNewIncident, + icon: , + color: 'secondary', + disabled: + !alertSource || + alertSource.status === 'DISABLED' || + alertSource.status === 'IN_MAINTENANCE', + }; + + const enableAlertSourceLink: IconLinkVerticalProps = { + label: 'Enable', + onClick: handleEnableAlertSource, + icon: , + disabled: !alertSource || isLoading, + }; + + const disableAlertSourceLink: IconLinkVerticalProps = { + label: 'Disable', + onClick: handleDisableAlertSourceWarningOpen, + icon: , + disabled: !alertSource || isLoading, + }; + + const maintenanceAlertSourceLink: IconLinkVerticalProps = { + label: 'Immediate maintenance', + onClick: handleMaintenanceAlertSource, + icon: , + disabled: !alertSource || isLoading, + }; + + const uptimeMonitorReportLink: IconLinkVerticalProps = { + label: 'Uptime Report', + href: uptimeMonitor ? uptimeMonitor.shareUrl : '', + icon: , + disabled: !alertSource || !uptimeMonitor || isLoading, + }; + + const links: IconLinkVerticalProps[] = [ + alertSourceLink, + createIncidentLink, + alertSource && alertSource.active + ? disableAlertSourceLink + : enableAlertSourceLink, + ]; + + if (alertSource && alertSource.integrationType === 'MONITOR') { + links.push(uptimeMonitorReportLink); + } + + if (alertSource && alertSource.status !== 'IN_MAINTENANCE') { + links.push(maintenanceAlertSourceLink); + } + + return ( + <> + + + + Disable alert source + + + + + Do you really want to disable this alert source? A disabled alert + source cannot create new incidents. + + + + + + + + + + ); +}; diff --git a/plugins/ilert/src/components/ILertCard/ILertCardEmptyState.tsx b/plugins/ilert/src/components/ILertCard/ILertCardEmptyState.tsx new file mode 100644 index 0000000000..42dc140696 --- /dev/null +++ b/plugins/ilert/src/components/ILertCard/ILertCardEmptyState.tsx @@ -0,0 +1,86 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { CodeSnippet } from '@backstage/core'; +import { BackstageTheme } from '@backstage/theme'; +import Card from '@material-ui/core/Card'; +import CardContent from '@material-ui/core/CardContent'; +import CardHeader from '@material-ui/core/CardHeader'; +import Button from '@material-ui/core/Button'; +import Divider from '@material-ui/core/Divider'; +import Typography from '@material-ui/core/Typography'; +import { makeStyles } from '@material-ui/core/styles'; +import React from 'react'; + +const ENTITY_YAML = `apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: example + description: example.com + annotations: + ilert.com/integration-key: [INTEGRATION_KEY] +spec: + type: website + lifecycle: production + owner: guest`; + +const useStyles = makeStyles(theme => ({ + code: { + borderRadius: 6, + margin: `${theme.spacing(2)}px 0px`, + background: theme.palette.type === 'dark' ? '#444' : '#fff', + }, + header: { + display: 'inline-block', + padding: `${theme.spacing(2)}px ${theme.spacing(2)}px ${theme.spacing( + 2, + )}px ${theme.spacing(2.5)}px`, + }, +})); + +export const ILertCardEmptyState = () => { + const classes = useStyles(); + + return ( + + + + + + No integration key defined for this entity. You can add integration + key to your entity YAML as shown in the highlighted example below: + +
+ +
+ +
+
+ ); +}; diff --git a/plugins/ilert/src/components/ILertCard/ILertCardHeaderStatus.tsx b/plugins/ilert/src/components/ILertCard/ILertCardHeaderStatus.tsx new file mode 100644 index 0000000000..2c1b44551e --- /dev/null +++ b/plugins/ilert/src/components/ILertCard/ILertCardHeaderStatus.tsx @@ -0,0 +1,46 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import Chip from '@material-ui/core/Chip'; +import { withStyles } from '@material-ui/core/styles'; +import { AlertSource } from '../../types'; + +const MaintenanceChip = withStyles({ + root: { + backgroundColor: '#92949c', + color: 'white', + marginTop: 8, + }, +})(Chip); + +export const ILertCardHeaderStatus = ({ + alertSource, +}: { + alertSource: AlertSource | null; +}) => { + if (!alertSource) { + return null; + } + + switch (alertSource.status) { + case 'IN_MAINTENANCE': + return ; + case 'DISABLED': + return ; + default: + return null; + } +}; diff --git a/plugins/ilert/src/components/ILertCard/ILertCardMaintenanceModal.tsx b/plugins/ilert/src/components/ILertCard/ILertCardMaintenanceModal.tsx new file mode 100644 index 0000000000..54d465a83c --- /dev/null +++ b/plugins/ilert/src/components/ILertCard/ILertCardMaintenanceModal.tsx @@ -0,0 +1,138 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { alertApiRef, useApi } from '@backstage/core'; +import Button from '@material-ui/core/Button'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import MenuItem from '@material-ui/core/MenuItem'; +import TextField from '@material-ui/core/TextField'; +import { ilertApiRef } from '../../api'; +import { AlertSource } from '../../types'; + +export const ILertCardMaintenanceModal = ({ + alertSource, + refetchAlertSource, + isModalOpened, + setIsModalOpened, +}: { + alertSource: AlertSource | null; + refetchAlertSource: () => void; + isModalOpened: boolean; + setIsModalOpened: (isModalOpened: boolean) => void; +}) => { + const ilertApi = useApi(ilertApiRef); + const alertApi = useApi(alertApiRef); + const [minutes, setMinutes] = React.useState(5); + + const handleClose = () => { + setIsModalOpened(false); + }; + + const handleImmediateMaintenance = () => { + if (!alertSource) { + return; + } + setIsModalOpened(false); + setTimeout(async () => { + try { + await ilertApi.addImmediateMaintenance(alertSource.id, minutes); + alertApi.post({ message: 'Maintenance started.' }); + refetchAlertSource(); + } catch (err) { + alertApi.post({ message: err, severity: 'error' }); + } + }, 250); + }; + + const handleMinutesChange = (event: any) => { + setMinutes(event.target.value); + }; + + const minuteOptions = [ + { + value: 5, + label: '5 minutes', + }, + { + value: 10, + label: '10 minutes', + }, + { + value: 15, + label: '15 minutes', + }, + { + value: 30, + label: '30 minutes', + }, + { + value: 60, + label: '60 minutes', + }, + ]; + + if (!alertSource) { + return null; + } + + return ( + + + New maintenance window + + + + Keep your alert sources quiet, when your systems are under + maintenance. + + + {minuteOptions.map(option => ( + + {option.label} + + ))} + + + + + + + + ); +}; diff --git a/plugins/ilert/src/components/ILertCard/index.ts b/plugins/ilert/src/components/ILertCard/index.ts new file mode 100644 index 0000000000..4ae259bfc5 --- /dev/null +++ b/plugins/ilert/src/components/ILertCard/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 * from './ILertCard'; diff --git a/plugins/ilert/src/components/ILertPage/ILertPage.tsx b/plugins/ilert/src/components/ILertPage/ILertPage.tsx new file mode 100644 index 0000000000..40934bfdfb --- /dev/null +++ b/plugins/ilert/src/components/ILertPage/ILertPage.tsx @@ -0,0 +1,68 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { + Page, + Header, + HeaderTabs, + HeaderLabel, + Content, +} from '@backstage/core'; +import { IncidentsPage } from '../IncidentsPage'; +import { UptimeMonitorsPage } from '../UptimeMonitorsPage'; +import { OnCallSchedulesPage } from '../OnCallSchedulesPage'; + +export const ILertPage = () => { + const [selectedTab, setSelectedTab] = React.useState(0); + const tabs = [ + { label: 'Who is on call?' }, + { label: 'Incidents' }, + { label: 'Uptime Monitors' }, + ]; + const renderTab = () => { + switch (selectedTab) { + case 0: + return ; + case 1: + return ; + case 2: + return ; + default: + return null; + } + }; + + return ( + +
+ + +
+ setSelectedTab(index)} + tabs={tabs.map(({ label }, index) => ({ + id: index.toString(), + label, + }))} + /> + + {renderTab()} +
+ ); +}; + +export default ILertPage; diff --git a/plugins/ilert/src/components/ILertPage/index.ts b/plugins/ilert/src/components/ILertPage/index.ts new file mode 100644 index 0000000000..5de46b63d4 --- /dev/null +++ b/plugins/ilert/src/components/ILertPage/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 * from './ILertPage'; diff --git a/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx b/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx new file mode 100644 index 0000000000..576f037c8c --- /dev/null +++ b/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx @@ -0,0 +1,148 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { alertApiRef, useApi } from '@backstage/core'; +import { IconButton, Menu, MenuItem, Typography } from '@material-ui/core'; +import Link from '@material-ui/core/Link'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; +import { ilertApiRef } from '../../api'; +import { Incident } from '../../types'; +import { IncidentAssignModal } from './IncidentAssignModal'; + +export const IncidentActionsMenu = ({ + incident, + onIncidentChanged, + setIsLoading, +}: { + incident: Incident; + onIncidentChanged?: (incident: Incident) => void; + setIsLoading?: (isLoading: boolean) => void; +}) => { + const ilertApi = useApi(ilertApiRef); + const alertApi = useApi(alertApiRef); + const [anchorEl, setAnchorEl] = React.useState(null); + const callback = onIncidentChanged || ((_: Incident): void => {}); + const setProcessing = setIsLoading || ((_: boolean): void => {}); + const [ + isAssignIncidentModalOpened, + setIsAssignIncidentModalOpened, + ] = React.useState(false); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleCloseMenu = () => { + setAnchorEl(null); + }; + + const handleAccept = async (): Promise => { + try { + handleCloseMenu(); + setProcessing(true); + const newIncident = await ilertApi.acceptIncident(incident); + alertApi.post({ message: 'Incident accepted.' }); + + callback(newIncident); + setProcessing(false); + } catch (err) { + setProcessing(false); + alertApi.post({ message: err, severity: 'error' }); + } + }; + + const handleResolve = async (): Promise => { + try { + handleCloseMenu(); + setProcessing(true); + const newIncident = await ilertApi.resolveIncident(incident); + alertApi.post({ message: 'Incident resolved.' }); + + callback(newIncident); + setProcessing(false); + } catch (err) { + setProcessing(false); + alertApi.post({ message: err, severity: 'error' }); + } + }; + + const handleAssign = () => { + handleCloseMenu(); + setIsAssignIncidentModalOpened(true); + }; + + return ( + <> + + + + + {incident.status === 'PENDING' ? ( + + + Accept + + + ) : null} + + {incident.status !== 'RESOLVED' ? ( + + + Resolve + + + ) : null} + + {incident.status !== 'RESOLVED' ? ( + + + Assign + + + ) : null} + + + + + View in iLert + + + + + + + ); +}; diff --git a/plugins/ilert/src/components/Incident/IncidentAssignModal.tsx b/plugins/ilert/src/components/Incident/IncidentAssignModal.tsx new file mode 100644 index 0000000000..edac5e4e0e --- /dev/null +++ b/plugins/ilert/src/components/Incident/IncidentAssignModal.tsx @@ -0,0 +1,183 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { alertApiRef, useApi } from '@backstage/core'; +import { makeStyles } from '@material-ui/core/styles'; +import Alert from '@material-ui/lab/Alert'; +import Button from '@material-ui/core/Button'; +import TextField from '@material-ui/core/TextField'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import { useAssignIncident } from '../../hooks/useAssignIncident'; +import { Typography } from '@material-ui/core'; +import { ilertApiRef } from '../../api'; +import { Incident } from '../../types'; + +const useStyles = makeStyles(() => ({ + container: { + display: 'flex', + flexWrap: 'wrap', + }, + formControl: { + minWidth: 120, + width: '100%', + }, + option: { + fontSize: 15, + '& > span': { + marginRight: 10, + fontSize: 18, + }, + }, + optionWrapper: { + display: 'flex', + width: '100%', + }, + sourceImage: { + height: 22, + paddingRight: 4, + }, +})); + +export const IncidentAssignModal = ({ + incident, + isModalOpened, + setIsModalOpened, + onIncidentChanged, +}: { + incident: Incident | null; + isModalOpened: boolean; + setIsModalOpened: (open: boolean) => void; + onIncidentChanged?: (incident: Incident) => void; +}) => { + const [ + { incidentRespondersList, incidentResponder, isLoading }, + { setIsLoading, setIncidentResponder, setIncidentRespondersList }, + ] = useAssignIncident(incident, isModalOpened); + const callback = onIncidentChanged || ((_: Incident): void => {}); + const ilertApi = useApi(ilertApiRef); + const alertApi = useApi(alertApiRef); + const classes = useStyles(); + + const handleClose = () => { + setIncidentRespondersList([]); + setIsModalOpened(false); + }; + + const handleAssign = () => { + if (!incident || !incidentResponder) { + return; + } + setIsLoading(true); + setIncidentRespondersList([]); + setTimeout(async () => { + try { + const newIncident = await ilertApi.assignIncident( + incident, + incidentResponder, + ); + callback(newIncident); + alertApi.post({ message: 'Incident assigned.' }); + } catch (err) { + alertApi.post({ message: err, severity: 'error' }); + } + setIsLoading(false); + setIsModalOpened(false); + }, 250); + }; + + const canAssign = !!incidentResponder; + + return ( + + + Select responder to assign + + + + + This action will assign the incident to the selected responder. + + + { + setIncidentResponder(newValue); + }} + autoHighlight + groupBy={option => { + switch (option.group) { + case 'SUGGESTED': + return 'Suggested responders'; + case 'USER': + return 'Users'; + case 'ESCALATION_POLICY': + return 'Escalation policies'; + case 'ON_CALL_SCHEDULE': + return 'Schedules'; + default: + return ''; + } + }} + getOptionLabel={a => a.name} + renderOption={a => ( +
+ {a.name} +
+ )} + renderInput={params => ( + + )} + /> +
+ + + + +
+ ); +}; diff --git a/plugins/ilert/src/components/Incident/IncidentLink.tsx b/plugins/ilert/src/components/Incident/IncidentLink.tsx new file mode 100644 index 0000000000..87818f363a --- /dev/null +++ b/plugins/ilert/src/components/Incident/IncidentLink.tsx @@ -0,0 +1,45 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { useApi } from '@backstage/core'; +import Link from '@material-ui/core/Link'; +import { makeStyles } from '@material-ui/core/styles'; +import { Incident } from '../../types'; +import { ilertApiRef } from '../../api'; + +const useStyles = makeStyles({ + link: { + lineHeight: '22px', + }, +}); + +export const IncidentLink = ({ incident }: { incident: Incident | null }) => { + const ilertApi = useApi(ilertApiRef); + const classes = useStyles(); + + if (!incident) { + return null; + } + + return ( + + #{incident.id} + + ); +}; diff --git a/plugins/ilert/src/components/Incident/IncidentNewModal.tsx b/plugins/ilert/src/components/Incident/IncidentNewModal.tsx new file mode 100644 index 0000000000..3fae62bf0c --- /dev/null +++ b/plugins/ilert/src/components/Incident/IncidentNewModal.tsx @@ -0,0 +1,230 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { alertApiRef, identityApiRef, useApi } from '@backstage/core'; +import { makeStyles } from '@material-ui/core/styles'; +import Alert from '@material-ui/lab/Alert'; +import Button from '@material-ui/core/Button'; +import TextField from '@material-ui/core/TextField'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import { useNewIncident } from '../../hooks/useNewIncident'; +import { Typography } from '@material-ui/core'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; +import { ilertApiRef } from '../../api'; +import { AlertSource } from '../../types'; + +const useStyles = makeStyles(() => ({ + container: { + display: 'flex', + flexWrap: 'wrap', + }, + formControl: { + minWidth: 120, + width: '100%', + }, + option: { + fontSize: 15, + '& > span': { + marginRight: 10, + fontSize: 18, + }, + }, + optionWrapper: { + display: 'flex', + width: '100%', + }, + sourceImage: { + height: 22, + paddingRight: 4, + }, +})); + +export const IncidentNewModal = ({ + isModalOpened, + setIsModalOpened, + refetchIncidents, + initialAlertSource, + entityName, +}: { + isModalOpened: boolean; + setIsModalOpened: (open: boolean) => void; + refetchIncidents: () => void; + initialAlertSource?: AlertSource | null; + entityName?: string; +}) => { + const [ + { alertSources, alertSource, summary, details, isLoading }, + { setAlertSource, setSummary, setDetails, setIsLoading }, + ] = useNewIncident(isModalOpened, initialAlertSource); + const ilertApi = useApi(ilertApiRef); + const alertApi = useApi(alertApiRef); + const identityApi = useApi(identityApiRef); + const userName = identityApi.getUserId(); + const source = window.location.toString(); + const classes = useStyles(); + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); + + const handleClose = () => { + setIsModalOpened(false); + }; + + let integrationKey = ''; + if (initialAlertSource && initialAlertSource.integrationKey) { + integrationKey = initialAlertSource.integrationKey; + } else if (alertSource && alertSource.integrationKey) { + integrationKey = alertSource.integrationKey; + } + const handleCreate = () => { + if (!integrationKey) { + return; + } + setIsLoading(true); + setTimeout(async () => { + try { + const success = await ilertApi.createIncident({ + integrationKey, + summary, + details, + userName, + source, + }); + if (success) { + alertApi.post({ message: 'Incident created.' }); + refetchIncidents(); + } + } catch (err) { + alertApi.post({ message: err, severity: 'error' }); + } + setIsModalOpened(false); + }, 250); + }; + + const canCreate = !!integrationKey && !!summary; + + return ( + + + {entityName ? ( +
+ This action will trigger an incident for{' '} + "{entityName}". +
+ ) : ( + 'New incident' + )} +
+ + + + Please describe the problem you want to report. Be as descriptive as + possible. Your signed in user and a reference to the current page + will automatically be amended to the alarm so that the receiver can + reach out to you if necessary. + + + {!initialAlertSource ? ( + { + setAlertSource(newValue); + }} + autoHighlight + getOptionLabel={a => a.name} + renderOption={a => ( +
+ {a.name} + {a.name} +
+ )} + renderInput={params => ( + + )} + /> + ) : null} + { + setSummary(event.target.value); + }} + /> + { + setDetails(event.target.value); + }} + /> +
+ + + + +
+ ); +}; diff --git a/plugins/ilert/src/components/Incident/IncidentStatus.tsx b/plugins/ilert/src/components/Incident/IncidentStatus.tsx new file mode 100644 index 0000000000..931a472ec6 --- /dev/null +++ b/plugins/ilert/src/components/Incident/IncidentStatus.tsx @@ -0,0 +1,48 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { StatusError, StatusOK } from '@backstage/core'; +import { makeStyles } from '@material-ui/core/styles'; +import Tooltip from '@material-ui/core/Tooltip'; +import { ACCEPTED, Incident, PENDING, RESOLVED } from '../../types'; + +const useStyles = makeStyles({ + denseListIcon: { + marginRight: 0, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + }, +}); + +export const incidentStatusLabels = { + [RESOLVED]: 'Resolved', + [ACCEPTED]: 'Accepted', + [PENDING]: 'Pending', +} as Record; + +export const IncidentStatus = ({ incident }: { incident: Incident }) => { + const classes = useStyles(); + + return ( + +
+ {incident.status === 'PENDING' ? : } +
+
+ ); +}; diff --git a/plugins/ilert/src/components/Incident/index.ts b/plugins/ilert/src/components/Incident/index.ts new file mode 100644 index 0000000000..df8bab5d95 --- /dev/null +++ b/plugins/ilert/src/components/Incident/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 * from './IncidentActionsMenu'; +export * from './IncidentStatus'; diff --git a/plugins/ilert/src/components/IncidentsPage/IncidentsPage.tsx b/plugins/ilert/src/components/IncidentsPage/IncidentsPage.tsx new file mode 100644 index 0000000000..e552ce7d26 --- /dev/null +++ b/plugins/ilert/src/components/IncidentsPage/IncidentsPage.tsx @@ -0,0 +1,93 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { Content, ContentHeader, SupportButton } from '@backstage/core'; +import Button from '@material-ui/core/Button'; +import AddIcon from '@material-ui/icons/Add'; +import { UnauthorizedError } from '../../api'; +import Alert from '@material-ui/lab/Alert'; +import { IncidentsTable } from './IncidentsTable'; +import { MissingAuthorizationHeaderError } from '../Errors'; +import { useIncidents } from '../../hooks/useIncidents'; +import { IncidentNewModal } from '../Incident/IncidentNewModal'; + +export const IncidentsPage = () => { + const [ + { tableState, states, incidents, incidentsCount, isLoading, error }, + { + onIncidentStatesChange, + onChangePage, + onChangeRowsPerPage, + onIncidentChanged, + refetchIncidents, + setIsLoading, + }, + ] = useIncidents(true); + + const [isModalOpened, setIsModalOpened] = React.useState(false); + + const handleCreateNewIncidentClick = () => { + setIsModalOpened(true); + }; + + if (error) { + if (error instanceof UnauthorizedError) { + return ; + } + + return ( + + {error.message} + + ); + } + + return ( + + + + + + This helps you to bring iLert into your developer portal. + + + + + ); +}; diff --git a/plugins/ilert/src/components/IncidentsPage/IncidentsTable.tsx b/plugins/ilert/src/components/IncidentsPage/IncidentsTable.tsx new file mode 100644 index 0000000000..b7ee298062 --- /dev/null +++ b/plugins/ilert/src/components/IncidentsPage/IncidentsTable.tsx @@ -0,0 +1,253 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { Table, TableColumn, useApi } from '@backstage/core'; +import { makeStyles } from '@material-ui/core/styles'; +import { ilertApiRef, TableState } from '../../api'; +import { Incident, IncidentStatus } from '../../types'; +import { StatusChip } from './StatusChip'; +import { AlertSourceLink } from '../AlertSource/AlertSourceLink'; +import { TableTitle } from './TableTitle'; +import Typography from '@material-ui/core/Typography'; +import moment from 'moment'; +import { IncidentActionsMenu } from '../Incident/IncidentActionsMenu'; +import { IncidentLink } from '../Incident/IncidentLink'; + +const useStyles = makeStyles(theme => ({ + empty: { + padding: theme.spacing(2), + display: 'flex', + justifyContent: 'center', + }, +})); + +export const IncidentsTable = ({ + incidents, + incidentsCount, + tableState, + states, + isLoading, + onIncidentChanged, + setIsLoading, + onIncidentStatesChange, + onChangePage, + onChangeRowsPerPage, + compact, +}: { + incidents: Incident[]; + incidentsCount: number; + tableState: TableState; + states: IncidentStatus[]; + isLoading: boolean; + onIncidentChanged: (incident: Incident) => void; + setIsLoading: (isLoading: boolean) => void; + onIncidentStatesChange: (states: IncidentStatus[]) => void; + onChangePage: (page: number) => void; + onChangeRowsPerPage: (pageSize: number) => void; + compact?: boolean; +}) => { + const ilertApi = useApi(ilertApiRef); + const classes = useStyles(); + + const xsColumnStyle = { + width: '5%', + maxWidth: '5%', + }; + const smColumnStyle = { + width: '10%', + maxWidth: '10%', + }; + const mdColumnStyle = { + width: '15%', + maxWidth: '15%', + }; + const lgColumnStyle = { + width: '20%', + maxWidth: '20%', + }; + const xlColumnStyle = { + width: '30%', + maxWidth: '30%', + }; + + const idColumn: TableColumn = { + title: 'ID', + field: 'id', + highlight: true, + cellStyle: smColumnStyle, + headerStyle: smColumnStyle, + render: rowData => , + }; + const summaryColumn: TableColumn = { + title: 'Summary', + field: 'summary', + cellStyle: !compact ? xlColumnStyle : undefined, + headerStyle: !compact ? xlColumnStyle : undefined, + render: rowData => {(rowData as Incident).summary}, + }; + const sourceColumn: TableColumn = { + title: 'Source', + field: 'source', + cellStyle: mdColumnStyle, + headerStyle: mdColumnStyle, + render: rowData => ( + + ), + }; + const durationColumn: TableColumn = { + title: 'Duration', + field: 'reportTime', + type: 'datetime', + cellStyle: smColumnStyle, + headerStyle: smColumnStyle, + render: rowData => ( + + {(rowData as Incident).status !== 'RESOLVED' + ? moment + .duration(moment((rowData as Incident).reportTime).diff(moment())) + .humanize() + : moment + .duration( + moment((rowData as Incident).reportTime).diff( + moment((rowData as Incident).resolvedOn), + ), + ) + .humanize()} + + ), + }; + const assignedToColumn: TableColumn = { + title: 'Assigned to', + field: 'assignedTo', + cellStyle: !compact ? mdColumnStyle : lgColumnStyle, + headerStyle: !compact ? mdColumnStyle : lgColumnStyle, + render: rowData => ( + + {ilertApi.getUserInitials((rowData as Incident).assignedTo)} + + ), + }; + const priorityColumn: TableColumn = { + title: 'Priority', + field: 'priority', + cellStyle: smColumnStyle, + headerStyle: smColumnStyle, + render: rowData => ( + + {(rowData as Incident).priority === 'HIGH' ? 'High' : 'Low'} + + ), + }; + const statusColumn: TableColumn = { + title: 'Status', + field: 'status', + cellStyle: xsColumnStyle, + headerStyle: xsColumnStyle, + render: rowData => , + }; + const actionsColumn: TableColumn = { + title: '', + field: '', + cellStyle: xsColumnStyle, + headerStyle: xsColumnStyle, + render: rowData => ( + + ), + }; + + const columns: TableColumn[] = compact + ? [ + summaryColumn, + durationColumn, + assignedToColumn, + statusColumn, + actionsColumn, + ] + : [ + idColumn, + summaryColumn, + sourceColumn, + durationColumn, + assignedToColumn, + priorityColumn, + statusColumn, + actionsColumn, + ]; + let tableStyle: React.CSSProperties = {}; + if (compact) { + tableStyle = { + width: '100%', + maxWidth: '100%', + minWidth: '0', + height: 'calc(100% - 10px)', + boxShadow: 'none !important', + borderRadius: 'none !important', + }; + } else { + tableStyle = { + width: '100%', + maxWidth: '100%', + }; + } + + return ( + + No incidents right now + + } + title={ + !compact ? ( + + ) : ( + + INCIDENTS + + ) + } + page={tableState.page} + totalCount={incidentsCount} + onChangePage={onChangePage} + onChangeRowsPerPage={onChangeRowsPerPage} + // localization={{ header: { actions: undefined } }} + columns={columns} + data={incidents} + isLoading={isLoading} + /> + ); +}; diff --git a/plugins/ilert/src/components/IncidentsPage/StatusChip.tsx b/plugins/ilert/src/components/IncidentsPage/StatusChip.tsx new file mode 100644 index 0000000000..9713b16a55 --- /dev/null +++ b/plugins/ilert/src/components/IncidentsPage/StatusChip.tsx @@ -0,0 +1,57 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { Chip, withStyles } from '@material-ui/core'; +import { Incident, PENDING, ACCEPTED, RESOLVED } from '../../types'; +import { incidentStatusLabels } from '../Incident/IncidentStatus'; + +const ResolvedChip = withStyles({ + root: { + backgroundColor: '#4caf50', + color: 'white', + margin: 0, + }, +})(Chip); + +const AcceptedChip = withStyles({ + root: { + backgroundColor: '#ffb74d', + color: 'white', + margin: 0, + }, +})(Chip); +const PendingChip = withStyles({ + root: { + backgroundColor: '#d32f2f', + color: 'white', + margin: 0, + }, +})(Chip); + +export const StatusChip = ({ incident }: { incident: Incident }) => { + const label = `${incidentStatusLabels[incident.status]}`; + + switch (incident.status) { + case RESOLVED: + return ; + case ACCEPTED: + return ; + case PENDING: + return ; + default: + return ; + } +}; diff --git a/plugins/ilert/src/components/IncidentsPage/TableTitle.tsx b/plugins/ilert/src/components/IncidentsPage/TableTitle.tsx new file mode 100644 index 0000000000..f35981dec2 --- /dev/null +++ b/plugins/ilert/src/components/IncidentsPage/TableTitle.tsx @@ -0,0 +1,97 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { PENDING, ACCEPTED, RESOLVED, IncidentStatus } from '../../types'; +import { incidentStatusLabels } from '../Incident/IncidentStatus'; +import FormControl from '@material-ui/core/FormControl'; +import ListItemText from '@material-ui/core/ListItemText'; +import Select from '@material-ui/core/Select'; +import Typography from '@material-ui/core/Typography'; +import MenuItem from '@material-ui/core/MenuItem'; +import Checkbox from '@material-ui/core/Checkbox'; +import { makeStyles } from '@material-ui/core/styles'; + +const ITEM_HEIGHT = 48; +const ITEM_PADDING_TOP = 8; +const MenuProps = { + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + width: 250, + }, + }, +}; + +const useStyles = makeStyles({ + root: { + display: 'flex', + }, + label: { + marginTop: 8, + marginRight: 4, + }, + formControl: { + minWidth: 120, + maxWidth: 300, + }, + grow: { + flexGrow: 1, + }, +}); + +export const TableTitle = ({ + incidentStates, + onIncidentStatesChange, +}: { + incidentStates: IncidentStatus[]; + onIncidentStatesChange: (states: IncidentStatus[]) => void; +}) => { + const classes = useStyles(); + const handleIncidentStatusSelectChange = (event: any) => { + onIncidentStatesChange(event.target.value); + }; + + return ( +
+ + Status: + + + + +
+ ); +}; diff --git a/plugins/ilert/src/components/IncidentsPage/index.ts b/plugins/ilert/src/components/IncidentsPage/index.ts new file mode 100644 index 0000000000..7159247c68 --- /dev/null +++ b/plugins/ilert/src/components/IncidentsPage/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 * from './IncidentsPage'; +export * from './IncidentsTable'; diff --git a/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx new file mode 100644 index 0000000000..34601009ff --- /dev/null +++ b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx @@ -0,0 +1,150 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { useApi, ItemCardGrid, Progress } from '@backstage/core'; +import { makeStyles } from '@material-ui/core/styles'; +import Card from '@material-ui/core/Card'; +import CardContent from '@material-ui/core/CardContent'; +import CardHeader from '@material-ui/core/CardHeader'; +import Typography from '@material-ui/core/Typography'; +import Link from '@material-ui/core/Link'; +import { Schedule } from '../../types'; +import { ilertApiRef } from '../../api'; +import { OnCallShiftItem } from './OnCallShiftItem'; + +const useStyles = makeStyles(() => ({ + card: { + margin: 16, + width: 'calc(100% - 32px)', + }, + + cardHeader: { + maxWidth: '100%', + }, + + cardContent: { + marginLeft: 80, + borderLeft: '1px #808289 solid', + position: 'relative', + }, + + indicatorNext: { + position: 'absolute', + top: 'calc(40% - 10px)', + left: -8, + width: 16, + height: 16, + background: '#92949c !important', + borderRadius: '50%', + }, + + indicatorCurrent: { + position: 'absolute', + top: 'calc(40% - 10px)', + left: -8, + width: 16, + height: 16, + background: '#ffb74d !important', + borderRadius: '50%', + }, + + beforeText: { + position: 'absolute', + top: 'calc(31% - 10px)', + left: -78, + width: 65, + height: 20, + textAlign: 'center', + color: '#808289', + }, + + marginBottom: { + marginBottom: 16, + }, + + link: { + fontSize: '1.5rem', + fontWeight: 700, + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + display: 'block', + }, +})); + +export const OnCallSchedulesGrid = ({ + onCallSchedules, + isLoading, + refetchOnCallSchedules, +}: { + onCallSchedules: Schedule[]; + isLoading: boolean; + refetchOnCallSchedules: () => void; +}) => { + const ilertApi = useApi(ilertApiRef); + const classes = useStyles(); + + if (isLoading) { + return ; + } + return ( + + {!onCallSchedules?.length + ? null + : onCallSchedules.map((schedule, index) => ( + + + {schedule.name} + + } + /> + + +
+ + + On call now + + + + +
+ + + Next on call + + + + ))} + + ); +}; diff --git a/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesPage.tsx b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesPage.tsx new file mode 100644 index 0000000000..09c2f2fc63 --- /dev/null +++ b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesPage.tsx @@ -0,0 +1,56 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { Content, ContentHeader, SupportButton } from '@backstage/core'; +import { UnauthorizedError } from '../../api'; +import Alert from '@material-ui/lab/Alert'; +import { OnCallSchedulesGrid } from './OnCallSchedulesGrid'; +import { MissingAuthorizationHeaderError } from '../Errors'; +import { useOnCallSchedules } from '../../hooks/useOnCallSchedules'; + +export const OnCallSchedulesPage = () => { + const [ + { onCallSchedules, isLoading, error }, + { refetchOnCallSchedules }, + ] = useOnCallSchedules(); + + if (error) { + if (error instanceof UnauthorizedError) { + return ; + } + + return ( + + {error.message} + + ); + } + + return ( + + + + This helps you to bring iLert into your developer portal. + + + + + ); +}; diff --git a/plugins/ilert/src/components/OnCallSchedulesPage/OnCallShiftItem.tsx b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallShiftItem.tsx new file mode 100644 index 0000000000..c636609781 --- /dev/null +++ b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallShiftItem.tsx @@ -0,0 +1,105 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import Button from '@material-ui/core/Button'; +import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; +import RepeatIcon from '@material-ui/icons/Repeat'; +import { Shift } from '../../types'; +import moment from 'moment'; +import { makeStyles } from '@material-ui/core/styles'; +import { ShiftOverrideModal } from '../Shift/ShiftOverrideModal'; + +const useStyles = makeStyles({ + button: { + marginTop: 4, + padding: 0, + lineHeight: 1.8, + '& span': { + lineHeight: 1.8, + fontSize: '0.65rem', + }, + '& svg': { + fontSize: '0.85rem !important', + }, + }, +}); + +export const OnCallShiftItem = ({ + scheduleId, + shift, + refetchOnCallSchedules, +}: { + scheduleId: number; + shift: Shift; + refetchOnCallSchedules: () => void; +}) => { + const classes = useStyles(); + const [isModalOpened, setIsModalOpened] = React.useState(false); + + const handleOverride = () => { + setIsModalOpened(true); + }; + + if (!shift || !shift.start) { + return ( + + + + Nobody + + + + ); + } + + return ( + + {shift && shift.user ? ( + + + {`${shift.user.firstName} ${shift.user.lastName} (${shift.user.username})`} + + + ) : null} + + + {`${moment(shift.start).format('D MMM, HH:mm')} - ${moment( + shift.end, + ).format('D MMM, HH:mm')}`} + + + + + + + + ); +}; diff --git a/plugins/ilert/src/components/OnCallSchedulesPage/index.ts b/plugins/ilert/src/components/OnCallSchedulesPage/index.ts new file mode 100644 index 0000000000..a52accc2bb --- /dev/null +++ b/plugins/ilert/src/components/OnCallSchedulesPage/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 * from './OnCallSchedulesPage'; +export * from './OnCallSchedulesGrid'; diff --git a/plugins/ilert/src/components/Shift/ShiftOverrideModal.tsx b/plugins/ilert/src/components/Shift/ShiftOverrideModal.tsx new file mode 100644 index 0000000000..1fdd05cb4c --- /dev/null +++ b/plugins/ilert/src/components/Shift/ShiftOverrideModal.tsx @@ -0,0 +1,196 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { alertApiRef, useApi } from '@backstage/core'; +import { makeStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import TextField from '@material-ui/core/TextField'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import { Typography } from '@material-ui/core'; +import { ilertApiRef } from '../../api'; +import { useShiftOverride } from '../../hooks/useShiftOverride'; +import { Shift } from '../../types'; +import { DateTimePicker, MuiPickersUtilsProvider } from '@material-ui/pickers'; +import DateFnsUtils from '@date-io/date-fns'; + +const useStyles = makeStyles(() => ({ + container: { + display: 'flex', + flexWrap: 'wrap', + }, + formControl: { + minWidth: 120, + width: '100%', + }, + option: { + fontSize: 15, + '& > span': { + marginRight: 10, + fontSize: 18, + }, + }, + optionWrapper: { + display: 'flex', + width: '100%', + }, + sourceImage: { + height: 22, + paddingRight: 4, + }, + grow: { + flexGrow: 1, + }, +})); + +export const ShiftOverrideModal = ({ + scheduleId, + shift, + refetchOnCallSchedules, + isModalOpened, + setIsModalOpened, +}: { + scheduleId: number; + shift: Shift; + refetchOnCallSchedules: () => void; + isModalOpened: boolean; + setIsModalOpened: (isModalOpened: boolean) => void; +}) => { + const [ + { isLoading, users, user, start, end }, + { setUser, setStart, setEnd, setIsLoading }, + ] = useShiftOverride(shift, isModalOpened); + const ilertApi = useApi(ilertApiRef); + const alertApi = useApi(alertApiRef); + const classes = useStyles(); + + const handleClose = () => { + setIsModalOpened(false); + }; + + const handleOverride = () => { + if (!shift || !shift.user) { + return; + } + setIsLoading(true); + setTimeout(async () => { + try { + const success = await ilertApi.overrideShift( + scheduleId, + user.id, + start, + end, + ); + if (success) { + alertApi.post({ message: 'Shift overridden.' }); + refetchOnCallSchedules(); + } + } catch (err) { + alertApi.post({ message: err, severity: 'error' }); + } + setIsModalOpened(false); + }, 250); + }; + + if (!shift) { + return null; + } + + return ( + + Shift override + + + { + setUser(newValue); + }} + autoHighlight + getOptionLabel={a => ilertApi.getUserInitials(a)} + renderOption={a => ( +
+ {ilertApi.getUserInitials(a)} +
+ )} + renderInput={params => ( + + )} + /> + { + setStart(date ? date.toISOString() : ''); + }} + /> + { + setEnd(date ? date.toISOString() : ''); + }} + /> +
+
+ + + + +
+ ); +}; diff --git a/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorActionsMenu.tsx b/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorActionsMenu.tsx new file mode 100644 index 0000000000..efd69a4acf --- /dev/null +++ b/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorActionsMenu.tsx @@ -0,0 +1,134 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { alertApiRef, useApi } from '@backstage/core'; +import { IconButton, Menu, MenuItem, Typography } from '@material-ui/core'; +import Link from '@material-ui/core/Link'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; + +import { ilertApiRef } from '../../api'; +import { UptimeMonitor } from '../../types'; + +export const UptimeMonitorActionsMenu = ({ + uptimeMonitor, + onUptimeMonitorChanged, +}: { + uptimeMonitor: UptimeMonitor; + onUptimeMonitorChanged?: (uptimeMonitor: UptimeMonitor) => void; +}) => { + const ilertApi = useApi(ilertApiRef); + const alertApi = useApi(alertApiRef); + const [anchorEl, setAnchorEl] = React.useState(null); + const callback = onUptimeMonitorChanged || ((_: UptimeMonitor): void => {}); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleCloseMenu = () => { + setAnchorEl(null); + }; + + const handlePause = async (): Promise => { + try { + const newUptimeMonitor = await ilertApi.pauseUptimeMonitor(uptimeMonitor); + handleCloseMenu(); + alertApi.post({ message: 'Uptime monitor paused.' }); + + callback(newUptimeMonitor); + } catch (err) { + alertApi.post({ message: err, severity: 'error' }); + } + }; + + const handleResume = async (): Promise => { + try { + const newUptimeMonitor = await ilertApi.resumeUptimeMonitor( + uptimeMonitor, + ); + handleCloseMenu(); + alertApi.post({ message: 'Uptime monitor resumed.' }); + + callback(newUptimeMonitor); + } catch (err) { + alertApi.post({ message: err, severity: 'error' }); + } + }; + + const handleOpenReport = async (): Promise => { + try { + const um = await ilertApi.fetchUptimeMonitor(uptimeMonitor.id); + handleCloseMenu(); + window.open(um.shareUrl, '_blank'); + } catch (err) { + alertApi.post({ message: err, severity: 'error' }); + } + }; + + return ( + <> + + + + + {uptimeMonitor.paused ? ( + + + Resume + + + ) : null} + + {!uptimeMonitor.paused ? ( + + + Pause + + + ) : null} + + + + View Report + + + + + + + View in iLert + + + + + + ); +}; diff --git a/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorLink.tsx b/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorLink.tsx new file mode 100644 index 0000000000..0af5cd89df --- /dev/null +++ b/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorLink.tsx @@ -0,0 +1,49 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { useApi } from '@backstage/core'; +import Link from '@material-ui/core/Link'; +import { makeStyles } from '@material-ui/core/styles'; +import { UptimeMonitor } from '../../types'; +import { ilertApiRef } from '../../api'; + +const useStyles = makeStyles({ + link: { + lineHeight: '22px', + }, +}); + +export const UptimeMonitorLink = ({ + uptimeMonitor, +}: { + uptimeMonitor: UptimeMonitor | null; +}) => { + const ilertApi = useApi(ilertApiRef); + const classes = useStyles(); + + if (!uptimeMonitor) { + return null; + } + + return ( + + #{uptimeMonitor.id} + + ); +}; diff --git a/plugins/ilert/src/components/UptimeMonitor/index.ts b/plugins/ilert/src/components/UptimeMonitor/index.ts new file mode 100644 index 0000000000..f30d1ef15a --- /dev/null +++ b/plugins/ilert/src/components/UptimeMonitor/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 * from './UptimeMonitorActionsMenu'; diff --git a/plugins/ilert/src/components/UptimeMonitorsPage/StatusChip.tsx b/plugins/ilert/src/components/UptimeMonitorsPage/StatusChip.tsx new file mode 100644 index 0000000000..dd17b1758b --- /dev/null +++ b/plugins/ilert/src/components/UptimeMonitorsPage/StatusChip.tsx @@ -0,0 +1,73 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import Chip from '@material-ui/core/Chip'; +import { withStyles } from '@material-ui/core/styles'; +import { UptimeMonitor } from '../../types'; + +const UpChip = withStyles({ + root: { + backgroundColor: '#4caf50', + color: 'white', + margin: 0, + }, +})(Chip); + +const DownChip = withStyles({ + root: { + backgroundColor: '#d32f2f', + color: 'white', + margin: 0, + }, +})(Chip); + +const UnknownChip = withStyles({ + root: { + backgroundColor: '#92949c', + color: 'white', + margin: 0, + }, +})(Chip); + +export const uptimeMonitorStatusLabels = { + ['up']: 'Up', + ['down']: 'Down', + ['unknown']: 'Unknown', +} as Record; + +export const StatusChip = ({ + uptimeMonitor, +}: { + uptimeMonitor: UptimeMonitor; +}) => { + let label = `${uptimeMonitorStatusLabels[uptimeMonitor.status]}`; + + if (uptimeMonitor.paused) { + label = 'Paused'; + return ; + } + + switch (uptimeMonitor.status) { + case 'up': + return ; + case 'down': + return ; + case 'unknown': + return ; + default: + return ; + } +}; diff --git a/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorCheckType.tsx b/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorCheckType.tsx new file mode 100644 index 0000000000..3fdecd65e9 --- /dev/null +++ b/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorCheckType.tsx @@ -0,0 +1,39 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { UptimeMonitor } from '../../types'; +import Typography from '@material-ui/core/Typography'; + +export const UptimeMonitorCheckType = ({ + uptimeMonitor, +}: { + uptimeMonitor: UptimeMonitor; +}) => { + switch (uptimeMonitor.region) { + case 'EU': + return ( + {`${uptimeMonitor.checkType.toUpperCase()} 🇩🇪`} + ); + default: + return ( + {`${uptimeMonitor.checkType.toUpperCase()} 🇺🇸`} + ); + } +}; diff --git a/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsPage.tsx b/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsPage.tsx new file mode 100644 index 0000000000..dc7ea77a6c --- /dev/null +++ b/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsPage.tsx @@ -0,0 +1,59 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { Content, ContentHeader, SupportButton } from '@backstage/core'; +import { UnauthorizedError } from '../../api'; +import Alert from '@material-ui/lab/Alert'; +import { UptimeMonitorsTable } from './UptimeMonitorsTable'; +import { MissingAuthorizationHeaderError } from '../Errors'; +import { useUptimeMonitors } from '../../hooks/useUptimeMonitors'; + +export const UptimeMonitorsPage = () => { + const [ + { tableState, uptimeMonitors, isLoading, error }, + { onChangePage, onChangeRowsPerPage, onUptimeMonitorChanged }, + ] = useUptimeMonitors(); + + if (error) { + if (error instanceof UnauthorizedError) { + return ; + } + + return ( + + {error.message} + + ); + } + + return ( + + + + This helps you to bring iLert into your developer portal. + + + + + ); +}; diff --git a/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsTable.tsx b/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsTable.tsx new file mode 100644 index 0000000000..bd55310ad7 --- /dev/null +++ b/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsTable.tsx @@ -0,0 +1,173 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { Table, TableColumn } from '@backstage/core'; +import { TableState } from '../../api'; +import { UptimeMonitor } from '../../types'; +import { StatusChip } from './StatusChip'; +import Typography from '@material-ui/core/Typography'; +import moment from 'moment'; +import { EscalationPolicyLink } from '../EscalationPolicy/EscalationPolicyLink'; +import { UptimeMonitorCheckType } from './UptimeMonitorCheckType'; +import { UptimeMonitorActionsMenu } from '../UptimeMonitor/UptimeMonitorActionsMenu'; +import { UptimeMonitorLink } from '../UptimeMonitor/UptimeMonitorLink'; + +const useStyles = makeStyles(theme => ({ + empty: { + padding: theme.spacing(2), + display: 'flex', + justifyContent: 'center', + }, +})); + +export const UptimeMonitorsTable = ({ + uptimeMonitors, + tableState, + isLoading, + onChangePage, + onChangeRowsPerPage, + onUptimeMonitorChanged, +}: { + uptimeMonitors: UptimeMonitor[]; + tableState: TableState; + isLoading: boolean; + onChangePage: (page: number) => void; + onChangeRowsPerPage: (pageSize: number) => void; + onUptimeMonitorChanged: (uptimeMonitor: UptimeMonitor) => void; +}) => { + const classes = useStyles(); + + const smColumnStyle = { + width: '5%', + maxWidth: '5%', + }; + const mdColumnStyle = { + width: '10%', + maxWidth: '10%', + }; + const lgColumnStyle = { + width: '15%', + maxWidth: '15%', + }; + + const columns: TableColumn[] = [ + { + title: 'ID', + field: 'id', + highlight: true, + cellStyle: mdColumnStyle, + headerStyle: mdColumnStyle, + render: rowData => ( + + ), + }, + { + title: 'Name', + field: 'name', + render: rowData => ( + {(rowData as UptimeMonitor).name} + ), + }, + { + title: 'Check Type', + field: 'checkType', + cellStyle: lgColumnStyle, + headerStyle: lgColumnStyle, + render: rowData => ( + + ), + }, + { + title: 'Last state change', + field: 'lastStatusChange', + type: 'datetime', + cellStyle: mdColumnStyle, + headerStyle: mdColumnStyle, + render: rowData => ( + + {moment + .duration( + moment((rowData as UptimeMonitor).lastStatusChange).diff( + moment(), + ), + ) + .humanize()} + + ), + }, + { + title: 'Escalation policy', + field: 'assignedTo', + cellStyle: lgColumnStyle, + headerStyle: lgColumnStyle, + render: rowData => ( + + ), + }, + { + title: 'Status', + field: 'status', + cellStyle: smColumnStyle, + headerStyle: smColumnStyle, + render: rowData => ( + + ), + }, + { + title: '', + field: '', + cellStyle: smColumnStyle, + headerStyle: smColumnStyle, + render: rowData => ( + + ), + }, + ]; + + return ( +
+ No uptime monitor + + } + page={tableState.page} + onChangePage={onChangePage} + onChangeRowsPerPage={onChangeRowsPerPage} + localization={{ header: { actions: undefined } }} + isLoading={isLoading} + columns={columns} + data={uptimeMonitors} + /> + ); +}; diff --git a/plugins/ilert/src/components/UptimeMonitorsPage/index.ts b/plugins/ilert/src/components/UptimeMonitorsPage/index.ts new file mode 100644 index 0000000000..d1a8809a4e --- /dev/null +++ b/plugins/ilert/src/components/UptimeMonitorsPage/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 * from './UptimeMonitorsPage'; +export * from './UptimeMonitorsTable'; diff --git a/plugins/ilert/src/components/index.ts b/plugins/ilert/src/components/index.ts new file mode 100644 index 0000000000..81f7e02e23 --- /dev/null +++ b/plugins/ilert/src/components/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 * from './ILertPage'; +export * from './ILertCard'; diff --git a/plugins/ilert/src/constants.ts b/plugins/ilert/src/constants.ts new file mode 100644 index 0000000000..edaa06b293 --- /dev/null +++ b/plugins/ilert/src/constants.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 const ILERT_INTEGRATION_KEY = 'ilert.com/integration-key'; diff --git a/plugins/ilert/src/hooks/index.ts b/plugins/ilert/src/hooks/index.ts new file mode 100644 index 0000000000..e21c9584d2 --- /dev/null +++ b/plugins/ilert/src/hooks/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { useEntity } from '@backstage/plugin-catalog-react'; + +import { ILERT_INTEGRATION_KEY } from '../constants'; + +export function useILertEntity() { + const { entity } = useEntity(); + const integrationKey = + entity.metadata.annotations?.[ILERT_INTEGRATION_KEY] || ''; + const name = entity.metadata.name; + + return { integrationKey, name }; +} diff --git a/plugins/ilert/src/hooks/useAlertSource.ts b/plugins/ilert/src/hooks/useAlertSource.ts new file mode 100644 index 0000000000..950f8f41c6 --- /dev/null +++ b/plugins/ilert/src/hooks/useAlertSource.ts @@ -0,0 +1,103 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ilertApiRef, UnauthorizedError } from '../api'; +import { useApi, errorApiRef } from '@backstage/core'; +import { useAsyncRetry } from 'react-use'; +import { AlertSource, UptimeMonitor } from '../types'; + +export const useAlertSource = (integrationKey: string) => { + const ilertApi = useApi(ilertApiRef); + const errorApi = useApi(errorApiRef); + + const [alertSource, setAlertSource] = React.useState( + null, + ); + const [isAlertSourceLoading, setIsAlertSourceLoading] = React.useState(false); + const [ + uptimeMonitor, + setUptimeMonitor, + ] = React.useState(null); + const [isUptimeMonitorLoading, setIsUptimeMonitorLoading] = React.useState( + false, + ); + + const fetchAlertSourceCall = async () => { + try { + if (!integrationKey) { + return; + } + setIsAlertSourceLoading(true); + const data = await ilertApi.fetchAlertSource(integrationKey); + setAlertSource(data || null); + setIsAlertSourceLoading(false); + } catch (e) { + setIsAlertSourceLoading(false); + if (!(e instanceof UnauthorizedError)) { + errorApi.post(e); + } + throw e; + } + }; + + const { + error: alertSourceError, + retry: alertSourceRetry, + } = useAsyncRetry(fetchAlertSourceCall, [integrationKey]); + + const fetchUptimeMonitorCall = async () => { + try { + if (!alertSource || alertSource.integrationType !== 'MONITOR') { + return; + } + setIsUptimeMonitorLoading(true); + const data = await ilertApi.fetchUptimeMonitor(alertSource.id); + setUptimeMonitor(data || null); + setIsUptimeMonitorLoading(false); + } catch (e) { + setIsUptimeMonitorLoading(false); + if (!(e instanceof UnauthorizedError)) { + errorApi.post(e); + } + throw e; + } + }; + + const { + error: uptimeMonitorError, + retry: uptimeMonitorRetry, + } = useAsyncRetry(fetchUptimeMonitorCall, [alertSource]); + + const retry = () => { + alertSourceRetry(); + uptimeMonitorRetry(); + }; + + return [ + { + alertSource, + uptimeMonitor, + error: alertSourceError || uptimeMonitorError, + isLoading: isAlertSourceLoading || isUptimeMonitorLoading, + }, + { + retry, + setIsLoading: setIsAlertSourceLoading, + refetchAlertSource: fetchAlertSourceCall, + setAlertSource, + }, + ] as const; +}; diff --git a/plugins/ilert/src/hooks/useAssignIncident.ts b/plugins/ilert/src/hooks/useAssignIncident.ts new file mode 100644 index 0000000000..1671acdc51 --- /dev/null +++ b/plugins/ilert/src/hooks/useAssignIncident.ts @@ -0,0 +1,73 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ilertApiRef, UnauthorizedError } from '../api'; +import { useApi, errorApiRef } from '@backstage/core'; +import { useAsyncRetry } from 'react-use'; +import { Incident, IncidentResponder } from '../types'; + +export const useAssignIncident = (incident: Incident | null, open: boolean) => { + const ilertApi = useApi(ilertApiRef); + const errorApi = useApi(errorApiRef); + + const [incidentRespondersList, setIncidentRespondersList] = React.useState< + IncidentResponder[] + >([]); + const [ + incidentResponder, + setIncidentResponder, + ] = React.useState(null); + const [isLoading, setIsLoading] = React.useState(false); + + const { error, retry } = useAsyncRetry(async () => { + try { + if (!incident || !open) { + return; + } + const data = await ilertApi.fetchIncidentResponders(incident); + if (data && Array.isArray(data)) { + const groups = [ + 'SUGGESTED', + 'USER', + 'ESCALATION_POLICY', + 'ON_CALL_SCHEDULE', + ]; + data.sort((a, b) => groups.indexOf(a.group) - groups.indexOf(b.group)); + setIncidentRespondersList(data); + } + } catch (e) { + if (!(e instanceof UnauthorizedError)) { + errorApi.post(e); + } + throw e; + } + }, [incident, open]); + + return [ + { + incidentRespondersList, + incidentResponder, + error, + isLoading, + }, + { + setIncidentRespondersList, + setIncidentResponder, + setIsLoading, + retry, + }, + ] as const; +}; diff --git a/plugins/ilert/src/hooks/useIncidents.ts b/plugins/ilert/src/hooks/useIncidents.ts new file mode 100644 index 0000000000..a252dfb14f --- /dev/null +++ b/plugins/ilert/src/hooks/useIncidents.ts @@ -0,0 +1,151 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { + GetIncidentsOpts, + ilertApiRef, + TableState, + UnauthorizedError, +} from '../api'; +import { useApi, errorApiRef } from '@backstage/core'; +import { useAsyncRetry } from 'react-use'; +import { ACCEPTED, PENDING, Incident, IncidentStatus } from '../types'; + +export const useIncidents = ( + paging: boolean, + alertSources?: number[] | string[], +) => { + const ilertApi = useApi(ilertApiRef); + const errorApi = useApi(errorApiRef); + + const [tableState, setTableState] = React.useState({ + page: 0, + pageSize: 10, + }); + const [states, setStates] = React.useState([ + ACCEPTED, + PENDING, + ]); + const [incidentsList, setIncidentsList] = React.useState([]); + const [incidentsCount, setIncidentsCount] = React.useState(0); + const [isLoading, setIsLoading] = React.useState(false); + + const fetchIncidentsCall = async () => { + try { + setIsLoading(true); + const opts: GetIncidentsOpts = { + states, + alertSources, + }; + if (paging) { + opts.maxResults = tableState.pageSize; + opts.startIndex = tableState.page * tableState.pageSize; + } + const data = await ilertApi.fetchIncidents(opts); + setIncidentsList(data || []); + setIsLoading(false); + } catch (e) { + if (!(e instanceof UnauthorizedError)) { + errorApi.post(e); + } + setIsLoading(false); + throw e; + } + }; + + const fetchIncidentsCountCall = async () => { + try { + const count = await ilertApi.fetchIncidentsCount({ states }); + setIncidentsCount(count || 0); + } catch (e) { + if (!(e instanceof UnauthorizedError)) { + errorApi.post(e); + } + throw e; + } + }; + const fetchIncidents = useAsyncRetry(fetchIncidentsCall, [ + tableState, + states, + ]); + + const refetchIncidents = () => { + setTableState({ ...tableState, page: 0 }); + Promise.all([fetchIncidentsCall(), fetchIncidentsCountCall()]); + }; + + const fetchIncidentsCount = useAsyncRetry(fetchIncidentsCountCall, [states]); + + const error = fetchIncidents.error || fetchIncidentsCount.error; + const retry = () => { + fetchIncidents.retry(); + fetchIncidentsCount.retry(); + }; + + const onIncidentChanged = (newIncident: Incident) => { + let shouldRefetchIncidents = false; + setIncidentsList( + incidentsList.reduce((acc: Incident[], incident: Incident) => { + if (newIncident.id === incident.id) { + if (states.includes(newIncident.status)) { + acc.push(newIncident); + } else { + shouldRefetchIncidents = true; + } + return acc; + } + acc.push(incident); + return acc; + }, []), + ); + if (shouldRefetchIncidents) { + refetchIncidents(); + } + }; + + const onChangePage = (page: number) => { + setTableState({ ...tableState, page }); + }; + const onChangeRowsPerPage = (p: number) => { + setTableState({ ...tableState, pageSize: p }); + }; + const onIncidentStatesChange = (s: IncidentStatus[]) => { + setStates(s); + }; + + return [ + { + tableState, + states, + incidents: incidentsList, + incidentsCount, + error, + isLoading, + }, + { + setTableState, + setStates, + setIncidentsList, + setIsLoading, + retry, + onIncidentChanged, + refetchIncidents, + onChangePage, + onChangeRowsPerPage, + onIncidentStatesChange, + }, + ] as const; +}; diff --git a/plugins/ilert/src/hooks/useNewIncident.ts b/plugins/ilert/src/hooks/useNewIncident.ts new file mode 100644 index 0000000000..669fa74b63 --- /dev/null +++ b/plugins/ilert/src/hooks/useNewIncident.ts @@ -0,0 +1,77 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ilertApiRef, UnauthorizedError } from '../api'; +import { useApi, errorApiRef } from '@backstage/core'; +import { useAsyncRetry } from 'react-use'; +import { AlertSource } from '../types'; + +export const useNewIncident = ( + open: boolean, + initialAlertSource?: AlertSource | null, +) => { + const ilertApi = useApi(ilertApiRef); + const errorApi = useApi(errorApiRef); + + const [alertSourcesList, setAlertSourcesList] = React.useState( + [], + ); + const [alertSource, setAlertSource] = React.useState( + null, + ); + const [summary, setSummary] = React.useState(''); + const [details, setDetails] = React.useState(''); + const [isLoading, setIsLoading] = React.useState(false); + + const fetchAlertSources = useAsyncRetry(async () => { + try { + if (!open || initialAlertSource) { + return; + } + const count = await ilertApi.fetchAlertSources(); + setAlertSourcesList(count || 0); + } catch (e) { + if (!(e instanceof UnauthorizedError)) { + errorApi.post(e); + } + throw e; + } + }, [open]); + + const error = fetchAlertSources.error; + const retry = () => { + fetchAlertSources.retry(); + }; + + return [ + { + alertSources: alertSourcesList, + alertSource: initialAlertSource ? initialAlertSource : alertSource, + summary, + details, + error, + isLoading, + }, + { + setAlertSourcesList, + setAlertSource, + setSummary, + setDetails, + setIsLoading, + retry, + }, + ] as const; +}; diff --git a/plugins/ilert/src/hooks/useOnCallSchedules.ts b/plugins/ilert/src/hooks/useOnCallSchedules.ts new file mode 100644 index 0000000000..5e0538a370 --- /dev/null +++ b/plugins/ilert/src/hooks/useOnCallSchedules.ts @@ -0,0 +1,60 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ilertApiRef, UnauthorizedError } from '../api'; +import { useApi, errorApiRef } from '@backstage/core'; +import { useAsyncRetry } from 'react-use'; +import { Schedule } from '../types'; + +export const useOnCallSchedules = () => { + const ilertApi = useApi(ilertApiRef); + const errorApi = useApi(errorApiRef); + + const [onCallSchedulesList, setOnCallSchedulesList] = React.useState< + Schedule[] + >([]); + const [isLoading, setIsLoading] = React.useState(false); + + const fetchOnCallSchedulesCall = async () => { + try { + setIsLoading(true); + const data = await ilertApi.fetchOnCallSchedules(); + setOnCallSchedulesList(data || []); + setIsLoading(false); + } catch (e) { + setIsLoading(false); + if (!(e instanceof UnauthorizedError)) { + errorApi.post(e); + } + throw e; + } + }; + + const { error, retry } = useAsyncRetry(fetchOnCallSchedulesCall, []); + + return [ + { + onCallSchedules: onCallSchedulesList, + error, + isLoading, + }, + { + retry, + setIsLoading, + refetchOnCallSchedules: fetchOnCallSchedulesCall, + }, + ] as const; +}; diff --git a/plugins/ilert/src/hooks/useShiftOverride.ts b/plugins/ilert/src/hooks/useShiftOverride.ts new file mode 100644 index 0000000000..fd1da7423b --- /dev/null +++ b/plugins/ilert/src/hooks/useShiftOverride.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ilertApiRef, UnauthorizedError } from '../api'; +import { useApi, errorApiRef } from '@backstage/core'; +import { useAsyncRetry } from 'react-use'; +import { User, Shift } from '../types'; + +export const useShiftOverride = (s: Shift, isModalOpened: boolean) => { + const ilertApi = useApi(ilertApiRef); + const errorApi = useApi(errorApiRef); + + const [shift, setShift] = React.useState(s); + const [usersList, setUsersList] = React.useState([]); + const [isLoading, setIsLoading] = React.useState(false); + + const { error, retry } = useAsyncRetry(async () => { + try { + if (!isModalOpened) { + return; + } + setIsLoading(true); + const data = await ilertApi.fetchUsers(); + setUsersList(data || []); + setIsLoading(false); + } catch (e) { + setIsLoading(false); + if (!(e instanceof UnauthorizedError)) { + errorApi.post(e); + } + throw e; + } + }, [isModalOpened]); + + const setUser = (user: User) => { + setShift({ ...shift, user }); + }; + + const setStart = (start: string) => { + setShift({ ...shift, start }); + }; + + const setEnd = (end: string) => { + setShift({ ...shift, end }); + }; + + return [ + { + shift, + users: usersList, + user: shift.user, + start: shift.start, + end: shift.end, + error, + isLoading, + }, + { + retry, + setIsLoading, + setUser, + setStart, + setEnd, + }, + ] as const; +}; diff --git a/plugins/ilert/src/hooks/useUptimeMonitors.ts b/plugins/ilert/src/hooks/useUptimeMonitors.ts new file mode 100644 index 0000000000..02cd5aaa2f --- /dev/null +++ b/plugins/ilert/src/hooks/useUptimeMonitors.ts @@ -0,0 +1,88 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ilertApiRef, TableState, UnauthorizedError } from '../api'; +import { useApi, errorApiRef } from '@backstage/core'; +import { useAsyncRetry } from 'react-use'; +import { UptimeMonitor } from '../types'; + +export const useUptimeMonitors = () => { + const ilertApi = useApi(ilertApiRef); + const errorApi = useApi(errorApiRef); + + const [tableState, setTableState] = React.useState({ + page: 0, + pageSize: 10, + }); + const [uptimeMonitorsList, setUptimeMonitorsList] = React.useState< + UptimeMonitor[] + >([]); + const [isLoading, setIsLoading] = React.useState(false); + + const { error, retry } = useAsyncRetry(async () => { + try { + setIsLoading(true); + const data = await ilertApi.fetchUptimeMonitors(); + setUptimeMonitorsList(data || []); + setIsLoading(false); + } catch (e) { + setIsLoading(false); + if (!(e instanceof UnauthorizedError)) { + errorApi.post(e); + } + throw e; + } + }, [tableState]); + + const onUptimeMonitorChanged = (newUptimeMonitor: UptimeMonitor) => { + setUptimeMonitorsList( + uptimeMonitorsList.map( + (uptimeMonitor: UptimeMonitor): UptimeMonitor => { + if (newUptimeMonitor.id === uptimeMonitor.id) { + return newUptimeMonitor; + } + + return uptimeMonitor; + }, + ), + ); + }; + + const onChangePage = (page: number) => { + setTableState({ ...tableState, page }); + }; + const onChangeRowsPerPage = (pageSize: number) => { + setTableState({ ...tableState, pageSize }); + }; + + return [ + { + tableState, + uptimeMonitors: uptimeMonitorsList, + error, + isLoading, + }, + { + setTableState, + setUptimeMonitorsList, + retry, + onUptimeMonitorChanged, + onChangePage, + onChangeRowsPerPage, + setIsLoading, + }, + ] as const; +}; diff --git a/plugins/ilert/src/index.ts b/plugins/ilert/src/index.ts new file mode 100644 index 0000000000..2e5301f151 --- /dev/null +++ b/plugins/ilert/src/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { IconComponent } from '@backstage/core'; +import ILertIconComponent from './assets/ilert.icon.svg'; + +export { + ilertPlugin, + ilertPlugin as plugin, + ILertPage, + EntityILertCard, +} from './plugin'; +export { + ILertPage as Router, + isPluginApplicableToEntity, + isPluginApplicableToEntity as isILertAvailable, + ILertCard, +} from './components'; +export * from './api'; +export * from './route-refs'; +export const ILertIcon: IconComponent = ILertIconComponent; diff --git a/plugins/ilert/src/plugin.test.ts b/plugins/ilert/src/plugin.test.ts new file mode 100644 index 0000000000..89a0cb231f --- /dev/null +++ b/plugins/ilert/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { ilertPlugin } from './plugin'; + +describe('plugin-ilert', () => { + it('should export plugin', () => { + expect(ilertPlugin).toBeDefined(); + }); +}); diff --git a/plugins/ilert/src/plugin.ts b/plugins/ilert/src/plugin.ts new file mode 100644 index 0000000000..546b77911d --- /dev/null +++ b/plugins/ilert/src/plugin.ts @@ -0,0 +1,60 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { + configApiRef, + createApiFactory, + createPlugin, + discoveryApiRef, + identityApiRef, + createRoutableExtension, + createComponentExtension, +} from '@backstage/core'; +import { ILertClient, ilertApiRef } from './api'; +import { iLertRouteRef } from './route-refs'; + +export const ilertPlugin = createPlugin({ + id: 'ilert', + apis: [ + createApiFactory({ + api: ilertApiRef, + deps: { + discoveryApi: discoveryApiRef, + identityApi: identityApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, configApi }) => + ILertClient.fromConfig(configApi, discoveryApi), + }), + ], + routes: { + root: iLertRouteRef, + }, +}); + +export const ILertPage = ilertPlugin.provide( + createRoutableExtension({ + component: () => import('./components').then(m => m.ILertPage), + mountPoint: iLertRouteRef, + }), +); + +export const EntityILertCard = ilertPlugin.provide( + createComponentExtension({ + component: { + lazy: () => import('./components/ILertCard').then(m => m.ILertCard), + }, + }), +); diff --git a/plugins/ilert/src/route-refs.tsx b/plugins/ilert/src/route-refs.tsx new file mode 100644 index 0000000000..b63ba15315 --- /dev/null +++ b/plugins/ilert/src/route-refs.tsx @@ -0,0 +1,24 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { createRouteRef } from '@backstage/core'; +import ILertIcon from './assets/ilert.icon.svg'; + +export const iLertRouteRef = createRouteRef({ + icon: ILertIcon, + path: '/ilert', + title: 'iLert', +}); diff --git a/plugins/ilert/src/setupTests.ts b/plugins/ilert/src/setupTests.ts new file mode 100644 index 0000000000..0cec5b395d --- /dev/null +++ b/plugins/ilert/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/ilert/src/types.ts b/plugins/ilert/src/types.ts new file mode 100644 index 0000000000..dd9aab42d7 --- /dev/null +++ b/plugins/ilert/src/types.ts @@ -0,0 +1,312 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 Incident { + id: number; + summary: string; + details: string; + reportTime: string; + resolvedOn: string; + status: IncidentStatus; + priority: IncidentPriority; + incidentKey: string; + alertSource: AlertSource | null; + assignedTo: User | null; + logEntries: LogEntry[]; + links: Link[]; + images: Image[]; + subscribers: Subscriber[]; + commentText: string; + commentPublishToSubscribers: boolean; +} + +export const PENDING = 'PENDING'; +export const ACCEPTED = 'ACCEPTED'; +export const RESOLVED = 'RESOLVED'; +export type IncidentStatus = typeof PENDING | typeof ACCEPTED | typeof RESOLVED; +export type IncidentPriority = 'HIGH' | 'LOW'; + +export interface Link { + href: string; + text: string; +} + +export interface Image { + src: string; + href: string; + alt: string; +} + +export type SubscriberType = 'TEAM' | 'USER'; + +export interface Subscriber { + id: number; + name: string; + type: SubscriberType; +} + +export interface LogEntry { + id: number; + timestamp: string; + logEntryType: string; + text: string; + incidentId?: number; + iconName?: string; + iconClass?: string; + filterTypes?: string[]; +} + +export interface User { + id: number; + username: string; + firstName: string; + lastName: string; + email: string; + mobile: Phone; + landline: Phone; + timezone?: string; + language?: Language; + role?: UserRole; + notificationPreferences?: any[]; + position: string; + department: string; +} + +export type UserRole = + | 'USER' + | 'ADMIN' + | 'STAKEHOLDER' + | 'ACCOUNT_OWNER' + | 'RESPONDER'; +export type Language = 'de' | 'en'; +export interface Phone { + regionCode: string; + number: string; +} + +export interface AlertSource { + id: number; + name: string; + status: AlertSourceStatus; + escalationPolicy: EscalationPolicy; + integrationType: AlertSourceIntegrationType; + integrationKey?: string; + iconUrl?: string; + lightIconUrl?: string; + darkIconUrl?: string; + incidentCreation?: AlertSourceIncidentCreation; + incidentPriorityRule?: AlertSourceIncidentPriorityRule; + emailFiltered?: boolean; + emailResolveFiltered?: boolean; + active?: boolean; + emailPredicates?: AlertSourceEmailPredicate[]; + emailResolvePredicates?: AlertSourceEmailPredicate[]; + filterOperator?: AlertSourceFilterOperator; + resolveFilterOperator?: AlertSourceFilterOperator; + supportHours?: AlertSourceSupportHours; + heartbeat?: AlertSourceHeartbeat; + autotaskMetadata?: AlertSourceAutotaskMetadata; + autoResolutionTimeout?: string; + teams: TeamShort[]; +} + +export interface TeamShort { + id: number; + name: string; +} + +export interface TeamMember { + user: User; + role: 'STAKEHOLDER' | 'RESPONDER' | 'USER' | 'ADMIN'; +} + +export type AlertSourceStatus = + | 'PENDING' + | 'ALL_ACCEPTED' + | 'ALL_RESOLVED' + | 'IN_MAINTENANCE' + | 'DISABLED'; +export type AlertSourceIntegrationType = + | 'NAGIOS' + | 'ICINGA' + | 'EMAIL' + | 'SMS' + | 'API' + | 'CRN' + | 'HEARTBEAT' + | 'PRTG' + | 'PINGDOM' + | 'CLOUDWATCH' + | 'AWSPHD' + | 'STACKDRIVER' + | 'INSTANA' + | 'ZABBIX' + | 'SOLARWINDS' + | 'PROMETHEUS' + | 'NEWRELIC' + | 'GRAFANA' + | 'GITHUB' + | 'DATADOG' + | 'UPTIMEROBOT' + | 'APPDYNAMICS' + | 'DYNATRACE' + | 'TOPDESK' + | 'STATUSCAKE' + | 'MONITOR' + | 'TOOL' + | 'CHECKMK' + | 'AUTOTASK' + | 'AWSBUDGET' + | 'KENTIXAM' + | 'CONSUL' + | 'ZAMMAD' + | 'SIGNALFX' + | 'SPLUNK' + | 'KUBERNETES' + | 'SEMATEXT' + | 'SENTRY' + | 'SUMOLOGIC' + | 'RAYGUN' + | 'MXTOOLBOX' + | 'ESWATCHER' + | 'AMAZONSNS' + | 'KAPACITOR' + | 'CORTEXXSOAR' + | string; +export type AlertSourceIncidentCreation = + | 'ONE_INCIDENT_PER_EMAIL' + | 'ONE_INCIDENT_PER_EMAIL_SUBJECT' + | 'ONE_PENDING_INCIDENT_ALLOWED' + | 'ONE_OPEN_INCIDENT_ALLOWED' + | 'OPEN_RESOLVE_ON_EXTRACTION'; +export type AlertSourceFilterOperator = 'AND' | 'OR'; +export type AlertSourceIncidentPriorityRule = + | 'HIGH' + | 'LOW' + | 'HIGH_DURING_SUPPORT_HOURS' + | 'LOW_DURING_SUPPORT_HOURS'; +export interface AlertSourceEmailPredicate { + field: 'EMAIL_FROM' | 'EMAIL_SUBJECT' | 'EMAIL_BODY'; + criteria: + | 'CONTAINS_ANY_WORDS' + | 'CONTAINS_NOT_WORDS' + | 'CONTAINS_STRING' + | 'CONTAINS_NOT_STRING' + | 'IS_STRING' + | 'IS_NOT_STRING' + | 'MATCHES_REGEX' + | 'MATCHES_NOT_REGEX'; + value: string; +} +export type AlertSourceTimeZone = + | 'Europe/Berlin' + | 'America/New_York' + | 'America/Los_Angeles' + | 'Asia/Istanbul'; +export interface AlertSourceSupportDay { + start: string; + end: string; +} +export interface AlertSourceSupportHours { + timezone: AlertSourceTimeZone; + autoRaiseIncidents: boolean; + supportDays: { + MONDAY: AlertSourceSupportDay; + TUESDAY: AlertSourceSupportDay; + WEDNESDAY: AlertSourceSupportDay; + THURSDAY: AlertSourceSupportDay; + FRIDAY: AlertSourceSupportDay; + SATURDAY: AlertSourceSupportDay; + SUNDAY: AlertSourceSupportDay; + }; +} +export interface AlertSourceHeartbeat { + summary: string; + intervalSec: number; + status: 'OVERDUE' | 'ON_TIME' | 'NEVER_RECEIVED'; +} + +export interface AlertSourceAutotaskMetadata { + userName: string; + secret: string; + apiIntegrationCode: string; + webServer: string; +} + +export interface EscalationPolicy { + id: number; + name: string; + escalationRules: EscalationRule[]; + newEscalationRule: EscalationRule; + repeating?: boolean; + frequency?: number; + teams: TeamShort[]; +} + +export interface EscalationRule { + user: User | null; + schedule: Schedule | null; + escalationTimeout: number; +} + +export interface Schedule { + id: number; + name: string; + timezone: string; + startsOn: string; + currentShift: Shift; + nextShift: Shift; + shifts: Shift[]; + overrides: Shift[]; + teams: TeamShort[]; +} + +export interface Shift { + user: User; + start: string; + end: string; +} + +export interface UptimeMonitor { + id: number; + name: string; + region: 'EU' | 'US'; + checkType: 'http' | 'tcp' | 'udp' | 'ping'; + checkParams: UptimeMonitorCheckParams; + intervalSec: number; + timeoutMs: number; + createIncidentAfterFailedChecks: number; + paused: boolean; + embedUrl: string; + shareUrl: string; + status: string; + lastStatusChange: string; + escalationPolicy: EscalationPolicy; + teams: TeamShort[]; +} + +export interface UptimeMonitorCheckParams { + host?: string; + port?: number; + url?: string; +} + +export interface IncidentResponder { + group: 'SUGGESTED' | 'USER' | 'ESCALATION_POLICY' | 'ON_CALL_SCHEDULE'; + id: number; + name: string; + disabled: boolean; +} From 687d973c6be3c5511743a64d73cf19e3a61a867d Mon Sep 17 00:00:00 2001 From: yacut Date: Wed, 14 Apr 2021 20:31:18 +0200 Subject: [PATCH 10/95] add incident actions to ilert plugin Signed-off-by: yacut --- plugins/ilert/README.md | 10 ++- plugins/ilert/src/api/client.ts | 60 ++++++++++++++---- plugins/ilert/src/api/types.ts | 12 +++- .../src/components/ILertCard/ILertCard.tsx | 3 +- .../Incident/IncidentActionsMenu.tsx | 54 +++++++++++++++- plugins/ilert/src/hooks/useIncidentActions.ts | 61 +++++++++++++++++++ plugins/ilert/src/hooks/useIncidents.ts | 18 +++++- plugins/ilert/src/types.ts | 16 +++++ 8 files changed, 206 insertions(+), 28 deletions(-) create mode 100644 plugins/ilert/src/hooks/useIncidentActions.ts diff --git a/plugins/ilert/README.md b/plugins/ilert/README.md index 55e41ba5bd..0265f4172b 100644 --- a/plugins/ilert/README.md +++ b/plugins/ilert/README.md @@ -17,6 +17,9 @@ This plugin provides: - A list of incidents - A way to trigger a new incident - A way to reassign/acknowledge/resolve an incident +- A way to trigger an incident action +- A way to trigger an immediate maintenance +- A way to disable/enable an alert source - A list of uptime monitors ## Setup instructions @@ -41,7 +44,6 @@ import { isPluginApplicableToEntity as isILertAvailable, EntityILertCard, } from '@backstage/plugin-ilert'; -// add to code { isILertAvailable(entity) && ( @@ -58,10 +60,8 @@ import { Modify your app routes in [`App.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/App.tsx) to include the Router component exported from the plugin, for example: ```tsx -// At the top imports import { ILertPage } from '@backstage/plugin-ilert'; -// Inside App component // ... } /> @@ -72,10 +72,8 @@ import { ILertPage } from '@backstage/plugin-ilert'; Modify your sidebar in [`Root.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/components/Root/Root.tsx) to include the icon component exported from the plugin, for example: ```tsx -// At the top imports import { ILertIcon } from '@backstage/plugin-ilert'; -// Inside Sidebar component // ... @@ -91,7 +89,7 @@ In `app-config.yaml`: ```yaml ilert: - domain: https://my-org.ilert.com/ + baseUrl: https://my-org.ilert.com/ ``` ## Providing the Authorization Header diff --git a/plugins/ilert/src/api/client.ts b/plugins/ilert/src/api/client.ts index 259502eff0..ed79681320 100644 --- a/plugins/ilert/src/api/client.ts +++ b/plugins/ilert/src/api/client.ts @@ -18,6 +18,7 @@ import { AlertSource, EscalationPolicy, Incident, + IncidentAction, IncidentResponder, Schedule, UptimeMonitor, @@ -44,22 +45,22 @@ export class UnauthorizedError extends Error {} export class ILertClient implements ILertApi { private readonly discoveryApi: DiscoveryApi; private readonly proxyPath: string; - private readonly domain: string; + private readonly baseUrl: string; static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) { - const domainUrl: string = - configApi.getOptionalString('domainUrl') ?? 'https://api.ilert.com'; + const baseUrl: string = + configApi.getOptionalString('ilert.baseUrl') ?? 'https://app.ilert.com'; return new ILertClient({ discoveryApi: discoveryApi, - domain: domainUrl, + baseUrl, proxyPath: configApi.getOptionalString('ilert.proxyPath'), }); } constructor(opts: Options) { this.discoveryApi = opts.discoveryApi; - this.domain = opts.domain; + this.baseUrl = opts.baseUrl; this.proxyPath = opts.proxyPath ?? DEFAULT_PROXY_PATH; } @@ -94,7 +95,7 @@ export class ILertClient implements ILertApi { query.append('start-index', `${opts.startIndex}`); } if (opts && opts.alertSources) { - opts.alertSources.forEach((a: string | number) => { + opts.alertSources.forEach((a: number) => { if (a) { query.append('alert-source', `${a}`); } @@ -153,6 +154,22 @@ export class ILertClient implements ILertApi { return response; } + async fetchIncidentActions(incident: Incident): Promise { + const init = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + const response = await this.fetch( + `/api/v1/incidents/${incident.id}/actions`, + init, + ); + + return response; + } + async acceptIncident(incident: Incident): Promise { const init = { method: 'PUT', @@ -222,6 +239,27 @@ export class ILertClient implements ILertApi { return response; } + async triggerIncidentAction( + incident: Incident, + action: IncidentAction, + ): Promise { + const init = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ + webhookId: action.webhookId, + extensionId: action.extensionId, + type: action.type, + name: action.name, + }), + }; + + await this.fetch(`/api/v1/incidents/${incident.id}/actions`, init); + } + async createIncident(eventRequest: EventRequest): Promise { const init = { method: 'POST', @@ -461,26 +499,26 @@ export class ILertClient implements ILertApi { } getIncidentDetailsURL(incident: Incident): string { - return `${this.domain}/incident/view.jsf?id=${incident.id}`; + return `${this.baseUrl}/incident/view.jsf?id=${incident.id}`; } getAlertSourceDetailsURL(alertSource: AlertSource | null): string { if (!alertSource) { return ''; } - return `${this.domain}/source/view.jsf?id=${alertSource.id}`; + return `${this.baseUrl}/source/view.jsf?id=${alertSource.id}`; } getEscalationPolicyDetailsURL(escalationPolicy: EscalationPolicy): string { - return `${this.domain}/policy/view.jsf?id=${escalationPolicy.id}`; + return `${this.baseUrl}/policy/view.jsf?id=${escalationPolicy.id}`; } getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string { - return `${this.domain}/uptime/view.jsf?id=${uptimeMonitor.id}`; + return `${this.baseUrl}/uptime/view.jsf?id=${uptimeMonitor.id}`; } getScheduleDetailsURL(schedule: Schedule): string { - return `${this.domain}/schedule/view.jsf?id=${schedule.id}`; + return `${this.baseUrl}/schedule/view.jsf?id=${schedule.id}`; } getUserInitials(assignedTo: User | null) { diff --git a/plugins/ilert/src/api/types.ts b/plugins/ilert/src/api/types.ts index 4c33455d86..b37ae73c93 100644 --- a/plugins/ilert/src/api/types.ts +++ b/plugins/ilert/src/api/types.ts @@ -23,6 +23,7 @@ import { EscalationPolicy, Schedule, IncidentResponder, + IncidentAction, } from '../types'; export type TableState = { @@ -34,7 +35,7 @@ export type GetIncidentsOpts = { maxResults?: number; startIndex?: number; states?: IncidentStatus[]; - alertSources?: number[] | string[]; + alertSources?: number[]; }; export type GetIncidentsCountOpts = { @@ -53,6 +54,7 @@ export interface ILertApi { fetchIncidents(opts?: GetIncidentsOpts): Promise; fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise; fetchIncidentResponders(incident: Incident): Promise; + fetchIncidentActions(incident: Incident): Promise; acceptIncident(incident: Incident): Promise; resolveIncident(incident: Incident): Promise; assignIncident( @@ -60,6 +62,10 @@ export interface ILertApi { responder: IncidentResponder, ): Promise; createIncident(eventRequest: EventRequest): Promise; + triggerIncidentAction( + incident: Incident, + action: IncidentAction, + ): Promise; fetchUptimeMonitors(): Promise; pauseUptimeMonitor(uptimeMonitor: UptimeMonitor): Promise; @@ -98,10 +104,10 @@ export type Options = { discoveryApi: DiscoveryApi; /** - * Domain used by users to access iLert web UI. + * URL used by users to access iLert web UI. * Example: https://my-org.ilert.com/ */ - domain: string; + baseUrl: string; /** * Path to use for requests via the proxy, defaults to /ilert/api diff --git a/plugins/ilert/src/components/ILertCard/ILertCard.tsx b/plugins/ilert/src/components/ILertCard/ILertCard.tsx index b376b319b5..9e2c93d3da 100644 --- a/plugins/ilert/src/components/ILertCard/ILertCard.tsx +++ b/plugins/ilert/src/components/ILertCard/ILertCard.tsx @@ -56,7 +56,6 @@ export const ILertCard = () => { { alertSource, uptimeMonitor }, { setAlertSource, refetchAlertSource }, ] = useAlertSource(integrationKey); - const alertSourcesFilter = alertSource ? [alertSource.id] : [integrationKey]; const [ { tableState, states, incidents, incidentsCount, isLoading, error }, { @@ -67,7 +66,7 @@ export const ILertCard = () => { refetchIncidents, setIsLoading, }, - ] = useIncidents(false, alertSourcesFilter); + ] = useIncidents(false, true, alertSource); const [ isNewIncidentModalOpened, diff --git a/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx b/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx index 576f037c8c..0a1ae95ded 100644 --- a/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx +++ b/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx @@ -14,13 +14,14 @@ * limitations under the License. */ import React from 'react'; -import { alertApiRef, useApi } from '@backstage/core'; +import { alertApiRef, Progress, useApi } from '@backstage/core'; import { IconButton, Menu, MenuItem, Typography } from '@material-ui/core'; import Link from '@material-ui/core/Link'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import { ilertApiRef } from '../../api'; -import { Incident } from '../../types'; +import { Incident, IncidentAction } from '../../types'; import { IncidentAssignModal } from './IncidentAssignModal'; +import { useIncidentActions } from '../../hooks/useIncidentActions'; export const IncidentActionsMenu = ({ incident, @@ -41,6 +42,11 @@ export const IncidentActionsMenu = ({ setIsAssignIncidentModalOpened, ] = React.useState(false); + const [{ incidentActions, isLoading }] = useIncidentActions( + incident, + Boolean(anchorEl), + ); + const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; @@ -84,6 +90,40 @@ export const IncidentActionsMenu = ({ setIsAssignIncidentModalOpened(true); }; + const handleTriggerAction = (action: IncidentAction) => async () => { + try { + handleCloseMenu(); + setProcessing(true); + await ilertApi.triggerIncidentAction(incident, action); + alertApi.post({ message: 'Incident action triggered.' }); + setProcessing(false); + } catch (err) { + setProcessing(false); + alertApi.post({ message: err, severity: 'error' }); + } + }; + + const actions: React.ReactNode[] = incidentActions.map(a => { + const successTrigger = a.history + ? a.history.find(h => h.success) + : undefined; + const triggeredBy = + successTrigger && successTrigger.actor + ? `${successTrigger.actor.firstName} ${successTrigger.actor.lastName}` + : ''; + return ( + + + {triggeredBy ? `${a.name} (by ${triggeredBy})` : a.name} + + + ); + }); + return ( <> {incident.status === 'PENDING' ? ( @@ -129,6 +169,14 @@ export const IncidentActionsMenu = ({ ) : null} + {isLoading ? ( + + + + ) : ( + actions + )} + diff --git a/plugins/ilert/src/hooks/useIncidentActions.ts b/plugins/ilert/src/hooks/useIncidentActions.ts new file mode 100644 index 0000000000..d8dd08e17a --- /dev/null +++ b/plugins/ilert/src/hooks/useIncidentActions.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ilertApiRef, UnauthorizedError } from '../api'; +import { useApi, errorApiRef } from '@backstage/core'; +import { useAsyncRetry } from 'react-use'; +import { Incident, IncidentAction } from '../types'; + +export const useIncidentActions = ( + incident: Incident | null, + open: boolean, +) => { + const ilertApi = useApi(ilertApiRef); + const errorApi = useApi(errorApiRef); + + const [incidentActionsList, setIncidentActionsList] = React.useState< + IncidentAction[] + >([]); + const [isLoading, setIsLoading] = React.useState(false); + + const { error, retry } = useAsyncRetry(async () => { + try { + if (!incident || !open) { + return; + } + const data = await ilertApi.fetchIncidentActions(incident); + setIncidentActionsList(data); + } catch (e) { + if (!(e instanceof UnauthorizedError)) { + errorApi.post(e); + } + throw e; + } + }, [incident, open]); + + return [ + { + incidentActions: incidentActionsList, + error, + isLoading, + }, + { + setIncidentActionsList, + setIsLoading, + retry, + }, + ] as const; +}; diff --git a/plugins/ilert/src/hooks/useIncidents.ts b/plugins/ilert/src/hooks/useIncidents.ts index a252dfb14f..2f3d12cf17 100644 --- a/plugins/ilert/src/hooks/useIncidents.ts +++ b/plugins/ilert/src/hooks/useIncidents.ts @@ -22,11 +22,18 @@ import { } from '../api'; import { useApi, errorApiRef } from '@backstage/core'; import { useAsyncRetry } from 'react-use'; -import { ACCEPTED, PENDING, Incident, IncidentStatus } from '../types'; +import { + ACCEPTED, + PENDING, + Incident, + IncidentStatus, + AlertSource, +} from '../types'; export const useIncidents = ( paging: boolean, - alertSources?: number[] | string[], + singleSource?: boolean, + alertSource?: AlertSource | null, ) => { const ilertApi = useApi(ilertApiRef); const errorApi = useApi(errorApiRef); @@ -45,10 +52,13 @@ export const useIncidents = ( const fetchIncidentsCall = async () => { try { + if (singleSource && !alertSource) { + return; + } setIsLoading(true); const opts: GetIncidentsOpts = { states, - alertSources, + alertSources: alertSource ? [alertSource.id] : [], }; if (paging) { opts.maxResults = tableState.pageSize; @@ -80,6 +90,8 @@ export const useIncidents = ( const fetchIncidents = useAsyncRetry(fetchIncidentsCall, [ tableState, states, + singleSource, + alertSource, ]); const refetchIncidents = () => { diff --git a/plugins/ilert/src/types.ts b/plugins/ilert/src/types.ts index dd9aab42d7..a209b99533 100644 --- a/plugins/ilert/src/types.ts +++ b/plugins/ilert/src/types.ts @@ -310,3 +310,19 @@ export interface IncidentResponder { name: string; disabled: boolean; } + +export interface IncidentAction { + name: string; + type: string; + webhookId: string; + extensionId?: string; + history?: IncidentActionHistory[]; +} + +export interface IncidentActionHistory { + id: string; + webhookId: string; + incidentId: number; + actor: User; + success: boolean; +} From 78589718b823c40aeddab15adb9d18e172abbd81 Mon Sep 17 00:00:00 2001 From: yacut Date: Fri, 16 Apr 2021 22:10:34 +0200 Subject: [PATCH 11/95] add on-call to entity card Signed-off-by: yacut --- plugins/ilert/src/api/client.ts | 43 ++++- plugins/ilert/src/api/types.ts | 5 +- .../src/components/ILertCard/ILertCard.tsx | 2 + .../components/ILertCard/ILertCardOnCall.tsx | 120 +++++++++++++ .../ILertCard/ILertCardOnCallEmptyState.tsx | 45 +++++ .../ILertCard/ILertCardOnCallItem.tsx | 169 ++++++++++++++++++ .../OnCallSchedulesGrid.tsx | 34 +++- .../ilert/src/hooks/useAlertSourceOnCalls.ts | 63 +++++++ plugins/ilert/src/types.ts | 9 + 9 files changed, 478 insertions(+), 12 deletions(-) create mode 100644 plugins/ilert/src/components/ILertCard/ILertCardOnCall.tsx create mode 100644 plugins/ilert/src/components/ILertCard/ILertCardOnCallEmptyState.tsx create mode 100644 plugins/ilert/src/components/ILertCard/ILertCardOnCallItem.tsx create mode 100644 plugins/ilert/src/hooks/useAlertSourceOnCalls.ts diff --git a/plugins/ilert/src/api/client.ts b/plugins/ilert/src/api/client.ts index ed79681320..f3c263e9e7 100644 --- a/plugins/ilert/src/api/client.ts +++ b/plugins/ilert/src/api/client.ts @@ -20,6 +20,7 @@ import { Incident, IncidentAction, IncidentResponder, + OnCall, Schedule, UptimeMonitor, User, @@ -388,6 +389,24 @@ export class ILertClient implements ILertApi { return response; } + async fetchAlertSourceOnCalls(alertSource: AlertSource): Promise { + const init = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + const response = await this.fetch( + `/api/v1/on-calls?policies=${ + alertSource.escalationPolicy.id + }&expand=user&expand=escalationPolicy&timezone=${momentTimezone.tz.guess()}`, + init, + ); + + return response; + } + async enableAlertSource(alertSource: AlertSource): Promise { const init = { method: 'PUT', @@ -521,14 +540,28 @@ export class ILertClient implements ILertApi { return `${this.baseUrl}/schedule/view.jsf?id=${schedule.id}`; } - getUserInitials(assignedTo: User | null) { - if (!assignedTo) { + getUserPhoneNumber(user: User | null) { + if (!user) { return ''; } - if (!assignedTo.firstName && !assignedTo.lastName) { - return assignedTo.username; + if (user.mobile) { + return user.mobile.number; } - return `${assignedTo.firstName} ${assignedTo.lastName} (${assignedTo.username})`; + + if (user.landline) { + return user.landline.number; + } + return ''; + } + + getUserInitials(user: User | null) { + if (!user) { + return ''; + } + if (!user.firstName && !user.lastName) { + return user.username; + } + return `${user.firstName} ${user.lastName} (${user.username})`; } private async apiUrl() { diff --git a/plugins/ilert/src/api/types.ts b/plugins/ilert/src/api/types.ts index b37ae73c93..84b4ecf654 100644 --- a/plugins/ilert/src/api/types.ts +++ b/plugins/ilert/src/api/types.ts @@ -24,6 +24,7 @@ import { Schedule, IncidentResponder, IncidentAction, + OnCall, } from '../types'; export type TableState = { @@ -74,6 +75,7 @@ export interface ILertApi { fetchAlertSources(): Promise; fetchAlertSource(idOrIntegrationKey: number | string): Promise; + fetchAlertSourceOnCalls(alertSource: AlertSource): Promise; enableAlertSource(alertSource: AlertSource): Promise; disableAlertSource(alertSource: AlertSource): Promise; @@ -97,7 +99,8 @@ export interface ILertApi { getEscalationPolicyDetailsURL(escalationPolicy: EscalationPolicy): string; getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string; getScheduleDetailsURL(schedule: Schedule): string; - getUserInitials(assignedTo: User | null): string; + getUserPhoneNumber(user: User | null): string; + getUserInitials(user: User | null): string; } export type Options = { diff --git a/plugins/ilert/src/components/ILertCard/ILertCard.tsx b/plugins/ilert/src/components/ILertCard/ILertCard.tsx index 9e2c93d3da..dd4bdde3cf 100644 --- a/plugins/ilert/src/components/ILertCard/ILertCard.tsx +++ b/plugins/ilert/src/components/ILertCard/ILertCard.tsx @@ -33,6 +33,7 @@ import { useILertEntity } from '../../hooks'; import { ILertCardHeaderStatus } from './ILertCardHeaderStatus'; import { ILertCardMaintenanceModal } from './ILertCardMaintenanceModal'; import { ILertCardEmptyState } from './ILertCardEmptyState'; +import { ILertCardOnCall } from './ILertCardOnCall'; export const isPluginApplicableToEntity = (entity: Entity) => Boolean(entity.metadata.annotations?.[ILERT_INTEGRATION_KEY]); @@ -111,6 +112,7 @@ export const ILertCard = () => { /> + ({ + repeatText: { + fontStyle: 'italic', + }, + repeatIcon: { + marginLeft: theme.spacing(1), + }, +})); + +export const ILertCardOnCall = ({ + alertSource, +}: { + alertSource: AlertSource | null; +}) => { + const classes = useStyles(); + const [{ onCalls, isLoading }, {}] = useAlertSourceOnCalls(alertSource); + + if (isLoading) { + return ; + } + + if (!alertSource || !onCalls) { + return null; + } + + const repeatInfo = () => { + if ( + !onCalls || + !onCalls.length || + !onCalls[onCalls.length - 1].escalationPolicy || + !onCalls[onCalls.length - 1].escalationPolicy.repeating || + !onCalls[onCalls.length - 1].escalationPolicy.frequency + ) { + return null; + } + + return ( + + + + + + {`Repeat ${ + onCalls[onCalls.length - 1].escalationPolicy.frequency + } times`} + + } + /> + + ); + }; + + if (!onCalls.length) { + return ( + ON CALL}> + + + ); + } + + if (onCalls.length === 1) { + ON CALL}> + + ; + } + + return ( + ON CALL}> + {onCalls.map((onCall, index) => ( + + ))} + {repeatInfo()} + + ); +}; diff --git a/plugins/ilert/src/components/ILertCard/ILertCardOnCallEmptyState.tsx b/plugins/ilert/src/components/ILertCard/ILertCardOnCallEmptyState.tsx new file mode 100644 index 0000000000..2d43aeccb8 --- /dev/null +++ b/plugins/ilert/src/components/ILertCard/ILertCardOnCallEmptyState.tsx @@ -0,0 +1,45 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemText from '@material-ui/core/ListItemText'; +import Typography from '@material-ui/core/Typography'; + +const useStyles = makeStyles({ + text: { + fontStyle: 'italic', + }, +}); + +export const ILertCardOnCallEmptyState = () => { + const classes = useStyles(); + + return ( + + + + Nobody + + + + ); +}; diff --git a/plugins/ilert/src/components/ILertCard/ILertCardOnCallItem.tsx b/plugins/ilert/src/components/ILertCard/ILertCardOnCallItem.tsx new file mode 100644 index 0000000000..69ceb0ad55 --- /dev/null +++ b/plugins/ilert/src/components/ILertCard/ILertCardOnCallItem.tsx @@ -0,0 +1,169 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import Divider from '@material-ui/core/Divider'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import Avatar from '@material-ui/core/Avatar'; +import ListItemText from '@material-ui/core/ListItemText'; +import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; +import Tooltip from '@material-ui/core/Tooltip'; +import IconButton from '@material-ui/core/IconButton'; +import Typography from '@material-ui/core/Typography'; +import EmailIcon from '@material-ui/icons/Email'; +import PhoneIcon from '@material-ui/icons/Phone'; +import { makeStyles } from '@material-ui/core/styles'; +import { OnCall } from '../../types'; +import { useApi } from '@backstage/core'; +import { ilertApiRef } from '../../api'; +import moment from 'moment'; + +const useStyles = makeStyles({ + listItemPrimary: { + fontWeight: 'bold', + }, + fistItemLine: { + position: 'absolute', + bottom: 0, + height: '50%', + left: 36, + }, + lastItemLine: { + position: 'absolute', + top: 0, + height: '50%', + left: 36, + }, + itemLine: { + position: 'absolute', + top: 0, + bottom: 0, + height: '100%', + left: 36, + }, +}); + +export const ILertCardOnCallItem = ({ + onCall, + fistItem = false, + lastItem = false, +}: { + onCall: OnCall; + fistItem?: boolean; + lastItem?: boolean; +}) => { + const ilertApi = useApi(ilertApiRef); + const classes = useStyles(); + + if (!onCall || !onCall.user) { + return null; + } + + const phoneNumber = ilertApi.getUserPhoneNumber(onCall.user); + const escalationRepeating = onCall.escalationPolicy.repeating; + const escalationSeconds = + onCall.escalationPolicy.escalationRules[onCall.escalationLevel - 1] + .escalationTimeout; + const escalationHoursOnly = Math.floor(escalationSeconds / 60); + const escalationMinutesOnly = escalationSeconds % 60; + + let escalationText = ''; + if (!lastItem || (lastItem && escalationRepeating)) { + escalationText = 'escalate'; + if (escalationSeconds === 0) { + escalationText += ' immediately'; + } else { + escalationText += ' after'; + if (escalationHoursOnly > 0) { + escalationText += ` ${escalationHoursOnly} ${ + escalationHoursOnly === 1 ? 'hour' : 'hours' + }`; + } + if (escalationMinutesOnly > 0 || escalationSeconds === 0) { + escalationText += ` ${escalationMinutesOnly} ${ + escalationMinutesOnly === 1 ? 'minute' : 'minutes' + }`; + } + } + } + + return ( + + {fistItem ? ( + + ) : null} + {lastItem ? ( + + ) : null} + {!fistItem && !lastItem ? ( + + ) : null} + + + {onCall.escalationLevel} + + + {onCall.schedule ? ( + + + {ilertApi.getUserInitials(onCall.user)} + + } + secondary={escalationText} + /> + + ) : ( + + {ilertApi.getUserInitials(onCall.user)} + + } + secondary={escalationText} + /> + )} + + {phoneNumber ? ( + + + + + + ) : null} + + + + + + + + ); +}; diff --git a/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx index 34601009ff..c84e0a7b8d 100644 --- a/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx +++ b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx @@ -44,9 +44,9 @@ const useStyles = makeStyles(() => ({ indicatorNext: { position: 'absolute', top: 'calc(40% - 10px)', - left: -8, - width: 16, - height: 16, + left: -6, + width: 12, + height: 12, background: '#92949c !important', borderRadius: '50%', }, @@ -54,11 +54,33 @@ const useStyles = makeStyles(() => ({ indicatorCurrent: { position: 'absolute', top: 'calc(40% - 10px)', - left: -8, - width: 16, - height: 16, + left: -6, + width: 12, + height: 12, background: '#ffb74d !important', + color: '#ffb74d !important', borderRadius: '50%', + '&::after': { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + borderRadius: '50%', + animation: '$ripple 1.2s infinite ease-in-out', + border: '1px solid currentColor', + content: '""', + }, + }, + '@keyframes ripple': { + '0%': { + transform: 'scale(.8)', + opacity: 1, + }, + '100%': { + transform: 'scale(2.4)', + opacity: 0, + }, }, beforeText: { diff --git a/plugins/ilert/src/hooks/useAlertSourceOnCalls.ts b/plugins/ilert/src/hooks/useAlertSourceOnCalls.ts new file mode 100644 index 0000000000..ac36c70097 --- /dev/null +++ b/plugins/ilert/src/hooks/useAlertSourceOnCalls.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ilertApiRef, UnauthorizedError } from '../api'; +import { useApi, errorApiRef } from '@backstage/core'; +import { useAsyncRetry } from 'react-use'; +import { AlertSource, OnCall } from '../types'; + +export const useAlertSourceOnCalls = (alertSource?: AlertSource | null) => { + const ilertApi = useApi(ilertApiRef); + const errorApi = useApi(errorApiRef); + + const [onCallsList, setOnCallsList] = React.useState([]); + const [isLoading, setIsLoading] = React.useState(false); + + const fetchAlertSourceOnCallsCall = async () => { + try { + if (!alertSource) { + return; + } + setIsLoading(true); + const data = await ilertApi.fetchAlertSourceOnCalls(alertSource); + setOnCallsList(data || []); + setIsLoading(false); + } catch (e) { + setIsLoading(false); + if (!(e instanceof UnauthorizedError)) { + errorApi.post(e); + } + throw e; + } + }; + + const { error, retry } = useAsyncRetry(fetchAlertSourceOnCallsCall, [ + alertSource, + ]); + + return [ + { + onCalls: onCallsList, + error, + isLoading, + }, + { + retry, + setIsLoading, + refetchAlertSourceOnCalls: fetchAlertSourceOnCallsCall, + }, + ] as const; +}; diff --git a/plugins/ilert/src/types.ts b/plugins/ilert/src/types.ts index a209b99533..d7746c9d9b 100644 --- a/plugins/ilert/src/types.ts +++ b/plugins/ilert/src/types.ts @@ -326,3 +326,12 @@ export interface IncidentActionHistory { actor: User; success: boolean; } + +export interface OnCall { + user: User; + escalationPolicy: EscalationPolicy; + schedule?: Schedule; + start: string; + end: string; + escalationLevel: number; +} From 7ddf3ef3b4f5c2d40c84f58081ae7a78c1460c39 Mon Sep 17 00:00:00 2001 From: yacut Date: Sun, 18 Apr 2021 05:03:39 +0200 Subject: [PATCH 12/95] add app config and refactor card for iLert Signed-off-by: yacut --- app-config.yaml | 8 ++++++++ .../src/components/ILertCard/ILertCardOnCall.tsx | 13 +++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 309fa28234..03097069e6 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -82,6 +82,14 @@ proxy: Authorization: $env: SENTRY_TOKEN + '/ilert': + target: https://api.ilert.com + allowedMethods: ['GET', 'POST', 'PUT'] + allowedHeaders: ['Authorization'] + headers: + Authorization: + $env: ILERT_AUTH_HEADER + organization: name: My Company diff --git a/plugins/ilert/src/components/ILertCard/ILertCardOnCall.tsx b/plugins/ilert/src/components/ILertCard/ILertCardOnCall.tsx index 9c347eeddc..8f28790375 100644 --- a/plugins/ilert/src/components/ILertCard/ILertCardOnCall.tsx +++ b/plugins/ilert/src/components/ILertCard/ILertCardOnCall.tsx @@ -55,11 +55,10 @@ export const ILertCardOnCall = ({ const repeatInfo = () => { if ( - !onCalls || - !onCalls.length || - !onCalls[onCalls.length - 1].escalationPolicy || - !onCalls[onCalls.length - 1].escalationPolicy.repeating || - !onCalls[onCalls.length - 1].escalationPolicy.frequency + !alertSource || + !alertSource.escalationPolicy || + !alertSource.escalationPolicy.repeating || + !alertSource.escalationPolicy.frequency ) { return null; } @@ -76,9 +75,7 @@ export const ILertCardOnCall = ({ color="textSecondary" className={classes.repeatText} > - {`Repeat ${ - onCalls[onCalls.length - 1].escalationPolicy.frequency - } times`} + {`Repeat ${alertSource.escalationPolicy.frequency} times`} } /> From 7d9739fe708a3e9169dfe23d4e0a159b5863d0d3 Mon Sep 17 00:00:00 2001 From: yacut Date: Tue, 20 Apr 2021 19:46:39 +0100 Subject: [PATCH 13/95] refactor ilert readme Signed-off-by: yacut --- microsite/data/plugins/ilert.yaml | 6 ++++-- plugins/ilert/README.md | 32 ++++++++++++++----------------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/microsite/data/plugins/ilert.yaml b/microsite/data/plugins/ilert.yaml index 99cfca01bc..172f005ed1 100644 --- a/microsite/data/plugins/ilert.yaml +++ b/microsite/data/plugins/ilert.yaml @@ -1,9 +1,9 @@ --- title: iLert author: iLert -authorUrl: https://github.com/iLert +authorUrl: https://ilert.com category: Monitoring -description: iLert is a platform for alerting, on-call management and uptime monitoring. +description: iLert is a platform for alerting, on-call management and uptime monitoring targeted at DevOps and IT teams. documentation: https://github.com/backstage/backstage/tree/master/plugins/ilert iconUrl: https://avatars.githubusercontent.com/u/13230510?s=200&v=4 npmPackageName: '@backstage/plugin-ilert' @@ -11,3 +11,5 @@ tags: - monitoring - errors - alerting + - uptime + - on-call diff --git a/plugins/ilert/README.md b/plugins/ilert/README.md index 0265f4172b..130883d6e0 100644 --- a/plugins/ilert/README.md +++ b/plugins/ilert/README.md @@ -2,19 +2,18 @@ ## Introduction -[iLert](https://www.ilert.com) is a platform for alerting, on-call management and uptime monitoring. It helps teams to reduce response times to critical incidents by extending monitoring tools with reliable alerting, automatic escalations, on-call schedules and other features to support the incident response process, such as [informing stakeholders](https://docs.ilert.com/getting-started/stakeholder-engagement) or creating tickets in external incident management tools. +[iLert](https://www.ilert.com) is a platform for alerting, on-call management and uptime monitoring. It helps teams to reduce response times to critical incidents by extending monitoring tools with reliable alerting, automatic escalations, on-call schedules and other features to support the incident response process, such as informing stakeholders or creating tickets in external incident management tools. ## Overview -This plugin displays iLert information about an entity such as if there are any active incidents, wo is on-call now and uptime monitor status. +This plugin gives an overview about ongoing iLert incidents, on-call and uptime monitor status. +See who is on-call, which incidents are active and trigger incidents directly from backstage for the configured alert sources. -There is also an easy way to trigger an incident directly to the person who is currently on-call. +In detail this plugin provides: -This plugin provides: - -- Information details about the persons on-call -- A way to override current person on-call -- A list of incidents +- Information details about the person on-call (all escalation levels of the current time) +- A way to override the current on-call person +- A list of active incidents - A way to trigger a new incident - A way to reassign/acknowledge/resolve an incident - A way to trigger an incident action @@ -30,8 +29,7 @@ Install the plugin: yarn add @backstage/plugin-ilert ``` -Then make sure to export the plugin in your app's [`plugins.ts`](https://github.com/backstage/backstage/blob/master/packages/app/src/plugins.ts) -to enable the plugin: +Then make sure to export the plugin in your app's [`plugins.ts`](https://github.com/backstage/backstage/blob/master/packages/app/src/plugins.ts) to enable the plugin: ```js export { plugin as ILert } from '@backstage/plugin-ilert'; @@ -53,15 +51,14 @@ import { } ``` -> To force iLert card for each entity just add the `` component, so an instruction card will appears if no integration key is set. +> To force an iLert card for each entity just add the `` component. An instruction card will appear if no integration key is set. -## Add iLert explorer to the App sidebar +## Add iLert explorer to the app sidebar -Modify your app routes in [`App.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/App.tsx) to include the Router component exported from the plugin, for example: +Modify your app routes in [`App.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/App.tsx) to include the Router component exported by the plugin - for example: ```tsx import { ILertPage } from '@backstage/plugin-ilert'; - // ... } /> @@ -69,11 +66,10 @@ import { ILertPage } from '@backstage/plugin-ilert'; ; ``` -Modify your sidebar in [`Root.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/components/Root/Root.tsx) to include the icon component exported from the plugin, for example: +Modify your sidebar in [`Root.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/components/Root/Root.tsx) to include the icon component exported by the plugin - for example: ```tsx import { ILertIcon } from '@backstage/plugin-ilert'; - // ... @@ -94,7 +90,7 @@ ilert: ## Providing the Authorization Header -In order to make the API calls, you need to provide a new proxy config which will redirect to the [iLert API](https://api.ilert.com/api-docs/) endpoint it needs an [Authorization Header](https://api.ilert.com/api-docs/#section/Authentication). +In order to make the API calls, you need to provide a new proxy config which will redirect to the [iLert API](https://api.ilert.com/api-docs/) endpoint. It needs an [Authorization Header](https://api.ilert.com/api-docs/#section/Authentication). Add the proxy configuration in `app-config.yaml` @@ -110,7 +106,7 @@ proxy: $env: ILERT_AUTH_HEADER ``` -Then start the backend passing the token as an environment variable: +Then start the backend, passing the token as environment variable: ```bash $ ILERT_AUTH_HEADER='Basic ' yarn start From 3896a88351a268e0c97e64334445d7f2e48ebafe Mon Sep 17 00:00:00 2001 From: yacut Date: Tue, 20 Apr 2021 20:02:00 +0100 Subject: [PATCH 14/95] fix ilert plugin link Signed-off-by: yacut --- microsite/data/plugins/ilert.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/data/plugins/ilert.yaml b/microsite/data/plugins/ilert.yaml index 172f005ed1..29cc4f6aa5 100644 --- a/microsite/data/plugins/ilert.yaml +++ b/microsite/data/plugins/ilert.yaml @@ -1,7 +1,7 @@ --- title: iLert author: iLert -authorUrl: https://ilert.com +authorUrl: https://www.ilert.com category: Monitoring description: iLert is a platform for alerting, on-call management and uptime monitoring targeted at DevOps and IT teams. documentation: https://github.com/backstage/backstage/tree/master/plugins/ilert From d5c0b6b6ef946cafc9b8fe5b00b1a717bad722be Mon Sep 17 00:00:00 2001 From: yacut Date: Wed, 28 Apr 2021 03:51:05 +0100 Subject: [PATCH 15/95] adjust ilert readme Signed-off-by: yacut --- plugins/ilert/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ilert/README.md b/plugins/ilert/README.md index 130883d6e0..87b67a2052 100644 --- a/plugins/ilert/README.md +++ b/plugins/ilert/README.md @@ -114,7 +114,7 @@ $ ILERT_AUTH_HEADER='Basic ' yarn start ## Integration Key -The information displayed for each entity is based on the [alert source integration key](https://docs.ilert.com/integrations/backstage). +The information displayed for each entity is based on the alert source integration key. ### Adding the integration key to the entity annotation From eedbe7452fbea9d28a839c0f670cd7fc911a9e5b Mon Sep 17 00:00:00 2001 From: yacut Date: Wed, 28 Apr 2021 03:58:56 +0100 Subject: [PATCH 16/95] Add iLert plugin Signed-off-by: Roman Frey roman@ilert.com Signed-off-by: yacut --- .changeset/blue-sloths-camp.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/blue-sloths-camp.md diff --git a/.changeset/blue-sloths-camp.md b/.changeset/blue-sloths-camp.md new file mode 100644 index 0000000000..f05326831f --- /dev/null +++ b/.changeset/blue-sloths-camp.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-ilert': minor +--- + +Add iLert plugin From 078e4457a215b76ea5da78c415d98b0951834aaa Mon Sep 17 00:00:00 2001 From: yacut Date: Mon, 3 May 2021 16:51:44 +0200 Subject: [PATCH 17/95] fix dev page for iLert plugin Signed-off-by: yacut --- plugins/ilert/dev/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/ilert/dev/index.tsx b/plugins/ilert/dev/index.tsx index c05e1da3d9..737666e650 100644 --- a/plugins/ilert/dev/index.tsx +++ b/plugins/ilert/dev/index.tsx @@ -16,12 +16,12 @@ import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; import { ilertPlugin } from '../src/plugin'; -import { IlertPage } from '../src'; +import { ILertPage } from '../src'; createDevApp() .registerPlugin(ilertPlugin) .addPage({ - element: , + element: , title: 'Root Page', }) .render(); From 9c0c10241c5235cb8bd94d0f6c1ca7555d56ada4 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Wed, 5 May 2021 15:25:53 +0200 Subject: [PATCH 18/95] export types Signed-off-by: Samira Mokaram --- plugins/kubernetes/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/kubernetes/src/index.ts b/plugins/kubernetes/src/index.ts index eca6b068f1..a8dd556b48 100644 --- a/plugins/kubernetes/src/index.ts +++ b/plugins/kubernetes/src/index.ts @@ -19,3 +19,5 @@ export { EntityKubernetesContent, } from './plugin'; export { Router } from './Router'; +export { kubernetesAuthProvidersApiRef } from './kubernetes-auth-provider/types'; +export { KubernetesAuthProviders } from './kubernetes-auth-provider/KubernetesAuthProviders'; From db8fdd64d6f722949de842858f936d52a532e1a9 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Wed, 5 May 2021 15:57:40 +0200 Subject: [PATCH 19/95] create changeset Signed-off-by: Samira Mokaram --- .changeset/large-penguins-yell.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/large-penguins-yell.md diff --git a/.changeset/large-penguins-yell.md b/.changeset/large-penguins-yell.md new file mode 100644 index 0000000000..9446374f47 --- /dev/null +++ b/.changeset/large-penguins-yell.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-kubernetes-backend': patch +--- + +Export types From 96e693a47512f839642570069679042f5d0b4831 Mon Sep 17 00:00:00 2001 From: Heather Lee Date: Wed, 5 May 2021 15:20:32 -0700 Subject: [PATCH 20/95] Update CatalogPage to take in columns Signed-off-by: Heather Lee --- packages/app/src/App.tsx | 14 +++ packages/catalog-model/src/index.ts | 2 +- packages/catalog-model/src/types.ts | 12 ++ .../components/CatalogPage/CatalogPage.tsx | 4 + .../components/CatalogTable/CatalogTable.tsx | 104 +++-------------- .../src/components/CatalogTable/columns.tsx | 110 ++++++++++++++++++ .../src/components/CatalogTable/types.ts | 27 +++++ 7 files changed, 184 insertions(+), 89 deletions(-) create mode 100644 plugins/catalog/src/components/CatalogTable/columns.tsx create mode 100644 plugins/catalog/src/components/CatalogTable/types.ts diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index c5e5033973..1fb1125518 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -20,6 +20,7 @@ import { FlatRoutes, OAuthRequestDialog, SignInPage, + TableColumn, } from '@backstage/core'; import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs'; import { @@ -56,6 +57,8 @@ import { Root } from './components/Root'; import { entityPage } from './components/catalog/EntityPage'; import { providers } from './identityProviders'; import * as plugins from './plugins'; +import { EntityRow } from '../../catalog-model/src'; +import { EntityRefLink } from '@backstage/plugin-catalog-react'; const app = createApp({ apis, @@ -95,6 +98,17 @@ const app = createApp({ const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); +const columns: TableColumn[] = [ + { + title: 'Name', + field: 'resolved.name', + highlight: true, + render: ({ entity }) => ( + + ), + }, +]; + const routes = ( diff --git a/packages/catalog-model/src/index.ts b/packages/catalog-model/src/index.ts index 976b5f6148..825e765453 100644 --- a/packages/catalog-model/src/index.ts +++ b/packages/catalog-model/src/index.ts @@ -18,5 +18,5 @@ export * from './entity'; export { EntityPolicies } from './EntityPolicies'; export * from './kinds'; export * from './location'; -export type { EntityName, EntityRef, JSONSchema } from './types'; +export type { EntityName, EntityRef, JSONSchema, EntityRow } from './types'; export * from './validation'; diff --git a/packages/catalog-model/src/types.ts b/packages/catalog-model/src/types.ts index edac03466d..963e57919d 100644 --- a/packages/catalog-model/src/types.ts +++ b/packages/catalog-model/src/types.ts @@ -16,6 +16,7 @@ import { JsonValue } from '@backstage/config'; import { JSONSchema7 } from 'json-schema'; +import { Entity } from './entity'; export type JSONSchema = JSONSchema7 & { [key in string]?: JsonValue }; @@ -44,3 +45,14 @@ export type EntityRef = namespace?: string; name: string; }; + +export type EntityRow = { + entity: Entity; + resolved: { + name: string; + partOfSystemRelationTitle?: string; + partOfSystemRelations: EntityName[]; + ownedByRelationsTitle?: string; + ownedByRelations: EntityName[]; + }; +}; diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index fd3d63edd7..2484b5cd29 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -20,9 +20,11 @@ import { ContentHeader, errorApiRef, SupportButton, + TableColumn, useApi, useRouteRef, } from '@backstage/core'; +import { EntityRow } from '@backstage/catalog-model'; import { catalogApiRef, isOwnerOf, @@ -61,6 +63,7 @@ const useStyles = makeStyles(theme => ({ export type CatalogPageProps = { initiallySelectedFilter?: string; + columns?: TableColumn[]; }; const CatalogPageContents = (props: CatalogPageProps) => { @@ -210,6 +213,7 @@ const CatalogPageContents = (props: CatalogPageProps) => { [] = [ - { - title: 'Name', - field: 'resolved.name', - highlight: true, - render: ({ entity }) => ( - - ), - }, - { - title: 'System', - field: 'resolved.partOfSystemRelationTitle', - render: ({ resolved }) => ( - - ), - }, - { - title: 'Owner', - field: 'resolved.ownedByRelationsTitle', - render: ({ resolved }) => ( - - ), - }, - { - title: 'Type', - field: 'entity.spec.type', - hidden: true, - }, - { - title: 'Lifecycle', - field: 'entity.spec.lifecycle', - }, - { - title: 'Description', - field: 'entity.metadata.description', - render: ({ entity }) => ( - - ), - width: 'auto', - }, - { - title: 'Tags', - field: 'entity.metadata.tags', - cellStyle: { - padding: '0px 16px 0px 20px', - }, - render: ({ entity }) => ( - <> - {entity.metadata.tags && - entity.metadata.tags.map(t => ( - - ))} - - ), - }, +const defaultColumns: TableColumn[] = [ + columnFactories.createNameColumn(), + columnFactories.createSystemColumn(), + columnFactories.createOwnerColumn(), + columnFactories.createSpecTypeColumn(), + columnFactories.createSpecLifecycleColumn(), + columnFactories.createMetadataDescriptionColumn(), + columnFactories.createTagsColumn(), ]; type CatalogTableProps = { @@ -136,6 +60,7 @@ type CatalogTableProps = { loading: boolean; error?: any; view?: string; + columns?: TableColumn[]; }; export const CatalogTable = ({ @@ -144,6 +69,7 @@ export const CatalogTable = ({ error, titlePreamble, view, + columns, }: CatalogTableProps) => { const { isStarredEntity, toggleStarredEntity } = useStarredEntities(); @@ -224,7 +150,7 @@ export const CatalogTable = ({ }; }); - const typeColumn = columns.find(c => c.title === 'Type'); + const typeColumn = defaultColumns.find(c => c.title === 'Type'); if (typeColumn) { typeColumn.hidden = view !== 'Other'; } @@ -232,7 +158,7 @@ export const CatalogTable = ({ return ( isLoading={loading} - columns={columns} + columns={columns || defaultColumns} options={{ paging: true, pageSize: 20, @@ -248,3 +174,5 @@ export const CatalogTable = ({ /> ); }; + +CatalogTable.columns = columnFactories; diff --git a/plugins/catalog/src/components/CatalogTable/columns.tsx b/plugins/catalog/src/components/CatalogTable/columns.tsx new file mode 100644 index 0000000000..4f65e4b0c7 --- /dev/null +++ b/plugins/catalog/src/components/CatalogTable/columns.tsx @@ -0,0 +1,110 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { EntityRefLink, EntityRefLinks } from '@backstage/plugin-catalog-react'; +import { OverflowTooltip, TableColumn } from '@backstage/core'; +import { Chip } from '@material-ui/core'; +import { EntityRow } from './types'; + +export function createNameColumn(): TableColumn { + return { + title: 'Name', + field: 'resolved.name', + highlight: true, + render: ({ entity }) => ( + + ), + }; +} + +export function createSystemColumn(): TableColumn { + return { + title: 'System', + field: 'resolved.partOfSystemRelationTitle', + render: ({ resolved }) => ( + + ), + }; +} + +export function createOwnerColumn(): TableColumn { + return { + title: 'Owner', + field: 'resolved.ownedByRelationsTitle', + render: ({ resolved }) => ( + + ), + }; +} + +export function createSpecTypeColumn(): TableColumn { + return { + title: 'Type', + field: 'entity.spec.type', + hidden: true, + }; +} + +export function createSpecLifecycleColumn(): TableColumn { + return { + title: 'Lifecycle', + field: 'entity.spec.lifecycle', + }; +} + +export function createMetadataDescriptionColumn(): TableColumn { + return { + title: 'Description', + field: 'entity.metadata.description', + render: ({ entity }) => ( + + ), + width: 'auto', + }; +} + +export function createTagsColumn(): TableColumn { + return { + title: 'Tags', + field: 'entity.metadata.tags', + cellStyle: { + padding: '0px 16px 0px 20px', + }, + render: ({ entity }) => ( + <> + {entity.metadata.tags && + entity.metadata.tags.map(t => ( + + ))} + + ), + }; +} diff --git a/plugins/catalog/src/components/CatalogTable/types.ts b/plugins/catalog/src/components/CatalogTable/types.ts new file mode 100644 index 0000000000..b1a0ce2739 --- /dev/null +++ b/plugins/catalog/src/components/CatalogTable/types.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { Entity, EntityName } from '@backstage/catalog-model'; + +export type EntityRow = { + entity: Entity; + resolved: { + name: string; + partOfSystemRelationTitle?: string; + partOfSystemRelations: EntityName[]; + ownedByRelationsTitle?: string; + ownedByRelations: EntityName[]; + }; +}; From b209b23e60676aa29ada30a22cd05e0f6b0ab92e Mon Sep 17 00:00:00 2001 From: Heather Lee Date: Wed, 5 May 2021 15:31:29 -0700 Subject: [PATCH 21/95] Update the export to work properly for CatalogTable Signed-off-by: Heather Lee --- .../catalog/src/components/CatalogTable/index.ts | 16 ++++++++++++++++ plugins/catalog/src/index.ts | 1 + 2 files changed, 17 insertions(+) create mode 100644 plugins/catalog/src/components/CatalogTable/index.ts diff --git a/plugins/catalog/src/components/CatalogTable/index.ts b/plugins/catalog/src/components/CatalogTable/index.ts new file mode 100644 index 0000000000..9148a22a41 --- /dev/null +++ b/plugins/catalog/src/components/CatalogTable/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { CatalogTable } from './CatalogTable'; diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index 7ab7b95dd7..fb269fc484 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -17,6 +17,7 @@ export { AboutCard } from './components/AboutCard'; export { EntityLayout } from './components/EntityLayout'; export { EntityPageLayout } from './components/EntityPageLayout'; +export { CatalogTable } from './components/CatalogTable'; export * from './components/EntitySwitch'; export { Router } from './components/Router'; export { From 75885e5cff010807ccea66cf17a760e94197b004 Mon Sep 17 00:00:00 2001 From: Heather Lee Date: Wed, 5 May 2021 15:37:08 -0700 Subject: [PATCH 22/95] Reverting unnecessary changes Signed-off-by: Heather Lee --- packages/app/src/App.tsx | 14 -------------- packages/catalog-model/src/types.ts | 12 ------------ 2 files changed, 26 deletions(-) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 1fb1125518..c5e5033973 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -20,7 +20,6 @@ import { FlatRoutes, OAuthRequestDialog, SignInPage, - TableColumn, } from '@backstage/core'; import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs'; import { @@ -57,8 +56,6 @@ import { Root } from './components/Root'; import { entityPage } from './components/catalog/EntityPage'; import { providers } from './identityProviders'; import * as plugins from './plugins'; -import { EntityRow } from '../../catalog-model/src'; -import { EntityRefLink } from '@backstage/plugin-catalog-react'; const app = createApp({ apis, @@ -98,17 +95,6 @@ const app = createApp({ const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); -const columns: TableColumn[] = [ - { - title: 'Name', - field: 'resolved.name', - highlight: true, - render: ({ entity }) => ( - - ), - }, -]; - const routes = ( diff --git a/packages/catalog-model/src/types.ts b/packages/catalog-model/src/types.ts index 963e57919d..edac03466d 100644 --- a/packages/catalog-model/src/types.ts +++ b/packages/catalog-model/src/types.ts @@ -16,7 +16,6 @@ import { JsonValue } from '@backstage/config'; import { JSONSchema7 } from 'json-schema'; -import { Entity } from './entity'; export type JSONSchema = JSONSchema7 & { [key in string]?: JsonValue }; @@ -45,14 +44,3 @@ export type EntityRef = namespace?: string; name: string; }; - -export type EntityRow = { - entity: Entity; - resolved: { - name: string; - partOfSystemRelationTitle?: string; - partOfSystemRelations: EntityName[]; - ownedByRelationsTitle?: string; - ownedByRelations: EntityName[]; - }; -}; From 6c5af91993a1ab1f95bdb102588714b483788c44 Mon Sep 17 00:00:00 2001 From: Heather Lee Date: Wed, 5 May 2021 16:01:45 -0700 Subject: [PATCH 23/95] Fix references to type Signed-off-by: Heather Lee --- plugins/catalog/src/components/CatalogPage/CatalogPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 2484b5cd29..bbb26d7be7 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -24,7 +24,6 @@ import { useApi, useRouteRef, } from '@backstage/core'; -import { EntityRow } from '@backstage/catalog-model'; import { catalogApiRef, isOwnerOf, @@ -44,6 +43,7 @@ import { CatalogFilterType, } from '../CatalogFilter/CatalogFilter'; import { CatalogTable } from '../CatalogTable/CatalogTable'; +import { EntityRow } from '../CatalogTable/types'; import { ResultsFilter } from '../ResultsFilter/ResultsFilter'; import { useOwnUser } from '../useOwnUser'; import CatalogLayout from './CatalogLayout'; From d7ef50d9647d617dc86443b520fecba3fd446736 Mon Sep 17 00:00:00 2001 From: Heather Lee Date: Wed, 5 May 2021 16:09:26 -0700 Subject: [PATCH 24/95] Remove extra definition of EntityRow from catalog-model Signed-off-by: Heather Lee --- packages/catalog-model/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/catalog-model/src/index.ts b/packages/catalog-model/src/index.ts index 825e765453..976b5f6148 100644 --- a/packages/catalog-model/src/index.ts +++ b/packages/catalog-model/src/index.ts @@ -18,5 +18,5 @@ export * from './entity'; export { EntityPolicies } from './EntityPolicies'; export * from './kinds'; export * from './location'; -export type { EntityName, EntityRef, JSONSchema, EntityRow } from './types'; +export type { EntityName, EntityRef, JSONSchema } from './types'; export * from './validation'; From 129bb3f8c5c333b6024af78e060d69a9b462b494 Mon Sep 17 00:00:00 2001 From: Heather Lee Date: Wed, 5 May 2021 17:02:41 -0700 Subject: [PATCH 25/95] Adjust type column to render as a result of overridden columns, or key off defaultColumns if nothing is passed in Signed-off-by: Heather Lee --- plugins/catalog/src/components/CatalogTable/CatalogTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index d8e152ed75..03dece50fe 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -150,7 +150,7 @@ export const CatalogTable = ({ }; }); - const typeColumn = defaultColumns.find(c => c.title === 'Type'); + const typeColumn = (columns || defaultColumns).find(c => c.title === 'Type'); if (typeColumn) { typeColumn.hidden = view !== 'Other'; } From 9a207f052fa25b74ce4719f079b9ecbe8097ceeb Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 6 May 2021 15:55:26 +0200 Subject: [PATCH 26/95] Support integrations in `GithubOrgReaderProcessor` Signed-off-by: Oliver Sand --- .changeset/polite-plants-exercise.md | 28 ++++++ plugins/catalog-backend/config.d.ts | 2 + .../GithubOrgReaderProcessor.test.ts | 40 +++++++- .../processors/GithubOrgReaderProcessor.ts | 94 +++++++++++++++---- .../processors/github/config.test.ts | 10 -- .../src/ingestion/processors/github/config.ts | 9 -- 6 files changed, 145 insertions(+), 38 deletions(-) create mode 100644 .changeset/polite-plants-exercise.md diff --git a/.changeset/polite-plants-exercise.md b/.changeset/polite-plants-exercise.md new file mode 100644 index 0000000000..42df6875e6 --- /dev/null +++ b/.changeset/polite-plants-exercise.md @@ -0,0 +1,28 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Port `GithubOrgReaderProcessor` to support configuration via +[`integrations`](https://backstage.io/docs/integrations/github/locations) in +addition to [`catalog.processors.githubOrg.providers`](https://backstage.io/docs/integrations/github/org#configuration). +The `integrations` package supports authentication with both personal access +tokens and GitHub apps. + +This deprecates the `catalog.processors.githubOrg.providers` configuration. If +you still have a configuration for providers the processor keeps working, but +consider moving the [`integrations` configuration](https://backstage.io/docs/integrations/github/locations) +as the providers will be removed in the future. You might need to allow +additional scopes for the credentials. + +If you want to stay with providers for now, this introduces a small breaking +change, previously if you had no provider configured, one for GitHub was automatically added. To keep the behavior, add a +default provider for GitHub: + +```yaml +catalog: + processors: + githubOrg: + providers: + - target: https://github.com + apiBaseUrl: https://api.github.com +``` diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index 564dd7511e..a913473ff8 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -114,6 +114,8 @@ export interface Config { processors?: { /** * GithubOrgReaderProcessor configuration + * + * @deprecated Configure an GitHub integration instead. */ githubOrg?: { /** diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.test.ts index 19a08d219f..9b724def28 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.test.ts @@ -16,6 +16,11 @@ import { getVoidLogger } from '@backstage/backend-common'; import { LocationSpec } from '@backstage/catalog-model'; +import { + GitHubIntegration, + ScmIntegrations, + ScmIntegrationsGroup, +} from '@backstage/integration'; import { GithubOrgReaderProcessor, parseUrl } from './GithubOrgReaderProcessor'; describe('GithubOrgReaderProcessor', () => { @@ -29,6 +34,20 @@ describe('GithubOrgReaderProcessor', () => { }); describe('implementation', () => { + let integrations: ScmIntegrations; + let github: jest.Mocked>; + + beforeEach(() => { + github = { + byHost: jest.fn(), + byUrl: jest.fn(), + list: jest.fn(), + }; + integrations = ({ + github, + } as Partial) as ScmIntegrations; + }); + it('rejects unknown types', async () => { const processor = new GithubOrgReaderProcessor({ providers: [ @@ -37,6 +56,7 @@ describe('GithubOrgReaderProcessor', () => { apiBaseUrl: 'https://api.github.com', }, ], + integrations, logger: getVoidLogger(), }); const location: LocationSpec = { @@ -48,7 +68,7 @@ describe('GithubOrgReaderProcessor', () => { ).resolves.toBeFalsy(); }); - it('rejects unknown targets', async () => { + it('rejects unknown targets from providers', async () => { const processor = new GithubOrgReaderProcessor({ providers: [ { @@ -56,6 +76,24 @@ describe('GithubOrgReaderProcessor', () => { apiBaseUrl: 'https://api.github.com', }, ], + integrations, + logger: getVoidLogger(), + }); + const location: LocationSpec = { + type: 'github-org', + target: 'https://not.github.com/apa', + }; + await expect( + processor.readLocation(location, false, () => {}), + ).rejects.toThrow( + /There is no GitHub Org provider that matches https:\/\/not.github.com\/apa/, + ); + }); + + it('rejects unknown targets from integrations', async () => { + const processor = new GithubOrgReaderProcessor({ + providers: [], + integrations, logger: getVoidLogger(), }); const location: LocationSpec = { diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts index 6a83d705f6..db61886b89 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts @@ -16,6 +16,10 @@ import { LocationSpec } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; +import { + GithubCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; import { graphql } from '@octokit/graphql'; import { Logger } from 'winston'; import { @@ -28,22 +32,33 @@ import * as results from './results'; import { CatalogProcessor, CatalogProcessorEmit } from './types'; import { buildOrgHierarchy } from './util/org'; +type GraphQL = typeof graphql; + /** * Extracts teams and users out of a GitHub org. */ export class GithubOrgReaderProcessor implements CatalogProcessor { private readonly providers: ProviderConfig[]; + private readonly integrations: ScmIntegrations; private readonly logger: Logger; static fromConfig(config: Config, options: { logger: Logger }) { + const integrations = ScmIntegrations.fromConfig(config); + return new GithubOrgReaderProcessor({ ...options, providers: readGithubConfig(config), + integrations, }); } - constructor(options: { providers: ProviderConfig[]; logger: Logger }) { + constructor(options: { + providers: ProviderConfig[]; + integrations: ScmIntegrations; + logger: Logger; + }) { this.providers = options.providers; + this.integrations = options.integrations; this.logger = options.logger; } @@ -56,24 +71,8 @@ export class GithubOrgReaderProcessor implements CatalogProcessor { return false; } - const provider = this.providers.find(p => - location.target.startsWith(`${p.target}/`), - ); - if (!provider) { - throw new Error( - `There is no GitHub Org provider that matches ${location.target}. Please add a configuration entry for it under catalog.processors.githubOrg.providers.`, - ); - } - + const client = await this.createClient(location.target); const { org } = parseUrl(location.target); - const client = !provider.token - ? graphql - : graphql.defaults({ - baseUrl: provider.apiBaseUrl, - headers: { - authorization: `token ${provider.token}`, - }, - }); // Read out all of the raw data const startTimestamp = Date.now(); @@ -112,6 +111,65 @@ export class GithubOrgReaderProcessor implements CatalogProcessor { return true; } + + private async createClient(orgUrl: string): Promise { + let client = this.createClientFromProvider(orgUrl); + + if (!client) { + client = await this.createClientFromIntegrations(orgUrl); + } + + if (!client) { + throw new Error( + `There is no GitHub Org provider that matches ${orgUrl}. Please add a configuration for an integration or add an entry for it under catalog.processors.githubOrg.providers.`, + ); + } + + return client; + } + + private createClientFromProvider(orgUrl: string): GraphQL | undefined { + const provider = this.providers.find(p => + orgUrl.startsWith(`${p.target}/`), + ); + + if (!provider) { + return undefined; + } + + this.logger.warn( + 'GithubOrgReaderProcessor uses provider defined in catalog.processors.githubOrg.providers, migrate to integrations instead.', + ); + + return !provider.token + ? graphql + : graphql.defaults({ + baseUrl: provider.apiBaseUrl, + headers: { + authorization: `token ${provider.token}`, + }, + }); + } + + private async createClientFromIntegrations( + orgUrl: string, + ): Promise { + const gitHubConfig = this.integrations.github.byUrl(orgUrl)?.config; + if (!gitHubConfig) { + return undefined; + } + + const credentialsProvider = GithubCredentialsProvider.create(gitHubConfig); + + const { headers } = await credentialsProvider.getCredentials({ + url: orgUrl, + }); + + return graphql.defaults({ + baseUrl: gitHubConfig.apiBaseUrl, + headers, + }); + } } /* diff --git a/plugins/catalog-backend/src/ingestion/processors/github/config.test.ts b/plugins/catalog-backend/src/ingestion/processors/github/config.test.ts index 14f3caa42c..b39a7a7ff9 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/config.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/github/config.test.ts @@ -27,16 +27,6 @@ describe('config', () => { }); } - it('adds a default GitHub entry when missing', () => { - const output = readGithubConfig(config([])); - expect(output).toEqual([ - { - target: 'https://github.com', - apiBaseUrl: 'https://api.github.com', - }, - ]); - }); - it('injects the correct GitHub API base URL when missing', () => { const output = readGithubConfig( config([{ target: 'https://github.com' }]), diff --git a/plugins/catalog-backend/src/ingestion/processors/github/config.ts b/plugins/catalog-backend/src/ingestion/processors/github/config.ts index 88f2f96218..7b16448b9e 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/config.ts +++ b/plugins/catalog-backend/src/ingestion/processors/github/config.ts @@ -71,14 +71,5 @@ export function readGithubConfig(config: Config): ProviderConfig[] { providers.push({ target, apiBaseUrl, token }); } - // If no explicit github.com provider was added, put one in the list as - // a convenience - if (!providers.some(p => p.target === 'https://github.com')) { - providers.push({ - target: 'https://github.com', - apiBaseUrl: 'https://api.github.com', - }); - } - return providers; } From ca1e981945926e370e9c8488a18e3e79f05b5a61 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 7 May 2021 09:42:45 +0200 Subject: [PATCH 27/95] Fix typos Signed-off-by: Oliver Sand --- .changeset/polite-plants-exercise.md | 2 +- plugins/catalog-backend/config.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/polite-plants-exercise.md b/.changeset/polite-plants-exercise.md index 42df6875e6..ac4a8d1403 100644 --- a/.changeset/polite-plants-exercise.md +++ b/.changeset/polite-plants-exercise.md @@ -10,7 +10,7 @@ tokens and GitHub apps. This deprecates the `catalog.processors.githubOrg.providers` configuration. If you still have a configuration for providers the processor keeps working, but -consider moving the [`integrations` configuration](https://backstage.io/docs/integrations/github/locations) +consider moving to the [`integrations` configuration](https://backstage.io/docs/integrations/github/locations) as the providers will be removed in the future. You might need to allow additional scopes for the credentials. diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index a913473ff8..20fcd975cf 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -115,7 +115,7 @@ export interface Config { /** * GithubOrgReaderProcessor configuration * - * @deprecated Configure an GitHub integration instead. + * @deprecated Configure a GitHub integration instead. */ githubOrg?: { /** From 751bd6d3e8540580165661b43f72e357137c039e Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 7 May 2021 09:44:27 +0200 Subject: [PATCH 28/95] Add link in warn message Signed-off-by: Oliver Sand --- .../src/ingestion/processors/GithubOrgReaderProcessor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts index db61886b89..5840daed74 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts @@ -138,7 +138,7 @@ export class GithubOrgReaderProcessor implements CatalogProcessor { } this.logger.warn( - 'GithubOrgReaderProcessor uses provider defined in catalog.processors.githubOrg.providers, migrate to integrations instead.', + 'GithubOrgReaderProcessor uses provider defined in catalog.processors.githubOrg.providers, migrate to integrations instead. See https://backstage.io/docs/integrations/github/locations', ); return !provider.token From b4ea71e2193b1811246eaa79e147d3b7212fda46 Mon Sep 17 00:00:00 2001 From: yacut Date: Wed, 5 May 2021 19:51:56 +0200 Subject: [PATCH 29/95] upgrade deps for iLert plugin Signed-off-by: yacut Signed-off-by: yacut --- plugins/ilert/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index b31af34753..9a16960233 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -21,9 +21,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.6", - "@backstage/core": "^0.7.4", + "@backstage/core": "^0.7.7", "@backstage/plugin-catalog-react": "^0.1.4", - "@backstage/theme": "^0.2.5", + "@backstage/theme": "^0.2.6", "@date-io/date-fns": "1.x", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -37,7 +37,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.7", + "@backstage/cli": "^0.6.9", "@backstage/dev-utils": "^0.1.13", "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", From d27ec62d82ab8470c651090e873e41e9ffdd4906 Mon Sep 17 00:00:00 2001 From: Roman Frey Date: Wed, 5 May 2021 21:59:57 +0100 Subject: [PATCH 30/95] Update app-config.yaml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: yacut --- app-config.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 3187feee63..b1c915f85a 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -86,8 +86,7 @@ proxy: allowedMethods: ['GET', 'POST', 'PUT'] allowedHeaders: ['Authorization'] headers: - Authorization: - $env: ILERT_AUTH_HEADER + Authorization: ${ILERT_AUTH_HEADER} organization: name: My Company From 3b73a5ce9c1fab4bf441dc69a2bc30c22b319109 Mon Sep 17 00:00:00 2001 From: Roman Frey Date: Wed, 5 May 2021 22:00:37 +0100 Subject: [PATCH 31/95] Update plugins/ilert/README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: yacut --- plugins/ilert/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/ilert/README.md b/plugins/ilert/README.md index 87b67a2052..c6b7a94731 100644 --- a/plugins/ilert/README.md +++ b/plugins/ilert/README.md @@ -26,6 +26,8 @@ In detail this plugin provides: Install the plugin: ```bash +# From the Backstage repository root +cd packages/app yarn add @backstage/plugin-ilert ``` From 40ce1954f12b05da3b747e224bb1034d3817c521 Mon Sep 17 00:00:00 2001 From: Roman Frey Date: Wed, 5 May 2021 22:04:34 +0100 Subject: [PATCH 32/95] Update plugins/ilert/src/constants.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: yacut --- plugins/ilert/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ilert/src/constants.ts b/plugins/ilert/src/constants.ts index edaa06b293..bba644785f 100644 --- a/plugins/ilert/src/constants.ts +++ b/plugins/ilert/src/constants.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export const ILERT_INTEGRATION_KEY = 'ilert.com/integration-key'; +export const ILERT_INTEGRATION_KEY_ANNOTATION = 'ilert.com/integration-key'; From 78a71f46ae91187c9ac25d1c31120264cb962c1b Mon Sep 17 00:00:00 2001 From: yacut Date: Fri, 7 May 2021 11:05:20 +0200 Subject: [PATCH 33/95] fix issues after first review for iLert plugin Signed-off-by: yacut --- .changeset/blue-sloths-camp.md | 5 - plugins/ilert/README.md | 38 +-- plugins/ilert/config.d.ts | 31 +++ plugins/ilert/package.json | 14 +- plugins/ilert/src/api/client.ts | 230 +++++++----------- plugins/ilert/src/api/index.ts | 2 +- plugins/ilert/src/api/types.ts | 7 +- .../AlertSource/AlertSourceLink.tsx | 5 +- .../EscalationPolicy/EscalationPolicyLink.tsx | 5 +- .../src/components/ILertCard/ILertCard.tsx | 16 +- .../ILertCard/ILertCardEmptyState.tsx | 6 +- .../ILertCard/ILertCardOnCallItem.tsx | 6 +- .../Incident/IncidentActionsMenu.tsx | 17 +- .../src/components/Incident/IncidentLink.tsx | 5 +- .../components/Incident/IncidentNewModal.tsx | 8 +- .../IncidentsPage/IncidentsPage.tsx | 24 +- .../IncidentsPage/IncidentsTable.tsx | 29 ++- .../OnCallSchedulesGrid.tsx | 5 +- .../OnCallSchedulesPage.tsx | 24 +- .../OnCallSchedulesPage/OnCallShiftItem.tsx | 8 +- .../components/Shift/ShiftOverrideModal.tsx | 8 +- .../UptimeMonitorActionsMenu.tsx | 9 +- .../UptimeMonitor/UptimeMonitorLink.tsx | 5 +- .../UptimeMonitorsPage/UptimeMonitorsPage.tsx | 24 +- .../UptimeMonitorsTable.tsx | 17 +- plugins/ilert/src/hooks/index.ts | 9 +- plugins/ilert/src/hooks/useAlertSource.ts | 7 +- .../ilert/src/hooks/useAlertSourceOnCalls.ts | 5 +- plugins/ilert/src/hooks/useAssignIncident.ts | 5 +- plugins/ilert/src/hooks/useIncidentActions.ts | 5 +- plugins/ilert/src/hooks/useIncidents.ts | 12 +- plugins/ilert/src/hooks/useNewIncident.ts | 5 +- plugins/ilert/src/hooks/useOnCallSchedules.ts | 5 +- plugins/ilert/src/hooks/useShiftOverride.ts | 5 +- plugins/ilert/src/hooks/useUptimeMonitors.ts | 5 +- 35 files changed, 312 insertions(+), 299 deletions(-) delete mode 100644 .changeset/blue-sloths-camp.md create mode 100644 plugins/ilert/config.d.ts diff --git a/.changeset/blue-sloths-camp.md b/.changeset/blue-sloths-camp.md deleted file mode 100644 index f05326831f..0000000000 --- a/.changeset/blue-sloths-camp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-ilert': minor ---- - -Add iLert plugin diff --git a/plugins/ilert/README.md b/plugins/ilert/README.md index c6b7a94731..da019d6bb6 100644 --- a/plugins/ilert/README.md +++ b/plugins/ilert/README.md @@ -31,12 +31,6 @@ cd packages/app yarn add @backstage/plugin-ilert ``` -Then make sure to export the plugin in your app's [`plugins.ts`](https://github.com/backstage/backstage/blob/master/packages/app/src/plugins.ts) to enable the plugin: - -```js -export { plugin as ILert } from '@backstage/plugin-ilert'; -``` - Add it to the `EntityPage.tsx`: ```ts @@ -44,13 +38,16 @@ import { isPluginApplicableToEntity as isILertAvailable, EntityILertCard, } from '@backstage/plugin-ilert'; -{ - isILertAvailable(entity) && ( - + +// ... + + + - ); -} + +; +// ... ``` > To force an iLert card for each entity just add the `` component. An instruction card will appear if no integration key is set. @@ -61,11 +58,11 @@ Modify your app routes in [`App.tsx`](https://github.com/backstage/backstage/blo ```tsx import { ILertPage } from '@backstage/plugin-ilert'; - + // ... } /> // ... -; +; ``` Modify your sidebar in [`Root.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/components/Root/Root.tsx) to include the icon component exported by the plugin - for example: @@ -104,14 +101,13 @@ proxy: allowedMethods: ['GET', 'POST', 'PUT'] allowedHeaders: ['Authorization'] headers: - Authorization: - $env: ILERT_AUTH_HEADER + Authorization: ${ILERT_AUTH_HEADER} ``` -Then start the backend, passing the token as environment variable: +Then start the backend, passing the authorization header (bearer token or basic auth) as environment variable: ```bash -$ ILERT_AUTH_HEADER='Basic ' yarn start +$ ILERT_AUTH_HEADER='' yarn start ``` ## Integration Key @@ -123,6 +119,10 @@ The information displayed for each entity is based on the alert source integrati If you want to use this plugin for an entity, you need to label it with the below annotation: ```yml -annotations: - ilert.com/integration-key: [INTEGRATION_KEY] +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: example + annotations: + ilert.com/integration-key: [INTEGRATION_KEY] ``` diff --git a/plugins/ilert/config.d.ts b/plugins/ilert/config.d.ts new file mode 100644 index 0000000000..52712454e8 --- /dev/null +++ b/plugins/ilert/config.d.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { + ilert: { + /** + * Domain used by users to access iLert web UI. + * Example: https://my-app.ilert.com/ + * @visibility frontend + */ + baseUrl: string; + + /** + * Path to use for requests via the proxy, defaults to /ilert/api + * @visibility frontend + */ + proxyPath?: string; + }; +} diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index 9a16960233..31eff40dee 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -22,16 +22,16 @@ "dependencies": { "@backstage/catalog-model": "^0.7.6", "@backstage/core": "^0.7.7", + "@backstage/errors": "^0.1.1", "@backstage/plugin-catalog-react": "^0.1.4", "@backstage/theme": "^0.2.6", - "@date-io/date-fns": "1.x", + "@date-io/luxon": "1.x", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "@material-ui/pickers": "^3.3.10", - "date-fns": "^2.20.2", - "moment": "^2.29.1", - "moment-timezone": "^0.5.33", + "humanize-duration": "^3.26.0", + "luxon": "^1.26.0", "react": "^16.13.1", "react-dom": "^16.13.1", "react-use": "^15.3.3" @@ -49,6 +49,8 @@ "msw": "^0.21.2" }, "files": [ - "dist" - ] + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" } diff --git a/plugins/ilert/src/api/client.ts b/plugins/ilert/src/api/client.ts index f3c263e9e7..e635d76b99 100644 --- a/plugins/ilert/src/api/client.ts +++ b/plugins/ilert/src/api/client.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { ConfigApi, createApiRef, DiscoveryApi } from '@backstage/core'; +import { AuthenticationError, ResponseError } from '@backstage/errors'; import { AlertSource, EscalationPolicy, @@ -32,8 +33,7 @@ import { Options, EventRequest, } from './types'; -import moment from 'moment'; -import momentTimezone from 'moment-timezone'; +import { DateTime as dt } from 'luxon'; export const ilertApiRef = createApiRef({ id: 'plugin.ilert.service', @@ -41,7 +41,10 @@ export const ilertApiRef = createApiRef({ }); const DEFAULT_PROXY_PATH = '/ilert'; -export class UnauthorizedError extends Error {} +const JSON_HEADERS = { + 'Content-Type': 'application/json', + Accept: 'application/json', +}; export class ILertClient implements ILertApi { private readonly discoveryApi: DiscoveryApi; @@ -55,14 +58,15 @@ export class ILertClient implements ILertApi { return new ILertClient({ discoveryApi: discoveryApi, baseUrl, - proxyPath: configApi.getOptionalString('ilert.proxyPath'), + proxyPath: + configApi.getOptionalString('ilert.proxyPath') ?? DEFAULT_PROXY_PATH, }); } constructor(opts: Options) { this.discoveryApi = opts.discoveryApi; this.baseUrl = opts.baseUrl; - this.proxyPath = opts.proxyPath ?? DEFAULT_PROXY_PATH; + this.proxyPath = opts.proxyPath; } private async fetch(input: string, init?: RequestInit): Promise { @@ -70,40 +74,37 @@ export class ILertClient implements ILertApi { const response = await fetch(`${apiUrl}${input}`, init); if (response.status === 401) { - throw new UnauthorizedError(''); - } - if (!response.ok) { - throw new Error( - `Request failed with ${response.status} ${response.statusText}`, + throw new AuthenticationError( + 'This request requires HTTP authentication.', ); } + if (!response.ok || response.status >= 400) { + throw await ResponseError.fromResponse(response); + } return await response.json(); } async fetchIncidents(opts?: GetIncidentsOpts): Promise { const init = { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const query = new URLSearchParams(); - if (opts && opts.maxResults) { - query.append('max-results', `${opts.maxResults}`); + if (opts?.maxResults !== undefined) { + query.append('max-results', String(opts.maxResults)); } - if (opts && opts.startIndex) { - query.append('start-index', `${opts.startIndex}`); + if (opts?.startIndex !== undefined) { + query.append('start-index', String(opts.startIndex)); } - if (opts && opts.alertSources) { + if (opts?.alertSources !== undefined && Array.isArray(opts.alertSources)) { opts.alertSources.forEach((a: number) => { if (a) { - query.append('alert-source', `${a}`); + query.append('alert-source', String(a)); } }); } - if (opts && opts.states && Array.isArray(opts.states)) { + if (opts?.states !== undefined && Array.isArray(opts.states)) { opts.states.forEach(state => { query.append('state', state); }); @@ -118,10 +119,7 @@ export class ILertClient implements ILertApi { async fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise { const init = { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const query = new URLSearchParams(); if (opts && opts.states && Array.isArray(opts.states)) { @@ -137,14 +135,21 @@ export class ILertClient implements ILertApi { return response && response.count ? response.count : 0; } + async fetchIncident(id: number): Promise { + const init = { + headers: JSON_HEADERS, + }; + + const response = await this.fetch(`/api/v1/incidents/${id}`, init); + + return response; + } + async fetchIncidentResponders( incident: Incident, ): Promise { const init = { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const response = await this.fetch( @@ -157,10 +162,7 @@ export class ILertClient implements ILertApi { async fetchIncidentActions(incident: Incident): Promise { const init = { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const response = await this.fetch( @@ -171,40 +173,42 @@ export class ILertClient implements ILertApi { return response; } - async acceptIncident(incident: Incident): Promise { + async acceptIncident( + incident: Incident, + userName: string, + ): Promise { const init = { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - body: JSON.stringify({ summary: 'Backstage — iLert plugin' }), + method: 'POST', + headers: JSON_HEADERS, + body: JSON.stringify({ + apiKey: incident.alertSource?.integrationKey || '', + incidentKey: incident.incidentKey, + summary: `from ${userName} via Backstage plugin`, + eventType: 'ACCEPT', + }), }; - const response = await this.fetch( - `/api/v1/incidents/${incident.id}/accept`, - init, - ); - - return response; + await this.fetch('/api/v1/events', init); + return this.fetchIncident(incident.id); } - async resolveIncident(incident: Incident): Promise { + async resolveIncident( + incident: Incident, + userName: string, + ): Promise { const init = { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - body: JSON.stringify({ summary: 'Backstage — iLert plugin' }), + method: 'POST', + headers: JSON_HEADERS, + body: JSON.stringify({ + apiKey: incident.alertSource?.integrationKey || '', + incidentKey: incident.incidentKey, + summary: `from ${userName} via Backstage plugin`, + eventType: 'RESOLVE', + }), }; - const response = await this.fetch( - `/api/v1/incidents/${incident.id}/resolve`, - init, - ); - - return response; + await this.fetch('/api/v1/events', init); + return this.fetchIncident(incident.id); } async assignIncident( @@ -213,22 +217,19 @@ export class ILertClient implements ILertApi { ): Promise { const init = { method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const query = new URLSearchParams(); switch (responder.group) { case 'ESCALATION_POLICY': - query.append('policy-id', `${responder.id}`); + query.append('policy-id', String(responder.id)); break; case 'ON_CALL_SCHEDULE': - query.append('schedule-id', `${responder.id}`); + query.append('schedule-id', String(responder.id)); break; default: - query.append('user-id', `${responder.id}`); + query.append('user-id', String(responder.id)); break; } @@ -246,10 +247,7 @@ export class ILertClient implements ILertApi { ): Promise { const init = { method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, body: JSON.stringify({ webhookId: action.webhookId, extensionId: action.extensionId, @@ -264,10 +262,7 @@ export class ILertClient implements ILertApi { async createIncident(eventRequest: EventRequest): Promise { const init = { method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, body: JSON.stringify({ apiKey: eventRequest.integrationKey, summary: eventRequest.summary, @@ -286,15 +281,12 @@ export class ILertClient implements ILertApi { }; const response = await this.fetch('/api/v1/events', init); - return response.responseCode === 'NEW_INCIDENT_CREATED'; + return response; } async fetchUptimeMonitors(): Promise { const init = { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const response = await this.fetch('/api/v1/uptime-monitors', init); @@ -304,10 +296,7 @@ export class ILertClient implements ILertApi { async fetchUptimeMonitor(id: number): Promise { const init = { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const response: UptimeMonitor = await this.fetch( @@ -323,10 +312,7 @@ export class ILertClient implements ILertApi { ): Promise { const init = { method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, body: JSON.stringify({ ...uptimeMonitor, paused: true }), }; @@ -343,10 +329,7 @@ export class ILertClient implements ILertApi { ): Promise { const init = { method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, body: JSON.stringify({ ...uptimeMonitor, paused: false }), }; @@ -360,10 +343,7 @@ export class ILertClient implements ILertApi { async fetchAlertSources(): Promise { const init = { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const response = await this.fetch('/api/v1/alert-sources', init); @@ -375,10 +355,7 @@ export class ILertClient implements ILertApi { idOrIntegrationKey: number | string, ): Promise { const init = { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const response = await this.fetch( @@ -391,16 +368,13 @@ export class ILertClient implements ILertApi { async fetchAlertSourceOnCalls(alertSource: AlertSource): Promise { const init = { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const response = await this.fetch( `/api/v1/on-calls?policies=${ alertSource.escalationPolicy.id - }&expand=user&expand=escalationPolicy&timezone=${momentTimezone.tz.guess()}`, + }&expand=user&expand=escalationPolicy&timezone=${dt.local().zoneName}`, init, ); @@ -410,10 +384,7 @@ export class ILertClient implements ILertApi { async enableAlertSource(alertSource: AlertSource): Promise { const init = { method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, body: JSON.stringify({ ...alertSource, active: true }), }; @@ -428,10 +399,7 @@ export class ILertClient implements ILertApi { async disableAlertSource(alertSource: AlertSource): Promise { const init = { method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, body: JSON.stringify({ ...alertSource, active: false }), }; @@ -449,16 +417,13 @@ export class ILertClient implements ILertApi { ): Promise { const init = { method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, body: JSON.stringify({ - start: moment().utc().toISOString(), - end: moment().add(minutes, 'minutes').utc().toISOString(), + start: dt.utc().toISO(), + end: dt.utc().plus({ minutes }).toISO(), description: `Immediate maintenance window for ${minutes} minutes. Backstage — iLert plugin.`, createdBy: 'Backstage', - timezone: momentTimezone.tz.guess(), + timezone: dt.local().zoneName, alertSources: [{ id: alertSourceId }], }), }; @@ -470,10 +435,7 @@ export class ILertClient implements ILertApi { async fetchOnCallSchedules(): Promise { const init = { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const response = await this.fetch('/api/v1/schedules', init); @@ -483,10 +445,7 @@ export class ILertClient implements ILertApi { async fetchUsers(): Promise { const init = { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, }; const response = await this.fetch('/api/v1/users', init); @@ -502,10 +461,7 @@ export class ILertClient implements ILertApi { ): Promise { const init = { method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, + headers: JSON_HEADERS, body: JSON.stringify({ user: { id: userId }, start, end }), }; @@ -541,17 +497,7 @@ export class ILertClient implements ILertApi { } getUserPhoneNumber(user: User | null) { - if (!user) { - return ''; - } - if (user.mobile) { - return user.mobile.number; - } - - if (user.landline) { - return user.landline.number; - } - return ''; + return user?.mobile?.number || user?.landline?.number || ''; } getUserInitials(user: User | null) { diff --git a/plugins/ilert/src/api/index.ts b/plugins/ilert/src/api/index.ts index 128856ebdf..65c3571bff 100644 --- a/plugins/ilert/src/api/index.ts +++ b/plugins/ilert/src/api/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export { ILertClient, ilertApiRef, UnauthorizedError } from './client'; +export { ILertClient, ilertApiRef } from './client'; export type { ILertApi, GetIncidentsCountOpts, diff --git a/plugins/ilert/src/api/types.ts b/plugins/ilert/src/api/types.ts index 84b4ecf654..ab9cca3dc6 100644 --- a/plugins/ilert/src/api/types.ts +++ b/plugins/ilert/src/api/types.ts @@ -54,10 +54,11 @@ export type EventRequest = { export interface ILertApi { fetchIncidents(opts?: GetIncidentsOpts): Promise; fetchIncidentsCount(opts?: GetIncidentsCountOpts): Promise; + fetchIncident(id: number): Promise; fetchIncidentResponders(incident: Incident): Promise; fetchIncidentActions(incident: Incident): Promise; - acceptIncident(incident: Incident): Promise; - resolveIncident(incident: Incident): Promise; + acceptIncident(incident: Incident, userName: string): Promise; + resolveIncident(incident: Incident, userName: string): Promise; assignIncident( incident: Incident, responder: IncidentResponder, @@ -115,5 +116,5 @@ export type Options = { /** * Path to use for requests via the proxy, defaults to /ilert/api */ - proxyPath?: string; + proxyPath: string; }; diff --git a/plugins/ilert/src/components/AlertSource/AlertSourceLink.tsx b/plugins/ilert/src/components/AlertSource/AlertSourceLink.tsx index d94a9b0039..f7f6dbb78c 100644 --- a/plugins/ilert/src/components/AlertSource/AlertSourceLink.tsx +++ b/plugins/ilert/src/components/AlertSource/AlertSourceLink.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { useApi } from '@backstage/core'; -import Link from '@material-ui/core/Link'; +import { useApi, Link } from '@backstage/core'; import Grid from '@material-ui/core/Grid'; import { AlertSource } from '../../types'; import { ilertApiRef } from '../../api'; @@ -61,7 +60,7 @@ export const AlertSourceLink = ({ {alertSource.name} diff --git a/plugins/ilert/src/components/EscalationPolicy/EscalationPolicyLink.tsx b/plugins/ilert/src/components/EscalationPolicy/EscalationPolicyLink.tsx index 7f1e3524ff..9e7ff102f8 100644 --- a/plugins/ilert/src/components/EscalationPolicy/EscalationPolicyLink.tsx +++ b/plugins/ilert/src/components/EscalationPolicy/EscalationPolicyLink.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { useApi } from '@backstage/core'; -import Link from '@material-ui/core/Link'; +import { useApi, Link } from '@backstage/core'; import { makeStyles } from '@material-ui/core/styles'; import { EscalationPolicy } from '../../types'; import { ilertApiRef } from '../../api'; @@ -41,7 +40,7 @@ export const EscalationPolicyLink = ({ return ( {escalationPolicy.name} diff --git a/plugins/ilert/src/components/ILertCard/ILertCard.tsx b/plugins/ilert/src/components/ILertCard/ILertCard.tsx index dd4bdde3cf..bbc82c2b2d 100644 --- a/plugins/ilert/src/components/ILertCard/ILertCard.tsx +++ b/plugins/ilert/src/components/ILertCard/ILertCard.tsx @@ -15,14 +15,14 @@ */ import React from 'react'; import { Entity } from '@backstage/catalog-model'; -import { UnauthorizedError } from '../../api'; -import Alert from '@material-ui/lab/Alert'; +import { AuthenticationError } from '@backstage/errors'; +import { ResponseErrorPanel } from '@backstage/core'; import Card from '@material-ui/core/Card'; import CardContent from '@material-ui/core/CardContent'; import CardHeader from '@material-ui/core/CardHeader'; import Divider from '@material-ui/core/Divider'; import { makeStyles } from '@material-ui/core/styles'; -import { ILERT_INTEGRATION_KEY } from '../../constants'; +import { ILERT_INTEGRATION_KEY_ANNOTATION } from '../../constants'; import { MissingAuthorizationHeaderError } from '../Errors'; import { useIncidents } from '../../hooks/useIncidents'; import { IncidentsTable } from '../IncidentsPage'; @@ -36,7 +36,7 @@ import { ILertCardEmptyState } from './ILertCardEmptyState'; import { ILertCardOnCall } from './ILertCardOnCall'; export const isPluginApplicableToEntity = (entity: Entity) => - Boolean(entity.metadata.annotations?.[ILERT_INTEGRATION_KEY]); + Boolean(entity.metadata.annotations?.[ILERT_INTEGRATION_KEY_ANNOTATION]); const useStyles = makeStyles({ content: { @@ -79,15 +79,11 @@ export const ILertCard = () => { ] = React.useState(false); if (error) { - if (error instanceof UnauthorizedError) { + if (error instanceof AuthenticationError) { return ; } - return ( - - {error.message} - - ); + return ; } if (!integrationKey) { diff --git a/plugins/ilert/src/components/ILertCard/ILertCardEmptyState.tsx b/plugins/ilert/src/components/ILertCard/ILertCardEmptyState.tsx index 42dc140696..955d514564 100644 --- a/plugins/ilert/src/components/ILertCard/ILertCardEmptyState.tsx +++ b/plugins/ilert/src/components/ILertCard/ILertCardEmptyState.tsx @@ -40,14 +40,12 @@ spec: const useStyles = makeStyles(theme => ({ code: { borderRadius: 6, - margin: `${theme.spacing(2)}px 0px`, + margin: theme.spacing(2, 0), background: theme.palette.type === 'dark' ? '#444' : '#fff', }, header: { display: 'inline-block', - padding: `${theme.spacing(2)}px ${theme.spacing(2)}px ${theme.spacing( - 2, - )}px ${theme.spacing(2.5)}px`, + padding: theme.spacing(2, 2, 2, 2.5), }, })); diff --git a/plugins/ilert/src/components/ILertCard/ILertCardOnCallItem.tsx b/plugins/ilert/src/components/ILertCard/ILertCardOnCallItem.tsx index 69ceb0ad55..994666084b 100644 --- a/plugins/ilert/src/components/ILertCard/ILertCardOnCallItem.tsx +++ b/plugins/ilert/src/components/ILertCard/ILertCardOnCallItem.tsx @@ -29,7 +29,7 @@ import { makeStyles } from '@material-ui/core/styles'; import { OnCall } from '../../types'; import { useApi } from '@backstage/core'; import { ilertApiRef } from '../../api'; -import moment from 'moment'; +import { DateTime as dt } from 'luxon'; const useStyles = makeStyles({ listItemPrimary: { @@ -123,8 +123,8 @@ export const ILertCardOnCallItem = ({ diff --git a/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx b/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx index 0a1ae95ded..5b06cc4eb6 100644 --- a/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx +++ b/plugins/ilert/src/components/Incident/IncidentActionsMenu.tsx @@ -14,9 +14,14 @@ * limitations under the License. */ import React from 'react'; -import { alertApiRef, Progress, useApi } from '@backstage/core'; +import { + alertApiRef, + Progress, + useApi, + identityApiRef, + Link, +} from '@backstage/core'; import { IconButton, Menu, MenuItem, Typography } from '@material-ui/core'; -import Link from '@material-ui/core/Link'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import { ilertApiRef } from '../../api'; import { Incident, IncidentAction } from '../../types'; @@ -34,6 +39,8 @@ export const IncidentActionsMenu = ({ }) => { const ilertApi = useApi(ilertApiRef); const alertApi = useApi(alertApiRef); + const identityApi = useApi(identityApiRef); + const userName = identityApi.getUserId(); const [anchorEl, setAnchorEl] = React.useState(null); const callback = onIncidentChanged || ((_: Incident): void => {}); const setProcessing = setIsLoading || ((_: boolean): void => {}); @@ -59,7 +66,7 @@ export const IncidentActionsMenu = ({ try { handleCloseMenu(); setProcessing(true); - const newIncident = await ilertApi.acceptIncident(incident); + const newIncident = await ilertApi.acceptIncident(incident, userName); alertApi.post({ message: 'Incident accepted.' }); callback(newIncident); @@ -74,7 +81,7 @@ export const IncidentActionsMenu = ({ try { handleCloseMenu(); setProcessing(true); - const newIncident = await ilertApi.resolveIncident(incident); + const newIncident = await ilertApi.resolveIncident(incident, userName); alertApi.post({ message: 'Incident resolved.' }); callback(newIncident); @@ -179,7 +186,7 @@ export const IncidentActionsMenu = ({ - + View in iLert diff --git a/plugins/ilert/src/components/Incident/IncidentLink.tsx b/plugins/ilert/src/components/Incident/IncidentLink.tsx index 87818f363a..7bfe08b7a9 100644 --- a/plugins/ilert/src/components/Incident/IncidentLink.tsx +++ b/plugins/ilert/src/components/Incident/IncidentLink.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { useApi } from '@backstage/core'; -import Link from '@material-ui/core/Link'; +import { useApi, Link } from '@backstage/core'; import { makeStyles } from '@material-ui/core/styles'; import { Incident } from '../../types'; import { ilertApiRef } from '../../api'; @@ -37,7 +36,7 @@ export const IncidentLink = ({ incident }: { incident: Incident | null }) => { return ( #{incident.id} diff --git a/plugins/ilert/src/components/Incident/IncidentNewModal.tsx b/plugins/ilert/src/components/Incident/IncidentNewModal.tsx index 3fae62bf0c..8ff33e6319 100644 --- a/plugins/ilert/src/components/Incident/IncidentNewModal.tsx +++ b/plugins/ilert/src/components/Incident/IncidentNewModal.tsx @@ -98,17 +98,15 @@ export const IncidentNewModal = ({ setIsLoading(true); setTimeout(async () => { try { - const success = await ilertApi.createIncident({ + await ilertApi.createIncident({ integrationKey, summary, details, userName, source, }); - if (success) { - alertApi.post({ message: 'Incident created.' }); - refetchIncidents(); - } + alertApi.post({ message: 'Incident created.' }); + refetchIncidents(); } catch (err) { alertApi.post({ message: err, severity: 'error' }); } diff --git a/plugins/ilert/src/components/IncidentsPage/IncidentsPage.tsx b/plugins/ilert/src/components/IncidentsPage/IncidentsPage.tsx index e552ce7d26..782848645e 100644 --- a/plugins/ilert/src/components/IncidentsPage/IncidentsPage.tsx +++ b/plugins/ilert/src/components/IncidentsPage/IncidentsPage.tsx @@ -14,11 +14,15 @@ * limitations under the License. */ import React from 'react'; -import { Content, ContentHeader, SupportButton } from '@backstage/core'; +import { + Content, + ContentHeader, + SupportButton, + ResponseErrorPanel, +} from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import Button from '@material-ui/core/Button'; import AddIcon from '@material-ui/icons/Add'; -import { UnauthorizedError } from '../../api'; -import Alert from '@material-ui/lab/Alert'; import { IncidentsTable } from './IncidentsTable'; import { MissingAuthorizationHeaderError } from '../Errors'; import { useIncidents } from '../../hooks/useIncidents'; @@ -44,14 +48,18 @@ export const IncidentsPage = () => { }; if (error) { - if (error instanceof UnauthorizedError) { - return ; + if (error instanceof AuthenticationError) { + return ( + + + + ); } return ( - - {error.message} - + + + ); } diff --git a/plugins/ilert/src/components/IncidentsPage/IncidentsTable.tsx b/plugins/ilert/src/components/IncidentsPage/IncidentsTable.tsx index b7ee298062..087b2bfd40 100644 --- a/plugins/ilert/src/components/IncidentsPage/IncidentsTable.tsx +++ b/plugins/ilert/src/components/IncidentsPage/IncidentsTable.tsx @@ -22,7 +22,8 @@ import { StatusChip } from './StatusChip'; import { AlertSourceLink } from '../AlertSource/AlertSourceLink'; import { TableTitle } from './TableTitle'; import Typography from '@material-ui/core/Typography'; -import moment from 'moment'; +import { DateTime as dt, Interval } from 'luxon'; +import humanizeDuration from 'humanize-duration'; import { IncidentActionsMenu } from '../Incident/IncidentActionsMenu'; import { IncidentLink } from '../Incident/IncidentLink'; @@ -116,16 +117,24 @@ export const IncidentsTable = ({ render: rowData => ( {(rowData as Incident).status !== 'RESOLVED' - ? moment - .duration(moment((rowData as Incident).reportTime).diff(moment())) - .humanize() - : moment - .duration( - moment((rowData as Incident).reportTime).diff( - moment((rowData as Incident).resolvedOn), - ), + ? humanizeDuration( + Interval.fromDateTimes( + dt.fromISO((rowData as Incident).reportTime), + dt.now(), ) - .humanize()} + .toDuration() + .valueOf(), + { units: ['h', 'm', 's'], largest: 2, round: true }, + ) + : humanizeDuration( + Interval.fromDateTimes( + dt.fromISO((rowData as Incident).reportTime), + dt.fromISO((rowData as Incident).resolvedOn), + ) + .toDuration() + .valueOf(), + { units: ['h', 'm', 's'], largest: 2, round: true }, + )} ), }; diff --git a/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx index c84e0a7b8d..31da9c1097 100644 --- a/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx +++ b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesGrid.tsx @@ -14,13 +14,12 @@ * limitations under the License. */ import React from 'react'; -import { useApi, ItemCardGrid, Progress } from '@backstage/core'; +import { useApi, ItemCardGrid, Progress, Link } from '@backstage/core'; import { makeStyles } from '@material-ui/core/styles'; import Card from '@material-ui/core/Card'; import CardContent from '@material-ui/core/CardContent'; import CardHeader from '@material-ui/core/CardHeader'; import Typography from '@material-ui/core/Typography'; -import Link from '@material-ui/core/Link'; import { Schedule } from '../../types'; import { ilertApiRef } from '../../api'; import { OnCallShiftItem } from './OnCallShiftItem'; @@ -132,7 +131,7 @@ export const OnCallSchedulesGrid = ({ classes={{ content: classes.cardHeader }} title={ {schedule.name} diff --git a/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesPage.tsx b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesPage.tsx index 09c2f2fc63..bb0cd0218e 100644 --- a/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesPage.tsx +++ b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallSchedulesPage.tsx @@ -14,9 +14,13 @@ * limitations under the License. */ import React from 'react'; -import { Content, ContentHeader, SupportButton } from '@backstage/core'; -import { UnauthorizedError } from '../../api'; -import Alert from '@material-ui/lab/Alert'; +import { + Content, + ContentHeader, + SupportButton, + ResponseErrorPanel, +} from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import { OnCallSchedulesGrid } from './OnCallSchedulesGrid'; import { MissingAuthorizationHeaderError } from '../Errors'; import { useOnCallSchedules } from '../../hooks/useOnCallSchedules'; @@ -28,14 +32,18 @@ export const OnCallSchedulesPage = () => { ] = useOnCallSchedules(); if (error) { - if (error instanceof UnauthorizedError) { - return ; + if (error instanceof AuthenticationError) { + return ( + + + + ); } return ( - - {error.message} - + + + ); } diff --git a/plugins/ilert/src/components/OnCallSchedulesPage/OnCallShiftItem.tsx b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallShiftItem.tsx index c636609781..46f6963445 100644 --- a/plugins/ilert/src/components/OnCallSchedulesPage/OnCallShiftItem.tsx +++ b/plugins/ilert/src/components/OnCallSchedulesPage/OnCallShiftItem.tsx @@ -19,7 +19,7 @@ import Grid from '@material-ui/core/Grid'; import Typography from '@material-ui/core/Typography'; import RepeatIcon from '@material-ui/icons/Repeat'; import { Shift } from '../../types'; -import moment from 'moment'; +import { DateTime as dt } from 'luxon'; import { makeStyles } from '@material-ui/core/styles'; import { ShiftOverrideModal } from '../Shift/ShiftOverrideModal'; @@ -77,9 +77,9 @@ export const OnCallShiftItem = ({ ) : null} - {`${moment(shift.start).format('D MMM, HH:mm')} - ${moment( - shift.end, - ).format('D MMM, HH:mm')}`} + {`${dt.fromISO(shift.start).toFormat('D MMM, HH:mm')} - ${dt + .fromISO(shift.end) + .toFormat('D MMM, HH:mm')}`} diff --git a/plugins/ilert/src/components/Shift/ShiftOverrideModal.tsx b/plugins/ilert/src/components/Shift/ShiftOverrideModal.tsx index 1fdd05cb4c..47e13f9b7e 100644 --- a/plugins/ilert/src/components/Shift/ShiftOverrideModal.tsx +++ b/plugins/ilert/src/components/Shift/ShiftOverrideModal.tsx @@ -28,7 +28,7 @@ import { ilertApiRef } from '../../api'; import { useShiftOverride } from '../../hooks/useShiftOverride'; import { Shift } from '../../types'; import { DateTimePicker, MuiPickersUtilsProvider } from '@material-ui/pickers'; -import DateFnsUtils from '@date-io/date-fns'; +import LuxonUtils from '@date-io/luxon'; const useStyles = makeStyles(() => ({ container: { @@ -120,7 +120,7 @@ export const ShiftOverrideModal = ({ > Shift override - + { - setStart(date ? date.toISOString() : ''); + setStart(date ? date.toISO() : ''); }} /> { - setEnd(date ? date.toISOString() : ''); + setEnd(date ? date.toISO() : ''); }} /> diff --git a/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorActionsMenu.tsx b/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorActionsMenu.tsx index efd69a4acf..36671bff90 100644 --- a/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorActionsMenu.tsx +++ b/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorActionsMenu.tsx @@ -14,9 +14,8 @@ * limitations under the License. */ import React from 'react'; -import { alertApiRef, useApi } from '@backstage/core'; +import { alertApiRef, useApi, Link } from '@backstage/core'; import { IconButton, Menu, MenuItem, Typography } from '@material-ui/core'; -import Link from '@material-ui/core/Link'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import { ilertApiRef } from '../../api'; @@ -117,13 +116,15 @@ export const UptimeMonitorActionsMenu = ({ - View Report + + View Report + - + View in iLert diff --git a/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorLink.tsx b/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorLink.tsx index 0af5cd89df..507e0208e2 100644 --- a/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorLink.tsx +++ b/plugins/ilert/src/components/UptimeMonitor/UptimeMonitorLink.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { useApi } from '@backstage/core'; -import Link from '@material-ui/core/Link'; +import { useApi, Link } from '@backstage/core'; import { makeStyles } from '@material-ui/core/styles'; import { UptimeMonitor } from '../../types'; import { ilertApiRef } from '../../api'; @@ -41,7 +40,7 @@ export const UptimeMonitorLink = ({ return ( #{uptimeMonitor.id} diff --git a/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsPage.tsx b/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsPage.tsx index dc7ea77a6c..9769fa408d 100644 --- a/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsPage.tsx +++ b/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsPage.tsx @@ -14,9 +14,13 @@ * limitations under the License. */ import React from 'react'; -import { Content, ContentHeader, SupportButton } from '@backstage/core'; -import { UnauthorizedError } from '../../api'; -import Alert from '@material-ui/lab/Alert'; +import { + Content, + ContentHeader, + SupportButton, + ResponseErrorPanel, +} from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import { UptimeMonitorsTable } from './UptimeMonitorsTable'; import { MissingAuthorizationHeaderError } from '../Errors'; import { useUptimeMonitors } from '../../hooks/useUptimeMonitors'; @@ -28,14 +32,18 @@ export const UptimeMonitorsPage = () => { ] = useUptimeMonitors(); if (error) { - if (error instanceof UnauthorizedError) { - return ; + if (error instanceof AuthenticationError) { + return ( + + + + ); } return ( - - {error.message} - + + + ); } diff --git a/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsTable.tsx b/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsTable.tsx index bd55310ad7..2173f8e1b7 100644 --- a/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsTable.tsx +++ b/plugins/ilert/src/components/UptimeMonitorsPage/UptimeMonitorsTable.tsx @@ -20,7 +20,8 @@ import { TableState } from '../../api'; import { UptimeMonitor } from '../../types'; import { StatusChip } from './StatusChip'; import Typography from '@material-ui/core/Typography'; -import moment from 'moment'; +import { DateTime as dt, Interval } from 'luxon'; +import humanizeDuration from 'humanize-duration'; import { EscalationPolicyLink } from '../EscalationPolicy/EscalationPolicyLink'; import { UptimeMonitorCheckType } from './UptimeMonitorCheckType'; import { UptimeMonitorActionsMenu } from '../UptimeMonitor/UptimeMonitorActionsMenu'; @@ -99,13 +100,15 @@ export const UptimeMonitorsTable = ({ headerStyle: mdColumnStyle, render: rowData => ( - {moment - .duration( - moment((rowData as UptimeMonitor).lastStatusChange).diff( - moment(), - ), + {humanizeDuration( + Interval.fromDateTimes( + dt.fromISO((rowData as UptimeMonitor).lastStatusChange), + dt.now(), ) - .humanize()} + .toDuration() + .valueOf(), + { units: ['h', 'm', 's'], largest: 2, round: true }, + )} ), }, diff --git a/plugins/ilert/src/hooks/index.ts b/plugins/ilert/src/hooks/index.ts index e21c9584d2..6e7e207112 100644 --- a/plugins/ilert/src/hooks/index.ts +++ b/plugins/ilert/src/hooks/index.ts @@ -16,13 +16,16 @@ import { useEntity } from '@backstage/plugin-catalog-react'; -import { ILERT_INTEGRATION_KEY } from '../constants'; +import { ILERT_INTEGRATION_KEY_ANNOTATION } from '../constants'; export function useILertEntity() { const { entity } = useEntity(); const integrationKey = - entity.metadata.annotations?.[ILERT_INTEGRATION_KEY] || ''; + entity.metadata.annotations?.[ILERT_INTEGRATION_KEY_ANNOTATION] || ''; const name = entity.metadata.name; + const identifier = `${entity.kind}:${ + entity.metadata.namespace || 'default' + }/${entity.metadata.name}`; - return { integrationKey, name }; + return { integrationKey, name, identifier }; } diff --git a/plugins/ilert/src/hooks/useAlertSource.ts b/plugins/ilert/src/hooks/useAlertSource.ts index 950f8f41c6..15cc213b48 100644 --- a/plugins/ilert/src/hooks/useAlertSource.ts +++ b/plugins/ilert/src/hooks/useAlertSource.ts @@ -14,8 +14,9 @@ * limitations under the License. */ import React from 'react'; -import { ilertApiRef, UnauthorizedError } from '../api'; +import { ilertApiRef } from '../api'; import { useApi, errorApiRef } from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import { useAsyncRetry } from 'react-use'; import { AlertSource, UptimeMonitor } from '../types'; @@ -46,7 +47,7 @@ export const useAlertSource = (integrationKey: string) => { setIsAlertSourceLoading(false); } catch (e) { setIsAlertSourceLoading(false); - if (!(e instanceof UnauthorizedError)) { + if (!(e instanceof AuthenticationError)) { errorApi.post(e); } throw e; @@ -69,7 +70,7 @@ export const useAlertSource = (integrationKey: string) => { setIsUptimeMonitorLoading(false); } catch (e) { setIsUptimeMonitorLoading(false); - if (!(e instanceof UnauthorizedError)) { + if (!(e instanceof AuthenticationError)) { errorApi.post(e); } throw e; diff --git a/plugins/ilert/src/hooks/useAlertSourceOnCalls.ts b/plugins/ilert/src/hooks/useAlertSourceOnCalls.ts index ac36c70097..0fbb190a02 100644 --- a/plugins/ilert/src/hooks/useAlertSourceOnCalls.ts +++ b/plugins/ilert/src/hooks/useAlertSourceOnCalls.ts @@ -14,8 +14,9 @@ * limitations under the License. */ import React from 'react'; -import { ilertApiRef, UnauthorizedError } from '../api'; +import { ilertApiRef } from '../api'; import { useApi, errorApiRef } from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import { useAsyncRetry } from 'react-use'; import { AlertSource, OnCall } from '../types'; @@ -37,7 +38,7 @@ export const useAlertSourceOnCalls = (alertSource?: AlertSource | null) => { setIsLoading(false); } catch (e) { setIsLoading(false); - if (!(e instanceof UnauthorizedError)) { + if (!(e instanceof AuthenticationError)) { errorApi.post(e); } throw e; diff --git a/plugins/ilert/src/hooks/useAssignIncident.ts b/plugins/ilert/src/hooks/useAssignIncident.ts index 1671acdc51..8f0f663d2b 100644 --- a/plugins/ilert/src/hooks/useAssignIncident.ts +++ b/plugins/ilert/src/hooks/useAssignIncident.ts @@ -14,8 +14,9 @@ * limitations under the License. */ import React from 'react'; -import { ilertApiRef, UnauthorizedError } from '../api'; +import { ilertApiRef } from '../api'; import { useApi, errorApiRef } from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import { useAsyncRetry } from 'react-use'; import { Incident, IncidentResponder } from '../types'; @@ -49,7 +50,7 @@ export const useAssignIncident = (incident: Incident | null, open: boolean) => { setIncidentRespondersList(data); } } catch (e) { - if (!(e instanceof UnauthorizedError)) { + if (!(e instanceof AuthenticationError)) { errorApi.post(e); } throw e; diff --git a/plugins/ilert/src/hooks/useIncidentActions.ts b/plugins/ilert/src/hooks/useIncidentActions.ts index d8dd08e17a..e23341ed9e 100644 --- a/plugins/ilert/src/hooks/useIncidentActions.ts +++ b/plugins/ilert/src/hooks/useIncidentActions.ts @@ -14,8 +14,9 @@ * limitations under the License. */ import React from 'react'; -import { ilertApiRef, UnauthorizedError } from '../api'; +import { ilertApiRef } from '../api'; import { useApi, errorApiRef } from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import { useAsyncRetry } from 'react-use'; import { Incident, IncidentAction } from '../types'; @@ -39,7 +40,7 @@ export const useIncidentActions = ( const data = await ilertApi.fetchIncidentActions(incident); setIncidentActionsList(data); } catch (e) { - if (!(e instanceof UnauthorizedError)) { + if (!(e instanceof AuthenticationError)) { errorApi.post(e); } throw e; diff --git a/plugins/ilert/src/hooks/useIncidents.ts b/plugins/ilert/src/hooks/useIncidents.ts index 2f3d12cf17..5f130fb20c 100644 --- a/plugins/ilert/src/hooks/useIncidents.ts +++ b/plugins/ilert/src/hooks/useIncidents.ts @@ -14,13 +14,9 @@ * limitations under the License. */ import React from 'react'; -import { - GetIncidentsOpts, - ilertApiRef, - TableState, - UnauthorizedError, -} from '../api'; +import { GetIncidentsOpts, ilertApiRef, TableState } from '../api'; import { useApi, errorApiRef } from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import { useAsyncRetry } from 'react-use'; import { ACCEPTED, @@ -68,7 +64,7 @@ export const useIncidents = ( setIncidentsList(data || []); setIsLoading(false); } catch (e) { - if (!(e instanceof UnauthorizedError)) { + if (!(e instanceof AuthenticationError)) { errorApi.post(e); } setIsLoading(false); @@ -81,7 +77,7 @@ export const useIncidents = ( const count = await ilertApi.fetchIncidentsCount({ states }); setIncidentsCount(count || 0); } catch (e) { - if (!(e instanceof UnauthorizedError)) { + if (!(e instanceof AuthenticationError)) { errorApi.post(e); } throw e; diff --git a/plugins/ilert/src/hooks/useNewIncident.ts b/plugins/ilert/src/hooks/useNewIncident.ts index 669fa74b63..6fbcfbb5a5 100644 --- a/plugins/ilert/src/hooks/useNewIncident.ts +++ b/plugins/ilert/src/hooks/useNewIncident.ts @@ -14,8 +14,9 @@ * limitations under the License. */ import React from 'react'; -import { ilertApiRef, UnauthorizedError } from '../api'; +import { ilertApiRef } from '../api'; import { useApi, errorApiRef } from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import { useAsyncRetry } from 'react-use'; import { AlertSource } from '../types'; @@ -44,7 +45,7 @@ export const useNewIncident = ( const count = await ilertApi.fetchAlertSources(); setAlertSourcesList(count || 0); } catch (e) { - if (!(e instanceof UnauthorizedError)) { + if (!(e instanceof AuthenticationError)) { errorApi.post(e); } throw e; diff --git a/plugins/ilert/src/hooks/useOnCallSchedules.ts b/plugins/ilert/src/hooks/useOnCallSchedules.ts index 5e0538a370..ce76a0770c 100644 --- a/plugins/ilert/src/hooks/useOnCallSchedules.ts +++ b/plugins/ilert/src/hooks/useOnCallSchedules.ts @@ -14,8 +14,9 @@ * limitations under the License. */ import React from 'react'; -import { ilertApiRef, UnauthorizedError } from '../api'; +import { ilertApiRef } from '../api'; import { useApi, errorApiRef } from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import { useAsyncRetry } from 'react-use'; import { Schedule } from '../types'; @@ -36,7 +37,7 @@ export const useOnCallSchedules = () => { setIsLoading(false); } catch (e) { setIsLoading(false); - if (!(e instanceof UnauthorizedError)) { + if (!(e instanceof AuthenticationError)) { errorApi.post(e); } throw e; diff --git a/plugins/ilert/src/hooks/useShiftOverride.ts b/plugins/ilert/src/hooks/useShiftOverride.ts index fd1da7423b..136d20d286 100644 --- a/plugins/ilert/src/hooks/useShiftOverride.ts +++ b/plugins/ilert/src/hooks/useShiftOverride.ts @@ -14,8 +14,9 @@ * limitations under the License. */ import React from 'react'; -import { ilertApiRef, UnauthorizedError } from '../api'; +import { ilertApiRef } from '../api'; import { useApi, errorApiRef } from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import { useAsyncRetry } from 'react-use'; import { User, Shift } from '../types'; @@ -38,7 +39,7 @@ export const useShiftOverride = (s: Shift, isModalOpened: boolean) => { setIsLoading(false); } catch (e) { setIsLoading(false); - if (!(e instanceof UnauthorizedError)) { + if (!(e instanceof AuthenticationError)) { errorApi.post(e); } throw e; diff --git a/plugins/ilert/src/hooks/useUptimeMonitors.ts b/plugins/ilert/src/hooks/useUptimeMonitors.ts index 02cd5aaa2f..fa1576642a 100644 --- a/plugins/ilert/src/hooks/useUptimeMonitors.ts +++ b/plugins/ilert/src/hooks/useUptimeMonitors.ts @@ -14,8 +14,9 @@ * limitations under the License. */ import React from 'react'; -import { ilertApiRef, TableState, UnauthorizedError } from '../api'; +import { ilertApiRef, TableState } from '../api'; import { useApi, errorApiRef } from '@backstage/core'; +import { AuthenticationError } from '@backstage/errors'; import { useAsyncRetry } from 'react-use'; import { UptimeMonitor } from '../types'; @@ -40,7 +41,7 @@ export const useUptimeMonitors = () => { setIsLoading(false); } catch (e) { setIsLoading(false); - if (!(e instanceof UnauthorizedError)) { + if (!(e instanceof AuthenticationError)) { errorApi.post(e); } throw e; From 6df7f02dba8e8328731ba8910a02f27a0827606a Mon Sep 17 00:00:00 2001 From: yacut Date: Fri, 7 May 2021 15:30:46 +0200 Subject: [PATCH 34/95] add encode to api urls for the iLert plugin Signed-off-by: yacut --- plugins/ilert/src/api/client.ts | 58 +++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/plugins/ilert/src/api/client.ts b/plugins/ilert/src/api/client.ts index e635d76b99..ed1e671bc0 100644 --- a/plugins/ilert/src/api/client.ts +++ b/plugins/ilert/src/api/client.ts @@ -140,7 +140,10 @@ export class ILertClient implements ILertApi { headers: JSON_HEADERS, }; - const response = await this.fetch(`/api/v1/incidents/${id}`, init); + const response = await this.fetch( + `/api/v1/incidents/${encodeURIComponent(id)}`, + init, + ); return response; } @@ -153,7 +156,7 @@ export class ILertClient implements ILertApi { }; const response = await this.fetch( - `/api/v1/incidents/${incident.id}/responders`, + `/api/v1/incidents/${encodeURIComponent(incident.id)}/responders`, init, ); @@ -166,7 +169,7 @@ export class ILertClient implements ILertApi { }; const response = await this.fetch( - `/api/v1/incidents/${incident.id}/actions`, + `/api/v1/incidents/${encodeURIComponent(incident.id)}/actions`, init, ); @@ -234,7 +237,9 @@ export class ILertClient implements ILertApi { } const response = await this.fetch( - `/api/v1/incidents/${incident.id}/assign?${query.toString()}`, + `/api/v1/incidents/${encodeURIComponent( + incident.id, + )}/assign?${query.toString()}`, init, ); @@ -256,7 +261,10 @@ export class ILertClient implements ILertApi { }), }; - await this.fetch(`/api/v1/incidents/${incident.id}/actions`, init); + await this.fetch( + `/api/v1/incidents/${encodeURIComponent(incident.id)}/actions`, + init, + ); } async createIncident(eventRequest: EventRequest): Promise { @@ -300,7 +308,7 @@ export class ILertClient implements ILertApi { }; const response: UptimeMonitor = await this.fetch( - `/api/v1/uptime-monitors/${id}`, + `/api/v1/uptime-monitors/${encodeURIComponent(id)}`, init, ); @@ -317,7 +325,7 @@ export class ILertClient implements ILertApi { }; const response = await this.fetch( - `/api/v1/uptime-monitors/${uptimeMonitor.id}`, + `/api/v1/uptime-monitors/${encodeURIComponent(uptimeMonitor.id)}`, init, ); @@ -334,7 +342,7 @@ export class ILertClient implements ILertApi { }; const response = await this.fetch( - `/api/v1/uptime-monitors/${uptimeMonitor.id}`, + `/api/v1/uptime-monitors/${encodeURIComponent(uptimeMonitor.id)}`, init, ); @@ -359,7 +367,7 @@ export class ILertClient implements ILertApi { }; const response = await this.fetch( - `/api/v1/alert-sources/${idOrIntegrationKey}`, + `/api/v1/alert-sources/${encodeURIComponent(idOrIntegrationKey)}`, init, ); @@ -372,9 +380,9 @@ export class ILertClient implements ILertApi { }; const response = await this.fetch( - `/api/v1/on-calls?policies=${ - alertSource.escalationPolicy.id - }&expand=user&expand=escalationPolicy&timezone=${dt.local().zoneName}`, + `/api/v1/on-calls?policies=${encodeURIComponent( + alertSource.escalationPolicy.id, + )}&expand=user&expand=escalationPolicy&timezone=${dt.local().zoneName}`, init, ); @@ -389,7 +397,7 @@ export class ILertClient implements ILertApi { }; const response = await this.fetch( - `/api/v1/alert-sources/${alertSource.id}`, + `/api/v1/alert-sources/${encodeURIComponent(alertSource.id)}`, init, ); @@ -404,7 +412,7 @@ export class ILertClient implements ILertApi { }; const response = await this.fetch( - `/api/v1/alert-sources/${alertSource.id}`, + `/api/v1/alert-sources/${encodeURIComponent(alertSource.id)}`, init, ); @@ -466,7 +474,7 @@ export class ILertClient implements ILertApi { }; const response = await this.fetch( - `/api/v1/schedules/${scheduleId}/overrides`, + `/api/v1/schedules/${encodeURIComponent(scheduleId)}/overrides`, init, ); @@ -474,26 +482,36 @@ export class ILertClient implements ILertApi { } getIncidentDetailsURL(incident: Incident): string { - return `${this.baseUrl}/incident/view.jsf?id=${incident.id}`; + return `${this.baseUrl}/incident/view.jsf?id=${encodeURIComponent( + incident.id, + )}`; } getAlertSourceDetailsURL(alertSource: AlertSource | null): string { if (!alertSource) { return ''; } - return `${this.baseUrl}/source/view.jsf?id=${alertSource.id}`; + return `${this.baseUrl}/source/view.jsf?id=${encodeURIComponent( + alertSource.id, + )}`; } getEscalationPolicyDetailsURL(escalationPolicy: EscalationPolicy): string { - return `${this.baseUrl}/policy/view.jsf?id=${escalationPolicy.id}`; + return `${this.baseUrl}/policy/view.jsf?id=${encodeURIComponent( + escalationPolicy.id, + )}`; } getUptimeMonitorDetailsURL(uptimeMonitor: UptimeMonitor): string { - return `${this.baseUrl}/uptime/view.jsf?id=${uptimeMonitor.id}`; + return `${this.baseUrl}/uptime/view.jsf?id=${encodeURIComponent( + uptimeMonitor.id, + )}`; } getScheduleDetailsURL(schedule: Schedule): string { - return `${this.baseUrl}/schedule/view.jsf?id=${schedule.id}`; + return `${this.baseUrl}/schedule/view.jsf?id=${encodeURIComponent( + schedule.id, + )}`; } getUserPhoneNumber(user: User | null) { From cb7a25f778687fef1a77949d6faccfe51abbd9a1 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 7 May 2021 15:32:16 +0100 Subject: [PATCH 35/95] as per comments Signed-off-by: Andrew Johnson --- .changeset/smooth-vans-travel.md | 2 +- plugins/github-deployments/src/api/index.ts | 38 +++++++------------ .../components/GithubDeploymentsCard.test.tsx | 15 +++----- .../src/components/GithubDeploymentsCard.tsx | 12 +++--- 4 files changed, 27 insertions(+), 40 deletions(-) diff --git a/.changeset/smooth-vans-travel.md b/.changeset/smooth-vans-travel.md index eb074c8ecf..0038c35e8e 100644 --- a/.changeset/smooth-vans-travel.md +++ b/.changeset/smooth-vans-travel.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-github-deployments': minor +'@backstage/plugin-github-deployments': patch --- Support for GitHub Enterprise with GithubDeploymentsPlugin diff --git a/plugins/github-deployments/src/api/index.ts b/plugins/github-deployments/src/api/index.ts index 3a348f2b56..23a381f771 100644 --- a/plugins/github-deployments/src/api/index.ts +++ b/plugins/github-deployments/src/api/index.ts @@ -21,29 +21,28 @@ import { graphql } from '@octokit/graphql'; const getBaseUrl = ( scmIntegrationsApi: ScmIntegrationRegistry, - locations: string[], + host: string | undefined, ): string => { - const targets = locations - .map(parseLocationReference) - .filter(location => location.type === 'url') - .map(location => location.target); - - if (targets.length === 0) { + if (host === undefined) { return 'https://api.github.com'; } - const location = targets[0]; - const config = scmIntegrationsApi.github.byUrl(location); + const location = parseLocationReference(host); + if (location.type !== 'github') { + return 'https://api.github.com'; + } + + const config = scmIntegrationsApi.github.byHost(location.target); if (!config) { throw new InputError( - `No matching GitHub integration configuration for location ${location}, please check your integrations config`, + `No matching GitHub integration configuration for host ${host}, please check your integrations config`, ); } if (!config.config.apiBaseUrl) { throw new InputError( - `No apiBaseUrl available for location ${location}, please check your integrations config`, + `No apiBaseUrl available for host ${host}, please check your integrations config`, ); } @@ -65,20 +64,14 @@ export type GithubDeployment = { }; type QueryParams = { + host: string | undefined; owner: string; repo: string; last: number; }; -type QueryOptions = { - locations: string[]; -}; - export interface GithubDeploymentsApi { - listDeployments( - params: QueryParams, - options: QueryOptions, - ): Promise; + listDeployments(params: QueryParams): Promise; } export const githubDeploymentsApiRef = createApiRef({ @@ -130,11 +123,8 @@ export class GithubDeploymentsApiClient implements GithubDeploymentsApi { this.scmIntegrationsApi = options.scmIntegrationsApi; } - async listDeployments( - params: QueryParams, - options: QueryOptions, - ): Promise { - const baseUrl = getBaseUrl(this.scmIntegrationsApi, options.locations); + async listDeployments(params: QueryParams): Promise { + const baseUrl = getBaseUrl(this.scmIntegrationsApi, params.host); const token = await this.githubAuthApi.getAccessToken(['repo']); const graphQLWithAuth = graphql.defaults({ diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index cae8313422..5914e4b422 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -250,8 +250,7 @@ describe('github-deployments', () => { entity = entityStub; entity.entity.metadata.annotations = { 'github.com/project-slug': 'org/repo', - 'backstage.io/source-location': - 'url:https://my-github-1.com/org/repo', + 'backstage.io/source-location': 'github:my-github-1.com', }; }); @@ -272,8 +271,7 @@ describe('github-deployments', () => { entity = entityStub; entity.entity.metadata.annotations = { 'github.com/project-slug': 'org/repo', - 'backstage.io/managed-by-location': - 'url:https://my-github-2.com/org/repo', + 'backstage.io/managed-by-location': 'github:my-github-2.com', }; }); @@ -294,8 +292,7 @@ describe('github-deployments', () => { entity = entityStub; entity.entity.metadata.annotations = { 'github.com/project-slug': 'org/repo', - 'backstage.io/managed-by-location': - 'url:https://my-github-3.com/org/repo', + 'backstage.io/managed-by-location': 'github:my-github-3.com', }; }); @@ -308,7 +305,7 @@ describe('github-deployments', () => { expect( await rendered.findByText( - 'Warning: No apiBaseUrl available for location https://my-github-3.com/org/repo, please check your integrations config', + 'Warning: No apiBaseUrl available for host github:my-github-3.com, please check your integrations config', ), ).toBeInTheDocument(); }); @@ -320,7 +317,7 @@ describe('github-deployments', () => { entity.entity.metadata.annotations = { 'github.com/project-slug': 'org/repo', 'backstage.io/managed-by-location': - 'url:https://my-github-unknown.com/org/repo', + 'github:my-github-unknown.com/org/repo', }; }); @@ -333,7 +330,7 @@ describe('github-deployments', () => { expect( await rendered.findByText( - 'Warning: No matching GitHub integration configuration for location https://my-github-unknown.com/org/repo, please check your integrations config', + 'Warning: No matching GitHub integration configuration for host github:my-github-unknown.com/org/repo, please check your integrations config', ), ).toBeInTheDocument(); }); diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx index 959926683f..b7f2e7b1ba 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx @@ -37,18 +37,18 @@ const GithubDeploymentsComponent = ({ projectSlug, last, columns, - locations, + host, }: { projectSlug: string; last: number; - locations: string[]; columns: TableColumn[]; + host: string | undefined; }) => { const api = useApi(githubDeploymentsApiRef); const [owner, repo] = projectSlug.split('/'); const { loading, value, error, retry: reload } = useAsyncRetry( - async () => await api.listDeployments({ owner, repo, last }, { locations }), + async () => await api.listDeployments({ host, owner, repo, last }), ); if (error) { @@ -73,10 +73,10 @@ export const GithubDeploymentsCard = ({ columns?: TableColumn[]; }) => { const { entity } = useEntity(); - const locations = [ + const host: string | undefined = [ entity?.metadata.annotations?.[SOURCE_LOCATION_ANNOTATION], entity?.metadata.annotations?.[LOCATION_ANNOTATION], - ].filter(location => location !== undefined) as string[]; + ].filter(Boolean)[0]; return !isGithubDeploymentsAvailable(entity) ? ( @@ -86,7 +86,7 @@ export const GithubDeploymentsCard = ({ entity?.metadata.annotations?.[GITHUB_PROJECT_SLUG_ANNOTATION] || '' } last={last || 10} - locations={locations} + host={host} columns={columns || GithubDeploymentsTable.defaultDeploymentColumns} /> ); From 5542de095aaefa0d103413f220fcd317652a68ab Mon Sep 17 00:00:00 2001 From: Heather Lee Date: Fri, 7 May 2021 08:32:50 -0700 Subject: [PATCH 36/95] Add changeset Signed-off-by: Heather Lee --- .changeset/clean-starfishes-drop.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/clean-starfishes-drop.md diff --git a/.changeset/clean-starfishes-drop.md b/.changeset/clean-starfishes-drop.md new file mode 100644 index 0000000000..884bb8de9f --- /dev/null +++ b/.changeset/clean-starfishes-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +This makes the CatalogTable configurable with custom columns, passed through the CatalogPage component rendered on the home page. From 93ff8275dd050834972dda609723f2a9bf5b33c7 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 1 May 2021 21:23:50 +0200 Subject: [PATCH 37/95] WIP store-agnostic cache manager. Signed-off-by: Eric Peterson --- packages/backend-common/config.d.ts | 22 +++++ packages/backend-common/package.json | 4 + .../backend-common/src/cache/CacheManager.ts | 87 +++++++++++++++++++ packages/backend-common/src/cache/index.ts | 17 ++++ packages/backend-common/src/index.ts | 1 + packages/backend/src/index.ts | 4 +- packages/backend/src/types.ts | 2 + yarn.lock | 74 ++++++++++++++-- 8 files changed, 202 insertions(+), 9 deletions(-) create mode 100644 packages/backend-common/src/cache/CacheManager.ts create mode 100644 packages/backend-common/src/cache/index.ts diff --git a/packages/backend-common/config.d.ts b/packages/backend-common/config.d.ts index 41845f952e..b4cf8580d9 100644 --- a/packages/backend-common/config.d.ts +++ b/packages/backend-common/config.d.ts @@ -68,6 +68,28 @@ export interface Config { connection: string | object; }; + /** Cache connection configuration, select cache type using the `store` field */ + cache?: + | { + store: 'memory'; + } + | { + store: 'memcache'; + connection: { + /** + * An array of memcache hosts, optionally including a port number. + * e.g. 127.0.0.1:11211 + */ + hosts: string[]; + + /** + * Number of milliseconds to wait before assuming there is a + * network timeout. Defaults to 500ms. + */ + netTimeout?: number; + }; + }; + cors?: { origin?: string | string[]; methods?: string | string[]; diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index a144f1c23f..513fe2f985 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -36,10 +36,13 @@ "@backstage/integration": "^0.5.2", "@google-cloud/storage": "^5.8.0", "@octokit/rest": "^18.5.3", + "@types/cache-manager": "^3.4.0", "@types/cors": "^2.8.6", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "archiver": "^5.0.2", + "cache-manager": "^3.4.3", + "cache-manager-memcached-store": "^4.0.0", "compression": "^1.7.4", "concat-stream": "^2.0.0", "cors": "^2.8.5", @@ -54,6 +57,7 @@ "knex": "^0.95.1", "lodash": "^4.17.15", "logform": "^2.1.1", + "memcache-pp": "^0.3.3", "minimatch": "^3.0.4", "minimist": "^1.2.5", "morgan": "^1.10.0", diff --git a/packages/backend-common/src/cache/CacheManager.ts b/packages/backend-common/src/cache/CacheManager.ts new file mode 100644 index 0000000000..83e902a743 --- /dev/null +++ b/packages/backend-common/src/cache/CacheManager.ts @@ -0,0 +1,87 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; +import cacheManager from 'cache-manager'; +// @ts-expect-error +import Memcache from 'memcache-pp'; +// @ts-expect-error +import memcachedStore from 'cache-manager-memcached-store'; + +/** + * A Cache Manager, which is able to retrieve a `node-cache-manager`-compliant + * cache client, configured according to app configuration. If no cache is + * configured, or an unknown store is provided, a no-op cache instance will be + * returned. + */ +export class CacheManager { + private readonly storeGetterMap = { + memcache: this.getMemcacheClient, + memory: this.getMemoryClient, + none: this.getNoneClient, + }; + + /** + * Creates a new CacheManager instance by reading from the `backend` config + * section, specifically the `.cache` key. + * + * @param config The loaded application configuration. + */ + static fromConfig(config: Config): CacheManager { + return new CacheManager(config.getConfig('backend.cache')); + } + + private constructor(private readonly config: Config) {} + + getClientWithTtl(ttl: number): cacheManager.Cache { + const store = this.config.getOptionalString( + 'store', + ) as keyof CacheManager['storeGetterMap']; + + if (this.storeGetterMap.hasOwnProperty(store)) { + return this.storeGetterMap[store].call(this, ttl); + } + return this.storeGetterMap.none.call(this, ttl); + } + + private getMemcacheClient(ttl: number): cacheManager.Cache { + const hosts = this.config.getStringArray('connection.hosts'); + const netTimeout = this.config.getOptionalNumber('connection.netTimeout'); + return cacheManager.caching({ + store: memcachedStore, + driver: Memcache, + options: { + hosts, + ...(netTimeout && { netTimeout }), + }, + ttl, + }); + } + + private getMemoryClient(ttl: number): cacheManager.Cache { + return cacheManager.caching({ + store: 'memory', + ttl, + }); + } + + private getNoneClient(ttl: number): cacheManager.Cache { + return cacheManager.caching({ + store: 'none', + ttl, + }); + } +} diff --git a/packages/backend-common/src/cache/index.ts b/packages/backend-common/src/cache/index.ts new file mode 100644 index 0000000000..d405f02f46 --- /dev/null +++ b/packages/backend-common/src/cache/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 * from './CacheManager'; diff --git a/packages/backend-common/src/index.ts b/packages/backend-common/src/index.ts index 29b1062750..dab981a824 100644 --- a/packages/backend-common/src/index.ts +++ b/packages/backend-common/src/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +export * from './cache'; export * from './config'; export * from './database'; export * from './discovery'; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 00ed248df5..82b8e1cb7b 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -24,6 +24,7 @@ import Router from 'express-promise-router'; import { + CacheManager, createServiceBuilder, getRootLogger, loadBackendConfig, @@ -59,11 +60,12 @@ function makeCreateEnv(config: Config) { root.info(`Created UrlReader ${reader}`); const databaseManager = SingleConnectionDatabaseManager.fromConfig(config); + const cache = CacheManager.fromConfig(config); return (plugin: string): PluginEnvironment => { const logger = root.child({ type: 'plugin', plugin }); const database = databaseManager.forPlugin(plugin); - return { logger, database, config, reader, discovery }; + return { logger, cache, database, config, reader, discovery }; }; } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index f63b9dd780..a951562553 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -17,6 +17,7 @@ import { Logger } from 'winston'; import { Config } from '@backstage/config'; import { + CacheManager, PluginDatabaseManager, PluginEndpointDiscovery, UrlReader, @@ -24,6 +25,7 @@ import { export type PluginEnvironment = { logger: Logger; + cache: CacheManager; database: PluginDatabaseManager; config: Config; reader: UrlReader; diff --git a/yarn.lock b/yarn.lock index 77607f0637..4b1ebed024 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5737,6 +5737,11 @@ resolved "https://registry.npmjs.org/@types/btoa-lite/-/btoa-lite-1.0.0.tgz#e190a5a548e0b348adb0df9ac7fa5f1151c7cca4" integrity sha512-wJsiX1tosQ+J5+bY5LrSahHxr2wT+uME5UDwdN1kg4frt40euqA+wzECkmq4t5QbveHiJepfdThgQrPw6KiSlg== +"@types/cache-manager@^3.4.0": + version "3.4.0" + resolved "https://registry.npmjs.org/@types/cache-manager/-/cache-manager-3.4.0.tgz#414136ea3807a8cd071b8f20370c5df5dbffd382" + integrity sha512-XVbn2HS+O+Mk2SKRCjr01/8oD5p2Tv1fxxdBqJ0+Cl+UBNiz0WVY5rusHpMGx+qF6Vc2pnRwPVwSKbGaDApCpw== + "@types/cacheable-request@^6.0.1": version "6.0.1" resolved "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976" @@ -8152,6 +8157,11 @@ async@0.9.x: resolved "https://registry.npmjs.org/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0= +async@3.2.0, async@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" + integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== + async@^2.0.1, async@^2.6.1, async@^2.6.2: version "2.6.3" resolved "https://registry.npmjs.org/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" @@ -8826,7 +8836,7 @@ block-stream@*: dependencies: inherits "~2.0.0" -bluebird@3.7.2, bluebird@^3.3.5, bluebird@^3.5.1, bluebird@^3.5.5, bluebird@^3.7.2: +bluebird@3.7.2, bluebird@^3.3.5, bluebird@^3.4.1, bluebird@^3.5.1, bluebird@^3.5.5, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -9251,6 +9261,20 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" +cache-manager-memcached-store@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/cache-manager-memcached-store/-/cache-manager-memcached-store-4.0.0.tgz#f68d00cdfaa73696d13137b7b0f39397167d9220" + integrity sha512-Lv1WMaRC1FQHHqxE8Xbi7YWInhgfxjOmLEE6u1K0A3Rwmq+XRLyekKDM7aW9WIzF+PuII/RDKqBK/Ezo5H2QVw== + +cache-manager@^3.4.3: + version "3.4.3" + resolved "https://registry.npmjs.org/cache-manager/-/cache-manager-3.4.3.tgz#c978d58f82b414ade08903d4d72b56a80a4978be" + integrity sha512-6+Hfzy1SNs/thUwo+07pV0ozgxc4sadrAN0eFVGvXl/X9nz3J0BqEnnEoyxEn8jnF+UkEo0MKpyk9BO80hMeiQ== + dependencies: + async "3.2.0" + lodash "^4.17.21" + lru-cache "6.0.0" + cacheable-lookup@^5.0.3: version "5.0.3" resolved "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz#049fdc59dffdd4fc285e8f4f82936591bd59fec3" @@ -9457,6 +9481,11 @@ capture-exit@^2.0.0: dependencies: rsvp "^4.8.4" +carrier@^0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/carrier/-/carrier-0.3.0.tgz#bd295d1d3a7524cac63dd779b929ee22a362bad4" + integrity sha1-vSldHTp1JMrGPdd5uSnuIqNiutQ= + case-sensitive-paths-webpack-plugin@^2.2.0: version "2.3.0" resolved "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.3.0.tgz#23ac613cc9a856e4f88ff8bb73bbb5e989825cf7" @@ -10273,6 +10302,11 @@ connect-history-api-fallback@^1.6.0: resolved "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== +connection-parse@0.0.x: + version "0.0.7" + resolved "https://registry.npmjs.org/connection-parse/-/connection-parse-0.0.7.tgz#18e7318aab06a699267372b10c5226d25a1c9a69" + integrity sha1-GOcxiqsGppkmc3KxDFIm0locmmk= + console-browserify@^1.1.0: version "1.2.0" resolved "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" @@ -14746,6 +14780,14 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hashring@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/hashring/-/hashring-3.2.0.tgz#fda4efde8aa22cdb97fb1d2a65e88401e1c144ce" + integrity sha1-/aTv3oqiLNuX+x0qZeiEAeHBRM4= + dependencies: + connection-parse "0.0.x" + simple-lru-cache "0.0.x" + hast-util-parse-selector@^2.0.0: version "2.2.4" resolved "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.4.tgz#60c99d0b519e12ab4ed32e58f150ec3f61ed1974" @@ -18023,6 +18065,13 @@ lru-cache@2: resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" integrity sha1-bUUk6LlV+V1PW1iFHOId1y+06VI= +lru-cache@6.0.0, lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + lru-cache@^4.0.1: version "4.1.5" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -18038,13 +18087,6 @@ lru-cache@^5.0.0, lru-cache@^5.1.1: dependencies: yallist "^3.0.2" -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - lru-queue@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" @@ -18336,6 +18378,17 @@ media-typer@0.3.0: resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= +memcache-pp@^0.3.3: + version "0.3.3" + resolved "https://registry.npmjs.org/memcache-pp/-/memcache-pp-0.3.3.tgz#3124830e27d55a252499726510841590ac130c99" + integrity sha512-fMitetT5ulUs4DnFaoVqWHrTBiddLG/l8rMHXbV+ibuL7uWjBulaEf8koTBEweyrjmflqhbH4Uy9V709LadXSA== + dependencies: + bluebird "^3.4.1" + carrier "^0.3.0" + debug "^2.6.9" + hashring "^3.2.0" + immutable "^3.8.1" + memoize-one@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" @@ -23780,6 +23833,11 @@ simple-get@^3.0.2, simple-get@^3.0.3: once "^1.3.1" simple-concat "^1.0.0" +simple-lru-cache@0.0.x: + version "0.0.2" + resolved "https://registry.npmjs.org/simple-lru-cache/-/simple-lru-cache-0.0.2.tgz#d59cc3a193c1a5d0320f84ee732f6e4713e511dd" + integrity sha1-1ZzDoZPBpdAyD4Tucy9uRxPlEd0= + simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" From 6e0be00e1d1a4a27bf314983f9dbc74c7557682c Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sun, 2 May 2021 15:18:06 +0200 Subject: [PATCH 38/95] Wrap cache clients in a Backstage-specific CacheClient to handle key normalization and error handling + allow us the freedom to move away from cache-manager in the future if necessary. Signed-off-by: Eric Peterson --- app-config.yaml | 2 + .../backend-common/src/cache/CacheClient.ts | 94 +++++++++++++++++++ .../backend-common/src/cache/CacheManager.ts | 43 +++++++-- packages/backend-common/src/cache/index.ts | 1 + packages/backend/src/index.ts | 3 +- packages/backend/src/types.ts | 4 +- 6 files changed, 137 insertions(+), 10 deletions(-) create mode 100644 packages/backend-common/src/cache/CacheClient.ts diff --git a/app-config.yaml b/app-config.yaml index 3c4f2d7ea5..8224b2fb6a 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -26,6 +26,8 @@ backend: baseUrl: http://localhost:7000 listen: port: 7000 + cache: + store: memory database: client: sqlite3 connection: ':memory:' diff --git a/packages/backend-common/src/cache/CacheClient.ts b/packages/backend-common/src/cache/CacheClient.ts new file mode 100644 index 0000000000..9146131a10 --- /dev/null +++ b/packages/backend-common/src/cache/CacheClient.ts @@ -0,0 +1,94 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { JsonValue } from '@backstage/config'; +import cacheManager from 'cache-manager'; +import { createHash } from 'crypto'; + +type CacheClientArgs = { + client: cacheManager.Cache; + pluginId: string; +}; + +export interface CacheClient { + get(key: string): Promise; + set(key: string, value: JsonValue, ttl?: number): Promise; + delete(key: string): Promise; +} + +export class ConcreteCacheClient implements CacheClient { + private readonly client: cacheManager.Cache; + private readonly pluginId: string; + + constructor({ client, pluginId }: CacheClientArgs) { + this.client = client; + this.pluginId = pluginId; + } + + async get(key: string): Promise { + const k = this.getNormalizedKey(key); + try { + const data = (await this.client.get(k)) as string | undefined; + return this.unserializeData(data); + } catch (_e) { + return null; + } + } + + async set(key: string, value: JsonValue, ttl?: number): Promise { + const k = this.getNormalizedKey(key); + try { + const data = this.serializeData(value); + await this.client.set(k, data, ttl ? { ttl } : undefined); + } catch (_e) { + return; + } + } + + async delete(key: string): Promise { + const k = this.getNormalizedKey(key); + try { + await this.client.del(k); + } catch (_e) { + return; + } + } + + /** + * Namespaces key by plugin to discourage cross-plugin integration via the + * cache store. + */ + private getNormalizedKey(key: string): string { + // Namespace key by plugin ID and remove potentially invalid characters. + const candidateKey = `${this.pluginId}:${key}`; + const wellFormedKey = Buffer.from(candidateKey).toString('base64'); + + // Memcache in particular doesn't do well with keys > 250 bytes. + if (wellFormedKey.length < 250) { + return wellFormedKey; + } + + return createHash('md5').update(candidateKey).digest('hex'); + } + + private serializeData(data: JsonValue): string { + return JSON.stringify(data); + } + + private unserializeData(data: string | undefined): JsonValue { + return data ? JSON.parse(data) : null; + } +} diff --git a/packages/backend-common/src/cache/CacheManager.ts b/packages/backend-common/src/cache/CacheManager.ts index 83e902a743..f1c3b279fd 100644 --- a/packages/backend-common/src/cache/CacheManager.ts +++ b/packages/backend-common/src/cache/CacheManager.ts @@ -14,20 +14,28 @@ * limitations under the License. */ -import { Config } from '@backstage/config'; +import { Config, ConfigReader } from '@backstage/config'; import cacheManager from 'cache-manager'; // @ts-expect-error import Memcache from 'memcache-pp'; // @ts-expect-error import memcachedStore from 'cache-manager-memcached-store'; +import { ConcreteCacheClient, CacheClient } from './CacheClient'; + +export type PluginCacheManager = { + getClient: (ttl: number) => CacheClient; +}; /** - * A Cache Manager, which is able to retrieve a `node-cache-manager`-compliant - * cache client, configured according to app configuration. If no cache is - * configured, or an unknown store is provided, a no-op cache instance will be - * returned. + * Implements a Cache Manager which will automatically create new cache clients + * for plugins when requested. All requested cacheclients are created with the + * credentials provided. */ export class CacheManager { + /** + * Keys represented supported `backend.cache.store` values, mapped to getters + * that return cacheManager.Cache instances appropriate to the store. + */ private readonly storeGetterMap = { memcache: this.getMemcacheClient, memory: this.getMemoryClient, @@ -41,12 +49,33 @@ export class CacheManager { * @param config The loaded application configuration. */ static fromConfig(config: Config): CacheManager { - return new CacheManager(config.getConfig('backend.cache')); + // If no `backend.cache` config is provided, instantiate the CacheManager + // with empty config; allowing a "none" cache client will be returned. + return new CacheManager( + config.getOptionalConfig('backend.cache') || new ConfigReader(undefined), + ); } private constructor(private readonly config: Config) {} - getClientWithTtl(ttl: number): cacheManager.Cache { + /** + * Generates a CacheManagerInstance for consumption by plugins. + * + * @param pluginId The plugin that the cache manager should be created for. Plugin names should be unique. + */ + forPlugin(pluginId: string): PluginCacheManager { + return { + getClient: (ttl: number): CacheClient => { + const concreteClient = this.getClientWithTtl(ttl); + return new ConcreteCacheClient({ + client: concreteClient, + pluginId: pluginId, + }); + }, + }; + } + + private getClientWithTtl(ttl: number): cacheManager.Cache { const store = this.config.getOptionalString( 'store', ) as keyof CacheManager['storeGetterMap']; diff --git a/packages/backend-common/src/cache/index.ts b/packages/backend-common/src/cache/index.ts index d405f02f46..ba1625d043 100644 --- a/packages/backend-common/src/cache/index.ts +++ b/packages/backend-common/src/cache/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ +export * from './CacheClient'; export * from './CacheManager'; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 82b8e1cb7b..d43c8f0101 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -60,11 +60,12 @@ function makeCreateEnv(config: Config) { root.info(`Created UrlReader ${reader}`); const databaseManager = SingleConnectionDatabaseManager.fromConfig(config); - const cache = CacheManager.fromConfig(config); + const cacheManager = CacheManager.fromConfig(config); return (plugin: string): PluginEnvironment => { const logger = root.child({ type: 'plugin', plugin }); const database = databaseManager.forPlugin(plugin); + const cache = cacheManager.forPlugin(plugin); return { logger, cache, database, config, reader, discovery }; }; } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index a951562553..356dd08d5f 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -17,7 +17,7 @@ import { Logger } from 'winston'; import { Config } from '@backstage/config'; import { - CacheManager, + PluginCacheManager, PluginDatabaseManager, PluginEndpointDiscovery, UrlReader, @@ -25,7 +25,7 @@ import { export type PluginEnvironment = { logger: Logger; - cache: CacheManager; + cache: PluginCacheManager; database: PluginDatabaseManager; config: Config; reader: UrlReader; From c7fa03cd760c03021b0b2f4e6fedfb83e1572832 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sun, 2 May 2021 16:30:06 +0200 Subject: [PATCH 39/95] Test coverage for CacheManager and CacheClient Signed-off-by: Eric Peterson --- .../src/cache/CacheClient.test.ts | 141 ++++++++++++++++ .../src/cache/CacheManager.test.ts | 154 ++++++++++++++++++ 2 files changed, 295 insertions(+) create mode 100644 packages/backend-common/src/cache/CacheClient.test.ts create mode 100644 packages/backend-common/src/cache/CacheManager.test.ts diff --git a/packages/backend-common/src/cache/CacheClient.test.ts b/packages/backend-common/src/cache/CacheClient.test.ts new file mode 100644 index 0000000000..aadcee7386 --- /dev/null +++ b/packages/backend-common/src/cache/CacheClient.test.ts @@ -0,0 +1,141 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { ConcreteCacheClient } from './CacheClient'; +import cacheManager from 'cache-manager'; + +describe('CacheClient', () => { + const pluginId = 'test'; + let client: cacheManager.Cache; + const b64 = (k: string) => Buffer.from(k).toString('base64'); + + beforeEach(() => { + client = cacheManager.caching({ store: 'none', ttl: 0 }); + client.get = jest.fn(); + client.set = jest.fn(); + client.del = jest.fn(); + }); + + afterEach(() => jest.resetAllMocks()); + + describe('CacheClient.get', () => { + it('calls client with normalized key', async () => { + const sut = new ConcreteCacheClient({ client, pluginId }); + const keyPartial = 'somekey'; + + await sut.get(keyPartial); + + expect(client.get).toHaveBeenCalledWith(b64(`${pluginId}:${keyPartial}`)); + }); + + it('calls client with normalized key (very long key)', async () => { + const sut = new ConcreteCacheClient({ client, pluginId }); + const keyPartial = 'x'.repeat(251); + + await sut.get(keyPartial); + + const spy = client.get as jest.Mock; + const actualKey = spy.mock.calls[0][0]; + expect(actualKey).not.toEqual(b64(`${pluginId}:${keyPartial}`)); + expect(actualKey.length).toBeLessThan(250); + }); + + it('performs deserialization on returned data', async () => { + const expectedValue = { some: 'value' }; + const sut = new ConcreteCacheClient({ client, pluginId }); + client.get = jest.fn().mockResolvedValue(JSON.stringify(expectedValue)); + + const actualValue = await sut.get('someKey'); + + expect(actualValue).toMatchObject(expectedValue); + }); + + it('returns null on any underlying error', async () => { + const sut = new ConcreteCacheClient({ client, pluginId }); + client.get = jest.fn().mockRejectedValue(undefined); + + const actualValue = await sut.get('someKey'); + + expect(actualValue).toStrictEqual(null); + }); + }); + + describe('CacheClient.set', () => { + it('calls client with normalized key', async () => { + const sut = new ConcreteCacheClient({ client, pluginId }); + const keyPartial = 'somekey'; + + await sut.set(keyPartial, {}); + + const spy = client.set as jest.Mock; + const actualKey = spy.mock.calls[0][0]; + expect(actualKey).toEqual(b64(`${pluginId}:${keyPartial}`)); + }); + + it('performs serialization on given data', async () => { + const sut = new ConcreteCacheClient({ client, pluginId }); + const expectedData = { some: 'value' }; + + await sut.set('someKey', expectedData); + + const spy = client.set as jest.Mock; + const actualData = spy.mock.calls[0][1]; + expect(actualData).toEqual(JSON.stringify(expectedData)); + }); + + it('passes ttl to client when given', async () => { + const sut = new ConcreteCacheClient({ client, pluginId }); + const expectedTtl = 3600; + + await sut.set('someKey', {}, expectedTtl); + + const spy = client.set as jest.Mock; + const actualOptions = spy.mock.calls[0][2]; + expect(actualOptions.ttl).toEqual(expectedTtl); + }); + + it('does not throw errors on any client error', async () => { + const sut = new ConcreteCacheClient({ client, pluginId }); + client.set = jest.fn().mockRejectedValue(undefined); + + expect(async () => { + await sut.set('someKey', {}); + }).not.toThrow(); + }); + }); + + describe('CacheClient.delete', () => { + it('calls client with normalized key', async () => { + const sut = new ConcreteCacheClient({ client, pluginId }); + const keyPartial = 'somekey'; + + await sut.delete(keyPartial); + + const spy = client.del as jest.Mock; + const actualKey = spy.mock.calls[0][0]; + expect(actualKey).toEqual(b64(`${pluginId}:${keyPartial}`)); + }); + + it('does not throw errors on any client error', async () => { + const sut = new ConcreteCacheClient({ client, pluginId }); + client.del = jest.fn().mockRejectedValue(undefined); + + expect(async () => { + await sut.delete('someKey'); + }).not.toThrow(); + }); + }); +}); diff --git a/packages/backend-common/src/cache/CacheManager.test.ts b/packages/backend-common/src/cache/CacheManager.test.ts new file mode 100644 index 0000000000..af8450975a --- /dev/null +++ b/packages/backend-common/src/cache/CacheManager.test.ts @@ -0,0 +1,154 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigReader } from '@backstage/config'; +import cacheManager from 'cache-manager'; +import { ConcreteCacheClient } from './CacheClient'; +import { CacheManager } from './CacheManager'; + +cacheManager.caching = jest.fn() as jest.Mock; +jest.mock('./CacheClient', () => { + return { + ConcreteCacheClient: jest.fn(), + }; +}); + +describe('CacheManager', () => { + const defaultConfigOptions = { + backend: { + cache: { + store: 'memory', + }, + }, + }; + const defaultConfig = () => new ConfigReader(defaultConfigOptions); + + afterEach(() => jest.resetAllMocks()); + + describe('CacheManager.fromConfig', () => { + it('accesses the backend.cache key', () => { + const getOptionalConfig = jest.fn(); + const config = defaultConfig(); + config.getOptionalConfig = getOptionalConfig; + + CacheManager.fromConfig(config); + + expect(getOptionalConfig.mock.calls[0][0]).toEqual('backend.cache'); + }); + + it('does not require the backend.cache key', () => { + const config = new ConfigReader({ backend: {} }); + expect(() => { + CacheManager.fromConfig(config); + }).not.toThrowError(); + }); + }); + + describe('CacheManager.forPlugin', () => { + const manager = CacheManager.fromConfig(defaultConfig()); + + it('connects to a cache store scoped to the plugin', async () => { + const pluginId = 'test1'; + const expectedTtl = 3600; + manager.forPlugin(pluginId).getClient(expectedTtl); + + const client = ConcreteCacheClient as jest.Mock; + expect(client).toHaveBeenCalledTimes(1); + }); + + it('provides different plugins different cache clients', async () => { + const plugin1Id = 'test1'; + const plugin2Id = 'test2'; + const expectedTtl = 3600; + manager.forPlugin(plugin1Id).getClient(expectedTtl); + manager.forPlugin(plugin2Id).getClient(expectedTtl); + + const cache = cacheManager.caching as jest.Mock; + const client = ConcreteCacheClient as jest.Mock; + expect(cache).toHaveBeenCalledTimes(2); + expect(client).toHaveBeenCalledTimes(2); + + const plugin1CallArgs = client.mock.calls[0]; + const plugin2CallArgs = client.mock.calls[1]; + expect(plugin1CallArgs[0].pluginId).not.toEqual( + plugin2CallArgs[0].pluginId, + ); + }); + }); + + describe('CacheManager.forPlugin stores', () => { + it('returns none client when no cache is configured', () => { + const manager = CacheManager.fromConfig( + new ConfigReader({ backend: {} }), + ); + const expectedTtl = 3600; + manager.forPlugin('test').getClient(expectedTtl); + + const cache = cacheManager.caching as jest.Mock; + const mockCalls = cache.mock.calls.splice(-1); + const callArgs = mockCalls[0]; + expect(callArgs[0]).toMatchObject({ + store: 'none', + ttl: expectedTtl, + }); + }); + + it('returns memory client when configured', () => { + const manager = CacheManager.fromConfig(defaultConfig()); + const expectedTtl = 3600; + manager.forPlugin('test').getClient(expectedTtl); + + const cache = cacheManager.caching as jest.Mock; + const mockCalls = cache.mock.calls.splice(-1); + const callArgs = mockCalls[0]; + expect(callArgs[0]).toMatchObject({ + store: 'memory', + ttl: expectedTtl, + }); + }); + + it('returns a memcache client when configured', () => { + const expectedHost = '127.0.0.1:11211'; + const expectedTimeout = 1000; + const manager = CacheManager.fromConfig( + new ConfigReader({ + backend: { + cache: { + store: 'memcache', + connection: { + hosts: [expectedHost], + netTimeout: expectedTimeout, + }, + }, + }, + }), + ); + const expectedTtl = 3600; + manager.forPlugin('test').getClient(expectedTtl); + + const cache = cacheManager.caching as jest.Mock; + const mockCalls = cache.mock.calls.splice(-1); + const callArgs = mockCalls[0]; + expect(callArgs[0]).toMatchObject({ + options: { + hosts: [expectedHost], + netTimeout: expectedTimeout, + }, + ttl: expectedTtl, + }); + }); + }); +}); From b57fa279c5885e89ff25e757048c28a34724c6df Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sun, 2 May 2021 16:45:47 +0200 Subject: [PATCH 40/95] Clean up and document exported types Signed-off-by: Eric Peterson --- .../backend-common/src/cache/CacheClient.ts | 21 +++++++++++++ .../backend-common/src/cache/CacheManager.ts | 5 +-- packages/backend-common/src/cache/index.ts | 3 +- packages/backend-common/src/cache/types.ts | 31 +++++++++++++++++++ 4 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 packages/backend-common/src/cache/types.ts diff --git a/packages/backend-common/src/cache/CacheClient.ts b/packages/backend-common/src/cache/CacheClient.ts index 9146131a10..0716b159cb 100644 --- a/packages/backend-common/src/cache/CacheClient.ts +++ b/packages/backend-common/src/cache/CacheClient.ts @@ -23,12 +23,33 @@ type CacheClientArgs = { pluginId: string; }; +/** + * A pre-configured, storage agnostic cache client suitable for use by + * Backstage plugins. + */ export interface CacheClient { + /** + * Reads data from a cache store for the given key. + */ get(key: string): Promise; + + /** + * Writes the given data to a cache store, associated with the given key. An + * optional TTL may also be provided, otherwise it defaults to the TTL that + * was provided when the client was instantiated. + */ set(key: string, value: JsonValue, ttl?: number): Promise; + + /** + * Removes the given key from the cache store. + */ delete(key: string): Promise; } +/** + * A simple, concrete implementation of the CacheClient, suitable for almost + * all uses in Backstage. + */ export class ConcreteCacheClient implements CacheClient { private readonly client: cacheManager.Cache; private readonly pluginId: string; diff --git a/packages/backend-common/src/cache/CacheManager.ts b/packages/backend-common/src/cache/CacheManager.ts index f1c3b279fd..de27bb0e3d 100644 --- a/packages/backend-common/src/cache/CacheManager.ts +++ b/packages/backend-common/src/cache/CacheManager.ts @@ -21,10 +21,7 @@ import Memcache from 'memcache-pp'; // @ts-expect-error import memcachedStore from 'cache-manager-memcached-store'; import { ConcreteCacheClient, CacheClient } from './CacheClient'; - -export type PluginCacheManager = { - getClient: (ttl: number) => CacheClient; -}; +import { PluginCacheManager } from './types'; /** * Implements a Cache Manager which will automatically create new cache clients diff --git a/packages/backend-common/src/cache/index.ts b/packages/backend-common/src/cache/index.ts index ba1625d043..c233621bd4 100644 --- a/packages/backend-common/src/cache/index.ts +++ b/packages/backend-common/src/cache/index.ts @@ -14,5 +14,6 @@ * limitations under the License. */ -export * from './CacheClient'; +export type { CacheClient } from './CacheClient'; export * from './CacheManager'; +export * from './types'; diff --git a/packages/backend-common/src/cache/types.ts b/packages/backend-common/src/cache/types.ts new file mode 100644 index 0000000000..f1b9e96709 --- /dev/null +++ b/packages/backend-common/src/cache/types.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { CacheClient } from './CacheClient'; + +/** + * The PluginCacheManager manages access to cache stores that Plugins get. + */ +export type PluginCacheManager = { + /** + * getClient provides backend plugins cache connections for itself. + * + * The purpose of this method is to allow plugins to get isolated data + * stores so that plugins are discouraged from cache-level integration + * and/or cache key collisions. + */ + getClient: (ttl: number) => CacheClient; +}; From 669ff5ee2ecca049c25008db86d2b93221831355 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sun, 2 May 2021 16:46:12 +0200 Subject: [PATCH 41/95] Declare new public API Signed-off-by: Eric Peterson --- packages/backend-common/api-report.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index f0cb066dc0..339a291ff3 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -16,6 +16,7 @@ import { GithubCredentialsProvider } from '@backstage/integration'; import { GitHubIntegration } from '@backstage/integration'; import { GitLabIntegration } from '@backstage/integration'; import * as http from 'http'; +import { JsonValue } from '@backstage/config'; import { Knex } from 'knex'; import { Logger } from 'winston'; import { MergeResult } from 'isomorphic-git'; @@ -62,6 +63,19 @@ export class BitbucketUrlReader implements UrlReader { toString(): string; } +// @public +export interface CacheClient { + delete(key: string): Promise; + get(key: string): Promise; + set(key: string, value: JsonValue, ttl?: number): Promise; +} + +// @public +export class CacheManager { + forPlugin(pluginId: string): PluginCacheManager; + static fromConfig(config: Config): CacheManager; + } + // @public (undocumented) export const coloredFormat: winston.Logform.Format; @@ -238,6 +252,11 @@ export function loadBackendConfig(options: Options): Promise; // @public export function notFoundHandler(): RequestHandler; +// @public +export type PluginCacheManager = { + getClient: (ttl: number) => CacheClient; +}; + // @public export interface PluginDatabaseManager { getClient(): Promise; From 22fd8ce2ac4cfccef1aa8d1af30a8af6c0011e60 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sun, 2 May 2021 17:10:10 +0200 Subject: [PATCH 42/95] Changeset Signed-off-by: Eric Peterson --- .changeset/cool-cache-bro.md | 44 ++++++++++++++++++++++++++++++++++++ .github/styles/vocab.txt | 1 + 2 files changed, 45 insertions(+) create mode 100644 .changeset/cool-cache-bro.md diff --git a/.changeset/cool-cache-bro.md b/.changeset/cool-cache-bro.md new file mode 100644 index 0000000000..f86dc39339 --- /dev/null +++ b/.changeset/cool-cache-bro.md @@ -0,0 +1,44 @@ +--- +'@backstage/backend-common': minor +--- + +Introducing: a standard API for App Integrators to configure cache stores and Plugin Developers to interact with them. + +Two cache stores are currently supported. + +- `memory`, which is a very simple in-memory LRU cache, intended for local development. +- `memcache`, which can be used to connect to one or more memcache hosts. + +Configuring and working with cache stores is very similar to the process for database connections. + +```yaml +backend: + cache: + store: memcache + connection: + hosts: + - cache-a.example.com:11211 + - cache-b.example.com:11211 +``` + +```typescript +import { CacheManager } from '@backstage/backend-common'; + +// Instantiating a cache client for a plugin. +const cacheManager = CacheManager.fromConfig(config); +const somePluginCache = cacheManager.forPlugin('somePlugin'); +const defaultTtl = 3600; +const cacheClient = somePluginCache.getClient(defaultTtl); + +// Using the cache client: +const cachedValue = await cacheClient.get('someKey'); +if (cachedValue) { + return cachedValue; +} else { + const someValue = await someExpensiveProcess(); + await cacheClient.set('someKey', someValue); +} +await cacheClient.delete('someKey'); +``` + +Configuring a cache store is optional. Even when no cache store is configured, the cache manager will dutifully pass plugins a manager that resolves a cache client that does not actually write or read any data. diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 694226b8cc..ff856239bf 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -167,6 +167,7 @@ mailto maintainership makefile md +memcache microsite middleware minikube From ac8f7e4666c9c8e856e141d1ac0b060079e1a747 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sun, 2 May 2021 17:27:01 +0200 Subject: [PATCH 43/95] Dev dependency. Signed-off-by: Eric Peterson --- packages/backend-common/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 513fe2f985..0f331ccc6b 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -80,6 +80,7 @@ "@backstage/cli": "^0.6.10", "@backstage/test-utils": "^0.1.10", "@types/archiver": "^5.1.0", + "@types/cache-manager": "^3.4.0", "@types/compression": "^1.7.0", "@types/concat-stream": "^1.6.0", "@types/fs-extra": "^9.0.3", From b73dc5f46e19fcc45400279e25c0814d144f1850 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 3 May 2021 16:57:59 +0200 Subject: [PATCH 44/95] Clean up some types Signed-off-by: Eric Peterson --- .../src/cache/CacheClient.test.ts | 24 ++++++------- .../backend-common/src/cache/CacheClient.ts | 28 +++++++++------ .../src/cache/CacheManager.test.ts | 20 +++++------ .../backend-common/src/cache/CacheManager.ts | 35 ++++++++++--------- packages/backend-common/src/cache/index.ts | 4 +-- packages/backend-common/src/cache/types.ts | 6 +++- 6 files changed, 65 insertions(+), 52 deletions(-) diff --git a/packages/backend-common/src/cache/CacheClient.test.ts b/packages/backend-common/src/cache/CacheClient.test.ts index aadcee7386..5d56d84c61 100644 --- a/packages/backend-common/src/cache/CacheClient.test.ts +++ b/packages/backend-common/src/cache/CacheClient.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ConcreteCacheClient } from './CacheClient'; +import { DefaultCacheClient } from './CacheClient'; import cacheManager from 'cache-manager'; describe('CacheClient', () => { @@ -33,7 +33,7 @@ describe('CacheClient', () => { describe('CacheClient.get', () => { it('calls client with normalized key', async () => { - const sut = new ConcreteCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, pluginId }); const keyPartial = 'somekey'; await sut.get(keyPartial); @@ -42,7 +42,7 @@ describe('CacheClient', () => { }); it('calls client with normalized key (very long key)', async () => { - const sut = new ConcreteCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, pluginId }); const keyPartial = 'x'.repeat(251); await sut.get(keyPartial); @@ -55,7 +55,7 @@ describe('CacheClient', () => { it('performs deserialization on returned data', async () => { const expectedValue = { some: 'value' }; - const sut = new ConcreteCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, pluginId }); client.get = jest.fn().mockResolvedValue(JSON.stringify(expectedValue)); const actualValue = await sut.get('someKey'); @@ -64,7 +64,7 @@ describe('CacheClient', () => { }); it('returns null on any underlying error', async () => { - const sut = new ConcreteCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, pluginId }); client.get = jest.fn().mockRejectedValue(undefined); const actualValue = await sut.get('someKey'); @@ -75,7 +75,7 @@ describe('CacheClient', () => { describe('CacheClient.set', () => { it('calls client with normalized key', async () => { - const sut = new ConcreteCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, pluginId }); const keyPartial = 'somekey'; await sut.set(keyPartial, {}); @@ -86,7 +86,7 @@ describe('CacheClient', () => { }); it('performs serialization on given data', async () => { - const sut = new ConcreteCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, pluginId }); const expectedData = { some: 'value' }; await sut.set('someKey', expectedData); @@ -97,10 +97,10 @@ describe('CacheClient', () => { }); it('passes ttl to client when given', async () => { - const sut = new ConcreteCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, pluginId }); const expectedTtl = 3600; - await sut.set('someKey', {}, expectedTtl); + await sut.set('someKey', {}, { ttl: expectedTtl }); const spy = client.set as jest.Mock; const actualOptions = spy.mock.calls[0][2]; @@ -108,7 +108,7 @@ describe('CacheClient', () => { }); it('does not throw errors on any client error', async () => { - const sut = new ConcreteCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, pluginId }); client.set = jest.fn().mockRejectedValue(undefined); expect(async () => { @@ -119,7 +119,7 @@ describe('CacheClient', () => { describe('CacheClient.delete', () => { it('calls client with normalized key', async () => { - const sut = new ConcreteCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, pluginId }); const keyPartial = 'somekey'; await sut.delete(keyPartial); @@ -130,7 +130,7 @@ describe('CacheClient', () => { }); it('does not throw errors on any client error', async () => { - const sut = new ConcreteCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, pluginId }); client.del = jest.fn().mockRejectedValue(undefined); expect(async () => { diff --git a/packages/backend-common/src/cache/CacheClient.ts b/packages/backend-common/src/cache/CacheClient.ts index 0716b159cb..68c762eb87 100644 --- a/packages/backend-common/src/cache/CacheClient.ts +++ b/packages/backend-common/src/cache/CacheClient.ts @@ -23,6 +23,10 @@ type CacheClientArgs = { pluginId: string; }; +type CacheSetOptions = { + ttl?: number; +}; + /** * A pre-configured, storage agnostic cache client suitable for use by * Backstage plugins. @@ -38,7 +42,7 @@ export interface CacheClient { * optional TTL may also be provided, otherwise it defaults to the TTL that * was provided when the client was instantiated. */ - set(key: string, value: JsonValue, ttl?: number): Promise; + set(key: string, value: JsonValue, options: CacheSetOptions): Promise; /** * Removes the given key from the cache store. @@ -50,7 +54,7 @@ export interface CacheClient { * A simple, concrete implementation of the CacheClient, suitable for almost * all uses in Backstage. */ -export class ConcreteCacheClient implements CacheClient { +export class DefaultCacheClient implements CacheClient { private readonly client: cacheManager.Cache; private readonly pluginId: string; @@ -63,18 +67,22 @@ export class ConcreteCacheClient implements CacheClient { const k = this.getNormalizedKey(key); try { const data = (await this.client.get(k)) as string | undefined; - return this.unserializeData(data); - } catch (_e) { + return this.deserializeData(data); + } catch { return null; } } - async set(key: string, value: JsonValue, ttl?: number): Promise { + async set( + key: string, + value: JsonValue, + opts: CacheSetOptions = {}, + ): Promise { const k = this.getNormalizedKey(key); try { const data = this.serializeData(value); - await this.client.set(k, data, ttl ? { ttl } : undefined); - } catch (_e) { + await this.client.set(k, data, opts.ttl ? { ttl: opts.ttl } : undefined); + } catch { return; } } @@ -83,7 +91,7 @@ export class ConcreteCacheClient implements CacheClient { const k = this.getNormalizedKey(key); try { await this.client.del(k); - } catch (_e) { + } catch { return; } } @@ -102,14 +110,14 @@ export class ConcreteCacheClient implements CacheClient { return wellFormedKey; } - return createHash('md5').update(candidateKey).digest('hex'); + return createHash('md5').update(candidateKey).digest('base64'); } private serializeData(data: JsonValue): string { return JSON.stringify(data); } - private unserializeData(data: string | undefined): JsonValue { + private deserializeData(data: string | undefined): JsonValue { return data ? JSON.parse(data) : null; } } diff --git a/packages/backend-common/src/cache/CacheManager.test.ts b/packages/backend-common/src/cache/CacheManager.test.ts index af8450975a..625c772dec 100644 --- a/packages/backend-common/src/cache/CacheManager.test.ts +++ b/packages/backend-common/src/cache/CacheManager.test.ts @@ -16,13 +16,13 @@ import { ConfigReader } from '@backstage/config'; import cacheManager from 'cache-manager'; -import { ConcreteCacheClient } from './CacheClient'; +import { DefaultCacheClient } from './CacheClient'; import { CacheManager } from './CacheManager'; cacheManager.caching = jest.fn() as jest.Mock; jest.mock('./CacheClient', () => { return { - ConcreteCacheClient: jest.fn(), + DefaultCacheClient: jest.fn(), }; }); @@ -63,9 +63,9 @@ describe('CacheManager', () => { it('connects to a cache store scoped to the plugin', async () => { const pluginId = 'test1'; const expectedTtl = 3600; - manager.forPlugin(pluginId).getClient(expectedTtl); + manager.forPlugin(pluginId).getClient({ defaultTtl: expectedTtl }); - const client = ConcreteCacheClient as jest.Mock; + const client = DefaultCacheClient as jest.Mock; expect(client).toHaveBeenCalledTimes(1); }); @@ -73,11 +73,11 @@ describe('CacheManager', () => { const plugin1Id = 'test1'; const plugin2Id = 'test2'; const expectedTtl = 3600; - manager.forPlugin(plugin1Id).getClient(expectedTtl); - manager.forPlugin(plugin2Id).getClient(expectedTtl); + manager.forPlugin(plugin1Id).getClient({ defaultTtl: expectedTtl }); + manager.forPlugin(plugin2Id).getClient({ defaultTtl: expectedTtl }); const cache = cacheManager.caching as jest.Mock; - const client = ConcreteCacheClient as jest.Mock; + const client = DefaultCacheClient as jest.Mock; expect(cache).toHaveBeenCalledTimes(2); expect(client).toHaveBeenCalledTimes(2); @@ -95,7 +95,7 @@ describe('CacheManager', () => { new ConfigReader({ backend: {} }), ); const expectedTtl = 3600; - manager.forPlugin('test').getClient(expectedTtl); + manager.forPlugin('test').getClient({ defaultTtl: expectedTtl }); const cache = cacheManager.caching as jest.Mock; const mockCalls = cache.mock.calls.splice(-1); @@ -109,7 +109,7 @@ describe('CacheManager', () => { it('returns memory client when configured', () => { const manager = CacheManager.fromConfig(defaultConfig()); const expectedTtl = 3600; - manager.forPlugin('test').getClient(expectedTtl); + manager.forPlugin('test').getClient({ defaultTtl: expectedTtl }); const cache = cacheManager.caching as jest.Mock; const mockCalls = cache.mock.calls.splice(-1); @@ -137,7 +137,7 @@ describe('CacheManager', () => { }), ); const expectedTtl = 3600; - manager.forPlugin('test').getClient(expectedTtl); + manager.forPlugin('test').getClient({ defaultTtl: expectedTtl }); const cache = cacheManager.caching as jest.Mock; const mockCalls = cache.mock.calls.splice(-1); diff --git a/packages/backend-common/src/cache/CacheManager.ts b/packages/backend-common/src/cache/CacheManager.ts index de27bb0e3d..065a0b1db5 100644 --- a/packages/backend-common/src/cache/CacheManager.ts +++ b/packages/backend-common/src/cache/CacheManager.ts @@ -20,7 +20,7 @@ import cacheManager from 'cache-manager'; import Memcache from 'memcache-pp'; // @ts-expect-error import memcachedStore from 'cache-manager-memcached-store'; -import { ConcreteCacheClient, CacheClient } from './CacheClient'; +import { DefaultCacheClient, CacheClient } from './CacheClient'; import { PluginCacheManager } from './types'; /** @@ -30,10 +30,11 @@ import { PluginCacheManager } from './types'; */ export class CacheManager { /** - * Keys represented supported `backend.cache.store` values, mapped to getters - * that return cacheManager.Cache instances appropriate to the store. + * Keys represented supported `backend.cache.store` values, mapped to + * factories that return cacheManager.Cache instances appropriate to the + * store. */ - private readonly storeGetterMap = { + private readonly storeFactories = { memcache: this.getMemcacheClient, memory: this.getMemoryClient, none: this.getNoneClient, @@ -62,9 +63,9 @@ export class CacheManager { */ forPlugin(pluginId: string): PluginCacheManager { return { - getClient: (ttl: number): CacheClient => { - const concreteClient = this.getClientWithTtl(ttl); - return new ConcreteCacheClient({ + getClient: ({ defaultTtl }): CacheClient => { + const concreteClient = this.getClientWithTtl(defaultTtl); + return new DefaultCacheClient({ client: concreteClient, pluginId: pluginId, }); @@ -75,15 +76,15 @@ export class CacheManager { private getClientWithTtl(ttl: number): cacheManager.Cache { const store = this.config.getOptionalString( 'store', - ) as keyof CacheManager['storeGetterMap']; + ) as keyof CacheManager['storeFactories']; - if (this.storeGetterMap.hasOwnProperty(store)) { - return this.storeGetterMap[store].call(this, ttl); + if (this.storeFactories.hasOwnProperty(store)) { + return this.storeFactories[store].call(this, ttl); } - return this.storeGetterMap.none.call(this, ttl); + return this.storeFactories.none.call(this, ttl); } - private getMemcacheClient(ttl: number): cacheManager.Cache { + private getMemcacheClient(defaultTtl: number): cacheManager.Cache { const hosts = this.config.getStringArray('connection.hosts'); const netTimeout = this.config.getOptionalNumber('connection.netTimeout'); return cacheManager.caching({ @@ -93,21 +94,21 @@ export class CacheManager { hosts, ...(netTimeout && { netTimeout }), }, - ttl, + ttl: defaultTtl, }); } - private getMemoryClient(ttl: number): cacheManager.Cache { + private getMemoryClient(defaultTtl: number): cacheManager.Cache { return cacheManager.caching({ store: 'memory', - ttl, + ttl: defaultTtl, }); } - private getNoneClient(ttl: number): cacheManager.Cache { + private getNoneClient(defaultTtl: number): cacheManager.Cache { return cacheManager.caching({ store: 'none', - ttl, + ttl: defaultTtl, }); } } diff --git a/packages/backend-common/src/cache/index.ts b/packages/backend-common/src/cache/index.ts index c233621bd4..0cc8178b1a 100644 --- a/packages/backend-common/src/cache/index.ts +++ b/packages/backend-common/src/cache/index.ts @@ -15,5 +15,5 @@ */ export type { CacheClient } from './CacheClient'; -export * from './CacheManager'; -export * from './types'; +export { CacheManager } from './CacheManager'; +export type { PluginCacheManager } from './types'; diff --git a/packages/backend-common/src/cache/types.ts b/packages/backend-common/src/cache/types.ts index f1b9e96709..c93455e6b2 100644 --- a/packages/backend-common/src/cache/types.ts +++ b/packages/backend-common/src/cache/types.ts @@ -16,6 +16,10 @@ import { CacheClient } from './CacheClient'; +type ClientOptions = { + defaultTtl: number; +}; + /** * The PluginCacheManager manages access to cache stores that Plugins get. */ @@ -27,5 +31,5 @@ export type PluginCacheManager = { * stores so that plugins are discouraged from cache-level integration * and/or cache key collisions. */ - getClient: (ttl: number) => CacheClient; + getClient: (options: ClientOptions) => CacheClient; }; From cc6047512793026b842d678a9d57e81dd247b846 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 3 May 2021 17:09:20 +0200 Subject: [PATCH 45/95] Clarify default TTL behavior. Signed-off-by: Eric Peterson --- .../src/cache/CacheClient.test.ts | 31 +++++++++++++------ .../backend-common/src/cache/CacheClient.ts | 9 ++++-- .../backend-common/src/cache/CacheManager.ts | 1 + 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/packages/backend-common/src/cache/CacheClient.test.ts b/packages/backend-common/src/cache/CacheClient.test.ts index 5d56d84c61..2e14fdbea1 100644 --- a/packages/backend-common/src/cache/CacheClient.test.ts +++ b/packages/backend-common/src/cache/CacheClient.test.ts @@ -19,6 +19,7 @@ import cacheManager from 'cache-manager'; describe('CacheClient', () => { const pluginId = 'test'; + const defaultTtl = 60; let client: cacheManager.Cache; const b64 = (k: string) => Buffer.from(k).toString('base64'); @@ -33,7 +34,7 @@ describe('CacheClient', () => { describe('CacheClient.get', () => { it('calls client with normalized key', async () => { - const sut = new DefaultCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); const keyPartial = 'somekey'; await sut.get(keyPartial); @@ -42,7 +43,7 @@ describe('CacheClient', () => { }); it('calls client with normalized key (very long key)', async () => { - const sut = new DefaultCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); const keyPartial = 'x'.repeat(251); await sut.get(keyPartial); @@ -55,7 +56,7 @@ describe('CacheClient', () => { it('performs deserialization on returned data', async () => { const expectedValue = { some: 'value' }; - const sut = new DefaultCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); client.get = jest.fn().mockResolvedValue(JSON.stringify(expectedValue)); const actualValue = await sut.get('someKey'); @@ -64,7 +65,7 @@ describe('CacheClient', () => { }); it('returns null on any underlying error', async () => { - const sut = new DefaultCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); client.get = jest.fn().mockRejectedValue(undefined); const actualValue = await sut.get('someKey'); @@ -75,7 +76,7 @@ describe('CacheClient', () => { describe('CacheClient.set', () => { it('calls client with normalized key', async () => { - const sut = new DefaultCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); const keyPartial = 'somekey'; await sut.set(keyPartial, {}); @@ -86,7 +87,7 @@ describe('CacheClient', () => { }); it('performs serialization on given data', async () => { - const sut = new DefaultCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); const expectedData = { some: 'value' }; await sut.set('someKey', expectedData); @@ -97,7 +98,7 @@ describe('CacheClient', () => { }); it('passes ttl to client when given', async () => { - const sut = new DefaultCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); const expectedTtl = 3600; await sut.set('someKey', {}, { ttl: expectedTtl }); @@ -107,8 +108,18 @@ describe('CacheClient', () => { expect(actualOptions.ttl).toEqual(expectedTtl); }); + it('passes defaultTtl to client when not', async () => { + const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); + + await sut.set('someKey', {}); + + const spy = client.set as jest.Mock; + const actualOptions = spy.mock.calls[0][2]; + expect(actualOptions.ttl).toEqual(defaultTtl); + }); + it('does not throw errors on any client error', async () => { - const sut = new DefaultCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); client.set = jest.fn().mockRejectedValue(undefined); expect(async () => { @@ -119,7 +130,7 @@ describe('CacheClient', () => { describe('CacheClient.delete', () => { it('calls client with normalized key', async () => { - const sut = new DefaultCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); const keyPartial = 'somekey'; await sut.delete(keyPartial); @@ -130,7 +141,7 @@ describe('CacheClient', () => { }); it('does not throw errors on any client error', async () => { - const sut = new DefaultCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); client.del = jest.fn().mockRejectedValue(undefined); expect(async () => { diff --git a/packages/backend-common/src/cache/CacheClient.ts b/packages/backend-common/src/cache/CacheClient.ts index 68c762eb87..52703a28d4 100644 --- a/packages/backend-common/src/cache/CacheClient.ts +++ b/packages/backend-common/src/cache/CacheClient.ts @@ -21,6 +21,7 @@ import { createHash } from 'crypto'; type CacheClientArgs = { client: cacheManager.Cache; pluginId: string; + defaultTtl: number; }; type CacheSetOptions = { @@ -56,10 +57,12 @@ export interface CacheClient { */ export class DefaultCacheClient implements CacheClient { private readonly client: cacheManager.Cache; + private readonly defaultTtl: number; private readonly pluginId: string; - constructor({ client, pluginId }: CacheClientArgs) { + constructor({ client, defaultTtl, pluginId }: CacheClientArgs) { this.client = client; + this.defaultTtl = defaultTtl; this.pluginId = pluginId; } @@ -81,7 +84,9 @@ export class DefaultCacheClient implements CacheClient { const k = this.getNormalizedKey(key); try { const data = this.serializeData(value); - await this.client.set(k, data, opts.ttl ? { ttl: opts.ttl } : undefined); + await this.client.set(k, data, { + ttl: opts.ttl || this.defaultTtl, + }); } catch { return; } diff --git a/packages/backend-common/src/cache/CacheManager.ts b/packages/backend-common/src/cache/CacheManager.ts index 065a0b1db5..ecccbcf712 100644 --- a/packages/backend-common/src/cache/CacheManager.ts +++ b/packages/backend-common/src/cache/CacheManager.ts @@ -67,6 +67,7 @@ export class CacheManager { const concreteClient = this.getClientWithTtl(defaultTtl); return new DefaultCacheClient({ client: concreteClient, + defaultTtl, pluginId: pluginId, }); }, From b71c230beb1f5985eebe8deb78cfd88ce38e6134 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 3 May 2021 17:37:28 +0200 Subject: [PATCH 46/95] Refactor CacheManager.fromConfig Signed-off-by: Eric Peterson --- .../src/cache/CacheManager.test.ts | 16 ++++++++- .../backend-common/src/cache/CacheManager.ts | 35 +++++++++++-------- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/packages/backend-common/src/cache/CacheManager.test.ts b/packages/backend-common/src/cache/CacheManager.test.ts index 625c772dec..08fa06e311 100644 --- a/packages/backend-common/src/cache/CacheManager.test.ts +++ b/packages/backend-common/src/cache/CacheManager.test.ts @@ -41,12 +41,17 @@ describe('CacheManager', () => { describe('CacheManager.fromConfig', () => { it('accesses the backend.cache key', () => { const getOptionalConfig = jest.fn(); + const getOptionalString = jest.fn(); const config = defaultConfig(); config.getOptionalConfig = getOptionalConfig; + config.getOptionalString = getOptionalString; CacheManager.fromConfig(config); - expect(getOptionalConfig.mock.calls[0][0]).toEqual('backend.cache'); + expect(getOptionalString.mock.calls[0][0]).toEqual('backend.cache.store'); + expect(getOptionalConfig.mock.calls[0][0]).toEqual( + 'backend.cache.connection', + ); }); it('does not require the backend.cache key', () => { @@ -55,6 +60,15 @@ describe('CacheManager', () => { CacheManager.fromConfig(config); }).not.toThrowError(); }); + + it('throws on unknown cache store', () => { + const config = new ConfigReader({ + backend: { cache: { store: 'notreal' } }, + }); + expect(() => { + CacheManager.fromConfig(config); + }).toThrowError(); + }); }); describe('CacheManager.forPlugin', () => { diff --git a/packages/backend-common/src/cache/CacheManager.ts b/packages/backend-common/src/cache/CacheManager.ts index ecccbcf712..1c2c003b39 100644 --- a/packages/backend-common/src/cache/CacheManager.ts +++ b/packages/backend-common/src/cache/CacheManager.ts @@ -30,7 +30,7 @@ import { PluginCacheManager } from './types'; */ export class CacheManager { /** - * Keys represented supported `backend.cache.store` values, mapped to + * Keys represents supported `backend.cache.store` values, mapped to * factories that return cacheManager.Cache instances appropriate to the * store. */ @@ -40,6 +40,9 @@ export class CacheManager { none: this.getNoneClient, }; + private readonly store: keyof CacheManager['storeFactories']; + private readonly connection: Config; + /** * Creates a new CacheManager instance by reading from the `backend` config * section, specifically the `.cache` key. @@ -49,12 +52,21 @@ export class CacheManager { static fromConfig(config: Config): CacheManager { // If no `backend.cache` config is provided, instantiate the CacheManager // with empty config; allowing a "none" cache client will be returned. - return new CacheManager( - config.getOptionalConfig('backend.cache') || new ConfigReader(undefined), - ); + const store = config.getOptionalString('backend.cache.store') || 'none'; + const connectionConfig = + config.getOptionalConfig('backend.cache.connection') || + new ConfigReader(undefined); + + return new CacheManager(store, connectionConfig); } - private constructor(private readonly config: Config) {} + private constructor(store: string, connectionConfig: Config) { + if (!this.storeFactories.hasOwnProperty(store)) { + throw new Error(`Unknown cache store: ${store}`); + } + this.store = store as keyof CacheManager['storeFactories']; + this.connection = connectionConfig; + } /** * Generates a CacheManagerInstance for consumption by plugins. @@ -75,19 +87,12 @@ export class CacheManager { } private getClientWithTtl(ttl: number): cacheManager.Cache { - const store = this.config.getOptionalString( - 'store', - ) as keyof CacheManager['storeFactories']; - - if (this.storeFactories.hasOwnProperty(store)) { - return this.storeFactories[store].call(this, ttl); - } - return this.storeFactories.none.call(this, ttl); + return this.storeFactories[this.store].call(this, ttl); } private getMemcacheClient(defaultTtl: number): cacheManager.Cache { - const hosts = this.config.getStringArray('connection.hosts'); - const netTimeout = this.config.getOptionalNumber('connection.netTimeout'); + const hosts = this.connection.getStringArray('hosts'); + const netTimeout = this.connection.getOptionalNumber('netTimeout'); return cacheManager.caching({ store: memcachedStore, driver: Memcache, From 368a5158cc5ebaea6c21b2a8c6428307713e04f3 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 3 May 2021 18:13:12 +0200 Subject: [PATCH 47/95] Return an explicit undefined on no data. Signed-off-by: Eric Peterson --- .../backend-common/src/cache/CacheClient.test.ts | 4 ++-- packages/backend-common/src/cache/CacheClient.ts | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/backend-common/src/cache/CacheClient.test.ts b/packages/backend-common/src/cache/CacheClient.test.ts index 2e14fdbea1..2ae7cee494 100644 --- a/packages/backend-common/src/cache/CacheClient.test.ts +++ b/packages/backend-common/src/cache/CacheClient.test.ts @@ -64,13 +64,13 @@ describe('CacheClient', () => { expect(actualValue).toMatchObject(expectedValue); }); - it('returns null on any underlying error', async () => { + it('returns undefined on any underlying error', async () => { const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); client.get = jest.fn().mockRejectedValue(undefined); const actualValue = await sut.get('someKey'); - expect(actualValue).toStrictEqual(null); + expect(actualValue).toStrictEqual(undefined); }); }); diff --git a/packages/backend-common/src/cache/CacheClient.ts b/packages/backend-common/src/cache/CacheClient.ts index 52703a28d4..523dae6fed 100644 --- a/packages/backend-common/src/cache/CacheClient.ts +++ b/packages/backend-common/src/cache/CacheClient.ts @@ -34,9 +34,10 @@ type CacheSetOptions = { */ export interface CacheClient { /** - * Reads data from a cache store for the given key. + * Reads data from a cache store for the given key. If no data was found, + * returns undefined. */ - get(key: string): Promise; + get(key: string): Promise; /** * Writes the given data to a cache store, associated with the given key. An @@ -66,13 +67,13 @@ export class DefaultCacheClient implements CacheClient { this.pluginId = pluginId; } - async get(key: string): Promise { + async get(key: string): Promise { const k = this.getNormalizedKey(key); try { const data = (await this.client.get(k)) as string | undefined; return this.deserializeData(data); } catch { - return null; + return undefined; } } @@ -122,7 +123,7 @@ export class DefaultCacheClient implements CacheClient { return JSON.stringify(data); } - private deserializeData(data: string | undefined): JsonValue { - return data ? JSON.parse(data) : null; + private deserializeData(data: string | undefined): JsonValue | undefined { + return data ? JSON.parse(data) : undefined; } } From 7ff6a755752725bd855d5ccdfcff5e8076a2f04a Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 3 May 2021 18:50:07 +0200 Subject: [PATCH 48/95] Add an onError config to enable plugin devs to catch and handle underlying client errors Signed-off-by: Eric Peterson --- .../src/cache/CacheClient.test.ts | 129 +++++++++++++++--- .../backend-common/src/cache/CacheClient.ts | 22 ++- .../backend-common/src/cache/CacheManager.ts | 3 +- packages/backend-common/src/cache/types.ts | 1 + 4 files changed, 129 insertions(+), 26 deletions(-) diff --git a/packages/backend-common/src/cache/CacheClient.test.ts b/packages/backend-common/src/cache/CacheClient.test.ts index 2ae7cee494..a08bb245f8 100644 --- a/packages/backend-common/src/cache/CacheClient.test.ts +++ b/packages/backend-common/src/cache/CacheClient.test.ts @@ -20,6 +20,7 @@ import cacheManager from 'cache-manager'; describe('CacheClient', () => { const pluginId = 'test'; const defaultTtl = 60; + const onError = 'returnEmpty'; let client: cacheManager.Cache; const b64 = (k: string) => Buffer.from(k).toString('base64'); @@ -34,7 +35,12 @@ describe('CacheClient', () => { describe('CacheClient.get', () => { it('calls client with normalized key', async () => { - const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); + const sut = new DefaultCacheClient({ + client, + defaultTtl, + pluginId, + onError, + }); const keyPartial = 'somekey'; await sut.get(keyPartial); @@ -43,7 +49,12 @@ describe('CacheClient', () => { }); it('calls client with normalized key (very long key)', async () => { - const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); + const sut = new DefaultCacheClient({ + client, + defaultTtl, + pluginId, + onError, + }); const keyPartial = 'x'.repeat(251); await sut.get(keyPartial); @@ -56,7 +67,12 @@ describe('CacheClient', () => { it('performs deserialization on returned data', async () => { const expectedValue = { some: 'value' }; - const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); + const sut = new DefaultCacheClient({ + client, + defaultTtl, + pluginId, + onError, + }); client.get = jest.fn().mockResolvedValue(JSON.stringify(expectedValue)); const actualValue = await sut.get('someKey'); @@ -65,18 +81,41 @@ describe('CacheClient', () => { }); it('returns undefined on any underlying error', async () => { - const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); + const sut = new DefaultCacheClient({ + client, + defaultTtl, + pluginId, + onError, + }); client.get = jest.fn().mockRejectedValue(undefined); const actualValue = await sut.get('someKey'); expect(actualValue).toStrictEqual(undefined); }); + + it('rejects on underlying error if configured', async () => { + const sut = new DefaultCacheClient({ + client, + defaultTtl, + pluginId, + onError: 'reject', + }); + const expectedError = new Error('Some runtime error'); + client.get = jest.fn().mockRejectedValue(expectedError); + + return expect(sut.get('someKey')).rejects.toEqual(expectedError); + }); }); describe('CacheClient.set', () => { it('calls client with normalized key', async () => { - const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); + const sut = new DefaultCacheClient({ + client, + defaultTtl, + pluginId, + onError, + }); const keyPartial = 'somekey'; await sut.set(keyPartial, {}); @@ -87,7 +126,12 @@ describe('CacheClient', () => { }); it('performs serialization on given data', async () => { - const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); + const sut = new DefaultCacheClient({ + client, + defaultTtl, + pluginId, + onError, + }); const expectedData = { some: 'value' }; await sut.set('someKey', expectedData); @@ -98,7 +142,12 @@ describe('CacheClient', () => { }); it('passes ttl to client when given', async () => { - const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); + const sut = new DefaultCacheClient({ + client, + defaultTtl, + pluginId, + onError, + }); const expectedTtl = 3600; await sut.set('someKey', {}, { ttl: expectedTtl }); @@ -109,7 +158,12 @@ describe('CacheClient', () => { }); it('passes defaultTtl to client when not', async () => { - const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); + const sut = new DefaultCacheClient({ + client, + defaultTtl, + pluginId, + onError, + }); await sut.set('someKey', {}); @@ -118,19 +172,40 @@ describe('CacheClient', () => { expect(actualOptions.ttl).toEqual(defaultTtl); }); - it('does not throw errors on any client error', async () => { - const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); + it('does not reject on any client error by default', async () => { + const sut = new DefaultCacheClient({ + client, + defaultTtl, + pluginId, + onError, + }); client.set = jest.fn().mockRejectedValue(undefined); - expect(async () => { - await sut.set('someKey', {}); - }).not.toThrow(); + return expect(sut.set('someKey', {})).resolves.toBeUndefined(); + }); + + it('rejects on underlying error if configured', async () => { + const sut = new DefaultCacheClient({ + client, + defaultTtl, + pluginId, + onError: 'reject', + }); + const expectedError = new Error('Some runtime error'); + client.set = jest.fn().mockRejectedValue(expectedError); + + return expect(sut.set('someKey', {})).rejects.toEqual(expectedError); }); }); describe('CacheClient.delete', () => { it('calls client with normalized key', async () => { - const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); + const sut = new DefaultCacheClient({ + client, + defaultTtl, + pluginId, + onError, + }); const keyPartial = 'somekey'; await sut.delete(keyPartial); @@ -140,13 +215,29 @@ describe('CacheClient', () => { expect(actualKey).toEqual(b64(`${pluginId}:${keyPartial}`)); }); - it('does not throw errors on any client error', async () => { - const sut = new DefaultCacheClient({ client, defaultTtl, pluginId }); + it('does not reject on any client error by default', async () => { + const sut = new DefaultCacheClient({ + client, + defaultTtl, + pluginId, + onError, + }); client.del = jest.fn().mockRejectedValue(undefined); - expect(async () => { - await sut.delete('someKey'); - }).not.toThrow(); + return expect(sut.delete('someKey')).resolves.toBeUndefined(); + }); + + it('rejects on underlying error if configured', async () => { + const sut = new DefaultCacheClient({ + client, + defaultTtl, + pluginId, + onError: 'reject', + }); + const expectedError = new Error('Some runtime error'); + client.del = jest.fn().mockRejectedValue(expectedError); + + return expect(sut.delete('someKey')).rejects.toEqual(expectedError); }); }); }); diff --git a/packages/backend-common/src/cache/CacheClient.ts b/packages/backend-common/src/cache/CacheClient.ts index 523dae6fed..e1f849ccb7 100644 --- a/packages/backend-common/src/cache/CacheClient.ts +++ b/packages/backend-common/src/cache/CacheClient.ts @@ -22,6 +22,7 @@ type CacheClientArgs = { client: cacheManager.Cache; pluginId: string; defaultTtl: number; + onError: 'reject' | 'returnEmpty'; }; type CacheSetOptions = { @@ -60,11 +61,13 @@ export class DefaultCacheClient implements CacheClient { private readonly client: cacheManager.Cache; private readonly defaultTtl: number; private readonly pluginId: string; + private readonly onError: 'reject' | 'returnEmpty'; - constructor({ client, defaultTtl, pluginId }: CacheClientArgs) { + constructor({ client, defaultTtl, pluginId, onError }: CacheClientArgs) { this.client = client; this.defaultTtl = defaultTtl; this.pluginId = pluginId; + this.onError = onError; } async get(key: string): Promise { @@ -72,7 +75,10 @@ export class DefaultCacheClient implements CacheClient { try { const data = (await this.client.get(k)) as string | undefined; return this.deserializeData(data); - } catch { + } catch (e) { + if (this.onError === 'reject') { + throw e; + } return undefined; } } @@ -88,8 +94,10 @@ export class DefaultCacheClient implements CacheClient { await this.client.set(k, data, { ttl: opts.ttl || this.defaultTtl, }); - } catch { - return; + } catch (e) { + if (this.onError === 'reject') { + throw e; + } } } @@ -97,8 +105,10 @@ export class DefaultCacheClient implements CacheClient { const k = this.getNormalizedKey(key); try { await this.client.del(k); - } catch { - return; + } catch (e) { + if (this.onError === 'reject') { + throw e; + } } } diff --git a/packages/backend-common/src/cache/CacheManager.ts b/packages/backend-common/src/cache/CacheManager.ts index 1c2c003b39..13fb75e314 100644 --- a/packages/backend-common/src/cache/CacheManager.ts +++ b/packages/backend-common/src/cache/CacheManager.ts @@ -75,12 +75,13 @@ export class CacheManager { */ forPlugin(pluginId: string): PluginCacheManager { return { - getClient: ({ defaultTtl }): CacheClient => { + getClient: ({ defaultTtl, onError }): CacheClient => { const concreteClient = this.getClientWithTtl(defaultTtl); return new DefaultCacheClient({ client: concreteClient, defaultTtl, pluginId: pluginId, + onError: onError || 'returnEmpty', }); }, }; diff --git a/packages/backend-common/src/cache/types.ts b/packages/backend-common/src/cache/types.ts index c93455e6b2..5cc5782b18 100644 --- a/packages/backend-common/src/cache/types.ts +++ b/packages/backend-common/src/cache/types.ts @@ -18,6 +18,7 @@ import { CacheClient } from './CacheClient'; type ClientOptions = { defaultTtl: number; + onError?: 'reject' | 'returnEmpty'; }; /** From 884c2bd54f08b0d9e72eaee4fb81294ab2a92201 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 3 May 2021 18:54:28 +0200 Subject: [PATCH 49/95] Update docs to reflect changes Signed-off-by: Eric Peterson --- .changeset/cool-cache-bro.md | 17 ++++++++++++++++- packages/backend-common/api-report.md | 6 +++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/.changeset/cool-cache-bro.md b/.changeset/cool-cache-bro.md index f86dc39339..f342f39e55 100644 --- a/.changeset/cool-cache-bro.md +++ b/.changeset/cool-cache-bro.md @@ -28,7 +28,7 @@ import { CacheManager } from '@backstage/backend-common'; const cacheManager = CacheManager.fromConfig(config); const somePluginCache = cacheManager.forPlugin('somePlugin'); const defaultTtl = 3600; -const cacheClient = somePluginCache.getClient(defaultTtl); +const cacheClient = somePluginCache.getClient({ defaultTtl }); // Using the cache client: const cachedValue = await cacheClient.get('someKey'); @@ -41,4 +41,19 @@ if (cachedValue) { await cacheClient.delete('someKey'); ``` +For simplicity of use, cache clients will swallow client errors by default. Plugin developers may optionally pass an `onError` argument to the `CacheManager.getClient()` method if they wish to capture and handle those errors in a custom way. + +```typescript +const cacheClient = somePluginCache.getClient({ + defaultTtl: 3600, + onError: 'reject', +}); + +try { + await cacheClient.delete('someKey'); +} catch (e) { + // Attempt again, log, alert, etc. +} +``` + Configuring a cache store is optional. Even when no cache store is configured, the cache manager will dutifully pass plugins a manager that resolves a cache client that does not actually write or read any data. diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 339a291ff3..bd1b017574 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -66,8 +66,8 @@ export class BitbucketUrlReader implements UrlReader { // @public export interface CacheClient { delete(key: string): Promise; - get(key: string): Promise; - set(key: string, value: JsonValue, ttl?: number): Promise; + get(key: string): Promise; + set(key: string, value: JsonValue, options: CacheSetOptions): Promise; } // @public @@ -254,7 +254,7 @@ export function notFoundHandler(): RequestHandler; // @public export type PluginCacheManager = { - getClient: (ttl: number) => CacheClient; + getClient: (options: ClientOptions) => CacheClient; }; // @public From d6936dcbde0d4f0b7e542ab15a0be90d31a7d443 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sun, 9 May 2021 17:00:59 +0200 Subject: [PATCH 50/95] Swap out cache-manager for Keyv implementation. Signed-off-by: Eric Peterson --- packages/backend-common/config.d.ts | 18 +- packages/backend-common/package.json | 7 +- .../src/cache/CacheClient.test.ts | 215 +++++------------- .../backend-common/src/cache/CacheClient.ts | 78 ++----- .../src/cache/CacheManager.test.ts | 66 +++--- .../backend-common/src/cache/CacheManager.ts | 79 +++---- packages/backend-common/src/cache/types.ts | 10 +- yarn.lock | 109 ++++----- 8 files changed, 192 insertions(+), 390 deletions(-) diff --git a/packages/backend-common/config.d.ts b/packages/backend-common/config.d.ts index b4cf8580d9..875660a594 100644 --- a/packages/backend-common/config.d.ts +++ b/packages/backend-common/config.d.ts @@ -75,19 +75,11 @@ export interface Config { } | { store: 'memcache'; - connection: { - /** - * An array of memcache hosts, optionally including a port number. - * e.g. 127.0.0.1:11211 - */ - hosts: string[]; - - /** - * Number of milliseconds to wait before assuming there is a - * network timeout. Defaults to 500ms. - */ - netTimeout?: number; - }; + /** + * A memcache connection string in the form `user:pass@host:port`. + * @secret + */ + connection: string; }; cors?: { diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 0f331ccc6b..9bd7de1227 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -36,13 +36,10 @@ "@backstage/integration": "^0.5.2", "@google-cloud/storage": "^5.8.0", "@octokit/rest": "^18.5.3", - "@types/cache-manager": "^3.4.0", "@types/cors": "^2.8.6", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "archiver": "^5.0.2", - "cache-manager": "^3.4.3", - "cache-manager-memcached-store": "^4.0.0", "compression": "^1.7.4", "concat-stream": "^2.0.0", "cors": "^2.8.5", @@ -54,10 +51,11 @@ "git-url-parse": "^11.4.4", "helmet": "^4.0.0", "isomorphic-git": "^1.8.0", + "keyv": "^4.0.3", + "keyv-memcache": "^1.2.5", "knex": "^0.95.1", "lodash": "^4.17.15", "logform": "^2.1.1", - "memcache-pp": "^0.3.3", "minimatch": "^3.0.4", "minimist": "^1.2.5", "morgan": "^1.10.0", @@ -80,7 +78,6 @@ "@backstage/cli": "^0.6.10", "@backstage/test-utils": "^0.1.10", "@types/archiver": "^5.1.0", - "@types/cache-manager": "^3.4.0", "@types/compression": "^1.7.0", "@types/concat-stream": "^1.6.0", "@types/fs-extra": "^9.0.3", diff --git a/packages/backend-common/src/cache/CacheClient.test.ts b/packages/backend-common/src/cache/CacheClient.test.ts index a08bb245f8..eb32d4a28f 100644 --- a/packages/backend-common/src/cache/CacheClient.test.ts +++ b/packages/backend-common/src/cache/CacheClient.test.ts @@ -15,229 +15,132 @@ */ import { DefaultCacheClient } from './CacheClient'; -import cacheManager from 'cache-manager'; +import Keyv from 'keyv'; describe('CacheClient', () => { - const pluginId = 'test'; - const defaultTtl = 60; - const onError = 'returnEmpty'; - let client: cacheManager.Cache; + let client: Keyv; const b64 = (k: string) => Buffer.from(k).toString('base64'); beforeEach(() => { - client = cacheManager.caching({ store: 'none', ttl: 0 }); + client = new Keyv(); client.get = jest.fn(); client.set = jest.fn(); - client.del = jest.fn(); + client.delete = jest.fn(); }); afterEach(() => jest.resetAllMocks()); describe('CacheClient.get', () => { it('calls client with normalized key', async () => { - const sut = new DefaultCacheClient({ - client, - defaultTtl, - pluginId, - onError, - }); - const keyPartial = 'somekey'; + const sut = new DefaultCacheClient({ client }); + const key = 'somekey'; - await sut.get(keyPartial); + await sut.get(key); - expect(client.get).toHaveBeenCalledWith(b64(`${pluginId}:${keyPartial}`)); + expect(client.get).toHaveBeenCalledWith(b64(key)); }); it('calls client with normalized key (very long key)', async () => { - const sut = new DefaultCacheClient({ - client, - defaultTtl, - pluginId, - onError, - }); - const keyPartial = 'x'.repeat(251); + const sut = new DefaultCacheClient({ client }); + const key = 'x'.repeat(251); - await sut.get(keyPartial); + await sut.get(key); const spy = client.get as jest.Mock; const actualKey = spy.mock.calls[0][0]; - expect(actualKey).not.toEqual(b64(`${pluginId}:${keyPartial}`)); + expect(actualKey).not.toEqual(b64(key)); expect(actualKey.length).toBeLessThan(250); }); - it('performs deserialization on returned data', async () => { - const expectedValue = { some: 'value' }; - const sut = new DefaultCacheClient({ - client, - defaultTtl, - pluginId, - onError, - }); - client.get = jest.fn().mockResolvedValue(JSON.stringify(expectedValue)); - - const actualValue = await sut.get('someKey'); - - expect(actualValue).toMatchObject(expectedValue); - }); - - it('returns undefined on any underlying error', async () => { - const sut = new DefaultCacheClient({ - client, - defaultTtl, - pluginId, - onError, - }); - client.get = jest.fn().mockRejectedValue(undefined); - - const actualValue = await sut.get('someKey'); - - expect(actualValue).toStrictEqual(undefined); - }); - - it('rejects on underlying error if configured', async () => { - const sut = new DefaultCacheClient({ - client, - defaultTtl, - pluginId, - onError: 'reject', - }); + it('rejects on underlying error', async () => { + const sut = new DefaultCacheClient({ client }); const expectedError = new Error('Some runtime error'); client.get = jest.fn().mockRejectedValue(expectedError); return expect(sut.get('someKey')).rejects.toEqual(expectedError); }); + + it('resolves what underlying client resolves', async () => { + const sut = new DefaultCacheClient({ client }); + const expectedValue = 'some value'; + client.get = jest.fn().mockResolvedValue(expectedValue); + + const actualValue = await sut.get('someKey'); + + return expect(actualValue).toEqual(expectedValue); + }); }); describe('CacheClient.set', () => { it('calls client with normalized key', async () => { - const sut = new DefaultCacheClient({ - client, - defaultTtl, - pluginId, - onError, - }); - const keyPartial = 'somekey'; + const sut = new DefaultCacheClient({ client }); + const key = 'somekey'; - await sut.set(keyPartial, {}); + await sut.set(key, {}); const spy = client.set as jest.Mock; const actualKey = spy.mock.calls[0][0]; - expect(actualKey).toEqual(b64(`${pluginId}:${keyPartial}`)); - }); - - it('performs serialization on given data', async () => { - const sut = new DefaultCacheClient({ - client, - defaultTtl, - pluginId, - onError, - }); - const expectedData = { some: 'value' }; - - await sut.set('someKey', expectedData); - - const spy = client.set as jest.Mock; - const actualData = spy.mock.calls[0][1]; - expect(actualData).toEqual(JSON.stringify(expectedData)); + expect(actualKey).toEqual(b64(key)); }); it('passes ttl to client when given', async () => { - const sut = new DefaultCacheClient({ - client, - defaultTtl, - pluginId, - onError, - }); + const sut = new DefaultCacheClient({ client }); const expectedTtl = 3600; await sut.set('someKey', {}, { ttl: expectedTtl }); const spy = client.set as jest.Mock; - const actualOptions = spy.mock.calls[0][2]; - expect(actualOptions.ttl).toEqual(expectedTtl); - }); - - it('passes defaultTtl to client when not', async () => { - const sut = new DefaultCacheClient({ - client, - defaultTtl, - pluginId, - onError, - }); - - await sut.set('someKey', {}); - - const spy = client.set as jest.Mock; - const actualOptions = spy.mock.calls[0][2]; - expect(actualOptions.ttl).toEqual(defaultTtl); - }); - - it('does not reject on any client error by default', async () => { - const sut = new DefaultCacheClient({ - client, - defaultTtl, - pluginId, - onError, - }); - client.set = jest.fn().mockRejectedValue(undefined); - - return expect(sut.set('someKey', {})).resolves.toBeUndefined(); + const actualTtl = spy.mock.calls[0][2]; + expect(actualTtl).toEqual(expectedTtl); }); it('rejects on underlying error if configured', async () => { - const sut = new DefaultCacheClient({ - client, - defaultTtl, - pluginId, - onError: 'reject', - }); + const sut = new DefaultCacheClient({ client }); const expectedError = new Error('Some runtime error'); client.set = jest.fn().mockRejectedValue(expectedError); return expect(sut.set('someKey', {})).rejects.toEqual(expectedError); }); + + it('resolves what underlying client resolves', async () => { + const sut = new DefaultCacheClient({ client }); + const expectedResponse = true; + client.set = jest.fn().mockReturnValue(expectedResponse); + + const actualResponse = await sut.set('someKey', {}); + + return expect(actualResponse).toEqual(expectedResponse); + }); }); describe('CacheClient.delete', () => { it('calls client with normalized key', async () => { - const sut = new DefaultCacheClient({ - client, - defaultTtl, - pluginId, - onError, - }); - const keyPartial = 'somekey'; + const sut = new DefaultCacheClient({ client }); + const key = 'somekey'; - await sut.delete(keyPartial); + await sut.delete(key); - const spy = client.del as jest.Mock; + const spy = client.delete as jest.Mock; const actualKey = spy.mock.calls[0][0]; - expect(actualKey).toEqual(b64(`${pluginId}:${keyPartial}`)); - }); - - it('does not reject on any client error by default', async () => { - const sut = new DefaultCacheClient({ - client, - defaultTtl, - pluginId, - onError, - }); - client.del = jest.fn().mockRejectedValue(undefined); - - return expect(sut.delete('someKey')).resolves.toBeUndefined(); + expect(actualKey).toEqual(b64(key)); }); it('rejects on underlying error if configured', async () => { - const sut = new DefaultCacheClient({ - client, - defaultTtl, - pluginId, - onError: 'reject', - }); + const sut = new DefaultCacheClient({ client }); const expectedError = new Error('Some runtime error'); - client.del = jest.fn().mockRejectedValue(expectedError); + client.delete = jest.fn().mockRejectedValue(expectedError); return expect(sut.delete('someKey')).rejects.toEqual(expectedError); }); + + it('resolves what underlying client resolves', async () => { + const sut = new DefaultCacheClient({ client }); + const expectedResponse = false; + client.delete = jest.fn().mockResolvedValue(expectedResponse); + + const actualResponse = await sut.delete('someKey'); + + return expect(actualResponse).toEqual(expectedResponse); + }); }); }); diff --git a/packages/backend-common/src/cache/CacheClient.ts b/packages/backend-common/src/cache/CacheClient.ts index e1f849ccb7..943de055ae 100644 --- a/packages/backend-common/src/cache/CacheClient.ts +++ b/packages/backend-common/src/cache/CacheClient.ts @@ -15,17 +15,18 @@ */ import { JsonValue } from '@backstage/config'; -import cacheManager from 'cache-manager'; import { createHash } from 'crypto'; +import Keyv from 'keyv'; type CacheClientArgs = { - client: cacheManager.Cache; - pluginId: string; - defaultTtl: number; - onError: 'reject' | 'returnEmpty'; + client: Keyv; }; type CacheSetOptions = { + /** + * Optional TTL in milliseconds. Defaults to the TTL provided when the client + * was set up (or no TTL if none are provided). + */ ttl?: number; }; @@ -45,80 +46,50 @@ export interface CacheClient { * optional TTL may also be provided, otherwise it defaults to the TTL that * was provided when the client was instantiated. */ - set(key: string, value: JsonValue, options: CacheSetOptions): Promise; + set(key: string, value: JsonValue, options?: CacheSetOptions): Promise; /** - * Removes the given key from the cache store. + * Removes the given key from the cache store. Resolves true if the key + * existed, or false if not. */ - delete(key: string): Promise; + delete(key: string): Promise; } /** - * A simple, concrete implementation of the CacheClient, suitable for almost + * A basic, concrete implementation of the CacheClient, suitable for almost * all uses in Backstage. */ export class DefaultCacheClient implements CacheClient { - private readonly client: cacheManager.Cache; - private readonly defaultTtl: number; - private readonly pluginId: string; - private readonly onError: 'reject' | 'returnEmpty'; + private readonly client: Keyv; - constructor({ client, defaultTtl, pluginId, onError }: CacheClientArgs) { + constructor({ client }: CacheClientArgs) { this.client = client; - this.defaultTtl = defaultTtl; - this.pluginId = pluginId; - this.onError = onError; } async get(key: string): Promise { const k = this.getNormalizedKey(key); - try { - const data = (await this.client.get(k)) as string | undefined; - return this.deserializeData(data); - } catch (e) { - if (this.onError === 'reject') { - throw e; - } - return undefined; - } + return await this.client.get(k); } async set( key: string, value: JsonValue, opts: CacheSetOptions = {}, - ): Promise { + ): Promise { const k = this.getNormalizedKey(key); - try { - const data = this.serializeData(value); - await this.client.set(k, data, { - ttl: opts.ttl || this.defaultTtl, - }); - } catch (e) { - if (this.onError === 'reject') { - throw e; - } - } + return await this.client.set(k, value, opts.ttl) } - async delete(key: string): Promise { + async delete(key: string): Promise { const k = this.getNormalizedKey(key); - try { - await this.client.del(k); - } catch (e) { - if (this.onError === 'reject') { - throw e; - } - } + return await this.client.delete(k); } /** - * Namespaces key by plugin to discourage cross-plugin integration via the - * cache store. + * Ensures keys are well-formed for any/all cache stores. */ - private getNormalizedKey(key: string): string { - // Namespace key by plugin ID and remove potentially invalid characters. - const candidateKey = `${this.pluginId}:${key}`; + private getNormalizedKey(candidateKey: string): string { + // Remove potentially invalid characters. const wellFormedKey = Buffer.from(candidateKey).toString('base64'); // Memcache in particular doesn't do well with keys > 250 bytes. @@ -129,11 +100,4 @@ export class DefaultCacheClient implements CacheClient { return createHash('md5').update(candidateKey).digest('base64'); } - private serializeData(data: JsonValue): string { - return JSON.stringify(data); - } - - private deserializeData(data: string | undefined): JsonValue | undefined { - return data ? JSON.parse(data) : undefined; - } } diff --git a/packages/backend-common/src/cache/CacheManager.test.ts b/packages/backend-common/src/cache/CacheManager.test.ts index 08fa06e311..558964b55c 100644 --- a/packages/backend-common/src/cache/CacheManager.test.ts +++ b/packages/backend-common/src/cache/CacheManager.test.ts @@ -15,11 +15,16 @@ */ import { ConfigReader } from '@backstage/config'; -import cacheManager from 'cache-manager'; +import Keyv from 'keyv'; +/* @ts-expect-error */ +import KeyvMemcache from 'keyv-memcache'; import { DefaultCacheClient } from './CacheClient'; import { CacheManager } from './CacheManager'; -cacheManager.caching = jest.fn() as jest.Mock; +jest.createMockFromModule('keyv'); +jest.mock('keyv'); +jest.createMockFromModule('keyv-memcache'); +jest.mock('keyv-memcache'); jest.mock('./CacheClient', () => { return { DefaultCacheClient: jest.fn(), @@ -40,18 +45,14 @@ describe('CacheManager', () => { describe('CacheManager.fromConfig', () => { it('accesses the backend.cache key', () => { - const getOptionalConfig = jest.fn(); const getOptionalString = jest.fn(); const config = defaultConfig(); - config.getOptionalConfig = getOptionalConfig; config.getOptionalString = getOptionalString; CacheManager.fromConfig(config); expect(getOptionalString.mock.calls[0][0]).toEqual('backend.cache.store'); - expect(getOptionalConfig.mock.calls[0][0]).toEqual( - 'backend.cache.connection', - ); + expect(getOptionalString.mock.calls[1][0]).toEqual('backend.cache.connection'); }); it('does not require the backend.cache key', () => { @@ -76,8 +77,7 @@ describe('CacheManager', () => { it('connects to a cache store scoped to the plugin', async () => { const pluginId = 'test1'; - const expectedTtl = 3600; - manager.forPlugin(pluginId).getClient({ defaultTtl: expectedTtl }); + manager.forPlugin(pluginId).getClient(); const client = DefaultCacheClient as jest.Mock; expect(client).toHaveBeenCalledTimes(1); @@ -90,62 +90,60 @@ describe('CacheManager', () => { manager.forPlugin(plugin1Id).getClient({ defaultTtl: expectedTtl }); manager.forPlugin(plugin2Id).getClient({ defaultTtl: expectedTtl }); - const cache = cacheManager.caching as jest.Mock; const client = DefaultCacheClient as jest.Mock; + const cache = Keyv as unknown as jest.Mock; expect(cache).toHaveBeenCalledTimes(2); expect(client).toHaveBeenCalledTimes(2); - const plugin1CallArgs = client.mock.calls[0]; - const plugin2CallArgs = client.mock.calls[1]; - expect(plugin1CallArgs[0].pluginId).not.toEqual( - plugin2CallArgs[0].pluginId, + const plugin1CallArgs = cache.mock.calls[0]; + const plugin2CallArgs = cache.mock.calls[1]; + expect(plugin1CallArgs[0].namespace).not.toEqual( + plugin2CallArgs[0].namespace, ); }); }); describe('CacheManager.forPlugin stores', () => { - it('returns none client when no cache is configured', () => { + it('returns memory client when no cache is configured', () => { const manager = CacheManager.fromConfig( new ConfigReader({ backend: {} }), ); const expectedTtl = 3600; - manager.forPlugin('test').getClient({ defaultTtl: expectedTtl }); + const expectedNamespace = 'test-plugin'; + manager.forPlugin(expectedNamespace).getClient({ defaultTtl: expectedTtl }); - const cache = cacheManager.caching as jest.Mock; + const cache = Keyv as unknown as jest.Mock; const mockCalls = cache.mock.calls.splice(-1); const callArgs = mockCalls[0]; expect(callArgs[0]).toMatchObject({ - store: 'none', ttl: expectedTtl, + namespace: expectedNamespace, }); }); - it('returns memory client when configured', () => { + it('returns memory client when explicitly configured', () => { const manager = CacheManager.fromConfig(defaultConfig()); const expectedTtl = 3600; - manager.forPlugin('test').getClient({ defaultTtl: expectedTtl }); + const expectedNamespace = 'test-plugin'; + manager.forPlugin(expectedNamespace).getClient({ defaultTtl: expectedTtl }); - const cache = cacheManager.caching as jest.Mock; + const cache = Keyv as unknown as jest.Mock; const mockCalls = cache.mock.calls.splice(-1); const callArgs = mockCalls[0]; expect(callArgs[0]).toMatchObject({ - store: 'memory', ttl: expectedTtl, + namespace: expectedNamespace, }); }); it('returns a memcache client when configured', () => { const expectedHost = '127.0.0.1:11211'; - const expectedTimeout = 1000; const manager = CacheManager.fromConfig( new ConfigReader({ backend: { cache: { store: 'memcache', - connection: { - hosts: [expectedHost], - netTimeout: expectedTimeout, - }, + connection: expectedHost, }, }, }), @@ -153,16 +151,14 @@ describe('CacheManager', () => { const expectedTtl = 3600; manager.forPlugin('test').getClient({ defaultTtl: expectedTtl }); - const cache = cacheManager.caching as jest.Mock; - const mockCalls = cache.mock.calls.splice(-1); - const callArgs = mockCalls[0]; - expect(callArgs[0]).toMatchObject({ - options: { - hosts: [expectedHost], - netTimeout: expectedTimeout, - }, + const cache = Keyv as unknown as jest.Mock; + const mockCacheCalls = cache.mock.calls.splice(-1); + expect(mockCacheCalls[0][0]).toMatchObject({ ttl: expectedTtl, }); + const memcache = KeyvMemcache as jest.Mock; + const mockMemcacheCalls = memcache.mock.calls.splice(-1); + expect(mockMemcacheCalls[0][0]).toEqual(expectedHost); }); }); }); diff --git a/packages/backend-common/src/cache/CacheManager.ts b/packages/backend-common/src/cache/CacheManager.ts index 13fb75e314..00c4cebf18 100644 --- a/packages/backend-common/src/cache/CacheManager.ts +++ b/packages/backend-common/src/cache/CacheManager.ts @@ -14,34 +14,30 @@ * limitations under the License. */ -import { Config, ConfigReader } from '@backstage/config'; -import cacheManager from 'cache-manager'; +import { Config } from '@backstage/config'; +import Keyv from 'keyv'; // @ts-expect-error -import Memcache from 'memcache-pp'; -// @ts-expect-error -import memcachedStore from 'cache-manager-memcached-store'; +import KeyvMemcache from 'keyv-memcache'; import { DefaultCacheClient, CacheClient } from './CacheClient'; import { PluginCacheManager } from './types'; /** * Implements a Cache Manager which will automatically create new cache clients * for plugins when requested. All requested cacheclients are created with the - * credentials provided. + * connection details provided. */ export class CacheManager { /** - * Keys represents supported `backend.cache.store` values, mapped to - * factories that return cacheManager.Cache instances appropriate to the - * store. + * Keys represent supported `backend.cache.store` values, mapped to factories + * that return Keyv instances appropriate to the store. */ private readonly storeFactories = { memcache: this.getMemcacheClient, memory: this.getMemoryClient, - none: this.getNoneClient, }; private readonly store: keyof CacheManager['storeFactories']; - private readonly connection: Config; + private readonly connection: string; /** * Creates a new CacheManager instance by reading from the `backend` config @@ -51,21 +47,18 @@ export class CacheManager { */ static fromConfig(config: Config): CacheManager { // If no `backend.cache` config is provided, instantiate the CacheManager - // with empty config; allowing a "none" cache client will be returned. - const store = config.getOptionalString('backend.cache.store') || 'none'; - const connectionConfig = - config.getOptionalConfig('backend.cache.connection') || - new ConfigReader(undefined); - - return new CacheManager(store, connectionConfig); + // with a "memory" cache client. + const store = config.getOptionalString('backend.cache.store') || 'memory'; + const connectionString = config.getOptionalString('backend.cache.connection') || ''; + return new CacheManager(store, connectionString); } - private constructor(store: string, connectionConfig: Config) { + private constructor(store: string, connectionString: string) { if (!this.storeFactories.hasOwnProperty(store)) { throw new Error(`Unknown cache store: ${store}`); } this.store = store as keyof CacheManager['storeFactories']; - this.connection = connectionConfig; + this.connection = connectionString; } /** @@ -75,47 +68,33 @@ export class CacheManager { */ forPlugin(pluginId: string): PluginCacheManager { return { - getClient: ({ defaultTtl, onError }): CacheClient => { - const concreteClient = this.getClientWithTtl(defaultTtl); + getClient: (opts = {}): CacheClient => { + const concreteClient = this.getClientWithTtl(pluginId, opts.defaultTtl); return new DefaultCacheClient({ client: concreteClient, - defaultTtl, - pluginId: pluginId, - onError: onError || 'returnEmpty', }); }, }; } - private getClientWithTtl(ttl: number): cacheManager.Cache { - return this.storeFactories[this.store].call(this, ttl); + private getClientWithTtl(pluginId: string, ttl: number | undefined): Keyv { + return this.storeFactories[this.store].call(this, pluginId, ttl); } - private getMemcacheClient(defaultTtl: number): cacheManager.Cache { - const hosts = this.connection.getStringArray('hosts'); - const netTimeout = this.connection.getOptionalNumber('netTimeout'); - return cacheManager.caching({ - store: memcachedStore, - driver: Memcache, - options: { - hosts, - ...(netTimeout && { netTimeout }), - }, + private getMemcacheClient(pluginId: string, defaultTtl: number | undefined): Keyv { + const memcache = new KeyvMemcache(this.connection); + return new Keyv({ + namespace: pluginId, + ttl: defaultTtl, + store: memcache, + }); + } + + private getMemoryClient(pluginId: string, defaultTtl: number | undefined): Keyv { + return new Keyv({ + namespace: pluginId, ttl: defaultTtl, }); } - private getMemoryClient(defaultTtl: number): cacheManager.Cache { - return cacheManager.caching({ - store: 'memory', - ttl: defaultTtl, - }); - } - - private getNoneClient(defaultTtl: number): cacheManager.Cache { - return cacheManager.caching({ - store: 'none', - ttl: defaultTtl, - }); - } } diff --git a/packages/backend-common/src/cache/types.ts b/packages/backend-common/src/cache/types.ts index 5cc5782b18..4dd3cf7d1b 100644 --- a/packages/backend-common/src/cache/types.ts +++ b/packages/backend-common/src/cache/types.ts @@ -17,8 +17,12 @@ import { CacheClient } from './CacheClient'; type ClientOptions = { - defaultTtl: number; - onError?: 'reject' | 'returnEmpty'; + /** + * An optional default TTL (in milliseconds) to be set when getting a client + * instance. If not provided, data will persist indefinitely by default (or + * can be configured per entry at set-time). + */ + defaultTtl?: number; }; /** @@ -32,5 +36,5 @@ export type PluginCacheManager = { * stores so that plugins are discouraged from cache-level integration * and/or cache key collisions. */ - getClient: (options: ClientOptions) => CacheClient; + getClient: (options?: ClientOptions) => CacheClient; }; diff --git a/yarn.lock b/yarn.lock index 4b1ebed024..e792f84bfb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5737,11 +5737,6 @@ resolved "https://registry.npmjs.org/@types/btoa-lite/-/btoa-lite-1.0.0.tgz#e190a5a548e0b348adb0df9ac7fa5f1151c7cca4" integrity sha512-wJsiX1tosQ+J5+bY5LrSahHxr2wT+uME5UDwdN1kg4frt40euqA+wzECkmq4t5QbveHiJepfdThgQrPw6KiSlg== -"@types/cache-manager@^3.4.0": - version "3.4.0" - resolved "https://registry.npmjs.org/@types/cache-manager/-/cache-manager-3.4.0.tgz#414136ea3807a8cd071b8f20370c5df5dbffd382" - integrity sha512-XVbn2HS+O+Mk2SKRCjr01/8oD5p2Tv1fxxdBqJ0+Cl+UBNiz0WVY5rusHpMGx+qF6Vc2pnRwPVwSKbGaDApCpw== - "@types/cacheable-request@^6.0.1": version "6.0.1" resolved "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976" @@ -8157,11 +8152,6 @@ async@0.9.x: resolved "https://registry.npmjs.org/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0= -async@3.2.0, async@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" - integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== - async@^2.0.1, async@^2.6.1, async@^2.6.2: version "2.6.3" resolved "https://registry.npmjs.org/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" @@ -8836,7 +8826,7 @@ block-stream@*: dependencies: inherits "~2.0.0" -bluebird@3.7.2, bluebird@^3.3.5, bluebird@^3.4.1, bluebird@^3.5.1, bluebird@^3.5.5, bluebird@^3.7.2: +bluebird@3.7.2, bluebird@^3.3.5, bluebird@^3.5.1, bluebird@^3.5.5, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -9261,20 +9251,6 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" -cache-manager-memcached-store@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/cache-manager-memcached-store/-/cache-manager-memcached-store-4.0.0.tgz#f68d00cdfaa73696d13137b7b0f39397167d9220" - integrity sha512-Lv1WMaRC1FQHHqxE8Xbi7YWInhgfxjOmLEE6u1K0A3Rwmq+XRLyekKDM7aW9WIzF+PuII/RDKqBK/Ezo5H2QVw== - -cache-manager@^3.4.3: - version "3.4.3" - resolved "https://registry.npmjs.org/cache-manager/-/cache-manager-3.4.3.tgz#c978d58f82b414ade08903d4d72b56a80a4978be" - integrity sha512-6+Hfzy1SNs/thUwo+07pV0ozgxc4sadrAN0eFVGvXl/X9nz3J0BqEnnEoyxEn8jnF+UkEo0MKpyk9BO80hMeiQ== - dependencies: - async "3.2.0" - lodash "^4.17.21" - lru-cache "6.0.0" - cacheable-lookup@^5.0.3: version "5.0.3" resolved "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz#049fdc59dffdd4fc285e8f4f82936591bd59fec3" @@ -9481,11 +9457,6 @@ capture-exit@^2.0.0: dependencies: rsvp "^4.8.4" -carrier@^0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/carrier/-/carrier-0.3.0.tgz#bd295d1d3a7524cac63dd779b929ee22a362bad4" - integrity sha1-vSldHTp1JMrGPdd5uSnuIqNiutQ= - case-sensitive-paths-webpack-plugin@^2.2.0: version "2.3.0" resolved "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.3.0.tgz#23ac613cc9a856e4f88ff8bb73bbb5e989825cf7" @@ -10302,11 +10273,6 @@ connect-history-api-fallback@^1.6.0: resolved "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== -connection-parse@0.0.x: - version "0.0.7" - resolved "https://registry.npmjs.org/connection-parse/-/connection-parse-0.0.7.tgz#18e7318aab06a699267372b10c5226d25a1c9a69" - integrity sha1-GOcxiqsGppkmc3KxDFIm0locmmk= - console-browserify@^1.1.0: version "1.2.0" resolved "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" @@ -14489,9 +14455,9 @@ graphql-extensions@^0.12.8: apollo-server-env "^3.0.0" apollo-server-types "^0.6.3" -graphql-language-service-interface@2.8.2, graphql-language-service-interface@^2.8.2: +graphql-language-service-interface@^2.8.2: version "2.8.2" - resolved "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-2.8.2.tgz#b3bb2aef7eaf0dff0b4ea419fa412c5f66fa268b" + resolved "https://registry.yarnpkg.com/graphql-language-service-interface/-/graphql-language-service-interface-2.8.2.tgz#b3bb2aef7eaf0dff0b4ea419fa412c5f66fa268b" integrity sha512-otbOQmhgkAJU1QJgQkMztNku6SbJLu/uodoFOYOOtJsizTjrMs93vkYaHCcYnLA3oi1Goj27XcHjMnRCYQOZXQ== dependencies: graphql-language-service-parser "^1.9.0" @@ -14499,9 +14465,9 @@ graphql-language-service-interface@2.8.2, graphql-language-service-interface@^2. graphql-language-service-utils "^2.5.1" vscode-languageserver-types "^3.15.1" -graphql-language-service-parser@1.9.0, graphql-language-service-parser@^1.9.0: +graphql-language-service-parser@^1.9.0: version "1.9.0" - resolved "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.9.0.tgz#79af21294119a0a7e81b6b994a1af36833bab724" + resolved "https://registry.yarnpkg.com/graphql-language-service-parser/-/graphql-language-service-parser-1.9.0.tgz#79af21294119a0a7e81b6b994a1af36833bab724" integrity sha512-B5xPZLbBmIp0kHvpY1Z35I5DtPoDK9wGxQVRDIzcBaiIvAmlTrDvjo3bu7vKREdjFbYKvWNgrEWENuprMbF17Q== dependencies: graphql-language-service-types "^1.8.0" @@ -14780,14 +14746,6 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" -hashring@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/hashring/-/hashring-3.2.0.tgz#fda4efde8aa22cdb97fb1d2a65e88401e1c144ce" - integrity sha1-/aTv3oqiLNuX+x0qZeiEAeHBRM4= - dependencies: - connection-parse "0.0.x" - simple-lru-cache "0.0.x" - hast-util-parse-selector@^2.0.0: version "2.2.4" resolved "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.4.tgz#60c99d0b519e12ab4ed32e58f150ec3f61ed1974" @@ -16939,7 +16897,7 @@ json-buffer@3.0.0: resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= -json-buffer@3.0.1: +json-buffer@3.0.1, json-buffer@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== @@ -17291,6 +17249,14 @@ keytar@^5.4.0: nan "2.14.1" prebuild-install "5.3.3" +keyv-memcache@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/keyv-memcache/-/keyv-memcache-1.2.5.tgz#9097af5c617dc740e7300ebfd4a2efb8917429e3" + integrity sha512-iG+GHlhXyV83gmlCtMFIqNYVvv0i0towAy42NvziUR7D+k9jp81fAq0lu66H4gooOnW+ojkdigRvRpL40j1f7w== + dependencies: + json-buffer "^3.0.1" + memjs "^1.3.0" + keyv@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" @@ -17305,6 +17271,13 @@ keyv@^4.0.0: dependencies: json-buffer "3.0.1" +keyv@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.3.tgz#4f3aa98de254803cafcd2896734108daa35e4254" + integrity sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA== + dependencies: + json-buffer "3.0.1" + killable@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" @@ -18065,13 +18038,6 @@ lru-cache@2: resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" integrity sha1-bUUk6LlV+V1PW1iFHOId1y+06VI= -lru-cache@6.0.0, lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - lru-cache@^4.0.1: version "4.1.5" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -18087,6 +18053,13 @@ lru-cache@^5.0.0, lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + lru-queue@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" @@ -18378,16 +18351,10 @@ media-typer@0.3.0: resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -memcache-pp@^0.3.3: - version "0.3.3" - resolved "https://registry.npmjs.org/memcache-pp/-/memcache-pp-0.3.3.tgz#3124830e27d55a252499726510841590ac130c99" - integrity sha512-fMitetT5ulUs4DnFaoVqWHrTBiddLG/l8rMHXbV+ibuL7uWjBulaEf8koTBEweyrjmflqhbH4Uy9V709LadXSA== - dependencies: - bluebird "^3.4.1" - carrier "^0.3.0" - debug "^2.6.9" - hashring "^3.2.0" - immutable "^3.8.1" +memjs@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/memjs/-/memjs-1.3.0.tgz#b7959b4ff3770e4c785463fd147f1e4fafd47a24" + integrity sha512-y/V9a0auepA9Lgyr4QieK6K2FczjHucEdTpSS+hHVNmVEkYxruXhkHu8n6DSRQ4HXHEE3cc6Sf9f88WCJXGXsQ== memoize-one@^5.1.1: version "5.1.1" @@ -23833,11 +23800,6 @@ simple-get@^3.0.2, simple-get@^3.0.3: once "^1.3.1" simple-concat "^1.0.0" -simple-lru-cache@0.0.x: - version "0.0.2" - resolved "https://registry.npmjs.org/simple-lru-cache/-/simple-lru-cache-0.0.2.tgz#d59cc3a193c1a5d0320f84ee732f6e4713e511dd" - integrity sha1-1ZzDoZPBpdAyD4Tucy9uRxPlEd0= - simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" @@ -25779,11 +25741,16 @@ typescript-json-schema@^0.49.0: typescript "^4.1.3" yargs "^16.2.0" -typescript@^4.0.3, typescript@^4.1.3, typescript@~4.1.3: +typescript@^4.0.3, typescript@^4.1.3: version "4.2.3" resolved "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3" integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw== +typescript@~4.1.3: + version "4.1.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72" + integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA== + ua-parser-js@^0.7.18: version "0.7.28" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" From 38e943da0ccec78bb36a506b40ba9d9b0e5e496c Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sun, 9 May 2021 17:52:06 +0200 Subject: [PATCH 51/95] Implement a NoStore cache backend, and do not provide a cache backend by default. Signed-off-by: Eric Peterson --- app-config.yaml | 2 - .../src/cache/CacheManager.test.ts | 12 +++--- .../backend-common/src/cache/CacheManager.ts | 17 +++++--- packages/backend-common/src/cache/NoStore.ts | 43 +++++++++++++++++++ 4 files changed, 60 insertions(+), 14 deletions(-) create mode 100644 packages/backend-common/src/cache/NoStore.ts diff --git a/app-config.yaml b/app-config.yaml index 8224b2fb6a..3c4f2d7ea5 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -26,8 +26,6 @@ backend: baseUrl: http://localhost:7000 listen: port: 7000 - cache: - store: memory database: client: sqlite3 connection: ':memory:' diff --git a/packages/backend-common/src/cache/CacheManager.test.ts b/packages/backend-common/src/cache/CacheManager.test.ts index 558964b55c..153ccfd7ac 100644 --- a/packages/backend-common/src/cache/CacheManager.test.ts +++ b/packages/backend-common/src/cache/CacheManager.test.ts @@ -20,6 +20,7 @@ import Keyv from 'keyv'; import KeyvMemcache from 'keyv-memcache'; import { DefaultCacheClient } from './CacheClient'; import { CacheManager } from './CacheManager'; +import { NoStore } from './NoStore'; jest.createMockFromModule('keyv'); jest.mock('keyv'); @@ -104,21 +105,17 @@ describe('CacheManager', () => { }); describe('CacheManager.forPlugin stores', () => { - it('returns memory client when no cache is configured', () => { + it('returns none client when no cache is configured', () => { const manager = CacheManager.fromConfig( new ConfigReader({ backend: {} }), ); - const expectedTtl = 3600; const expectedNamespace = 'test-plugin'; - manager.forPlugin(expectedNamespace).getClient({ defaultTtl: expectedTtl }); + manager.forPlugin(expectedNamespace).getClient(); const cache = Keyv as unknown as jest.Mock; const mockCalls = cache.mock.calls.splice(-1); const callArgs = mockCalls[0]; - expect(callArgs[0]).toMatchObject({ - ttl: expectedTtl, - namespace: expectedNamespace, - }); + expect(callArgs[0].store).toBeInstanceOf(NoStore); }); it('returns memory client when explicitly configured', () => { @@ -156,6 +153,7 @@ describe('CacheManager', () => { expect(mockCacheCalls[0][0]).toMatchObject({ ttl: expectedTtl, }); + expect(mockCacheCalls[0][0].store).toBeInstanceOf(KeyvMemcache); const memcache = KeyvMemcache as jest.Mock; const mockMemcacheCalls = memcache.mock.calls.splice(-1); expect(mockMemcacheCalls[0][0]).toEqual(expectedHost); diff --git a/packages/backend-common/src/cache/CacheManager.ts b/packages/backend-common/src/cache/CacheManager.ts index 00c4cebf18..967ff4224b 100644 --- a/packages/backend-common/src/cache/CacheManager.ts +++ b/packages/backend-common/src/cache/CacheManager.ts @@ -19,11 +19,12 @@ import Keyv from 'keyv'; // @ts-expect-error import KeyvMemcache from 'keyv-memcache'; import { DefaultCacheClient, CacheClient } from './CacheClient'; +import { NoStore } from './NoStore'; import { PluginCacheManager } from './types'; /** * Implements a Cache Manager which will automatically create new cache clients - * for plugins when requested. All requested cacheclients are created with the + * for plugins when requested. All requested cache clients are created with the * connection details provided. */ export class CacheManager { @@ -34,6 +35,7 @@ export class CacheManager { private readonly storeFactories = { memcache: this.getMemcacheClient, memory: this.getMemoryClient, + none: this.getNoneClient, }; private readonly store: keyof CacheManager['storeFactories']; @@ -47,8 +49,8 @@ export class CacheManager { */ static fromConfig(config: Config): CacheManager { // If no `backend.cache` config is provided, instantiate the CacheManager - // with a "memory" cache client. - const store = config.getOptionalString('backend.cache.store') || 'memory'; + // with a "NoStore" cache client. + const store = config.getOptionalString('backend.cache.store') || 'none'; const connectionString = config.getOptionalString('backend.cache.connection') || ''; return new CacheManager(store, connectionString); } @@ -82,11 +84,10 @@ export class CacheManager { } private getMemcacheClient(pluginId: string, defaultTtl: number | undefined): Keyv { - const memcache = new KeyvMemcache(this.connection); return new Keyv({ namespace: pluginId, ttl: defaultTtl, - store: memcache, + store: new KeyvMemcache(this.connection), }); } @@ -97,4 +98,10 @@ export class CacheManager { }); } + private getNoneClient(pluginId: string): Keyv { + return new Keyv({ + namespace: pluginId, + store: new NoStore(), + }) + } } diff --git a/packages/backend-common/src/cache/NoStore.ts b/packages/backend-common/src/cache/NoStore.ts new file mode 100644 index 0000000000..6acb7c0309 --- /dev/null +++ b/packages/backend-common/src/cache/NoStore.ts @@ -0,0 +1,43 @@ +/* + * Copyright 2021 Spotify AB + * + * 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. + */ + +/** + * Storage class compatible with Keyv which always results in a no-op. This is + * used when no cache store is configured in a Backstage backend instance. + */ +export class NoStore extends Map { + + clear(): void { + return; + } + + delete(_key: string): boolean { + return false; + } + + get(_key: string) { + return; + } + + has(_key: string): boolean { + return false; + } + + set(_key: string, _value: any): this { + return this; + } + +} From d48b375a81718c9a2b9da7d5c19631a8c754b80d Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sun, 9 May 2021 17:52:38 +0200 Subject: [PATCH 52/95] Update changeset to reflect most recent changes. Signed-off-by: Eric Peterson --- .changeset/cool-cache-bro.md | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/.changeset/cool-cache-bro.md b/.changeset/cool-cache-bro.md index f342f39e55..9d5f998897 100644 --- a/.changeset/cool-cache-bro.md +++ b/.changeset/cool-cache-bro.md @@ -6,8 +6,8 @@ Introducing: a standard API for App Integrators to configure cache stores and Pl Two cache stores are currently supported. -- `memory`, which is a very simple in-memory LRU cache, intended for local development. -- `memcache`, which can be used to connect to one or more memcache hosts. +- `memory`, which is a very simple in-memory key/value store, intended for local development. +- `memcache`, which can be used to connect to a memcache host. Configuring and working with cache stores is very similar to the process for database connections. @@ -15,10 +15,7 @@ Configuring and working with cache stores is very similar to the process for dat backend: cache: store: memcache - connection: - hosts: - - cache-a.example.com:11211 - - cache-b.example.com:11211 + connection: user:pass@cache.example.com:11211 ``` ```typescript @@ -27,8 +24,7 @@ import { CacheManager } from '@backstage/backend-common'; // Instantiating a cache client for a plugin. const cacheManager = CacheManager.fromConfig(config); const somePluginCache = cacheManager.forPlugin('somePlugin'); -const defaultTtl = 3600; -const cacheClient = somePluginCache.getClient({ defaultTtl }); +const cacheClient = somePluginCache.getClient(); // Using the cache client: const cachedValue = await cacheClient.get('someKey'); @@ -41,19 +37,16 @@ if (cachedValue) { await cacheClient.delete('someKey'); ``` -For simplicity of use, cache clients will swallow client errors by default. Plugin developers may optionally pass an `onError` argument to the `CacheManager.getClient()` method if they wish to capture and handle those errors in a custom way. +Cache clients deal with TTLs in milliseconds. A TTL can be provided as a defaultTtl when getting a client, or may be passed when setting specific objects. If no TTL is provided, data will be persisted indefinitely. ```typescript +// Getting a client with a default TTL const cacheClient = somePluginCache.getClient({ - defaultTtl: 3600, - onError: 'reject', + defaultTtl: 3600000, }); -try { - await cacheClient.delete('someKey'); -} catch (e) { - // Attempt again, log, alert, etc. -} +// Setting a TTL on a per-object basis. +cacheClient.set('someKey', data, { ttl: 3600000 }); ``` Configuring a cache store is optional. Even when no cache store is configured, the cache manager will dutifully pass plugins a manager that resolves a cache client that does not actually write or read any data. From 03ae33c064aa194b45b7ba5c6a756cb7438bd633 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sun, 9 May 2021 18:08:57 +0200 Subject: [PATCH 53/95] Prettier Signed-off-by: Eric Peterson --- packages/backend-common/src/cache/CacheClient.ts | 9 ++++++--- .../src/cache/CacheManager.test.ts | 16 ++++++++++------ .../backend-common/src/cache/CacheManager.ts | 15 +++++++++++---- packages/backend-common/src/cache/NoStore.ts | 2 -- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/packages/backend-common/src/cache/CacheClient.ts b/packages/backend-common/src/cache/CacheClient.ts index 943de055ae..a2c048c265 100644 --- a/packages/backend-common/src/cache/CacheClient.ts +++ b/packages/backend-common/src/cache/CacheClient.ts @@ -46,7 +46,11 @@ export interface CacheClient { * optional TTL may also be provided, otherwise it defaults to the TTL that * was provided when the client was instantiated. */ - set(key: string, value: JsonValue, options?: CacheSetOptions): Promise; + set( + key: string, + value: JsonValue, + options?: CacheSetOptions, + ): Promise; /** * Removes the given key from the cache store. Resolves true if the key @@ -77,7 +81,7 @@ export class DefaultCacheClient implements CacheClient { opts: CacheSetOptions = {}, ): Promise { const k = this.getNormalizedKey(key); - return await this.client.set(k, value, opts.ttl) + return await this.client.set(k, value, opts.ttl); } async delete(key: string): Promise { @@ -99,5 +103,4 @@ export class DefaultCacheClient implements CacheClient { return createHash('md5').update(candidateKey).digest('base64'); } - } diff --git a/packages/backend-common/src/cache/CacheManager.test.ts b/packages/backend-common/src/cache/CacheManager.test.ts index 153ccfd7ac..ece4687ee6 100644 --- a/packages/backend-common/src/cache/CacheManager.test.ts +++ b/packages/backend-common/src/cache/CacheManager.test.ts @@ -53,7 +53,9 @@ describe('CacheManager', () => { CacheManager.fromConfig(config); expect(getOptionalString.mock.calls[0][0]).toEqual('backend.cache.store'); - expect(getOptionalString.mock.calls[1][0]).toEqual('backend.cache.connection'); + expect(getOptionalString.mock.calls[1][0]).toEqual( + 'backend.cache.connection', + ); }); it('does not require the backend.cache key', () => { @@ -92,7 +94,7 @@ describe('CacheManager', () => { manager.forPlugin(plugin2Id).getClient({ defaultTtl: expectedTtl }); const client = DefaultCacheClient as jest.Mock; - const cache = Keyv as unknown as jest.Mock; + const cache = (Keyv as unknown) as jest.Mock; expect(cache).toHaveBeenCalledTimes(2); expect(client).toHaveBeenCalledTimes(2); @@ -112,7 +114,7 @@ describe('CacheManager', () => { const expectedNamespace = 'test-plugin'; manager.forPlugin(expectedNamespace).getClient(); - const cache = Keyv as unknown as jest.Mock; + const cache = (Keyv as unknown) as jest.Mock; const mockCalls = cache.mock.calls.splice(-1); const callArgs = mockCalls[0]; expect(callArgs[0].store).toBeInstanceOf(NoStore); @@ -122,9 +124,11 @@ describe('CacheManager', () => { const manager = CacheManager.fromConfig(defaultConfig()); const expectedTtl = 3600; const expectedNamespace = 'test-plugin'; - manager.forPlugin(expectedNamespace).getClient({ defaultTtl: expectedTtl }); + manager + .forPlugin(expectedNamespace) + .getClient({ defaultTtl: expectedTtl }); - const cache = Keyv as unknown as jest.Mock; + const cache = (Keyv as unknown) as jest.Mock; const mockCalls = cache.mock.calls.splice(-1); const callArgs = mockCalls[0]; expect(callArgs[0]).toMatchObject({ @@ -148,7 +152,7 @@ describe('CacheManager', () => { const expectedTtl = 3600; manager.forPlugin('test').getClient({ defaultTtl: expectedTtl }); - const cache = Keyv as unknown as jest.Mock; + const cache = (Keyv as unknown) as jest.Mock; const mockCacheCalls = cache.mock.calls.splice(-1); expect(mockCacheCalls[0][0]).toMatchObject({ ttl: expectedTtl, diff --git a/packages/backend-common/src/cache/CacheManager.ts b/packages/backend-common/src/cache/CacheManager.ts index 967ff4224b..fbd7e3ba0a 100644 --- a/packages/backend-common/src/cache/CacheManager.ts +++ b/packages/backend-common/src/cache/CacheManager.ts @@ -51,7 +51,8 @@ export class CacheManager { // If no `backend.cache` config is provided, instantiate the CacheManager // with a "NoStore" cache client. const store = config.getOptionalString('backend.cache.store') || 'none'; - const connectionString = config.getOptionalString('backend.cache.connection') || ''; + const connectionString = + config.getOptionalString('backend.cache.connection') || ''; return new CacheManager(store, connectionString); } @@ -83,7 +84,10 @@ export class CacheManager { return this.storeFactories[this.store].call(this, pluginId, ttl); } - private getMemcacheClient(pluginId: string, defaultTtl: number | undefined): Keyv { + private getMemcacheClient( + pluginId: string, + defaultTtl: number | undefined, + ): Keyv { return new Keyv({ namespace: pluginId, ttl: defaultTtl, @@ -91,7 +95,10 @@ export class CacheManager { }); } - private getMemoryClient(pluginId: string, defaultTtl: number | undefined): Keyv { + private getMemoryClient( + pluginId: string, + defaultTtl: number | undefined, + ): Keyv { return new Keyv({ namespace: pluginId, ttl: defaultTtl, @@ -102,6 +109,6 @@ export class CacheManager { return new Keyv({ namespace: pluginId, store: new NoStore(), - }) + }); } } diff --git a/packages/backend-common/src/cache/NoStore.ts b/packages/backend-common/src/cache/NoStore.ts index 6acb7c0309..c89e814c93 100644 --- a/packages/backend-common/src/cache/NoStore.ts +++ b/packages/backend-common/src/cache/NoStore.ts @@ -19,7 +19,6 @@ * used when no cache store is configured in a Backstage backend instance. */ export class NoStore extends Map { - clear(): void { return; } @@ -39,5 +38,4 @@ export class NoStore extends Map { set(_key: string, _value: any): this { return this; } - } From 6e2b9a92d655d0cd7faa34bb9854b9fec7e959fc Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sun, 9 May 2021 18:16:24 +0200 Subject: [PATCH 54/95] Updating backend-common API Signed-off-by: Eric Peterson --- packages/backend-common/api-report.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index bd1b017574..5a9b61dd20 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -65,9 +65,9 @@ export class BitbucketUrlReader implements UrlReader { // @public export interface CacheClient { - delete(key: string): Promise; + delete(key: string): Promise; get(key: string): Promise; - set(key: string, value: JsonValue, options: CacheSetOptions): Promise; + set(key: string, value: JsonValue, options?: CacheSetOptions): Promise; } // @public @@ -254,7 +254,7 @@ export function notFoundHandler(): RequestHandler; // @public export type PluginCacheManager = { - getClient: (options: ClientOptions) => CacheClient; + getClient: (options?: ClientOptions) => CacheClient; }; // @public From f9fb4a205e206a678a00f6cfcba4a6ebf226a46d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sun, 9 May 2021 22:42:56 +0200 Subject: [PATCH 55/95] Prep work for mysql support in backend-common MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/mighty-poets-cheer.md | 5 + .github/styles/vocab.txt | 1 + packages/backend-common/package.json | 4 +- .../src/database/connection.test.ts | 6 +- .../backend-common/src/database/connection.ts | 7 +- .../backend-common/src/database/mysql.test.ts | 188 ++++++++++++++++++ packages/backend-common/src/database/mysql.ts | 161 +++++++++++++++ yarn.lock | 51 ++++- 8 files changed, 416 insertions(+), 7 deletions(-) create mode 100644 .changeset/mighty-poets-cheer.md create mode 100644 packages/backend-common/src/database/mysql.test.ts create mode 100644 packages/backend-common/src/database/mysql.ts diff --git a/.changeset/mighty-poets-cheer.md b/.changeset/mighty-poets-cheer.md new file mode 100644 index 0000000000..47538489e4 --- /dev/null +++ b/.changeset/mighty-poets-cheer.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Prep work for mysql support in backend-common diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 694226b8cc..f95eeb6f89 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -177,6 +177,7 @@ mkdocs monorepo monorepos msw +mysql namespace namespaced namespaces diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index a144f1c23f..6b47cd9599 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -62,7 +62,8 @@ "stoppable": "^1.1.0", "tar": "^6.0.5", "unzipper": "^0.10.11", - "winston": "^3.2.1" + "winston": "^3.2.1", + "yn": "^4.0.0" }, "peerDependencies": { "pg-connection-string": "^2.3.0" @@ -94,6 +95,7 @@ "jest": "^26.0.1", "mock-fs": "^4.13.0", "msw": "^0.21.2", + "mysql2": "^2.2.5", "recursive-readdir": "^2.2.2", "supertest": "^6.1.3" }, diff --git a/packages/backend-common/src/database/connection.test.ts b/packages/backend-common/src/database/connection.test.ts index 9810ff9bef..6ab163ff7f 100644 --- a/packages/backend-common/src/database/connection.test.ts +++ b/packages/backend-common/src/database/connection.test.ts @@ -46,11 +46,11 @@ describe('database connection', () => { ).toBeTruthy(); }); - it('tries to create a mysql connection as a passthrough', () => { + it('returns a mysql connection', () => { expect(() => createDatabaseClient( new ConfigReader({ - client: 'mysql', + client: 'mysql2', connection: { host: '127.0.0.1', user: 'foo', @@ -59,7 +59,7 @@ describe('database connection', () => { }, }), ), - ).toThrowError(/Cannot find module 'mysql'/); + ).toBeTruthy(); }); it('accepts overrides', () => { diff --git a/packages/backend-common/src/database/connection.ts b/packages/backend-common/src/database/connection.ts index 3502e21674..fb16915cf4 100644 --- a/packages/backend-common/src/database/connection.ts +++ b/packages/backend-common/src/database/connection.ts @@ -14,9 +14,10 @@ * limitations under the License. */ -import knexFactory, { Knex } from 'knex'; import { Config } from '@backstage/config'; +import knexFactory, { Knex } from 'knex'; import { mergeDatabaseConfig } from './config'; +import { createMysqlDatabaseClient, ensureMysqlDatabaseExists } from './mysql'; import { createPgDatabaseClient, ensurePgDatabaseExists } from './postgres'; import { createSqliteDatabaseClient } from './sqlite3'; @@ -36,6 +37,8 @@ export function createDatabaseClient( if (client === 'pg') { return createPgDatabaseClient(dbConfig, overrides); + } else if (client === 'mysql' || client === 'mysql2') { + return createMysqlDatabaseClient(dbConfig, overrides); } else if (client === 'sqlite3') { return createSqliteDatabaseClient(dbConfig, overrides); } @@ -60,6 +63,8 @@ export async function ensureDatabaseExists( if (client === 'pg') { return ensurePgDatabaseExists(dbConfig, ...databases); + } else if (client === 'mysql' || client === 'mysql2') { + return ensureMysqlDatabaseExists(dbConfig, ...databases); } return undefined; diff --git a/packages/backend-common/src/database/mysql.test.ts b/packages/backend-common/src/database/mysql.test.ts new file mode 100644 index 0000000000..9e23585d5b --- /dev/null +++ b/packages/backend-common/src/database/mysql.test.ts @@ -0,0 +1,188 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config, ConfigReader } from '@backstage/config'; +import { + buildMysqlDatabaseConfig, + createMysqlDatabaseClient, + getMysqlConnectionConfig, + parseMysqlConnectionString, +} from './mysql'; + +describe('mysql', () => { + const createMockConnection = () => ({ + host: 'acme', + user: 'foo', + password: 'bar', + database: 'foodb', + }); + + const createMockConnectionString = () => 'mysql://foo:bar@acme:3306/foodb'; + + const createConfig = (connection: any): Config => + new ConfigReader({ client: 'mysql2', connection }); + + describe('buildMysqlDatabaseConfig', () => { + it('builds a mysql config', () => { + const mockConnection = createMockConnection(); + + expect(buildMysqlDatabaseConfig(createConfig(mockConnection))).toEqual({ + client: 'mysql2', + connection: mockConnection, + useNullAsDefault: true, + }); + }); + + it('builds a connection string config', () => { + const mockConnectionString = createMockConnectionString(); + + expect( + buildMysqlDatabaseConfig(createConfig(mockConnectionString)), + ).toEqual({ + client: 'mysql2', + connection: mockConnectionString, + useNullAsDefault: true, + }); + }); + + it('overrides the database name', () => { + const mockConnection = createMockConnection(); + + expect( + buildMysqlDatabaseConfig(createConfig(mockConnection), { + connection: { database: 'other_db' }, + }), + ).toEqual({ + client: 'mysql2', + connection: { + ...mockConnection, + database: 'other_db', + }, + useNullAsDefault: true, + }); + }); + + it('adds additional config settings', () => { + const mockConnection = createMockConnection(); + + expect( + buildMysqlDatabaseConfig(createConfig(mockConnection), { + connection: { database: 'other_db' }, + pool: { min: 0, max: 7 }, + debug: true, + }), + ).toEqual({ + client: 'mysql2', + connection: { + ...mockConnection, + database: 'other_db', + }, + useNullAsDefault: true, + pool: { min: 0, max: 7 }, + debug: true, + }); + }); + + it('overrides the database from connection string', () => { + const mockConnectionString = createMockConnectionString(); + const mockConnection = createMockConnection(); + + expect( + buildMysqlDatabaseConfig(createConfig(mockConnectionString), { + connection: { database: 'other_db' }, + }), + ).toEqual({ + client: 'mysql2', + connection: { + ...mockConnection, + port: 3306, + database: 'other_db', + }, + useNullAsDefault: true, + }); + }); + }); + + describe('getMysqlConnectionConfig', () => { + it('returns the connection object back', () => { + const mockConnection = createMockConnection(); + const config = createConfig(mockConnection); + + expect(getMysqlConnectionConfig(config)).toEqual(mockConnection); + }); + + it('does not parse the connection string', () => { + const mockConnection = createMockConnection(); + const config = createConfig(mockConnection); + + expect(getMysqlConnectionConfig(config, true)).toEqual(mockConnection); + }); + + it('automatically parses the connection string', () => { + const mockConnection = createMockConnection(); + const mockConnectionString = createMockConnectionString(); + const config = createConfig(mockConnectionString); + + expect(getMysqlConnectionConfig(config)).toEqual({ + ...mockConnection, + port: 3306, + }); + }); + + it('parses the connection string', () => { + const mockConnection = createMockConnection(); + const mockConnectionString = createMockConnectionString(); + const config = createConfig(mockConnectionString); + + expect(getMysqlConnectionConfig(config, true)).toEqual({ + ...mockConnection, + port: 3306, + }); + }); + }); + + describe('createMysqlDatabaseClient', () => { + it('creates a mysql knex instance', () => { + expect( + createMysqlDatabaseClient( + createConfig({ + host: 'acme', + user: 'foo', + password: 'bar', + database: 'foodb', + }), + ), + ).toBeTruthy(); + }); + }); + + describe('parseMysqlConnectionString', () => { + it('parses a connection string uri', () => { + expect( + parseMysqlConnectionString( + 'mysql://u:pass@foobar:3307/dbname?ssl=require', + ), + ).toEqual({ + host: 'foobar', + user: 'u', + password: 'pass', + port: 3307, + database: 'dbname', + ssl: 'require', + }); + }); + }); +}); diff --git a/packages/backend-common/src/database/mysql.ts b/packages/backend-common/src/database/mysql.ts new file mode 100644 index 0000000000..be4632baf6 --- /dev/null +++ b/packages/backend-common/src/database/mysql.ts @@ -0,0 +1,161 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; +import { InputError } from '@backstage/errors'; +import knexFactory, { Knex } from 'knex'; +import { mergeDatabaseConfig } from './config'; +import yn from 'yn'; + +/** + * Creates a knex mysql database connection + * + * @param dbConfig The database config + * @param overrides Additional options to merge with the config + */ +export function createMysqlDatabaseClient( + dbConfig: Config, + overrides?: Knex.Config, +) { + const knexConfig = buildMysqlDatabaseConfig(dbConfig, overrides); + const database = knexFactory(knexConfig); + return database; +} + +/** + * Builds a knex mysql database connection + * + * @param dbConfig The database config + * @param overrides Additional options to merge with the config + */ +export function buildMysqlDatabaseConfig( + dbConfig: Config, + overrides?: Knex.Config, +) { + return mergeDatabaseConfig( + dbConfig.get(), + { + connection: getMysqlConnectionConfig(dbConfig, !!overrides), + useNullAsDefault: true, + }, + overrides, + ); +} + +/** + * Gets the mysql connection config + * + * @param dbConfig The database config + * @param parseConnectionString Flag to explicitly control connection string parsing + */ +export function getMysqlConnectionConfig( + dbConfig: Config, + parseConnectionString?: boolean, +): Knex.MySqlConnectionConfig | string { + const connection = dbConfig.get('connection') as any; + const isConnectionString = + typeof connection === 'string' || connection instanceof String; + const autoParse = typeof parseConnectionString !== 'boolean'; + + const shouldParseConnectionString = autoParse + ? isConnectionString + : parseConnectionString && isConnectionString; + + return shouldParseConnectionString + ? parseMysqlConnectionString(connection as string) + : connection; +} + +/** + * Parses a mysql connection string. + * + * e.g. mysql://examplename:somepassword@examplehost:3306/dbname + * @param connectionString The mysql connection string + */ +export function parseMysqlConnectionString( + connectionString: string, +): Knex.MySqlConnectionConfig { + try { + const { + protocol, + username, + password, + port, + hostname, + pathname, + searchParams, + } = new URL(connectionString); + + if (protocol !== 'mysql:') { + throw new Error(`Unknown protocol ${protocol}`); + } else if (!username || !password) { + throw new Error(`Missing username/password`); + } else if (!pathname.match(/^\/[^/]+$/)) { + throw new Error(`Expected single path segment`); + } + + const result: Knex.MySqlConnectionConfig = { + user: username, + password, + host: hostname, + port: Number(port || 3306), + database: decodeURIComponent(pathname.substr(1)), + }; + + const ssl = searchParams.get('ssl'); + if (ssl) { + result.ssl = ssl; + } + + const debug = searchParams.get('debug'); + if (debug) { + result.debug = yn(debug); + } + + return result; + } catch (e) { + throw new InputError( + `Error while parsing MySQL connection string, ${e}`, + e, + ); + } +} + +/** + * Creates the missing mysql database if it does not exist + * + * @param dbConfig The database config + * @param databases The names of the databases to create + */ +export async function ensureMysqlDatabaseExists( + dbConfig: Config, + ...databases: Array +) { + const admin = createMysqlDatabaseClient(dbConfig, { + connection: { + database: (null as unknown) as string, + }, + }); + + try { + const ensureDatabase = async (database: string) => { + await admin.raw(`CREATE DATABASE IF NOT EXISTS ??`, [database]); + }; + await Promise.all(databases.map(ensureDatabase)); + } finally { + await admin.destroy(); + } +} diff --git a/yarn.lock b/yarn.lock index 23c8118e7e..a21c6ef0f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11495,6 +11495,11 @@ delegates@^1.0.0: resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= +denque@^1.4.1: + version "1.5.0" + resolved "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz#773de0686ff2d8ec2ff92914316a47b73b1c73de" + integrity sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ== + depd@^1.1.2, depd@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" @@ -13822,6 +13827,13 @@ gcs-resumable-upload@^3.1.4: pumpify "^2.0.0" stream-events "^1.0.4" +generate-function@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" + integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== + dependencies: + is-property "^1.0.2" + generic-names@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/generic-names/-/generic-names-2.0.1.tgz#f8a378ead2ccaa7a34f0317b05554832ae41b872" @@ -15920,6 +15932,11 @@ is-promise@^2.1.0, is-promise@^2.2.2: resolved "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== +is-property@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ= + is-reference@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" @@ -17996,7 +18013,7 @@ lru-cache@2: resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" integrity sha1-bUUk6LlV+V1PW1iFHOId1y+06VI= -lru-cache@^4.0.1: +lru-cache@^4.0.1, lru-cache@^4.1.3: version "4.1.5" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== @@ -18668,7 +18685,6 @@ minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a" integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ== dependencies: - encoding "^0.1.12" minipass "^3.1.0" minipass-sized "^1.0.3" minizlib "^2.0.0" @@ -18993,6 +19009,20 @@ mv@~2: ncp "~2.0.0" rimraf "~2.4.0" +mysql2@^2.2.5: + version "2.2.5" + resolved "https://registry.npmjs.org/mysql2/-/mysql2-2.2.5.tgz#72624ffb4816f80f96b9c97fedd8c00935f9f340" + integrity sha512-XRqPNxcZTpmFdXbJqb+/CtYVLCx14x1RTeNMD4954L331APu75IC74GDqnZMEt1kwaXy6TySo55rF2F3YJS78g== + dependencies: + denque "^1.4.1" + generate-function "^2.3.1" + iconv-lite "^0.6.2" + long "^4.0.0" + lru-cache "^6.0.0" + named-placeholders "^1.1.2" + seq-queue "^0.0.5" + sqlstring "^2.3.2" + mz@^2.7.0: version "2.7.0" resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" @@ -19002,6 +19032,13 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" +named-placeholders@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.2.tgz#ceb1fbff50b6b33492b5cf214ccf5e39cef3d0e8" + integrity sha512-wiFWqxoLL3PGVReSZpjLVxyJ1bRqe+KKJVbr4hGs1KWfTZTQyezHFBbuKj9hsizHyGV2ne7EMjHdxEGAybD5SA== + dependencies: + lru-cache "^4.1.3" + nan@2.14.1, nan@^2.12.1, nan@^2.14.0: version "2.14.1" resolved "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" @@ -23532,6 +23569,11 @@ sentence-case@^3.0.4: tslib "^2.0.3" upper-case-first "^2.0.2" +seq-queue@^0.0.5: + version "0.0.5" + resolved "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e" + integrity sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4= + serialize-error@^8.0.1, serialize-error@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz#3a069970c712f78634942ddd50fbbc0eaebe2f67" @@ -24137,6 +24179,11 @@ sqlite3@^5.0.0: optionalDependencies: node-gyp "3.x" +sqlstring@^2.3.2: + version "2.3.2" + resolved "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.2.tgz#cdae7169389a1375b18e885f2e60b3e460809514" + integrity sha512-vF4ZbYdKS8OnoJAWBmMxCQDkiEBkGQYU7UZPtL8flbDRSNkhaXvRJ279ZtI6M+zDaQovVU4tuRgzK5fVhvFAhg== + ssh2-streams@~0.4.10: version "0.4.10" resolved "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz#48ef7e8a0e39d8f2921c30521d56dacb31d23a34" From 35e09160402bc5484359687562311f9eb02626af Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 10 May 2021 17:45:19 +0200 Subject: [PATCH 56/95] Handle URLs with a `#hash` correctly when rewriting link URLs Signed-off-by: Oliver Sand --- .changeset/techdocs-lemon-cooks-drum.md | 5 +++++ .../reader/transformers/rewriteDocLinks.test.ts | 15 +++++++++++++++ .../src/reader/transformers/rewriteDocLinks.ts | 15 ++++++++++++--- 3 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 .changeset/techdocs-lemon-cooks-drum.md diff --git a/.changeset/techdocs-lemon-cooks-drum.md b/.changeset/techdocs-lemon-cooks-drum.md new file mode 100644 index 0000000000..d8f2954c2a --- /dev/null +++ b/.changeset/techdocs-lemon-cooks-drum.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Handle URLs with a `#hash` correctly when rewriting link URLs. diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts index 06da8a447c..ca85c64fbe 100644 --- a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts +++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts @@ -16,6 +16,7 @@ import { createTestShadowDom, getSample } from '../../test-utils'; import { rewriteDocLinks } from '../transformers'; +import { normalizeUrl } from './rewriteDocLinks'; describe('rewriteDocLinks', () => { it('should not do anything', () => { @@ -56,3 +57,17 @@ describe('rewriteDocLinks', () => { ]); }); }); + +describe('normalizeUrl', () => { + it.each([ + ['http://example.org', 'http://example.org/'], + ['http://example.org/', 'http://example.org/'], + ['http://example.org/folder', 'http://example.org/folder/'], + ['http://example.org/folder/', 'http://example.org/folder/'], + ['http://example.org/folder#intro', 'http://example.org/folder/#intro'], + ['http://example.org/folder/#intro', 'http://example.org/folder/#intro'], + ['http://example.org/folder#', 'http://example.org/folder/#'], + ])('should handle %s', (url, expected) => { + expect(normalizeUrl(url)).toEqual(expected); + }); +}); diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts index 7b0c23c3e3..54e5537347 100644 --- a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts +++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts @@ -31,9 +31,7 @@ export const rewriteDocLinks = (): Transformer => { if (elemAttribute.match(/^https?:\/\//i)) { elem.setAttribute('target', '_blank'); } - const normalizedWindowLocation = window.location.href.endsWith('/') - ? window.location.href - : `${window.location.href}/`; + const normalizedWindowLocation = normalizeUrl(window.location.href); elem.setAttribute( attributeName, @@ -48,3 +46,14 @@ export const rewriteDocLinks = (): Transformer => { return dom; }; }; + +/** Make sure that the input url always ends with a '/' */ +export function normalizeUrl(input: string): string { + const url = new URL(input); + + if (!url.pathname.endsWith('/')) { + url.pathname += '/'; + } + + return url.toString(); +} From a8c70b3ae45d133d47d42ef82de30590788d81e1 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 10 May 2021 18:21:27 +0200 Subject: [PATCH 57/95] Maybe not THAT transparent. Signed-off-by: Eric Peterson --- packages/backend-common/api-report.md | 4 ++-- .../src/cache/CacheClient.test.ts | 20 ------------------- .../backend-common/src/cache/CacheClient.ts | 19 +++++++----------- 3 files changed, 9 insertions(+), 34 deletions(-) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 5a9b61dd20..23e22fe34b 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -65,9 +65,9 @@ export class BitbucketUrlReader implements UrlReader { // @public export interface CacheClient { - delete(key: string): Promise; + delete(key: string): Promise; get(key: string): Promise; - set(key: string, value: JsonValue, options?: CacheSetOptions): Promise; + set(key: string, value: JsonValue, options?: CacheSetOptions): Promise; } // @public diff --git a/packages/backend-common/src/cache/CacheClient.test.ts b/packages/backend-common/src/cache/CacheClient.test.ts index eb32d4a28f..81e8351b58 100644 --- a/packages/backend-common/src/cache/CacheClient.test.ts +++ b/packages/backend-common/src/cache/CacheClient.test.ts @@ -101,16 +101,6 @@ describe('CacheClient', () => { return expect(sut.set('someKey', {})).rejects.toEqual(expectedError); }); - - it('resolves what underlying client resolves', async () => { - const sut = new DefaultCacheClient({ client }); - const expectedResponse = true; - client.set = jest.fn().mockReturnValue(expectedResponse); - - const actualResponse = await sut.set('someKey', {}); - - return expect(actualResponse).toEqual(expectedResponse); - }); }); describe('CacheClient.delete', () => { @@ -132,15 +122,5 @@ describe('CacheClient', () => { return expect(sut.delete('someKey')).rejects.toEqual(expectedError); }); - - it('resolves what underlying client resolves', async () => { - const sut = new DefaultCacheClient({ client }); - const expectedResponse = false; - client.delete = jest.fn().mockResolvedValue(expectedResponse); - - const actualResponse = await sut.delete('someKey'); - - return expect(actualResponse).toEqual(expectedResponse); - }); }); }); diff --git a/packages/backend-common/src/cache/CacheClient.ts b/packages/backend-common/src/cache/CacheClient.ts index a2c048c265..49b5c367dd 100644 --- a/packages/backend-common/src/cache/CacheClient.ts +++ b/packages/backend-common/src/cache/CacheClient.ts @@ -46,17 +46,12 @@ export interface CacheClient { * optional TTL may also be provided, otherwise it defaults to the TTL that * was provided when the client was instantiated. */ - set( - key: string, - value: JsonValue, - options?: CacheSetOptions, - ): Promise; + set(key: string, value: JsonValue, options?: CacheSetOptions): Promise; /** - * Removes the given key from the cache store. Resolves true if the key - * existed, or false if not. + * Removes the given key from the cache store. */ - delete(key: string): Promise; + delete(key: string): Promise; } /** @@ -79,14 +74,14 @@ export class DefaultCacheClient implements CacheClient { key: string, value: JsonValue, opts: CacheSetOptions = {}, - ): Promise { + ): Promise { const k = this.getNormalizedKey(key); - return await this.client.set(k, value, opts.ttl); + await this.client.set(k, value, opts.ttl); } - async delete(key: string): Promise { + async delete(key: string): Promise { const k = this.getNormalizedKey(key); - return await this.client.delete(k); + await this.client.delete(k); } /** From 69ad4e630fd3afe00bff8e540381ea5d6471ba7f Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Tue, 11 May 2021 13:14:51 +0200 Subject: [PATCH 58/95] address comments Signed-off-by: Samira Mokaram --- .changeset/large-penguins-yell.md | 6 ------ plugins/kubernetes/src/index.ts | 3 +-- plugins/kubernetes/src/kubernetes-auth-provider/index.ts | 2 ++ 3 files changed, 3 insertions(+), 8 deletions(-) delete mode 100644 .changeset/large-penguins-yell.md create mode 100644 plugins/kubernetes/src/kubernetes-auth-provider/index.ts diff --git a/.changeset/large-penguins-yell.md b/.changeset/large-penguins-yell.md deleted file mode 100644 index 9446374f47..0000000000 --- a/.changeset/large-penguins-yell.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-kubernetes': patch -'@backstage/plugin-kubernetes-backend': patch ---- - -Export types diff --git a/plugins/kubernetes/src/index.ts b/plugins/kubernetes/src/index.ts index a8dd556b48..aabf36f1dc 100644 --- a/plugins/kubernetes/src/index.ts +++ b/plugins/kubernetes/src/index.ts @@ -19,5 +19,4 @@ export { EntityKubernetesContent, } from './plugin'; export { Router } from './Router'; -export { kubernetesAuthProvidersApiRef } from './kubernetes-auth-provider/types'; -export { KubernetesAuthProviders } from './kubernetes-auth-provider/KubernetesAuthProviders'; +export * from './kubernetes-auth-provider'; diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/index.ts b/plugins/kubernetes/src/kubernetes-auth-provider/index.ts new file mode 100644 index 0000000000..0718d074b5 --- /dev/null +++ b/plugins/kubernetes/src/kubernetes-auth-provider/index.ts @@ -0,0 +1,2 @@ +export { kubernetesAuthProvidersApiRef } from './types'; +export { KubernetesAuthProviders } from './KubernetesAuthProviders'; From 2dca7ecaa5462c0f66edebeeb316187a6a928e16 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Tue, 11 May 2021 13:28:12 +0200 Subject: [PATCH 59/95] add notice header Signed-off-by: Samira Mokaram --- .../src/kubernetes-auth-provider/index.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/index.ts b/plugins/kubernetes/src/kubernetes-auth-provider/index.ts index 0718d074b5..8f54913903 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/index.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/index.ts @@ -1,2 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { kubernetesAuthProvidersApiRef } from './types'; export { KubernetesAuthProviders } from './KubernetesAuthProviders'; From ee103bc710285ff5d06248346b70d1aecc0c2bb6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 May 2021 12:39:58 +0000 Subject: [PATCH 60/95] chore(deps): bump @testing-library/user-event from 12.8.3 to 13.1.8 Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 12.8.3 to 13.1.8. - [Release notes](https://github.com/testing-library/user-event/releases) - [Changelog](https://github.com/testing-library/user-event/blob/master/CHANGELOG.md) - [Commits](https://github.com/testing-library/user-event/compare/v12.8.3...v13.1.8) Signed-off-by: dependabot[bot] --- packages/app/package.json | 2 +- packages/core-api/package.json | 2 +- packages/core/package.json | 2 +- packages/dev-utils/package.json | 2 +- packages/integration-react/package.json | 2 +- packages/test-utils/package.json | 2 +- plugins/api-docs/package.json | 2 +- plugins/badges/package.json | 2 +- plugins/bitrise/package.json | 2 +- plugins/catalog-import/package.json | 2 +- plugins/catalog-react/package.json | 2 +- plugins/catalog/package.json | 2 +- plugins/circleci/package.json | 2 +- plugins/cloudbuild/package.json | 2 +- plugins/code-coverage/package.json | 2 +- plugins/config-schema/package.json | 2 +- plugins/cost-insights/package.json | 2 +- plugins/explore-react/package.json | 2 +- plugins/explore/package.json | 2 +- plugins/fossa/package.json | 2 +- plugins/gcp-projects/package.json | 2 +- plugins/github-actions/package.json | 2 +- plugins/github-deployments/package.json | 2 +- plugins/gitops-profiles/package.json | 2 +- plugins/graphiql/package.json | 2 +- plugins/jenkins/package.json | 2 +- plugins/kafka/package.json | 2 +- plugins/kubernetes/package.json | 2 +- plugins/lighthouse/package.json | 2 +- plugins/newrelic/package.json | 2 +- plugins/org/package.json | 2 +- plugins/pagerduty/package.json | 2 +- plugins/register-component/package.json | 2 +- plugins/rollbar/package.json | 2 +- plugins/scaffolder/package.json | 2 +- plugins/search/package.json | 2 +- plugins/sentry/package.json | 2 +- plugins/shortcuts/package.json | 2 +- plugins/sonarqube/package.json | 2 +- plugins/splunk-on-call/package.json | 2 +- plugins/tech-radar/package.json | 2 +- plugins/techdocs/package.json | 2 +- plugins/todo/package.json | 2 +- plugins/user-settings/package.json | 2 +- plugins/welcome/package.json | 2 +- yarn.lock | 9 ++++----- 46 files changed, 49 insertions(+), 50 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index cbc8c5f1a3..714d420744 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -60,7 +60,7 @@ "@testing-library/cypress": "^7.0.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/jquery": "^3.3.34", "@types/node": "^14.14.32", diff --git a/packages/core-api/package.json b/packages/core-api/package.json index 13d7efe77b..f9fd3d29a6 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -48,7 +48,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.3.0", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/zen-observable": "^0.8.0", diff --git a/packages/core/package.json b/packages/core/package.json index 315441a87c..16928d63ca 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -75,7 +75,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/classnames": "^2.2.9", "@types/d3-selection": "^2.0.0", "@types/d3-shape": "^2.0.0", diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 3845da7e9b..421cd0a2b8 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -39,7 +39,7 @@ "@material-ui/icons": "^4.9.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/react": "^16.9", "react": "^16.12.0", "react-dom": "^16.12.0", diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index 7833952e8e..8df1eef096 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -37,7 +37,7 @@ "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "msw": "^0.21.2", diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index a81455df01..74716f7a16 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -35,7 +35,7 @@ "@material-ui/core": "^4.11.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/react": "^16.9", "msw": "^0.21.3", "react": "^16.12.0", diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 9cccb07d56..c94680de9b 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -54,7 +54,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/swagger-ui-react": "^3.23.3", diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 24660899e2..e757567401 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -39,7 +39,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index 45b32a11d1..f37d08f725 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -42,7 +42,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/recharts": "^1.8.15", diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index c5fcdd4d37..0558be18ec 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -59,7 +59,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.3.0", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index a9092ef75a..491df00c08 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -46,7 +46,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.3.0", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index f1a0e6979d..140c42cfdb 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -59,7 +59,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.3.0", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 98af012a49..4503f59636 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -55,7 +55,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/react-lazylog": "^4.5.0", diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 325dfb4e06..59fbda33f6 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -52,7 +52,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index 9b227c1df1..837c3d4794 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -44,7 +44,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/highlightjs": "^10.1.0", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index caa03b9ffb..43c6e67f1a 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -39,7 +39,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "msw": "^0.21.2", diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 3a5601041c..3fd057aa4a 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -60,7 +60,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/pluralize": "^0.0.29", diff --git a/plugins/explore-react/package.json b/plugins/explore-react/package.json index b6852ea0f0..cf8c944433 100644 --- a/plugins/explore-react/package.json +++ b/plugins/explore-react/package.json @@ -36,7 +36,7 @@ "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 2a25a9db41..3069b719f0 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -50,7 +50,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index 95762e52d7..e6fa50f7da 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -52,7 +52,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index fd6048ab2a..d50e63725e 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -46,7 +46,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 95608c3c49..9bdbaf3c4b 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -55,7 +55,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index 84a0fe1660..1086fc0d17 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -39,7 +39,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index c00dbde482..21744a6f54 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -47,7 +47,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index ae3c0dfe8f..053fbc066a 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -48,7 +48,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/codemirror": "^0.0.108", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index c4ba600d7e..aa57fa1d1a 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -52,7 +52,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/testing-library__jest-dom": "^5.9.1", diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index e405460262..e18d5188d0 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -39,7 +39,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.4.2", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 19865a6fcc..aaa55fef13 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -55,7 +55,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.4.2", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index f70e29f69b..039f8b6dce 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -51,7 +51,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/react": "^16.9", diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 20ee5521f4..96ef75b532 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -46,7 +46,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/plugins/org/package.json b/plugins/org/package.json index 012514f1c4..a0bb1ba06e 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -40,7 +40,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 53f0ddbb23..7cbb0a21b7 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -51,7 +51,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index 3efb61b982..376526a059 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -50,7 +50,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 8b559e0bf1..3e1f7e4964 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -53,7 +53,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.3.0", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/react": "^16.9", diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 1992028157..e21e457c6c 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -65,7 +65,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/humanize-duration": "^3.18.1", "@testing-library/react-hooks": "^3.3.0", "@types/jest": "^26.0.7", diff --git a/plugins/search/package.json b/plugins/search/package.json index f503b9a78e..ee7d15ddc3 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -49,7 +49,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 6784debcf9..d33b57e975 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -51,7 +51,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/react": "^16.9", diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index f87fe0e683..a0f8a0c498 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -40,7 +40,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index df5304d746..6b7e5ff4d9 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -52,7 +52,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index 020fbc37cd..4ec6e6a0f4 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -50,7 +50,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/luxon": "^1.25.0", "@types/node": "^14.14.32", diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 5e33024f87..538511565b 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -48,7 +48,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/color": "^3.0.1", "@types/d3-force": "^2.1.1", "@types/jest": "^26.0.7", diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 20fca364da..87cb1185a0 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -56,7 +56,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/react": "^16.9", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", diff --git a/plugins/todo/package.json b/plugins/todo/package.json index b7564f7080..8a3d327929 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -44,7 +44,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "msw": "^0.21.2", diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 32e232cad4..b52584267c 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -46,7 +46,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index 72ec24bf71..5f557f00b1 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -46,7 +46,7 @@ "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/yarn.lock b/yarn.lock index 437a9be678..8aaf6d3761 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5606,10 +5606,10 @@ "@babel/runtime" "^7.12.5" "@testing-library/dom" "^7.28.1" -"@testing-library/user-event@^12.0.7": - version "12.8.3" - resolved "https://registry.npmjs.org/@testing-library/user-event/-/user-event-12.8.3.tgz#1aa3ed4b9f79340a1e1836bc7f57c501e838704a" - integrity sha512-IR0iWbFkgd56Bu5ZI/ej8yQwrkCv8Qydx6RzwbKz9faXazR/+5tvYKsZQgyXJiwgpcva127YO6JcWy7YlCfofQ== +"@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== dependencies: "@babel/runtime" "^7.12.5" @@ -18668,7 +18668,6 @@ minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a" integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ== dependencies: - encoding "^0.1.12" minipass "^3.1.0" minipass-sized "^1.0.3" minizlib "^2.0.0" From f6706308e1dbabfc237f7eb304644c5e6ecf6e74 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 11 May 2021 10:24:11 +0200 Subject: [PATCH 61/95] chore: fix failing test, it was failing in a good way. Signed-off-by: blam --- .../StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx index 3e918db419..03882d48ce 100644 --- a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx +++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx @@ -98,7 +98,11 @@ describe('', () => { ); await act(async () => { - userEvent.click(getByRole('button', { name: /Analyze/i })); + try { + userEvent.click(getByRole('button', { name: /Analyze/i })); + } catch { + return; + } }); expect(catalogImportApi.analyzeUrl).toBeCalledTimes(0); From 062bbf90f89b67ce9aa61d7074b524e94dc6a051 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 11 May 2021 10:32:43 +0200 Subject: [PATCH 62/95] chore: add changeset Signed-off-by: blam --- .changeset/afraid-waves-remember.md | 49 +++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .changeset/afraid-waves-remember.md diff --git a/.changeset/afraid-waves-remember.md b/.changeset/afraid-waves-remember.md new file mode 100644 index 0000000000..13df956acc --- /dev/null +++ b/.changeset/afraid-waves-remember.md @@ -0,0 +1,49 @@ +--- +'@backstage/core': patch +'@backstage/core-api': patch +'@backstage/dev-utils': patch +'@backstage/integration-react': patch +'@backstage/test-utils': patch +'@backstage/plugin-api-docs': patch +'@backstage/plugin-badges': patch +'@backstage/plugin-bitrise': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-circleci': patch +'@backstage/plugin-cloudbuild': patch +'@backstage/plugin-code-coverage': patch +'@backstage/plugin-config-schema': patch +'@backstage/plugin-cost-insights': patch +'@backstage/plugin-explore': patch +'@backstage/plugin-explore-react': patch +'@backstage/plugin-fossa': patch +'@backstage/plugin-gcp-projects': patch +'@backstage/plugin-github-actions': patch +'@backstage/plugin-github-deployments': patch +'@backstage/plugin-gitops-profiles': patch +'@backstage/plugin-graphiql': patch +'@backstage/plugin-jenkins': patch +'@backstage/plugin-kafka': patch +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-lighthouse': patch +'@backstage/plugin-newrelic': patch +'@backstage/plugin-org': patch +'@backstage/plugin-pagerduty': patch +'@backstage/plugin-register-component': patch +'@backstage/plugin-rollbar': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-search': patch +'@backstage/plugin-sentry': patch +'@backstage/plugin-shortcuts': patch +'@backstage/plugin-sonarqube': patch +'@backstage/plugin-splunk-on-call': patch +'@backstage/plugin-tech-radar': patch +'@backstage/plugin-techdocs': patch +'@backstage/plugin-todo': patch +'@backstage/plugin-user-settings': patch +'@backstage/plugin-welcome': patch +--- + +chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 From 90385750c981010e144e789513028c8130725d45 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 11 May 2021 13:18:33 +0200 Subject: [PATCH 63/95] chore: fixing user-event versioning for create-plugin templates Signed-off-by: blam --- packages/cli/templates/default-plugin/package.json.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index 1b4f8ea6ab..1d2766eab4 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -39,7 +39,7 @@ "@backstage/test-utils": "^{{version '@backstage/test-utils'}}", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "msw": "^0.21.2", From 1917a19ad90cb0bfb7c82733d423c65a01ea0e12 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 11 May 2021 14:31:25 +0200 Subject: [PATCH 64/95] chore: fixing user-events version for new package Signed-off-by: blam --- plugins/git-release-manager/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index 9aca27d1a9..cae3bcdb70 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -43,7 +43,7 @@ "@types/recharts": "^1.8.15", "@testing-library/react-hooks": "^3.4.2", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", From 52e6a73fe17475364d1ccbbbd3c5d84b3c18b1a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 May 2021 13:34:25 +0000 Subject: [PATCH 65/95] chore(deps): bump hosted-git-info from 2.8.8 to 2.8.9 Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9. - [Release notes](https://github.com/npm/hosted-git-info/releases) - [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md) - [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9) Signed-off-by: dependabot[bot] --- yarn.lock | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 437a9be678..f4353b9a6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14802,9 +14802,9 @@ hoopy@^0.1.4: integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ== hosted-git-info@^2.1.4: - version "2.8.8" - resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" - integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== hosted-git-info@^3.0.6: version "3.0.8" @@ -18668,7 +18668,6 @@ minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a" integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ== dependencies: - encoding "^0.1.12" minipass "^3.1.0" minipass-sized "^1.0.3" minizlib "^2.0.0" From a62cfe068220c21dbd2e043873de69c29d347026 Mon Sep 17 00:00:00 2001 From: Emre Date: Tue, 11 May 2021 17:15:22 +0200 Subject: [PATCH 66/95] add height of md-tabs on sidebar position Signed-off-by: Emre --- .changeset/stupid-balloons-change.md | 5 +++++ plugins/techdocs/src/reader/components/Reader.tsx | 12 ++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 .changeset/stupid-balloons-change.md diff --git a/.changeset/stupid-balloons-change.md b/.changeset/stupid-balloons-change.md new file mode 100644 index 0000000000..98c40b4d60 --- /dev/null +++ b/.changeset/stupid-balloons-change.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Bug fix on sidebar position when Tab-Bar is enabled diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index a67443e40f..7bcf182d56 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -89,12 +89,17 @@ export const Reader = ({ entityId, onReady }: Props) => { useEffect(() => { const updateSidebarPosition = () => { if (!!shadowDomRef.current && !!sidebars) { + const mdTabs = shadowDomRef.current!.querySelector( + '.md-container > .md-tabs', + ); sidebars!.forEach(sidebar => { const newTop = Math.max( shadowDomRef.current!.getBoundingClientRect().top, 0, ); - sidebar.style.top = `${newTop}px`; + sidebar.style.top = mdTabs + ? `${newTop + mdTabs.getBoundingClientRect().height}px` + : `${newTop}px`; }); } }; @@ -318,8 +323,11 @@ export const Reader = ({ entityId, onReady }: Props) => { // set sidebar height so they don't initially render in wrong position const docTopPosition = (dom as HTMLElement).getBoundingClientRect() .top; + const mdTabs = dom.querySelector('.md-container > .md-tabs'); sideDivs!.forEach(sidebar => { - sidebar.style.top = `${docTopPosition}px`; + sidebar.style.top = mdTabs + ? `${docTopPosition + mdTabs.getBoundingClientRect().height}px` + : `${docTopPosition}px`; }); }, }), From 01a9ebf6a531e001fd0b8099b997d482da16cba2 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 11 May 2021 18:01:27 +0200 Subject: [PATCH 67/95] Make integrations take precedence over providers Signed-off-by: Oliver Sand --- .changeset/polite-plants-exercise.md | 22 ++++--------------- .../processors/GithubOrgReaderProcessor.ts | 8 ++++--- .../processors/github/config.test.ts | 10 +++++++++ .../src/ingestion/processors/github/config.ts | 9 ++++++++ 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/.changeset/polite-plants-exercise.md b/.changeset/polite-plants-exercise.md index ac4a8d1403..2cfa38697a 100644 --- a/.changeset/polite-plants-exercise.md +++ b/.changeset/polite-plants-exercise.md @@ -8,21 +8,7 @@ addition to [`catalog.processors.githubOrg.providers`](https://backstage.io/docs The `integrations` package supports authentication with both personal access tokens and GitHub apps. -This deprecates the `catalog.processors.githubOrg.providers` configuration. If -you still have a configuration for providers the processor keeps working, but -consider moving to the [`integrations` configuration](https://backstage.io/docs/integrations/github/locations) -as the providers will be removed in the future. You might need to allow -additional scopes for the credentials. - -If you want to stay with providers for now, this introduces a small breaking -change, previously if you had no provider configured, one for GitHub was automatically added. To keep the behavior, add a -default provider for GitHub: - -```yaml -catalog: - processors: - githubOrg: - providers: - - target: https://github.com - apiBaseUrl: https://api.github.com -``` +This deprecates the `catalog.processors.githubOrg.providers` configuration. +A [`integrations` configuration](https://backstage.io/docs/integrations/github/locations) +for the same host takes precedence over the provider configuration. +You might need to add additional scopes for the credentials. diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts index 5840daed74..14204adde7 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts @@ -113,10 +113,10 @@ export class GithubOrgReaderProcessor implements CatalogProcessor { } private async createClient(orgUrl: string): Promise { - let client = this.createClientFromProvider(orgUrl); + let client = await this.createClientFromIntegrations(orgUrl); if (!client) { - client = await this.createClientFromIntegrations(orgUrl); + client = await this.createClientFromProvider(orgUrl); } if (!client) { @@ -128,7 +128,9 @@ export class GithubOrgReaderProcessor implements CatalogProcessor { return client; } - private createClientFromProvider(orgUrl: string): GraphQL | undefined { + private async createClientFromProvider( + orgUrl: string, + ): Promise { const provider = this.providers.find(p => orgUrl.startsWith(`${p.target}/`), ); diff --git a/plugins/catalog-backend/src/ingestion/processors/github/config.test.ts b/plugins/catalog-backend/src/ingestion/processors/github/config.test.ts index b39a7a7ff9..14f3caa42c 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/config.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/github/config.test.ts @@ -27,6 +27,16 @@ describe('config', () => { }); } + it('adds a default GitHub entry when missing', () => { + const output = readGithubConfig(config([])); + expect(output).toEqual([ + { + target: 'https://github.com', + apiBaseUrl: 'https://api.github.com', + }, + ]); + }); + it('injects the correct GitHub API base URL when missing', () => { const output = readGithubConfig( config([{ target: 'https://github.com' }]), diff --git a/plugins/catalog-backend/src/ingestion/processors/github/config.ts b/plugins/catalog-backend/src/ingestion/processors/github/config.ts index 7b16448b9e..88f2f96218 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/config.ts +++ b/plugins/catalog-backend/src/ingestion/processors/github/config.ts @@ -71,5 +71,14 @@ export function readGithubConfig(config: Config): ProviderConfig[] { providers.push({ target, apiBaseUrl, token }); } + // If no explicit github.com provider was added, put one in the list as + // a convenience + if (!providers.some(p => p.target === 'https://github.com')) { + providers.push({ + target: 'https://github.com', + apiBaseUrl: 'https://api.github.com', + }); + } + return providers; } From a7e5c54f80702528ff866d7d895912b327662493 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 May 2021 00:35:14 +0000 Subject: [PATCH 68/95] chore(deps): bump json-pointer from 0.6.0 to 0.6.1 Bumps [json-pointer](https://github.com/manuelstofer/json-pointer) from 0.6.0 to 0.6.1. - [Release notes](https://github.com/manuelstofer/json-pointer/releases) - [Commits](https://github.com/manuelstofer/json-pointer/commits) Signed-off-by: dependabot[bot] --- yarn.lock | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/yarn.lock b/yarn.lock index 27a778fd03..16d48d6252 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13490,7 +13490,7 @@ for-own@^0.1.3: foreach@^2.0.4: version "2.0.5" - resolved "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" + resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= forever-agent@~0.6.1: @@ -14428,9 +14428,9 @@ graphql-extensions@^0.12.8: apollo-server-env "^3.0.0" apollo-server-types "^0.6.3" -graphql-language-service-interface@^2.8.2: +graphql-language-service-interface@2.8.2, graphql-language-service-interface@^2.8.2: version "2.8.2" - resolved "https://registry.yarnpkg.com/graphql-language-service-interface/-/graphql-language-service-interface-2.8.2.tgz#b3bb2aef7eaf0dff0b4ea419fa412c5f66fa268b" + resolved "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-2.8.2.tgz#b3bb2aef7eaf0dff0b4ea419fa412c5f66fa268b" integrity sha512-otbOQmhgkAJU1QJgQkMztNku6SbJLu/uodoFOYOOtJsizTjrMs93vkYaHCcYnLA3oi1Goj27XcHjMnRCYQOZXQ== dependencies: graphql-language-service-parser "^1.9.0" @@ -14438,9 +14438,9 @@ graphql-language-service-interface@^2.8.2: graphql-language-service-utils "^2.5.1" vscode-languageserver-types "^3.15.1" -graphql-language-service-parser@^1.9.0: +graphql-language-service-parser@1.9.0, graphql-language-service-parser@^1.9.0: version "1.9.0" - resolved "https://registry.yarnpkg.com/graphql-language-service-parser/-/graphql-language-service-parser-1.9.0.tgz#79af21294119a0a7e81b6b994a1af36833bab724" + resolved "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.9.0.tgz#79af21294119a0a7e81b6b994a1af36833bab724" integrity sha512-B5xPZLbBmIp0kHvpY1Z35I5DtPoDK9wGxQVRDIzcBaiIvAmlTrDvjo3bu7vKREdjFbYKvWNgrEWENuprMbF17Q== dependencies: graphql-language-service-types "^1.8.0" @@ -16886,9 +16886,9 @@ json-parse-even-better-errors@^2.3.0: integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== json-pointer@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.0.tgz#8e500550a6aac5464a473377da57aa6cc22828d7" - integrity sha1-jlAFUKaqxUZKRzN32leqbMIoKNc= + version "0.6.1" + resolved "https://registry.yarnpkg.com/json-pointer/-/json-pointer-0.6.1.tgz#3c6caa6ac139e2599f5a1659d39852154015054d" + integrity sha512-3OvjqKdCBvH41DLpV4iSt6v2XhZXV1bPB4OROuknvUXI7ZQNofieCPkmE26stEJ9zdQuvIxDHCuYhfgxFAAs+Q== dependencies: foreach "^2.0.4" @@ -25719,16 +25719,11 @@ typescript-json-schema@^0.49.0: typescript "^4.1.3" yargs "^16.2.0" -typescript@^4.0.3, typescript@^4.1.3: +typescript@^4.0.3, typescript@^4.1.3, typescript@~4.1.3: version "4.2.3" resolved "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3" integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw== -typescript@~4.1.3: - version "4.1.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72" - integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA== - ua-parser-js@^0.7.18: version "0.7.28" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" From bd389ae394a9659b358bb0e232bee0f642df5ca5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 May 2021 02:57:53 +0000 Subject: [PATCH 69/95] chore(deps): bump merge from 2.1.0 to 2.1.1 Bumps [merge](https://github.com/yeikos/js.merge) from 2.1.0 to 2.1.1. - [Release notes](https://github.com/yeikos/js.merge/releases) - [Commits](https://github.com/yeikos/js.merge/compare/v2.1.0...v2.1.1) Signed-off-by: dependabot[bot] --- yarn.lock | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/yarn.lock b/yarn.lock index 27a778fd03..8f52b5cf6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14428,9 +14428,9 @@ graphql-extensions@^0.12.8: apollo-server-env "^3.0.0" apollo-server-types "^0.6.3" -graphql-language-service-interface@^2.8.2: +graphql-language-service-interface@2.8.2, graphql-language-service-interface@^2.8.2: version "2.8.2" - resolved "https://registry.yarnpkg.com/graphql-language-service-interface/-/graphql-language-service-interface-2.8.2.tgz#b3bb2aef7eaf0dff0b4ea419fa412c5f66fa268b" + resolved "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-2.8.2.tgz#b3bb2aef7eaf0dff0b4ea419fa412c5f66fa268b" integrity sha512-otbOQmhgkAJU1QJgQkMztNku6SbJLu/uodoFOYOOtJsizTjrMs93vkYaHCcYnLA3oi1Goj27XcHjMnRCYQOZXQ== dependencies: graphql-language-service-parser "^1.9.0" @@ -14438,9 +14438,9 @@ graphql-language-service-interface@^2.8.2: graphql-language-service-utils "^2.5.1" vscode-languageserver-types "^3.15.1" -graphql-language-service-parser@^1.9.0: +graphql-language-service-parser@1.9.0, graphql-language-service-parser@^1.9.0: version "1.9.0" - resolved "https://registry.yarnpkg.com/graphql-language-service-parser/-/graphql-language-service-parser-1.9.0.tgz#79af21294119a0a7e81b6b994a1af36833bab724" + resolved "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.9.0.tgz#79af21294119a0a7e81b6b994a1af36833bab724" integrity sha512-B5xPZLbBmIp0kHvpY1Z35I5DtPoDK9wGxQVRDIzcBaiIvAmlTrDvjo3bu7vKREdjFbYKvWNgrEWENuprMbF17Q== dependencies: graphql-language-service-types "^1.8.0" @@ -18446,9 +18446,9 @@ merge2@^1.2.3, merge2@^1.3.0: integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw== merge@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/merge/-/merge-2.1.0.tgz#91fff62458ba2eca378dd395fa85f1690bf87f60" - integrity sha512-TcuhVDV+e6X457MQAm7xIb19rWhZuEDEho7RrwxMpQ/3GhD5sDlnP188gjQQuweXHy9igdke5oUtVOXX1X8Sxg== + version "2.1.1" + resolved "https://registry.yarnpkg.com/merge/-/merge-2.1.1.tgz#59ef4bf7e0b3e879186436e8481c06a6c162ca98" + integrity sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w== meros@^1.1.2: version "1.1.4" @@ -25719,16 +25719,11 @@ typescript-json-schema@^0.49.0: typescript "^4.1.3" yargs "^16.2.0" -typescript@^4.0.3, typescript@^4.1.3: +typescript@^4.0.3, typescript@^4.1.3, typescript@~4.1.3: version "4.2.3" resolved "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3" integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw== -typescript@~4.1.3: - version "4.1.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72" - integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA== - ua-parser-js@^0.7.18: version "0.7.28" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" From 2aed43c43554f103ffbcf2f00b43d1740964237c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 May 2021 04:38:01 +0000 Subject: [PATCH 70/95] chore(deps): bump cross-fetch from 3.0.6 to 3.1.4 Bumps [cross-fetch](https://github.com/lquixada/cross-fetch) from 3.0.6 to 3.1.4. - [Release notes](https://github.com/lquixada/cross-fetch/releases) - [Commits](https://github.com/lquixada/cross-fetch/compare/v3.0.6...v3.1.4) Signed-off-by: dependabot[bot] --- yarn.lock | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/yarn.lock b/yarn.lock index 27a778fd03..cc98fe4fff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10611,13 +10611,20 @@ cross-env@^7.0.0: dependencies: cross-spawn "^7.0.1" -cross-fetch@3.0.6, cross-fetch@^3.0.4, cross-fetch@^3.0.5, cross-fetch@^3.0.6: +cross-fetch@3.0.6: version "3.0.6" resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.6.tgz#3a4040bc8941e653e0e9cf17f29ebcd177d3365c" integrity sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ== dependencies: node-fetch "2.6.1" +cross-fetch@^3.0.4, cross-fetch@^3.0.5, cross-fetch@^3.0.6: + version "3.1.4" + resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39" + integrity sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ== + dependencies: + node-fetch "2.6.1" + cross-spawn@7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz#0ab56286e0f7c24e153d04cc2aa027e43a9a5d14" @@ -14428,9 +14435,9 @@ graphql-extensions@^0.12.8: apollo-server-env "^3.0.0" apollo-server-types "^0.6.3" -graphql-language-service-interface@^2.8.2: +graphql-language-service-interface@2.8.2, graphql-language-service-interface@^2.8.2: version "2.8.2" - resolved "https://registry.yarnpkg.com/graphql-language-service-interface/-/graphql-language-service-interface-2.8.2.tgz#b3bb2aef7eaf0dff0b4ea419fa412c5f66fa268b" + resolved "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-2.8.2.tgz#b3bb2aef7eaf0dff0b4ea419fa412c5f66fa268b" integrity sha512-otbOQmhgkAJU1QJgQkMztNku6SbJLu/uodoFOYOOtJsizTjrMs93vkYaHCcYnLA3oi1Goj27XcHjMnRCYQOZXQ== dependencies: graphql-language-service-parser "^1.9.0" @@ -14438,9 +14445,9 @@ graphql-language-service-interface@^2.8.2: graphql-language-service-utils "^2.5.1" vscode-languageserver-types "^3.15.1" -graphql-language-service-parser@^1.9.0: +graphql-language-service-parser@1.9.0, graphql-language-service-parser@^1.9.0: version "1.9.0" - resolved "https://registry.yarnpkg.com/graphql-language-service-parser/-/graphql-language-service-parser-1.9.0.tgz#79af21294119a0a7e81b6b994a1af36833bab724" + resolved "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.9.0.tgz#79af21294119a0a7e81b6b994a1af36833bab724" integrity sha512-B5xPZLbBmIp0kHvpY1Z35I5DtPoDK9wGxQVRDIzcBaiIvAmlTrDvjo3bu7vKREdjFbYKvWNgrEWENuprMbF17Q== dependencies: graphql-language-service-types "^1.8.0" @@ -25719,16 +25726,11 @@ typescript-json-schema@^0.49.0: typescript "^4.1.3" yargs "^16.2.0" -typescript@^4.0.3, typescript@^4.1.3: +typescript@^4.0.3, typescript@^4.1.3, typescript@~4.1.3: version "4.2.3" resolved "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3" integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw== -typescript@~4.1.3: - version "4.1.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72" - integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA== - ua-parser-js@^0.7.18: version "0.7.28" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" From 5c6bcebe2dece5fd592dcaeafc2909377e558976 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 May 2021 04:39:44 +0000 Subject: [PATCH 71/95] chore(deps): bump graphql-tag from 2.11.0 to 2.12.4 Bumps [graphql-tag](https://github.com/apollographql/graphql-tag) from 2.11.0 to 2.12.4. - [Release notes](https://github.com/apollographql/graphql-tag/releases) - [Changelog](https://github.com/apollographql/graphql-tag/blob/main/CHANGELOG.md) - [Commits](https://github.com/apollographql/graphql-tag/compare/v2.11.0...v2.12.4) Signed-off-by: dependabot[bot] --- yarn.lock | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/yarn.lock b/yarn.lock index 27a778fd03..f838037050 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14428,9 +14428,9 @@ graphql-extensions@^0.12.8: apollo-server-env "^3.0.0" apollo-server-types "^0.6.3" -graphql-language-service-interface@^2.8.2: +graphql-language-service-interface@2.8.2, graphql-language-service-interface@^2.8.2: version "2.8.2" - resolved "https://registry.yarnpkg.com/graphql-language-service-interface/-/graphql-language-service-interface-2.8.2.tgz#b3bb2aef7eaf0dff0b4ea419fa412c5f66fa268b" + resolved "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-2.8.2.tgz#b3bb2aef7eaf0dff0b4ea419fa412c5f66fa268b" integrity sha512-otbOQmhgkAJU1QJgQkMztNku6SbJLu/uodoFOYOOtJsizTjrMs93vkYaHCcYnLA3oi1Goj27XcHjMnRCYQOZXQ== dependencies: graphql-language-service-parser "^1.9.0" @@ -14438,9 +14438,9 @@ graphql-language-service-interface@^2.8.2: graphql-language-service-utils "^2.5.1" vscode-languageserver-types "^3.15.1" -graphql-language-service-parser@^1.9.0: +graphql-language-service-parser@1.9.0, graphql-language-service-parser@^1.9.0: version "1.9.0" - resolved "https://registry.yarnpkg.com/graphql-language-service-parser/-/graphql-language-service-parser-1.9.0.tgz#79af21294119a0a7e81b6b994a1af36833bab724" + resolved "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.9.0.tgz#79af21294119a0a7e81b6b994a1af36833bab724" integrity sha512-B5xPZLbBmIp0kHvpY1Z35I5DtPoDK9wGxQVRDIzcBaiIvAmlTrDvjo3bu7vKREdjFbYKvWNgrEWENuprMbF17Q== dependencies: graphql-language-service-types "^1.8.0" @@ -14483,9 +14483,11 @@ graphql-subscriptions@^1.0.0: iterall "^1.2.1" graphql-tag@^2.11.0: - version "2.11.0" - resolved "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.11.0.tgz#1deb53a01c46a7eb401d6cb59dec86fa1cccbffd" - integrity sha512-VmsD5pJqWJnQZMUeRwrDhfgoyqcfwEkvtpANqcoUG8/tOLkwNgU9mzub/Mc78OJMhHjx7gfAMTxzdG43VGg3bA== + version "2.12.4" + resolved "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.4.tgz#d34066688a4f09e72d6f4663c74211e9b4b7c4bf" + integrity sha512-VV1U4O+9x99EkNpNmCUV5RZwq6MnK4+pGbRYWG+lA/m3uo7TSqJF81OkcOP148gFP6fzdl7JWYBrwWVTS9jXww== + dependencies: + tslib "^2.1.0" graphql-tools@5.0.0: version "5.0.0" @@ -25719,16 +25721,11 @@ typescript-json-schema@^0.49.0: typescript "^4.1.3" yargs "^16.2.0" -typescript@^4.0.3, typescript@^4.1.3: +typescript@^4.0.3, typescript@^4.1.3, typescript@~4.1.3: version "4.2.3" resolved "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3" integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw== -typescript@~4.1.3: - version "4.1.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72" - integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA== - ua-parser-js@^0.7.18: version "0.7.28" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" From babe473dd10f9ef59c734a0f301f2ba3762c41e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 10 May 2021 10:31:50 +0200 Subject: [PATCH 72/95] Introduce `@backstage/backend-testing`, with `TestDatabases` to start off MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/wise-pens-count.md | 5 + packages/backend-testing/.eslintrc.js | 3 + packages/backend-testing/README.md | 18 + packages/backend-testing/api-report.md | 21 ++ packages/backend-testing/package.json | 50 +++ .../src/database/TestDatabases.test.ts | 154 ++++++++ .../src/database/TestDatabases.ts | 278 ++++++++++++++ .../backend-testing/src/database/index.ts | 18 + packages/backend-testing/src/index.ts | 17 + packages/backend-testing/src/setupTests.ts | 17 + plugins/catalog-backend/package.json | 1 + .../catalog-backend/src/next/Stitcher.test.ts | 341 +++++++++--------- scripts/api-extractor.ts | 1 + yarn.lock | 107 +++++- 14 files changed, 866 insertions(+), 165 deletions(-) create mode 100644 .changeset/wise-pens-count.md create mode 100644 packages/backend-testing/.eslintrc.js create mode 100644 packages/backend-testing/README.md create mode 100644 packages/backend-testing/api-report.md create mode 100644 packages/backend-testing/package.json create mode 100644 packages/backend-testing/src/database/TestDatabases.test.ts create mode 100644 packages/backend-testing/src/database/TestDatabases.ts create mode 100644 packages/backend-testing/src/database/index.ts create mode 100644 packages/backend-testing/src/index.ts create mode 100644 packages/backend-testing/src/setupTests.ts diff --git a/.changeset/wise-pens-count.md b/.changeset/wise-pens-count.md new file mode 100644 index 0000000000..26eda866bc --- /dev/null +++ b/.changeset/wise-pens-count.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Use `TestDatabases` from `@backstage/backend-testing` diff --git a/packages/backend-testing/.eslintrc.js b/packages/backend-testing/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/packages/backend-testing/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/packages/backend-testing/README.md b/packages/backend-testing/README.md new file mode 100644 index 0000000000..f12e94ac25 --- /dev/null +++ b/packages/backend-testing/README.md @@ -0,0 +1,18 @@ +# @backstage/backend-testing + +Test helpers library for Backstage backends. + +## Usage + +Add the library as a `devDependency` to your backend package: + +```sh +# From the Backstage root directory, go to your backend package, or to a backend plugin +cd plugins/my-plugin-backend +yarn add --dev @backstage/backend-testing +``` + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) diff --git a/packages/backend-testing/api-report.md b/packages/backend-testing/api-report.md new file mode 100644 index 0000000000..c4c78408ec --- /dev/null +++ b/packages/backend-testing/api-report.md @@ -0,0 +1,21 @@ +## API Report File for "@backstage/backend-testing" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Knex } from 'knex'; + +// @public +export type TestDatabaseId = 'POSTGRES_13' | 'POSTGRES_9' | 'MYSQL_8' | 'SQLITE_3'; + +// @public +export class TestDatabases { + static create(): TestDatabases; + init(id: TestDatabaseId): Promise; + } + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/backend-testing/package.json b/packages/backend-testing/package.json new file mode 100644 index 0000000000..345d294a6c --- /dev/null +++ b/packages/backend-testing/package.json @@ -0,0 +1,50 @@ +{ + "name": "@backstage/backend-testing", + "description": "Test helpers library for Backstage backends", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/backend-testing" + }, + "keywords": [ + "backstage", + "test" + ], + "license": "Apache-2.0", + "scripts": { + "build": "backstage-cli build --outputs cjs,types", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/backend-common": "^0.7.0", + "@backstage/cli": "^0.6.10", + "@backstage/config": "^0.1.5", + "knex": "^0.95.1", + "mysql2": "^2.2.5", + "pg": "^8.3.0", + "sqlite3": "^5.0.0", + "testcontainers": "^7.10.0", + "uuid": "^8.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.6.10", + "jest": "^26.0.1" + }, + "files": [ + "dist" + ] +} diff --git a/packages/backend-testing/src/database/TestDatabases.test.ts b/packages/backend-testing/src/database/TestDatabases.test.ts new file mode 100644 index 0000000000..61220e3090 --- /dev/null +++ b/packages/backend-testing/src/database/TestDatabases.test.ts @@ -0,0 +1,154 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 knexFactory from 'knex'; +import { GenericContainer } from 'testcontainers'; +import { TestDatabases } from './TestDatabases'; + +describe('TestDatabases', () => { + const OLD_ENV = process.env; + beforeEach(() => { + jest.resetModules(); + process.env = { ...OLD_ENV }; + }); + afterAll(() => { + process.env = OLD_ENV; + }); + + it.each([ + ['POSTGRES_13'], + ['POSTGRES_9'], + ['MYSQL_8'], + ['SQLITE_3'], + ] as const)( + 'creates distinct %p databases', + async databaseId => { + const dbs = TestDatabases.create(); + const db1 = await dbs.init(databaseId); + const db2 = await dbs.init(databaseId); + await db1.schema.createTable('a', table => table.string('x').primary()); + await db2.schema.createTable('a', table => table.string('y').primary()); + await expect(db1.select({ a: db1.raw('1') })).resolves.toEqual([ + { a: 1 }, + ]); + }, + 60_000, + ); + + it('obeys a provided connection string for postgres 13', async () => { + const dbs = TestDatabases.create(); + const container = await new GenericContainer('postgres:13') + .withExposedPorts(5432) + .withEnv('POSTGRES_USER', 'user') + .withEnv('POSTGRES_PASSWORD', 'pass') + .withTmpFs({ '/var/lib/postgresql/data': 'rw' }) + .start(); + const host = container.getHost(); + const port = container.getMappedPort(5432); + + try { + // Leave a mark + process.env.BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING = `postgresql://user:pass@${host}:${port}`; + const input = await dbs.init('POSTGRES_13'); + await input.schema.createTable('a', table => table.string('x').primary()); + await input.insert({ x: 'y' }).into('a'); + + // Look for the mark + const output = knexFactory({ + client: 'pg', + connection: { + host, + port, + user: 'user', + password: 'pass', + database: 'backstage_plugin_0', + }, + }); + await expect(output.select('x').from('a')).resolves.toEqual([{ x: 'y' }]); + } finally { + await container.stop({ timeout: 10_000 }); + } + }, 60_000); + + it('obeys a provided connection string for postgres 9', async () => { + const dbs = TestDatabases.create(); + const container = await new GenericContainer('postgres:9') + .withExposedPorts(5432) + .withEnv('POSTGRES_USER', 'user') + .withEnv('POSTGRES_PASSWORD', 'pass') + .withTmpFs({ '/var/lib/postgresql/data': 'rw' }) + .start(); + const host = container.getHost(); + const port = container.getMappedPort(5432); + + try { + // Leave a mark + process.env.BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING = `postgresql://user:pass@${host}:${port}`; + const input = await dbs.init('POSTGRES_9'); + await input.schema.createTable('a', table => table.string('x').primary()); + await input.insert({ x: 'y' }).into('a'); + + // Look for the mark + const output = knexFactory({ + client: 'pg', + connection: { + host, + port, + user: 'user', + password: 'pass', + database: 'backstage_plugin_0', + }, + }); + await expect(output.select('x').from('a')).resolves.toEqual([{ x: 'y' }]); + } finally { + await container.stop({ timeout: 10_000 }); + } + }, 60_000); + + it('obeys a provided connection string for mysql 8', async () => { + const dbs = TestDatabases.create(); + const container = await new GenericContainer('mysql:8') + .withExposedPorts(3306) + .withEnv('MYSQL_ROOT_PASSWORD', 'pass') + .withTmpFs({ '/var/lib/mysql': 'rw' }) + .start(); + const host = container.getHost(); + const port = container.getMappedPort(3306); + + try { + // Leave a mark + process.env.BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING = `mysql://root:pass@${host}:${port}/ignored`; + const input = await dbs.init('MYSQL_8'); + await input.schema.createTable('a', table => table.string('x').primary()); + await input.insert({ x: 'y' }).into('a'); + + // Look for the mark + const output = knexFactory({ + client: 'mysql2', + connection: { + host, + port, + user: 'root', + password: 'pass', + database: 'backstage_plugin_0', + }, + }); + await expect(output.select('x').from('a')).resolves.toEqual([{ x: 'y' }]); + } finally { + await container.stop({ timeout: 10_000 }); + } + }, 60_000); +}); diff --git a/packages/backend-testing/src/database/TestDatabases.ts b/packages/backend-testing/src/database/TestDatabases.ts new file mode 100644 index 0000000000..927ff2632c --- /dev/null +++ b/packages/backend-testing/src/database/TestDatabases.ts @@ -0,0 +1,278 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { SingleConnectionDatabaseManager } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { Knex } from 'knex'; +import { GenericContainer, StartedTestContainer } from 'testcontainers'; +import { v4 as uuid } from 'uuid'; + +type TestDatabaseProperties = { + name: string; + driver: string; + dockerImageName?: string; + connectionStringEnvironmentVariableName?: string; +}; + +type Instance = { + container?: StartedTestContainer; + databaseManager: SingleConnectionDatabaseManager; + connections: Array; +}; + +const supportedDatabases = Object.freeze({ + POSTGRES_13: { + name: 'Postgres 13.x', + driver: 'pg', + dockerImageName: 'postgres:13', + connectionStringEnvironmentVariableName: + 'BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING', + }, + POSTGRES_9: { + name: 'Postgres 9.x', + driver: 'pg', + dockerImageName: 'postgres:9', + connectionStringEnvironmentVariableName: + 'BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING', + }, + MYSQL_8: { + name: 'MySQL 8.x', + driver: 'mysql2', + dockerImageName: 'mysql:8', + connectionStringEnvironmentVariableName: + 'BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING', + }, + SQLITE_3: { + name: 'SQLite 3.x', + driver: 'sqlite3', + }, +} as const); + +/** + * The possible databases to test against. + */ +export type TestDatabaseId = + | 'POSTGRES_13' + | 'POSTGRES_9' + | 'MYSQL_8' + | 'SQLITE_3'; + +/** + * Encapsulates the creation of ephemeral test database instances for use + * inside unit or integration tests. + */ +export class TestDatabases { + private instanceById: Map = new Map(); + private lastDatabaseId = 0; + + /** + * Creates an empty `TestDatabases` instance, and sets up Jest to clean up + * all of its acquired resources after all tests finish. + * + * You typically want to create just a single instance like this at the top + * of your test file or `describe` block, and then call `init` many times on + * that instance inside the individual tests. Spinning up a "physical" + * database instance takes a considerable amount of time, slowing down tests. + * But initializing a new logical database inside that instance using `init` + * is very fast. + */ + static create() { + const databases = new TestDatabases(); + + afterAll(async () => { + await databases.shutdown(); + }); + + return databases; + } + + private constructor() {} + + /** + * Returns a fresh, unique, empty logical database on an instance of the + * given database ID platform. + * + * @param id The ID of the database platform to use, e.g. 'POSTGRES_13' + * @returns A `Knex` connection object + */ + async init(id: TestDatabaseId): Promise { + const properties = supportedDatabases[id]; + if (!properties) { + const candidates = Object.keys(supportedDatabases).join(', '); + throw new Error( + `Unsupported test database ${id}, possible values are ${candidates}`, + ); + } + + let instance: Instance | undefined = this.instanceById.get(id); + + // Ensure that a testcontainers instance is up for this ID + if (!instance) { + instance = await this.initAny(properties); + this.instanceById.set(id, instance); + } + + // Ensure that a unique logical database is created in the instance + const connection = await instance.databaseManager + .forPlugin(String(this.lastDatabaseId++)) + .getClient(); + + instance.connections.push(connection); + + return connection; + } + + private async initAny(properties: TestDatabaseProperties): Promise { + // Use the connection string if provided + if (properties.driver === 'pg' || properties.driver === 'mysql2') { + const envVarName = properties.connectionStringEnvironmentVariableName; + if (envVarName) { + const connectionString = process.env[envVarName]; + if (connectionString) { + const databaseManager = SingleConnectionDatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: properties.driver, + connection: connectionString, + }, + }, + }), + ); + return { + databaseManager, + connections: [], + }; + } + } + } + + // Otherwise start a container for the purpose + switch (properties.driver) { + case 'pg': + return this.initPostgres(properties); + case 'mysql2': + return this.initMysql(properties); + case 'sqlite3': + return this.initSqlite(properties); + default: + throw new Error(`Unknown database driver ${properties.driver}`); + } + } + + private async initPostgres( + properties: TestDatabaseProperties, + ): Promise { + const password = uuid(); + + const container = await new GenericContainer(properties.dockerImageName!) + .withExposedPorts(5432) + .withEnv('POSTGRES_PASSWORD', password) + .withTmpFs({ '/var/lib/postgresql/data': 'rw' }) + .start(); + + const databaseManager = SingleConnectionDatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'pg', + connection: { + host: container.getHost(), + port: container.getMappedPort(5432), + user: 'postgres', + password, + }, + }, + }, + }), + ); + + return { + container, + databaseManager, + connections: [], + }; + } + + private async initMysql( + properties: TestDatabaseProperties, + ): Promise { + const password = uuid(); + + const container = await new GenericContainer(properties.dockerImageName!) + .withExposedPorts(3306) + .withEnv('MYSQL_ROOT_PASSWORD', password) + .withTmpFs({ '/var/lib/mysql': 'rw' }) + .start(); + + const databaseManager = SingleConnectionDatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'mysql2', + connection: { + host: container.getHost(), + port: container.getMappedPort(3306), + user: 'root', + password, + }, + }, + }, + }), + ); + + return { + container, + databaseManager, + connections: [], + }; + } + + private async initSqlite( + _properties: TestDatabaseProperties, + ): Promise { + const databaseManager = SingleConnectionDatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'sqlite3', + connection: ':memory:', + }, + }, + }), + ); + + return { + databaseManager, + connections: [], + }; + } + + private async shutdown() { + for (const { container, connections } of this.instanceById.values()) { + try { + await Promise.all(connections.map(c => c.destroy())); + } catch { + // ignore + } + try { + await container?.stop({ timeout: 10_000 }); + } catch { + // ignore + } + } + } +} diff --git a/packages/backend-testing/src/database/index.ts b/packages/backend-testing/src/database/index.ts new file mode 100644 index 0000000000..b88ba38010 --- /dev/null +++ b/packages/backend-testing/src/database/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { TestDatabases } from './TestDatabases'; +export type { TestDatabaseId } from './TestDatabases'; diff --git a/packages/backend-testing/src/index.ts b/packages/backend-testing/src/index.ts new file mode 100644 index 0000000000..3febc35a77 --- /dev/null +++ b/packages/backend-testing/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 * from './database'; diff --git a/packages/backend-testing/src/setupTests.ts b/packages/backend-testing/src/setupTests.ts new file mode 100644 index 0000000000..ba33cf996b --- /dev/null +++ b/packages/backend-testing/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 {}; diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index c8e17bb943..e6fa30882c 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -63,6 +63,7 @@ "yup": "^0.29.3" }, "devDependencies": { + "@backstage/backend-testing": "^0.1.0", "@backstage/cli": "^0.6.10", "@backstage/test-utils": "^0.1.9", "@types/core-js": "^2.5.4", diff --git a/plugins/catalog-backend/src/next/Stitcher.test.ts b/plugins/catalog-backend/src/next/Stitcher.test.ts index f38af384fa..e4ff41039d 100644 --- a/plugins/catalog-backend/src/next/Stitcher.test.ts +++ b/plugins/catalog-backend/src/next/Stitcher.test.ts @@ -15,7 +15,7 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { Knex } from 'knex'; +import { TestDatabases } from '@backstage/backend-testing'; import { DatabaseManager } from './database/DatabaseManager'; import { DbRefreshStateReferencesRow, @@ -26,181 +26,200 @@ import { DbSearchRow } from './search'; import { DbFinalEntitiesRow, Stitcher } from './Stitcher'; describe('Stitcher', () => { - let db: Knex; + const databases = TestDatabases.create(); const logger = getVoidLogger(); - beforeEach(async () => { - db = await DatabaseManager.createTestDatabaseConnection(); - await DatabaseManager.createDatabase(db); - }); + it.each([ + ['POSTGRES_13'], + ['POSTGRES_9'], + ['SQLITE_3'], + // TODO(freben): mysql:8 is not ready to use yet + ] as const)( + 'runs the happy path for %p', + async databaseId => { + const db = await databases.init(databaseId); + await DatabaseManager.createDatabase(db); - it('runs the happy path', async () => { - const stitcher = new Stitcher(db, logger); + const stitcher = new Stitcher(db, logger); - await db.transaction(async tx => { - await tx('refresh_state').insert([ - { - entity_id: 'my-id', - entity_ref: 'k:ns/n', - unprocessed_entity: JSON.stringify({}), - processed_entity: JSON.stringify({ - apiVersion: 'a', - kind: 'k', - metadata: { - name: 'n', - namespace: 'ns', - }, - spec: { - k: 'v', - }, - }), - errors: '[]', - next_update_at: tx.fn.now(), - last_discovery_at: tx.fn.now(), - }, - ]); - await tx('refresh_state_references').insert([ - { source_key: 'a', target_entity_ref: 'k:ns/n' }, - ]); - await tx('relations').insert([ - { - originating_entity_id: 'my-id', - source_entity_ref: 'k:ns/n', - type: 'looksAt', - target_entity_ref: 'k:ns/other', - }, - ]); - }); - - await stitcher.stitch(new Set(['k:ns/n'])); - - let firstHash: string; - await db.transaction(async tx => { - const entities = await tx('final_entities'); - - expect(entities.length).toBe(1); - const entity = JSON.parse(entities[0].final_entity); - expect(entity).toEqual({ - relations: [ + await db.transaction(async tx => { + await tx('refresh_state').insert([ { - type: 'looksAt', - target: { + entity_id: 'my-id', + entity_ref: 'k:ns/n', + unprocessed_entity: JSON.stringify({}), + processed_entity: JSON.stringify({ + apiVersion: 'a', kind: 'k', - namespace: 'ns', - name: 'other', - }, + metadata: { + name: 'n', + namespace: 'ns', + }, + spec: { + k: 'v', + }, + }), + errors: '[]', + next_update_at: tx.fn.now(), + last_discovery_at: tx.fn.now(), }, - ], - apiVersion: 'a', - kind: 'k', - metadata: { - name: 'n', - namespace: 'ns', - etag: expect.any(String), - generation: 1, - uid: 'my-id', - }, - spec: { - k: 'v', - }, + ]); + await tx( + 'refresh_state_references', + ).insert([{ source_key: 'a', target_entity_ref: 'k:ns/n' }]); + await tx('relations').insert([ + { + originating_entity_id: 'my-id', + source_entity_ref: 'k:ns/n', + type: 'looksAt', + target_entity_ref: 'k:ns/other', + }, + ]); }); - expect(entity.metadata.etag).toEqual(entities[0].hash); - firstHash = entities[0].hash; + await stitcher.stitch(new Set(['k:ns/n'])); - const search = await tx('search'); - expect(search).toEqual( - expect.arrayContaining([ - { entity_id: 'my-id', key: 'relations.looksat', value: 'k:ns/other' }, - { entity_id: 'my-id', key: 'apiversion', value: 'a' }, - { entity_id: 'my-id', key: 'kind', value: 'k' }, - { entity_id: 'my-id', key: 'metadata.name', value: 'n' }, - { entity_id: 'my-id', key: 'metadata.namespace', value: 'ns' }, - { entity_id: 'my-id', key: 'metadata.uid', value: 'my-id' }, - { entity_id: 'my-id', key: 'spec.k', value: 'v' }, - ]), - ); - }); + let firstHash: string; + await db.transaction(async tx => { + const entities = await tx('final_entities'); - // Re-stitch without any changes - await stitcher.stitch(new Set(['k:ns/n'])); - - await db.transaction(async tx => { - const entities = await tx('final_entities'); - expect(entities.length).toBe(1); - const entity = JSON.parse(entities[0].final_entity); - expect(entities[0].hash).toEqual(firstHash); - expect(entity.metadata.etag).toEqual(firstHash); - }); - - // Now add one more relation and re-stitch - await db.transaction(async tx => { - await tx('relations').insert([ - { - originating_entity_id: 'my-id', - source_entity_ref: 'k:ns/n', - type: 'looksAt', - target_entity_ref: 'k:ns/third', - }, - ]); - }); - - await stitcher.stitch(new Set(['k:ns/n'])); - - await db.transaction(async tx => { - const entities = await tx('final_entities'); - - expect(entities.length).toBe(1); - const entity = JSON.parse(entities[0].final_entity); - expect(entity).toEqual({ - relations: expect.arrayContaining([ - { - type: 'looksAt', - target: { - kind: 'k', - namespace: 'ns', - name: 'other', + expect(entities.length).toBe(1); + const entity = JSON.parse(entities[0].final_entity); + expect(entity).toEqual({ + relations: [ + { + type: 'looksAt', + target: { + kind: 'k', + namespace: 'ns', + name: 'other', + }, }, + ], + apiVersion: 'a', + kind: 'k', + metadata: { + name: 'n', + namespace: 'ns', + etag: expect.any(String), + generation: 1, + uid: 'my-id', }, - { - type: 'looksAt', - target: { - kind: 'k', - namespace: 'ns', - name: 'third', + spec: { + k: 'v', + }, + }); + + expect(entity.metadata.etag).toEqual(entities[0].hash); + firstHash = entities[0].hash; + + const search = await tx('search'); + expect(search).toEqual( + expect.arrayContaining([ + { + entity_id: 'my-id', + key: 'relations.looksat', + value: 'k:ns/other', }, - }, - ]), - apiVersion: 'a', - kind: 'k', - metadata: { - name: 'n', - namespace: 'ns', - etag: expect.any(String), - generation: 1, - uid: 'my-id', - }, - spec: { - k: 'v', - }, + { entity_id: 'my-id', key: 'apiversion', value: 'a' }, + { entity_id: 'my-id', key: 'kind', value: 'k' }, + { entity_id: 'my-id', key: 'metadata.name', value: 'n' }, + { entity_id: 'my-id', key: 'metadata.namespace', value: 'ns' }, + { entity_id: 'my-id', key: 'metadata.uid', value: 'my-id' }, + { entity_id: 'my-id', key: 'spec.k', value: 'v' }, + ]), + ); }); - expect(entities[0].hash).not.toEqual(firstHash); - expect(entities[0].hash).toEqual(entity.metadata.etag); + // Re-stitch without any changes + await stitcher.stitch(new Set(['k:ns/n'])); - const search = await tx('search'); - expect(search).toEqual( - expect.arrayContaining([ - { entity_id: 'my-id', key: 'relations.looksat', value: 'k:ns/other' }, - { entity_id: 'my-id', key: 'relations.looksat', value: 'k:ns/third' }, - { entity_id: 'my-id', key: 'apiversion', value: 'a' }, - { entity_id: 'my-id', key: 'kind', value: 'k' }, - { entity_id: 'my-id', key: 'metadata.name', value: 'n' }, - { entity_id: 'my-id', key: 'metadata.namespace', value: 'ns' }, - { entity_id: 'my-id', key: 'metadata.uid', value: 'my-id' }, - { entity_id: 'my-id', key: 'spec.k', value: 'v' }, - ]), - ); - }); - }); + await db.transaction(async tx => { + const entities = await tx('final_entities'); + expect(entities.length).toBe(1); + const entity = JSON.parse(entities[0].final_entity); + expect(entities[0].hash).toEqual(firstHash); + expect(entity.metadata.etag).toEqual(firstHash); + }); + + // Now add one more relation and re-stitch + await db.transaction(async tx => { + await tx('relations').insert([ + { + originating_entity_id: 'my-id', + source_entity_ref: 'k:ns/n', + type: 'looksAt', + target_entity_ref: 'k:ns/third', + }, + ]); + }); + + await stitcher.stitch(new Set(['k:ns/n'])); + + await db.transaction(async tx => { + const entities = await tx('final_entities'); + + expect(entities.length).toBe(1); + const entity = JSON.parse(entities[0].final_entity); + expect(entity).toEqual({ + relations: expect.arrayContaining([ + { + type: 'looksAt', + target: { + kind: 'k', + namespace: 'ns', + name: 'other', + }, + }, + { + type: 'looksAt', + target: { + kind: 'k', + namespace: 'ns', + name: 'third', + }, + }, + ]), + apiVersion: 'a', + kind: 'k', + metadata: { + name: 'n', + namespace: 'ns', + etag: expect.any(String), + generation: 1, + uid: 'my-id', + }, + spec: { + k: 'v', + }, + }); + + expect(entities[0].hash).not.toEqual(firstHash); + expect(entities[0].hash).toEqual(entity.metadata.etag); + + const search = await tx('search'); + expect(search).toEqual( + expect.arrayContaining([ + { + entity_id: 'my-id', + key: 'relations.looksat', + value: 'k:ns/other', + }, + { + entity_id: 'my-id', + key: 'relations.looksat', + value: 'k:ns/third', + }, + { entity_id: 'my-id', key: 'apiversion', value: 'a' }, + { entity_id: 'my-id', key: 'kind', value: 'k' }, + { entity_id: 'my-id', key: 'metadata.name', value: 'n' }, + { entity_id: 'my-id', key: 'metadata.namespace', value: 'ns' }, + { entity_id: 'my-id', key: 'metadata.uid', value: 'my-id' }, + { entity_id: 'my-id', key: 'spec.k', value: 'v' }, + ]), + ); + }); + }, + 30000, + ); }); diff --git a/scripts/api-extractor.ts b/scripts/api-extractor.ts index 37dc4769fd..4fe76cb38d 100644 --- a/scripts/api-extractor.ts +++ b/scripts/api-extractor.ts @@ -62,6 +62,7 @@ PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor = function tryGetPackag const DOCUMENTED_PACKAGES = [ 'packages/backend-common', + 'packages/backend-testing', 'packages/catalog-client', 'packages/catalog-model', 'packages/cli-common', diff --git a/yarn.lock b/yarn.lock index a21c6ef0f1..ae7b15a344 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6763,6 +6763,21 @@ resolved "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== +"@types/ssh2-streams@*": + version "0.1.8" + resolved "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.8.tgz#142af404dae059931aea7fcd1511b5478964feb6" + integrity sha512-I7gixRPUvVIyJuCEvnmhr3KvA2dC0639kKswqD4H5b4/FOcnPtNU+qWLiXdKIqqX9twUvi5j0U1mwKE5CUsrfA== + dependencies: + "@types/node" "*" + +"@types/ssh2@^0.5.45": + version "0.5.46" + resolved "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.46.tgz#e12341a242aea0e98ac2dec89e039bf421fd3584" + integrity sha512-1pC8FHrMPYdkLoUOwTYYifnSEPzAFZRsp3JFC/vokQ+dRrVI+hDBwz0SNmQ3pL6h39OSZlPs0uCG7wKJkftnaA== + dependencies: + "@types/node" "*" + "@types/ssh2-streams" "*" + "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" @@ -7606,7 +7621,7 @@ any-observable@^0.3.0: resolved "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b" integrity sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog== -any-promise@^1.0.0: +any-promise@^1.0.0, any-promise@^1.1.0: version "1.3.0" resolved "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= @@ -7883,6 +7898,19 @@ archiver@^5.0.2: tar-stream "^2.1.4" zip-stream "^4.0.4" +archiver@^5.2.0: + version "5.3.0" + resolved "https://registry.npmjs.org/archiver/-/archiver-5.3.0.tgz#dd3e097624481741df626267564f7dd8640a45ba" + integrity sha512-iUw+oDwK0fgNpvveEsdQ0Ase6IIKztBJU2U0E9MzszMfmVVUyv1QJhS2ITW9ZCqx8dktAxVAjWWkKehuZE8OPg== + dependencies: + archiver-utils "^2.1.0" + async "^3.2.0" + buffer-crc32 "^0.2.1" + readable-stream "^3.6.0" + readdir-glob "^1.0.0" + tar-stream "^2.2.0" + zip-stream "^4.1.0" + are-we-there-yet@~1.1.2: version "1.1.5" resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" @@ -10118,6 +10146,16 @@ compress-commons@^4.0.2: normalize-path "^3.0.0" readable-stream "^3.6.0" +compress-commons@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.0.tgz#25ec7a4528852ccd1d441a7d4353cd0ece11371b" + integrity sha512-ofaaLqfraD1YRTkrRKPCrGJ1pFeDG/MVCkVVV2FNGeWquSlqw5wOrwOfPQ1xF2u+blpeWASie5EubHz+vsNIgA== + dependencies: + buffer-crc32 "^0.2.13" + crc32-stream "^4.0.1" + normalize-path "^3.0.0" + readable-stream "^3.6.0" + compressible@^2.0.12, compressible@~2.0.16: version "2.0.18" resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" @@ -11675,6 +11713,13 @@ dns-txt@^2.0.2: dependencies: buffer-indexof "^1.0.0" +docker-compose@^0.23.5: + version "0.23.9" + resolved "https://registry.npmjs.org/docker-compose/-/docker-compose-0.23.9.tgz#d24343397792562f72a53eb895e76f5c7c751ee8" + integrity sha512-ucDcqBoavyQMuxPOpc8y0LqtALc5Y7h/Ga/FEiZ8m+ZqaVf3WuCh5HODtqvGO8HZMGrcH7HPyBqJ+gRJG+dBvg== + dependencies: + yaml "^1.10.2" + docker-modem@^2.1.0: version "2.1.3" resolved "https://registry.npmjs.org/docker-modem/-/docker-modem-2.1.3.tgz#15432225f63db02eb5de4bb9a621b7293e5f264d" @@ -24184,6 +24229,14 @@ sqlstring@^2.3.2: resolved "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.2.tgz#cdae7169389a1375b18e885f2e60b3e460809514" integrity sha512-vF4ZbYdKS8OnoJAWBmMxCQDkiEBkGQYU7UZPtL8flbDRSNkhaXvRJ279ZtI6M+zDaQovVU4tuRgzK5fVhvFAhg== +ssh-remote-port-forward@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.3.tgz#074cebed6d54a42ce4659d6aa24987e433ff5fef" + integrity sha512-PJ6qGFmB6n0iMCQIp+qzx8qVUE5cgacopvEh63t3NJjEQHOaza/JT9zywmmVlaol/eGtCmpvBXx2A03ih1Y+xg== + dependencies: + "@types/ssh2" "^0.5.45" + ssh2 "^0.8.9" + ssh2-streams@~0.4.10: version "0.4.10" resolved "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz#48ef7e8a0e39d8f2921c30521d56dacb31d23a34" @@ -24193,7 +24246,7 @@ ssh2-streams@~0.4.10: bcrypt-pbkdf "^1.0.2" streamsearch "~0.1.2" -ssh2@^0.8.7: +ssh2@^0.8.7, ssh2@^0.8.9: version "0.8.9" resolved "https://registry.npmjs.org/ssh2/-/ssh2-0.8.9.tgz#54da3a6c4ba3daf0d8477a538a481326091815f3" integrity sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw== @@ -24397,6 +24450,13 @@ stream-shift@^1.0.0: resolved "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== +stream-to-array@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/stream-to-array/-/stream-to-array-2.3.0.tgz#bbf6b39f5f43ec30bc71babcb37557acecf34353" + integrity sha1-u/azn19D7DC8cbq8s3VXrOzzQ1M= + dependencies: + any-promise "^1.1.0" + stream-transform@^2.0.1: version "2.0.2" resolved "https://registry.npmjs.org/stream-transform/-/stream-transform-2.0.2.tgz#3cb7a14c802eb39bc40caaab0535e584f3a65caf" @@ -24951,7 +25011,7 @@ tapable@^1.0.0, tapable@^1.1.3: resolved "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== -tar-fs@^2.0.0: +tar-fs@^2.0.0, tar-fs@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== @@ -24982,6 +25042,17 @@ tar-stream@^2.0.0, tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" +tar-stream@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + tar@^2.0.0: version "2.2.2" resolved "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" @@ -25139,6 +25210,25 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +testcontainers@^7.10.0: + version "7.10.0" + resolved "https://registry.npmjs.org/testcontainers/-/testcontainers-7.10.0.tgz#d0306ac9e54c24006e0de0e4505ca7d4a5a0db9e" + integrity sha512-xwz1dzRUmaYTNL4qH8//YlJzSGfVmooGtJNc0ynFm0mtUTyYu4HJLpPZB9e7+/ZiQlWp73LSTH/gSwuNtl8FXw== + dependencies: + "@types/archiver" "^5.1.0" + "@types/dockerode" "^3.2.1" + archiver "^5.2.0" + byline "^5.0.0" + debug "^4.3.1" + docker-compose "^0.23.5" + dockerode "^3.2.1" + get-port "^5.1.1" + glob "^7.1.6" + slash "^3.0.0" + ssh-remote-port-forward "^1.0.3" + stream-to-array "^2.3.0" + tar-fs "^2.1.1" + text-encoding-utf-8@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" @@ -27093,7 +27183,7 @@ yaml-ast-parser@0.0.43, yaml-ast-parser@^0.0.43: resolved "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz#e8a23e6fb4c38076ab92995c5dca33f3d3d7c9bb" integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A== -yaml@^1.10.0, yaml@^1.7.2, yaml@^1.9.2: +yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2, yaml@^1.9.2: version "1.10.2" resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== @@ -27275,6 +27365,15 @@ zip-stream@^4.0.4: compress-commons "^4.0.2" readable-stream "^3.6.0" +zip-stream@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz#51dd326571544e36aa3f756430b313576dc8fc79" + integrity sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A== + dependencies: + archiver-utils "^2.1.0" + compress-commons "^4.1.0" + readable-stream "^3.6.0" + zombie@^6.1.4: version "6.1.4" resolved "https://registry.npmjs.org/zombie/-/zombie-6.1.4.tgz#9f0f53f3d9a032beb7f3fe5b382146a3475a4d47" From ea21d46f0722a86688429540a81625d127a7d62f Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Wed, 12 May 2021 09:56:00 +0200 Subject: [PATCH 73/95] add changeset Signed-off-by: Samira Mokaram --- .changeset/khaki-waves-dream.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/khaki-waves-dream.md diff --git a/.changeset/khaki-waves-dream.md b/.changeset/khaki-waves-dream.md new file mode 100644 index 0000000000..38254c21be --- /dev/null +++ b/.changeset/khaki-waves-dream.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes': patch +--- + +Export types From a66c6f986c2b72bf4a03bdf7a6bc4641996b6497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 12 May 2021 10:29:16 +0200 Subject: [PATCH 74/95] Revert "Introduce `@backstage/backend-testing`, with `TestDatabases` to start off" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/wise-pens-count.md | 5 - packages/backend-testing/.eslintrc.js | 3 - packages/backend-testing/README.md | 18 - packages/backend-testing/api-report.md | 21 -- packages/backend-testing/package.json | 50 --- .../src/database/TestDatabases.test.ts | 154 -------- .../src/database/TestDatabases.ts | 278 -------------- .../backend-testing/src/database/index.ts | 18 - packages/backend-testing/src/index.ts | 17 - packages/backend-testing/src/setupTests.ts | 17 - plugins/catalog-backend/package.json | 1 - .../catalog-backend/src/next/Stitcher.test.ts | 341 +++++++++--------- scripts/api-extractor.ts | 1 - yarn.lock | 107 +----- 14 files changed, 165 insertions(+), 866 deletions(-) delete mode 100644 .changeset/wise-pens-count.md delete mode 100644 packages/backend-testing/.eslintrc.js delete mode 100644 packages/backend-testing/README.md delete mode 100644 packages/backend-testing/api-report.md delete mode 100644 packages/backend-testing/package.json delete mode 100644 packages/backend-testing/src/database/TestDatabases.test.ts delete mode 100644 packages/backend-testing/src/database/TestDatabases.ts delete mode 100644 packages/backend-testing/src/database/index.ts delete mode 100644 packages/backend-testing/src/index.ts delete mode 100644 packages/backend-testing/src/setupTests.ts diff --git a/.changeset/wise-pens-count.md b/.changeset/wise-pens-count.md deleted file mode 100644 index 26eda866bc..0000000000 --- a/.changeset/wise-pens-count.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Use `TestDatabases` from `@backstage/backend-testing` diff --git a/packages/backend-testing/.eslintrc.js b/packages/backend-testing/.eslintrc.js deleted file mode 100644 index 16a033dbc6..0000000000 --- a/packages/backend-testing/.eslintrc.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; diff --git a/packages/backend-testing/README.md b/packages/backend-testing/README.md deleted file mode 100644 index f12e94ac25..0000000000 --- a/packages/backend-testing/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# @backstage/backend-testing - -Test helpers library for Backstage backends. - -## Usage - -Add the library as a `devDependency` to your backend package: - -```sh -# From the Backstage root directory, go to your backend package, or to a backend plugin -cd plugins/my-plugin-backend -yarn add --dev @backstage/backend-testing -``` - -## Documentation - -- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) -- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) diff --git a/packages/backend-testing/api-report.md b/packages/backend-testing/api-report.md deleted file mode 100644 index c4c78408ec..0000000000 --- a/packages/backend-testing/api-report.md +++ /dev/null @@ -1,21 +0,0 @@ -## API Report File for "@backstage/backend-testing" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts - -import { Knex } from 'knex'; - -// @public -export type TestDatabaseId = 'POSTGRES_13' | 'POSTGRES_9' | 'MYSQL_8' | 'SQLITE_3'; - -// @public -export class TestDatabases { - static create(): TestDatabases; - init(id: TestDatabaseId): Promise; - } - - -// (No @packageDocumentation comment for this package) - -``` diff --git a/packages/backend-testing/package.json b/packages/backend-testing/package.json deleted file mode 100644 index 345d294a6c..0000000000 --- a/packages/backend-testing/package.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "@backstage/backend-testing", - "description": "Test helpers library for Backstage backends", - "version": "0.1.0", - "main": "src/index.ts", - "types": "src/index.ts", - "private": false, - "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" - }, - "homepage": "https://backstage.io", - "repository": { - "type": "git", - "url": "https://github.com/backstage/backstage", - "directory": "packages/backend-testing" - }, - "keywords": [ - "backstage", - "test" - ], - "license": "Apache-2.0", - "scripts": { - "build": "backstage-cli build --outputs cjs,types", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" - }, - "dependencies": { - "@backstage/backend-common": "^0.7.0", - "@backstage/cli": "^0.6.10", - "@backstage/config": "^0.1.5", - "knex": "^0.95.1", - "mysql2": "^2.2.5", - "pg": "^8.3.0", - "sqlite3": "^5.0.0", - "testcontainers": "^7.10.0", - "uuid": "^8.0.0" - }, - "devDependencies": { - "@backstage/cli": "^0.6.10", - "jest": "^26.0.1" - }, - "files": [ - "dist" - ] -} diff --git a/packages/backend-testing/src/database/TestDatabases.test.ts b/packages/backend-testing/src/database/TestDatabases.test.ts deleted file mode 100644 index 61220e3090..0000000000 --- a/packages/backend-testing/src/database/TestDatabases.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright 2021 Spotify AB - * - * 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 knexFactory from 'knex'; -import { GenericContainer } from 'testcontainers'; -import { TestDatabases } from './TestDatabases'; - -describe('TestDatabases', () => { - const OLD_ENV = process.env; - beforeEach(() => { - jest.resetModules(); - process.env = { ...OLD_ENV }; - }); - afterAll(() => { - process.env = OLD_ENV; - }); - - it.each([ - ['POSTGRES_13'], - ['POSTGRES_9'], - ['MYSQL_8'], - ['SQLITE_3'], - ] as const)( - 'creates distinct %p databases', - async databaseId => { - const dbs = TestDatabases.create(); - const db1 = await dbs.init(databaseId); - const db2 = await dbs.init(databaseId); - await db1.schema.createTable('a', table => table.string('x').primary()); - await db2.schema.createTable('a', table => table.string('y').primary()); - await expect(db1.select({ a: db1.raw('1') })).resolves.toEqual([ - { a: 1 }, - ]); - }, - 60_000, - ); - - it('obeys a provided connection string for postgres 13', async () => { - const dbs = TestDatabases.create(); - const container = await new GenericContainer('postgres:13') - .withExposedPorts(5432) - .withEnv('POSTGRES_USER', 'user') - .withEnv('POSTGRES_PASSWORD', 'pass') - .withTmpFs({ '/var/lib/postgresql/data': 'rw' }) - .start(); - const host = container.getHost(); - const port = container.getMappedPort(5432); - - try { - // Leave a mark - process.env.BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING = `postgresql://user:pass@${host}:${port}`; - const input = await dbs.init('POSTGRES_13'); - await input.schema.createTable('a', table => table.string('x').primary()); - await input.insert({ x: 'y' }).into('a'); - - // Look for the mark - const output = knexFactory({ - client: 'pg', - connection: { - host, - port, - user: 'user', - password: 'pass', - database: 'backstage_plugin_0', - }, - }); - await expect(output.select('x').from('a')).resolves.toEqual([{ x: 'y' }]); - } finally { - await container.stop({ timeout: 10_000 }); - } - }, 60_000); - - it('obeys a provided connection string for postgres 9', async () => { - const dbs = TestDatabases.create(); - const container = await new GenericContainer('postgres:9') - .withExposedPorts(5432) - .withEnv('POSTGRES_USER', 'user') - .withEnv('POSTGRES_PASSWORD', 'pass') - .withTmpFs({ '/var/lib/postgresql/data': 'rw' }) - .start(); - const host = container.getHost(); - const port = container.getMappedPort(5432); - - try { - // Leave a mark - process.env.BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING = `postgresql://user:pass@${host}:${port}`; - const input = await dbs.init('POSTGRES_9'); - await input.schema.createTable('a', table => table.string('x').primary()); - await input.insert({ x: 'y' }).into('a'); - - // Look for the mark - const output = knexFactory({ - client: 'pg', - connection: { - host, - port, - user: 'user', - password: 'pass', - database: 'backstage_plugin_0', - }, - }); - await expect(output.select('x').from('a')).resolves.toEqual([{ x: 'y' }]); - } finally { - await container.stop({ timeout: 10_000 }); - } - }, 60_000); - - it('obeys a provided connection string for mysql 8', async () => { - const dbs = TestDatabases.create(); - const container = await new GenericContainer('mysql:8') - .withExposedPorts(3306) - .withEnv('MYSQL_ROOT_PASSWORD', 'pass') - .withTmpFs({ '/var/lib/mysql': 'rw' }) - .start(); - const host = container.getHost(); - const port = container.getMappedPort(3306); - - try { - // Leave a mark - process.env.BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING = `mysql://root:pass@${host}:${port}/ignored`; - const input = await dbs.init('MYSQL_8'); - await input.schema.createTable('a', table => table.string('x').primary()); - await input.insert({ x: 'y' }).into('a'); - - // Look for the mark - const output = knexFactory({ - client: 'mysql2', - connection: { - host, - port, - user: 'root', - password: 'pass', - database: 'backstage_plugin_0', - }, - }); - await expect(output.select('x').from('a')).resolves.toEqual([{ x: 'y' }]); - } finally { - await container.stop({ timeout: 10_000 }); - } - }, 60_000); -}); diff --git a/packages/backend-testing/src/database/TestDatabases.ts b/packages/backend-testing/src/database/TestDatabases.ts deleted file mode 100644 index 927ff2632c..0000000000 --- a/packages/backend-testing/src/database/TestDatabases.ts +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright 2021 Spotify AB - * - * 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 { SingleConnectionDatabaseManager } from '@backstage/backend-common'; -import { ConfigReader } from '@backstage/config'; -import { Knex } from 'knex'; -import { GenericContainer, StartedTestContainer } from 'testcontainers'; -import { v4 as uuid } from 'uuid'; - -type TestDatabaseProperties = { - name: string; - driver: string; - dockerImageName?: string; - connectionStringEnvironmentVariableName?: string; -}; - -type Instance = { - container?: StartedTestContainer; - databaseManager: SingleConnectionDatabaseManager; - connections: Array; -}; - -const supportedDatabases = Object.freeze({ - POSTGRES_13: { - name: 'Postgres 13.x', - driver: 'pg', - dockerImageName: 'postgres:13', - connectionStringEnvironmentVariableName: - 'BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING', - }, - POSTGRES_9: { - name: 'Postgres 9.x', - driver: 'pg', - dockerImageName: 'postgres:9', - connectionStringEnvironmentVariableName: - 'BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING', - }, - MYSQL_8: { - name: 'MySQL 8.x', - driver: 'mysql2', - dockerImageName: 'mysql:8', - connectionStringEnvironmentVariableName: - 'BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING', - }, - SQLITE_3: { - name: 'SQLite 3.x', - driver: 'sqlite3', - }, -} as const); - -/** - * The possible databases to test against. - */ -export type TestDatabaseId = - | 'POSTGRES_13' - | 'POSTGRES_9' - | 'MYSQL_8' - | 'SQLITE_3'; - -/** - * Encapsulates the creation of ephemeral test database instances for use - * inside unit or integration tests. - */ -export class TestDatabases { - private instanceById: Map = new Map(); - private lastDatabaseId = 0; - - /** - * Creates an empty `TestDatabases` instance, and sets up Jest to clean up - * all of its acquired resources after all tests finish. - * - * You typically want to create just a single instance like this at the top - * of your test file or `describe` block, and then call `init` many times on - * that instance inside the individual tests. Spinning up a "physical" - * database instance takes a considerable amount of time, slowing down tests. - * But initializing a new logical database inside that instance using `init` - * is very fast. - */ - static create() { - const databases = new TestDatabases(); - - afterAll(async () => { - await databases.shutdown(); - }); - - return databases; - } - - private constructor() {} - - /** - * Returns a fresh, unique, empty logical database on an instance of the - * given database ID platform. - * - * @param id The ID of the database platform to use, e.g. 'POSTGRES_13' - * @returns A `Knex` connection object - */ - async init(id: TestDatabaseId): Promise { - const properties = supportedDatabases[id]; - if (!properties) { - const candidates = Object.keys(supportedDatabases).join(', '); - throw new Error( - `Unsupported test database ${id}, possible values are ${candidates}`, - ); - } - - let instance: Instance | undefined = this.instanceById.get(id); - - // Ensure that a testcontainers instance is up for this ID - if (!instance) { - instance = await this.initAny(properties); - this.instanceById.set(id, instance); - } - - // Ensure that a unique logical database is created in the instance - const connection = await instance.databaseManager - .forPlugin(String(this.lastDatabaseId++)) - .getClient(); - - instance.connections.push(connection); - - return connection; - } - - private async initAny(properties: TestDatabaseProperties): Promise { - // Use the connection string if provided - if (properties.driver === 'pg' || properties.driver === 'mysql2') { - const envVarName = properties.connectionStringEnvironmentVariableName; - if (envVarName) { - const connectionString = process.env[envVarName]; - if (connectionString) { - const databaseManager = SingleConnectionDatabaseManager.fromConfig( - new ConfigReader({ - backend: { - database: { - client: properties.driver, - connection: connectionString, - }, - }, - }), - ); - return { - databaseManager, - connections: [], - }; - } - } - } - - // Otherwise start a container for the purpose - switch (properties.driver) { - case 'pg': - return this.initPostgres(properties); - case 'mysql2': - return this.initMysql(properties); - case 'sqlite3': - return this.initSqlite(properties); - default: - throw new Error(`Unknown database driver ${properties.driver}`); - } - } - - private async initPostgres( - properties: TestDatabaseProperties, - ): Promise { - const password = uuid(); - - const container = await new GenericContainer(properties.dockerImageName!) - .withExposedPorts(5432) - .withEnv('POSTGRES_PASSWORD', password) - .withTmpFs({ '/var/lib/postgresql/data': 'rw' }) - .start(); - - const databaseManager = SingleConnectionDatabaseManager.fromConfig( - new ConfigReader({ - backend: { - database: { - client: 'pg', - connection: { - host: container.getHost(), - port: container.getMappedPort(5432), - user: 'postgres', - password, - }, - }, - }, - }), - ); - - return { - container, - databaseManager, - connections: [], - }; - } - - private async initMysql( - properties: TestDatabaseProperties, - ): Promise { - const password = uuid(); - - const container = await new GenericContainer(properties.dockerImageName!) - .withExposedPorts(3306) - .withEnv('MYSQL_ROOT_PASSWORD', password) - .withTmpFs({ '/var/lib/mysql': 'rw' }) - .start(); - - const databaseManager = SingleConnectionDatabaseManager.fromConfig( - new ConfigReader({ - backend: { - database: { - client: 'mysql2', - connection: { - host: container.getHost(), - port: container.getMappedPort(3306), - user: 'root', - password, - }, - }, - }, - }), - ); - - return { - container, - databaseManager, - connections: [], - }; - } - - private async initSqlite( - _properties: TestDatabaseProperties, - ): Promise { - const databaseManager = SingleConnectionDatabaseManager.fromConfig( - new ConfigReader({ - backend: { - database: { - client: 'sqlite3', - connection: ':memory:', - }, - }, - }), - ); - - return { - databaseManager, - connections: [], - }; - } - - private async shutdown() { - for (const { container, connections } of this.instanceById.values()) { - try { - await Promise.all(connections.map(c => c.destroy())); - } catch { - // ignore - } - try { - await container?.stop({ timeout: 10_000 }); - } catch { - // ignore - } - } - } -} diff --git a/packages/backend-testing/src/database/index.ts b/packages/backend-testing/src/database/index.ts deleted file mode 100644 index b88ba38010..0000000000 --- a/packages/backend-testing/src/database/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2021 Spotify AB - * - * 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 { TestDatabases } from './TestDatabases'; -export type { TestDatabaseId } from './TestDatabases'; diff --git a/packages/backend-testing/src/index.ts b/packages/backend-testing/src/index.ts deleted file mode 100644 index 3febc35a77..0000000000 --- a/packages/backend-testing/src/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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 * from './database'; diff --git a/packages/backend-testing/src/setupTests.ts b/packages/backend-testing/src/setupTests.ts deleted file mode 100644 index ba33cf996b..0000000000 --- a/packages/backend-testing/src/setupTests.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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 {}; diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index e6fa30882c..c8e17bb943 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -63,7 +63,6 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/backend-testing": "^0.1.0", "@backstage/cli": "^0.6.10", "@backstage/test-utils": "^0.1.9", "@types/core-js": "^2.5.4", diff --git a/plugins/catalog-backend/src/next/Stitcher.test.ts b/plugins/catalog-backend/src/next/Stitcher.test.ts index e4ff41039d..f38af384fa 100644 --- a/plugins/catalog-backend/src/next/Stitcher.test.ts +++ b/plugins/catalog-backend/src/next/Stitcher.test.ts @@ -15,7 +15,7 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { TestDatabases } from '@backstage/backend-testing'; +import { Knex } from 'knex'; import { DatabaseManager } from './database/DatabaseManager'; import { DbRefreshStateReferencesRow, @@ -26,200 +26,181 @@ import { DbSearchRow } from './search'; import { DbFinalEntitiesRow, Stitcher } from './Stitcher'; describe('Stitcher', () => { - const databases = TestDatabases.create(); + let db: Knex; const logger = getVoidLogger(); - it.each([ - ['POSTGRES_13'], - ['POSTGRES_9'], - ['SQLITE_3'], - // TODO(freben): mysql:8 is not ready to use yet - ] as const)( - 'runs the happy path for %p', - async databaseId => { - const db = await databases.init(databaseId); - await DatabaseManager.createDatabase(db); + beforeEach(async () => { + db = await DatabaseManager.createTestDatabaseConnection(); + await DatabaseManager.createDatabase(db); + }); - const stitcher = new Stitcher(db, logger); + it('runs the happy path', async () => { + const stitcher = new Stitcher(db, logger); - await db.transaction(async tx => { - await tx('refresh_state').insert([ + await db.transaction(async tx => { + await tx('refresh_state').insert([ + { + entity_id: 'my-id', + entity_ref: 'k:ns/n', + unprocessed_entity: JSON.stringify({}), + processed_entity: JSON.stringify({ + apiVersion: 'a', + kind: 'k', + metadata: { + name: 'n', + namespace: 'ns', + }, + spec: { + k: 'v', + }, + }), + errors: '[]', + next_update_at: tx.fn.now(), + last_discovery_at: tx.fn.now(), + }, + ]); + await tx('refresh_state_references').insert([ + { source_key: 'a', target_entity_ref: 'k:ns/n' }, + ]); + await tx('relations').insert([ + { + originating_entity_id: 'my-id', + source_entity_ref: 'k:ns/n', + type: 'looksAt', + target_entity_ref: 'k:ns/other', + }, + ]); + }); + + await stitcher.stitch(new Set(['k:ns/n'])); + + let firstHash: string; + await db.transaction(async tx => { + const entities = await tx('final_entities'); + + expect(entities.length).toBe(1); + const entity = JSON.parse(entities[0].final_entity); + expect(entity).toEqual({ + relations: [ { - entity_id: 'my-id', - entity_ref: 'k:ns/n', - unprocessed_entity: JSON.stringify({}), - processed_entity: JSON.stringify({ - apiVersion: 'a', + type: 'looksAt', + target: { kind: 'k', - metadata: { - name: 'n', - namespace: 'ns', - }, - spec: { - k: 'v', - }, - }), - errors: '[]', - next_update_at: tx.fn.now(), - last_discovery_at: tx.fn.now(), + namespace: 'ns', + name: 'other', + }, }, - ]); - await tx( - 'refresh_state_references', - ).insert([{ source_key: 'a', target_entity_ref: 'k:ns/n' }]); - await tx('relations').insert([ + ], + apiVersion: 'a', + kind: 'k', + metadata: { + name: 'n', + namespace: 'ns', + etag: expect.any(String), + generation: 1, + uid: 'my-id', + }, + spec: { + k: 'v', + }, + }); + + expect(entity.metadata.etag).toEqual(entities[0].hash); + firstHash = entities[0].hash; + + const search = await tx('search'); + expect(search).toEqual( + expect.arrayContaining([ + { entity_id: 'my-id', key: 'relations.looksat', value: 'k:ns/other' }, + { entity_id: 'my-id', key: 'apiversion', value: 'a' }, + { entity_id: 'my-id', key: 'kind', value: 'k' }, + { entity_id: 'my-id', key: 'metadata.name', value: 'n' }, + { entity_id: 'my-id', key: 'metadata.namespace', value: 'ns' }, + { entity_id: 'my-id', key: 'metadata.uid', value: 'my-id' }, + { entity_id: 'my-id', key: 'spec.k', value: 'v' }, + ]), + ); + }); + + // Re-stitch without any changes + await stitcher.stitch(new Set(['k:ns/n'])); + + await db.transaction(async tx => { + const entities = await tx('final_entities'); + expect(entities.length).toBe(1); + const entity = JSON.parse(entities[0].final_entity); + expect(entities[0].hash).toEqual(firstHash); + expect(entity.metadata.etag).toEqual(firstHash); + }); + + // Now add one more relation and re-stitch + await db.transaction(async tx => { + await tx('relations').insert([ + { + originating_entity_id: 'my-id', + source_entity_ref: 'k:ns/n', + type: 'looksAt', + target_entity_ref: 'k:ns/third', + }, + ]); + }); + + await stitcher.stitch(new Set(['k:ns/n'])); + + await db.transaction(async tx => { + const entities = await tx('final_entities'); + + expect(entities.length).toBe(1); + const entity = JSON.parse(entities[0].final_entity); + expect(entity).toEqual({ + relations: expect.arrayContaining([ { - originating_entity_id: 'my-id', - source_entity_ref: 'k:ns/n', type: 'looksAt', - target_entity_ref: 'k:ns/other', - }, - ]); - }); - - await stitcher.stitch(new Set(['k:ns/n'])); - - let firstHash: string; - await db.transaction(async tx => { - const entities = await tx('final_entities'); - - expect(entities.length).toBe(1); - const entity = JSON.parse(entities[0].final_entity); - expect(entity).toEqual({ - relations: [ - { - type: 'looksAt', - target: { - kind: 'k', - namespace: 'ns', - name: 'other', - }, + target: { + kind: 'k', + namespace: 'ns', + name: 'other', }, - ], - apiVersion: 'a', - kind: 'k', - metadata: { - name: 'n', - namespace: 'ns', - etag: expect.any(String), - generation: 1, - uid: 'my-id', }, - spec: { - k: 'v', - }, - }); - - expect(entity.metadata.etag).toEqual(entities[0].hash); - firstHash = entities[0].hash; - - const search = await tx('search'); - expect(search).toEqual( - expect.arrayContaining([ - { - entity_id: 'my-id', - key: 'relations.looksat', - value: 'k:ns/other', - }, - { entity_id: 'my-id', key: 'apiversion', value: 'a' }, - { entity_id: 'my-id', key: 'kind', value: 'k' }, - { entity_id: 'my-id', key: 'metadata.name', value: 'n' }, - { entity_id: 'my-id', key: 'metadata.namespace', value: 'ns' }, - { entity_id: 'my-id', key: 'metadata.uid', value: 'my-id' }, - { entity_id: 'my-id', key: 'spec.k', value: 'v' }, - ]), - ); - }); - - // Re-stitch without any changes - await stitcher.stitch(new Set(['k:ns/n'])); - - await db.transaction(async tx => { - const entities = await tx('final_entities'); - expect(entities.length).toBe(1); - const entity = JSON.parse(entities[0].final_entity); - expect(entities[0].hash).toEqual(firstHash); - expect(entity.metadata.etag).toEqual(firstHash); - }); - - // Now add one more relation and re-stitch - await db.transaction(async tx => { - await tx('relations').insert([ { - originating_entity_id: 'my-id', - source_entity_ref: 'k:ns/n', type: 'looksAt', - target_entity_ref: 'k:ns/third', + target: { + kind: 'k', + namespace: 'ns', + name: 'third', + }, }, - ]); + ]), + apiVersion: 'a', + kind: 'k', + metadata: { + name: 'n', + namespace: 'ns', + etag: expect.any(String), + generation: 1, + uid: 'my-id', + }, + spec: { + k: 'v', + }, }); - await stitcher.stitch(new Set(['k:ns/n'])); + expect(entities[0].hash).not.toEqual(firstHash); + expect(entities[0].hash).toEqual(entity.metadata.etag); - await db.transaction(async tx => { - const entities = await tx('final_entities'); - - expect(entities.length).toBe(1); - const entity = JSON.parse(entities[0].final_entity); - expect(entity).toEqual({ - relations: expect.arrayContaining([ - { - type: 'looksAt', - target: { - kind: 'k', - namespace: 'ns', - name: 'other', - }, - }, - { - type: 'looksAt', - target: { - kind: 'k', - namespace: 'ns', - name: 'third', - }, - }, - ]), - apiVersion: 'a', - kind: 'k', - metadata: { - name: 'n', - namespace: 'ns', - etag: expect.any(String), - generation: 1, - uid: 'my-id', - }, - spec: { - k: 'v', - }, - }); - - expect(entities[0].hash).not.toEqual(firstHash); - expect(entities[0].hash).toEqual(entity.metadata.etag); - - const search = await tx('search'); - expect(search).toEqual( - expect.arrayContaining([ - { - entity_id: 'my-id', - key: 'relations.looksat', - value: 'k:ns/other', - }, - { - entity_id: 'my-id', - key: 'relations.looksat', - value: 'k:ns/third', - }, - { entity_id: 'my-id', key: 'apiversion', value: 'a' }, - { entity_id: 'my-id', key: 'kind', value: 'k' }, - { entity_id: 'my-id', key: 'metadata.name', value: 'n' }, - { entity_id: 'my-id', key: 'metadata.namespace', value: 'ns' }, - { entity_id: 'my-id', key: 'metadata.uid', value: 'my-id' }, - { entity_id: 'my-id', key: 'spec.k', value: 'v' }, - ]), - ); - }); - }, - 30000, - ); + const search = await tx('search'); + expect(search).toEqual( + expect.arrayContaining([ + { entity_id: 'my-id', key: 'relations.looksat', value: 'k:ns/other' }, + { entity_id: 'my-id', key: 'relations.looksat', value: 'k:ns/third' }, + { entity_id: 'my-id', key: 'apiversion', value: 'a' }, + { entity_id: 'my-id', key: 'kind', value: 'k' }, + { entity_id: 'my-id', key: 'metadata.name', value: 'n' }, + { entity_id: 'my-id', key: 'metadata.namespace', value: 'ns' }, + { entity_id: 'my-id', key: 'metadata.uid', value: 'my-id' }, + { entity_id: 'my-id', key: 'spec.k', value: 'v' }, + ]), + ); + }); + }); }); diff --git a/scripts/api-extractor.ts b/scripts/api-extractor.ts index 4fe76cb38d..37dc4769fd 100644 --- a/scripts/api-extractor.ts +++ b/scripts/api-extractor.ts @@ -62,7 +62,6 @@ PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor = function tryGetPackag const DOCUMENTED_PACKAGES = [ 'packages/backend-common', - 'packages/backend-testing', 'packages/catalog-client', 'packages/catalog-model', 'packages/cli-common', diff --git a/yarn.lock b/yarn.lock index 366a903446..330463a73a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6763,21 +6763,6 @@ resolved "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== -"@types/ssh2-streams@*": - version "0.1.8" - resolved "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.8.tgz#142af404dae059931aea7fcd1511b5478964feb6" - integrity sha512-I7gixRPUvVIyJuCEvnmhr3KvA2dC0639kKswqD4H5b4/FOcnPtNU+qWLiXdKIqqX9twUvi5j0U1mwKE5CUsrfA== - dependencies: - "@types/node" "*" - -"@types/ssh2@^0.5.45": - version "0.5.46" - resolved "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.46.tgz#e12341a242aea0e98ac2dec89e039bf421fd3584" - integrity sha512-1pC8FHrMPYdkLoUOwTYYifnSEPzAFZRsp3JFC/vokQ+dRrVI+hDBwz0SNmQ3pL6h39OSZlPs0uCG7wKJkftnaA== - dependencies: - "@types/node" "*" - "@types/ssh2-streams" "*" - "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" @@ -7621,7 +7606,7 @@ any-observable@^0.3.0: resolved "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b" integrity sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog== -any-promise@^1.0.0, any-promise@^1.1.0: +any-promise@^1.0.0: version "1.3.0" resolved "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= @@ -7898,19 +7883,6 @@ archiver@^5.0.2: tar-stream "^2.1.4" zip-stream "^4.0.4" -archiver@^5.2.0: - version "5.3.0" - resolved "https://registry.npmjs.org/archiver/-/archiver-5.3.0.tgz#dd3e097624481741df626267564f7dd8640a45ba" - integrity sha512-iUw+oDwK0fgNpvveEsdQ0Ase6IIKztBJU2U0E9MzszMfmVVUyv1QJhS2ITW9ZCqx8dktAxVAjWWkKehuZE8OPg== - dependencies: - archiver-utils "^2.1.0" - async "^3.2.0" - buffer-crc32 "^0.2.1" - readable-stream "^3.6.0" - readdir-glob "^1.0.0" - tar-stream "^2.2.0" - zip-stream "^4.1.0" - are-we-there-yet@~1.1.2: version "1.1.5" resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" @@ -10146,16 +10118,6 @@ compress-commons@^4.0.2: normalize-path "^3.0.0" readable-stream "^3.6.0" -compress-commons@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.0.tgz#25ec7a4528852ccd1d441a7d4353cd0ece11371b" - integrity sha512-ofaaLqfraD1YRTkrRKPCrGJ1pFeDG/MVCkVVV2FNGeWquSlqw5wOrwOfPQ1xF2u+blpeWASie5EubHz+vsNIgA== - dependencies: - buffer-crc32 "^0.2.13" - crc32-stream "^4.0.1" - normalize-path "^3.0.0" - readable-stream "^3.6.0" - compressible@^2.0.12, compressible@~2.0.16: version "2.0.18" resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" @@ -11720,13 +11682,6 @@ dns-txt@^2.0.2: dependencies: buffer-indexof "^1.0.0" -docker-compose@^0.23.5: - version "0.23.9" - resolved "https://registry.npmjs.org/docker-compose/-/docker-compose-0.23.9.tgz#d24343397792562f72a53eb895e76f5c7c751ee8" - integrity sha512-ucDcqBoavyQMuxPOpc8y0LqtALc5Y7h/Ga/FEiZ8m+ZqaVf3WuCh5HODtqvGO8HZMGrcH7HPyBqJ+gRJG+dBvg== - dependencies: - yaml "^1.10.2" - docker-modem@^2.1.0: version "2.1.3" resolved "https://registry.npmjs.org/docker-modem/-/docker-modem-2.1.3.tgz#15432225f63db02eb5de4bb9a621b7293e5f264d" @@ -24258,14 +24213,6 @@ sqlstring@^2.3.2: resolved "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.2.tgz#cdae7169389a1375b18e885f2e60b3e460809514" integrity sha512-vF4ZbYdKS8OnoJAWBmMxCQDkiEBkGQYU7UZPtL8flbDRSNkhaXvRJ279ZtI6M+zDaQovVU4tuRgzK5fVhvFAhg== -ssh-remote-port-forward@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.3.tgz#074cebed6d54a42ce4659d6aa24987e433ff5fef" - integrity sha512-PJ6qGFmB6n0iMCQIp+qzx8qVUE5cgacopvEh63t3NJjEQHOaza/JT9zywmmVlaol/eGtCmpvBXx2A03ih1Y+xg== - dependencies: - "@types/ssh2" "^0.5.45" - ssh2 "^0.8.9" - ssh2-streams@~0.4.10: version "0.4.10" resolved "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz#48ef7e8a0e39d8f2921c30521d56dacb31d23a34" @@ -24275,7 +24222,7 @@ ssh2-streams@~0.4.10: bcrypt-pbkdf "^1.0.2" streamsearch "~0.1.2" -ssh2@^0.8.7, ssh2@^0.8.9: +ssh2@^0.8.7: version "0.8.9" resolved "https://registry.npmjs.org/ssh2/-/ssh2-0.8.9.tgz#54da3a6c4ba3daf0d8477a538a481326091815f3" integrity sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw== @@ -24479,13 +24426,6 @@ stream-shift@^1.0.0: resolved "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== -stream-to-array@^2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/stream-to-array/-/stream-to-array-2.3.0.tgz#bbf6b39f5f43ec30bc71babcb37557acecf34353" - integrity sha1-u/azn19D7DC8cbq8s3VXrOzzQ1M= - dependencies: - any-promise "^1.1.0" - stream-transform@^2.0.1: version "2.0.2" resolved "https://registry.npmjs.org/stream-transform/-/stream-transform-2.0.2.tgz#3cb7a14c802eb39bc40caaab0535e584f3a65caf" @@ -25040,7 +24980,7 @@ tapable@^1.0.0, tapable@^1.1.3: resolved "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== -tar-fs@^2.0.0, tar-fs@^2.1.1: +tar-fs@^2.0.0: version "2.1.1" resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== @@ -25071,17 +25011,6 @@ tar-stream@^2.0.0, tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" -tar-stream@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" - integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== - dependencies: - bl "^4.0.3" - end-of-stream "^1.4.1" - fs-constants "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.1.1" - tar@^2.0.0: version "2.2.2" resolved "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" @@ -25239,25 +25168,6 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" -testcontainers@^7.10.0: - version "7.10.0" - resolved "https://registry.npmjs.org/testcontainers/-/testcontainers-7.10.0.tgz#d0306ac9e54c24006e0de0e4505ca7d4a5a0db9e" - integrity sha512-xwz1dzRUmaYTNL4qH8//YlJzSGfVmooGtJNc0ynFm0mtUTyYu4HJLpPZB9e7+/ZiQlWp73LSTH/gSwuNtl8FXw== - dependencies: - "@types/archiver" "^5.1.0" - "@types/dockerode" "^3.2.1" - archiver "^5.2.0" - byline "^5.0.0" - debug "^4.3.1" - docker-compose "^0.23.5" - dockerode "^3.2.1" - get-port "^5.1.1" - glob "^7.1.6" - slash "^3.0.0" - ssh-remote-port-forward "^1.0.3" - stream-to-array "^2.3.0" - tar-fs "^2.1.1" - text-encoding-utf-8@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" @@ -27212,7 +27122,7 @@ yaml-ast-parser@0.0.43, yaml-ast-parser@^0.0.43: resolved "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz#e8a23e6fb4c38076ab92995c5dca33f3d3d7c9bb" integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A== -yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2, yaml@^1.9.2: +yaml@^1.10.0, yaml@^1.7.2, yaml@^1.9.2: version "1.10.2" resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== @@ -27394,15 +27304,6 @@ zip-stream@^4.0.4: compress-commons "^4.0.2" readable-stream "^3.6.0" -zip-stream@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz#51dd326571544e36aa3f756430b313576dc8fc79" - integrity sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A== - dependencies: - archiver-utils "^2.1.0" - compress-commons "^4.1.0" - readable-stream "^3.6.0" - zombie@^6.1.4: version "6.1.4" resolved "https://registry.npmjs.org/zombie/-/zombie-6.1.4.tgz#9f0f53f3d9a032beb7f3fe5b382146a3475a4d47" From aee7212366ade4b8fb6371e2ff3de30706906b36 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 12 May 2021 10:54:54 +0200 Subject: [PATCH 75/95] Updating changelogs Signed-off-by: blam --- packages/create-app/CHANGELOG.md | 3 --- plugins/scaffolder-backend/CHANGELOG.md | 1 - 2 files changed, 4 deletions(-) diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 8c3dc14245..f714e44fa7 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -15,8 +15,6 @@ + import { DockerContainerRunner } from '@backstage/backend-common'; - // ... - export default async function createPlugin({ logger, config, @@ -76,7 +74,6 @@ + SingleHostDiscovery, + } from '@backstage/backend-common'; - export default async function createPlugin({ logger, config, diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index b4aa856606..987109e62f 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -16,7 +16,6 @@ + SingleHostDiscovery, + } from '@backstage/backend-common'; - export default async function createPlugin({ logger, config, From e7e9e93f5730ccbda905d388b382f3a7fe16c1a6 Mon Sep 17 00:00:00 2001 From: Andrew Johnson <57673895+anderoo@users.noreply.github.com> Date: Wed, 12 May 2021 09:41:25 +0100 Subject: [PATCH 76/95] Update GithubDeploymentsCard.tsx Signed-off-by: anderoo --- .../src/components/GithubDeploymentsCard.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx index b7f2e7b1ba..4cb01f04a8 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx @@ -73,10 +73,8 @@ export const GithubDeploymentsCard = ({ columns?: TableColumn[]; }) => { const { entity } = useEntity(); - const host: string | undefined = [ - entity?.metadata.annotations?.[SOURCE_LOCATION_ANNOTATION], - entity?.metadata.annotations?.[LOCATION_ANNOTATION], - ].filter(Boolean)[0]; + const [host] = [entity?.metadata.annotations?.[SOURCE_LOCATION_ANNOTATION], + entity?.metadata.annotations?.[LOCATION_ANNOTATION]].filter(Boolean); return !isGithubDeploymentsAvailable(entity) ? ( From d8e24a7ae30f5a7bf1d9b7aa3b50cb78d490302b Mon Sep 17 00:00:00 2001 From: Andrew Johnson <57673895+anderoo@users.noreply.github.com> Date: Wed, 12 May 2021 09:46:29 +0100 Subject: [PATCH 77/95] Update GithubDeploymentsCard.test.tsx Signed-off-by: anderoo --- .../components/GithubDeploymentsCard.test.tsx | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index 5914e4b422..4af0c0096e 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -336,7 +336,31 @@ describe('github-deployments', () => { }); }); - describe('entity with non url location', () => { + describe('entity with url location type', () => { + beforeEach(() => { + entity = entityStub; + entity.entity.metadata.annotations = { + 'github.com/project-slug': 'org/repo', + 'backstage.io/source-location': + 'url:my-favourite-url-location/org/repo', + 'backstage.io/managed-by-location': + 'url:my-favourite-url-location/org/repo', + }; + }); + + it('displays fetched data using default github api', async () => { + worker.use( + GRAPHQL_GITHUB_API.query('deployments', (_, res, ctx) => + res(ctx.data(responseStub)), + ), + ); + + await assertFetchedData(); + expect.assertions(7); + }); + }); + + describe('entity with other location types', () => { beforeEach(() => { entity = entityStub; entity.entity.metadata.annotations = { From b23260677a7000355bfc849a5c2781554713e947 Mon Sep 17 00:00:00 2001 From: anderoo Date: Wed, 12 May 2021 10:09:56 +0100 Subject: [PATCH 78/95] prettier Signed-off-by: anderoo --- .../src/components/GithubDeploymentsCard.test.tsx | 2 +- .../src/components/GithubDeploymentsCard.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index 4af0c0096e..bec60c47bc 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -359,7 +359,7 @@ describe('github-deployments', () => { expect.assertions(7); }); }); - + describe('entity with other location types', () => { beforeEach(() => { entity = entityStub; diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx index 4cb01f04a8..a13c205084 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx @@ -73,8 +73,10 @@ export const GithubDeploymentsCard = ({ columns?: TableColumn[]; }) => { const { entity } = useEntity(); - const [host] = [entity?.metadata.annotations?.[SOURCE_LOCATION_ANNOTATION], - entity?.metadata.annotations?.[LOCATION_ANNOTATION]].filter(Boolean); + const [host] = [ + entity?.metadata.annotations?.[SOURCE_LOCATION_ANNOTATION], + entity?.metadata.annotations?.[LOCATION_ANNOTATION], + ].filter(Boolean); return !isGithubDeploymentsAvailable(entity) ? ( From df31bdca8f71664190d3ff285dc8c72698866dab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 12 May 2021 09:39:51 +0000 Subject: [PATCH 79/95] Version Packages --- .changeset/afraid-waves-remember.md | 49 --------------- .changeset/beige-cheetahs-jog.md | 5 -- .changeset/breezy-carrots-hunt.md | 5 -- .changeset/calm-insects-melt.md | 7 --- .changeset/clean-starfishes-drop.md | 5 -- .changeset/cool-cache-bro.md | 52 ---------------- .changeset/cyan-beans-marry.md | 5 -- .changeset/forty-pumpkins-agree.md | 5 -- .changeset/grumpy-goats-refuse.md | 5 -- .changeset/khaki-waves-dream.md | 5 -- .changeset/mighty-poets-cheer.md | 5 -- .changeset/ninety-pots-guess.md | 5 -- .changeset/polite-plants-exercise.md | 14 ----- .changeset/shaggy-drinks-hammer.md | 5 -- .changeset/short-bobcats-mate.md | 7 --- .changeset/smart-sheep-itch.md | 5 -- .changeset/smooth-vans-travel.md | 5 -- .changeset/sour-kids-compete.md | 5 -- .changeset/stale-gifts-push.md | 5 -- .changeset/techdocs-spaces-in-objects.md | 5 -- .changeset/ten-paws-ring.md | 44 -------------- .changeset/wild-moles-care.md | 5 -- packages/app/CHANGELOG.md | 52 ++++++++++++++++ packages/app/package.json | 70 +++++++++++----------- packages/backend-common/CHANGELOG.md | 57 ++++++++++++++++++ packages/backend-common/package.json | 6 +- packages/backend/CHANGELOG.md | 31 ++++++++++ packages/backend/package.json | 38 ++++++------ packages/catalog-model/CHANGELOG.md | 7 +++ packages/catalog-model/package.json | 4 +- packages/cli/CHANGELOG.md | 8 +++ packages/cli/package.json | 10 ++-- packages/core-api/CHANGELOG.md | 7 +++ packages/core-api/package.json | 6 +- packages/core/CHANGELOG.md | 13 ++++ packages/core/package.json | 8 +-- packages/create-app/CHANGELOG.md | 47 +++++++++++++++ packages/create-app/package.json | 2 +- packages/dev-utils/CHANGELOG.md | 17 ++++++ packages/dev-utils/package.json | 14 ++--- packages/integration-react/CHANGELOG.md | 13 ++++ packages/integration-react/package.json | 10 ++-- packages/techdocs-common/CHANGELOG.md | 12 ++++ packages/techdocs-common/package.json | 8 +-- packages/test-utils/CHANGELOG.md | 9 +++ packages/test-utils/package.json | 6 +- plugins/api-docs/CHANGELOG.md | 18 ++++++ plugins/api-docs/package.json | 14 ++--- plugins/app-backend/CHANGELOG.md | 8 +++ plugins/app-backend/package.json | 6 +- plugins/auth-backend/CHANGELOG.md | 13 ++++ plugins/auth-backend/package.json | 10 ++-- plugins/badges-backend/CHANGELOG.md | 11 ++++ plugins/badges-backend/package.json | 8 +-- plugins/badges/CHANGELOG.md | 16 +++++ plugins/badges/package.json | 14 ++--- plugins/bitrise/CHANGELOG.md | 16 +++++ plugins/bitrise/package.json | 14 ++--- plugins/catalog-backend/CHANGELOG.md | 24 ++++++++ plugins/catalog-backend/package.json | 10 ++-- plugins/catalog-graphql/CHANGELOG.md | 12 ++++ plugins/catalog-graphql/package.json | 10 ++-- plugins/catalog-import/CHANGELOG.md | 17 ++++++ plugins/catalog-import/package.json | 16 ++--- plugins/catalog-react/CHANGELOG.md | 15 +++++ plugins/catalog-react/package.json | 12 ++-- plugins/catalog/CHANGELOG.md | 18 ++++++ plugins/catalog/package.json | 16 ++--- plugins/circleci/CHANGELOG.md | 16 +++++ plugins/circleci/package.json | 14 ++--- plugins/cloudbuild/CHANGELOG.md | 16 +++++ plugins/cloudbuild/package.json | 14 ++--- plugins/code-coverage-backend/CHANGELOG.md | 11 ++++ plugins/code-coverage-backend/package.json | 8 +-- plugins/code-coverage/CHANGELOG.md | 16 +++++ plugins/code-coverage/package.json | 14 ++--- plugins/config-schema/CHANGELOG.md | 13 ++++ plugins/config-schema/package.json | 10 ++-- plugins/cost-insights/CHANGELOG.md | 12 ++++ plugins/cost-insights/package.json | 10 ++-- plugins/explore-react/CHANGELOG.md | 11 ++++ plugins/explore-react/package.json | 10 ++-- plugins/explore/CHANGELOG.md | 18 ++++++ plugins/explore/package.json | 16 ++--- plugins/fossa/CHANGELOG.md | 16 +++++ plugins/fossa/package.json | 14 ++--- plugins/gcp-projects/CHANGELOG.md | 12 ++++ plugins/gcp-projects/package.json | 10 ++-- plugins/git-release-manager/package.json | 8 +-- plugins/github-actions/CHANGELOG.md | 16 +++++ plugins/github-actions/package.json | 14 ++--- plugins/github-deployments/CHANGELOG.md | 18 ++++++ plugins/github-deployments/package.json | 16 ++--- plugins/gitops-profiles/CHANGELOG.md | 12 ++++ plugins/gitops-profiles/package.json | 10 ++-- plugins/graphiql/CHANGELOG.md | 12 ++++ plugins/graphiql/package.json | 10 ++-- plugins/graphql/CHANGELOG.md | 10 ++++ plugins/graphql/package.json | 8 +-- plugins/jenkins/CHANGELOG.md | 16 +++++ plugins/jenkins/package.json | 14 ++--- plugins/kafka-backend/CHANGELOG.md | 11 ++++ plugins/kafka-backend/package.json | 8 +-- plugins/kafka/CHANGELOG.md | 16 +++++ plugins/kafka/package.json | 14 ++--- plugins/kubernetes-backend/CHANGELOG.md | 12 ++++ plugins/kubernetes-backend/package.json | 8 +-- plugins/kubernetes/CHANGELOG.md | 17 ++++++ plugins/kubernetes/package.json | 14 ++--- plugins/lighthouse/CHANGELOG.md | 16 +++++ plugins/lighthouse/package.json | 14 ++--- plugins/newrelic/CHANGELOG.md | 12 ++++ plugins/newrelic/package.json | 10 ++-- plugins/org/CHANGELOG.md | 18 ++++++ plugins/org/package.json | 16 ++--- plugins/pagerduty/CHANGELOG.md | 16 +++++ plugins/pagerduty/package.json | 14 ++--- plugins/proxy-backend/CHANGELOG.md | 8 +++ plugins/proxy-backend/package.json | 6 +- plugins/register-component/CHANGELOG.md | 16 +++++ plugins/register-component/package.json | 14 ++--- plugins/rollbar-backend/CHANGELOG.md | 8 +++ plugins/rollbar-backend/package.json | 6 +- plugins/rollbar/CHANGELOG.md | 16 +++++ plugins/rollbar/package.json | 14 ++--- plugins/scaffolder-backend/CHANGELOG.md | 14 +++++ plugins/scaffolder-backend/package.json | 10 ++-- plugins/scaffolder/CHANGELOG.md | 18 ++++++ plugins/scaffolder/package.json | 16 ++--- plugins/search-backend-node/package.json | 4 +- plugins/search-backend/CHANGELOG.md | 8 +++ plugins/search-backend/package.json | 6 +- plugins/search/CHANGELOG.md | 16 +++++ plugins/search/package.json | 14 ++--- plugins/sentry/CHANGELOG.md | 16 +++++ plugins/sentry/package.json | 14 ++--- plugins/shortcuts/CHANGELOG.md | 12 ++++ plugins/shortcuts/package.json | 10 ++-- plugins/sonarqube/CHANGELOG.md | 16 +++++ plugins/sonarqube/package.json | 14 ++--- plugins/splunk-on-call/CHANGELOG.md | 16 +++++ plugins/splunk-on-call/package.json | 14 ++--- plugins/tech-radar/CHANGELOG.md | 12 ++++ plugins/tech-radar/package.json | 10 ++-- plugins/techdocs-backend/CHANGELOG.md | 13 ++++ plugins/techdocs-backend/package.json | 10 ++-- plugins/techdocs/CHANGELOG.md | 17 ++++++ plugins/techdocs/package.json | 16 ++--- plugins/todo-backend/CHANGELOG.md | 11 ++++ plugins/todo-backend/package.json | 8 +-- plugins/todo/CHANGELOG.md | 17 ++++++ plugins/todo/package.json | 14 ++--- plugins/user-settings/CHANGELOG.md | 12 ++++ plugins/user-settings/package.json | 10 ++-- plugins/welcome/CHANGELOG.md | 12 ++++ plugins/welcome/package.json | 10 ++-- 156 files changed, 1457 insertions(+), 668 deletions(-) delete mode 100644 .changeset/afraid-waves-remember.md delete mode 100644 .changeset/beige-cheetahs-jog.md delete mode 100644 .changeset/breezy-carrots-hunt.md delete mode 100644 .changeset/calm-insects-melt.md delete mode 100644 .changeset/clean-starfishes-drop.md delete mode 100644 .changeset/cool-cache-bro.md delete mode 100644 .changeset/cyan-beans-marry.md delete mode 100644 .changeset/forty-pumpkins-agree.md delete mode 100644 .changeset/grumpy-goats-refuse.md delete mode 100644 .changeset/khaki-waves-dream.md delete mode 100644 .changeset/mighty-poets-cheer.md delete mode 100644 .changeset/ninety-pots-guess.md delete mode 100644 .changeset/polite-plants-exercise.md delete mode 100644 .changeset/shaggy-drinks-hammer.md delete mode 100644 .changeset/short-bobcats-mate.md delete mode 100644 .changeset/smart-sheep-itch.md delete mode 100644 .changeset/smooth-vans-travel.md delete mode 100644 .changeset/sour-kids-compete.md delete mode 100644 .changeset/stale-gifts-push.md delete mode 100644 .changeset/techdocs-spaces-in-objects.md delete mode 100644 .changeset/ten-paws-ring.md delete mode 100644 .changeset/wild-moles-care.md create mode 100644 packages/integration-react/CHANGELOG.md create mode 100644 plugins/config-schema/CHANGELOG.md create mode 100644 plugins/shortcuts/CHANGELOG.md create mode 100644 plugins/todo/CHANGELOG.md diff --git a/.changeset/afraid-waves-remember.md b/.changeset/afraid-waves-remember.md deleted file mode 100644 index 13df956acc..0000000000 --- a/.changeset/afraid-waves-remember.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -'@backstage/core': patch -'@backstage/core-api': patch -'@backstage/dev-utils': patch -'@backstage/integration-react': patch -'@backstage/test-utils': patch -'@backstage/plugin-api-docs': patch -'@backstage/plugin-badges': patch -'@backstage/plugin-bitrise': patch -'@backstage/plugin-catalog': patch -'@backstage/plugin-catalog-import': patch -'@backstage/plugin-catalog-react': patch -'@backstage/plugin-circleci': patch -'@backstage/plugin-cloudbuild': patch -'@backstage/plugin-code-coverage': patch -'@backstage/plugin-config-schema': patch -'@backstage/plugin-cost-insights': patch -'@backstage/plugin-explore': patch -'@backstage/plugin-explore-react': patch -'@backstage/plugin-fossa': patch -'@backstage/plugin-gcp-projects': patch -'@backstage/plugin-github-actions': patch -'@backstage/plugin-github-deployments': patch -'@backstage/plugin-gitops-profiles': patch -'@backstage/plugin-graphiql': patch -'@backstage/plugin-jenkins': patch -'@backstage/plugin-kafka': patch -'@backstage/plugin-kubernetes': patch -'@backstage/plugin-lighthouse': patch -'@backstage/plugin-newrelic': patch -'@backstage/plugin-org': patch -'@backstage/plugin-pagerduty': patch -'@backstage/plugin-register-component': patch -'@backstage/plugin-rollbar': patch -'@backstage/plugin-scaffolder': patch -'@backstage/plugin-scaffolder-backend': patch -'@backstage/plugin-search': patch -'@backstage/plugin-sentry': patch -'@backstage/plugin-shortcuts': patch -'@backstage/plugin-sonarqube': patch -'@backstage/plugin-splunk-on-call': patch -'@backstage/plugin-tech-radar': patch -'@backstage/plugin-techdocs': patch -'@backstage/plugin-todo': patch -'@backstage/plugin-user-settings': patch -'@backstage/plugin-welcome': patch ---- - -chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 diff --git a/.changeset/beige-cheetahs-jog.md b/.changeset/beige-cheetahs-jog.md deleted file mode 100644 index 87ef3ef44c..0000000000 --- a/.changeset/beige-cheetahs-jog.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Add context for versions:bump on what version it was bumped to. Updated tests for the same. diff --git a/.changeset/breezy-carrots-hunt.md b/.changeset/breezy-carrots-hunt.md deleted file mode 100644 index 0075009a2d..0000000000 --- a/.changeset/breezy-carrots-hunt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-explore': patch ---- - -Add "Organization" tab with a diagram diff --git a/.changeset/calm-insects-melt.md b/.changeset/calm-insects-melt.md deleted file mode 100644 index 4b17764c33..0000000000 --- a/.changeset/calm-insects-melt.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/cli': patch -'@backstage/create-app': patch -'@backstage/plugin-catalog-graphql': patch ---- - -chore: bump `ts-node` versions to 9.1.1 diff --git a/.changeset/clean-starfishes-drop.md b/.changeset/clean-starfishes-drop.md deleted file mode 100644 index 884bb8de9f..0000000000 --- a/.changeset/clean-starfishes-drop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -This makes the CatalogTable configurable with custom columns, passed through the CatalogPage component rendered on the home page. diff --git a/.changeset/cool-cache-bro.md b/.changeset/cool-cache-bro.md deleted file mode 100644 index 9d5f998897..0000000000 --- a/.changeset/cool-cache-bro.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -'@backstage/backend-common': minor ---- - -Introducing: a standard API for App Integrators to configure cache stores and Plugin Developers to interact with them. - -Two cache stores are currently supported. - -- `memory`, which is a very simple in-memory key/value store, intended for local development. -- `memcache`, which can be used to connect to a memcache host. - -Configuring and working with cache stores is very similar to the process for database connections. - -```yaml -backend: - cache: - store: memcache - connection: user:pass@cache.example.com:11211 -``` - -```typescript -import { CacheManager } from '@backstage/backend-common'; - -// Instantiating a cache client for a plugin. -const cacheManager = CacheManager.fromConfig(config); -const somePluginCache = cacheManager.forPlugin('somePlugin'); -const cacheClient = somePluginCache.getClient(); - -// Using the cache client: -const cachedValue = await cacheClient.get('someKey'); -if (cachedValue) { - return cachedValue; -} else { - const someValue = await someExpensiveProcess(); - await cacheClient.set('someKey', someValue); -} -await cacheClient.delete('someKey'); -``` - -Cache clients deal with TTLs in milliseconds. A TTL can be provided as a defaultTtl when getting a client, or may be passed when setting specific objects. If no TTL is provided, data will be persisted indefinitely. - -```typescript -// Getting a client with a default TTL -const cacheClient = somePluginCache.getClient({ - defaultTtl: 3600000, -}); - -// Setting a TTL on a per-object basis. -cacheClient.set('someKey', data, { ttl: 3600000 }); -``` - -Configuring a cache store is optional. Even when no cache store is configured, the cache manager will dutifully pass plugins a manager that resolves a cache client that does not actually write or read any data. diff --git a/.changeset/cyan-beans-marry.md b/.changeset/cyan-beans-marry.md deleted file mode 100644 index 99d7ad47ce..0000000000 --- a/.changeset/cyan-beans-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/catalog-model': patch ---- - -Renamed parameters to input in template schema diff --git a/.changeset/forty-pumpkins-agree.md b/.changeset/forty-pumpkins-agree.md deleted file mode 100644 index 2d8892c862..0000000000 --- a/.changeset/forty-pumpkins-agree.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -The apiBaseUrl setting for Bitbucket Server integrations will now be used when it is set. Otherwise, it will default back to the host setting. diff --git a/.changeset/grumpy-goats-refuse.md b/.changeset/grumpy-goats-refuse.md deleted file mode 100644 index 8dc2e390fc..0000000000 --- a/.changeset/grumpy-goats-refuse.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch ---- - -Show error on task page if task does not exist. diff --git a/.changeset/khaki-waves-dream.md b/.changeset/khaki-waves-dream.md deleted file mode 100644 index 38254c21be..0000000000 --- a/.changeset/khaki-waves-dream.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes': patch ---- - -Export types diff --git a/.changeset/mighty-poets-cheer.md b/.changeset/mighty-poets-cheer.md deleted file mode 100644 index 47538489e4..0000000000 --- a/.changeset/mighty-poets-cheer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Prep work for mysql support in backend-common diff --git a/.changeset/ninety-pots-guess.md b/.changeset/ninety-pots-guess.md deleted file mode 100644 index 4b9a770c29..0000000000 --- a/.changeset/ninety-pots-guess.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -update plugins created to use react-use 17.2.4 diff --git a/.changeset/polite-plants-exercise.md b/.changeset/polite-plants-exercise.md deleted file mode 100644 index 2cfa38697a..0000000000 --- a/.changeset/polite-plants-exercise.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': minor ---- - -Port `GithubOrgReaderProcessor` to support configuration via -[`integrations`](https://backstage.io/docs/integrations/github/locations) in -addition to [`catalog.processors.githubOrg.providers`](https://backstage.io/docs/integrations/github/org#configuration). -The `integrations` package supports authentication with both personal access -tokens and GitHub apps. - -This deprecates the `catalog.processors.githubOrg.providers` configuration. -A [`integrations` configuration](https://backstage.io/docs/integrations/github/locations) -for the same host takes precedence over the provider configuration. -You might need to add additional scopes for the credentials. diff --git a/.changeset/shaggy-drinks-hammer.md b/.changeset/shaggy-drinks-hammer.md deleted file mode 100644 index 67f284ea3f..0000000000 --- a/.changeset/shaggy-drinks-hammer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-org': patch ---- - -Paginate group members to only display 50 members maximum. diff --git a/.changeset/short-bobcats-mate.md b/.changeset/short-bobcats-mate.md deleted file mode 100644 index d63e7058a9..0000000000 --- a/.changeset/short-bobcats-mate.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/core': patch -'@backstage/plugin-api-docs': patch ---- - -Fix state persisted in the URL make search input in the table toolbar lose their -focus. diff --git a/.changeset/smart-sheep-itch.md b/.changeset/smart-sheep-itch.md deleted file mode 100644 index 12a0ff5ad0..0000000000 --- a/.changeset/smart-sheep-itch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/catalog-model': patch ---- - -Improve error messages when schema validation fails diff --git a/.changeset/smooth-vans-travel.md b/.changeset/smooth-vans-travel.md deleted file mode 100644 index 0038c35e8e..0000000000 --- a/.changeset/smooth-vans-travel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-github-deployments': patch ---- - -Support for GitHub Enterprise with GithubDeploymentsPlugin diff --git a/.changeset/sour-kids-compete.md b/.changeset/sour-kids-compete.md deleted file mode 100644 index ce94305c48..0000000000 --- a/.changeset/sour-kids-compete.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Move `jest-when` to the dev dependencies diff --git a/.changeset/stale-gifts-push.md b/.changeset/stale-gifts-push.md deleted file mode 100644 index f1e13ba14d..0000000000 --- a/.changeset/stale-gifts-push.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core': patch ---- - -Add count of older messages when multiple messages exist in AlertDisplay diff --git a/.changeset/techdocs-spaces-in-objects.md b/.changeset/techdocs-spaces-in-objects.md deleted file mode 100644 index 7bbac5d2a8..0000000000 --- a/.changeset/techdocs-spaces-in-objects.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/techdocs-common': patch ---- - -Fixed a bug that prevented loading static assets from GCS, S3, Azure, and OpenStackSwift whose keys contain spaces or other special characters. diff --git a/.changeset/ten-paws-ring.md b/.changeset/ten-paws-ring.md deleted file mode 100644 index 8ed2158f6c..0000000000 --- a/.changeset/ten-paws-ring.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -'@backstage/core': patch -'@backstage/core-api': patch -'@backstage/integration-react': patch -'@backstage/plugin-api-docs': patch -'@backstage/plugin-badges': patch -'@backstage/plugin-bitrise': patch -'@backstage/plugin-catalog': patch -'@backstage/plugin-catalog-import': patch -'@backstage/plugin-catalog-react': patch -'@backstage/plugin-circleci': patch -'@backstage/plugin-cloudbuild': patch -'@backstage/plugin-code-coverage': patch -'@backstage/plugin-config-schema': patch -'@backstage/plugin-cost-insights': patch -'@backstage/plugin-explore': patch -'@backstage/plugin-fossa': patch -'@backstage/plugin-gcp-projects': patch -'@backstage/plugin-github-actions': patch -'@backstage/plugin-github-deployments': patch -'@backstage/plugin-gitops-profiles': patch -'@backstage/plugin-graphiql': patch -'@backstage/plugin-jenkins': patch -'@backstage/plugin-kafka': patch -'@backstage/plugin-kubernetes': patch -'@backstage/plugin-lighthouse': patch -'@backstage/plugin-newrelic': patch -'@backstage/plugin-org': patch -'@backstage/plugin-pagerduty': patch -'@backstage/plugin-register-component': patch -'@backstage/plugin-rollbar': patch -'@backstage/plugin-scaffolder': patch -'@backstage/plugin-search': patch -'@backstage/plugin-sentry': patch -'@backstage/plugin-sonarqube': patch -'@backstage/plugin-splunk-on-call': patch -'@backstage/plugin-tech-radar': patch -'@backstage/plugin-techdocs': patch -'@backstage/plugin-todo': patch -'@backstage/plugin-user-settings': patch -'@backstage/plugin-welcome': patch ---- - -chore: bump `react-use` dependency in all packages diff --git a/.changeset/wild-moles-care.md b/.changeset/wild-moles-care.md deleted file mode 100644 index 35b886c2c1..0000000000 --- a/.changeset/wild-moles-care.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-kubernetes-backend': patch ---- - -Add possibility to configure TLS verification for `gke` type clusters diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 33ef204f63..1cc00e5ff9 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,57 @@ # example-app +## 0.2.28 + +### Patch Changes + +- Updated dependencies [062bbf90f] +- Updated dependencies [2cd70e164] +- Updated dependencies [0b033d07b] +- Updated dependencies [3be844496] +- Updated dependencies [5542de095] +- Updated dependencies [10c008a3a] +- Updated dependencies [81ef1d57b] +- Updated dependencies [ea21d46f0] +- Updated dependencies [e3fc89df6] +- Updated dependencies [f59a945b7] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/integration-react@0.1.2 + - @backstage/plugin-api-docs@0.4.13 + - @backstage/plugin-badges@0.2.1 + - @backstage/plugin-catalog@0.5.7 + - @backstage/plugin-catalog-import@0.5.6 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/plugin-circleci@0.2.14 + - @backstage/plugin-cloudbuild@0.2.14 + - @backstage/plugin-code-coverage@0.1.3 + - @backstage/plugin-cost-insights@0.9.1 + - @backstage/plugin-explore@0.3.5 + - @backstage/plugin-gcp-projects@0.2.6 + - @backstage/plugin-github-actions@0.4.6 + - @backstage/plugin-graphiql@0.2.11 + - @backstage/plugin-jenkins@0.4.3 + - @backstage/plugin-kafka@0.2.7 + - @backstage/plugin-kubernetes@0.4.4 + - @backstage/plugin-lighthouse@0.2.16 + - @backstage/plugin-newrelic@0.2.7 + - @backstage/plugin-org@0.3.13 + - @backstage/plugin-pagerduty@0.3.4 + - @backstage/plugin-rollbar@0.3.5 + - @backstage/plugin-scaffolder@0.9.4 + - @backstage/plugin-search@0.3.6 + - @backstage/plugin-sentry@0.3.10 + - @backstage/plugin-shortcuts@0.1.2 + - @backstage/plugin-tech-radar@0.3.11 + - @backstage/plugin-techdocs@0.9.2 + - @backstage/plugin-todo@0.1.1 + - @backstage/plugin-user-settings@0.2.10 + - @backstage/cli@0.6.11 + - @backstage/catalog-model@0.7.9 + ## 0.2.27 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 714d420744..6acba6f4cc 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,42 +1,42 @@ { "name": "example-app", - "version": "0.2.27", + "version": "0.2.28", "private": true, "bundled": true, "dependencies": { - "@backstage/catalog-model": "^0.7.8", - "@backstage/cli": "^0.6.10", - "@backstage/core": "^0.7.8", - "@backstage/integration-react": "^0.1.1", - "@backstage/plugin-api-docs": "^0.4.12", - "@backstage/plugin-badges": "^0.2.0", - "@backstage/plugin-catalog": "^0.5.6", - "@backstage/plugin-catalog-import": "^0.5.5", - "@backstage/plugin-catalog-react": "^0.1.5", - "@backstage/plugin-circleci": "^0.2.13", - "@backstage/plugin-cloudbuild": "^0.2.13", - "@backstage/plugin-code-coverage": "^0.1.2", - "@backstage/plugin-cost-insights": "^0.9.0", - "@backstage/plugin-explore": "^0.3.4", - "@backstage/plugin-gcp-projects": "^0.2.5", - "@backstage/plugin-github-actions": "^0.4.5", - "@backstage/plugin-graphiql": "^0.2.10", - "@backstage/plugin-jenkins": "^0.4.2", - "@backstage/plugin-kafka": "^0.2.6", - "@backstage/plugin-kubernetes": "^0.4.3", - "@backstage/plugin-lighthouse": "^0.2.15", - "@backstage/plugin-newrelic": "^0.2.6", - "@backstage/plugin-org": "^0.3.12", - "@backstage/plugin-pagerduty": "0.3.3", - "@backstage/plugin-rollbar": "^0.3.4", - "@backstage/plugin-scaffolder": "^0.9.3", - "@backstage/plugin-search": "^0.3.5", - "@backstage/plugin-sentry": "^0.3.9", - "@backstage/plugin-shortcuts": "^0.1.1", - "@backstage/plugin-tech-radar": "^0.3.10", - "@backstage/plugin-techdocs": "^0.9.1", - "@backstage/plugin-todo": "^0.1.0", - "@backstage/plugin-user-settings": "^0.2.8", + "@backstage/catalog-model": "^0.7.9", + "@backstage/cli": "^0.6.11", + "@backstage/core": "^0.7.9", + "@backstage/integration-react": "^0.1.2", + "@backstage/plugin-api-docs": "^0.4.13", + "@backstage/plugin-badges": "^0.2.1", + "@backstage/plugin-catalog": "^0.5.7", + "@backstage/plugin-catalog-import": "^0.5.6", + "@backstage/plugin-catalog-react": "^0.1.6", + "@backstage/plugin-circleci": "^0.2.14", + "@backstage/plugin-cloudbuild": "^0.2.14", + "@backstage/plugin-code-coverage": "^0.1.3", + "@backstage/plugin-cost-insights": "^0.9.1", + "@backstage/plugin-explore": "^0.3.5", + "@backstage/plugin-gcp-projects": "^0.2.6", + "@backstage/plugin-github-actions": "^0.4.6", + "@backstage/plugin-graphiql": "^0.2.11", + "@backstage/plugin-jenkins": "^0.4.3", + "@backstage/plugin-kafka": "^0.2.7", + "@backstage/plugin-kubernetes": "^0.4.4", + "@backstage/plugin-lighthouse": "^0.2.16", + "@backstage/plugin-newrelic": "^0.2.7", + "@backstage/plugin-org": "^0.3.13", + "@backstage/plugin-pagerduty": "0.3.4", + "@backstage/plugin-rollbar": "^0.3.5", + "@backstage/plugin-scaffolder": "^0.9.4", + "@backstage/plugin-search": "^0.3.6", + "@backstage/plugin-sentry": "^0.3.10", + "@backstage/plugin-shortcuts": "^0.1.2", + "@backstage/plugin-tech-radar": "^0.3.11", + "@backstage/plugin-techdocs": "^0.9.2", + "@backstage/plugin-todo": "^0.1.1", + "@backstage/plugin-user-settings": "^0.2.10", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -56,7 +56,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/test-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.11", "@testing-library/cypress": "^7.0.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 3568adf099..9c66f387d9 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,62 @@ # @backstage/backend-common +## 0.8.0 + +### Minor Changes + +- 22fd8ce2a: Introducing: a standard API for App Integrators to configure cache stores and Plugin Developers to interact with them. + + Two cache stores are currently supported. + + - `memory`, which is a very simple in-memory key/value store, intended for local development. + - `memcache`, which can be used to connect to a memcache host. + + Configuring and working with cache stores is very similar to the process for database connections. + + ```yaml + backend: + cache: + store: memcache + connection: user:pass@cache.example.com:11211 + ``` + + ```typescript + import { CacheManager } from '@backstage/backend-common'; + + // Instantiating a cache client for a plugin. + const cacheManager = CacheManager.fromConfig(config); + const somePluginCache = cacheManager.forPlugin('somePlugin'); + const cacheClient = somePluginCache.getClient(); + + // Using the cache client: + const cachedValue = await cacheClient.get('someKey'); + if (cachedValue) { + return cachedValue; + } else { + const someValue = await someExpensiveProcess(); + await cacheClient.set('someKey', someValue); + } + await cacheClient.delete('someKey'); + ``` + + Cache clients deal with TTLs in milliseconds. A TTL can be provided as a defaultTtl when getting a client, or may be passed when setting specific objects. If no TTL is provided, data will be persisted indefinitely. + + ```typescript + // Getting a client with a default TTL + const cacheClient = somePluginCache.getClient({ + defaultTtl: 3600000, + }); + + // Setting a TTL on a per-object basis. + cacheClient.set('someKey', data, { ttl: 3600000 }); + ``` + + Configuring a cache store is optional. Even when no cache store is configured, the cache manager will dutifully pass plugins a manager that resolves a cache client that does not actually write or read any data. + +### Patch Changes + +- f9fb4a205: Prep work for mysql support in backend-common + ## 0.7.0 ### Minor Changes diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 46caa82e1d..7e647e57d0 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.7.0", + "version": "0.8.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -76,8 +76,8 @@ } }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/test-utils": "^0.1.11", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", "@types/concat-stream": "^1.6.0", diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index ac65632142..977588b555 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,36 @@ # example-backend +## 0.2.28 + +### Patch Changes + +- Updated dependencies [062bbf90f] +- Updated dependencies [22fd8ce2a] +- Updated dependencies [10c008a3a] +- Updated dependencies [82ca1ac22] +- Updated dependencies [f9fb4a205] +- Updated dependencies [9a207f052] +- Updated dependencies [16be1d093] +- Updated dependencies [fd39d4662] +- Updated dependencies [f9f9d633d] + - @backstage/plugin-scaffolder-backend@0.11.1 + - @backstage/backend-common@0.8.0 + - @backstage/catalog-model@0.7.9 + - @backstage/plugin-catalog-backend@0.9.0 + - @backstage/plugin-kubernetes-backend@0.3.7 + - example-app@0.2.28 + - @backstage/plugin-app-backend@0.3.13 + - @backstage/plugin-auth-backend@0.3.10 + - @backstage/plugin-badges-backend@0.1.4 + - @backstage/plugin-code-coverage-backend@0.1.5 + - @backstage/plugin-graphql-backend@0.1.8 + - @backstage/plugin-kafka-backend@0.2.5 + - @backstage/plugin-proxy-backend@0.2.8 + - @backstage/plugin-rollbar-backend@0.1.11 + - @backstage/plugin-search-backend@0.1.5 + - @backstage/plugin-techdocs-backend@0.8.1 + - @backstage/plugin-todo-backend@0.1.5 + ## 0.2.27 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 6f70db7b19..4044a6632f 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.27", + "version": "0.2.28", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -27,30 +27,30 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.7.0", + "@backstage/backend-common": "^0.8.0", "@backstage/catalog-client": "^0.3.11", - "@backstage/catalog-model": "^0.7.8", + "@backstage/catalog-model": "^0.7.9", "@backstage/config": "^0.1.5", - "@backstage/plugin-app-backend": "^0.3.12", - "@backstage/plugin-auth-backend": "^0.3.9", - "@backstage/plugin-badges-backend": "^0.1.3", - "@backstage/plugin-catalog-backend": "^0.8.2", - "@backstage/plugin-code-coverage-backend": "^0.1.4", - "@backstage/plugin-graphql-backend": "^0.1.7", - "@backstage/plugin-kubernetes-backend": "^0.3.6", - "@backstage/plugin-kafka-backend": "^0.2.4", - "@backstage/plugin-proxy-backend": "^0.2.7", - "@backstage/plugin-rollbar-backend": "^0.1.10", - "@backstage/plugin-scaffolder-backend": "^0.11.0", - "@backstage/plugin-search-backend": "^0.1.4", + "@backstage/plugin-app-backend": "^0.3.13", + "@backstage/plugin-auth-backend": "^0.3.10", + "@backstage/plugin-badges-backend": "^0.1.4", + "@backstage/plugin-catalog-backend": "^0.9.0", + "@backstage/plugin-code-coverage-backend": "^0.1.5", + "@backstage/plugin-graphql-backend": "^0.1.8", + "@backstage/plugin-kubernetes-backend": "^0.3.7", + "@backstage/plugin-kafka-backend": "^0.2.5", + "@backstage/plugin-proxy-backend": "^0.2.8", + "@backstage/plugin-rollbar-backend": "^0.1.11", + "@backstage/plugin-scaffolder-backend": "^0.11.1", + "@backstage/plugin-search-backend": "^0.1.5", "@backstage/plugin-search-backend-node": "^0.1.3", - "@backstage/plugin-techdocs-backend": "^0.8.0", - "@backstage/plugin-todo-backend": "^0.1.4", + "@backstage/plugin-techdocs-backend": "^0.8.1", + "@backstage/plugin-todo-backend": "^0.1.5", "@gitbeaker/node": "^28.0.2", "@octokit/rest": "^18.5.3", "azure-devops-node-api": "^10.1.1", "dockerode": "^3.2.1", - "example-app": "^0.2.27", + "example-app": "^0.2.28", "express": "^4.17.1", "express-promise-router": "^4.1.0", "knex": "^0.95.1", @@ -60,7 +60,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.6.10", + "@backstage/cli": "^0.6.11", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5" diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index e289c5a0b3..312a329649 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/catalog-model +## 0.7.9 + +### Patch Changes + +- 10c008a3a: Renamed parameters to input in template schema +- 16be1d093: Improve error messages when schema validation fails + ## 0.7.8 ### Patch Changes diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index ac3b046a32..6b5b096cb6 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-model", - "version": "0.7.8", + "version": "0.7.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -39,7 +39,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.6.10", + "@backstage/cli": "^0.6.11", "@types/express": "^4.17.6", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 996513f60e..cf28960f13 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/cli +## 0.6.11 + +### Patch Changes + +- 2cd70e164: Add context for versions:bump on what version it was bumped to. Updated tests for the same. +- 3be844496: chore: bump `ts-node` versions to 9.1.1 +- e3fc89df6: update plugins created to use react-use 17.2.4 + ## 0.6.10 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index cd0385196d..b17bc8cbda 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.6.10", + "version": "0.6.11", "private": false, "publishConfig": { "access": "public" @@ -117,11 +117,11 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.7.0", + "@backstage/backend-common": "^0.8.0", "@backstage/config": "^0.1.5", - "@backstage/core": "^0.7.8", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/core": "^0.7.9", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@backstage/theme": "^0.2.7", "@types/diff": "^5.0.0", "@types/express": "^4.17.6", diff --git a/packages/core-api/CHANGELOG.md b/packages/core-api/CHANGELOG.md index fab82a77e5..3652cf6bc0 100644 --- a/packages/core-api/CHANGELOG.md +++ b/packages/core-api/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/core-api +## 0.2.18 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages + ## 0.2.17 ### Patch Changes diff --git a/packages/core-api/package.json b/packages/core-api/package.json index f9fd3d29a6..b5cd8cba69 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-api", "description": "Internal Core API used by Backstage plugins and apps", - "version": "0.2.17", + "version": "0.2.18", "private": false, "publishConfig": { "access": "public", @@ -42,8 +42,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.6.9", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/test-utils": "^0.1.11", "@backstage/test-utils-core": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index a215c7cb8e..51c6b3f935 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/core +## 0.7.9 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 889d89b6e: Fix state persisted in the URL make search input in the table toolbar lose their + focus. +- 3f988cb63: Add count of older messages when multiple messages exist in AlertDisplay +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [675a569a9] + - @backstage/core-api@0.2.18 + ## 0.7.8 ### Patch Changes diff --git a/packages/core/package.json b/packages/core/package.json index 16928d63ca..e418a969ce 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core", "description": "Core API used by Backstage plugins and apps", - "version": "0.7.8", + "version": "0.7.9", "private": false, "publishConfig": { "access": "public", @@ -30,7 +30,7 @@ }, "dependencies": { "@backstage/config": "^0.1.5", - "@backstage/core-api": "^0.2.17", + "@backstage/core-api": "^0.2.18", "@backstage/errors": "^0.1.1", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", @@ -71,8 +71,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index f714e44fa7..5d4f176246 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/create-app +## 1.0.0 + +### Patch Changes + +- 3be844496: chore: bump `ts-node` versions to 9.1.1 +- Updated dependencies [062bbf90f] +- Updated dependencies [2cd70e164] +- Updated dependencies [0b033d07b] +- Updated dependencies [3be844496] +- Updated dependencies [5542de095] +- Updated dependencies [22fd8ce2a] +- Updated dependencies [10c008a3a] +- Updated dependencies [82ca1ac22] +- Updated dependencies [81ef1d57b] +- Updated dependencies [f9fb4a205] +- Updated dependencies [e3fc89df6] +- Updated dependencies [9a207f052] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [fd39d4662] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/integration-react@0.1.2 + - @backstage/test-utils@0.1.11 + - @backstage/plugin-api-docs@0.4.13 + - @backstage/plugin-catalog@0.5.7 + - @backstage/plugin-catalog-import@0.5.6 + - @backstage/plugin-explore@0.3.5 + - @backstage/plugin-github-actions@0.4.6 + - @backstage/plugin-lighthouse@0.2.16 + - @backstage/plugin-scaffolder@0.9.4 + - @backstage/plugin-scaffolder-backend@0.11.1 + - @backstage/plugin-search@0.3.6 + - @backstage/plugin-tech-radar@0.3.11 + - @backstage/plugin-techdocs@0.9.2 + - @backstage/plugin-user-settings@0.2.10 + - @backstage/cli@0.6.11 + - @backstage/backend-common@0.8.0 + - @backstage/catalog-model@0.7.9 + - @backstage/plugin-catalog-backend@0.9.0 + - @backstage/plugin-app-backend@0.3.13 + - @backstage/plugin-auth-backend@0.3.10 + - @backstage/plugin-proxy-backend@0.2.8 + - @backstage/plugin-rollbar-backend@0.1.11 + - @backstage/plugin-techdocs-backend@0.8.1 + ## 0.3.21 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index b96b4e2065..d7dc99218e 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "Create app package for Backstage", - "version": "0.3.21", + "version": "1.0.0", "private": false, "publishConfig": { "access": "public" diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 3e8b8a96a0..ac88847489 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/dev-utils +## 0.1.14 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/integration-react@0.1.2 + - @backstage/test-utils@0.1.11 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.1.13 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 421cd0a2b8..fb1f8a26dc 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "0.1.13", + "version": "0.1.14", "private": false, "publishConfig": { "access": "public", @@ -29,11 +29,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.7.0", - "@backstage/catalog-model": "^0.7.3", - "@backstage/integration-react": "^0.1.1", - "@backstage/plugin-catalog-react": "^0.1.1", - "@backstage/test-utils": "^0.1.8", + "@backstage/core": "^0.7.9", + "@backstage/catalog-model": "^0.7.9", + "@backstage/integration-react": "^0.1.2", + "@backstage/plugin-catalog-react": "^0.1.6", + "@backstage/test-utils": "^0.1.11", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -48,7 +48,7 @@ "react-router-dom": "6.0.0-beta.0" }, "devDependencies": { - "@backstage/cli": "^0.6.3", + "@backstage/cli": "^0.6.11", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md new file mode 100644 index 0000000000..54aa3b3ab1 --- /dev/null +++ b/packages/integration-react/CHANGELOG.md @@ -0,0 +1,13 @@ +# @backstage/integration-react + +## 0.1.2 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [889d89b6e] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index 8df1eef096..fe831ca24f 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration-react", - "version": "0.1.1", + "version": "0.1.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,7 +21,7 @@ }, "dependencies": { "@backstage/config": "^0.1.2", - "@backstage/core": "^0.7.0", + "@backstage/core": "^0.7.9", "@backstage/integration": "^0.5.0", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", @@ -32,9 +32,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.3", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.8", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/packages/techdocs-common/CHANGELOG.md b/packages/techdocs-common/CHANGELOG.md index 644a3ebb5f..ec47f22475 100644 --- a/packages/techdocs-common/CHANGELOG.md +++ b/packages/techdocs-common/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/techdocs-common +## 0.6.1 + +### Patch Changes + +- e04f1ccfb: Fixed a bug that prevented loading static assets from GCS, S3, Azure, and OpenStackSwift whose keys contain spaces or other special characters. +- Updated dependencies [22fd8ce2a] +- Updated dependencies [10c008a3a] +- Updated dependencies [f9fb4a205] +- Updated dependencies [16be1d093] + - @backstage/backend-common@0.8.0 + - @backstage/catalog-model@0.7.9 + ## 0.6.0 ### Minor Changes diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 7c12f6f675..e6debbb817 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/techdocs-common", "description": "Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", - "version": "0.6.0", + "version": "0.6.1", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -38,8 +38,8 @@ "dependencies": { "@azure/identity": "^1.2.2", "@azure/storage-blob": "^12.4.0", - "@backstage/backend-common": "^0.7.0", - "@backstage/catalog-model": "^0.7.8", + "@backstage/backend-common": "^0.8.0", + "@backstage/catalog-model": "^0.7.9", "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.2", @@ -60,7 +60,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.6.10", + "@backstage/cli": "^0.6.11", "@types/fs-extra": "^9.0.5", "@types/git-url-parse": "^9.0.0", "@types/js-yaml": "^4.0.0", diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index d2c2748449..9a77665e28 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/test-utils +## 0.1.11 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- Updated dependencies [062bbf90f] +- Updated dependencies [675a569a9] + - @backstage/core-api@0.2.18 + ## 0.1.10 ### Patch Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 74716f7a16..7b7b73a64a 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "0.1.10", + "version": "0.1.11", "private": false, "publishConfig": { "access": "public", @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-api": "^0.2.16", + "@backstage/core-api": "^0.2.18", "@backstage/test-utils-core": "^0.1.1", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", @@ -45,7 +45,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.6.7", + "@backstage/cli": "^0.6.11", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index f268ba1c4d..94fd3fe74a 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-api-docs +## 0.4.13 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 889d89b6e: Fix state persisted in the URL make search input in the table toolbar lose their + focus. +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.4.12 ### Patch Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index c94680de9b..9610ae4e10 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.4.12", + "version": "0.4.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,9 +30,9 @@ }, "dependencies": { "@asyncapi/react-component": "^0.23.0", - "@backstage/catalog-model": "^0.7.7", - "@backstage/core": "^0.7.8", - "@backstage/plugin-catalog-react": "^0.1.4", + "@backstage/catalog-model": "^0.7.9", + "@backstage/core": "^0.7.9", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/theme": "^0.2.7", "@material-icons/font": "^1.0.2", "@material-ui/core": "^4.11.0", @@ -49,9 +49,9 @@ "swagger-ui-react": "^3.37.2" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index 73fa9fc3b1..ede16b6a5e 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-app-backend +## 0.3.13 + +### Patch Changes + +- Updated dependencies [22fd8ce2a] +- Updated dependencies [f9fb4a205] + - @backstage/backend-common@0.8.0 + ## 0.3.12 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 84fe743550..60fa678dd0 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-backend", - "version": "0.3.12", + "version": "0.3.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.7.0", + "@backstage/backend-common": "^0.8.0", "@backstage/config-loader": "^0.6.1", "@backstage/config": "^0.1.5", "@types/express": "^4.17.6", @@ -40,7 +40,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.6.10", + "@backstage/cli": "^0.6.11", "@types/supertest": "^2.0.8", "msw": "^0.20.5", "supertest": "^6.1.3" diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index e51942f742..1003140dec 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-auth-backend +## 0.3.10 + +### Patch Changes + +- Updated dependencies [062bbf90f] +- Updated dependencies [22fd8ce2a] +- Updated dependencies [10c008a3a] +- Updated dependencies [f9fb4a205] +- Updated dependencies [16be1d093] + - @backstage/test-utils@0.1.11 + - @backstage/backend-common@0.8.0 + - @backstage/catalog-model@0.7.9 + ## 0.3.9 ### Patch Changes diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 41a47c7788..1b0ad25a84 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.3.9", + "version": "0.3.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,12 +29,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.7.0", + "@backstage/backend-common": "^0.8.0", "@backstage/catalog-client": "^0.3.11", - "@backstage/catalog-model": "^0.7.8", + "@backstage/catalog-model": "^0.7.9", "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", - "@backstage/test-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.11", "@types/express": "^4.17.6", "@types/passport": "^1.0.3", "compression": "^1.7.4", @@ -68,7 +68,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.6.10", + "@backstage/cli": "^0.6.11", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", diff --git a/plugins/badges-backend/CHANGELOG.md b/plugins/badges-backend/CHANGELOG.md index abbc4e70bd..d171d0d590 100644 --- a/plugins/badges-backend/CHANGELOG.md +++ b/plugins/badges-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-badges-backend +## 0.1.4 + +### Patch Changes + +- Updated dependencies [22fd8ce2a] +- Updated dependencies [10c008a3a] +- Updated dependencies [f9fb4a205] +- Updated dependencies [16be1d093] + - @backstage/backend-common@0.8.0 + - @backstage/catalog-model@0.7.9 + ## 0.1.3 ### Patch Changes diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json index 347f8b9219..d7e0df88b5 100644 --- a/plugins/badges-backend/package.json +++ b/plugins/badges-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-badges-backend", - "version": "0.1.3", + "version": "0.1.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,9 +30,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.7.0", + "@backstage/backend-common": "^0.8.0", "@backstage/catalog-client": "^0.3.11", - "@backstage/catalog-model": "^0.7.8", + "@backstage/catalog-model": "^0.7.9", "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", "@types/express": "^4.17.6", @@ -45,7 +45,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.6.10", + "@backstage/cli": "^0.6.11", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/badges/CHANGELOG.md b/plugins/badges/CHANGELOG.md index a0efd43ccc..c5ac19b4ed 100644 --- a/plugins/badges/CHANGELOG.md +++ b/plugins/badges/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-badges +## 0.2.1 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.2.0 ### Minor Changes diff --git a/plugins/badges/package.json b/plugins/badges/package.json index e757567401..72aa2df787 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-badges", - "version": "0.2.0", + "version": "0.2.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,10 +20,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.6", - "@backstage/core": "^0.7.8", + "@backstage/catalog-model": "^0.7.9", + "@backstage/core": "^0.7.9", "@backstage/errors": "^0.1.1", - "@backstage/plugin-catalog-react": "^0.1.3", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -34,9 +34,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/bitrise/CHANGELOG.md b/plugins/bitrise/CHANGELOG.md index ecb26efd50..61a0af36ed 100644 --- a/plugins/bitrise/CHANGELOG.md +++ b/plugins/bitrise/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-bitrise +## 0.1.3 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.1.2 ### Patch Changes diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index f37d08f725..1414f729fd 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bitrise", - "version": "0.1.2", + "version": "0.1.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.2", - "@backstage/core": "^0.7.8", - "@backstage/plugin-catalog-react": "^0.1.2", + "@backstage/catalog-model": "^0.7.9", + "@backstage/core": "^0.7.9", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -37,9 +37,9 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 8d40d7c4bb..0018d83399 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-catalog-backend +## 0.9.0 + +### Minor Changes + +- 9a207f052: Port `GithubOrgReaderProcessor` to support configuration via + [`integrations`](https://backstage.io/docs/integrations/github/locations) in + addition to [`catalog.processors.githubOrg.providers`](https://backstage.io/docs/integrations/github/org#configuration). + The `integrations` package supports authentication with both personal access + tokens and GitHub apps. + + This deprecates the `catalog.processors.githubOrg.providers` configuration. + A [`integrations` configuration](https://backstage.io/docs/integrations/github/locations) + for the same host takes precedence over the provider configuration. + You might need to add additional scopes for the credentials. + +### Patch Changes + +- Updated dependencies [22fd8ce2a] +- Updated dependencies [10c008a3a] +- Updated dependencies [f9fb4a205] +- Updated dependencies [16be1d093] + - @backstage/backend-common@0.8.0 + - @backstage/catalog-model@0.7.9 + ## 0.8.2 ### Patch Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index c8e17bb943..dc9397de82 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "0.8.2", + "version": "0.9.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,8 +30,8 @@ }, "dependencies": { "@azure/msal-node": "^1.0.0-beta.3", - "@backstage/backend-common": "^0.7.0", - "@backstage/catalog-model": "^0.7.8", + "@backstage/backend-common": "^0.8.0", + "@backstage/catalog-model": "^0.7.9", "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.2", @@ -63,8 +63,8 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/test-utils": "^0.1.9", + "@backstage/cli": "^0.6.11", + "@backstage/test-utils": "^0.1.11", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", "@types/lodash": "^4.14.151", diff --git a/plugins/catalog-graphql/CHANGELOG.md b/plugins/catalog-graphql/CHANGELOG.md index 8140724840..c04cc8e3d2 100644 --- a/plugins/catalog-graphql/CHANGELOG.md +++ b/plugins/catalog-graphql/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog-graphql +## 0.2.9 + +### Patch Changes + +- 3be844496: chore: bump `ts-node` versions to 9.1.1 +- Updated dependencies [22fd8ce2a] +- Updated dependencies [10c008a3a] +- Updated dependencies [f9fb4a205] +- Updated dependencies [16be1d093] + - @backstage/backend-common@0.8.0 + - @backstage/catalog-model@0.7.9 + ## 0.2.8 ### Patch Changes diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index 983604ba03..5e0b8e49c2 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graphql", - "version": "0.2.8", + "version": "0.2.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,8 +30,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.7.0", - "@backstage/catalog-model": "^0.7.8", + "@backstage/backend-common": "^0.8.0", + "@backstage/catalog-model": "^0.7.9", "@backstage/config": "^0.1.5", "@graphql-modules/core": "^0.7.17", "apollo-server": "^2.16.1", @@ -42,8 +42,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/test-utils": "^0.1.9", + "@backstage/cli": "^0.6.11", + "@backstage/test-utils": "^0.1.11", "@graphql-codegen/cli": "^1.21.3", "@graphql-codegen/typescript": "^1.17.7", "@graphql-codegen/typescript-resolvers": "^1.17.7", diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 59baabfbc8..e41b135014 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-catalog-import +## 0.5.6 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/integration-react@0.1.2 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.5.5 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 0558be18ec..196b3d6e83 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-import", - "version": "0.5.5", + "version": "0.5.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,12 +30,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.8", + "@backstage/catalog-model": "^0.7.9", "@backstage/catalog-client": "^0.3.11", - "@backstage/core": "^0.7.8", + "@backstage/core": "^0.7.9", "@backstage/integration": "^0.5.2", - "@backstage/integration-react": "^0.1.1", - "@backstage/plugin-catalog-react": "^0.1.5", + "@backstage/integration-react": "^0.1.2", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -53,9 +53,9 @@ "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.3.0", diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index e5a857eca0..c50b72d059 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-catalog-react +## 0.1.6 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/catalog-model@0.7.9 + ## 0.1.5 ### Patch Changes diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 491df00c08..abce5a1996 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-react", - "version": "0.1.5", + "version": "0.1.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,8 +29,8 @@ }, "dependencies": { "@backstage/catalog-client": "^0.3.11", - "@backstage/catalog-model": "^0.7.8", - "@backstage/core": "^0.7.8", + "@backstage/catalog-model": "^0.7.9", + "@backstage/core": "^0.7.9", "@material-ui/core": "^4.11.0", "@types/react": "^16.9", "lodash": "^4.17.15", @@ -40,9 +40,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.9", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.3.0", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index d6254a347a..160440c69f 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog +## 0.5.7 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 5542de095: This makes the CatalogTable configurable with custom columns, passed through the CatalogPage component rendered on the home page. +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/integration-react@0.1.2 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.5.6 ### Patch Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 140c42cfdb..6354f00c5d 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "0.5.6", + "version": "0.5.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,12 +31,12 @@ }, "dependencies": { "@backstage/catalog-client": "^0.3.10", - "@backstage/catalog-model": "^0.7.7", - "@backstage/core": "^0.7.8", + "@backstage/catalog-model": "^0.7.9", + "@backstage/core": "^0.7.9", "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.1", - "@backstage/integration-react": "^0.1.1", - "@backstage/plugin-catalog-react": "^0.1.4", + "@backstage/integration-react": "^0.1.2", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -52,9 +52,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@microsoft/microsoft-graph-types": "^1.25.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md index 8c6d6e094d..d1347fde2a 100644 --- a/plugins/circleci/CHANGELOG.md +++ b/plugins/circleci/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-circleci +## 0.2.14 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.2.13 ### Patch Changes diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 4503f59636..e9b18c43d0 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-circleci", - "version": "0.2.13", + "version": "0.2.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,9 +31,9 @@ "postpack": "backstage-cli postpack" }, "dependencies": { - "@backstage/catalog-model": "^0.7.3", - "@backstage/core": "^0.7.8", - "@backstage/plugin-catalog-react": "^0.1.2", + "@backstage/catalog-model": "^0.7.9", + "@backstage/core": "^0.7.9", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -50,9 +50,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/cloudbuild/CHANGELOG.md b/plugins/cloudbuild/CHANGELOG.md index dea818c97d..a7540f2e72 100644 --- a/plugins/cloudbuild/CHANGELOG.md +++ b/plugins/cloudbuild/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-cloudbuild +## 0.2.14 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.2.13 ### Patch Changes diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 59fbda33f6..510f417bc2 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cloudbuild", - "version": "0.2.13", + "version": "0.2.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,9 +30,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.3", - "@backstage/plugin-catalog-react": "^0.1.2", - "@backstage/core": "^0.7.8", + "@backstage/catalog-model": "^0.7.9", + "@backstage/plugin-catalog-react": "^0.1.6", + "@backstage/core": "^0.7.9", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -47,9 +47,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/code-coverage-backend/CHANGELOG.md b/plugins/code-coverage-backend/CHANGELOG.md index 18e78c7454..3322186cbe 100644 --- a/plugins/code-coverage-backend/CHANGELOG.md +++ b/plugins/code-coverage-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-code-coverage-backend +## 0.1.5 + +### Patch Changes + +- Updated dependencies [22fd8ce2a] +- Updated dependencies [10c008a3a] +- Updated dependencies [f9fb4a205] +- Updated dependencies [16be1d093] + - @backstage/backend-common@0.8.0 + - @backstage/catalog-model@0.7.9 + ## 0.1.4 ### Patch Changes diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json index db53096932..5b73fe45f8 100644 --- a/plugins/code-coverage-backend/package.json +++ b/plugins/code-coverage-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-code-coverage-backend", - "version": "0.1.4", + "version": "0.1.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,9 +19,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.7.0", + "@backstage/backend-common": "^0.8.0", "@backstage/catalog-client": "^0.3.11", - "@backstage/catalog-model": "^0.7.8", + "@backstage/catalog-model": "^0.7.9", "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.2", @@ -36,7 +36,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.6.10", + "@backstage/cli": "^0.6.11", "@types/express-xml-bodyparser": "^0.3.2", "@types/supertest": "^2.0.8", "msw": "^0.21.2", diff --git a/plugins/code-coverage/CHANGELOG.md b/plugins/code-coverage/CHANGELOG.md index 4a55b7f0f5..6d1ea253a0 100644 --- a/plugins/code-coverage/CHANGELOG.md +++ b/plugins/code-coverage/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-code-coverage +## 0.1.3 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.1.2 ### Patch Changes diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index 837c3d4794..f90985145f 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-code-coverage", - "version": "0.1.2", + "version": "0.1.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,11 +20,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.7", + "@backstage/catalog-model": "^0.7.9", "@backstage/config": "^0.1.4", - "@backstage/core": "^0.7.8", + "@backstage/core": "^0.7.9", "@backstage/errors": "^0.1.1", - "@backstage/plugin-catalog-react": "^0.1.4", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -39,9 +39,9 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md new file mode 100644 index 0000000000..e54fc1a802 --- /dev/null +++ b/plugins/config-schema/CHANGELOG.md @@ -0,0 +1,13 @@ +# @backstage/plugin-config-schema + +## 0.1.2 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [889d89b6e] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index 43c6e67f1a..c878f418d0 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-config-schema", - "version": "0.1.1", + "version": "0.1.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,7 +21,7 @@ }, "dependencies": { "@backstage/config": "^0.1.4", - "@backstage/core": "^0.7.8", + "@backstage/core": "^0.7.9", "@backstage/errors": "^0.1.1", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", @@ -34,9 +34,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index e6a351f2a4..a94910cfb2 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-cost-insights +## 0.9.1 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [889d89b6e] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + ## 0.9.0 ### Minor Changes diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 3fd057aa4a..a467fc0542 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cost-insights", - "version": "0.9.0", + "version": "0.9.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ }, "dependencies": { "@backstage/config": "^0.1.5", - "@backstage/core": "^0.7.8", + "@backstage/core": "^0.7.9", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -55,9 +55,9 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/explore-react/CHANGELOG.md b/plugins/explore-react/CHANGELOG.md index 4141daf2b6..17e3775c6d 100644 --- a/plugins/explore-react/CHANGELOG.md +++ b/plugins/explore-react/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-explore-react +## 0.0.5 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- Updated dependencies [062bbf90f] +- Updated dependencies [889d89b6e] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + ## 0.0.4 ### Patch Changes diff --git a/plugins/explore-react/package.json b/plugins/explore-react/package.json index cf8c944433..5a8d082abe 100644 --- a/plugins/explore-react/package.json +++ b/plugins/explore-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-explore-react", - "version": "0.0.4", + "version": "0.0.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -28,12 +28,12 @@ "postpack": "backstage-cli postpack" }, "dependencies": { - "@backstage/core": "^0.7.1" + "@backstage/core": "^0.7.9" }, "devDependencies": { - "@backstage/cli": "^0.6.4", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.6", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/explore/CHANGELOG.md b/plugins/explore/CHANGELOG.md index 78275577f5..c3a756f928 100644 --- a/plugins/explore/CHANGELOG.md +++ b/plugins/explore/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-explore +## 0.3.5 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 0b033d07b: Add "Organization" tab with a diagram +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/plugin-explore-react@0.0.5 + - @backstage/catalog-model@0.7.9 + ## 0.3.4 ### Patch Changes diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 3069b719f0..d96c2f4d18 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-explore", - "version": "0.3.4", + "version": "0.3.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,10 +30,10 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/catalog-model": "^0.7.5", - "@backstage/core": "^0.7.8", - "@backstage/plugin-catalog-react": "^0.1.4", - "@backstage/plugin-explore-react": "^0.0.4", + "@backstage/catalog-model": "^0.7.9", + "@backstage/core": "^0.7.9", + "@backstage/plugin-catalog-react": "^0.1.6", + "@backstage/plugin-explore-react": "^0.0.5", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -45,9 +45,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/fossa/CHANGELOG.md b/plugins/fossa/CHANGELOG.md index bd7af91dca..1c5de31c0c 100644 --- a/plugins/fossa/CHANGELOG.md +++ b/plugins/fossa/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-fossa +## 0.2.7 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.2.6 ### Patch Changes diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index e6fa50f7da..51a6c5cf55 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-fossa", - "version": "0.2.6", + "version": "0.2.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,10 +31,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.3", - "@backstage/core": "^0.7.8", + "@backstage/catalog-model": "^0.7.9", + "@backstage/core": "^0.7.9", "@backstage/errors": "^0.1.1", - "@backstage/plugin-catalog-react": "^0.1.1", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -47,9 +47,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/gcp-projects/CHANGELOG.md b/plugins/gcp-projects/CHANGELOG.md index 8445cbb02b..3843a9519a 100644 --- a/plugins/gcp-projects/CHANGELOG.md +++ b/plugins/gcp-projects/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-gcp-projects +## 0.2.6 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [889d89b6e] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + ## 0.2.5 ### Patch Changes diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index d50e63725e..6b0c625501 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gcp-projects", - "version": "0.2.5", + "version": "0.2.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.7.8", + "@backstage/core": "^0.7.9", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -41,9 +41,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index cae3bcdb70..cccb452b34 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.7.8", + "@backstage/core": "^0.7.9", "@backstage/integration": "^0.5.1", "@backstage/theme": "^0.2.7", "recharts": "^1.8.5", @@ -36,9 +36,9 @@ "react": "^16.13.1" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@types/recharts": "^1.8.15", "@testing-library/react-hooks": "^3.4.2", diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index aa2fec5512..f435ff31bc 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-github-actions +## 0.4.6 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.4.5 ### Patch Changes diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 9bdbaf3c4b..841bb3ebc5 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-github-actions", - "version": "0.4.5", + "version": "0.4.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,9 +32,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.8", - "@backstage/plugin-catalog-react": "^0.1.5", - "@backstage/core": "^0.7.8", + "@backstage/catalog-model": "^0.7.9", + "@backstage/plugin-catalog-react": "^0.1.6", + "@backstage/core": "^0.7.9", "@backstage/integration": "^0.5.2", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", @@ -50,9 +50,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/github-deployments/CHANGELOG.md b/plugins/github-deployments/CHANGELOG.md index 23c8943349..3212382684 100644 --- a/plugins/github-deployments/CHANGELOG.md +++ b/plugins/github-deployments/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-github-deployments +## 0.1.5 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- f5a916c49: Support for GitHub Enterprise with GithubDeploymentsPlugin +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/integration-react@0.1.2 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.1.4 ### Patch Changes diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index df85cb2a55..1058a30fed 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-github-deployments", - "version": "0.1.4", + "version": "0.1.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,12 +20,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.6", - "@backstage/core": "^0.7.8", + "@backstage/catalog-model": "^0.7.9", + "@backstage/core": "^0.7.9", "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.1", - "@backstage/integration-react": "^0.1.1", - "@backstage/plugin-catalog-react": "^0.1.3", + "@backstage/integration-react": "^0.1.2", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -37,9 +37,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/gitops-profiles/CHANGELOG.md b/plugins/gitops-profiles/CHANGELOG.md index b72e3c23c5..ae6672581f 100644 --- a/plugins/gitops-profiles/CHANGELOG.md +++ b/plugins/gitops-profiles/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-gitops-profiles +## 0.2.7 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [889d89b6e] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + ## 0.2.6 ### Patch Changes diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 21744a6f54..dbb84e4994 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gitops-profiles", - "version": "0.2.6", + "version": "0.2.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.7.8", + "@backstage/core": "^0.7.9", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -42,9 +42,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/graphiql/CHANGELOG.md b/plugins/graphiql/CHANGELOG.md index 681005b13f..e0086af107 100644 --- a/plugins/graphiql/CHANGELOG.md +++ b/plugins/graphiql/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-graphiql +## 0.2.11 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [889d89b6e] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + ## 0.2.10 ### Patch Changes diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 053fbc066a..66c8790b8f 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphiql", "description": "Backstage plugin for browsing GraphQL APIs", - "version": "0.2.10", + "version": "0.2.11", "private": false, "publishConfig": { "access": "public", @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.7.8", + "@backstage/core": "^0.7.9", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -43,9 +43,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/graphql/CHANGELOG.md b/plugins/graphql/CHANGELOG.md index fc31d831dc..2604a15230 100644 --- a/plugins/graphql/CHANGELOG.md +++ b/plugins/graphql/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-graphql-backend +## 0.1.8 + +### Patch Changes + +- Updated dependencies [3be844496] +- Updated dependencies [22fd8ce2a] +- Updated dependencies [f9fb4a205] + - @backstage/plugin-catalog-graphql@0.2.9 + - @backstage/backend-common@0.8.0 + ## 0.1.7 ### Patch Changes diff --git a/plugins/graphql/package.json b/plugins/graphql/package.json index 0df78298d7..619df1d33b 100644 --- a/plugins/graphql/package.json +++ b/plugins/graphql/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-graphql-backend", - "version": "0.1.7", + "version": "0.1.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.7.0", + "@backstage/backend-common": "^0.8.0", "@backstage/config": "^0.1.5", - "@backstage/plugin-catalog-graphql": "^0.2.8", + "@backstage/plugin-catalog-graphql": "^0.2.9", "@graphql-modules/core": "^0.7.17", "@types/express": "^4.17.6", "apollo-server": "^2.16.1", @@ -45,7 +45,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.6.10", + "@backstage/cli": "^0.6.11", "@types/supertest": "^2.0.8", "eslint-plugin-graphql": "^4.0.0", "msw": "^0.20.5", diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index ac734e9a95..79e204b350 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-jenkins +## 0.4.3 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.4.2 ### Patch Changes diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index aa57fa1d1a..d14a18ffe7 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins", - "version": "0.4.2", + "version": "0.4.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,9 +31,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.3", - "@backstage/core": "^0.7.8", - "@backstage/plugin-catalog-react": "^0.1.3", + "@backstage/catalog-model": "^0.7.9", + "@backstage/core": "^0.7.9", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -47,9 +47,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/kafka-backend/CHANGELOG.md b/plugins/kafka-backend/CHANGELOG.md index ef006a360f..b71ac92e5a 100644 --- a/plugins/kafka-backend/CHANGELOG.md +++ b/plugins/kafka-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-kafka-backend +## 0.2.5 + +### Patch Changes + +- Updated dependencies [22fd8ce2a] +- Updated dependencies [10c008a3a] +- Updated dependencies [f9fb4a205] +- Updated dependencies [16be1d093] + - @backstage/backend-common@0.8.0 + - @backstage/catalog-model@0.7.9 + ## 0.2.4 ### Patch Changes diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index 3e3f22654a..14bbfef486 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kafka-backend", - "version": "0.2.4", + "version": "0.2.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.7.0", - "@backstage/catalog-model": "^0.7.8", + "@backstage/backend-common": "^0.8.0", + "@backstage/catalog-model": "^0.7.9", "@backstage/config": "^0.1.5", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -42,7 +42,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.6.10", + "@backstage/cli": "^0.6.11", "@types/jest-when": "^2.7.2", "@types/lodash": "^4.14.151", "jest-when": "^3.1.0", diff --git a/plugins/kafka/CHANGELOG.md b/plugins/kafka/CHANGELOG.md index f1c4c215ed..67c70c321d 100644 --- a/plugins/kafka/CHANGELOG.md +++ b/plugins/kafka/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-kafka +## 0.2.7 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.2.6 ### Patch Changes diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index e18d5188d0..b6c600a90c 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kafka", - "version": "0.2.6", + "version": "0.2.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.4", - "@backstage/core": "^0.7.8", - "@backstage/plugin-catalog-react": "^0.1.1", + "@backstage/catalog-model": "^0.7.9", + "@backstage/core": "^0.7.9", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -33,9 +33,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.4.2", diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index a9893925e2..265b7eb414 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-kubernetes-backend +## 0.3.7 + +### Patch Changes + +- f9f9d633d: Add possibility to configure TLS verification for `gke` type clusters +- Updated dependencies [22fd8ce2a] +- Updated dependencies [10c008a3a] +- Updated dependencies [f9fb4a205] +- Updated dependencies [16be1d093] + - @backstage/backend-common@0.8.0 + - @backstage/catalog-model@0.7.9 + ## 0.3.6 ### Patch Changes diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index f73166d5fc..9e438ce788 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-backend", - "version": "0.3.6", + "version": "0.3.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.7.0", - "@backstage/catalog-model": "^0.7.8", + "@backstage/backend-common": "^0.8.0", + "@backstage/catalog-model": "^0.7.9", "@backstage/config": "^0.1.5", "@backstage/plugin-kubernetes-common": "^0.1.0", "@google-cloud/container": "^2.2.0", @@ -53,7 +53,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.6.10", + "@backstage/cli": "^0.6.11", "@types/aws4": "^1.5.1", "supertest": "^6.1.3" }, diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index d78842f351..cca63f6389 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-kubernetes +## 0.4.4 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- ea21d46f0: Export types +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.4.3 ### Patch Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index aaa55fef13..5036e08852 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.4.3", + "version": "0.4.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,10 +30,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.8", + "@backstage/catalog-model": "^0.7.9", "@backstage/config": "^0.1.5", - "@backstage/core": "^0.7.8", - "@backstage/plugin-catalog-react": "^0.1.5", + "@backstage/core": "^0.7.9", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/plugin-kubernetes-common": "^0.1.0", "@backstage/theme": "^0.2.7", "@kubernetes/client-node": "^0.14.0", @@ -49,9 +49,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.4.2", diff --git a/plugins/lighthouse/CHANGELOG.md b/plugins/lighthouse/CHANGELOG.md index cb2cb5b9d1..d9b9b541d0 100644 --- a/plugins/lighthouse/CHANGELOG.md +++ b/plugins/lighthouse/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-lighthouse +## 0.2.16 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.2.15 ### Patch Changes diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 039f8b6dce..68f4722157 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-lighthouse", - "version": "0.2.15", + "version": "0.2.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,10 +31,10 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/catalog-model": "^0.7.3", + "@backstage/catalog-model": "^0.7.9", "@backstage/config": "^0.1.4", - "@backstage/core": "^0.7.8", - "@backstage/plugin-catalog-react": "^0.1.2", + "@backstage/core": "^0.7.9", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -46,9 +46,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/newrelic/CHANGELOG.md b/plugins/newrelic/CHANGELOG.md index f2745796d8..2091ef6a1d 100644 --- a/plugins/newrelic/CHANGELOG.md +++ b/plugins/newrelic/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-newrelic +## 0.2.7 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [889d89b6e] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + ## 0.2.6 ### Patch Changes diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 96ef75b532..7938f19100 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-newrelic", - "version": "0.2.6", + "version": "0.2.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.7.8", + "@backstage/core": "^0.7.9", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -41,9 +41,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index c62c618ff4..33ef38a7c2 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-org +## 0.3.13 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- f59a945b7: Paginate group members to only display 50 members maximum. +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/core-api@0.2.18 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.3.12 ### Patch Changes diff --git a/plugins/org/package.json b/plugins/org/package.json index a0bb1ba06e..4f8e37fa99 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org", - "version": "0.3.12", + "version": "0.3.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,10 +20,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.6", - "@backstage/core": "^0.7.8", - "@backstage/core-api": "^0.2.16", - "@backstage/plugin-catalog-react": "^0.1.4", + "@backstage/catalog-model": "^0.7.9", + "@backstage/core": "^0.7.9", + "@backstage/core-api": "^0.2.18", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -35,9 +35,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/pagerduty/CHANGELOG.md b/plugins/pagerduty/CHANGELOG.md index b7fc7fec40..21e79445b0 100644 --- a/plugins/pagerduty/CHANGELOG.md +++ b/plugins/pagerduty/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-pagerduty +## 0.3.4 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.3.3 ### Patch Changes diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 7cbb0a21b7..b3fbc37569 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-pagerduty", - "version": "0.3.3", + "version": "0.3.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,9 +30,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.3", - "@backstage/core": "^0.7.8", - "@backstage/plugin-catalog-react": "^0.1.1", + "@backstage/catalog-model": "^0.7.9", + "@backstage/core": "^0.7.9", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -46,9 +46,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/proxy-backend/CHANGELOG.md b/plugins/proxy-backend/CHANGELOG.md index 06a96576e6..2890039b96 100644 --- a/plugins/proxy-backend/CHANGELOG.md +++ b/plugins/proxy-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-proxy-backend +## 0.2.8 + +### Patch Changes + +- Updated dependencies [22fd8ce2a] +- Updated dependencies [f9fb4a205] + - @backstage/backend-common@0.8.0 + ## 0.2.7 ### Patch Changes diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 2c6fc9e08d..d2b0d64678 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-proxy-backend", - "version": "0.2.7", + "version": "0.2.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -28,7 +28,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.7.0", + "@backstage/backend-common": "^0.8.0", "@backstage/config": "^0.1.5", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -42,7 +42,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.6.10", + "@backstage/cli": "^0.6.11", "@types/http-proxy-middleware": "^0.19.3", "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", diff --git a/plugins/register-component/CHANGELOG.md b/plugins/register-component/CHANGELOG.md index 3634972e9a..ae0ae6562b 100644 --- a/plugins/register-component/CHANGELOG.md +++ b/plugins/register-component/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-register-component +## 0.2.15 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.2.14 ### Patch Changes diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index 376526a059..d10bdcfc38 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-register-component", - "version": "0.2.14", + "version": "0.2.15", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,9 +30,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.5", - "@backstage/core": "^0.7.8", - "@backstage/plugin-catalog-react": "^0.1.4", + "@backstage/catalog-model": "^0.7.9", + "@backstage/core": "^0.7.9", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -45,9 +45,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/rollbar-backend/CHANGELOG.md b/plugins/rollbar-backend/CHANGELOG.md index 9aa0d77e69..0318d3a111 100644 --- a/plugins/rollbar-backend/CHANGELOG.md +++ b/plugins/rollbar-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-rollbar-backend +## 0.1.11 + +### Patch Changes + +- Updated dependencies [22fd8ce2a] +- Updated dependencies [f9fb4a205] + - @backstage/backend-common@0.8.0 + ## 0.1.10 ### Patch Changes diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index c7eb819dc1..03be93dbfa 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-rollbar-backend", - "version": "0.1.10", + "version": "0.1.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.7.0", + "@backstage/backend-common": "^0.8.0", "@backstage/config": "^0.1.5", "@types/express": "^4.17.6", "axios": "^0.21.1", @@ -47,7 +47,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.6.10", + "@backstage/cli": "^0.6.11", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/rollbar/CHANGELOG.md b/plugins/rollbar/CHANGELOG.md index 1d8c2c399e..26b9913df2 100644 --- a/plugins/rollbar/CHANGELOG.md +++ b/plugins/rollbar/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-rollbar +## 0.3.5 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.3.4 ### Patch Changes diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 3e1f7e4964..37ada55f5f 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-rollbar", - "version": "0.3.4", + "version": "0.3.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,9 +31,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.3", - "@backstage/core": "^0.7.8", - "@backstage/plugin-catalog-react": "^0.1.1", + "@backstage/catalog-model": "^0.7.9", + "@backstage/core": "^0.7.9", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -47,9 +47,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^3.3.0", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 987109e62f..cdb8fd734b 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-scaffolder-backend +## 0.11.1 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 82ca1ac22: The apiBaseUrl setting for Bitbucket Server integrations will now be used when it is set. Otherwise, it will default back to the host setting. +- fd39d4662: Move `jest-when` to the dev dependencies +- Updated dependencies [22fd8ce2a] +- Updated dependencies [10c008a3a] +- Updated dependencies [f9fb4a205] +- Updated dependencies [16be1d093] + - @backstage/backend-common@0.8.0 + - @backstage/catalog-model@0.7.9 + ## 0.11.0 ### Minor Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 7452d94c63..2565a9d05a 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "0.11.0", + "version": "0.11.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.7.0", + "@backstage/backend-common": "^0.8.0", "@backstage/catalog-client": "^0.3.11", - "@backstage/catalog-model": "^0.7.8", + "@backstage/catalog-model": "^0.7.9", "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.2", @@ -64,8 +64,8 @@ "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/test-utils": "^0.1.11", "@types/fs-extra": "^9.0.1", "@types/mock-fs": "^4.13.0", "@types/supertest": "^2.0.8", diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index b67740ecc1..7e55dabc55 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-scaffolder +## 0.9.4 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 81ef1d57b: Show error on task page if task does not exist. +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/integration-react@0.1.2 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.9.3 ### Patch Changes diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index e21e457c6c..a29251f705 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "0.9.3", + "version": "0.9.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,13 +31,13 @@ }, "dependencies": { "@backstage/catalog-client": "^0.3.11", - "@backstage/catalog-model": "^0.7.8", + "@backstage/catalog-model": "^0.7.9", "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", - "@backstage/core": "^0.7.8", + "@backstage/core": "^0.7.9", "@backstage/integration": "^0.5.2", - "@backstage/integration-react": "^0.1.1", - "@backstage/plugin-catalog-react": "^0.1.5", + "@backstage/integration-react": "^0.1.2", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -60,9 +60,9 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index 99077b9b62..23234aca2f 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -25,8 +25,8 @@ "@types/lunr": "^2.3.3" }, "devDependencies": { - "@backstage/backend-common": "^0.7.0", - "@backstage/cli": "^0.6.10" + "@backstage/backend-common": "^0.8.0", + "@backstage/cli": "^0.6.11" }, "files": [ "dist" diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index fd429aae62..9d34d92575 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-search-backend +## 0.1.5 + +### Patch Changes + +- Updated dependencies [22fd8ce2a] +- Updated dependencies [f9fb4a205] + - @backstage/backend-common@0.8.0 + ## 0.1.4 ### Patch Changes diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index 4de8ada9bc..c06f54a266 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-backend", - "version": "0.1.4", + "version": "0.1.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,7 +19,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.7.0", + "@backstage/backend-common": "^0.8.0", "@backstage/search-common": "^0.1.1", "@backstage/plugin-search-backend-node": "^0.1.3", "@types/express": "^4.17.6", @@ -29,7 +29,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.6.10", + "@backstage/cli": "^0.6.11", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index b4bcd1f855..9c0ab2fa8c 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-search +## 0.3.6 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.3.5 ### Patch Changes diff --git a/plugins/search/package.json b/plugins/search/package.json index ee7d15ddc3..7593a2901d 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "0.3.5", + "version": "0.3.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.7.8", - "@backstage/catalog-model": "^0.7.3", - "@backstage/plugin-catalog-react": "^0.1.2", + "@backstage/core": "^0.7.9", + "@backstage/catalog-model": "^0.7.9", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/search-common": "^0.1.1", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", @@ -44,9 +44,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index 261972384a..b22b5539ef 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-sentry +## 0.3.10 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.3.9 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index d33b57e975..7da547dd94 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry", - "version": "0.3.9", + "version": "0.3.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,9 +31,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.3", - "@backstage/core": "^0.7.8", - "@backstage/plugin-catalog-react": "^0.1.1", + "@backstage/catalog-model": "^0.7.9", + "@backstage/core": "^0.7.9", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -46,9 +46,9 @@ "timeago.js": "^4.0.2" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/shortcuts/CHANGELOG.md b/plugins/shortcuts/CHANGELOG.md new file mode 100644 index 0000000000..bdc4b3adba --- /dev/null +++ b/plugins/shortcuts/CHANGELOG.md @@ -0,0 +1,12 @@ +# @backstage/plugin-shortcuts + +## 0.1.2 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- Updated dependencies [062bbf90f] +- Updated dependencies [889d89b6e] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index a0f8a0c498..aea2f8461e 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-shortcuts", - "version": "0.1.1", + "version": "0.1.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.7.8", + "@backstage/core": "^0.7.9", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -35,9 +35,9 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/sonarqube/CHANGELOG.md b/plugins/sonarqube/CHANGELOG.md index 48b9292791..e457d2bdf9 100644 --- a/plugins/sonarqube/CHANGELOG.md +++ b/plugins/sonarqube/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-sonarqube +## 0.1.18 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.1.17 ### Patch Changes diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 6b7e5ff4d9..9e3d6bf03b 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sonarqube", - "version": "0.1.17", + "version": "0.1.18", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,9 +32,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.7", - "@backstage/plugin-catalog-react": "^0.1.3", - "@backstage/core": "^0.7.8", + "@backstage/catalog-model": "^0.7.9", + "@backstage/plugin-catalog-react": "^0.1.6", + "@backstage/core": "^0.7.9", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -47,9 +47,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/splunk-on-call/CHANGELOG.md b/plugins/splunk-on-call/CHANGELOG.md index b3151c3e04..4f34ea3bcc 100644 --- a/plugins/splunk-on-call/CHANGELOG.md +++ b/plugins/splunk-on-call/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-splunk-on-call +## 0.2.1 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.2.0 ### Minor Changes diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index 4ec6e6a0f4..ebb6fdb42e 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-splunk-on-call", - "version": "0.2.0", + "version": "0.2.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,9 +30,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.4", - "@backstage/core": "^0.7.8", - "@backstage/plugin-catalog-react": "^0.1.1", + "@backstage/catalog-model": "^0.7.9", + "@backstage/core": "^0.7.9", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -45,9 +45,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/tech-radar/CHANGELOG.md b/plugins/tech-radar/CHANGELOG.md index 9f0a5b87c3..64f255adb1 100644 --- a/plugins/tech-radar/CHANGELOG.md +++ b/plugins/tech-radar/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-tech-radar +## 0.3.11 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [889d89b6e] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + ## 0.3.10 ### Patch Changes diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 538511565b..e65e3d27a7 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-radar", - "version": "0.3.10", + "version": "0.3.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,7 +30,7 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.7.8", + "@backstage/core": "^0.7.9", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -43,9 +43,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 59a116f653..4b348d49a4 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-techdocs-backend +## 0.8.1 + +### Patch Changes + +- Updated dependencies [22fd8ce2a] +- Updated dependencies [10c008a3a] +- Updated dependencies [f9fb4a205] +- Updated dependencies [16be1d093] +- Updated dependencies [e04f1ccfb] + - @backstage/backend-common@0.8.0 + - @backstage/catalog-model@0.7.9 + - @backstage/techdocs-common@0.6.1 + ## 0.8.0 ### Minor Changes diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 1424381f30..a702f63622 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "0.8.0", + "version": "0.8.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,11 +30,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.7.0", - "@backstage/catalog-model": "^0.7.8", + "@backstage/backend-common": "^0.8.0", + "@backstage/catalog-model": "^0.7.9", "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", - "@backstage/techdocs-common": "^0.6.0", + "@backstage/techdocs-common": "^0.6.1", "@types/express": "^4.17.6", "cross-fetch": "^3.0.6", "dockerode": "^3.2.1", @@ -45,7 +45,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.6.10", + "@backstage/cli": "^0.6.11", "@types/dockerode": "^3.2.1", "supertest": "^6.1.3" }, diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 69e988a433..8382ef9419 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-techdocs +## 0.9.2 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/integration-react@0.1.2 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 + ## 0.9.1 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 87cb1185a0..ce748f836d 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "0.9.1", + "version": "0.9.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,11 +32,11 @@ }, "dependencies": { "@backstage/config": "^0.1.5", - "@backstage/catalog-model": "^0.7.8", - "@backstage/core": "^0.7.8", + "@backstage/catalog-model": "^0.7.9", + "@backstage/core": "^0.7.9", "@backstage/integration": "^0.5.2", - "@backstage/integration-react": "^0.1.1", - "@backstage/plugin-catalog-react": "^0.1.5", + "@backstage/integration-react": "^0.1.2", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/theme": "^0.2.7", "@backstage/errors": "^0.1.1", "@material-ui/core": "^4.11.0", @@ -51,9 +51,9 @@ "sanitize-html": "^2.3.2" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/todo-backend/CHANGELOG.md b/plugins/todo-backend/CHANGELOG.md index b42f631a98..ebe3c2003c 100644 --- a/plugins/todo-backend/CHANGELOG.md +++ b/plugins/todo-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-todo-backend +## 0.1.5 + +### Patch Changes + +- Updated dependencies [22fd8ce2a] +- Updated dependencies [10c008a3a] +- Updated dependencies [f9fb4a205] +- Updated dependencies [16be1d093] + - @backstage/backend-common@0.8.0 + - @backstage/catalog-model@0.7.9 + ## 0.1.4 ### Patch Changes diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index 1af9f9b40c..5225adb648 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-todo-backend", - "version": "0.1.4", + "version": "0.1.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,9 +24,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.7.0", + "@backstage/backend-common": "^0.8.0", "@backstage/catalog-client": "^0.3.11", - "@backstage/catalog-model": "^0.7.8", + "@backstage/catalog-model": "^0.7.9", "@backstage/config": "^0.1.5", "@backstage/errors": "^0.1.1", "@backstage/integration": "^0.5.2", @@ -39,7 +39,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.6.10", + "@backstage/cli": "^0.6.11", "@types/supertest": "^2.0.8", "msw": "^0.21.2", "supertest": "^6.1.3" diff --git a/plugins/todo/CHANGELOG.md b/plugins/todo/CHANGELOG.md new file mode 100644 index 0000000000..f3732c923e --- /dev/null +++ b/plugins/todo/CHANGELOG.md @@ -0,0 +1,17 @@ +# @backstage/plugin-todo + +## 0.1.1 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [10c008a3a] +- Updated dependencies [889d89b6e] +- Updated dependencies [16be1d093] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + - @backstage/plugin-catalog-react@0.1.6 + - @backstage/catalog-model@0.7.9 diff --git a/plugins/todo/package.json b/plugins/todo/package.json index 8a3d327929..c9a9f4bb51 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-todo", - "version": "0.1.0", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -26,10 +26,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.3", - "@backstage/core": "^0.7.8", + "@backstage/catalog-model": "^0.7.9", + "@backstage/core": "^0.7.9", "@backstage/errors": "^0.1.1", - "@backstage/plugin-catalog-react": "^0.1.1", + "@backstage/plugin-catalog-react": "^0.1.6", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -39,9 +39,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index f6f3046bef..5b1706e0c7 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-user-settings +## 0.2.10 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [889d89b6e] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + ## 0.2.9 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index b52584267c..f47f5b50af 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings", - "version": "0.2.9", + "version": "0.2.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.7.8", + "@backstage/core": "^0.7.9", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -41,9 +41,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/welcome/CHANGELOG.md b/plugins/welcome/CHANGELOG.md index cf907b5b4f..53ffd59264 100644 --- a/plugins/welcome/CHANGELOG.md +++ b/plugins/welcome/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-welcome +## 0.2.8 + +### Patch Changes + +- 062bbf90f: chore: bump `@testing-library/user-event` from 12.8.3 to 13.1.8 +- 675a569a9: chore: bump `react-use` dependency in all packages +- Updated dependencies [062bbf90f] +- Updated dependencies [889d89b6e] +- Updated dependencies [3f988cb63] +- Updated dependencies [675a569a9] + - @backstage/core@0.7.9 + ## 0.2.7 ### Patch Changes diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index 5f557f00b1..df16ec7f4d 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-welcome", - "version": "0.2.7", + "version": "0.2.8", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -30,7 +30,7 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.7.8", + "@backstage/core": "^0.7.9", "@backstage/theme": "^0.2.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -41,9 +41,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.10", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", From 5b8638c15eb9e4bac91b3c36d1a0640dec70ae25 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 12 May 2021 11:30:32 +0200 Subject: [PATCH 80/95] chore: fix the create-app version Signed-off-by: blam --- packages/create-app/CHANGELOG.md | 2 +- packages/create-app/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 5d4f176246..2e62d43436 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,6 +1,6 @@ # @backstage/create-app -## 1.0.0 +## 0.3.22 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index d7dc99218e..b850893f9c 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "Create app package for Backstage", - "version": "1.0.0", + "version": "0.3.22", "private": false, "publishConfig": { "access": "public" From 68fdbf014ebc768f686606d5b0ffe606ca19b002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 11 May 2021 18:31:37 +0200 Subject: [PATCH 81/95] Add the `status` field to the Entity envelope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/mean-taxis-leave.md | 5 ++ .../software-catalog/descriptor-format.md | 49 ++++++++++++++++++ .../software-catalog/extending-the-model.md | 25 +++++++-- .../software-catalog/well-known-statuses.md | 51 +++++++++++++++++++ microsite/sidebars.json | 1 + mkdocs.yml | 1 + packages/catalog-model/api-report.md | 1 + packages/catalog-model/src/entity/Entity.ts | 8 +++ packages/catalog-model/src/entity/util.ts | 8 +-- .../src/schema/Entity.schema.json | 9 ++++ .../src/schema/shared/common.schema.json | 11 ++-- .../catalog-backend/src/next/Stitcher.test.ts | 10 ++++ plugins/catalog-backend/src/next/Stitcher.ts | 17 ++++++- 13 files changed, 183 insertions(+), 13 deletions(-) create mode 100644 .changeset/mean-taxis-leave.md create mode 100644 docs/features/software-catalog/well-known-statuses.md diff --git a/.changeset/mean-taxis-leave.md b/.changeset/mean-taxis-leave.md new file mode 100644 index 0000000000..8c1e5ea3c3 --- /dev/null +++ b/.changeset/mean-taxis-leave.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': patch +--- + +Add the `status` field to the Entity envelope diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index eb087a9c5f..665c729191 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -23,6 +23,7 @@ we recommend that you name them `catalog-info.yaml`. - [Common to All Kinds: The Envelope](#common-to-all-kinds-the-envelope) - [Common to All Kinds: The Metadata](#common-to-all-kinds-the-metadata) - [Common to All Kinds: Relations](#common-to-all-kinds-relations) +- [Common to All Kinds: Status](#common-to-all-kinds-status) - [Kind: Component](#kind-component) - [Kind: Template](#kind-template) - [Kind: API](#kind-api) @@ -396,6 +397,54 @@ with it (such as the default kind being `Group` if not specified). See the [well-known relations section](well-known-relations.md) for a list of well-known / common relations and their semantics. +## Common to All Kinds: Status + +The `status` root field is a read-only set of statuses, pertaining to the +current state or health of the entity, described in the +[well-known statuses section](well-known-statuses.md). Each status field +contains a specific blob of data that describes some aspect of the state of the +entity, as seen from the point of view of some specific system. Different +systems may contribute to this status object, under their own respective keys. + +The current main use case for this field is for the ingestion processes of the +catalog itself to convey information about failures and warnings back to the +user. + +A status field as part of a single entity that's read out of the API may look as +follows. + +```js +{ + // ... + "status": { + "backstage.io/processing-status": { + "errors": [] + } + }, + "spec": { + // ... + } +} +``` + +The keys of the `status` object are arbitrary strings. We recommend that any +statuses, that are not strictly private within the organization, be namespaced +to avoid collisions. Statuses emitted by Backstage core processes will for +example be prefixed with `backstage.io/` as in the example above. + +The values of the `status` object are currently left unrestricted, except that +they must be objects. We reserve the right to extend this model in the future, +such that some fields of those value objects gain standardized meaning. We may +for example want to add a standard concept of "severity" or "level" to these. + +Entity descriptor YAML files are not supposed to contain this field. Instead, +catalog processors analyze the entity descriptor data and its surroundings, and +deduce status entries that are then attached onto the entity as read from the +catalog. + +See the [well-known statuses section](well-known-statuses.md) for a list of +well-known / common relations and their semantics. + ## Kind: Component Describes the following entity kind: diff --git a/docs/features/software-catalog/extending-the-model.md b/docs/features/software-catalog/extending-the-model.md index d4dca0c059..4cc1bc1110 100644 --- a/docs/features/software-catalog/extending-the-model.md +++ b/docs/features/software-catalog/extending-the-model.md @@ -1,6 +1,7 @@ --- id: extending-the-model title: Extending the model +# prettier-ignore description: Documentation on extending the catalog model --- @@ -301,9 +302,9 @@ Example intents: > "We have this concept of service maintainership, separate from ownership, that > we would like to make relations to individual users for." -> We feel that we want to explicitly model the team-to-global-department mapping -> as a relation, because it is core to our org setup and we frequently query for -> it. +> "We feel that we want to explicitly model the team-to-global-department +> mapping as a relation, because it is core to our org setup and we frequently +> query for it." Any processor can emit relations for entities as they are being processed, and new processors can be added when building the backend catalog using the @@ -349,3 +350,21 @@ If you want to extend the use of an established relation type in a way that has an effect outside of your organization, reach out to the Backstage maintainers or a support partner to discuss risk/impact. It may even be that one end of the relation could be considered for addition to the core. + +## Adding a New Status field + +Example intents: + +> "We would like to convey entity statuses through the catalog in a generic way, +> as an integration layer. Our monitoring and alerting system has a plugin with +> Backstage, and it would be useful if the entity's status field contained the +> current alert state close to the actual entity data for anyone to consume. + +While we are considering a mechanism for contributing generic statuses to +entities, no such mechanism has yet been built. If you are interested in that +topic, [this issue](https://github.com/backstage/backstage/issues/2292) contains +more context. + +But in general, errors emitted (and exceptions thrown) by any processor +including custom ones, end up in the [well known key](well-known-statuses.md) +for ingestion status. diff --git a/docs/features/software-catalog/well-known-statuses.md b/docs/features/software-catalog/well-known-statuses.md new file mode 100644 index 0000000000..a516c61daf --- /dev/null +++ b/docs/features/software-catalog/well-known-statuses.md @@ -0,0 +1,51 @@ +--- +id: well-known-statuses +title: Well-known Status fields of Catalog Entities +sidebar_label: Well-known Statuses +# prettier-ignore +description: Lists a number of well known entity statuses, that have defined semantics. They can be attached to catalog entities and consumed by plugins as needed. +--- + +This section lists a number of well known +[entity status fields](descriptor-format.md#common-to-all-kinds-status), that +have defined semantics. They can be attached to catalog entities and consumed by +plugins as needed. + +If you are looking to extend the set of statuses, see +[Extending the model](extending-the-model.md). + +## Common Fields + +The values of statuses are currently left unrestricted, except that they must be +objects. They therefore currently formally have no common fields. + +We reserve the right to extend this model in the future, such that some fields +of those value objects gain standardized meaning. We may for example want to add +a standard concept of "severity" or "level" to these. + +## Statuses + +This is a (non-exhaustive) list of statuses that are known to be in active use. + +### `backstage.io/processing-status` + +Contains the current status of the catalog's ingestion of this entity. Errors +that may appear here include inability to read from the remote SCM provider, +syntax errors in the YAML file, and similar. + +Note that the entity data itself may be of an older version, when errors are +present. The ingestion system keeps the old, valid entity data untouched when +possible, so the errors described in this state may not seem to align with the +rest of the entity, because they pertain to a remote that could not be +successfully ingested. This is normal. + +```yaml +# Example: +status: + backstage.io/processing-status: + errors: [] +``` + +This status is in active development and its format will change unexpectedly. Do +not consume it in your own code until such a time that this documentation has +been updated. diff --git a/microsite/sidebars.json b/microsite/sidebars.json index c7b049a53e..3544ebdca3 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -40,6 +40,7 @@ "features/software-catalog/references", "features/software-catalog/well-known-annotations", "features/software-catalog/well-known-relations", + "features/software-catalog/well-known-statuses", "features/software-catalog/extending-the-model", "features/software-catalog/external-integrations", "features/software-catalog/software-catalog-api" diff --git a/mkdocs.yml b/mkdocs.yml index 7826b3221d..0e2e78be26 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -37,6 +37,7 @@ nav: - Entity References: 'features/software-catalog/references.md' - Well-known Annotations: 'features/software-catalog/well-known-annotations.md' - Well-known Relations: 'features/software-catalog/well-known-relations.md' + - Well-known Statuses: 'features/software-catalog/well-known-statuses.md' - Extending the model: 'features/software-catalog/extending-the-model.md' - External integrations: 'features/software-catalog/external-integrations.md' - API: 'features/software-catalog/api.md' diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 2b56879f48..00eaf8410e 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -112,6 +112,7 @@ export type Entity = { metadata: EntityMeta; spec?: JsonObject; relations?: EntityRelation[]; + status?: Record; }; // @public diff --git a/packages/catalog-model/src/entity/Entity.ts b/packages/catalog-model/src/entity/Entity.ts index 7abe1f9c0c..1b5d38dee9 100644 --- a/packages/catalog-model/src/entity/Entity.ts +++ b/packages/catalog-model/src/entity/Entity.ts @@ -48,6 +48,14 @@ export type Entity = { * The relations that this entity has with other entities. */ relations?: EntityRelation[]; + + /** + * The current status of the entity, as claimed by various sources. + * + * The keys are implementation defined and the values can be any JSON object + * with semantics that match that implementation. + */ + status?: Record; }; /** diff --git a/packages/catalog-model/src/entity/util.ts b/packages/catalog-model/src/entity/util.ts index 2c63cc4c49..116913c85c 100644 --- a/packages/catalog-model/src/entity/util.ts +++ b/packages/catalog-model/src/entity/util.ts @@ -42,11 +42,9 @@ export function generateEntityEtag(): string { * the next version of this entity. * * Significance, in this case, means that we do not compare generated fields - * such as uid, etag and generation, and we only check that no new annotations - * are added or existing annotations were changed (since they are effectively - * merged when doing updates). + * such as uid, etag and generation. * - * Note that this comparison does NOT take state, relations or similar into + * Note that this comparison does NOT take status, relations or similar into * account. It only compares the actual input entity data, i.e. metadata and * spec. * @@ -86,7 +84,9 @@ export function entityHasChanges(previous: Entity, next: Entity): boolean { // Remove things that we explicitly do not compare delete e1.relations; + delete e1.status; delete e2.relations; + delete e2.status; return !lodash.isEqual(e1, e2); } diff --git a/packages/catalog-model/src/schema/Entity.schema.json b/packages/catalog-model/src/schema/Entity.schema.json index 803e025796..e6b004dfd0 100644 --- a/packages/catalog-model/src/schema/Entity.schema.json +++ b/packages/catalog-model/src/schema/Entity.schema.json @@ -62,6 +62,15 @@ "items": { "$ref": "common#relation" } + }, + "status": { + "type": "object", + "description": "The current status of the entity, as claimed by various sources.", + "patternProperties": { + "^.+$": { + "$ref": "common#status" + } + } } } } diff --git a/packages/catalog-model/src/schema/shared/common.schema.json b/packages/catalog-model/src/schema/shared/common.schema.json index 84ae8a4147..cb4d82cdb9 100644 --- a/packages/catalog-model/src/schema/shared/common.schema.json +++ b/packages/catalog-model/src/schema/shared/common.schema.json @@ -29,7 +29,7 @@ "$id": "#relation", "type": "object", "description": "A directed relation from one entity to another.", - "required": ["type", "source", "target"], + "required": ["type", "target"], "additionalProperties": false, "properties": { "type": { @@ -38,13 +38,16 @@ "pattern": "^\\w+$", "description": "The type of relation." }, - "source": { - "$ref": "#reference" - }, "target": { "$ref": "#reference" } } + }, + "status": { + "$id": "#status", + "type": "object", + "description": "A specific status of an entity.", + "additionalProperties": true } } } diff --git a/plugins/catalog-backend/src/next/Stitcher.test.ts b/plugins/catalog-backend/src/next/Stitcher.test.ts index f38af384fa..da1b79600a 100644 --- a/plugins/catalog-backend/src/next/Stitcher.test.ts +++ b/plugins/catalog-backend/src/next/Stitcher.test.ts @@ -91,6 +91,11 @@ describe('Stitcher', () => { }, }, ], + status: { + 'backstage.io/processing-status': { + errors: [], + }, + }, apiVersion: 'a', kind: 'k', metadata: { @@ -171,6 +176,11 @@ describe('Stitcher', () => { }, }, ]), + status: { + 'backstage.io/processing-status': { + errors: [], + }, + }, apiVersion: 'a', kind: 'k', metadata: { diff --git a/plugins/catalog-backend/src/next/Stitcher.ts b/plugins/catalog-backend/src/next/Stitcher.ts index 2bddf41ac9..8cf96c6b7f 100644 --- a/plugins/catalog-backend/src/next/Stitcher.ts +++ b/plugins/catalog-backend/src/next/Stitcher.ts @@ -41,6 +41,11 @@ function generateStableHash(entity: Entity) { .digest('hex'); } +/** + * Performs the act of stitching - to take all of the various outputs from the + * ingestion process, and stitching them together into the final entity JSON + * shape. + */ export class Stitcher { constructor( private readonly database: Knex, @@ -109,7 +114,7 @@ export class Stitcher { const { entityId, processedEntity, - // errors, + errors, incomingReferenceCount, previousHash, } = result[0]; @@ -137,7 +142,15 @@ export class Stitcher { ['backstage.io/orphan']: 'true', }; } - + if (errors !== '') { + const parsedErrors = JSON.parse(errors); + entity.status = { + ...entity.status, + 'backstage.io/processing-status': { + errors: parsedErrors, + }, + }; + } // TODO: entityRef is lower case and should be uppercase in the final // result entity.relations = result From fa69982fb4a389833d685cbc284fd82ab57cc3b7 Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Wed, 12 May 2021 13:28:30 -0400 Subject: [PATCH 82/95] fix(plugin-github-deployments): properly parse ghe hostnames Signed-off-by: Phil Kuang --- .changeset/tame-scissors-bow.md | 5 ++++ plugins/github-deployments/src/api/index.ts | 12 ++++++---- .../components/GithubDeploymentsCard.test.tsx | 23 +++++++++++-------- 3 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 .changeset/tame-scissors-bow.md diff --git a/.changeset/tame-scissors-bow.md b/.changeset/tame-scissors-bow.md new file mode 100644 index 0000000000..14be080eea --- /dev/null +++ b/.changeset/tame-scissors-bow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-github-deployments': patch +--- + +Support GHE by properly parsing enterprise instance hosts diff --git a/plugins/github-deployments/src/api/index.ts b/plugins/github-deployments/src/api/index.ts index 23a381f771..d7e7e2fca6 100644 --- a/plugins/github-deployments/src/api/index.ts +++ b/plugins/github-deployments/src/api/index.ts @@ -28,21 +28,25 @@ const getBaseUrl = ( } const location = parseLocationReference(host); - if (location.type !== 'github') { + if (location.type !== 'github' && location.type !== 'url') { return 'https://api.github.com'; } - const config = scmIntegrationsApi.github.byHost(location.target); + const config = scmIntegrationsApi.github.byUrl(location.target); if (!config) { throw new InputError( - `No matching GitHub integration configuration for host ${host}, please check your integrations config`, + `No matching GitHub integration configuration for host ${ + new URL(location.target).hostname + }, please check your integrations config`, ); } if (!config.config.apiBaseUrl) { throw new InputError( - `No apiBaseUrl available for host ${host}, please check your integrations config`, + `No apiBaseUrl available for host ${ + new URL(location.target).hostname + }, please check your integrations config`, ); } diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index bec60c47bc..68f5b5df43 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -250,7 +250,8 @@ describe('github-deployments', () => { entity = entityStub; entity.entity.metadata.annotations = { 'github.com/project-slug': 'org/repo', - 'backstage.io/source-location': 'github:my-github-1.com', + 'backstage.io/source-location': + 'github:https://my-github-1.com/org/repo/tree/master/', }; }); @@ -271,7 +272,8 @@ describe('github-deployments', () => { entity = entityStub; entity.entity.metadata.annotations = { 'github.com/project-slug': 'org/repo', - 'backstage.io/managed-by-location': 'github:my-github-2.com', + 'backstage.io/managed-by-location': + 'github:https://my-github-2.com/org/repo/blob/master/catalog-info.yaml', }; }); @@ -292,7 +294,8 @@ describe('github-deployments', () => { entity = entityStub; entity.entity.metadata.annotations = { 'github.com/project-slug': 'org/repo', - 'backstage.io/managed-by-location': 'github:my-github-3.com', + 'backstage.io/managed-by-location': + 'github:https://my-github-3.com/org/repo/blob/master/catalog-info.yaml', }; }); @@ -305,7 +308,7 @@ describe('github-deployments', () => { expect( await rendered.findByText( - 'Warning: No apiBaseUrl available for host github:my-github-3.com, please check your integrations config', + 'Warning: No apiBaseUrl available for host my-github-3.com, please check your integrations config', ), ).toBeInTheDocument(); }); @@ -317,7 +320,7 @@ describe('github-deployments', () => { entity.entity.metadata.annotations = { 'github.com/project-slug': 'org/repo', 'backstage.io/managed-by-location': - 'github:my-github-unknown.com/org/repo', + 'github:https://my-github-unknown.com/org/repo/blob/master/catalog-info.yaml', }; }); @@ -330,7 +333,7 @@ describe('github-deployments', () => { expect( await rendered.findByText( - 'Warning: No matching GitHub integration configuration for host github:my-github-unknown.com/org/repo, please check your integrations config', + 'Warning: No matching GitHub integration configuration for host my-github-unknown.com, please check your integrations config', ), ).toBeInTheDocument(); }); @@ -342,15 +345,15 @@ describe('github-deployments', () => { entity.entity.metadata.annotations = { 'github.com/project-slug': 'org/repo', 'backstage.io/source-location': - 'url:my-favourite-url-location/org/repo', + 'url:https://my-github-1.com/org/repo/tree/master/', 'backstage.io/managed-by-location': - 'url:my-favourite-url-location/org/repo', + 'url:https://my-github-1.com/org/repo/blob/master/catalog-info.yaml', }; }); - it('displays fetched data using default github api', async () => { + it('should fetch data using custom api', async () => { worker.use( - GRAPHQL_GITHUB_API.query('deployments', (_, res, ctx) => + GRAPHQL_CUSTOM_API_1.query('deployments', (_, res, ctx) => res(ctx.data(responseStub)), ), ); From a286a639766df961348288135901efecb5e13c36 Mon Sep 17 00:00:00 2001 From: Bastian Gutschke Date: Wed, 12 May 2021 23:27:36 +0200 Subject: [PATCH 83/95] fix(badges-backend): properly build entity uri Signed-off-by: Bastian Gutschke --- .changeset/lucky-pens-ring.md | 5 +++++ plugins/badges-backend/src/badges.test.ts | 24 +++++++++++++++++++++++ plugins/badges-backend/src/badges.ts | 4 ++-- 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 .changeset/lucky-pens-ring.md diff --git a/.changeset/lucky-pens-ring.md b/.changeset/lucky-pens-ring.md new file mode 100644 index 0000000000..04b0749fd0 --- /dev/null +++ b/.changeset/lucky-pens-ring.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-badges-backend': patch +--- + +Fix generated badge links diff --git a/plugins/badges-backend/src/badges.test.ts b/plugins/badges-backend/src/badges.test.ts index 15be96154e..1d5e57141d 100644 --- a/plugins/badges-backend/src/badges.test.ts +++ b/plugins/badges-backend/src/badges.test.ts @@ -65,4 +65,28 @@ describe('BadgeFactories', () => { expect(badge.kind).toEqual('entity'); } }); + + it('returns valid link for entity', () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'service', + metadata: { + name: 'test', + }, + }; + + const context: BadgeContext = { + badgeUrl: '/dummy/url', + config, + entity, + }; + + expect.assertions(Object.keys(badgeFactories).length); + for (const badgeFactory of Object.values(badgeFactories)) { + const badge = badgeFactory.createBadge(context); + expect(badge.link).toContain( + 'http://localhost/catalog/default/service/test', + ); + } + }); }); diff --git a/plugins/badges-backend/src/badges.ts b/plugins/badges-backend/src/badges.ts index f8637dfe88..ccd5f55855 100644 --- a/plugins/badges-backend/src/badges.ts +++ b/plugins/badges-backend/src/badges.ts @@ -24,8 +24,8 @@ function appTitle(context: BadgeContext): string { function entityUrl(context: BadgeContext): string { const e = context.entity!; - const entityUri = `${e.kind}/${ - e.metadata.namespace || ENTITY_DEFAULT_NAMESPACE + const entityUri = `${e.metadata.namespace || ENTITY_DEFAULT_NAMESPACE}/${ + e.kind }/${e.metadata.name}`; const catalogUrl = `${context.config.getString('app.baseUrl')}/catalog`; return `${catalogUrl}/${entityUri}`.toLowerCase(); From 8012fd144c9eae308510f88df04460a8ce84747a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 May 2021 04:31:38 +0000 Subject: [PATCH 84/95] chore(deps-dev): bump husky from 4.3.8 to 6.0.0 Bumps [husky](https://github.com/typicode/husky) from 4.3.8 to 6.0.0. - [Release notes](https://github.com/typicode/husky/releases) - [Commits](https://github.com/typicode/husky/compare/v4.3.8...v6.0.0) Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 48 ++++-------------------------------------------- 2 files changed, 5 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index b0f79031de..c69a9c5dc5 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "command-exists": "^1.2.9", "concurrently": "^6.0.0", "fs-extra": "^9.0.0", - "husky": "^4.2.3", + "husky": "^6.0.0", "lerna": "^4.0.0", "lint-staged": "^10.1.0", "prettier": "^2.2.1", diff --git a/yarn.lock b/yarn.lock index 330463a73a..4c47ce3cb0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10098,11 +10098,6 @@ compare-func@^2.0.0: array-ify "^1.0.0" dot-prop "^5.1.0" -compare-versions@^3.6.0: - version "3.6.0" - resolved "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62" - integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== - component-emitter@^1.2.0, component-emitter@^1.2.1, component-emitter@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -13431,13 +13426,6 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -find-versions@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/find-versions/-/find-versions-4.0.0.tgz#3c57e573bf97769b8cb8df16934b627915da4965" - integrity sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ== - dependencies: - semver-regex "^3.1.2" - find-yarn-workspace-root2@1.2.16: version "1.2.16" resolved "https://registry.npmjs.org/find-yarn-workspace-root2/-/find-yarn-workspace-root2-1.2.16.tgz#60287009dd2f324f59646bdb4b7610a6b301c2a9" @@ -15126,21 +15114,10 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -husky@^4.2.3: - version "4.3.8" - resolved "https://registry.npmjs.org/husky/-/husky-4.3.8.tgz#31144060be963fd6850e5cc8f019a1dfe194296d" - integrity sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow== - dependencies: - chalk "^4.0.0" - ci-info "^2.0.0" - compare-versions "^3.6.0" - cosmiconfig "^7.0.0" - find-versions "^4.0.0" - opencollective-postinstall "^2.0.2" - pkg-dir "^5.0.0" - please-upgrade-node "^3.2.0" - slash "^3.0.0" - which-pm-runs "^1.0.0" +husky@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/husky/-/husky-6.0.0.tgz#810f11869adf51604c32ea577edbc377d7f9319e" + integrity sha512-SQS2gDTB7tBN486QSoKPKQItZw97BMOd+Kdb6ghfpBc0yXyzrddI0oDV5MkDAbuB4X2mO3/nj60TRMcYxwzZeQ== hyphenate-style-name@^1.0.2, hyphenate-style-name@^1.0.3: version "1.0.3" @@ -19877,11 +19854,6 @@ openapi-sampler@^1.0.0-beta.15: dependencies: json-pointer "^0.6.0" -opencollective-postinstall@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89" - integrity sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw== - openid-client@^4.1.1, openid-client@^4.2.1: version "4.2.1" resolved "https://registry.npmjs.org/openid-client/-/openid-client-4.2.1.tgz#8200c0ab6a3b8e954727dfa790847dc5cb8999c2" @@ -20824,13 +20796,6 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -pkg-dir@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760" - integrity sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA== - dependencies: - find-up "^5.0.0" - pkg-up@3.1.0, pkg-up@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" @@ -23533,11 +23498,6 @@ semver-diff@^3.1.1: dependencies: semver "^6.3.0" -semver-regex@^3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.2.tgz#34b4c0d361eef262e07199dbef316d0f2ab11807" - integrity sha512-bXWyL6EAKOJa81XG1OZ/Yyuq+oT0b2YLlxx7c+mrdYPaPbnj6WgVULXhinMIeZGufuUBu/eVRqXEhiv4imfwxA== - "semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" From 31f17e07d633c9ca10888cb3442ca92adea2aa9c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 May 2021 04:34:15 +0000 Subject: [PATCH 85/95] chore(deps): bump openid-client from 4.2.1 to 4.7.3 Bumps [openid-client](https://github.com/panva/node-openid-client) from 4.2.1 to 4.7.3. - [Release notes](https://github.com/panva/node-openid-client/releases) - [Changelog](https://github.com/panva/node-openid-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/panva/node-openid-client/compare/v4.2.1...v4.7.3) Signed-off-by: dependabot[bot] --- yarn.lock | 57 +++++++++++++++++++++++-------------------------------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/yarn.lock b/yarn.lock index 330463a73a..5a8f12ed44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7440,7 +7440,7 @@ agentkeepalive@^4.1.3: depd "^1.1.2" humanize-ms "^1.2.1" -aggregate-error@3.0.1, aggregate-error@^3.0.0: +aggregate-error@3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz#db2fe7246e536f40d9b5442a39e117d7dd6a24e0" integrity sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA== @@ -7448,6 +7448,14 @@ aggregate-error@3.0.1, aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" +aggregate-error@^3.0.0, aggregate-error@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + airbnb-js-shims@^2.2.1: version "2.2.1" resolved "https://registry.npmjs.org/airbnb-js-shims/-/airbnb-js-shims-2.2.1.tgz#db481102d682b98ed1daa4c5baa697a05ce5c040" @@ -8667,7 +8675,7 @@ base64-js@^1.0.2, base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1: resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -base64url@3.x.x, base64url@^3.0.1: +base64url@3.x.x: version "3.0.1" resolved "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== @@ -16733,10 +16741,10 @@ jose@^1.27.1: dependencies: "@panva/asn1.js" "^1.0.0" -jose@^2.0.2: - version "2.0.3" - resolved "https://registry.npmjs.org/jose/-/jose-2.0.3.tgz#9c931ab3e13e2d16a5b9e6183e60b2fc40a8e1b8" - integrity sha512-L+RlDgjO0Tk+Ki6/5IXCSEnmJCV8iMFZoBuEgu2vPQJJ4zfG/k3CAqZUMKDYNRHIDyy0QidJpOvX0NgpsAqFlw== +jose@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz#29746a18d9fff7dcf9d5d2a6f62cb0c7cd27abd3" + integrity sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA== dependencies: "@panva/asn1.js" "^1.0.0" @@ -19812,10 +19820,10 @@ octokit-plugin-create-pull-request@^3.9.3: dependencies: "@octokit/types" "^6.8.2" -oidc-token-hash@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.0.tgz#acdfb1f4310f58e64d5d74a4e8671a426986e888" - integrity sha512-8Yr4CZSv+Tn8ZkN3iN2i2w2G92mUKClp4z7EGUfdsERiYSbj7P4i/NHm72ft+aUdsiFx9UdIPSTwbyzQ6C4URg== +oidc-token-hash@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz#ae6beec3ec20f0fd885e5400d175191d6e2f10c6" + integrity sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ== on-finished@~2.3.0: version "2.3.0" @@ -19883,18 +19891,17 @@ opencollective-postinstall@^2.0.2: integrity sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw== openid-client@^4.1.1, openid-client@^4.2.1: - version "4.2.1" - resolved "https://registry.npmjs.org/openid-client/-/openid-client-4.2.1.tgz#8200c0ab6a3b8e954727dfa790847dc5cb8999c2" - integrity sha512-07eOcJeMH3ZHNvx5DVMZQmy3vZSTQqKSSunbtM1pXb+k5LBPi5hMum1vJCFReXlo4wuLEqZ/OgbsZvXPhbGRtA== + version "4.7.3" + resolved "https://registry.npmjs.org/openid-client/-/openid-client-4.7.3.tgz#ea4f6f9ff6203dfbbe3d9bb4415d5dce751b0a70" + integrity sha512-YLwZQLSjo3gdSVxw/G25ddoRp9oCpXkREZXssmenlejZQPsnTq+yQtFUcBmC7u3VVkx+gwqXZF7X0CtAAJrRRg== dependencies: - base64url "^3.0.1" + aggregate-error "^3.1.0" got "^11.8.0" - jose "^2.0.2" + jose "^2.0.5" lru-cache "^6.0.0" make-error "^1.3.6" object-hash "^2.0.1" - oidc-token-hash "^5.0.0" - p-any "^3.0.0" + oidc-token-hash "^5.0.1" opn@^5.5.0: version "5.5.0" @@ -20009,14 +20016,6 @@ p-all@^2.1.0: dependencies: p-map "^2.0.0" -p-any@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/p-any/-/p-any-3.0.0.tgz#79847aeed70b5d3a10ea625296c0c3d2e90a87b9" - integrity sha512-5rqbqfsRWNb0sukt0awwgJMlaep+8jV45S15SKKB34z4UuzjcofIfnriCBhWjZP2jbVtjt9yRl7buB6RlKsu9w== - dependencies: - p-cancelable "^2.0.0" - p-some "^5.0.0" - p-cancelable@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" @@ -20161,14 +20160,6 @@ p-retry@^3.0.1: dependencies: retry "^0.12.0" -p-some@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/p-some/-/p-some-5.0.0.tgz#8b730c74b4fe5169d7264a240ad010b6ebc686a4" - integrity sha512-Js5XZxo6vHjB9NOYAzWDYAIyyiPvva0DWESAIWIK7uhSpGsyg5FwUPxipU/SOQx5x9EqhOh545d1jo6cVkitig== - dependencies: - aggregate-error "^3.0.0" - p-cancelable "^2.0.0" - p-timeout@^3.1.0, p-timeout@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" From c6cc32b64eca164bffe04e1536940f6535cff6ad Mon Sep 17 00:00:00 2001 From: yacut Date: Thu, 13 May 2021 20:24:22 +0200 Subject: [PATCH 86/95] fix config schema for ilert Signed-off-by: yacut --- plugins/ilert/config.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ilert/config.d.ts b/plugins/ilert/config.d.ts index 52712454e8..6bf3f569ab 100644 --- a/plugins/ilert/config.d.ts +++ b/plugins/ilert/config.d.ts @@ -14,7 +14,7 @@ * limitations under the License. */ export interface Config { - ilert: { + ilert?: { /** * Domain used by users to access iLert web UI. * Example: https://my-app.ilert.com/ From fadb6c9dfa348f5c9563f1c43a07e10b5901d444 Mon Sep 17 00:00:00 2001 From: yacut Date: Thu, 13 May 2021 21:38:32 +0200 Subject: [PATCH 87/95] upgrade dependencies for ilert package Signed-off-by: yacut --- plugins/ilert/package.json | 14 +++++++------- yarn.lock | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index 31eff40dee..24376e03a8 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -21,10 +21,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.6", - "@backstage/core": "^0.7.7", + "@backstage/core": "^0.7.9", "@backstage/errors": "^0.1.1", "@backstage/plugin-catalog-react": "^0.1.4", - "@backstage/theme": "^0.2.6", + "@backstage/theme": "^0.2.7", "@date-io/luxon": "1.x", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -34,15 +34,15 @@ "luxon": "^1.26.0", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-use": "^15.3.3" + "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.6.9", - "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.10", + "@backstage/cli": "^0.6.11", + "@backstage/dev-utils": "^0.1.14", + "@backstage/test-utils": "^0.1.11", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^12.0.7", + "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", diff --git a/yarn.lock b/yarn.lock index 330463a73a..76856b26b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1951,6 +1951,13 @@ dependencies: "@date-io/core" "^1.3.13" +"@date-io/luxon@1.x": + version "1.3.13" + resolved "https://registry.npmjs.org/@date-io/luxon/-/luxon-1.3.13.tgz#68f0134bb38ef486b2ed6df01981f814c633e28a" + integrity sha512-9wUrJCNSMZJeYAiH+dbb45oGpnHeFP7TOH/Lt26If47gjFCkjvyINzWx+K5AGsnlP0Qosxc7hkF1yLi6ecutxw== + dependencies: + "@date-io/core" "^1.3.13" + "@emotion/cache@^10.0.27": version "10.0.29" resolved "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0" @@ -3729,6 +3736,18 @@ react-transition-group "^4.0.0" rifm "^0.7.0" +"@material-ui/pickers@^3.3.10": + version "3.3.10" + resolved "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.3.10.tgz#f1b0f963348cc191645ef0bdeff7a67c6aa25485" + integrity sha512-hS4pxwn1ZGXVkmgD4tpFpaumUaAg2ZzbTrxltfC5yPw4BJV+mGkfnQOB4VpWEYZw2jv65Z0wLwDE/piQiPPZ3w== + dependencies: + "@babel/runtime" "^7.6.0" + "@date-io/core" "1.x" + "@types/styled-jsx" "^2.2.8" + clsx "^1.0.2" + react-transition-group "^4.0.0" + rifm "^0.7.0" + "@material-ui/styles@^4.10.0", "@material-ui/styles@^4.11.0", "@material-ui/styles@^4.11.3", "@material-ui/styles@^4.9.6": version "4.11.3" resolved "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.3.tgz#1b8d97775a4a643b53478c895e3f2a464e8916f2" @@ -15119,6 +15138,11 @@ humanize-duration@^3.25.1: resolved "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.25.1.tgz#50e12bf4b3f515ec91106107ee981e8cfe955d6f" integrity sha512-P+dRo48gpLgc2R9tMRgiDRNULPKCmqFYgguwqOO2C0fjO35TgdURDQDANSR1Nt92iHlbHGMxOTnsB8H8xnMa2Q== +humanize-duration@^3.26.0: + version "3.26.0" + resolved "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.26.0.tgz#4d77f6b3d2fe0ca1ff14623ccc2b2f8b48ab1aaf" + integrity sha512-SddekX3p5ApvPY6bbAYppGKe874jP6iFZXYtrQToDV4R0j2UpTYPqwTFM2QpXpuw9DhS/eXTUnKYTF9TbXAJ6A== + humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" From b20a44b5218f0e341036279fcc33fd8faeabcfc2 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Fri, 14 May 2021 09:42:28 -0400 Subject: [PATCH 88/95] Change of company Signed-off-by: Adam Harvey --- OWNERS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OWNERS.md b/OWNERS.md index a79328dc20..cdbc234183 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -27,5 +27,5 @@ People that have made significant contributions to the project and earned write - Andrew Thauer - Wealthsimple (GitHub: [andrewthauer](https://github.com/andrewthauer)) - Oliver Sand - SDA SE (GitHub: [Fox32](https://github.com/Fox32)) - David Tuite - Roadie (GitHub: [dtuite](https://github.com/dtuite)) -- Adam Harvey - DXC Technology (GitHub: [adamdmharvey](https://github.com/adamdmharvey)) +- Adam Harvey - Cisco (GitHub: [adamdmharvey](https://github.com/adamdmharvey)) - Dominik Henneke - SDA SE (GitHub: [dhenneke](https://github.com/dhenneke)) From 8c78340cd38343abbdd86073bb9a85cc4fe83204 Mon Sep 17 00:00:00 2001 From: ImgBotApp Date: Mon, 17 May 2021 08:17:52 +0000 Subject: [PATCH 89/95] [ImgBot] Optimize images *Total -- 83.37kb -> 63.48kb (23.86%) /plugins/git-release-manager/src/features/Info/flow.png -- 76.89kb -> 57.33kb (25.44%) /plugins/ilert/src/assets/ilert.icon.svg -- 2.51kb -> 2.36kb (6.25%) /microsite/static/img/git-release-manager-logo.svg -- 3.96kb -> 3.79kb (4.24%) Signed-off-by: ImgBotApp --- .../static/img/git-release-manager-logo.svg | 14 +------------- .../src/features/Info/flow.png | Bin 78736 -> 58704 bytes plugins/ilert/src/assets/ilert.icon.svg | 12 +----------- 3 files changed, 2 insertions(+), 24 deletions(-) diff --git a/microsite/static/img/git-release-manager-logo.svg b/microsite/static/img/git-release-manager-logo.svg index b505d21560..b8e9cfe3ac 100644 --- a/microsite/static/img/git-release-manager-logo.svg +++ b/microsite/static/img/git-release-manager-logo.svg @@ -1,13 +1 @@ - - - Export - - - - - - - - - - \ No newline at end of file +Export \ No newline at end of file diff --git a/plugins/git-release-manager/src/features/Info/flow.png b/plugins/git-release-manager/src/features/Info/flow.png index e70df0d7faaa4944671e29177a0377fa07f6506a..04fe5bbe34fdbebdb697b5cbd205b3231937fe8a 100644 GIT binary patch literal 58704 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 E06HV!P5=M^ literal 78736 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 C@nBW} diff --git a/plugins/ilert/src/assets/ilert.icon.svg b/plugins/ilert/src/assets/ilert.icon.svg index e93f1e80ca..5109286355 100644 --- a/plugins/ilert/src/assets/ilert.icon.svg +++ b/plugins/ilert/src/assets/ilert.icon.svg @@ -1,11 +1 @@ - - - - + \ No newline at end of file From 9d7aba600fb3568f05974fbf6284a338a308692f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 May 2021 08:20:30 +0000 Subject: [PATCH 90/95] chore(deps): bump @material-ui/styles from 4.11.3 to 4.11.4 Bumps [@material-ui/styles](https://github.com/mui-org/material-ui/tree/HEAD/packages/material-ui-styles) from 4.11.3 to 4.11.4. - [Release notes](https://github.com/mui-org/material-ui/releases) - [Changelog](https://github.com/mui-org/material-ui/blob/v4.11.4/CHANGELOG.md) - [Commits](https://github.com/mui-org/material-ui/commits/v4.11.4/packages/material-ui-styles) Signed-off-by: dependabot[bot] --- yarn.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index 76856b26b9..b17b91845d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3749,13 +3749,13 @@ rifm "^0.7.0" "@material-ui/styles@^4.10.0", "@material-ui/styles@^4.11.0", "@material-ui/styles@^4.11.3", "@material-ui/styles@^4.9.6": - version "4.11.3" - resolved "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.3.tgz#1b8d97775a4a643b53478c895e3f2a464e8916f2" - integrity sha512-HzVzCG+PpgUGMUYEJ2rTEmQYeonGh41BYfILNFb/1ueqma+p1meSdu4RX6NjxYBMhf7k+jgfHFTTz+L1SXL/Zg== + version "4.11.4" + resolved "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz#eb9dfccfcc2d208243d986457dff025497afa00d" + integrity sha512-KNTIZcnj/zprG5LW0Sao7zw+yG3O35pviHzejMdcSGCdWbiO8qzRgOYL8JAxAsWBKOKYwVZxXtHWaB5T2Kvxew== dependencies: "@babel/runtime" "^7.4.4" "@emotion/hash" "^0.8.0" - "@material-ui/types" "^5.1.0" + "@material-ui/types" "5.1.0" "@material-ui/utils" "^4.11.2" clsx "^1.0.4" csstype "^2.5.2" @@ -3780,7 +3780,7 @@ csstype "^2.5.2" prop-types "^15.7.2" -"@material-ui/types@^5.1.0": +"@material-ui/types@5.1.0", "@material-ui/types@^5.1.0": version "5.1.0" resolved "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz#efa1c7a0b0eaa4c7c87ac0390445f0f88b0d88f2" integrity sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A== From f915a342d1d6717c98d4fdfa0d3f71d15a0fb87f Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Mon, 17 May 2021 10:37:03 +0200 Subject: [PATCH 91/95] chore: optimize images Signed-off-by: Ben Lambert --- .changeset/shaggy-glasses-lie.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/shaggy-glasses-lie.md diff --git a/.changeset/shaggy-glasses-lie.md b/.changeset/shaggy-glasses-lie.md new file mode 100644 index 0000000000..89570a93c2 --- /dev/null +++ b/.changeset/shaggy-glasses-lie.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-git-release-manager': patch +'@backstage/plugin-ilert': patch +--- + +[ImgBot] Optimize images From 21ebf8dd5e3b600c2af407259adf57cb78a8e6c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 17 May 2021 10:47:08 +0200 Subject: [PATCH 92/95] address comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../software-catalog/descriptor-format.md | 8 +++---- .../software-catalog/well-known-statuses.md | 4 ++-- .../catalog-backend/src/next/Stitcher.test.ts | 8 ++----- plugins/catalog-backend/src/next/Stitcher.ts | 22 +++++++++++++------ 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 665c729191..699574b986 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -417,7 +417,7 @@ follows. { // ... "status": { - "backstage.io/processing-status": { + "backstage.io/catalog-processing": { "errors": [] } }, @@ -428,9 +428,9 @@ follows. ``` The keys of the `status` object are arbitrary strings. We recommend that any -statuses, that are not strictly private within the organization, be namespaced -to avoid collisions. Statuses emitted by Backstage core processes will for -example be prefixed with `backstage.io/` as in the example above. +statuses that are not strictly private within the organization be namespaced to +avoid collisions. Statuses emitted by Backstage core processes will for example +be prefixed with `backstage.io/` as in the example above. The values of the `status` object are currently left unrestricted, except that they must be objects. We reserve the right to extend this model in the future, diff --git a/docs/features/software-catalog/well-known-statuses.md b/docs/features/software-catalog/well-known-statuses.md index a516c61daf..7ddf320d26 100644 --- a/docs/features/software-catalog/well-known-statuses.md +++ b/docs/features/software-catalog/well-known-statuses.md @@ -27,7 +27,7 @@ a standard concept of "severity" or "level" to these. This is a (non-exhaustive) list of statuses that are known to be in active use. -### `backstage.io/processing-status` +### `backstage.io/catalog-processing` Contains the current status of the catalog's ingestion of this entity. Errors that may appear here include inability to read from the remote SCM provider, @@ -42,7 +42,7 @@ successfully ingested. This is normal. ```yaml # Example: status: - backstage.io/processing-status: + backstage.io/catalog-processing: errors: [] ``` diff --git a/plugins/catalog-backend/src/next/Stitcher.test.ts b/plugins/catalog-backend/src/next/Stitcher.test.ts index da1b79600a..644284f3e4 100644 --- a/plugins/catalog-backend/src/next/Stitcher.test.ts +++ b/plugins/catalog-backend/src/next/Stitcher.test.ts @@ -92,9 +92,7 @@ describe('Stitcher', () => { }, ], status: { - 'backstage.io/processing-status': { - errors: [], - }, + 'backstage.io/catalog-processing': {}, }, apiVersion: 'a', kind: 'k', @@ -177,9 +175,7 @@ describe('Stitcher', () => { }, ]), status: { - 'backstage.io/processing-status': { - errors: [], - }, + 'backstage.io/catalog-processing': {}, }, apiVersion: 'a', kind: 'k', diff --git a/plugins/catalog-backend/src/next/Stitcher.ts b/plugins/catalog-backend/src/next/Stitcher.ts index 8cf96c6b7f..d7d4325ad0 100644 --- a/plugins/catalog-backend/src/next/Stitcher.ts +++ b/plugins/catalog-backend/src/next/Stitcher.ts @@ -15,6 +15,7 @@ */ import { Entity, parseEntityRef } from '@backstage/catalog-model'; +import { JsonObject } from '@backstage/config'; import { ConflictError } from '@backstage/errors'; import { createHash } from 'crypto'; import stableStringify from 'fast-json-stable-stringify'; @@ -35,6 +36,10 @@ export type DbFinalEntitiesRow = { final_entity: string; }; +type ProcessingStatus = { + errors?: JsonObject[]; +}; + function generateStableHash(entity: Entity) { return createHash('sha1') .update(stableStringify({ ...entity })) @@ -134,6 +139,7 @@ export class Stitcher { // it const entity = JSON.parse(processedEntity) as Entity; const isOrphan = Number(incomingReferenceCount) === 0; + const processingStatus: ProcessingStatus = {}; if (isOrphan) { this.logger.debug(`${entityRef} is an orphan`); @@ -142,15 +148,13 @@ export class Stitcher { ['backstage.io/orphan']: 'true', }; } - if (errors !== '') { + if (errors) { const parsedErrors = JSON.parse(errors); - entity.status = { - ...entity.status, - 'backstage.io/processing-status': { - errors: parsedErrors, - }, - }; + if (Array.isArray(parsedErrors) && parsedErrors.length) { + processingStatus.errors = parsedErrors; + } } + // TODO: entityRef is lower case and should be uppercase in the final // result entity.relations = result @@ -159,6 +163,10 @@ export class Stitcher { type: row.relationType!, target: parseEntityRef(row.relationTarget!), })); + entity.status = { + ...entity.status, + 'backstage.io/catalog-processing': processingStatus, + }; // If the output entity was actually not changed, just abort const hash = generateStableHash(entity); From aa19959b40033a1f1b75a06b14a1487847247541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 17 May 2021 11:26:00 +0200 Subject: [PATCH 93/95] migrate husky to v6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .husky/.gitignore | 1 + .husky/pre-commit | 4 ++++ package.json | 8 ++------ 3 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 .husky/.gitignore create mode 100755 .husky/pre-commit diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 0000000000..31354ec138 --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000000..d2ae35e84b --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn lint-staged diff --git a/package.json b/package.json index c69a9c5dc5..ed404565e4 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "prettier:check": "prettier --check .", "lerna": "lerna", "storybook": "yarn workspace storybook start", - "build-storybook": "yarn workspace storybook build-storybook" + "build-storybook": "yarn workspace storybook build-storybook", + "prepare": "husky install" }, "workspaces": { "packages": [ @@ -67,11 +68,6 @@ "recursive-readdir": "^2.2.2", "shx": "^0.3.2" }, - "husky": { - "hooks": { - "pre-commit": "lint-staged" - } - }, "prettier": "@spotify/prettier-config", "lint-staged": { "*.{js,jsx,ts,tsx}": [ From 290405276027ba0e34683a27636dd12b90630833 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 May 2021 04:32:27 +0000 Subject: [PATCH 94/95] chore(deps): bump json-schema-merge-allof from 0.7.0 to 0.8.1 Bumps [json-schema-merge-allof](https://github.com/mokkabonna/json-schema-merge-allof) from 0.7.0 to 0.8.1. - [Release notes](https://github.com/mokkabonna/json-schema-merge-allof/releases) - [Commits](https://github.com/mokkabonna/json-schema-merge-allof/commits) Signed-off-by: dependabot[bot] --- .changeset/purple-hairs-kick.md | 5 +++++ packages/config-loader/package.json | 2 +- yarn.lock | 30 ++++++++++++++--------------- 3 files changed, 21 insertions(+), 16 deletions(-) create mode 100644 .changeset/purple-hairs-kick.md diff --git a/.changeset/purple-hairs-kick.md b/.changeset/purple-hairs-kick.md new file mode 100644 index 0000000000..6e6d698d07 --- /dev/null +++ b/.changeset/purple-hairs-kick.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +Updated dependencies diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 16d7923d0b..77646ee521 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -36,7 +36,7 @@ "ajv": "^7.0.3", "fs-extra": "^9.0.0", "json-schema": "^0.3.0", - "json-schema-merge-allof": "^0.7.0", + "json-schema-merge-allof": "^0.8.1", "typescript-json-schema": "^0.49.0", "yaml": "^1.9.2", "yup": "^0.29.3" diff --git a/yarn.lock b/yarn.lock index 330463a73a..8312d0f042 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10138,21 +10138,21 @@ compression@^1.7.4: safe-buffer "5.1.2" vary "~1.1.2" -compute-gcd@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/compute-gcd/-/compute-gcd-1.2.0.tgz#fc1ede5b65001e950226502f46543863e4fea10e" - integrity sha1-/B7eW2UAHpUCJlAvRlQ4Y+T+oQ4= +compute-gcd@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/compute-gcd/-/compute-gcd-1.2.1.tgz#34d639f3825625e1357ce81f0e456a6249d8c77f" + integrity sha512-TwMbxBNz0l71+8Sc4czv13h4kEqnchV9igQZBi6QUaz09dnz13juGnnaWWJTRsP3brxOoxeB4SA2WELLw1hCtg== dependencies: validate.io-array "^1.0.3" validate.io-function "^1.0.2" validate.io-integer-array "^1.0.0" -compute-lcm@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/compute-lcm/-/compute-lcm-1.1.0.tgz#abd96d040b41b0a166f89944b5c8b7c511e21ad5" - integrity sha1-q9ltBAtBsKFm+JlEtci3xRHiGtU= +compute-lcm@^1.1.0, compute-lcm@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/compute-lcm/-/compute-lcm-1.1.2.tgz#9107c66b9dca28cefb22b4ab4545caac4034af23" + integrity sha512-OFNPdQAXnQhDSKioX8/XYT6sdUlXwpeMjfd6ApxMJfyZ4GxmLR1xvMERctlYhlHwIiz6CSpBc2+qYKjHGZw4TQ== dependencies: - compute-gcd "^1.2.0" + compute-gcd "^1.2.1" validate.io-array "^1.0.3" validate.io-function "^1.0.2" validate.io-integer-array "^1.0.0" @@ -16934,14 +16934,14 @@ json-schema-merge-allof@^0.6.0: json-schema-compare "^0.2.2" lodash "^4.17.4" -json-schema-merge-allof@^0.7.0: - version "0.7.0" - resolved "https://registry.npmjs.org/json-schema-merge-allof/-/json-schema-merge-allof-0.7.0.tgz#84d3e8c3e03d3060014286958eb8834fa9d76304" - integrity sha512-kvsuSVnl1n5xnNEu5ed4o8r8ujSA4/IgRtHmpgfMfa7FOMIRAzN4F9qbuklouTn5J8bi83y6MQ11n+ERMMTXZg== +json-schema-merge-allof@^0.8.1: + version "0.8.1" + resolved "https://registry.npmjs.org/json-schema-merge-allof/-/json-schema-merge-allof-0.8.1.tgz#ed2828cdd958616ff74f932830a26291789eaaf2" + integrity sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w== dependencies: - compute-lcm "^1.1.0" + compute-lcm "^1.1.2" json-schema-compare "^0.2.2" - lodash "^4.17.4" + lodash "^4.17.20" json-schema-traverse@^0.4.1: version "0.4.1" From 62579ced64b97fb84b62515a39019409c84f3c79 Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Mon, 17 May 2021 11:54:34 +0200 Subject: [PATCH 95/95] Skip adding entries to the entities_search table if their key exceeds a length limit. Signed-off-by: Dominik Henneke --- .changeset/shaggy-melons-destroy.md | 5 +++++ plugins/catalog-backend/src/database/search.test.ts | 6 ++++++ plugins/catalog-backend/src/database/search.ts | 3 ++- plugins/catalog-backend/src/next/search.test.ts | 6 ++++++ plugins/catalog-backend/src/next/search.ts | 3 ++- 5 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 .changeset/shaggy-melons-destroy.md diff --git a/.changeset/shaggy-melons-destroy.md b/.changeset/shaggy-melons-destroy.md new file mode 100644 index 0000000000..d202cab6cd --- /dev/null +++ b/.changeset/shaggy-melons-destroy.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Skip adding entries to the `entities_search` table if their `key` exceeds a length limit. diff --git a/plugins/catalog-backend/src/database/search.test.ts b/plugins/catalog-backend/src/database/search.test.ts index c6793cbdf4..656da8b6f5 100644 --- a/plugins/catalog-backend/src/database/search.test.ts +++ b/plugins/catalog-backend/src/database/search.test.ts @@ -101,6 +101,12 @@ describe('search', () => { expect(output).toEqual([{ entity_id: 'eid', key: 'foo', value: 'bar' }]); }); + it('skips very large keys', () => { + const input = [{ key: 'a'.repeat(10000), value: 'foo' }]; + const output = mapToRows(input, 'eid'); + expect(output).toEqual([]); + }); + it('skips very large values', () => { const input = [{ key: 'foo', value: 'a'.repeat(10000) }]; const output = mapToRows(input, 'eid'); diff --git a/plugins/catalog-backend/src/database/search.ts b/plugins/catalog-backend/src/database/search.ts index 9e58d2468b..67b6d14b4f 100644 --- a/plugins/catalog-backend/src/database/search.ts +++ b/plugins/catalog-backend/src/database/search.ts @@ -31,6 +31,7 @@ const SPECIAL_KEYS = [ // The maximum length allowed for search values. These columns are indexed, and // database engines do not like to index on massive values. For example, // postgres will balk after 8191 byte line sizes. +const MAX_KEY_LENGTH = 200; const MAX_VALUE_LENGTH = 200; type Kv = { @@ -136,7 +137,7 @@ export function mapToRows( result.push({ entity_id: entityId, key, value: null }); } else { const value = String(rawValue).toLowerCase(); - if (value.length <= MAX_VALUE_LENGTH) { + if (key.length <= MAX_KEY_LENGTH && value.length <= MAX_VALUE_LENGTH) { result.push({ entity_id: entityId, key, value }); } } diff --git a/plugins/catalog-backend/src/next/search.test.ts b/plugins/catalog-backend/src/next/search.test.ts index 2e0b07b4a9..be896319b7 100644 --- a/plugins/catalog-backend/src/next/search.test.ts +++ b/plugins/catalog-backend/src/next/search.test.ts @@ -104,6 +104,12 @@ describe('search', () => { expect(output).toEqual([{ entity_id: 'eid', key: 'foo', value: 'bar' }]); }); + it('skips very large keys', () => { + const input = [{ key: 'a'.repeat(10000), value: 'foo' }]; + const output = mapToRows(input, 'eid'); + expect(output).toEqual([]); + }); + it('skips very large values', () => { const input = [{ key: 'foo', value: 'a'.repeat(10000) }]; const output = mapToRows(input, 'eid'); diff --git a/plugins/catalog-backend/src/next/search.ts b/plugins/catalog-backend/src/next/search.ts index 383b29fd70..0681bffa17 100644 --- a/plugins/catalog-backend/src/next/search.ts +++ b/plugins/catalog-backend/src/next/search.ts @@ -43,6 +43,7 @@ const SPECIAL_KEYS = [ // The maximum length allowed for search values. These columns are indexed, and // database engines do not like to index on massive values. For example, // postgres will balk after 8191 byte line sizes. +const MAX_KEY_LENGTH = 200; const MAX_VALUE_LENGTH = 200; type Kv = { @@ -145,7 +146,7 @@ export function mapToRows(input: Kv[], entityId: string): DbSearchRow[] { result.push({ entity_id: entityId, key, value: null }); } else { const value = String(rawValue).toLocaleLowerCase('en-US'); - if (value.length <= MAX_VALUE_LENGTH) { + if (key.length <= MAX_KEY_LENGTH && value.length <= MAX_VALUE_LENGTH) { result.push({ entity_id: entityId, key, value }); } }