From 3dbdd1cfd30f652002a32a7cd604edfa821762cf Mon Sep 17 00:00:00 2001 From: Peter Grauvogel Date: Mon, 1 Mar 2021 19:24:42 +0100 Subject: [PATCH] feat(bitrise): add bitrise ci plugin Plugin to show bitrise.io builds and download build artifacts. Co-authored-by: Adrian Barwicki Co-authored-by: Kai Szybiak Co-authored-by: Dominik Schwank Signed-off-by: Peter Grauvogel --- .github/styles/vocab.txt | 1 + microsite/data/plugins/bitrise.yaml | 9 + plugins/bitrise/.eslintrc.js | 3 + plugins/bitrise/README.md | 62 +++++ plugins/bitrise/dev/index.tsx | 173 +++++++++++++ plugins/bitrise/package.json | 55 ++++ .../bitrise/src/api/bitriseApi.client.test.ts | 183 ++++++++++++++ plugins/bitrise/src/api/bitriseApi.client.ts | 133 ++++++++++ plugins/bitrise/src/api/bitriseApi.model.ts | 109 ++++++++ plugins/bitrise/src/api/bitriseApi.ts | 47 ++++ .../BitriseArtifactsComponent.test.tsx | 89 +++++++ .../BitriseArtifactsComponent.tsx | 66 +++++ .../BitriseArtifactsComponent/index.ts | 17 ++ .../BitriseBuildDetailsDialog.test.tsx | 59 +++++ .../BitriseBuildDetailsDialog.tsx | 84 +++++++ .../BitriseBuildDetailsDialog/index.ts | 17 ++ .../BitriseBuildsComponent.test.tsx | 57 +++++ .../BitriseBuildsComponent.tsx | 86 +++++++ .../BitriseBuildsComponent/index.ts | 20 ++ .../BitriseBuildsTableComponent.test.tsx | 112 +++++++++ .../BitriseBuildsTableComponent.tsx | 235 ++++++++++++++++++ .../BitriseBuildsTableComponent/index.ts | 17 ++ .../BitriseDownloadArtifactComponent.tsx | 80 ++++++ .../BitriseDownloadArtifactComponent/index.ts | 17 ++ .../bitrise/src/components/Select/Select.tsx | 83 +++++++ .../bitrise/src/components/Select/index.ts | 18 ++ .../components/useBitriseArtifactDetails.ts | 34 +++ .../src/components/useBitriseArtifacts.ts | 30 +++ plugins/bitrise/src/extensions.ts | 29 +++ .../src/hooks/useBitriseBuildWorkflows.ts | 46 ++++ plugins/bitrise/src/hooks/useBitriseBuilds.ts | 58 +++++ plugins/bitrise/src/index.ts | 18 ++ plugins/bitrise/src/plugin.test.ts | 23 ++ plugins/bitrise/src/plugin.ts | 44 ++++ plugins/bitrise/src/setupTests.ts | 18 ++ yarn.lock | 22 +- 36 files changed, 2153 insertions(+), 1 deletion(-) create mode 100644 microsite/data/plugins/bitrise.yaml create mode 100644 plugins/bitrise/.eslintrc.js create mode 100644 plugins/bitrise/README.md create mode 100644 plugins/bitrise/dev/index.tsx create mode 100644 plugins/bitrise/package.json create mode 100644 plugins/bitrise/src/api/bitriseApi.client.test.ts create mode 100644 plugins/bitrise/src/api/bitriseApi.client.ts create mode 100644 plugins/bitrise/src/api/bitriseApi.model.ts create mode 100644 plugins/bitrise/src/api/bitriseApi.ts create mode 100644 plugins/bitrise/src/components/BitriseArtifactsComponent/BitriseArtifactsComponent.test.tsx create mode 100644 plugins/bitrise/src/components/BitriseArtifactsComponent/BitriseArtifactsComponent.tsx create mode 100644 plugins/bitrise/src/components/BitriseArtifactsComponent/index.ts create mode 100644 plugins/bitrise/src/components/BitriseBuildDetailsDialog/BitriseBuildDetailsDialog.test.tsx create mode 100644 plugins/bitrise/src/components/BitriseBuildDetailsDialog/BitriseBuildDetailsDialog.tsx create mode 100644 plugins/bitrise/src/components/BitriseBuildDetailsDialog/index.ts create mode 100644 plugins/bitrise/src/components/BitriseBuildsComponent/BitriseBuildsComponent.test.tsx create mode 100644 plugins/bitrise/src/components/BitriseBuildsComponent/BitriseBuildsComponent.tsx create mode 100644 plugins/bitrise/src/components/BitriseBuildsComponent/index.ts create mode 100644 plugins/bitrise/src/components/BitriseBuildsTableComponent/BitriseBuildsTableComponent.test.tsx create mode 100644 plugins/bitrise/src/components/BitriseBuildsTableComponent/BitriseBuildsTableComponent.tsx create mode 100644 plugins/bitrise/src/components/BitriseBuildsTableComponent/index.ts create mode 100644 plugins/bitrise/src/components/BitriseDownloadArtifactComponent/BitriseDownloadArtifactComponent.tsx create mode 100644 plugins/bitrise/src/components/BitriseDownloadArtifactComponent/index.ts create mode 100644 plugins/bitrise/src/components/Select/Select.tsx create mode 100644 plugins/bitrise/src/components/Select/index.ts create mode 100644 plugins/bitrise/src/components/useBitriseArtifactDetails.ts create mode 100644 plugins/bitrise/src/components/useBitriseArtifacts.ts create mode 100644 plugins/bitrise/src/extensions.ts create mode 100644 plugins/bitrise/src/hooks/useBitriseBuildWorkflows.ts create mode 100644 plugins/bitrise/src/hooks/useBitriseBuilds.ts create mode 100644 plugins/bitrise/src/index.ts create mode 100644 plugins/bitrise/src/plugin.test.ts create mode 100644 plugins/bitrise/src/plugin.ts create mode 100644 plugins/bitrise/src/setupTests.ts diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index e60d9b9528..be5d966d62 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -5,6 +5,7 @@ Avro Balachandran Bigtable Billett +Bitrise Blackbox Chai Changesets diff --git a/microsite/data/plugins/bitrise.yaml b/microsite/data/plugins/bitrise.yaml new file mode 100644 index 0000000000..2a362a9d0f --- /dev/null +++ b/microsite/data/plugins/bitrise.yaml @@ -0,0 +1,9 @@ +--- +title: Bitrise +author: SDA SE +authorUrl: https://sda.se/ +category: CI/CD +description: View Bitrise builds and download the build artifacts within Backstage. +documentation: https://github.com/backstage/backstage/blob/master/plugins/bitrise/README.md +iconUrl: https://avatars.githubusercontent.com/u/7174390?s=400&v=4 +npmPackageName: '@backstage/plugin-bitrise' diff --git a/plugins/bitrise/.eslintrc.js b/plugins/bitrise/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/bitrise/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/bitrise/README.md b/plugins/bitrise/README.md new file mode 100644 index 0000000000..2bff1f0d74 --- /dev/null +++ b/plugins/bitrise/README.md @@ -0,0 +1,62 @@ +# Bitrise + +Welcome to the Bitrise plugin! + +- View recent Bitrise Builds for a Bitrise application +- Download build artifacts + +## Installation + +```sh +$ yarn add @backstage/plugin-bitrise +``` + +Then make sure to export the plugin in your app's [`plugins.ts`](https://github.com/backstage/backstage/blob/master/packages/app/src/plugins.ts) to enable the plugin: + +```js +export { bitrisePlugin } from '@backstage/plugin-bitrise'; +``` + +Bitrise Plugin exposes an "Entity Tab Content" component `EntityBitriseContent`. You can include it in the [`EntityPage.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/components/catalog/EntityPage.tsx)`: + +```tsx +// At the top imports +import { EntityBitriseContent } from '@backstage/plugin-bitrise'; + +// Inside `WebsiteEntityPage` component + + + + + + } +/>; +``` + +Now your plugin should be visible in the entity page, however in a state with a missing `bitrise.io/app` annotation. + +Add the annotation to your component [catalog-info.yaml](https://github.com/backstage/backstage/blob/master/catalog-info.yaml) as shown in the highlighted example below: + +```yaml +metadata: + annotations: + bitrise.io/app: '' +``` + +The plugin requires to configure a Bitrise API proxy with a `BITRISE_AUTH_TOKEN` for authentication in the [app-config.yaml](https://github.com/backstage/backstage/blob/master/app-config.yaml): + +```yaml +proxy: + '/bitrise': + target: 'https://api.bitrise.io/v0.1' + allowedMethods: ['GET'] + headers: + Authorization: + $env: BITRISE_AUTH_TOKEN +``` + +Learn on https://devcenter.bitrise.io/api/authentication how to create a new Bitrise token. diff --git a/plugins/bitrise/dev/index.tsx b/plugins/bitrise/dev/index.tsx new file mode 100644 index 0000000000..8d328b3e7f --- /dev/null +++ b/plugins/bitrise/dev/index.tsx @@ -0,0 +1,173 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { Content, Header, Page } from '@backstage/core'; +import { createDevApp, EntityGridItem } from '@backstage/dev-utils'; +import React from 'react'; +import { EntityBitriseContent } from '../src'; +import { BitriseApi } from '../src/api/bitriseApi'; +import { + BitriseBuildArtifact, + BitriseBuildArtifactDetails, + BitriseBuildResult, + BitrisePagingResponse, +} from '../src/api/bitriseApi.model'; +import { BITRISE_APP_ANNOTATION } from '../src/components/BitriseBuildsComponent'; +import { bitriseApiRef } from '../src/plugin'; + +const mockedPagingResponse: BitrisePagingResponse = { + next: 'fae3232de3d2', + page_item_limit: 20, + total_item_count: 400, +}; + +const entities: (string | undefined)[] = [ + 'builds', + 'searchBuilds', + 'searchNotFound', + 'empty', + 'error', + 'never', + undefined, +]; + +const mockedBuildResults: BitriseBuildResult[] = [ + { + appSlug: 'ios-app', + buildSlug: '3291j9390d39239d3', + commitHash: '27c988e4f13e26bc2d1b0af292026a3079aqsxe1', + id: 29525, + message: 'develop', + source: + 'https://github.com/ORG/APPNAME/commit/27c988e4f13e26bc2d1b0af292026a3079aqsxe1', + statusText: 'success', + status: 1, + workflow: 'app-ios-test', + triggerTime: '2019-01-27T17:55:17Z', + duration: '12 minutes', + }, + { + appSlug: 'ios-app', + buildSlug: '38i23sk2923js9231s1', + commitHash: '13c988d4f13e06bcdd1b0af292086a3079cdaxb0', + id: 19523, + message: 'main', + source: + 'https://github.com/ORG/APPNAME/commit/13c988d4f13e06bcdd1b0af292086a3079cdaxb0', + statusText: 'in-progress', + status: 0, + workflow: 'app-ios-release', + triggerTime: '2020-01-28T17:55:17Z', + duration: '19 minutes', + }, +]; +const entity = (name?: string) => + ({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + annotations: { + [BITRISE_APP_ANNOTATION]: name, + }, + name: name, + }, + } as Entity); + +createDevApp() + .registerApi({ + api: bitriseApiRef, + deps: {}, + factory: () => + ({ + getBuilds: async (appName: string) => { + switch (appName) { + case 'error': + throw new Error('Error!'); + + case 'never': + return new Promise(() => {}); + + case 'builds': + return { data: mockedBuildResults, paging: mockedPagingResponse }; + + case 'searchBuilds': + return { + data: [mockedBuildResults.find(b => b.message === 'develop')], + paging: mockedPagingResponse, + }; + + case 'searchNotFound': + return []; + + default: + return undefined; + } + }, + getBuildWorkflows: async (_buildSlug: string) => { + return ['app-ios-release', 'app-ios-test']; + }, + getApp: async (_appName: string) => { + return { + slug: _appName, + }; + }, + getBuildArtifacts: async (_appName: string, _buildSlug: string) => { + return [ + { + title: 'App artifact', + slug: 'some-artifact-slug', + }, + { + title: 'App artifact 2', + slug: 'some-artifact-slug-2', + }, + { + title: 'App artifact 3', + slug: 'some-artifact-slug-3', + }, + ] as BitriseBuildArtifact[]; + }, + getArtifactDetails: async ( + _appName: string, + _buildSlug: string, + _artifactSlug: string, + ) => { + return { + title: 'App artifact', + slug: 'some-artifact-slug', + public_install_page_url: 'some-url', + expiring_download_url: 'some-url', + } as BitriseBuildArtifactDetails; + }, + } as BitriseApi), + }) + .addPage({ + title: 'Bitrise CI', + element: ( + +
+ + {entities.map(entityItem => ( + + + + ))} + + + ), + }) + .render(); diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json new file mode 100644 index 0000000000..bc72dcc7b0 --- /dev/null +++ b/plugins/bitrise/package.json @@ -0,0 +1,55 @@ +{ + "name": "@backstage/plugin-bitrise", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/catalog-model": "^0.7.2", + "@backstage/core": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.1.0", + "@backstage/theme": "^0.2.3", + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "cross-fetch": "^3.0.6", + "lodash": "^4.17.21", + "luxon": "^1.26.0", + "qs": "^6.9.6", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-use": "^15.3.3", + "recharts": "^1.8.5" + }, + "devDependencies": { + "@backstage/cli": "^0.6.3", + "@backstage/dev-utils": "^0.1.13", + "@backstage/test-utils": "^0.1.8", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32", + "@types/recharts": "^1.8.15", + "cross-fetch": "^3.0.6", + "msw": "^0.21.2" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/bitrise/src/api/bitriseApi.client.test.ts b/plugins/bitrise/src/api/bitriseApi.client.test.ts new file mode 100644 index 0000000000..9e125d8d06 --- /dev/null +++ b/plugins/bitrise/src/api/bitriseApi.client.test.ts @@ -0,0 +1,183 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { UrlPatternDiscovery } from '@backstage/core'; +import { msw } from '@backstage/test-utils'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { BitriseClientApi } from './bitriseApi.client'; +import { BitriseApi } from './bitriseApi'; + +const server = setupServer(); + +describe('BitriseClientApi', () => { + msw.setupDefaultHandlers(server); + + const mockBaseUrl = 'http://backstage:9191/api/proxy'; + const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); + let client: BitriseApi; + + beforeEach(() => { + client = new BitriseClientApi(discoveryApi); + }); + + describe('getBuilds()', () => { + it('should get builds for given workflow', async () => { + server.use( + rest.get(`${mockBaseUrl}/*`, (_req, res, ctx) => { + return res( + ctx.json({ + data: [{ slug: 'some-build-slug' }], + }), + ); + }), + ); + + const builds = await client.getBuilds('some-app-slug', { + workflow: 'ios-develop', + }); + + expect(builds.data.length).toBe(1); + expect(builds.data[0].appSlug).toBe('some-app-slug'); + }); + + it('should get builds and map the results', async () => { + server.use( + rest.get(`${mockBaseUrl}/*`, (_req, res, ctx) => { + return res( + ctx.json({ + data: [ + { slug: 'some-build-slug' }, + { slug: 'some-build-slug-2' }, + ], + }), + ); + }), + ); + + const builds = await client.getBuilds('some-app-slug', { + workflow: '', + }); + + expect(builds.data.length).toBe(2); + expect(builds.data[0].appSlug).toBe('some-app-slug'); + expect(builds.data[0].buildSlug).toBe('some-build-slug'); + expect(builds.data[1].buildSlug).toBe('some-build-slug-2'); + }); + }); + + describe('getApp()', () => { + it('should get the app', async () => { + server.use( + rest.get(`${mockBaseUrl}/*`, (_req, res, ctx) => { + return res( + ctx.json({ + data: [{ title: 'some-app-title', slug: 'some-app-slug' }], + }), + ); + }), + ); + + const app = await client.getApp('some-app-title'); + + expect(app).toBeDefined(); + expect(app?.slug).toBe('some-app-slug'); + }); + }); + + describe('getBuildWorkflows()', () => { + it('should get workflows', async () => { + server.use( + rest.get(`${mockBaseUrl}/*`, (_req, res, ctx) => { + return res( + ctx.json({ + data: ['ios-develop', 'ios-master'], + }), + ); + }), + ); + + const workflows = await client.getBuildWorkflows('some-app-title'); + + expect(workflows).toBeDefined(); + expect(workflows.length).toEqual(2); + expect(workflows[0]).toBe('ios-develop'); + expect(workflows[1]).toBe('ios-master'); + }); + }); + + describe('getBuildArtifacts()', () => { + it('should get artifacts for a build', async () => { + server.use( + rest.get( + `${mockBaseUrl}/bitrise/apps/*/builds/*/artifacts`, + (_req, res, ctx) => { + return res( + ctx.json({ + data: [ + { + title: 'some-artifact-title-1', + slug: 'some-artifact-slug-1', + }, + { + title: 'some-artifact-title-2', + slug: 'some-artifact-slug-2', + }, + ], + }), + ); + }, + ), + ); + + const artifacts = await client.getBuildArtifacts( + 'some-app-slug', + 'some-build-slug', + ); + + expect(artifacts).toBeDefined(); + expect(artifacts.length).toBe(2); + expect(artifacts[0].slug).toBe('some-artifact-slug-1'); + expect(artifacts[1].slug).toBe('some-artifact-slug-2'); + }); + }); + + describe('getArtifactDetails()', () => { + it('should get the artifact details', async () => { + server.use( + rest.get(`${mockBaseUrl}/*`, (_req, res, ctx) => { + return res( + ctx.json({ + data: { + title: 'some-artifact-title', + slug: 'some-artifact-slug', + }, + }), + ); + }), + ); + + const artifact = await client.getArtifactDetails( + 'some-app-slug', + 'some-build-slug', + 'some-artifact-slug', + ); + + expect(artifact).toBeDefined(); + expect(artifact?.title).toBe('some-artifact-title'); + }); + }); +}); diff --git a/plugins/bitrise/src/api/bitriseApi.client.ts b/plugins/bitrise/src/api/bitriseApi.client.ts new file mode 100644 index 0000000000..4fc66c55a1 --- /dev/null +++ b/plugins/bitrise/src/api/bitriseApi.client.ts @@ -0,0 +1,133 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DiscoveryApi } from '@backstage/core'; +import { BitriseApi } from './bitriseApi'; +import { + BitriseBuildResponseItem, + BitriseApp, + BitriseBuildArtifact, + BitriseBuildArtifactDetails, + BitriseQueryParams, + BitriseBuildListResponse, + BitriseBuildResult, +} from './bitriseApi.model'; +import qs from 'qs'; +import { DateTime, Interval } from 'luxon'; +import { pickBy, identity } from 'lodash'; + +export class BitriseClientApi implements BitriseApi { + constructor(private readonly discoveryApi: DiscoveryApi) {} + + async getArtifactDetails( + appSlug: string, + buildSlug: string, + artifactSlug: string, + ): Promise { + const baseUrl = await this.discoveryApi.getBaseUrl('proxy'); + const artifactResponse = await fetch( + `${baseUrl}/bitrise/apps/${appSlug}/builds/${buildSlug}/artifacts/${artifactSlug}`, + ); + + const data = await artifactResponse.json(); + + return data.data; + } + + async getBuildArtifacts( + appSlug: string, + buildSlug: string, + ): Promise { + const baseUrl = await this.discoveryApi.getBaseUrl('proxy'); + const response = await fetch( + `${baseUrl}/bitrise/apps/${appSlug}/builds/${buildSlug}/artifacts`, + ); + + const data = await response.json(); + + return data.data; + } + + async getApps(): Promise { + const baseUrl = await this.discoveryApi.getBaseUrl('proxy'); + const appsResponse = await fetch(`${baseUrl}/bitrise/apps`); + + const appsData = await appsResponse.json(); + + return appsData.data; + } + + async getApp(appName: string): Promise { + const apps = await this.getApps(); + + return apps.find(app => app.title === appName); + } + + async getBuildWorkflows(appSlug: string): Promise { + const baseUrl = await this.discoveryApi.getBaseUrl('proxy'); + const response = await fetch( + `${baseUrl}/bitrise/apps/${appSlug}/build-workflows`, + ); + const data = await response.json(); + return data.data; + } + + async getBuilds( + appSlug: string, + params?: BitriseQueryParams, + ): Promise { + const baseUrl = await this.discoveryApi.getBaseUrl('proxy'); + let url = `${baseUrl}/bitrise/apps/${appSlug}/builds`; + + if (params) { + url = `${url}?${qs.stringify(pickBy(params, identity))}`; + } + + const response = await fetch(url); + const data = await response.json(); + const builds: BitriseBuildResponseItem[] = data.data; + + return { + data: builds.map( + (build: BitriseBuildResponseItem): BitriseBuildResult => { + const duration = String( + Math.round( + Interval.fromDateTimes( + DateTime.fromISO(build.started_on_worker_at), + DateTime.fromISO(build.finished_at), + ).length('minutes'), + ), + ); + + return { + id: build.build_number, + source: build.commit_view_url, + status: build.status, + statusText: build.status_text, + buildSlug: build.slug, + message: `${build.branch}`, + workflow: build.triggered_workflow, + commitHash: `${build.commit_hash}`, + triggerTime: build.triggered_at, + duration: `${duration} minutes`, + appSlug, + }; + }, + ), + paging: data.paging, + }; + } +} diff --git a/plugins/bitrise/src/api/bitriseApi.model.ts b/plugins/bitrise/src/api/bitriseApi.model.ts new file mode 100644 index 0000000000..f9aabe6690 --- /dev/null +++ b/plugins/bitrise/src/api/bitriseApi.model.ts @@ -0,0 +1,109 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type BitriseApp = { + title: string; + slug: string; +}; + +export type BitriseBuildArtifact = { + slug: string; + title: string; + artifact_type: string; +}; + +export type BitriseBuildArtifactDetails = { + slug: string; + title: string; + artifact_type: string; + public_install_page_url: string; + expiring_download_url: string; +}; + +export interface BitriseBuildResponseItem { + abort_reason: string; + branch: string; + build_number: number; + commit_hash: string; + commit_message: string; + commit_view_url: string; + environment_prepare_finished_at: string; + finished_at: string; + is_on_hold: true; + machine_type_id: string; + original_build_params: string; + pull_request_id: number; + pull_request_target_branch: string; + pull_request_view_url: string; + slug: string; + stack_identifier: string; + started_on_worker_at: string; + status: number; + status_text: string; + tag: string; + triggered_at: string; + triggered_by: string; + triggered_workflow: string; +} + +// not finished (0), successful (1), failed (2), aborted with failure (3), aborted with success (4) +export enum BitriseBuildResultStatus { + notFinished, + successful, + failed, + abortedWithFailure, + abortedWithSuccess, +} + +export interface BitriseBuildResult { + id: number; + source: string; + status: BitriseBuildResultStatus; + statusText: string; + buildSlug: string; + message: string; + workflow: string; + commitHash: string; + triggerTime: string; + duration: string; + appSlug: string; +} + +export interface BitriseQueryParams { + workflow: string; + branch?: string; + limit?: number; + next?: string; +} + +export interface BitriseBuildListResponse { + data: BitriseBuildResult[]; + paging?: BitrisePagingResponse; +} + +export interface BitrisePagingResponse { + /** + * Slug of the first build in the next page, `undefined` if there are no more results. + */ + next: string; + + /** + * Max number of elements per page (default: 50) + */ + page_item_limit: number; + + total_item_count: number; +} diff --git a/plugins/bitrise/src/api/bitriseApi.ts b/plugins/bitrise/src/api/bitriseApi.ts new file mode 100644 index 0000000000..676085049f --- /dev/null +++ b/plugins/bitrise/src/api/bitriseApi.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + BitriseApp, + BitriseBuildArtifact, + BitriseBuildArtifactDetails, + BitriseBuildListResponse, + BitriseQueryParams, +} from './bitriseApi.model'; + +export interface BitriseApi { + getBuilds( + appSlug: string, + params?: BitriseQueryParams, + ): Promise; + + getBuildWorkflows(appSlug: string): Promise; + + getBuildArtifacts( + appSlug: string, + buildSlug: string, + ): Promise; + + getArtifactDetails( + appSlug: string, + buildSlug: string, + artifactSlug: string, + ): Promise; + + getApps(): Promise; + + getApp(appName: string): Promise; +} diff --git a/plugins/bitrise/src/components/BitriseArtifactsComponent/BitriseArtifactsComponent.test.tsx b/plugins/bitrise/src/components/BitriseArtifactsComponent/BitriseArtifactsComponent.test.tsx new file mode 100644 index 0000000000..7a56d438e6 --- /dev/null +++ b/plugins/bitrise/src/components/BitriseArtifactsComponent/BitriseArtifactsComponent.test.tsx @@ -0,0 +1,89 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { BitriseArtifactsComponent } from './BitriseArtifactsComponent'; +import { useBitriseArtifacts } from '../useBitriseArtifacts'; +import { useBitriseArtifactDetails } from '../useBitriseArtifactDetails'; +import { BitriseBuildResult } from '../../api/bitriseApi.model'; + +jest.mock('../useBitriseArtifacts', () => ({ + useBitriseArtifacts: jest.fn(), +})); + +jest.mock('../useBitriseArtifactDetails', () => ({ + useBitriseArtifactDetails: jest.fn(), +})); + +describe('BitriseArtifactsComponent', () => { + const renderComponent = () => + render( + , + ); + + it('should display a progress bar when in a loading state', async () => { + (useBitriseArtifacts as jest.Mock).mockReturnValue({ loading: true }); + + const rendered = renderComponent(); + + expect(await rendered.findByTestId('progress')).toBeInTheDocument(); + }); + + it('should display an error bar when in an error state', async () => { + (useBitriseArtifacts as jest.Mock).mockReturnValue({ error: 'Ups!' }); + + const rendered = renderComponent(); + + expect(await rendered.findByRole('alert')).toBeInTheDocument(); + }); + + it('should display `no records` message if there are no artifacts', async () => { + (useBitriseArtifacts as jest.Mock).mockReturnValue({ value: [] }); + + const rendered = renderComponent(); + + expect(await rendered.findByText('No artifacts')).toBeInTheDocument(); + }); + + it('should display a table if there are artifacts', async () => { + (useBitriseArtifacts as jest.Mock).mockReturnValue({ + value: [ + { title: 'some-title-1', slug: 'some-slug' }, + { title: 'some-title-2', slug: 'some-slug' }, + ], + }); + + (useBitriseArtifactDetails as jest.Mock).mockReturnValue({ + value: { + public_install_page_url: 'some-url', + expiring_download_url: 'some-url-2', + }, + }); + + const rendered = renderComponent(); + + expect(await rendered.findByText('some-title-1')).toBeInTheDocument(); + expect(await rendered.findByText('some-title-2')).toBeInTheDocument(); + }); +}); diff --git a/plugins/bitrise/src/components/BitriseArtifactsComponent/BitriseArtifactsComponent.tsx b/plugins/bitrise/src/components/BitriseArtifactsComponent/BitriseArtifactsComponent.tsx new file mode 100644 index 0000000000..6ff1d50db5 --- /dev/null +++ b/plugins/bitrise/src/components/BitriseArtifactsComponent/BitriseArtifactsComponent.tsx @@ -0,0 +1,66 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { BitriseBuildResult } from '../../api/bitriseApi.model'; +import { Alert } from '@material-ui/lab'; +import { Progress } from '@backstage/core'; +import { BitriseDownloadArtifactComponent } from '../BitriseDownloadArtifactComponent'; +import { useBitriseArtifacts } from '../useBitriseArtifacts'; +import { + List, + ListItem, + ListItemSecondaryAction, + ListItemText, +} from '@material-ui/core'; + +type BitriseArtifactsComponentComponentProps = { + build: BitriseBuildResult; +}; + +export const BitriseArtifactsComponent = ( + props: BitriseArtifactsComponentComponentProps, +) => { + const { value, loading, error } = useBitriseArtifacts( + props.build.appSlug, + props.build.buildSlug, + ); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } else if (!value || !value.length) { + return No artifacts; + } + + return ( + + {value.map(row => ( + + + + + + + ))} + + ); +}; diff --git a/plugins/bitrise/src/components/BitriseArtifactsComponent/index.ts b/plugins/bitrise/src/components/BitriseArtifactsComponent/index.ts new file mode 100644 index 0000000000..24e48b9e65 --- /dev/null +++ b/plugins/bitrise/src/components/BitriseArtifactsComponent/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { BitriseArtifactsComponent } from './BitriseArtifactsComponent'; diff --git a/plugins/bitrise/src/components/BitriseBuildDetailsDialog/BitriseBuildDetailsDialog.test.tsx b/plugins/bitrise/src/components/BitriseBuildDetailsDialog/BitriseBuildDetailsDialog.test.tsx new file mode 100644 index 0000000000..9f719ec84c --- /dev/null +++ b/plugins/bitrise/src/components/BitriseBuildDetailsDialog/BitriseBuildDetailsDialog.test.tsx @@ -0,0 +1,59 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { BitriseBuildDetailsDialog } from './BitriseBuildDetailsDialog'; +import { BitriseBuildResult } from '../../api/bitriseApi.model'; + +jest.mock('../BitriseArtifactsComponent', () => ({ + BitriseArtifactsComponent: (_props: { build: string }) => <>, +})); + +describe('BitriseArtifactsComponent', () => { + const renderComponent = () => + render( + , + ); + + it('should display a button', async () => { + const rendered = renderComponent(); + + expect(await rendered.findByTestId('btn')).toBeInTheDocument(); + }); + + it('should change the state when the button is clicked', async () => { + const setOpen = jest.fn(); + const useStateMock: any = (initState: any) => [initState, setOpen]; + + jest.spyOn(React, 'useState').mockImplementation(useStateMock); + + const rendered = renderComponent(); + + const btn = await rendered.findByTestId('btn'); + + btn.click(); + + expect(setOpen).toHaveBeenCalled(); + }); +}); diff --git a/plugins/bitrise/src/components/BitriseBuildDetailsDialog/BitriseBuildDetailsDialog.tsx b/plugins/bitrise/src/components/BitriseBuildDetailsDialog/BitriseBuildDetailsDialog.tsx new file mode 100644 index 0000000000..8065567daf --- /dev/null +++ b/plugins/bitrise/src/components/BitriseBuildDetailsDialog/BitriseBuildDetailsDialog.tsx @@ -0,0 +1,84 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useState } from 'react'; +import { BitriseBuildResult } from '../../api/bitriseApi.model'; +import { BitriseArtifactsComponent } from '../BitriseArtifactsComponent'; +import { + Chip, + Dialog, + DialogContent, + DialogTitle, + IconButton, + makeStyles, +} from '@material-ui/core'; +import CloudDownload from '@material-ui/icons/CloudDownload'; +import CloseIcon from '@material-ui/icons/Close'; + +type BitriseBuildDetailsDialogProps = { + build: BitriseBuildResult; +}; + +const useStyles = makeStyles({ + dialogContent: { + paddingBottom: 16, + }, +}); + +export const BitriseBuildDetailsDialog = ({ + build, +}: BitriseBuildDetailsDialogProps) => { + const classes = useStyles(); + const [open, setOpen] = useState(false); + + const handleOpen = () => { + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + return ( + <> + + + + + + + {`Download artifacts for build #${build.id}?`} + + + + + +
+ + +
+ + + +
+ + ); +}; diff --git a/plugins/bitrise/src/components/BitriseBuildDetailsDialog/index.ts b/plugins/bitrise/src/components/BitriseBuildDetailsDialog/index.ts new file mode 100644 index 0000000000..76dfc73408 --- /dev/null +++ b/plugins/bitrise/src/components/BitriseBuildDetailsDialog/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { BitriseBuildDetailsDialog } from './BitriseBuildDetailsDialog'; diff --git a/plugins/bitrise/src/components/BitriseBuildsComponent/BitriseBuildsComponent.test.tsx b/plugins/bitrise/src/components/BitriseBuildsComponent/BitriseBuildsComponent.test.tsx new file mode 100644 index 0000000000..d6b9937e0c --- /dev/null +++ b/plugins/bitrise/src/components/BitriseBuildsComponent/BitriseBuildsComponent.test.tsx @@ -0,0 +1,57 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { BitriseBuildsComponent } from './BitriseBuildsComponent'; + +let entityValue: { + entity: { metadata: { annotations?: { [key: string]: string } } }; +}; + +jest.mock('../BitriseArtifactsComponent', () => ({ + BitriseArtifactsComponent: (_props: { build: string }) => <>, +})); + +jest.mock('../../hooks/useBitriseBuildWorkflows', () => ({ + useBitriseBuildWorkflows: () => [], +})); + +jest.mock('@backstage/plugin-catalog-react', () => ({ + useEntity: () => { + return entityValue; + }, +})); + +jest.mock('../BitriseBuildsTableComponent', () => ({ + BitriseBuildsTable: (_props: { + appName: string; + workflow: string; + error: string; + }) => <>mock builds table, +})); + +describe('BitriseArtifactsComponent', () => { + entityValue = { entity: { metadata: {} } }; + + const renderComponent = () => render(); + + it('should display an empty state if an app annotation is missing', async () => { + const rendered = renderComponent(); + + expect(await rendered.findByText('Missing Annotation')).toBeInTheDocument(); + }); +}); diff --git a/plugins/bitrise/src/components/BitriseBuildsComponent/BitriseBuildsComponent.tsx b/plugins/bitrise/src/components/BitriseBuildsComponent/BitriseBuildsComponent.tsx new file mode 100644 index 0000000000..0e4bd3e5db --- /dev/null +++ b/plugins/bitrise/src/components/BitriseBuildsComponent/BitriseBuildsComponent.tsx @@ -0,0 +1,86 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { + Content, + ContentHeader, + MissingAnnotationEmptyState, + Page, +} from '@backstage/core'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import React, { useState } from 'react'; +import { useBitriseBuildWorkflows } from '../../hooks/useBitriseBuildWorkflows'; +import { AsyncState } from 'react-use/lib/useAsync'; +import { BitriseBuildsTable } from '../BitriseBuildsTableComponent'; +import { Item, Select } from '../Select'; + +export type Props = { + entity: Entity; +}; + +export const BITRISE_APP_ANNOTATION = 'bitrise.io/app'; + +export const BitriseBuildsComponent = () => { + const { entity } = useEntity(); + const appName = entity.metadata.annotations?.[ + BITRISE_APP_ANNOTATION + ] as string; + const [selectedWorkflow, setSelectedWorkflow] = useState(' '); + const workflows = useBitriseBuildWorkflows(appName); + + const onSelectedWorkflowChanged = (workflow: any) => { + setSelectedWorkflow(workflow); + }; + + const getSelectionItems = (items: AsyncState): Item[] => { + const noWorkflowSpecified: Item = { label: 'All workflows', value: ' ' }; + return items.value + ? [ + noWorkflowSpecified, + ...items.value.map(item => { + return { label: item, value: item }; + }), + ] + : []; + }; + + return ( + <> + {!appName ? ( + + ) : ( + + + + + {renderItems(items)} + + + ); +}; diff --git a/plugins/bitrise/src/components/Select/index.ts b/plugins/bitrise/src/components/Select/index.ts new file mode 100644 index 0000000000..0b1b36cca1 --- /dev/null +++ b/plugins/bitrise/src/components/Select/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { SelectComponent as Select } from './Select'; +export type { Item } from './Select'; diff --git a/plugins/bitrise/src/components/useBitriseArtifactDetails.ts b/plugins/bitrise/src/components/useBitriseArtifactDetails.ts new file mode 100644 index 0000000000..06c38cf8b1 --- /dev/null +++ b/plugins/bitrise/src/components/useBitriseArtifactDetails.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useApi } from '@backstage/core'; +import { useAsync } from 'react-use'; +import { BitriseBuildArtifactDetails } from '../api/bitriseApi.model'; +import { bitriseApiRef } from '../plugin'; + +export const useBitriseArtifactDetails = ( + appSlug: string, + buildSlug: string, + artifactSlug: string, +) => { + const bitriseApi = useApi(bitriseApiRef); + + return useAsync( + (): Promise => + bitriseApi.getArtifactDetails(appSlug, buildSlug, artifactSlug), + [bitriseApi, appSlug, buildSlug, artifactSlug], + ); +}; diff --git a/plugins/bitrise/src/components/useBitriseArtifacts.ts b/plugins/bitrise/src/components/useBitriseArtifacts.ts new file mode 100644 index 0000000000..0194f1752d --- /dev/null +++ b/plugins/bitrise/src/components/useBitriseArtifacts.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useApi } from '@backstage/core'; +import { useAsync } from 'react-use'; +import { BitriseBuildArtifact } from '../api/bitriseApi.model'; +import { bitriseApiRef } from '../plugin'; + +export const useBitriseArtifacts = (appSlug: string, buildSlug: string) => { + const bitriseApi = useApi(bitriseApiRef); + + return useAsync( + (): Promise => + bitriseApi.getBuildArtifacts(appSlug, buildSlug), + [bitriseApi, appSlug, buildSlug], + ); +}; diff --git a/plugins/bitrise/src/extensions.ts b/plugins/bitrise/src/extensions.ts new file mode 100644 index 0000000000..04e9ee2a09 --- /dev/null +++ b/plugins/bitrise/src/extensions.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createComponentExtension } from '@backstage/core'; +import { bitrisePlugin } from './plugin'; + +export const EntityBitriseContent = bitrisePlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/BitriseBuildsComponent').then( + m => m.BitriseBuildsComponent, + ), + }, + }), +); diff --git a/plugins/bitrise/src/hooks/useBitriseBuildWorkflows.ts b/plugins/bitrise/src/hooks/useBitriseBuildWorkflows.ts new file mode 100644 index 0000000000..a1a7baf0a4 --- /dev/null +++ b/plugins/bitrise/src/hooks/useBitriseBuildWorkflows.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useApi } from '@backstage/core'; +import { useAsync } from 'react-use'; +import { bitriseApiRef } from '../plugin'; +import { AsyncState } from 'react-use/lib/useAsync'; +import { BitriseApp } from '../api/bitriseApi.model'; + +export const useBitriseBuildWorkflows = ( + appName: string, +): AsyncState => { + const bitriseApi = useApi(bitriseApiRef); + + const app = useAsync( + async (): Promise => bitriseApi.getApp(appName), + [appName], + ); + + const workflows = useAsync(async (): Promise => { + if (!app.value?.slug) { + return []; + } + + return bitriseApi.getBuildWorkflows(app.value.slug); + }, [app.value?.slug, bitriseApi]); + + return { + loading: app.loading || workflows.loading, + error: app.error || workflows.error, + value: workflows.value, + } as AsyncState; +}; diff --git a/plugins/bitrise/src/hooks/useBitriseBuilds.ts b/plugins/bitrise/src/hooks/useBitriseBuilds.ts new file mode 100644 index 0000000000..96bd8628b3 --- /dev/null +++ b/plugins/bitrise/src/hooks/useBitriseBuilds.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useApi } from '@backstage/core'; +import { useAsync } from 'react-use'; +import { + BitriseApp, + BitriseBuildListResponse, + BitriseQueryParams, +} from '../api/bitriseApi.model'; +import { bitriseApiRef } from '../plugin'; +import { AsyncState } from 'react-use/lib/useAsync'; + +export const useBitriseBuilds = ( + appName: string, + params: BitriseQueryParams, +): AsyncState => { + const bitriseApi = useApi(bitriseApiRef); + + const app = useAsync( + async (): Promise => bitriseApi.getApp(appName), + [appName], + ); + + const builds = useAsync(async (): Promise => { + if (!app.value?.slug) { + return { data: [] }; + } + + return bitriseApi.getBuilds(app.value.slug, params); + }, [ + app.value?.slug, + bitriseApi, + params.workflow, + params.branch, + params.limit, + params.next, + ]); + + return { + loading: app.loading || builds.loading, + error: app.error || builds.error, + value: builds.value, + } as AsyncState; +}; diff --git a/plugins/bitrise/src/index.ts b/plugins/bitrise/src/index.ts new file mode 100644 index 0000000000..83dad176c7 --- /dev/null +++ b/plugins/bitrise/src/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { bitrisePlugin } from './plugin'; +export { EntityBitriseContent } from './extensions'; diff --git a/plugins/bitrise/src/plugin.test.ts b/plugins/bitrise/src/plugin.test.ts new file mode 100644 index 0000000000..f464a846d6 --- /dev/null +++ b/plugins/bitrise/src/plugin.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bitrisePlugin } from './plugin'; + +describe('bitrise', () => { + it('should export plugin', () => { + expect(bitrisePlugin).toBeDefined(); + }); +}); diff --git a/plugins/bitrise/src/plugin.ts b/plugins/bitrise/src/plugin.ts new file mode 100644 index 0000000000..acd001d84b --- /dev/null +++ b/plugins/bitrise/src/plugin.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + discoveryApiRef, + createApiRef, + createApiFactory, + createPlugin, +} from '@backstage/core'; + +import { BitriseClientApi } from './api/bitriseApi.client'; +import { BitriseApi } from './api/bitriseApi'; + +export const bitriseApiRef = createApiRef({ + id: 'plugin.bitrise.service', + description: + 'Used by the BitriseCI plugin to retrieve information about builds.', +}); + +export const bitrisePlugin = createPlugin({ + id: 'bitrise', + apis: [ + createApiFactory({ + api: bitriseApiRef, + deps: { discoveryApi: discoveryApiRef }, + factory: ({ discoveryApi }) => { + return new BitriseClientApi(discoveryApi); + }, + }), + ], +}); diff --git a/plugins/bitrise/src/setupTests.ts b/plugins/bitrise/src/setupTests.ts new file mode 100644 index 0000000000..3ffe1424cc --- /dev/null +++ b/plugins/bitrise/src/setupTests.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/yarn.lock b/yarn.lock index b11c5eb0b8..3372e519c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6046,6 +6046,13 @@ dependencies: "@types/d3-path" "*" +"@types/d3-shape@^1": + version "1.3.5" + resolved "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.5.tgz#c0164c1be1429473016f855871d487f806c4e968" + integrity sha512-aPEax03owTAKynoK8ZkmkZEDZvvT4Y5pWgii4Jp4oQt0gH45j6siDl9gNDVC5kl64XHN2goN9jbYoHK88tFAcA== + dependencies: + "@types/d3-path" "^1" + "@types/d3-shape@^2.0.0": version "2.0.0" resolved "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-2.0.0.tgz#61aa065726f3c2641aedc59c3603475ab11aeb2f" @@ -6542,6 +6549,11 @@ resolved "https://registry.npmjs.org/@types/node/-/node-13.13.15.tgz#fe1cc3aa465a3ea6858b793fd380b66c39919766" integrity sha512-kwbcs0jySLxzLsa2nWUAGOd/s21WU1jebrEdtzhsj1D4Yps1EOuyI1Qcu+FD56dL7NRNIJtDDjcqIG22NwkgLw== +"@types/node@^14.14.32": + version "14.14.32" + resolved "https://registry.npmjs.org/@types/node/-/node-14.14.32.tgz#90c5c4a8d72bbbfe53033f122341343249183448" + integrity sha512-/Ctrftx/zp4m8JOujM5ZhwzlWLx22nbQJiVqz8/zE15gOeEW+uly3FSX4fGFpcfEvFzXcMCJwq9lGVWgyARXhg== + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -6796,6 +6808,14 @@ "@types/d3-shape" "*" "@types/react" "*" +"@types/recharts@^1.8.15": + version "1.8.19" + resolved "https://registry.npmjs.org/@types/recharts/-/recharts-1.8.19.tgz#047f72cf4c25df545aa1085fe3a085e58a2483c1" + integrity sha512-Fd2cYnBlWz/ga8rLmjwsZNBAc4CzXZiuTYPPqMIgrtQ02yI/OTm8WPM6ZyUuYlSdyangtsvFmHWzZ7W4tuknDA== + dependencies: + "@types/d3-shape" "^1" + "@types/react" "*" + "@types/recursive-readdir@^2.2.0": version "2.2.0" resolved "https://registry.npmjs.org/@types/recursive-readdir/-/recursive-readdir-2.2.0.tgz#b39cd5474fd58ea727fe434d5c68b7a20ba9121c" @@ -21540,7 +21560,7 @@ qs@6.7.0: resolved "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== -qs@^6.5.1, qs@^6.5.2, qs@^6.6.0, qs@^6.7.0, qs@^6.9.1, qs@^6.9.4: +qs@^6.5.1, qs@^6.5.2, qs@^6.6.0, qs@^6.7.0, qs@^6.9.1, qs@^6.9.4, qs@^6.9.6: version "6.9.6" resolved "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee" integrity sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==