From 8cd6f68b6f15e6ed50e8f18d398f83076552b275 Mon Sep 17 00:00:00 2001 From: "althaf.hameez" Date: Thu, 6 Aug 2020 02:43:35 +0800 Subject: [PATCH 01/26] Define a simple Group Entity --- packages/catalog-model/src/EntityPolicies.ts | 2 + .../src/kinds/GroupEntityV1alpha1.test.ts | 116 ++++++++++++++++++ .../src/kinds/GroupEntityV1alpha1.ts | 60 +++++++++ packages/catalog-model/src/kinds/index.ts | 5 + 4 files changed, 183 insertions(+) create mode 100644 packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts create mode 100644 packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts diff --git a/packages/catalog-model/src/EntityPolicies.ts b/packages/catalog-model/src/EntityPolicies.ts index 3dc7deae7c..120de5152e 100644 --- a/packages/catalog-model/src/EntityPolicies.ts +++ b/packages/catalog-model/src/EntityPolicies.ts @@ -24,6 +24,7 @@ import { } from './entity'; import { ComponentEntityV1alpha1Policy, + GroupEntityV1alpha1Policy, LocationEntityV1alpha1Policy, TemplateEntityV1alpha1Policy, } from './kinds'; @@ -74,6 +75,7 @@ export class EntityPolicies implements EntityPolicy { ]), EntityPolicies.anyOf([ new ComponentEntityV1alpha1Policy(), + new GroupEntityV1alpha1Policy(), new LocationEntityV1alpha1Policy(), new TemplateEntityV1alpha1Policy(), ]), diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts new file mode 100644 index 0000000000..c0a98685e9 --- /dev/null +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts @@ -0,0 +1,116 @@ +/* + * 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 { EntityPolicy } from '../types'; +import { + GroupEntityV1alpha1, + GroupEntityV1alpha1Policy, +} from './GroupEntityV1alpha1'; + +describe('GroupV1alpha1Policy', () => { + let entity: GroupEntityV1alpha1; + let policy: EntityPolicy; + + beforeEach(() => { + entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'doe-squad', + title: 'Doe Squad', + description: 'A squad for John and Jane', + }, + spec: { + type: 'squad', + owner: 'me', + parent: ['group a'], + ancestors: ['group-a', 'global-synergies', 'acme-corp'], + children: [], + grandchildren: [], + }, + }; + policy = new GroupEntityV1alpha1Policy(); + }); + + it('happy path: accepts valid data', async () => { + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('silently accepts v1beta1 as well', async () => { + (entity as any).apiVersion = 'backstage.io/v1beta1'; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('rejects unknown apiVersion', async () => { + (entity as any).apiVersion = 'backstage.io/v1beta0'; + await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/); + }); + + it('rejects unknown kind', async () => { + (entity as any).kind = 'Wizard'; + await expect(policy.enforce(entity)).rejects.toThrow(/kind/); + }); + + it('rejects missing type', async () => { + delete (entity as any).spec.type; + await expect(policy.enforce(entity)).rejects.toThrow(/type/); + }); + + it('rejects wrong type', async () => { + (entity as any).spec.type = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/type/); + }); + + it('rejects empty type', async () => { + (entity as any).spec.type = ''; + await expect(policy.enforce(entity)).rejects.toThrow(/type/); + }); + + it('rejects missing owner', async () => { + delete (entity as any).spec.owner; + await expect(policy.enforce(entity)).rejects.toThrow(/owner/); + }); + + it('rejects wrong owner', async () => { + (entity as any).spec.owner = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/owner/); + }); + + it('rejects empty owner', async () => { + (entity as any).spec.owner = ''; + await expect(policy.enforce(entity)).rejects.toThrow(/owner/); + }); + + it('accepts missing parent', async () => { + delete (entity as any).spec.parent; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('accepts missing ancestors', async () => { + delete (entity as any).spec.ancestors; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('accepts missing children', async () => { + delete (entity as any).spec.children; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('accepts missing grandchildren', async () => { + delete (entity as any).spec.grandchildren; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); +}); diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts new file mode 100644 index 0000000000..f7902dbd86 --- /dev/null +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts @@ -0,0 +1,60 @@ +/* + * 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 * as yup from 'yup'; +import type { Entity } from '../entity/Entity'; +import type { EntityPolicy } from '../types'; + +const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; +const KIND = 'Group' as const; + +export interface GroupEntityV1alpha1 extends Entity { + apiVersion: typeof API_VERSION[number]; + kind: typeof KIND; + spec: { + type: string; + owner: string; + parent?: string[]; + ancestors?: string[]; + children?: string[]; + grandchildren?: string[]; + }; +} + +export class GroupEntityV1alpha1Policy implements EntityPolicy { + private schema: yup.Schema; + + constructor() { + this.schema = yup.object>({ + apiVersion: yup.string().required().oneOf(API_VERSION), + kind: yup.string().required().equals([KIND]), + spec: yup + .object({ + type: yup.string().required().min(1), + owner: yup.string().required().min(1), + parent: yup.array(yup.string()).notRequired(), + ancestors: yup.array(yup.string()).notRequired(), + children: yup.array(yup.string()).notRequired(), + grandchildren: yup.array(yup.string()).notRequired(), + }) + .required(), + }); + } + + async enforce(envelope: Entity): Promise { + return await this.schema.validate(envelope, { strict: true }); + } +} diff --git a/packages/catalog-model/src/kinds/index.ts b/packages/catalog-model/src/kinds/index.ts index b5fad184d4..4526e99da4 100644 --- a/packages/catalog-model/src/kinds/index.ts +++ b/packages/catalog-model/src/kinds/index.ts @@ -19,6 +19,11 @@ export type { ComponentEntityV1alpha1 as ComponentEntity, ComponentEntityV1alpha1, } from './ComponentEntityV1alpha1'; +export { GroupEntityV1alpha1Policy } from './GroupEntityV1alpha1'; +export type { + GroupEntityV1alpha1 as GroupEntity, + GroupEntityV1alpha1, +} from './GroupEntityV1alpha1'; export { LocationEntityV1alpha1Policy } from './LocationEntityV1alpha1'; export type { LocationEntityV1alpha1 as LocationEntity, From 039509314ccc282b221894467de0745996c6ae9c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 10 Aug 2020 01:07:01 +0200 Subject: [PATCH 02/26] catalog-backend: add bootstrap location --- .../20200809202832_add_bootstrap_location.js | 41 +++++++++++++++++ .../catalog/DatabaseLocationsCatalog.test.ts | 18 ++++++-- .../src/database/CommonDatabase.test.ts | 44 ++++++++++++------- 3 files changed, 84 insertions(+), 19 deletions(-) create mode 100644 plugins/catalog-backend/migrations/20200809202832_add_bootstrap_location.js diff --git a/plugins/catalog-backend/migrations/20200809202832_add_bootstrap_location.js b/plugins/catalog-backend/migrations/20200809202832_add_bootstrap_location.js new file mode 100644 index 0000000000..3d13a4219a --- /dev/null +++ b/plugins/catalog-backend/migrations/20200809202832_add_bootstrap_location.js @@ -0,0 +1,41 @@ +/* + * 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. + */ + +// @ts-check + +/** + * @param {import('knex')} knex + */ +exports.up = async function up(knex) { + // Adds a single 'bootstrap' location that can be used to trigger work in processors. + // This is primarily here to fulfill foreign key constraints. + await knex('locations').insert({ + id: 'bootstrap', + type: 'bootstrap', + target: 'bootstrap', + }); +}; + +/** + * @param {import('knex')} knex + */ +exports.down = async function down(knex) { + await knex('locations') + .where({ + id: 'bootstrap', + }) + .del(); +}; diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts index 679e5b43c1..456f06b334 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts @@ -17,6 +17,12 @@ import { DatabaseManager } from '../database'; import { DatabaseLocationsCatalog } from './DatabaseLocationsCatalog'; +const bootstrapLocation = { + id: 'bootstrap', + type: 'bootstrap', + target: 'bootstrap', +}; + describe('DatabaseLocationsCatalog', () => { let catalog: DatabaseLocationsCatalog; @@ -35,9 +41,12 @@ describe('DatabaseLocationsCatalog', () => { await expect( catalog.location('dd12620d-0436-422f-93bd-929aa0788123'), ).resolves.toEqual(expect.objectContaining({ data: location })); - await expect(catalog.locations()).resolves.toEqual([ - expect.objectContaining({ data: location }), - ]); + await expect(catalog.locations()).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ data: location }), + expect.objectContaining({ data: bootstrapLocation }), + ]), + ); }); it('does not return duplicates of rows because of logs', async () => { @@ -60,11 +69,12 @@ describe('DatabaseLocationsCatalog', () => { catalog.logUpdateSuccess(location1.id), ).resolves.toBeUndefined(); const locations = await catalog.locations(); - expect(locations.length).toBe(2); + expect(locations.length).toBe(3); expect(locations).toEqual( expect.arrayContaining([ expect.objectContaining({ data: location1 }), expect.objectContaining({ data: location2 }), + expect.objectContaining({ data: bootstrapLocation }), ]), ); }); diff --git a/plugins/catalog-backend/src/database/CommonDatabase.test.ts b/plugins/catalog-backend/src/database/CommonDatabase.test.ts index 21dde4ca74..94dfc25aa4 100644 --- a/plugins/catalog-backend/src/database/CommonDatabase.test.ts +++ b/plugins/catalog-backend/src/database/CommonDatabase.test.ts @@ -24,6 +24,15 @@ import type { DbLocationsRowWithStatus, } from './types'; +const bootstrapLocation = { + id: 'bootstrap', + type: 'bootstrap', + target: 'bootstrap', + message: null, + status: null, + timestamp: null, +}; + describe('CommonDatabase', () => { let db: Database; let entityRequest: DbEntityRequest; @@ -85,8 +94,12 @@ describe('CommonDatabase', () => { await db.addLocation(input); const locations = await db.locations(); - expect(locations).toEqual([output]); - const location = await db.location(locations[0].id); + expect(locations).toEqual( + expect.arrayContaining([output, bootstrapLocation]), + ); + const location = await db.location( + locations.find(l => l.id !== 'bootstrap')!.id, + ); expect(location).toEqual(output); // If we add 2 new update log events, @@ -105,20 +118,21 @@ describe('CommonDatabase', () => { DatabaseLocationUpdateLogStatus.FAIL, ); - expect(await db.locations()).toEqual([ - { - ...output, - status: DatabaseLocationUpdateLogStatus.FAIL, - timestamp: expect.any(String), - }, - ]); - - await db.transaction(tx => db.removeLocation(tx, locations[0].id)); - - await expect(db.locations()).resolves.toEqual([]); - await expect(db.location(locations[0].id)).rejects.toThrow( - /Found no location/, + await expect(db.locations()).resolves.toEqual( + expect.arrayContaining([ + bootstrapLocation, + { + ...output, + status: DatabaseLocationUpdateLogStatus.FAIL, + timestamp: expect.any(String), + }, + ]), ); + + await db.transaction(tx => db.removeLocation(tx, location.id)); + + await expect(db.locations()).resolves.toEqual([bootstrapLocation]); + await expect(db.location(location.id)).rejects.toThrow(/Found no location/); }); describe('addEntity', () => { From 6bd0cac14696ad351bf128c42652187b0dee2781 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 10 Aug 2020 01:08:39 +0200 Subject: [PATCH 03/26] catalog-backend: added StaticLocationProcessor --- packages/backend/src/plugins/catalog.ts | 3 +- plugins/catalog-backend/package.json | 1 + .../src/ingestion/LocationReaders.ts | 29 +++++++--- .../processors/StaticLocationProcessor.ts | 53 +++++++++++++++++++ .../src/service/standaloneServer.ts | 9 +++- 5 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 plugins/catalog-backend/src/ingestion/processors/StaticLocationProcessor.ts diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index 41e4226001..fdac6dd938 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -29,8 +29,9 @@ import { useHotCleanup } from '@backstage/backend-common'; export default async function createPlugin({ logger, database, + config, }: PluginEnvironment) { - const locationReader = new LocationReaders(logger); + const locationReader = new LocationReaders({ logger, config }); const db = await DatabaseManager.createDatabase(database, { logger }); const entitiesCatalog = new DatabaseEntitiesCatalog(db); diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 2445ee206e..4fbe982de4 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -23,6 +23,7 @@ "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.18", "@backstage/catalog-model": "^0.1.1-alpha.18", + "@backstage/config": "^0.1.1-alpha.18", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^3.0.3", diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts index e5ef410456..7e91af7c59 100644 --- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts +++ b/plugins/catalog-backend/src/ingestion/LocationReaders.ts @@ -15,6 +15,7 @@ */ import { getVoidLogger } from '@backstage/backend-common'; +import { Config, ConfigReader } from '@backstage/config'; import { Entity, EntityPolicies, @@ -31,6 +32,7 @@ import { GitlabApiReaderProcessor } from './processors/GitlabApiReaderProcessor' import { GitlabReaderProcessor } from './processors/GitlabReaderProcessor'; import { UrlReaderProcessor } from './processors/UrlReaderProcessor'; import { LocationRefProcessor } from './processors/LocationEntityProcessor'; +import { StaticLocationProcessor } from './processors/StaticLocationProcessor'; import * as result from './processors/results'; import { LocationProcessor, @@ -47,6 +49,12 @@ import { LocationReader, ReadLocationResult } from './types'; // The max amount of nesting depth of generated work items const MAX_DEPTH = 10; +type Options = { + logger?: Logger; + config?: Config; + processors?: LocationProcessor[]; +}; + /** * Implements the reading of a location through a series of processor tasks. */ @@ -54,10 +62,16 @@ export class LocationReaders implements LocationReader { private readonly logger: Logger; private readonly processors: LocationProcessor[]; - static defaultProcessors( - entityPolicy: EntityPolicy = new EntityPolicies(), - ): LocationProcessor[] { + static defaultProcessors(options: { + config?: Config; + entityPolicy?: EntityPolicy; + }): LocationProcessor[] { + const { + config = new ConfigReader({}, 'missing-config'), + entityPolicy = new EntityPolicies(), + } = options; return [ + StaticLocationProcessor.fromConfig(config), new FileReaderProcessor(), new GithubReaderProcessor(), new GithubApiReaderProcessor(), @@ -71,10 +85,11 @@ export class LocationReaders implements LocationReader { ]; } - constructor( - logger: Logger = getVoidLogger(), - processors: LocationProcessor[] = LocationReaders.defaultProcessors(), - ) { + constructor({ + logger = getVoidLogger(), + config, + processors = LocationReaders.defaultProcessors({ config }), + }: Options) { this.logger = logger; this.processors = processors; } diff --git a/plugins/catalog-backend/src/ingestion/processors/StaticLocationProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/StaticLocationProcessor.ts new file mode 100644 index 0000000000..6a2d1096cc --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/StaticLocationProcessor.ts @@ -0,0 +1,53 @@ +/* + * 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 { LocationSpec } from '@backstage/catalog-model'; +import * as result from './results'; +import { Config } from '@backstage/config'; +import { LocationProcessorEmit } from './types'; + +export class StaticLocationProcessor implements StaticLocationProcessor { + static fromConfig(config: Config): StaticLocationProcessor { + const locations: LocationSpec[] = []; + + const lConfigs = config.getOptionalConfigArray('catalog.locations') ?? []; + for (const lConfig of lConfigs) { + const type = lConfig.getString('type'); + const target = lConfig.getString('target'); + locations.push({ type, target }); + } + + return new StaticLocationProcessor(locations); + } + + constructor(private readonly staticLocations: LocationSpec[]) {} + + async readLocation( + location: LocationSpec, + _optional: boolean, + emit: LocationProcessorEmit, + ): Promise { + if (location.type !== 'bootstrap') { + return false; + } + + for (const staticLocation of this.staticLocations) { + emit(result.location(staticLocation, false)); + } + + return true; + } +} diff --git a/plugins/catalog-backend/src/service/standaloneServer.ts b/plugins/catalog-backend/src/service/standaloneServer.ts index 5c7ccb23f5..0b532d1664 100644 --- a/plugins/catalog-backend/src/service/standaloneServer.ts +++ b/plugins/catalog-backend/src/service/standaloneServer.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { createServiceBuilder } from '@backstage/backend-common'; +import { + createServiceBuilder, + loadBackendConfig, +} from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; import { Server } from 'http'; import { Logger } from 'winston'; import { HigherOrderOperations } from '..'; @@ -34,12 +38,13 @@ export async function startStandaloneServer( options: ServerOptions, ): Promise { const logger = options.logger.child({ service: 'catalog-backend' }); + const config = ConfigReader.fromConfigs(await loadBackendConfig()); logger.debug('Creating application...'); const db = await DatabaseManager.createInMemoryDatabase({ logger }); const entitiesCatalog = new DatabaseEntitiesCatalog(db); const locationsCatalog = new DatabaseLocationsCatalog(db); - const locationReader = new LocationReaders(); + const locationReader = new LocationReaders({ logger, config }); const higherOrderOperation = new HigherOrderOperations( entitiesCatalog, locationsCatalog, From c9cd3e1354a5593244050e1472074f79f261a21f Mon Sep 17 00:00:00 2001 From: "althaf.hameez" Date: Tue, 11 Aug 2020 16:25:38 +0800 Subject: [PATCH 04/26] Update GroupEntity spec definition * Make parent a single string instead of an array * Rename certain spec parameters * Make children, ancestors & descendents non-optional --- .../src/kinds/GroupEntityV1alpha1.test.ts | 50 ++++++++++--------- .../src/kinds/GroupEntityV1alpha1.ts | 18 +++---- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts index c0a98685e9..e7594772c1 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts @@ -35,11 +35,10 @@ describe('GroupV1alpha1Policy', () => { }, spec: { type: 'squad', - owner: 'me', - parent: ['group a'], + parent: 'group-a', ancestors: ['group-a', 'global-synergies', 'acme-corp'], - children: [], - grandchildren: [], + children: ['child-a', 'child-b'], + descendents: ['desc-a', 'desc-b'], }, }; policy = new GroupEntityV1alpha1Policy(); @@ -79,38 +78,43 @@ describe('GroupV1alpha1Policy', () => { await expect(policy.enforce(entity)).rejects.toThrow(/type/); }); - it('rejects missing owner', async () => { - delete (entity as any).spec.owner; - await expect(policy.enforce(entity)).rejects.toThrow(/owner/); - }); - - it('rejects wrong owner', async () => { - (entity as any).spec.owner = 7; - await expect(policy.enforce(entity)).rejects.toThrow(/owner/); - }); - - it('rejects empty owner', async () => { - (entity as any).spec.owner = ''; - await expect(policy.enforce(entity)).rejects.toThrow(/owner/); - }); - it('accepts missing parent', async () => { delete (entity as any).spec.parent; await expect(policy.enforce(entity)).resolves.toBe(entity); }); - it('accepts missing ancestors', async () => { + it('rejects empty parent', async () => { + (entity as any).spec.parent = ''; + await expect(policy.enforce(entity)).rejects.toThrow(/parent/); + }); + + it('rejects missing ancestors', async () => { delete (entity as any).spec.ancestors; + await expect(policy.enforce(entity)).rejects.toThrow(/ancestor/); + }); + + it('accepts empty ancestors', async () => { + (entity as any).spec.ancestors = ['']; await expect(policy.enforce(entity)).resolves.toBe(entity); }); - it('accepts missing children', async () => { + it('rejects missing children', async () => { delete (entity as any).spec.children; + await expect(policy.enforce(entity)).rejects.toThrow(/children/); + }); + + it('accepts empty children', async () => { + (entity as any).spec.children = ['']; await expect(policy.enforce(entity)).resolves.toBe(entity); }); - it('accepts missing grandchildren', async () => { - delete (entity as any).spec.grandchildren; + it('rejects missing descendents', async () => { + delete (entity as any).spec.descendents; + await expect(policy.enforce(entity)).rejects.toThrow(/descendents/); + }); + + it('accepts empty descendents', async () => { + (entity as any).spec.descendents = ['']; await expect(policy.enforce(entity)).resolves.toBe(entity); }); }); diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts index f7902dbd86..b3ab406729 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts @@ -26,11 +26,10 @@ export interface GroupEntityV1alpha1 extends Entity { kind: typeof KIND; spec: { type: string; - owner: string; - parent?: string[]; - ancestors?: string[]; - children?: string[]; - grandchildren?: string[]; + parent?: string; + ancestors: string[]; + children: string[]; + descendents: string[]; }; } @@ -44,11 +43,10 @@ export class GroupEntityV1alpha1Policy implements EntityPolicy { spec: yup .object({ type: yup.string().required().min(1), - owner: yup.string().required().min(1), - parent: yup.array(yup.string()).notRequired(), - ancestors: yup.array(yup.string()).notRequired(), - children: yup.array(yup.string()).notRequired(), - grandchildren: yup.array(yup.string()).notRequired(), + parent: yup.string().notRequired().min(1), + ancestors: yup.array(yup.string()).required(), + children: yup.array(yup.string()).required(), + descendents: yup.array(yup.string()).required(), }) .required(), }); From 238b5c58834c13517035357d10413105058ddb47 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 12 Aug 2020 10:35:17 +0200 Subject: [PATCH 05/26] workflows/frontend: fix workflow never being triggered --- .github/workflows/frontend.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index d7a862806a..f72b81e1f8 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -2,8 +2,8 @@ name: Frontend CI on: pull_request: - paths: - - '!microsite/**' + paths-ignore: + - 'microsite/**' jobs: build: runs-on: ubuntu-latest From 9056a14a68537d4bb82d1becd579c07332859bb9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 12 Aug 2020 10:15:53 +0200 Subject: [PATCH 06/26] cli: remove copyright headers from plugin template but run lint --fix --- .../src/commands/create-plugin/createPlugin.ts | 7 ++++++- .../cli/templates/default-plugin/dev/index.tsx | 16 ---------------- .../ExampleComponent.test.tsx.hbs | 16 ---------------- .../ExampleComponent/ExampleComponent.tsx.hbs | 16 ---------------- .../src/components/ExampleComponent/index.ts | 16 ---------------- .../ExampleFetchComponent.test.tsx.hbs | 16 ---------------- .../ExampleFetchComponent.tsx.hbs | 16 ---------------- .../components/ExampleFetchComponent/index.ts | 16 ---------------- .../cli/templates/default-plugin/src/index.ts | 16 ---------------- .../default-plugin/src/plugin.test.ts.hbs | 16 ---------------- .../templates/default-plugin/src/plugin.ts.hbs | 16 ---------------- .../templates/default-plugin/src/setupTests.ts | 16 ---------------- 12 files changed, 6 insertions(+), 177 deletions(-) diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index 1898285a22..b6b39b0f17 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -142,7 +142,12 @@ async function cleanUp(tempDir: string) { } async function buildPlugin(pluginFolder: string) { - const commands = ['yarn install', 'yarn tsc', 'yarn build']; + const commands = [ + 'yarn install', + 'yarn lint --fix', + 'yarn tsc', + 'yarn build', + ]; for (const command of commands) { await Task.forItem('executing', command, async () => { process.chdir(pluginFolder); diff --git a/packages/cli/templates/default-plugin/dev/index.tsx b/packages/cli/templates/default-plugin/dev/index.tsx index 812a5585d4..6fce113093 100644 --- a/packages/cli/templates/default-plugin/dev/index.tsx +++ b/packages/cli/templates/default-plugin/dev/index.tsx @@ -1,19 +1,3 @@ -/* - * 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 { createDevApp } from '@backstage/dev-utils'; import { plugin } from '../src/plugin'; diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs index dff57e66a4..37759313ee 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs @@ -1,19 +1,3 @@ -/* - * 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 { render } from '@testing-library/react'; import mockFetch from 'jest-fetch-mock'; diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs index 34e37f3e7b..79adaefaa8 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs @@ -1,19 +1,3 @@ -/* - * 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, { FC } from 'react'; import { Typography, Grid } from '@material-ui/core'; import { diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/index.ts b/packages/cli/templates/default-plugin/src/components/ExampleComponent/index.ts index e785d45082..520a3bf553 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/index.ts +++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/index.ts @@ -1,17 +1 @@ -/* - * 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 { default } from './ExampleComponent'; diff --git a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs index 7fecdc6f11..cc61c215cd 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs @@ -1,19 +1,3 @@ -/* - * 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 { render } from '@testing-library/react'; import mockFetch from 'jest-fetch-mock'; diff --git a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs index 2dd54f726e..0af27a5935 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx.hbs @@ -1,19 +1,3 @@ -/* - * 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, { FC } from 'react'; import { makeStyles } from '@material-ui/core/styles'; import { Table, TableColumn, Progress } from '@backstage/core'; diff --git a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/index.ts b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/index.ts index 28482f9fe1..3e53453948 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/index.ts +++ b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/index.ts @@ -1,17 +1 @@ -/* - * 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 { default } from './ExampleFetchComponent'; diff --git a/packages/cli/templates/default-plugin/src/index.ts b/packages/cli/templates/default-plugin/src/index.ts index 3a0a0fe2d3..99edba26c3 100644 --- a/packages/cli/templates/default-plugin/src/index.ts +++ b/packages/cli/templates/default-plugin/src/index.ts @@ -1,17 +1 @@ -/* - * 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 { plugin } from './plugin'; diff --git a/packages/cli/templates/default-plugin/src/plugin.test.ts.hbs b/packages/cli/templates/default-plugin/src/plugin.test.ts.hbs index e34204c9ae..c2be0a5301 100644 --- a/packages/cli/templates/default-plugin/src/plugin.test.ts.hbs +++ b/packages/cli/templates/default-plugin/src/plugin.test.ts.hbs @@ -1,19 +1,3 @@ -/* - * 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 { plugin } from './plugin'; describe('{{ id }}', () => { diff --git a/packages/cli/templates/default-plugin/src/plugin.ts.hbs b/packages/cli/templates/default-plugin/src/plugin.ts.hbs index ad5db2e711..6a410a6a28 100644 --- a/packages/cli/templates/default-plugin/src/plugin.ts.hbs +++ b/packages/cli/templates/default-plugin/src/plugin.ts.hbs @@ -1,19 +1,3 @@ -/* - * 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 { createPlugin, createRouteRef } from '@backstage/core'; import ExampleComponent from './components/ExampleComponent'; diff --git a/packages/cli/templates/default-plugin/src/setupTests.ts b/packages/cli/templates/default-plugin/src/setupTests.ts index 8553642152..3fa8703ac4 100644 --- a/packages/cli/templates/default-plugin/src/setupTests.ts +++ b/packages/cli/templates/default-plugin/src/setupTests.ts @@ -1,19 +1,3 @@ -/* - * 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 '@testing-library/jest-dom'; require('jest-fetch-mock').enableMocks(); From 19046ef51e958131f9a1cda91a91e4c464e22d1c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 12 Aug 2020 10:45:05 +0200 Subject: [PATCH 07/26] docs: fix broken links --- microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.md | 2 +- plugins/newrelic/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.md b/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.md index b4fe534a67..7b1fbb2d11 100644 --- a/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.md +++ b/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.md @@ -50,7 +50,7 @@ yarn start And you are good to go! 👍 -Read the full documentation on how to [create an app](https://github.com/spotify/backstage/blob/master/docs/create-an-app.md) on GitHub. +Read the full documentation on how to [create an app](https://github.com/spotify/backstage/blob/master/docs/getting-started/create-an-app.md) on GitHub. ## What do I get? (Let's get technical...) diff --git a/plugins/newrelic/README.md b/plugins/newrelic/README.md index 032d4f035b..19bd24950c 100644 --- a/plugins/newrelic/README.md +++ b/plugins/newrelic/README.md @@ -36,4 +36,4 @@ New Relic Plugin Path: [/newrelic](http://localhost:3000/newrelic) You can also serve the plugin in isolation by running `yarn start` in the plugin directory. This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. -It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. +It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. From ccb74eb62aa32f1cfa5708c9c696abee55526cac Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 12 Aug 2020 11:41:13 +0200 Subject: [PATCH 08/26] package.json: add missing deps for verify-links script --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index fa71c19d7c..c13e0789e4 100644 --- a/package.json +++ b/package.json @@ -38,10 +38,12 @@ "@spotify/eslint-config-oss": "^1.0.1", "@spotify/prettier-config": "^8.0.0", "concurrently": "^5.2.0", + "fs-extra": "^9.0.0", "husky": "^4.2.3", "lerna": "^3.20.2", "lint-staged": "^10.1.0", - "prettier": "^2.0.5" + "prettier": "^2.0.5", + "recursive-readdir": "^2.2.2" }, "husky": { "hooks": { From 11d0644ff835e418a3c070c7397736d0c7c23d7e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 12 Aug 2020 11:54:08 +0200 Subject: [PATCH 09/26] workflows/frontend: fix verify-links being run before yarn install --- .github/workflows/frontend.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index f72b81e1f8..dc65b987a8 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -45,12 +45,12 @@ jobs: run: git diff --quiet origin/master HEAD -- yarn.lock continue-on-error: true - - name: verify doc links - run: node docs/verify-links.js - - name: yarn install run: yarn install --frozen-lockfile + - name: verify doc links + run: node docs/verify-links.js + - name: lint run: yarn lerna -- run lint --since origin/master From 7fd9ed1a1d5db97b4c8cd87e4f47dd470a2ff1e4 Mon Sep 17 00:00:00 2001 From: David Tuite Date: Mon, 10 Aug 2020 12:10:08 +0100 Subject: [PATCH 10/26] Install TravisCI plugin --- packages/app/package.json | 1 + packages/app/src/apis.ts | 4 ++++ packages/app/src/plugins.ts | 1 + yarn.lock | 27 +++++++++++++++++++++++---- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 41fcc054a3..706c2d82bb 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -25,6 +25,7 @@ "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@octokit/rest": "^18.0.0", + "@roadiehq/backstage-plugin-travis-ci": "^0.1.3", "history": "^5.0.0", "prop-types": "^15.7.2", "react": "^16.12.0", diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index cac3d6e171..de46418075 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -63,6 +63,8 @@ import { githubActionsApiRef, } from '@backstage/plugin-github-actions'; +import { TravisCIApi, travisCIApiRef } from '@roadiehq/backstage-plugin-travis-ci'; + export const apis = (config: ConfigApi) => { // eslint-disable-next-line no-console console.log(`Creating APIs for ${config.getString('app.title')}`); @@ -89,6 +91,8 @@ export const apis = (config: ConfigApi) => { builder.add(lighthouseApiRef, new LighthouseRestApi('http://localhost:3003')); + builder.add(travisCIApiRef, new TravisCIApi()); + const oauthRequestApi = builder.add( oauthRequestApiRef, new OAuthRequestManager(), diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index 95d9e6c557..3e2b69a28f 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -28,3 +28,4 @@ export { plugin as GraphiQL } from '@backstage/plugin-graphiql'; export { plugin as GithubActions } from '@backstage/plugin-github-actions'; export { plugin as Rollbar } from '@backstage/plugin-rollbar'; export { plugin as Newrelic } from '@backstage/plugin-newrelic'; +export { plugin as TravisCI } from '@roadiehq/backstage-plugin-travis-ci'; diff --git a/yarn.lock b/yarn.lock index 28814f9f85..6e4af10f6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2929,6 +2929,26 @@ resolved "https://registry.npmjs.org/@rjsf/material-ui/-/material-ui-2.1.0.tgz#a361b125af3a383b7f671634b8d254345f9f9311" integrity sha512-9mMttnPNP6GiP7BtZGimYcYsbWwjyviqg/PD8oxrkEtZykULXOIdC3WDMJ3nPSym8RvZsgSnB8bajCpa5iSYQQ== +"@roadiehq/backstage-plugin-travis-ci@^0.1.3": + version "0.1.3" + resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-travis-ci/-/backstage-plugin-travis-ci-0.1.3.tgz#597882bb6db119b9c8970f15cc506c5d70ba2c41" + integrity sha512-xTnc71VtQSp7qfiE/p92U2/FEAvsG32BHIELebYZGCIt+ZqY/fSiXZSGsOXFDlg9adTu0e8ro6HWnknXCdo2pw== + dependencies: + "@backstage/core" "0.1.1-alpha.18" + "@backstage/theme" "0.1.1-alpha.18" + "@material-ui/core" "^4.9.1" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.45" + date-fns "^2.15.0" + history "^5.0.0" + moment "^2.27.0" + react "^16.13.1" + react-dom "^16.13.1" + react-lazylog "^4.5.2" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-use "^15.3.3" + "@rollup/plugin-commonjs@^13.0.0": version "13.0.0" resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-13.0.0.tgz#8a1d684ba6848afe8b9e3d85649d4b2f6f7217ec" @@ -8208,7 +8228,7 @@ date-fns@^1.27.2: resolved "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== -date-fns@^2.0.0-alpha.27, date-fns@^2.0.1: +date-fns@^2.0.0-alpha.27, date-fns@^2.0.1, date-fns@^2.15.0: version "2.15.0" resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.15.0.tgz#424de6b3778e4e69d3ff27046ec136af58ae5d5f" integrity sha512-ZCPzAMJZn3rNUvvQIMlXhDr4A+Ar07eLeGsGREoWU19a3Pqf5oYa+ccd+B3F6XVtQY6HANMFdOQ8A+ipFnvJdQ== @@ -9848,9 +9868,8 @@ file-loader@^4.2.0: loader-utils "^1.2.3" schema-utils "^2.5.0" -"file-saver@github:eligrey/FileSaver.js#1.3.8": +file-saver@eligrey/FileSaver.js#1.3.8: version "1.3.8" - uid e865e37af9f9947ddcced76b549e27dc45c1cb2e resolved "https://codeload.github.com/eligrey/FileSaver.js/tar.gz/e865e37af9f9947ddcced76b549e27dc45c1cb2e" file-system-cache@^1.0.5: @@ -12962,7 +12981,7 @@ jspdf@1.5.3: integrity sha512-J9X76xnncMw+wIqb15HeWfPMqPwYxSpPY8yWPJ7rAZN/ZDzFkjCSZObryCyUe8zbrVRNiuCnIeQteCzMn7GnWw== dependencies: canvg "1.5.3" - file-saver "github:eligrey/FileSaver.js#1.3.8" + file-saver eligrey/FileSaver.js#1.3.8 html2canvas "1.0.0-alpha.12" omggif "1.0.7" promise-polyfill "8.1.0" From f080c3256f4a13c4a54d37a79e9b60614668d978 Mon Sep 17 00:00:00 2001 From: David Tuite Date: Sun, 21 Jun 2020 18:31:25 +0100 Subject: [PATCH 11/26] Add Jenkins plugin It doesn't do anything other than render "Builds go here" in a DOM but the scaffold is there to start making API calls. This plugin borrows heavily from the CircleCI plugin. --- packages/app/package.json | 1 + packages/app/src/plugins.ts | 1 + plugins/jenkins/.eslintrc.js | 3 + plugins/jenkins/README.md | 13 +++++ plugins/jenkins/dev/index.tsx | 20 +++++++ plugins/jenkins/package.json | 49 ++++++++++++++++ plugins/jenkins/src/components/App.tsx | 28 +++++++++ .../jenkins/src/components/Layout/Layout.tsx | 29 ++++++++++ .../jenkins/src/components/Layout/index.ts | 16 ++++++ .../components/PluginHeader/PluginHeader.tsx | 57 +++++++++++++++++++ .../src/components/PluginHeader/index.ts | 16 ++++++ plugins/jenkins/src/index.ts | 17 ++++++ .../src/pages/BuildsPage/BuildsPage.tsx | 43 ++++++++++++++ plugins/jenkins/src/pages/BuildsPage/index.ts | 16 ++++++ .../pages/BuildsPage/lib/Builds/Builds.tsx | 26 +++++++++ .../src/pages/BuildsPage/lib/Builds/index.ts | 16 ++++++ plugins/jenkins/src/plugin.test.ts | 23 ++++++++ plugins/jenkins/src/plugin.ts | 30 ++++++++++ plugins/jenkins/src/setupTests.ts | 18 ++++++ 19 files changed, 422 insertions(+) create mode 100644 plugins/jenkins/.eslintrc.js create mode 100644 plugins/jenkins/README.md create mode 100644 plugins/jenkins/dev/index.tsx create mode 100644 plugins/jenkins/package.json create mode 100644 plugins/jenkins/src/components/App.tsx create mode 100644 plugins/jenkins/src/components/Layout/Layout.tsx create mode 100644 plugins/jenkins/src/components/Layout/index.ts create mode 100644 plugins/jenkins/src/components/PluginHeader/PluginHeader.tsx create mode 100644 plugins/jenkins/src/components/PluginHeader/index.ts create mode 100644 plugins/jenkins/src/index.ts create mode 100644 plugins/jenkins/src/pages/BuildsPage/BuildsPage.tsx create mode 100644 plugins/jenkins/src/pages/BuildsPage/index.ts create mode 100644 plugins/jenkins/src/pages/BuildsPage/lib/Builds/Builds.tsx create mode 100644 plugins/jenkins/src/pages/BuildsPage/lib/Builds/index.ts create mode 100644 plugins/jenkins/src/plugin.test.ts create mode 100644 plugins/jenkins/src/plugin.ts create mode 100644 plugins/jenkins/src/setupTests.ts diff --git a/packages/app/package.json b/packages/app/package.json index 706c2d82bb..41d72a89db 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -9,6 +9,7 @@ "@backstage/plugin-circleci": "^0.1.1-alpha.18", "@backstage/plugin-explore": "^0.1.1-alpha.18", "@backstage/plugin-github-actions": "^0.1.1-alpha.18", + "@backstage/plugin-jenkins": "^0.1.1-alpha.18", "@backstage/plugin-gitops-profiles": "^0.1.1-alpha.18", "@backstage/plugin-graphiql": "^0.1.1-alpha.18", "@backstage/plugin-lighthouse": "^0.1.1-alpha.18", diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index 3e2b69a28f..7e803698fe 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -29,3 +29,4 @@ export { plugin as GithubActions } from '@backstage/plugin-github-actions'; export { plugin as Rollbar } from '@backstage/plugin-rollbar'; export { plugin as Newrelic } from '@backstage/plugin-newrelic'; export { plugin as TravisCI } from '@roadiehq/backstage-plugin-travis-ci'; +export { plugin as Jenkins } from '@backstage/plugin-jenkins'; diff --git a/plugins/jenkins/.eslintrc.js b/plugins/jenkins/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/jenkins/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/jenkins/README.md b/plugins/jenkins/README.md new file mode 100644 index 0000000000..230f96dc72 --- /dev/null +++ b/plugins/jenkins/README.md @@ -0,0 +1,13 @@ +# jenkins + +Welcome to the jenkins plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/jenkins](http://localhost:3000/jenkins). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. diff --git a/plugins/jenkins/dev/index.tsx b/plugins/jenkins/dev/index.tsx new file mode 100644 index 0000000000..812a5585d4 --- /dev/null +++ b/plugins/jenkins/dev/index.tsx @@ -0,0 +1,20 @@ +/* + * 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 { createDevApp } from '@backstage/dev-utils'; +import { plugin } from '../src/plugin'; + +createDevApp().registerPlugin(plugin).render(); diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json new file mode 100644 index 0000000000..766a79a290 --- /dev/null +++ b/plugins/jenkins/package.json @@ -0,0 +1,49 @@ +{ + "name": "@backstage/plugin-jenkins", + "version": "0.1.1-alpha.18", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "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/core": "^0.1.1-alpha.18", + "@backstage/theme": "^0.1.1-alpha.18", + "@material-ui/core": "^4.9.1", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-use": "^14.2.0", + "react-router": "^6.0.0-alpha.5", + "react-router-dom": "^6.0.0-alpha.5" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.9", + "@backstage/dev-utils": "^0.1.1-alpha.9", + "@testing-library/jest-dom": "^5.7.0", + "@testing-library/react": "^9.3.2", + "@testing-library/user-event": "^10.2.4", + "@types/jest": "^25.2.2", + "@types/node": "^12.0.0", + "@types/testing-library__jest-dom": "^5.0.4", + "jest-fetch-mock": "^3.0.3" + }, + "files": [ + "dist/**/*.{js,d.ts}" + ] +} diff --git a/plugins/jenkins/src/components/App.tsx b/plugins/jenkins/src/components/App.tsx new file mode 100644 index 0000000000..ca8bb4c454 --- /dev/null +++ b/plugins/jenkins/src/components/App.tsx @@ -0,0 +1,28 @@ +/* + * 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 { Route, Routes } from 'react-router'; +import { BuildsPage } from '../pages/BuildsPage'; + +export const App = () => { + return ( + <> + + } /> + + + ); +}; diff --git a/plugins/jenkins/src/components/Layout/Layout.tsx b/plugins/jenkins/src/components/Layout/Layout.tsx new file mode 100644 index 0000000000..c6000437a3 --- /dev/null +++ b/plugins/jenkins/src/components/Layout/Layout.tsx @@ -0,0 +1,29 @@ +/* + * 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 { Header, Page, pageTheme, HeaderLabel } from '@backstage/core'; + +export const Layout: React.FC = ({ children }) => { + return ( + +
+ + +
+ {children} +
+ ); +}; diff --git a/plugins/jenkins/src/components/Layout/index.ts b/plugins/jenkins/src/components/Layout/index.ts new file mode 100644 index 0000000000..236fc98851 --- /dev/null +++ b/plugins/jenkins/src/components/Layout/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 * from './Layout'; diff --git a/plugins/jenkins/src/components/PluginHeader/PluginHeader.tsx b/plugins/jenkins/src/components/PluginHeader/PluginHeader.tsx new file mode 100644 index 0000000000..60b774713d --- /dev/null +++ b/plugins/jenkins/src/components/PluginHeader/PluginHeader.tsx @@ -0,0 +1,57 @@ +/* + * 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, { FC } from 'react'; +import { Link as RouterLink, useLocation } from 'react-router-dom'; +import { ContentHeader, SupportButton } from '@backstage/core'; +import { Button, IconButton, Box, Typography } from '@material-ui/core'; +import ArrowBack from '@material-ui/icons/ArrowBack'; +import SettingsIcon from '@material-ui/icons/Settings'; + +export type Props = { title?: string }; +export const PluginHeader: FC = ({ title = 'CircleCI' }) => { + const location = useLocation(); + const notRoot = !location.pathname.match(/\/circleci\/?$/); + + return ( + ( + + {notRoot && ( + + + + )} + {title} + + )} + > + + + + This plugin allows you to view and interact with your builds in Jenkins. + + + ); +}; diff --git a/plugins/jenkins/src/components/PluginHeader/index.ts b/plugins/jenkins/src/components/PluginHeader/index.ts new file mode 100644 index 0000000000..4de972f6f2 --- /dev/null +++ b/plugins/jenkins/src/components/PluginHeader/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 * from './PluginHeader'; diff --git a/plugins/jenkins/src/index.ts b/plugins/jenkins/src/index.ts new file mode 100644 index 0000000000..3a0a0fe2d3 --- /dev/null +++ b/plugins/jenkins/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 { plugin } from './plugin'; diff --git a/plugins/jenkins/src/pages/BuildsPage/BuildsPage.tsx b/plugins/jenkins/src/pages/BuildsPage/BuildsPage.tsx new file mode 100644 index 0000000000..7b124c03c1 --- /dev/null +++ b/plugins/jenkins/src/pages/BuildsPage/BuildsPage.tsx @@ -0,0 +1,43 @@ +/* + * 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, { FC } from 'react'; +import { Content } from '@backstage/core'; +import { Grid } from '@material-ui/core'; +import { Builds as BuildsComp } from './lib/Builds'; +import { Layout } from '../../components/Layout'; +import { PluginHeader } from '../../components/PluginHeader'; + +const BuildsPage: FC<{}> = () => ( + + + + + +); + +const Builds = () => ( + <> + + + + + + + +); + +export default BuildsPage; +export { Builds }; diff --git a/plugins/jenkins/src/pages/BuildsPage/index.ts b/plugins/jenkins/src/pages/BuildsPage/index.ts new file mode 100644 index 0000000000..72b46d6bc9 --- /dev/null +++ b/plugins/jenkins/src/pages/BuildsPage/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 { default as BuildsPage, Builds } from './BuildsPage'; diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/Builds/Builds.tsx b/plugins/jenkins/src/pages/BuildsPage/lib/Builds/Builds.tsx new file mode 100644 index 0000000000..b14be76ddc --- /dev/null +++ b/plugins/jenkins/src/pages/BuildsPage/lib/Builds/Builds.tsx @@ -0,0 +1,26 @@ +/* + * 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, { FC } from 'react'; +// import { useBuilds } from '../../../../state'; + +export const Builds: FC<{}> = () => { + // const [ + // { total, loading, value, projectName, page, pageSize }, + // { setPage, retry, setPageSize }, + // ] = useBuilds(); + + return
Builds go here
; +}; diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/Builds/index.ts b/plugins/jenkins/src/pages/BuildsPage/lib/Builds/index.ts new file mode 100644 index 0000000000..e91a9496b7 --- /dev/null +++ b/plugins/jenkins/src/pages/BuildsPage/lib/Builds/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 { Builds } from './Builds'; diff --git a/plugins/jenkins/src/plugin.test.ts b/plugins/jenkins/src/plugin.test.ts new file mode 100644 index 0000000000..8c3f057ecb --- /dev/null +++ b/plugins/jenkins/src/plugin.test.ts @@ -0,0 +1,23 @@ +/* + * 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 { plugin } from './plugin'; + +describe('jenkins', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/plugins/jenkins/src/plugin.ts b/plugins/jenkins/src/plugin.ts new file mode 100644 index 0000000000..f5b2210257 --- /dev/null +++ b/plugins/jenkins/src/plugin.ts @@ -0,0 +1,30 @@ +/* + * 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 { createPlugin, createRouteRef } from '@backstage/core'; +import { App } from './components/App'; + +export const rootRouteRef = createRouteRef({ + path: '/jenkins', + title: 'jenkins', +}); + +export const plugin = createPlugin({ + id: 'jenkins', + register({ router }) { + router.addRoute(rootRouteRef, App, { exact: false }); + }, +}); diff --git a/plugins/jenkins/src/setupTests.ts b/plugins/jenkins/src/setupTests.ts new file mode 100644 index 0000000000..e34bc46f4b --- /dev/null +++ b/plugins/jenkins/src/setupTests.ts @@ -0,0 +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. + */ + +import '@testing-library/jest-dom'; +require('jest-fetch-mock').enableMocks(); From 005df1df2bb4d65f6a86d031e7e14c3830f1f36e Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Tue, 28 Jul 2020 15:38:08 +0100 Subject: [PATCH 12/26] Add Jenkins widgets --- app-config.yaml | 9 + packages/app/src/apis.ts | 7 + plugins/catalog/package.json | 1 + .../src/components/EntityPage/EntityPage.tsx | 25 ++- plugins/jenkins/README.md | 89 +++++++++- plugins/jenkins/package.json | 10 +- plugins/jenkins/src/api/index.ts | 124 ++++++++++++++ plugins/jenkins/src/assets/build-details.png | Bin 0 -> 85203 bytes plugins/jenkins/src/assets/folder-results.png | Bin 0 -> 77035 bytes .../jenkins/src/assets/last-master-build.png | Bin 0 -> 20098 bytes .../JenkinsBuildsWidget.tsx | 29 ++++ .../JenkinsLastBuildWidget.tsx | 94 ++++++++++ .../components/PluginHeader/PluginHeader.tsx | 25 +-- plugins/jenkins/src/index.ts | 3 +- .../BuildWithStepsPage/BuildWithStepsPage.tsx | 161 ++++++++++++++++++ .../BuildWithStepsPage/index.ts} | 17 +- .../lib/ActionOutput/ActionOutput.tsx | 67 ++++++++ .../lib/ActionOutput}/index.ts | 2 +- .../src/pages/BuildsPage/BuildsPage.tsx | 43 ----- .../pages/BuildsPage/lib/Builds/Builds.tsx | 35 +++- .../pages/BuildsPage/lib/CITable/CITable.tsx | 138 +++++++++++++++ .../src/pages/BuildsPage/lib/CITable/index.ts | 17 ++ .../lib/Status/JenkinsRunStatus.tsx | 71 ++++++++ .../src/pages/BuildsPage/lib/Status/index.ts | 16 ++ plugins/jenkins/src/plugin.ts | 14 +- plugins/jenkins/src/setupTests.ts | 1 + plugins/jenkins/src/state/index.ts | 17 ++ plugins/jenkins/src/state/useAsyncPolling.ts | 37 ++++ .../jenkins/src/state/useBuildWithSteps.ts | 63 +++++++ plugins/jenkins/src/state/useBuilds.ts | 82 +++++++++ 30 files changed, 1086 insertions(+), 111 deletions(-) create mode 100644 plugins/jenkins/src/api/index.ts create mode 100644 plugins/jenkins/src/assets/build-details.png create mode 100644 plugins/jenkins/src/assets/folder-results.png create mode 100644 plugins/jenkins/src/assets/last-master-build.png create mode 100644 plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsBuildsWidget.tsx create mode 100644 plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsLastBuildWidget.tsx create mode 100644 plugins/jenkins/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx rename plugins/jenkins/src/{components/App.tsx => pages/BuildWithStepsPage/index.ts} (68%) create mode 100644 plugins/jenkins/src/pages/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx rename plugins/jenkins/src/pages/{BuildsPage => BuildWithStepsPage/lib/ActionOutput}/index.ts (90%) delete mode 100644 plugins/jenkins/src/pages/BuildsPage/BuildsPage.tsx create mode 100644 plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx create mode 100644 plugins/jenkins/src/pages/BuildsPage/lib/CITable/index.ts create mode 100644 plugins/jenkins/src/pages/BuildsPage/lib/Status/JenkinsRunStatus.tsx create mode 100644 plugins/jenkins/src/pages/BuildsPage/lib/Status/index.ts create mode 100644 plugins/jenkins/src/state/index.ts create mode 100644 plugins/jenkins/src/state/useAsyncPolling.ts create mode 100644 plugins/jenkins/src/state/useBuildWithSteps.ts create mode 100644 plugins/jenkins/src/state/useBuilds.ts diff --git a/app-config.yaml b/app-config.yaml index 5a5fdc1cab..c5c7a3674c 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -16,6 +16,15 @@ proxy: changeOrigin: true pathRewrite: '^/proxy/circleci/api/': '/' + '/jenkins/api': + target: 'http://localhost:8080' + changeOrigin: true + headers: + Authorization: + $secret: + env: JENKINS_BASIC_AUTH_HEADER + pathRewrite: + '^/proxy/jenkins/api/': '/' organization: name: Spotify diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index de46418075..d07bfbcec1 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -62,6 +62,8 @@ import { GithubActionsClient, githubActionsApiRef, } from '@backstage/plugin-github-actions'; +import {jenkinsApiRef, JenkinsApi} from "@backstage/plugin-jenkins"; + import { TravisCIApi, travisCIApiRef } from '@roadiehq/backstage-plugin-travis-ci'; @@ -85,6 +87,11 @@ export const apis = (config: ConfigApi) => { new CircleCIApi(`${backendUrl}/proxy/circleci/api`), ); + builder.add( + jenkinsApiRef, + new JenkinsApi(`${backendUrl}/proxy/jenkins/api`), + ); + builder.add(githubActionsApiRef, new GithubActionsClient()); builder.add(featureFlagsApiRef, new FeatureFlags()); diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index a0e69a647d..4f0e5df322 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -24,6 +24,7 @@ "@backstage/catalog-model": "^0.1.1-alpha.18", "@backstage/core": "^0.1.1-alpha.18", "@backstage/plugin-github-actions": "^0.1.1-alpha.18", + "@backstage/plugin-jenkins": "^0.1.1-alpha.18", "@backstage/plugin-scaffolder": "^0.1.1-alpha.18", "@backstage/plugin-sentry": "^0.1.1-alpha.18", "@backstage/theme": "^0.1.1-alpha.18", diff --git a/plugins/catalog/src/components/EntityPage/EntityPage.tsx b/plugins/catalog/src/components/EntityPage/EntityPage.tsx index c11844f722..c459b721ac 100644 --- a/plugins/catalog/src/components/EntityPage/EntityPage.tsx +++ b/plugins/catalog/src/components/EntityPage/EntityPage.tsx @@ -29,6 +29,7 @@ import { } from '@backstage/core'; import { SentryIssuesWidget } from '@backstage/plugin-sentry'; import { Widget as GithubActionsWidget } from '@backstage/plugin-github-actions'; +import { JenkinsBuildsWidget, JenkinsLastBuildWidget } from '@backstage/plugin-jenkins'; import { Grid, Box } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; import React, { FC, useEffect, useState } from 'react'; @@ -187,12 +188,20 @@ export const EntityPage: FC<{}> = () => { - - + {entity.metadata?.annotations?.[ + 'backstage.io/jenkins-github-folder' + ] && ( + + + )} + {entity.metadata?.annotations?.[ + 'backstage.io/jenkins-github-folder' + ] && ( + + + + )} {entity.metadata?.annotations?.[ 'backstage.io/github-actions-id' ] && ( @@ -201,6 +210,12 @@ export const EntityPage: FC<{}> = () => { )} + + + +Folder results +Build detials -## Getting started +## Setup -Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/jenkins](http://localhost:3000/jenkins). +1. If you have a standalone app (you didn't clone this repo), then do -You can also serve the plugin in isolation by running `yarn start` in the plugin directory. -This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. -It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. +```bash +yarn add @backstage/plugin-jenkins +``` + +2. Add plugin API to your Backstage instance: + +```js +// packages/app/src/api.ts +import { JenkinsApi, jenkinsApiRef } from '@backstage/plugin-jenkins'; + +const builder = ApiRegistry.builder(); +builder.add(jenkinsApiRef, new JenkinsApi(`${backendUrl}/proxy/jenkins/api`)); +``` + +2. Add plugin itself: + +```js +// packages/app/src/plugins.ts +export { plugin as Jenkins } from '@backstage/plugin-jenkins'; +``` + +3. Add proxy configuration to `app-config.yaml` + +```yaml +proxy: + '/jenkins/api': + target: 'http://localhost:8080' # your Jenkins URL + changeOrigin: true + headers: + Authorization: + $secret: + env: JENKINS_BASIC_AUTH_HEADER + pathRewrite: + '^/proxy/jenkins/api/': '/' +``` + +4. Add an environment variable which contains the Jenkins credentials, (note: use an API token not your password) + +```shell +HEADER=$(echo -n user:api-token | base64) +export JENKINS_BASIC_AUTH_HEADER="Authorization: Basic $HEADER" +``` + +5. Run app with `yarn start` +6. Add the Jenkins folder annotation to your `component-info.yaml`, (note: currently this plugin only supports folders and Git SCM) + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: 'your-component' + description: 'a description' + annotations: + backstage.io/jenkins-github-folder: 'folder-name/job-name' +spec: + type: service + lifecycle: experimental + owner: your-name +``` + +7. Register your component + +8. Click the component in the catalog you should now see Jenkins builds, and a last build result for your master build. + +## Features + +- View all runs inside a folder +- Last build status for specified branch +- View summary of a build + +## Limitations + +- Only works with projects that use the Git SCM +- It requires jobs to be organised into folders diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 766a79a290..4861077fe9 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -21,20 +21,22 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/catalog-model": "^0.1.1-alpha.18", "@backstage/core": "^0.1.1-alpha.18", "@backstage/theme": "^0.1.1-alpha.18", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", + "jenkins": "^0.28.0", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-use": "^14.2.0", "react-router": "^6.0.0-alpha.5", - "react-router-dom": "^6.0.0-alpha.5" + "react-router-dom": "^6.0.0-alpha.5", + "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.9", - "@backstage/dev-utils": "^0.1.1-alpha.9", + "@backstage/cli": "^0.1.1-alpha.16", + "@backstage/dev-utils": "^0.1.1-alpha.16", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", diff --git a/plugins/jenkins/src/api/index.ts b/plugins/jenkins/src/api/index.ts new file mode 100644 index 0000000000..fba45e3b88 --- /dev/null +++ b/plugins/jenkins/src/api/index.ts @@ -0,0 +1,124 @@ +/* + * 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 { createApiRef } from '@backstage/core'; +import { CITableBuildInfo } from '../pages/BuildsPage/lib/CITable'; + +const jenkins = require('jenkins'); + +export const jenkinsApiRef = createApiRef({ + id: 'plugin.jenkins.service', + description: 'Used by the Jenkins plugin to make requests', +}); + +export class JenkinsApi { + apiUrl: string; + jenkins: any; + + constructor(apiUrl: string) { + this.apiUrl = apiUrl; + this.jenkins = jenkins({ baseUrl: apiUrl, promisify: true }); + } + + async retry(buildName: string) { + // looks like current SDK may not support this or at least not very well + // can't see any support for replay + return await this.jenkins.job.build(buildName); + } + + async getLastBuild(jobName: string) { + const job = await this.jenkins.job.get(jobName); + + const lastBuild = await this.jenkins.build.get( + jobName, + job.lastBuild.number, + ); + return lastBuild; + } + + async getFolder(folderName: string) { + const folder = await this.jenkins.job.get(folderName); + const results = []; + for (const jobSummary of folder.jobs) { + const jobDetails = await this.jenkins.job.get( + `${folderName}/${jobSummary.name}`, + ); + if (jobDetails.jobs) { + // skipping folders inside folders for now + } else { + for (const buildDetails of jobDetails.builds) { + const build = await this.jenkins.build.get( + `${folderName}/${jobSummary.name}`, + buildDetails.number, + ); + const ciTable = this.mapJenkinsBuildToCITable(build); + results.push(ciTable); + } + } + } + return results; + } + + private mapJenkinsBuildToCITable(jenkinsResult: any): CITableBuildInfo { + const source = jenkinsResult.actions + .filter( + (action: any) => action._class === 'hudson.plugins.git.util.BuildData', + ) + .map((action: any) => { + const [first]: any = Object.values(action.buildsByBranchName); + const branch = first.revision.branch[0]; + return { + branchName: branch.name, + commit: { + hash: branch.SHA1.substring(0, 8), + }, + }; + }) + .pop(); + + return { + id: new URL(jenkinsResult.url).pathname, + buildName: jenkinsResult.fullDisplayName, + status: jenkinsResult.building ? 'running' : jenkinsResult.result, + onRestartClick: () => {}, + source: source, + }; + } + + async getBuild(buildName: string) { + const { jobName, buildNumber } = this.extractJobDetailsFromBuildName( + buildName, + ); + const buildResult = await this.jenkins.build.get(jobName, buildNumber); + return buildResult; + } + + private extractJobDetailsFromBuildName(buildName: string) { + const trimmedBuild = buildName.replace(/\/job/g, '').replace(/\/$/, ''); + + const split = trimmedBuild.split('/'); + const buildNumber = parseInt(split[split.length - 1], 10); + const jobName = trimmedBuild.slice( + 0, + trimmedBuild.length - buildNumber.toString(10).length - 1, + ); + + return { + jobName, + buildNumber, + }; + } +} diff --git a/plugins/jenkins/src/assets/build-details.png b/plugins/jenkins/src/assets/build-details.png new file mode 100644 index 0000000000000000000000000000000000000000..396af428af58f7dcaba14124de9fe913da507908 GIT binary patch literal 85203 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 To$z!F;Gcqw>Wh-+CV~G4?FVZf literal 0 HcmV?d00001 diff --git a/plugins/jenkins/src/assets/folder-results.png b/plugins/jenkins/src/assets/folder-results.png new file mode 100644 index 0000000000000000000000000000000000000000..2e14f3551f6cbfbd58356051ad28584c5510b9c7 GIT binary patch literal 77035 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 P{JW%lMJfBdng9O)p?7;+ literal 0 HcmV?d00001 diff --git a/plugins/jenkins/src/assets/last-master-build.png b/plugins/jenkins/src/assets/last-master-build.png new file mode 100644 index 0000000000000000000000000000000000000000..517e7a4410ef9d303e88fe38868926040591d9d3 GIT binary patch literal 20098 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 { + const [owner, repo] = ( + entity?.metadata.annotations?.['backstage.io/jenkins-github-folder'] ?? '/' + ).split('/'); + + return ; +} diff --git a/plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsLastBuildWidget.tsx b/plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsLastBuildWidget.tsx new file mode 100644 index 0000000000..f81d276a2e --- /dev/null +++ b/plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsLastBuildWidget.tsx @@ -0,0 +1,94 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { + Link, + Theme, + makeStyles, + LinearProgress, +} from '@material-ui/core'; +import { + InfoCard, + StructuredMetadataTable, +} from '@backstage/core'; +import ExternalLinkIcon from '@material-ui/icons/Launch'; +import {useBuilds} from "../../state"; +import {JenkinsRunStatus} from "../../pages/BuildsPage/lib/Status"; + +const useStyles = makeStyles({ + externalLinkIcon: { + fontSize: 'inherit', + verticalAlign: 'bottom', + }, +}); + +const WidgetContent = ({ + loading, + lastRun, + }: { + loading?: boolean; + lastRun: any; + branch: string; +}) => { + const classes = useStyles(); + if (loading || !lastRun) return ; + return ( + + + + ), + build: lastRun.fullDisplayName, + url: ( + + See more on Jenkins{' '} + + + ), + }} + /> + ); +}; + +export const JenkinsLastBuildWidget = ({ + entity, + branch = 'master', + }: { + entity: Entity; + branch: string; +}) => { + const [owner, repo] = ( + entity?.metadata.annotations?.['backstage.io/jenkins-github-folder'] ?? '/' + ).split('/'); + const [ + {loading, value} + ] = useBuilds(owner, repo, branch); + + const lastRun = value ?? {} ; + + return ( + + + + ); +}; diff --git a/plugins/jenkins/src/components/PluginHeader/PluginHeader.tsx b/plugins/jenkins/src/components/PluginHeader/PluginHeader.tsx index 60b774713d..fa6e80f20f 100644 --- a/plugins/jenkins/src/components/PluginHeader/PluginHeader.tsx +++ b/plugins/jenkins/src/components/PluginHeader/PluginHeader.tsx @@ -14,41 +14,20 @@ * limitations under the License. */ import React, { FC } from 'react'; -import { Link as RouterLink, useLocation } from 'react-router-dom'; import { ContentHeader, SupportButton } from '@backstage/core'; -import { Button, IconButton, Box, Typography } from '@material-ui/core'; -import ArrowBack from '@material-ui/icons/ArrowBack'; -import SettingsIcon from '@material-ui/icons/Settings'; +import { Box, Typography } from '@material-ui/core'; export type Props = { title?: string }; -export const PluginHeader: FC = ({ title = 'CircleCI' }) => { - const location = useLocation(); - const notRoot = !location.pathname.match(/\/circleci\/?$/); - +export const PluginHeader: FC = ({ title = 'Jenkins' }) => { return ( ( - {notRoot && ( - - - - )} {title} )} > - - This plugin allows you to view and interact with your builds in Jenkins. diff --git a/plugins/jenkins/src/index.ts b/plugins/jenkins/src/index.ts index 3a0a0fe2d3..c7f2d8dc28 100644 --- a/plugins/jenkins/src/index.ts +++ b/plugins/jenkins/src/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ -export { plugin } from './plugin'; +export { plugin, JenkinsBuildsWidget, JenkinsLastBuildWidget } from './plugin'; +export * from './api'; diff --git a/plugins/jenkins/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx b/plugins/jenkins/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx new file mode 100644 index 0000000000..4d2ddc24c8 --- /dev/null +++ b/plugins/jenkins/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx @@ -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 React, { FC, useEffect } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { Content, InfoCard, Progress } from '@backstage/core'; +import { Grid, Box, Link, IconButton } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import { PluginHeader } from '../../components/PluginHeader'; +import { ActionOutput } from './lib/ActionOutput/ActionOutput'; +import { Layout } from '../../components/Layout'; +import LaunchIcon from '@material-ui/icons/Launch'; +import { useBuildWithSteps } from '../../state/useBuildWithSteps'; + +const IconLink = IconButton as typeof Link; +const BuildName: FC<{ build?: any }> = ({ build }) => ( + + {build?.fullDisplayName} + + + + +); +const useStyles = makeStyles(theme => ({ + neutral: {}, + failed: { + position: 'relative', + '&:after': { + pointerEvents: 'none', + content: '""', + position: 'absolute', + top: 0, + right: 0, + left: 0, + bottom: 0, + boxShadow: `inset 4px 0px 0px ${theme.palette.error.main}`, + }, + }, + running: { + position: 'relative', + '&:after': { + pointerEvents: 'none', + content: '""', + position: 'absolute', + top: 0, + right: 0, + left: 0, + bottom: 0, + boxShadow: `inset 4px 0px 0px ${theme.palette.info.main}`, + }, + }, + cardContent: { + backgroundColor: theme.palette.background.default, + }, + success: { + position: 'relative', + '&:after': { + pointerEvents: 'none', + content: '""', + position: 'absolute', + top: 0, + right: 0, + left: 0, + bottom: 0, + boxShadow: `inset 4px 0px 0px ${theme.palette.success.main}`, + }, + }, +})); + +const pickClassName = ( + classes: ReturnType, + build: any = {}, +) => { + if (build.result === 'UNSTABLE') return classes.failed; + if (build.result === 'FAILURE') return classes.failed; + if (build.building) return classes.running; + if (build.status === 'SUCCESS') return classes.success; + + return classes.neutral; +}; + +const Page = () => ( + + + + + +); + +const BuildWithStepsView: FC<{}> = () => { + const [searchParams] = useSearchParams(); + const buildPath = searchParams.get('url') || ''; + const classes = useStyles(); + const [{ loading, value }, { startPolling, stopPolling }] = useBuildWithSteps( + buildPath, + ); + + useEffect(() => { + startPolling(); + return () => stopPolling(); + }, [buildPath, startPolling, stopPolling]); + + return ( + <> + + + + + } + cardClassName={classes.cardContent} + > + {loading ? : } + + + + + ); +}; + +const BuildsList: FC<{ build?: any }> = ({ build }) => ( + + {build && + build.steps && + build.steps.map(({ name, actions }: { name: string; actions: any[] }) => ( + + ))} + +); + +const ActionsList: FC<{ actions: any[]; name: string }> = ({ actions }) => { + const classes = useStyles(); + return ( + <> + {actions.map((action: any) => ( + + ))} + + ); +}; + +export default Page; +export { BuildWithStepsView as BuildWithSteps }; diff --git a/plugins/jenkins/src/components/App.tsx b/plugins/jenkins/src/pages/BuildWithStepsPage/index.ts similarity index 68% rename from plugins/jenkins/src/components/App.tsx rename to plugins/jenkins/src/pages/BuildWithStepsPage/index.ts index ca8bb4c454..fddff7088c 100644 --- a/plugins/jenkins/src/components/App.tsx +++ b/plugins/jenkins/src/pages/BuildWithStepsPage/index.ts @@ -13,16 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { Route, Routes } from 'react-router'; -import { BuildsPage } from '../pages/BuildsPage'; - -export const App = () => { - return ( - <> - - } /> - - - ); -}; +export { + default as DetailedViewPage, + BuildWithSteps, +} from './BuildWithStepsPage'; diff --git a/plugins/jenkins/src/pages/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx b/plugins/jenkins/src/pages/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx new file mode 100644 index 0000000000..1143121a98 --- /dev/null +++ b/plugins/jenkins/src/pages/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx @@ -0,0 +1,67 @@ +/* + * 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, { useEffect, FC } from 'react'; +import { + ExpansionPanel, + ExpansionPanelSummary, + Typography, + ExpansionPanelDetails, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { makeStyles } from '@material-ui/core/styles'; + +const useStyles = makeStyles({ + expansionPanelDetails: { + padding: 0, + }, + button: { + order: -1, + marginRight: 0, + marginLeft: '-20px', + }, +}); + +export const ActionOutput: FC<{ + url: string; + name: string; + className?: string; + action: any; +}> = ({ url, name, className }) => { + const classes = useStyles(); + + useEffect(() => {}, [url]); + + return ( + + } + aria-controls={`panel-${name}-content`} + id={`panel-${name}-header`} + IconButtonProps={{ + className: classes.button, + }} + > + {name} + + + Nothing here... + + + ); +}; diff --git a/plugins/jenkins/src/pages/BuildsPage/index.ts b/plugins/jenkins/src/pages/BuildWithStepsPage/lib/ActionOutput/index.ts similarity index 90% rename from plugins/jenkins/src/pages/BuildsPage/index.ts rename to plugins/jenkins/src/pages/BuildWithStepsPage/lib/ActionOutput/index.ts index 72b46d6bc9..7cf74c73f8 100644 --- a/plugins/jenkins/src/pages/BuildsPage/index.ts +++ b/plugins/jenkins/src/pages/BuildWithStepsPage/lib/ActionOutput/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { default as BuildsPage, Builds } from './BuildsPage'; +export { ActionOutput } from './ActionOutput'; diff --git a/plugins/jenkins/src/pages/BuildsPage/BuildsPage.tsx b/plugins/jenkins/src/pages/BuildsPage/BuildsPage.tsx deleted file mode 100644 index 7b124c03c1..0000000000 --- a/plugins/jenkins/src/pages/BuildsPage/BuildsPage.tsx +++ /dev/null @@ -1,43 +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. - */ -import React, { FC } from 'react'; -import { Content } from '@backstage/core'; -import { Grid } from '@material-ui/core'; -import { Builds as BuildsComp } from './lib/Builds'; -import { Layout } from '../../components/Layout'; -import { PluginHeader } from '../../components/PluginHeader'; - -const BuildsPage: FC<{}> = () => ( - - - - - -); - -const Builds = () => ( - <> - - - - - - - -); - -export default BuildsPage; -export { Builds }; diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/Builds/Builds.tsx b/plugins/jenkins/src/pages/BuildsPage/lib/Builds/Builds.tsx index b14be76ddc..b08c0ac855 100644 --- a/plugins/jenkins/src/pages/BuildsPage/lib/Builds/Builds.tsx +++ b/plugins/jenkins/src/pages/BuildsPage/lib/Builds/Builds.tsx @@ -13,14 +13,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { FC } from 'react'; -// import { useBuilds } from '../../../../state'; +import React from 'react'; +import {CITable} from '../CITable'; +import {useBuilds} from '../../../../state'; -export const Builds: FC<{}> = () => { - // const [ - // { total, loading, value, projectName, page, pageSize }, - // { setPage, retry, setPageSize }, - // ] = useBuilds(); +export const Builds = ({ + owner, + repo + }: { + owner: string; + repo: string; +}) => { - return

Builds go here
; + const [ + {total, loading, value, projectName, page, pageSize}, + {setPage, retry, setPageSize}, + ] = useBuilds(owner, repo); + return ( + + ); }; diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx b/plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx new file mode 100644 index 0000000000..85a6056427 --- /dev/null +++ b/plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx @@ -0,0 +1,138 @@ +/* + * 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, { FC } from 'react'; +import { Link, Typography, Box, IconButton } from '@material-ui/core'; +import RetryIcon from '@material-ui/icons/Replay'; +import GitHubIcon from '@material-ui/icons/GitHub'; +import { Link as RouterLink } from 'react-router-dom'; +import { + Table, + TableColumn, +} from '@backstage/core'; +import {JenkinsRunStatus} from "../Status"; + +export type CITableBuildInfo = { + id: string; + buildName: string; + buildUrl?: string; + source: { + branchName: string; + commit: { + hash: string; + url: string; + }; + }; + status: string; + tests?: { + total: number; + passed: number; + skipped: number; + failed: number; + testUrl: string; // fixme better name + }; + onRestartClick: () => void; +}; + +const generatedColumns: TableColumn[] = [ + { + title: 'Build', + field: 'buildName', + highlight: true, + render: (row: Partial) => ( + + {row.buildName} + + ), + }, + { + title: 'Source', + render: (row: Partial) => ( + <> +

{row.source?.branchName}

+

{row.source?.commit.hash}

+ + ), + }, + { + title: 'Status', + render: (row: Partial) => { + return ( + + + + )}, + }, + { + title: 'Actions', + render: (row: Partial) => ( + + + + ), + width: '10%', + }, +]; + +type Props = { + loading: boolean; + retry: () => void; + builds: CITableBuildInfo[]; + projectName: string; + page: number; + onChangePage: (page: number) => void; + total: number; + pageSize: number; + onChangePageSize: (pageSize: number) => void; +}; +export const CITable: FC = ({ + projectName, + loading, + pageSize, + page, + retry, + builds, + onChangePage, + onChangePageSize, + total, +}) => { + return ( + , + tooltip: 'Refresh Data', + isFreeAction: true, + onClick: () => retry(), + }, + ]} + data={builds} + onChangePage={onChangePage} + onChangeRowsPerPage={onChangePageSize} + title={ + + + + {projectName} + + } + columns={generatedColumns} + /> + ); +}; diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/CITable/index.ts b/plugins/jenkins/src/pages/BuildsPage/lib/CITable/index.ts new file mode 100644 index 0000000000..358939e69f --- /dev/null +++ b/plugins/jenkins/src/pages/BuildsPage/lib/CITable/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 { CITable } from './CITable'; +export type { CITableBuildInfo } from './CITable'; diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/Status/JenkinsRunStatus.tsx b/plugins/jenkins/src/pages/BuildsPage/lib/Status/JenkinsRunStatus.tsx new file mode 100644 index 0000000000..8ac4880e38 --- /dev/null +++ b/plugins/jenkins/src/pages/BuildsPage/lib/Status/JenkinsRunStatus.tsx @@ -0,0 +1,71 @@ +/* + * 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 {StatusPending, StatusRunning, StatusOK, StatusWarning, StatusAborted, StatusError} from '@backstage/core'; +import React from 'react'; + +export const JenkinsRunStatus = ({ + status, + }: { + status: string | undefined; +}) => { + if (status === undefined) return null; + switch (status.toLowerCase()) { + case 'queued': + case 'scheduled': + return ( + <> + Queued + + ); + case 'running': + return ( + <> + In progress + + ); + case 'unstable': + return ( + <> + Unstable + + ); + case 'failure': + return ( + <> + Failed + + ); + case 'success': + return ( + <> + Completed + + ); + case 'aborted': + return ( + <> + Aborted + + ); + default: + return ( + <> + {status} + + ); + } +}; diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/Status/index.ts b/plugins/jenkins/src/pages/BuildsPage/lib/Status/index.ts new file mode 100644 index 0000000000..928d91c61d --- /dev/null +++ b/plugins/jenkins/src/pages/BuildsPage/lib/Status/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 { JenkinsRunStatus } from './JenkinsRunStatus' diff --git a/plugins/jenkins/src/plugin.ts b/plugins/jenkins/src/plugin.ts index f5b2210257..62fd3a108e 100644 --- a/plugins/jenkins/src/plugin.ts +++ b/plugins/jenkins/src/plugin.ts @@ -15,16 +15,20 @@ */ import { createPlugin, createRouteRef } from '@backstage/core'; -import { App } from './components/App'; +import {DetailedViewPage} from "./pages/BuildWithStepsPage"; -export const rootRouteRef = createRouteRef({ - path: '/jenkins', - title: 'jenkins', +export const buildRouteRef = createRouteRef({ + path: '/jenkins/job', + title: 'Jenkins run', }); + export const plugin = createPlugin({ id: 'jenkins', register({ router }) { - router.addRoute(rootRouteRef, App, { exact: false }); + router.addRoute(buildRouteRef, DetailedViewPage); }, }); + +export { JenkinsBuildsWidget } from './components/JenkinsPluginWidget/JenkinsBuildsWidget'; +export { JenkinsLastBuildWidget } from './components/JenkinsPluginWidget/JenkinsLastBuildWidget'; diff --git a/plugins/jenkins/src/setupTests.ts b/plugins/jenkins/src/setupTests.ts index e34bc46f4b..8553642152 100644 --- a/plugins/jenkins/src/setupTests.ts +++ b/plugins/jenkins/src/setupTests.ts @@ -15,4 +15,5 @@ */ import '@testing-library/jest-dom'; + require('jest-fetch-mock').enableMocks(); diff --git a/plugins/jenkins/src/state/index.ts b/plugins/jenkins/src/state/index.ts new file mode 100644 index 0000000000..d21a380c2a --- /dev/null +++ b/plugins/jenkins/src/state/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 './useBuilds'; +export * from './useBuildWithSteps'; diff --git a/plugins/jenkins/src/state/useAsyncPolling.ts b/plugins/jenkins/src/state/useAsyncPolling.ts new file mode 100644 index 0000000000..7ea0755368 --- /dev/null +++ b/plugins/jenkins/src/state/useAsyncPolling.ts @@ -0,0 +1,37 @@ +/* + * 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 { useRef } from 'react'; + +export const useAsyncPolling = ( + pollingFn: () => Promise, + interval: number, +) => { + const isPolling = useRef(false); + const startPolling = async () => { + if (isPolling.current === true) return; + isPolling.current = true; + + while (isPolling.current === true) { + await pollingFn(); + await new Promise(resolve => setTimeout(resolve, interval)); + } + }; + + const stopPolling = () => { + isPolling.current = false; + }; + return { startPolling, stopPolling }; +}; diff --git a/plugins/jenkins/src/state/useBuildWithSteps.ts b/plugins/jenkins/src/state/useBuildWithSteps.ts new file mode 100644 index 0000000000..a681438685 --- /dev/null +++ b/plugins/jenkins/src/state/useBuildWithSteps.ts @@ -0,0 +1,63 @@ +/* + * 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 { errorApiRef, useApi } from '@backstage/core'; +import { useCallback } from 'react'; +import { useAsyncRetry } from 'react-use'; +import { jenkinsApiRef } from '../api/index'; +import { useAsyncPolling } from './useAsyncPolling'; + +const INTERVAL_AMOUNT = 1500; +export function useBuildWithSteps(buildName: string) { + const api = useApi(jenkinsApiRef); + const errorApi = useApi(errorApiRef); + + const getBuildWithSteps = useCallback(async () => { + try { + const build = await api.getBuild(buildName); + return Promise.resolve(build); + } catch (e) { + errorApi.post(e); + return Promise.reject(e); + } + }, [buildName, api, errorApi]); + + const restartBuild = async () => { + try { + await api.retry(buildName); + } catch (e) { + errorApi.post(e); + } + }; + + const { loading, value, retry } = useAsyncRetry(() => getBuildWithSteps(), [ + getBuildWithSteps, + ]); + + const { startPolling, stopPolling } = useAsyncPolling( + getBuildWithSteps, + INTERVAL_AMOUNT, + ); + + return [ + { loading, value, retry }, + { + restartBuild, + getBuildWithSteps, + startPolling, + stopPolling, + }, + ] as const; +} diff --git a/plugins/jenkins/src/state/useBuilds.ts b/plugins/jenkins/src/state/useBuilds.ts new file mode 100644 index 0000000000..deb3125637 --- /dev/null +++ b/plugins/jenkins/src/state/useBuilds.ts @@ -0,0 +1,82 @@ +/* + * 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 { errorApiRef, useApi } from '@backstage/core'; +import { useCallback, useEffect, useState } from 'react'; +import { useAsyncRetry } from 'react-use'; +import { jenkinsApiRef } from '../api'; + +export function useBuilds(owner: string, repo: string, branch?: string) { + const api = useApi(jenkinsApiRef); + const errorApi = useApi(errorApiRef); + + const [total, setTotal] = useState(0); + const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState(5); + + const getBuilds = useCallback(async () => { + try { + let build; + if (branch) { + build = await api.getLastBuild(`${owner}/${repo}/${branch}`); + } else { + build = await api.getFolder(`${owner}/${repo}`); + } + return build; + } catch (e) { + errorApi.post(e); + return Promise.reject(e); + } + }, [api, branch, errorApi, owner, repo]); + + const restartBuild = async (buildName: string) => { + try { + await api.retry(buildName); + } catch (e) { + errorApi.post(e); + } + }; + + useEffect(() => { + getBuilds().then(b => { + const size = Array.isArray(b) ? b?.[0].build_num! : 1; + setTotal(size); + }); + }, [repo, getBuilds]); + + const { loading, value, retry } = useAsyncRetry( + () => getBuilds().then(builds => builds ?? [], restartBuild), + [page, pageSize, getBuilds], + ); + + const projectName = `${owner}/${repo}`; + return [ + { + page, + pageSize, + loading, + value, + projectName, + total, + }, + { + getBuilds, + setPage, + setPageSize, + restartBuild, + retry, + }, + ] as const; +} From 0e9b562223ec163d2ea149d2ce3521f1f39e2740 Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Fri, 31 Jul 2020 16:03:59 +0100 Subject: [PATCH 13/26] Add pagination warning to README --- plugins/jenkins/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/jenkins/README.md b/plugins/jenkins/README.md index fd0dbe67ad..983c780d49 100644 --- a/plugins/jenkins/README.md +++ b/plugins/jenkins/README.md @@ -84,3 +84,4 @@ spec: - Only works with projects that use the Git SCM - It requires jobs to be organised into folders +- No pagination support currently - don't run this on a Jenkins with lots of builds From 65d3e99840ebe1d29133fd40528ed3bb26764291 Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Fri, 31 Jul 2020 16:25:40 +0100 Subject: [PATCH 14/26] Remove some uses of FC --- plugins/jenkins/src/components/PluginHeader/PluginHeader.tsx | 4 ++-- .../src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/jenkins/src/components/PluginHeader/PluginHeader.tsx b/plugins/jenkins/src/components/PluginHeader/PluginHeader.tsx index fa6e80f20f..501649e49a 100644 --- a/plugins/jenkins/src/components/PluginHeader/PluginHeader.tsx +++ b/plugins/jenkins/src/components/PluginHeader/PluginHeader.tsx @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { FC } from 'react'; +import React from 'react'; import { ContentHeader, SupportButton } from '@backstage/core'; import { Box, Typography } from '@material-ui/core'; export type Props = { title?: string }; -export const PluginHeader: FC = ({ title = 'Jenkins' }) => { +export const PluginHeader = ({ title = 'Jenkins' }) => { return ( ( ); -const BuildWithStepsView: FC<{}> = () => { +const BuildWithStepsView = () => { const [searchParams] = useSearchParams(); const buildPath = searchParams.get('url') || ''; const classes = useStyles(); From 93c50514bc39309a1af1439930e5a74100e03f82 Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Fri, 31 Jul 2020 16:31:13 +0100 Subject: [PATCH 15/26] Fix packaging errors --- plugins/jenkins/package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 4861077fe9..0d0a972a8a 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -37,15 +37,15 @@ "devDependencies": { "@backstage/cli": "^0.1.1-alpha.16", "@backstage/dev-utils": "^0.1.1-alpha.16", - "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^10.2.4", - "@types/jest": "^25.2.2", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "@types/testing-library__jest-dom": "^5.0.4", "jest-fetch-mock": "^3.0.3" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } From fbdacc1f311d925de4b6148f04de8464211528bf Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Mon, 3 Aug 2020 08:46:55 +0100 Subject: [PATCH 16/26] Fix docs issue --- plugins/jenkins/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/jenkins/README.md b/plugins/jenkins/README.md index 983c780d49..b8a844f5d8 100644 --- a/plugins/jenkins/README.md +++ b/plugins/jenkins/README.md @@ -50,7 +50,7 @@ proxy: ```shell HEADER=$(echo -n user:api-token | base64) -export JENKINS_BASIC_AUTH_HEADER="Authorization: Basic $HEADER" +export JENKINS_BASIC_AUTH_HEADER="Basic $HEADER" ``` 5. Run app with `yarn start` From ea15090bb396ac46df6d41766869f516d0d2708c Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Mon, 3 Aug 2020 09:29:10 +0100 Subject: [PATCH 17/26] Add support for restarting builds --- plugins/jenkins/src/api/index.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/plugins/jenkins/src/api/index.ts b/plugins/jenkins/src/api/index.ts index fba45e3b88..0218a14cac 100644 --- a/plugins/jenkins/src/api/index.ts +++ b/plugins/jenkins/src/api/index.ts @@ -34,8 +34,8 @@ export class JenkinsApi { } async retry(buildName: string) { - // looks like current SDK may not support this or at least not very well - // can't see any support for replay + // looks like the current SDK only supports triggering a new build + // can't see any support for replay (re-running the specific build with the same SCM info) return await this.jenkins.job.build(buildName); } @@ -89,11 +89,17 @@ export class JenkinsApi { }) .pop(); + const path = new URL(jenkinsResult.url).pathname; + return { - id: new URL(jenkinsResult.url).pathname, + id: path, buildName: jenkinsResult.fullDisplayName, status: jenkinsResult.building ? 'running' : jenkinsResult.result, - onRestartClick: () => {}, + onRestartClick: () => { + // TODO: this won't handle non root context path, need a better way to get the job name + const { jobName } = this.extractJobDetailsFromBuildName(path); + return this.retry(jobName); + }, source: source, }; } From c85e95230002c7cd876762db713ab98b1048b35b Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Mon, 3 Aug 2020 17:02:34 +0100 Subject: [PATCH 18/26] Add links to GitHub and include PR title on build info --- plugins/jenkins/src/api/index.ts | 91 ++++++++++++++----- .../BuildWithStepsPage/BuildWithStepsPage.tsx | 13 ++- .../pages/BuildsPage/lib/CITable/CITable.tsx | 31 ++++--- .../jenkins/src/state/useBuildWithSteps.ts | 7 +- 4 files changed, 97 insertions(+), 45 deletions(-) diff --git a/plugins/jenkins/src/api/index.ts b/plugins/jenkins/src/api/index.ts index 0218a14cac..38f6f719c1 100644 --- a/plugins/jenkins/src/api/index.ts +++ b/plugins/jenkins/src/api/index.ts @@ -49,22 +49,53 @@ export class JenkinsApi { return lastBuild; } + extractScmDetailsFromJob(jobDetails: any): any { + const scmInfo = jobDetails.actions + .filter( + (action: any) => + action._class === 'jenkins.scm.api.metadata.ObjectMetadataAction', + ) + .map((action: any) => { + return { + url: action?.objectUrl, + // https://javadoc.jenkins.io/plugin/scm-api/jenkins/scm/api/metadata/ObjectMetadataAction.html + // branch name for regular builds, pull request title on pull requests + displayName: action?.objectDisplayName, + }; + }) + .pop(); + + return scmInfo; + } + + async getJob(jobName: string) { + return this.jenkins.job.get({ + name: jobName, + depth: 1, + }); + } + async getFolder(folderName: string) { const folder = await this.jenkins.job.get(folderName); const results = []; for (const jobSummary of folder.jobs) { - const jobDetails = await this.jenkins.job.get( - `${folderName}/${jobSummary.name}`, - ); + const jobDetails = await this.jenkins.job.get({ + name: `${folderName}/${jobSummary.name}`, + depth: 1, + }); + + const jobScmInfo = this.extractScmDetailsFromJob(jobDetails); if (jobDetails.jobs) { // skipping folders inside folders for now } else { for (const buildDetails of jobDetails.builds) { - const build = await this.jenkins.build.get( - `${folderName}/${jobSummary.name}`, - buildDetails.number, - ); - const ciTable = this.mapJenkinsBuildToCITable(build); + const build = await this.jenkins.build.get({ + name: `${folderName}/${jobSummary.name}`, + number: buildDetails.number, + depth: 1, + }); + + const ciTable = this.mapJenkinsBuildToCITable(build, jobScmInfo); results.push(ciTable); } } @@ -72,22 +103,32 @@ export class JenkinsApi { return results; } - private mapJenkinsBuildToCITable(jenkinsResult: any): CITableBuildInfo { - const source = jenkinsResult.actions - .filter( - (action: any) => action._class === 'hudson.plugins.git.util.BuildData', - ) - .map((action: any) => { - const [first]: any = Object.values(action.buildsByBranchName); - const branch = first.revision.branch[0]; - return { - branchName: branch.name, - commit: { - hash: branch.SHA1.substring(0, 8), - }, - }; - }) - .pop(); + mapJenkinsBuildToCITable( + jenkinsResult: any, + jobScmInfo?: any, + ): CITableBuildInfo { + const source = + jenkinsResult.actions + .filter( + (action: any) => + action._class === 'hudson.plugins.git.util.BuildData', + ) + .map((action: any) => { + const [first]: any = Object.values(action.buildsByBranchName); + const branch = first.revision.branch[0]; + return { + branchName: branch.name, + commit: { + hash: branch.SHA1.substring(0, 8), + }, + }; + }) + .pop() || {}; + + if (jobScmInfo) { + source.url = jobScmInfo?.url; + source.displayName = jobScmInfo?.displayName; + } const path = new URL(jenkinsResult.url).pathname; @@ -112,7 +153,7 @@ export class JenkinsApi { return buildResult; } - private extractJobDetailsFromBuildName(buildName: string) { + extractJobDetailsFromBuildName(buildName: string) { const trimmedBuild = buildName.replace(/\/job/g, '').replace(/\/$/, ''); const split = trimmedBuild.split('/'); diff --git a/plugins/jenkins/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx b/plugins/jenkins/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx index c8d726d186..ab4fc3f34f 100644 --- a/plugins/jenkins/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx +++ b/plugins/jenkins/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx @@ -22,14 +22,18 @@ import { PluginHeader } from '../../components/PluginHeader'; import { ActionOutput } from './lib/ActionOutput/ActionOutput'; import { Layout } from '../../components/Layout'; import LaunchIcon from '@material-ui/icons/Launch'; +import GitHubIcon from '@material-ui/icons/GitHub'; import { useBuildWithSteps } from '../../state/useBuildWithSteps'; const IconLink = IconButton as typeof Link; const BuildName: FC<{ build?: any }> = ({ build }) => ( - {build?.fullDisplayName} - - + {build?.buildName} + + {/* TODO use Jenkins logo*/} + + + ); @@ -111,10 +115,9 @@ const BuildWithStepsView = () => { startPolling(); return () => stopPolling(); }, [buildPath, startPolling, stopPolling]); - return ( <> - + diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx b/plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx index 85a6056427..90d8db616e 100644 --- a/plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx +++ b/plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx @@ -18,11 +18,8 @@ import { Link, Typography, Box, IconButton } from '@material-ui/core'; import RetryIcon from '@material-ui/icons/Replay'; import GitHubIcon from '@material-ui/icons/GitHub'; import { Link as RouterLink } from 'react-router-dom'; -import { - Table, - TableColumn, -} from '@backstage/core'; -import {JenkinsRunStatus} from "../Status"; +import { Table, TableColumn } from '@backstage/core'; +import { JenkinsRunStatus } from '../Status'; export type CITableBuildInfo = { id: string; @@ -30,9 +27,10 @@ export type CITableBuildInfo = { buildUrl?: string; source: { branchName: string; + url: string; + displayName: string; commit: { hash: string; - url: string; }; }; status: string; @@ -41,7 +39,7 @@ export type CITableBuildInfo = { passed: number; skipped: number; failed: number; - testUrl: string; // fixme better name + testUrl: string; }; onRestartClick: () => void; }; @@ -61,19 +59,24 @@ const generatedColumns: TableColumn[] = [ title: 'Source', render: (row: Partial) => ( <> -

{row.source?.branchName}

-

{row.source?.commit.hash}

+

+ + {row.source?.branchName} + +

+

{row.source?.commit?.hash}

), }, { title: 'Status', - render: (row: Partial) => { + render: (row: Partial) => { return ( - - - - )}, + + + + ); + }, }, { title: 'Actions', diff --git a/plugins/jenkins/src/state/useBuildWithSteps.ts b/plugins/jenkins/src/state/useBuildWithSteps.ts index a681438685..617b2b1f4f 100644 --- a/plugins/jenkins/src/state/useBuildWithSteps.ts +++ b/plugins/jenkins/src/state/useBuildWithSteps.ts @@ -27,7 +27,12 @@ export function useBuildWithSteps(buildName: string) { const getBuildWithSteps = useCallback(async () => { try { const build = await api.getBuild(buildName); - return Promise.resolve(build); + + const { jobName } = api.extractJobDetailsFromBuildName(buildName); + const job = await api.getJob(jobName); + const jobInfo = api.extractScmDetailsFromJob(job); + + return Promise.resolve(api.mapJenkinsBuildToCITable(build, jobInfo)); } catch (e) { errorApi.post(e); return Promise.reject(e); From 547604d973919c4de348e9e5a6d5316b4a518b62 Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Tue, 4 Aug 2020 09:27:31 +0100 Subject: [PATCH 19/26] Add test results to CI Table --- plugins/jenkins/src/api/index.ts | 27 +++++++ .../pages/BuildsPage/lib/CITable/CITable.tsx | 77 +++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/plugins/jenkins/src/api/index.ts b/plugins/jenkins/src/api/index.ts index 38f6f719c1..545a1da9cc 100644 --- a/plugins/jenkins/src/api/index.ts +++ b/plugins/jenkins/src/api/index.ts @@ -103,6 +103,32 @@ export class JenkinsApi { return results; } + private getTestReport( + jenkinsResult: any, + ): { + total: number; + passed: number; + skipped: number; + failed: number; + testUrl: string; + } { + return jenkinsResult.actions + .filter( + (action: any) => + action._class === 'hudson.tasks.junit.TestResultAction', + ) + .map((action: any) => { + return { + total: action.totalCount, + passed: action.totalCount - action.failCount - action.skipCount, + skipped: action.skipCount, + failed: action.failCount, + testUrl: `${jenkinsResult.url}${action.urlName}/`, + }; + }) + .pop(); + } + mapJenkinsBuildToCITable( jenkinsResult: any, jobScmInfo?: any, @@ -142,6 +168,7 @@ export class JenkinsApi { return this.retry(jobName); }, source: source, + tests: this.getTestReport(jenkinsResult), }; } diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx b/plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx index 90d8db616e..2edff01799 100644 --- a/plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx +++ b/plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx @@ -44,6 +44,61 @@ export type CITableBuildInfo = { onRestartClick: () => void; }; +const FailCount = ({ count }: { count: number }): JSX.Element | null => { + if (count !== 0) { + return <>{count} failed; + } + return null; +}; + +const SkippedCount = ({ count }: { count: number }): JSX.Element | null => { + if (count !== 0) { + return <>; + } + return null; +}; + +const FailSkippedWidget = ({ + skipped, + failed, +}: { + skipped: number; + failed: number; +}): JSX.Element | null => { + if (skipped === 0 && failed === 0) { + return null; + } + + if (skipped !== 0 && failed !== 0) { + return ( + <> + {' '} + (, ) + + ); + } + + if (failed !== 0) { + return ( + <> + {' '} + () + + ); + } + + if (skipped !== 0) { + return ( + <> + {' '} + () + + ); + } + + return null; +}; + const generatedColumns: TableColumn[] = [ { title: 'Build', @@ -78,6 +133,28 @@ const generatedColumns: TableColumn[] = [ ); }, }, + { + title: 'Tests', + render: (row: Partial) => { + return ( + <> +

+ {row.tests && ( + + {row.tests.passed} / {row.tests.total} passed + + + )} + + {!row.tests && 'n/a'} +

+ + ); + }, + }, { title: 'Actions', render: (row: Partial) => ( From 88df61309ef09cdcd0832d710fd3a87d48d8a8ec Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Tue, 4 Aug 2020 11:55:15 +0100 Subject: [PATCH 20/26] Fix CI --- plugins/jenkins/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 0d0a972a8a..12ea0315b6 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -32,7 +32,7 @@ "react-dom": "^16.13.1", "react-router": "^6.0.0-alpha.5", "react-router-dom": "^6.0.0-alpha.5", - "react-use": "^14.2.0" + "react-use": "^15.3.3" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.16", From affd8f0a2d63461e89bc45f2ba7729a4d1c0ae7a Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Tue, 4 Aug 2020 14:55:40 +0100 Subject: [PATCH 21/26] Fix skipped --- plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx b/plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx index 2edff01799..7f17107374 100644 --- a/plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx +++ b/plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx @@ -53,7 +53,7 @@ const FailCount = ({ count }: { count: number }): JSX.Element | null => { const SkippedCount = ({ count }: { count: number }): JSX.Element | null => { if (count !== 0) { - return <>; + return <>{count} skipped; } return null; }; From 3d450adb7584160e0d7488ca737df26f54f7bdf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 12 Aug 2020 10:34:55 +0200 Subject: [PATCH 22/26] fix(jenkins): cleaned up deps a bit --- packages/app/package.json | 2 +- plugins/jenkins/package.json | 10 +++++----- yarn.lock | 12 ++++++++++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 41d72a89db..f42456709c 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -9,9 +9,9 @@ "@backstage/plugin-circleci": "^0.1.1-alpha.18", "@backstage/plugin-explore": "^0.1.1-alpha.18", "@backstage/plugin-github-actions": "^0.1.1-alpha.18", - "@backstage/plugin-jenkins": "^0.1.1-alpha.18", "@backstage/plugin-gitops-profiles": "^0.1.1-alpha.18", "@backstage/plugin-graphiql": "^0.1.1-alpha.18", + "@backstage/plugin-jenkins": "^0.1.1-alpha.18", "@backstage/plugin-lighthouse": "^0.1.1-alpha.18", "@backstage/plugin-newrelic": "^0.1.1-alpha.18", "@backstage/plugin-register-component": "^0.1.1-alpha.18", diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 12ea0315b6..dce577801e 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -30,19 +30,19 @@ "jenkins": "^0.28.0", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-router": "^6.0.0-alpha.5", - "react-router-dom": "^6.0.0-alpha.5", + "react-router": "6.0.0-beta.0", + "react-router-dom": "6.0.0-beta.0", "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.16", - "@backstage/dev-utils": "^0.1.1-alpha.16", + "@backstage/cli": "^0.1.1-alpha.18", + "@backstage/dev-utils": "^0.1.1-alpha.18", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "@types/testing-library__jest-dom": "^5.0.4", + "@types/testing-library__jest-dom": "^5.9.1", "jest-fetch-mock": "^3.0.3" }, "files": [ diff --git a/yarn.lock b/yarn.lock index 6e4af10f6c..557ced9add 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12309,6 +12309,13 @@ iterate-value@^1.0.0: es-get-iterator "^1.0.2" iterate-iterator "^1.0.1" +jenkins@^0.28.0: + version "0.28.0" + resolved "https://registry.npmjs.org/jenkins/-/jenkins-0.28.0.tgz#72d6fcc452145403b34f6d4ecbd877ee1ab77fca" + integrity sha512-EGzzZcyFwXBCPZZoDNvZTPmZOqaHALfAStNfCF37oh+Mc6G/e0MwIuQjx5kMEynTXR9bF5EwLiuMTiTf5kHk5g== + dependencies: + papi "^0.29.0" + jest-changed-files@^26.0.1: version "26.0.1" resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.0.1.tgz#1334630c6a1ad75784120f39c3aa9278e59f349f" @@ -15325,6 +15332,11 @@ pako@~1.0.5: resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== +papi@^0.29.0: + version "0.29.1" + resolved "https://registry.npmjs.org/papi/-/papi-0.29.1.tgz#7373e2c527f5117d61fd2a0e6c6b1dd72bf7f180" + integrity sha512-Y9ipSMfWuuVFO3zY9PlxOmEg+bQ7CeJ28sa9/a0veYNynLf9fwjR3+3fld5otEy7okUaEOUuCHVH62MyTmACXQ== + parallel-transform@^1.1.0: version "1.2.0" resolved "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc" From bb2a68b1d04d435931acd89beefa9e238f9711bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 12 Aug 2020 10:50:39 +0200 Subject: [PATCH 23/26] chore(jenkins): ran prettier --- packages/app/src/apis.ts | 8 +--- .../src/components/EntityPage/EntityPage.tsx | 21 ++++---- .../JenkinsBuildsWidget.tsx | 12 ++--- .../JenkinsLastBuildWidget.tsx | 48 +++++++------------ .../pages/BuildsPage/lib/Builds/Builds.tsx | 17 ++----- .../lib/Status/JenkinsRunStatus.tsx | 13 +++-- .../src/pages/BuildsPage/lib/Status/index.ts | 2 +- plugins/jenkins/src/plugin.ts | 3 +- 8 files changed, 54 insertions(+), 70 deletions(-) diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index d07bfbcec1..d9e3302146 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -62,8 +62,7 @@ import { GithubActionsClient, githubActionsApiRef, } from '@backstage/plugin-github-actions'; -import {jenkinsApiRef, JenkinsApi} from "@backstage/plugin-jenkins"; - +import { jenkinsApiRef, JenkinsApi } from '@backstage/plugin-jenkins'; import { TravisCIApi, travisCIApiRef } from '@roadiehq/backstage-plugin-travis-ci'; @@ -87,10 +86,7 @@ export const apis = (config: ConfigApi) => { new CircleCIApi(`${backendUrl}/proxy/circleci/api`), ); - builder.add( - jenkinsApiRef, - new JenkinsApi(`${backendUrl}/proxy/jenkins/api`), - ); + builder.add(jenkinsApiRef, new JenkinsApi(`${backendUrl}/proxy/jenkins/api`)); builder.add(githubActionsApiRef, new GithubActionsClient()); diff --git a/plugins/catalog/src/components/EntityPage/EntityPage.tsx b/plugins/catalog/src/components/EntityPage/EntityPage.tsx index c459b721ac..5dd35bf8db 100644 --- a/plugins/catalog/src/components/EntityPage/EntityPage.tsx +++ b/plugins/catalog/src/components/EntityPage/EntityPage.tsx @@ -29,7 +29,10 @@ import { } from '@backstage/core'; import { SentryIssuesWidget } from '@backstage/plugin-sentry'; import { Widget as GithubActionsWidget } from '@backstage/plugin-github-actions'; -import { JenkinsBuildsWidget, JenkinsLastBuildWidget } from '@backstage/plugin-jenkins'; +import { + JenkinsBuildsWidget, + JenkinsLastBuildWidget, +} from '@backstage/plugin-jenkins'; import { Grid, Box } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; import React, { FC, useEffect, useState } from 'react'; @@ -190,17 +193,17 @@ export const EntityPage: FC<{}> = () => {
{entity.metadata?.annotations?.[ 'backstage.io/jenkins-github-folder' - ] && ( - - - + ] && ( + + + )} {entity.metadata?.annotations?.[ 'backstage.io/jenkins-github-folder' - ] && ( - - - + ] && ( + + + )} {entity.metadata?.annotations?.[ 'backstage.io/github-actions-id' diff --git a/plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsBuildsWidget.tsx b/plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsBuildsWidget.tsx index c9f606f334..469869af69 100644 --- a/plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsBuildsWidget.tsx +++ b/plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsBuildsWidget.tsx @@ -15,15 +15,13 @@ */ import React from 'react'; -import {Builds} from '../../pages/BuildsPage/lib/Builds'; -import {Entity} from "@backstage/catalog-model"; +import { Builds } from '../../pages/BuildsPage/lib/Builds'; +import { Entity } from '@backstage/catalog-model'; -export const JenkinsBuildsWidget = ({entity}: { - entity: Entity; -}) => { +export const JenkinsBuildsWidget = ({ entity }: { entity: Entity }) => { const [owner, repo] = ( entity?.metadata.annotations?.['backstage.io/jenkins-github-folder'] ?? '/' ).split('/'); - + return ; -} +}; diff --git a/plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsLastBuildWidget.tsx b/plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsLastBuildWidget.tsx index f81d276a2e..9bd16bf9e6 100644 --- a/plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsLastBuildWidget.tsx +++ b/plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsLastBuildWidget.tsx @@ -15,19 +15,11 @@ */ import React from 'react'; import { Entity } from '@backstage/catalog-model'; -import { - Link, - Theme, - makeStyles, - LinearProgress, -} from '@material-ui/core'; -import { - InfoCard, - StructuredMetadataTable, -} from '@backstage/core'; +import { Link, Theme, makeStyles, LinearProgress } from '@material-ui/core'; +import { InfoCard, StructuredMetadataTable } from '@backstage/core'; import ExternalLinkIcon from '@material-ui/icons/Launch'; -import {useBuilds} from "../../state"; -import {JenkinsRunStatus} from "../../pages/BuildsPage/lib/Status"; +import { useBuilds } from '../../state'; +import { JenkinsRunStatus } from '../../pages/BuildsPage/lib/Status'; const useStyles = makeStyles({ externalLinkIcon: { @@ -37,9 +29,9 @@ const useStyles = makeStyles({ }); const WidgetContent = ({ - loading, - lastRun, - }: { + loading, + lastRun, +}: { loading?: boolean; lastRun: any; branch: string; @@ -51,7 +43,9 @@ const WidgetContent = ({ metadata={{ status: ( <> - + ), build: lastRun.fullDisplayName, @@ -67,28 +61,22 @@ const WidgetContent = ({ }; export const JenkinsLastBuildWidget = ({ - entity, - branch = 'master', - }: { + entity, + branch = 'master', +}: { entity: Entity; branch: string; }) => { const [owner, repo] = ( entity?.metadata.annotations?.['backstage.io/jenkins-github-folder'] ?? '/' ).split('/'); - const [ - {loading, value} - ] = useBuilds(owner, repo, branch); - - const lastRun = value ?? {} ; - + const [{ loading, value }] = useBuilds(owner, repo, branch); + + const lastRun = value ?? {}; + return ( - + ); }; diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/Builds/Builds.tsx b/plugins/jenkins/src/pages/BuildsPage/lib/Builds/Builds.tsx index b08c0ac855..79f081c82d 100644 --- a/plugins/jenkins/src/pages/BuildsPage/lib/Builds/Builds.tsx +++ b/plugins/jenkins/src/pages/BuildsPage/lib/Builds/Builds.tsx @@ -14,20 +14,13 @@ * limitations under the License. */ import React from 'react'; -import {CITable} from '../CITable'; -import {useBuilds} from '../../../../state'; - -export const Builds = ({ - owner, - repo - }: { - owner: string; - repo: string; -}) => { +import { CITable } from '../CITable'; +import { useBuilds } from '../../../../state'; +export const Builds = ({ owner, repo }: { owner: string; repo: string }) => { const [ - {total, loading, value, projectName, page, pageSize}, - {setPage, retry, setPageSize}, + { total, loading, value, projectName, page, pageSize }, + { setPage, retry, setPageSize }, ] = useBuilds(owner, repo); return ( { if (status === undefined) return null; diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/Status/index.ts b/plugins/jenkins/src/pages/BuildsPage/lib/Status/index.ts index 928d91c61d..f5dd4a55e5 100644 --- a/plugins/jenkins/src/pages/BuildsPage/lib/Status/index.ts +++ b/plugins/jenkins/src/pages/BuildsPage/lib/Status/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { JenkinsRunStatus } from './JenkinsRunStatus' +export { JenkinsRunStatus } from './JenkinsRunStatus'; diff --git a/plugins/jenkins/src/plugin.ts b/plugins/jenkins/src/plugin.ts index 62fd3a108e..979b0a31cf 100644 --- a/plugins/jenkins/src/plugin.ts +++ b/plugins/jenkins/src/plugin.ts @@ -15,14 +15,13 @@ */ import { createPlugin, createRouteRef } from '@backstage/core'; -import {DetailedViewPage} from "./pages/BuildWithStepsPage"; +import { DetailedViewPage } from './pages/BuildWithStepsPage'; export const buildRouteRef = createRouteRef({ path: '/jenkins/job', title: 'Jenkins run', }); - export const plugin = createPlugin({ id: 'jenkins', register({ router }) { From c898200065c209977e486c30381511a4b3454277 Mon Sep 17 00:00:00 2001 From: "althaf.hameez" Date: Wed, 12 Aug 2020 19:23:43 +0800 Subject: [PATCH 24/26] Fix typo --- .../src/kinds/GroupEntityV1alpha1.test.ts | 12 ++++++------ .../catalog-model/src/kinds/GroupEntityV1alpha1.ts | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts index e7594772c1..94dcc2f14f 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts @@ -38,7 +38,7 @@ describe('GroupV1alpha1Policy', () => { parent: 'group-a', ancestors: ['group-a', 'global-synergies', 'acme-corp'], children: ['child-a', 'child-b'], - descendents: ['desc-a', 'desc-b'], + descendants: ['desc-a', 'desc-b'], }, }; policy = new GroupEntityV1alpha1Policy(); @@ -108,13 +108,13 @@ describe('GroupV1alpha1Policy', () => { await expect(policy.enforce(entity)).resolves.toBe(entity); }); - it('rejects missing descendents', async () => { - delete (entity as any).spec.descendents; - await expect(policy.enforce(entity)).rejects.toThrow(/descendents/); + it('rejects missing descendants', async () => { + delete (entity as any).spec.descendants; + await expect(policy.enforce(entity)).rejects.toThrow(/descendants/); }); - it('accepts empty descendents', async () => { - (entity as any).spec.descendents = ['']; + it('accepts empty descendants', async () => { + (entity as any).spec.descendants = ['']; await expect(policy.enforce(entity)).resolves.toBe(entity); }); }); diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts index b3ab406729..c1d39d5b44 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts @@ -29,7 +29,7 @@ export interface GroupEntityV1alpha1 extends Entity { parent?: string; ancestors: string[]; children: string[]; - descendents: string[]; + descendants: string[]; }; } @@ -46,7 +46,7 @@ export class GroupEntityV1alpha1Policy implements EntityPolicy { parent: yup.string().notRequired().min(1), ancestors: yup.array(yup.string()).required(), children: yup.array(yup.string()).required(), - descendents: yup.array(yup.string()).required(), + descendants: yup.array(yup.string()).required(), }) .required(), }); From 4bc9a33a3c6c9a6b85f116c7d7e67de591eebcca Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 12 Aug 2020 14:19:22 +0200 Subject: [PATCH 25/26] docs: split getting started section in readme into create-app vs clone Co-authored-by: Ben Lambert --- CONTRIBUTING.md | 30 +++++++++++++++++++++++++++++- README.md | 35 ++++++++++++++++++----------------- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cf7622d692..9981493868 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,7 +60,35 @@ Have you started using Backstage? Adding your company to [ADOPTERS](ADOPTERS.md) # Get Started! -So...feel ready to jump in? Let's do this. Head over to the [Getting Started guide](https://github.com/spotify/backstage#getting-started) 👏🏻💯 +So...feel ready to jump in? Let's do this. 👏🏻💯 + +To run a Backstage app, you will need to have the following installed: + +- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +- [NodeJS](https://nodejs.org/en/download/) - Active LTS Release, currently v12 +- [yarn](https://classic.yarnpkg.com/en/docs/install) + +After cloning this repo, open a terminal window and start the example app using the following commands from the project root: + +```bash +yarn install # Install dependencies + +yarn start # Start dev server, use --check to enable linting and type-checks +``` + +The final `yarn start` command should open a local instance of Backstage in your browser, otherwise open one of the URLs printed in the terminal. + +Depending on the work you're doing, you often also want to run the example backend. Start the backend in a separate terminal session using the following: + +```bash +cd packages/backend + +yarn start + +yarn lerna run mock-data # Populate the backend with mock data +``` + +And that's it! You are good to go 👍 If you need help, just jump into our [Discord chatroom](https://discord.gg/MUpMjP2). diff --git a/README.md b/README.md index 56932a1741..fa8151cbfa 100644 --- a/README.md +++ b/README.md @@ -49,27 +49,33 @@ The Backstage platform consists of a number of different components: - [**proxy**](https://github.com/spotify/backstage/tree/master/plugins/proxy-backend) - Terminates HTTPS and exposes any RESTful API to Plugins. - **identity** - A backend service that holds your organisation's metadata. -## Getting started +## Getting Started -To run a Backstage app, you will need to have the following installed: +There are two different ways to get started with Backstage, either by creating a standalone app, or by cloning this repo. Which method you use depends on what you're planning to do. -- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) -- [NodeJS](https://nodejs.org/en/download/) - Active LTS Release, currently v12 -- [yarn](https://classic.yarnpkg.com/en/docs/install) +Creating a standalone instance makes it simpler to customize the application for your needs whilst staying up to date with the project. You will also depend on `@backstage` packages from NPM, making the project much smaller. This is the recommended approach if you want to kick the tyres of Backstage or setup your own instance. -After cloning this repo, open a terminal window and start the example app using the following commands from the project root: +On the other hand, if you want to contribute plugins or to the project in general, it's easier to fork and clone this project. That will let you stay up to date with the latest changes, and gives you an easier path to make Pull Requests towards this repo. + +### Creating a Standalone App + +Backstage provides the `@backstage/create-app` package to scaffold standalone instances of Backstage. You will need to have +[NodeJS](https://nodejs.org/en/download/) Active LTS Release installed +(currently v12), and [yarn](https://classic.yarnpkg.com/en/docs/install). You will also need to have [Docker](https://docs.docker.com/engine/install/) installed to use some features like Software Templates and TechDocs. + +Using `npx` you can then run the following to create an app in a chosen subdirectory of your current working directory: ```bash -yarn install # Install dependencies - -yarn start # Start dev server, use --check to enable linting and type-checks +npx @backstage/create-app ``` -The final `yarn start` command should open a local instance of Backstage in your browser, otherwise open one of the URLs printed in the terminal. +You will be taken through a wizard to create your app, and the output should look something like this. You can read more about this process [here](docs/getting-started/create-an-app.md) -And that's it! You are good to go 👍 +### Contributing to Backstage -### Next step +You can read more in our [CONTRIBUTING.md](./CONTRIBUTING.md#get-started) guide, which can help you get setup with a Backstage development environment. + +### Next steps Take a look at the [Getting Started](docs/getting-started/index.md) guide to learn how to set up Backstage, and how to develop on the platform. @@ -77,15 +83,10 @@ Take a look at the [Getting Started](docs/getting-started/index.md) guide to lea - [Main documentation](docs/README.md) - [Service Catalog](docs/features/software-catalog/index.md) -- [Create a Backstage App](docs/getting-started/create-an-app.md) - [Architecture](docs/overview/architecture-terminology.md) ([Decisions](docs/architecture-decisions/index.md)) - [Designing for Backstage](docs/dls/design.md) - [Storybook - UI components](http://storybook.backstage.io) -## Contributing - -We would love your help in building Backstage! See [CONTRIBUTING](CONTRIBUTING.md) for more information. - ## Community - [Discord chatroom](https://discord.gg/MUpMjP2) - Get support or discuss the project From 0b252bd4b545c6e2a6cdbef4e3a469a941f56e94 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2020 14:42:03 +0200 Subject: [PATCH 26/26] chore(deps): bump moment from 2.26.0 to 2.27.0 (#1924) Bumps [moment](https://github.com/moment/moment) from 2.26.0 to 2.27.0. - [Release notes](https://github.com/moment/moment/releases) - [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md) - [Commits](https://github.com/moment/moment/compare/2.26.0...2.27.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 557ced9add..f2af00c7fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14267,12 +14267,12 @@ modify-values@^1.0.0: resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== -moment@2.26.0, moment@^2.25.3, moment@^2.26.0: +moment@2.26.0: version "2.26.0" resolved "https://registry.npmjs.org/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a" integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw== -moment@^2.27.0: +moment@^2.25.3, moment@^2.26.0, moment@^2.27.0: version "2.27.0" resolved "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==