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:
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -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.
|
||||
@@ -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();
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
+89
@@ -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();
|
||||
});
|
||||
});
|
||||
+66
@@ -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';
|
||||
+59
@@ -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();
|
||||
});
|
||||
});
|
||||
+84
@@ -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';
|
||||
+112
@@ -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();
|
||||
});
|
||||
});
|
||||
+235
@@ -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';
|
||||
+80
@@ -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],
|
||||
);
|
||||
};
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user