feat(bitrise): add bitrise ci plugin

Plugin to show bitrise.io builds and download
build artifacts.

Co-authored-by: Adrian Barwicki <adrian@adrianbarwicki.com>
Co-authored-by: Kai Szybiak <kaiszybiak@gmail.com>
Co-authored-by: Dominik Schwank <dominik.schwank@sda.se>
Signed-off-by: Peter Grauvogel <peter.grauvogel@sda-se.com>
This commit is contained in:
Peter Grauvogel
2021-03-01 19:24:42 +01:00
parent aa1805c103
commit 3dbdd1cfd3
36 changed files with 2153 additions and 1 deletions
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+62
View File
@@ -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
<EntityPageLayout.Content
path="/bitrise"
title="Bitrise"
element={
<Grid container spacing={3}>
<Grid item xs={12}>
<EntityBitrisePage />
</Grid>
</Grid>
}
/>;
```
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 NAME OF THE BITRISE 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.
+173
View File
@@ -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: (
<Page themeId="home">
<Header title="Bitrise CI" />
<Content>
{entities.map(entityItem => (
<EntityGridItem entity={entity(entityItem)} key={entityItem}>
<EntityBitriseContent />
</EntityGridItem>
))}
</Content>
</Page>
),
})
.render();
+55
View File
@@ -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"
]
}
@@ -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');
});
});
});
@@ -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<BitriseBuildArtifactDetails | undefined> {
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<BitriseBuildArtifact[]> {
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<BitriseApp[]> {
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<BitriseApp | undefined> {
const apps = await this.getApps();
return apps.find(app => app.title === appName);
}
async getBuildWorkflows(appSlug: string): Promise<string[]> {
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<BitriseBuildListResponse> {
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,
};
}
}
+109
View File
@@ -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;
}
+47
View File
@@ -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<BitriseBuildListResponse>;
getBuildWorkflows(appSlug: string): Promise<string[]>;
getBuildArtifacts(
appSlug: string,
buildSlug: string,
): Promise<BitriseBuildArtifact[]>;
getArtifactDetails(
appSlug: string,
buildSlug: string,
artifactSlug: string,
): Promise<BitriseBuildArtifactDetails | undefined>;
getApps(): Promise<BitriseApp[]>;
getApp(appName: string): Promise<BitriseApp | undefined>;
}
@@ -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(
<BitriseArtifactsComponent
build={
{
appSlug: 'some-app-slug',
buildSlug: 'some-build-slug',
} as BitriseBuildResult
}
/>,
);
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();
});
});
@@ -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 <Progress />;
} else if (error) {
return <Alert severity="error">{error.message}</Alert>;
} else if (!value || !value.length) {
return <Alert severity="info">No artifacts</Alert>;
}
return (
<List>
{value.map(row => (
<ListItem key={row.slug}>
<ListItemText primary={row.title} secondary={row.artifact_type} />
<ListItemSecondaryAction>
<BitriseDownloadArtifactComponent
appSlug={props.build.appSlug}
buildSlug={props.build.buildSlug}
artifactSlug={row.slug}
/>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
);
};
@@ -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';
@@ -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(
<BitriseBuildDetailsDialog
build={
{
appSlug: 'some-app-slug',
buildSlug: 'some-build-slug',
} as BitriseBuildResult
}
/>,
);
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();
});
});
@@ -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 (
<>
<IconButton data-testid="btn" onClick={handleOpen}>
<CloudDownload />
</IconButton>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{`Download artifacts for build #${build.id}?`}
<IconButton aria-label="close" edge="end" onClick={handleClose}>
<CloseIcon />
</IconButton>
<br />
<Chip size="small" label={build.message} />
</DialogTitle>
<DialogContent className={classes.dialogContent}>
<BitriseArtifactsComponent build={build} />
</DialogContent>
</Dialog>
</>
);
};
@@ -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';
@@ -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(<BitriseBuildsComponent />);
it('should display an empty state if an app annotation is missing', async () => {
const rendered = renderComponent();
expect(await rendered.findByText('Missing Annotation')).toBeInTheDocument();
});
});
@@ -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<string>(' ');
const workflows = useBitriseBuildWorkflows(appName);
const onSelectedWorkflowChanged = (workflow: any) => {
setSelectedWorkflow(workflow);
};
const getSelectionItems = (items: AsyncState<string[]>): Item[] => {
const noWorkflowSpecified: Item = { label: 'All workflows', value: ' ' };
return items.value
? [
noWorkflowSpecified,
...items.value.map(item => {
return { label: item, value: item };
}),
]
: [];
};
return (
<>
{!appName ? (
<MissingAnnotationEmptyState annotation={BITRISE_APP_ANNOTATION} />
) : (
<Page themeId="tool">
<Content noPadding>
<ContentHeader title={appName}>
<Select
value={selectedWorkflow}
onChange={workflow => onSelectedWorkflowChanged(workflow)}
label="Workflow"
items={getSelectionItems(workflows)}
/>
</ContentHeader>
<BitriseBuildsTable
appName={appName}
workflow={selectedWorkflow}
error={workflows.error}
/>
</Content>
</Page>
)}
</>
);
};
@@ -0,0 +1,20 @@
/*
* 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 {
BitriseBuildsComponent,
BITRISE_APP_ANNOTATION,
} from './BitriseBuildsComponent';
@@ -0,0 +1,112 @@
/*
* 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 { ApiProvider, ApiRegistry, UrlPatternDiscovery } from '@backstage/core';
import { bitriseApiRef } from '../../plugin';
import { BitriseClientApi } from '../../api/bitriseApi.client';
import { setupServer } from 'msw/node';
import { msw, renderInTestApp } from '@backstage/test-utils';
import { useBitriseBuilds } from '../../hooks/useBitriseBuilds';
import { BitriseBuildsTable } from './BitriseBuildsTableComponent';
jest.mock('../../hooks/useBitriseBuilds', () => ({
useBitriseBuilds: jest.fn(),
}));
const server = setupServer();
describe('BitriseBuildsFetchComponent', () => {
msw.setupDefaultHandlers(server);
const mockBaseUrl = 'http://backstage:9191';
const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl);
let apis: ApiRegistry;
beforeEach(() => {
apis = ApiRegistry.with(bitriseApiRef, new BitriseClientApi(discoveryApi));
});
it('should display `no records` message if there are no builds', async () => {
(useBitriseBuilds as jest.Mock).mockReturnValue({ value: [] });
const { getByText } = await renderInTestApp(
<ApiProvider apis={apis}>
<BitriseBuildsTable appName="some-app-name" />,
</ApiProvider>,
);
expect(getByText(/No records to display/)).toBeInTheDocument();
});
it('should display a table if there are builds', async () => {
(useBitriseBuilds as jest.Mock).mockReturnValue({
value: {
data: [
{
id: 'some-id-1',
slug: 'some-slug',
commitHash: 'some-commit',
},
{
id: 'some-id-2',
slug: 'some-slug',
commitHash: 'some-commit',
},
],
},
});
const { getByText } = await renderInTestApp(
<ApiProvider apis={apis}>
<BitriseBuildsTable appName="some-app-name" />,
</ApiProvider>,
);
expect(getByText(/some-id-1/)).toBeInTheDocument();
expect(getByText(/some-id-2/)).toBeInTheDocument();
});
it('should display pagination', async () => {
(useBitriseBuilds as jest.Mock).mockReturnValue({
value: {
data: [
{
id: 'some-id-1',
slug: 'some-slug',
commitHash: 'some-commit',
},
{
id: 'some-id-2',
slug: 'some-slug',
commitHash: 'some-commit',
},
],
paging: {
next: 'fae3232de3d2',
page_item_limit: 20,
total_item_count: 400,
},
},
});
const { getAllByText, getByText } = await renderInTestApp(
<ApiProvider apis={apis}>
<BitriseBuildsTable appName="some-app-name" />,
</ApiProvider>,
);
expect(getAllByText(/1-20 of 400/).length).toEqual(2);
expect(getByText(/20 rows/)).toBeInTheDocument();
});
});
@@ -0,0 +1,235 @@
/*
* 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 {
Table,
TableColumn,
Link,
SubvalueCell,
StatusOK,
StatusWarning,
StatusAborted,
StatusError,
StatusRunning,
} from '@backstage/core';
import { Alert } from '@material-ui/lab';
import { Button } from '@material-ui/core';
import GitHubIcon from '@material-ui/icons/GitHub';
import { useBitriseBuilds } from '../../hooks/useBitriseBuilds';
import {
BitriseBuildResult,
BitriseBuildResultStatus,
} from '../../api/bitriseApi.model';
import { BitriseBuildDetailsDialog } from '../BitriseBuildDetailsDialog';
import { DateTime } from 'luxon';
type BitriseBuildsProps = {
appName: string;
workflow?: string;
error?: Error | undefined;
};
const bitriseBaseUrl = `https://app.bitrise.io`;
const createBitriseBuildUrl = (buildSlug: string) =>
`${bitriseBaseUrl}/build/${buildSlug}#?tab=log`;
const renderBranch = (build: BitriseBuildResult): React.ReactNode => {
return <SubvalueCell value={build.message} subvalue={build.workflow} />;
};
const renderStatus = (build: BitriseBuildResult): React.ReactNode => {
switch (build.status) {
case BitriseBuildResultStatus.successful: {
return <StatusOK>{build.statusText}</StatusOK>;
}
case BitriseBuildResultStatus.failed: {
return <StatusError>{build.statusText}</StatusError>;
}
case BitriseBuildResultStatus.notFinished: {
return <StatusRunning>{build.statusText}</StatusRunning>;
}
case BitriseBuildResultStatus.abortedWithSuccess:
case BitriseBuildResultStatus.abortedWithFailure: {
return <StatusAborted>{build.statusText}</StatusAborted>;
}
default: {
return <StatusWarning>{build.statusText}</StatusWarning>;
}
}
};
const renderSource = (build: BitriseBuildResult): React.ReactNode => {
return build.source ? (
<Button
href={build.source}
target="_blank"
rel="noreferrer"
startIcon={<GitHubIcon />}
>
{build.commitHash.substr(0, 6)}
</Button>
) : null;
};
const renderId = (build: BitriseBuildResult): React.ReactNode => {
return (
<Link
to={`${createBitriseBuildUrl(build.buildSlug)}`}
target="_blank"
rel="noreferrer"
>
{build.id}
</Link>
);
};
const renderDownload = (build: BitriseBuildResult): React.ReactNode => {
return (
<>
{build.status === BitriseBuildResultStatus.successful && (
<BitriseBuildDetailsDialog build={build} />
)}
</>
);
};
const renderTriggerTime = (build: BitriseBuildResult): React.ReactNode => {
return (
<SubvalueCell
value={DateTime.fromISO(build.triggerTime).toLocaleString(
DateTime.DATETIME_MED,
)}
subvalue={
build.status !== BitriseBuildResultStatus.notFinished && build.duration
}
/>
);
};
const renderErrors = (errors: (Error | undefined)[]) => {
return errors.map(err =>
err ? <Alert severity="error">{err.message}</Alert> : null,
);
};
export const BitriseBuildsTable = ({
appName,
workflow,
error,
}: BitriseBuildsProps) => {
let selectedWorkflow = '';
const [rowsPerPage, setRowsPerPage] = React.useState<number>(20);
const [{ page, next }, setPage] = React.useState<{
page: number;
next?: string;
}>({ page: 0 });
const [selectedSearchTerm, setSelectedSearchTerm] = useState<string>('');
if (workflow) {
selectedWorkflow = workflow;
}
const builds = useBitriseBuilds(appName, {
workflow: selectedWorkflow.trim(),
branch: selectedSearchTerm.trim(),
limit: rowsPerPage,
next,
});
const columns: TableColumn<BitriseBuildResult>[] = [
{
title: 'ID',
field: 'id',
highlight: true,
render: build => renderId(build),
},
{
title: 'Branch',
customFilterAndSearch: (query, row: any) =>
`${row.message} ${row.workflow}`
.toUpperCase()
.includes(query.toUpperCase()),
field: 'message',
width: 'auto',
highlight: true,
render: build => renderBranch(build),
},
{
title: 'Status',
field: 'status',
render: build => renderStatus(build),
},
{
title: 'Triggered',
field: 'triggered',
render: build => renderTriggerTime(build),
},
{
title: 'Source',
field: 'source',
render: build => renderSource(build),
},
{
title: 'Download',
field: 'download',
render: build => renderDownload(build),
},
];
const handleChangePage = (newPage: number, nextId: string | undefined) => {
if (nextId) {
setPage({ page: newPage, next: nextId });
}
};
const handleChangeRowsPerPage = (amount: number) => {
setRowsPerPage(amount);
setPage({ page: 0, next: undefined });
};
return (
<>
{renderErrors([builds.error, error])}
{!builds.error && (
<Table
isLoading={builds.loading}
options={{
search: true,
searchAutoFocus: true,
debounceInterval: 800,
paging: true,
showFirstLastPageButtons: false,
pageSize: rowsPerPage,
pageSizeOptions: [5, 10, 20, 50],
}}
totalCount={builds.value?.paging?.total_item_count || 0}
page={page}
onChangePage={pageIndex => {
handleChangePage(pageIndex, builds.value?.paging?.next);
}}
onChangeRowsPerPage={handleChangeRowsPerPage}
columns={columns}
data={builds.value?.data || []}
onSearchChange={(searchTerm: string) =>
setSelectedSearchTerm(searchTerm)
}
/>
)}
</>
);
};
@@ -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 { BitriseBuildsTable } from './BitriseBuildsTableComponent';
@@ -0,0 +1,80 @@
/*
* 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 CloudDownload from '@material-ui/icons/CloudDownload';
import { CircularProgress, IconButton, Tooltip } from '@material-ui/core';
import LinkIcon from '@material-ui/icons/Link';
import { useBitriseArtifactDetails } from '../useBitriseArtifactDetails';
import { Alert } from '@material-ui/lab';
type BitriseDownloadArtifactComponentProps = {
appSlug: string;
buildSlug: string;
artifactSlug: string;
};
export const BitriseDownloadArtifactComponent = ({
appSlug,
buildSlug,
artifactSlug,
}: BitriseDownloadArtifactComponentProps) => {
const { value, loading, error } = useBitriseArtifactDetails(
appSlug,
buildSlug,
artifactSlug,
);
if (loading) {
return <CircularProgress />;
}
if (error) {
return <Alert severity="error">{error.message}</Alert>;
}
if (!value?.public_install_page_url && !value?.expiring_download_url) {
return <Alert severity="warning">Cannot be installed/downloaded.</Alert>;
}
return (
<>
{!!value.public_install_page_url && (
<Tooltip title="Install">
<IconButton
href={value.public_install_page_url}
target="_blank"
rel="noopener"
>
<LinkIcon />
</IconButton>
</Tooltip>
)}
{!!value.expiring_download_url && (
<Tooltip title="Download">
<IconButton
href={value.expiring_download_url}
target="_blank"
rel="noopener"
>
<CloudDownload />
</IconButton>
</Tooltip>
)}
</>
);
};
@@ -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 { BitriseDownloadArtifactComponent } from './BitriseDownloadArtifactComponent';
@@ -0,0 +1,83 @@
/*
* 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 FormControl from '@material-ui/core/FormControl';
import InputLabel from '@material-ui/core/InputLabel';
import MenuItem from '@material-ui/core/MenuItem';
import Select from '@material-ui/core/Select';
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles';
import React from 'react';
export type Item = {
label: string;
value: string | number;
};
const useStyles = makeStyles((theme: Theme) =>
createStyles({
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
selectEmpty: {
marginTop: theme.spacing(2),
},
}),
);
type SelectComponentProps = {
value: string;
items: Item[];
label: string;
onChange: (value: string) => void;
};
const renderItems = (items: Item[]) => {
return items.map(item => {
return (
<MenuItem value={item.value} key={item.label}>
{item.label}
</MenuItem>
);
});
};
export const SelectComponent = ({
value,
items,
label,
onChange,
}: SelectComponentProps) => {
const classes = useStyles();
const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => {
const val = event.target.value as string;
onChange(val);
};
return (
<FormControl
variant="outlined"
className={classes.formControl}
disabled={items.length === 0}
>
<InputLabel>{label}</InputLabel>
<Select label={label} value={value} onChange={handleChange}>
{renderItems(items)}
</Select>
</FormControl>
);
};
@@ -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';
@@ -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<BitriseBuildArtifactDetails | undefined> =>
bitriseApi.getArtifactDetails(appSlug, buildSlug, artifactSlug),
[bitriseApi, appSlug, buildSlug, artifactSlug],
);
};
@@ -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<BitriseBuildArtifact[]> =>
bitriseApi.getBuildArtifacts(appSlug, buildSlug),
[bitriseApi, appSlug, buildSlug],
);
};
+29
View File
@@ -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,
),
},
}),
);
@@ -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<string[]> => {
const bitriseApi = useApi(bitriseApiRef);
const app = useAsync(
async (): Promise<BitriseApp | undefined> => bitriseApi.getApp(appName),
[appName],
);
const workflows = useAsync(async (): Promise<string[]> => {
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<string[]>;
};
@@ -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<BitriseBuildListResponse> => {
const bitriseApi = useApi(bitriseApiRef);
const app = useAsync(
async (): Promise<BitriseApp | undefined> => bitriseApi.getApp(appName),
[appName],
);
const builds = useAsync(async (): Promise<BitriseBuildListResponse> => {
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<BitriseBuildListResponse>;
};
+18
View File
@@ -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';
+23
View File
@@ -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();
});
});
+44
View File
@@ -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<BitriseApi>({
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);
},
}),
],
});
+18
View File
@@ -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';