Merge branch 'master' of github.com:spotify/backstage into mob/docusaurus-structure

This commit is contained in:
Ivan Shmidt
2020-08-12 16:04:54 +02:00
67 changed files with 2006 additions and 239 deletions
+5 -5
View File
@@ -2,8 +2,8 @@ name: Frontend CI
on:
pull_request:
paths:
- '!microsite/**'
paths-ignore:
- 'microsite/**'
jobs:
build:
runs-on: ubuntu-latest
@@ -45,12 +45,12 @@ jobs:
run: git diff --quiet origin/master HEAD -- yarn.lock
continue-on-error: true
- name: verify doc links
run: node docs/verify-links.js
- name: yarn install
run: yarn install --frozen-lockfile
- name: verify doc links
run: node docs/verify-links.js
- name: lint
run: yarn lerna -- run lint --since origin/master
+29 -1
View File
@@ -63,7 +63,35 @@ Have you started using Backstage? Adding your company to [ADOPTERS](ADOPTERS.md)
# Get Started!
So...feel ready to jump in? Let's do this. Head over to the [Getting Started guide](https://github.com/spotify/backstage#getting-started) 👏🏻💯
So...feel ready to jump in? Let's do this. 👏🏻💯
To run a Backstage app, you will need to have the following installed:
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
- [NodeJS](https://nodejs.org/en/download/) - Active LTS Release, currently v12
- [yarn](https://classic.yarnpkg.com/en/docs/install)
After cloning this repo, open a terminal window and start the example app using the following commands from the project root:
```bash
yarn install # Install dependencies
yarn start # Start dev server, use --check to enable linting and type-checks
```
The final `yarn start` command should open a local instance of Backstage in your browser, otherwise open one of the URLs printed in the terminal.
Depending on the work you're doing, you often also want to run the example backend. Start the backend in a separate terminal session using the following:
```bash
cd packages/backend
yarn start
yarn lerna run mock-data # Populate the backend with mock data
```
And that's it! You are good to go 👍
If you need help, just jump into our [Discord chatroom](https://discord.gg/MUpMjP2).
+18 -17
View File
@@ -49,27 +49,33 @@ The Backstage platform consists of a number of different components:
- [**proxy**](https://github.com/spotify/backstage/tree/master/plugins/proxy-backend) - Terminates HTTPS and exposes any RESTful API to Plugins.
- **identity** - A backend service that holds your organisation's metadata.
## Getting started
## Getting Started
To run a Backstage app, you will need to have the following installed:
There are two different ways to get started with Backstage, either by creating a standalone app, or by cloning this repo. Which method you use depends on what you're planning to do.
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
- [NodeJS](https://nodejs.org/en/download/) - Active LTS Release, currently v12
- [yarn](https://classic.yarnpkg.com/en/docs/install)
Creating a standalone instance makes it simpler to customize the application for your needs whilst staying up to date with the project. You will also depend on `@backstage` packages from NPM, making the project much smaller. This is the recommended approach if you want to kick the tyres of Backstage or setup your own instance.
After cloning this repo, open a terminal window and start the example app using the following commands from the project root:
On the other hand, if you want to contribute plugins or to the project in general, it's easier to fork and clone this project. That will let you stay up to date with the latest changes, and gives you an easier path to make Pull Requests towards this repo.
### Creating a Standalone App
Backstage provides the `@backstage/create-app` package to scaffold standalone instances of Backstage. You will need to have
[NodeJS](https://nodejs.org/en/download/) Active LTS Release installed
(currently v12), and [yarn](https://classic.yarnpkg.com/en/docs/install). You will also need to have [Docker](https://docs.docker.com/engine/install/) installed to use some features like Software Templates and TechDocs.
Using `npx` you can then run the following to create an app in a chosen subdirectory of your current working directory:
```bash
yarn install # Install dependencies
yarn start # Start dev server, use --check to enable linting and type-checks
npx @backstage/create-app
```
The final `yarn start` command should open a local instance of Backstage in your browser, otherwise open one of the URLs printed in the terminal.
You will be taken through a wizard to create your app, and the output should look something like this. You can read more about this process [here](docs/getting-started/create-an-app.md)
And that's it! You are good to go 👍
### Contributing to Backstage
### Next step
You can read more in our [CONTRIBUTING.md](./CONTRIBUTING.md#get-started) guide, which can help you get setup with a Backstage development environment.
### Next steps
Take a look at the [Getting Started](docs/getting-started/index.md) guide to learn how to set up Backstage, and how to develop on the platform.
@@ -77,15 +83,10 @@ Take a look at the [Getting Started](docs/getting-started/index.md) guide to lea
- [Main documentation](docs/README.md)
- [Service Catalog](docs/features/software-catalog/index.md)
- [Create a Backstage App](docs/getting-started/create-an-app.md)
- [Architecture](docs/overview/architecture-terminology.md) ([Decisions](docs/architecture-decisions/index.md))
- [Designing for Backstage](docs/dls/design.md)
- [Storybook - UI components](http://storybook.backstage.io)
## Contributing
We would love your help in building Backstage! See [CONTRIBUTING](CONTRIBUTING.md) for more information.
## Community
- [Discord chatroom](https://discord.gg/MUpMjP2) - Get support or discuss the project
+9
View File
@@ -16,6 +16,15 @@ proxy:
changeOrigin: true
pathRewrite:
'^/proxy/circleci/api/': '/'
'/jenkins/api':
target: 'http://localhost:8080'
changeOrigin: true
headers:
Authorization:
$secret:
env: JENKINS_BASIC_AUTH_HEADER
pathRewrite:
'^/proxy/jenkins/api/': '/'
organization:
name: Spotify
+3 -1
View File
@@ -39,10 +39,12 @@
"@spotify/eslint-config-oss": "^1.0.1",
"@spotify/prettier-config": "^8.0.0",
"concurrently": "^5.2.0",
"fs-extra": "^9.0.0",
"husky": "^4.2.3",
"lerna": "^3.20.2",
"lint-staged": "^10.1.0",
"prettier": "^2.0.5"
"prettier": "^2.0.5",
"recursive-readdir": "^2.2.2"
},
"husky": {
"hooks": {
+2
View File
@@ -11,6 +11,7 @@
"@backstage/plugin-github-actions": "^0.1.1-alpha.18",
"@backstage/plugin-gitops-profiles": "^0.1.1-alpha.18",
"@backstage/plugin-graphiql": "^0.1.1-alpha.18",
"@backstage/plugin-jenkins": "^0.1.1-alpha.18",
"@backstage/plugin-lighthouse": "^0.1.1-alpha.18",
"@backstage/plugin-newrelic": "^0.1.1-alpha.18",
"@backstage/plugin-register-component": "^0.1.1-alpha.18",
@@ -25,6 +26,7 @@
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.0.0",
"@roadiehq/backstage-plugin-travis-ci": "^0.1.3",
"history": "^5.0.0",
"prop-types": "^15.7.2",
"react": "^16.12.0",
+7
View File
@@ -62,6 +62,9 @@ import {
GithubActionsClient,
githubActionsApiRef,
} from '@backstage/plugin-github-actions';
import { jenkinsApiRef, JenkinsApi } from '@backstage/plugin-jenkins';
import { TravisCIApi, travisCIApiRef } from '@roadiehq/backstage-plugin-travis-ci';
export const apis = (config: ConfigApi) => {
// eslint-disable-next-line no-console
@@ -83,12 +86,16 @@ export const apis = (config: ConfigApi) => {
new CircleCIApi(`${backendUrl}/proxy/circleci/api`),
);
builder.add(jenkinsApiRef, new JenkinsApi(`${backendUrl}/proxy/jenkins/api`));
builder.add(githubActionsApiRef, new GithubActionsClient());
builder.add(featureFlagsApiRef, new FeatureFlags());
builder.add(lighthouseApiRef, new LighthouseRestApi('http://localhost:3003'));
builder.add(travisCIApiRef, new TravisCIApi());
const oauthRequestApi = builder.add(
oauthRequestApiRef,
new OAuthRequestManager(),
+2
View File
@@ -28,3 +28,5 @@ export { plugin as GraphiQL } from '@backstage/plugin-graphiql';
export { plugin as GithubActions } from '@backstage/plugin-github-actions';
export { plugin as Rollbar } from '@backstage/plugin-rollbar';
export { plugin as Newrelic } from '@backstage/plugin-newrelic';
export { plugin as TravisCI } from '@roadiehq/backstage-plugin-travis-ci';
export { plugin as Jenkins } from '@backstage/plugin-jenkins';
+2 -1
View File
@@ -29,8 +29,9 @@ import { useHotCleanup } from '@backstage/backend-common';
export default async function createPlugin({
logger,
database,
config,
}: PluginEnvironment) {
const locationReader = new LocationReaders(logger);
const locationReader = new LocationReaders({ logger, config });
const db = await DatabaseManager.createDatabase(database, { logger });
const entitiesCatalog = new DatabaseEntitiesCatalog(db);
@@ -24,6 +24,7 @@ import {
} from './entity';
import {
ComponentEntityV1alpha1Policy,
GroupEntityV1alpha1Policy,
LocationEntityV1alpha1Policy,
TemplateEntityV1alpha1Policy,
} from './kinds';
@@ -74,6 +75,7 @@ export class EntityPolicies implements EntityPolicy {
]),
EntityPolicies.anyOf([
new ComponentEntityV1alpha1Policy(),
new GroupEntityV1alpha1Policy(),
new LocationEntityV1alpha1Policy(),
new TemplateEntityV1alpha1Policy(),
]),
@@ -0,0 +1,120 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EntityPolicy } from '../types';
import {
GroupEntityV1alpha1,
GroupEntityV1alpha1Policy,
} from './GroupEntityV1alpha1';
describe('GroupV1alpha1Policy', () => {
let entity: GroupEntityV1alpha1;
let policy: EntityPolicy;
beforeEach(() => {
entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
name: 'doe-squad',
title: 'Doe Squad',
description: 'A squad for John and Jane',
},
spec: {
type: 'squad',
parent: 'group-a',
ancestors: ['group-a', 'global-synergies', 'acme-corp'],
children: ['child-a', 'child-b'],
descendants: ['desc-a', 'desc-b'],
},
};
policy = new GroupEntityV1alpha1Policy();
});
it('happy path: accepts valid data', async () => {
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('silently accepts v1beta1 as well', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta1';
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('rejects unknown apiVersion', async () => {
(entity as any).apiVersion = 'backstage.io/v1beta0';
await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/);
});
it('rejects unknown kind', async () => {
(entity as any).kind = 'Wizard';
await expect(policy.enforce(entity)).rejects.toThrow(/kind/);
});
it('rejects missing type', async () => {
delete (entity as any).spec.type;
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
});
it('rejects wrong type', async () => {
(entity as any).spec.type = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
});
it('rejects empty type', async () => {
(entity as any).spec.type = '';
await expect(policy.enforce(entity)).rejects.toThrow(/type/);
});
it('accepts missing parent', async () => {
delete (entity as any).spec.parent;
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('rejects empty parent', async () => {
(entity as any).spec.parent = '';
await expect(policy.enforce(entity)).rejects.toThrow(/parent/);
});
it('rejects missing ancestors', async () => {
delete (entity as any).spec.ancestors;
await expect(policy.enforce(entity)).rejects.toThrow(/ancestor/);
});
it('accepts empty ancestors', async () => {
(entity as any).spec.ancestors = [''];
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('rejects missing children', async () => {
delete (entity as any).spec.children;
await expect(policy.enforce(entity)).rejects.toThrow(/children/);
});
it('accepts empty children', async () => {
(entity as any).spec.children = [''];
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('rejects missing descendants', async () => {
delete (entity as any).spec.descendants;
await expect(policy.enforce(entity)).rejects.toThrow(/descendants/);
});
it('accepts empty descendants', async () => {
(entity as any).spec.descendants = [''];
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
});
@@ -0,0 +1,58 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as yup from 'yup';
import type { Entity } from '../entity/Entity';
import type { EntityPolicy } from '../types';
const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const;
const KIND = 'Group' as const;
export interface GroupEntityV1alpha1 extends Entity {
apiVersion: typeof API_VERSION[number];
kind: typeof KIND;
spec: {
type: string;
parent?: string;
ancestors: string[];
children: string[];
descendants: string[];
};
}
export class GroupEntityV1alpha1Policy implements EntityPolicy {
private schema: yup.Schema<any>;
constructor() {
this.schema = yup.object<Partial<GroupEntityV1alpha1>>({
apiVersion: yup.string().required().oneOf(API_VERSION),
kind: yup.string().required().equals([KIND]),
spec: yup
.object({
type: yup.string().required().min(1),
parent: yup.string().notRequired().min(1),
ancestors: yup.array(yup.string()).required(),
children: yup.array(yup.string()).required(),
descendants: yup.array(yup.string()).required(),
})
.required(),
});
}
async enforce(envelope: Entity): Promise<Entity> {
return await this.schema.validate(envelope, { strict: true });
}
}
@@ -19,6 +19,11 @@ export type {
ComponentEntityV1alpha1 as ComponentEntity,
ComponentEntityV1alpha1,
} from './ComponentEntityV1alpha1';
export { GroupEntityV1alpha1Policy } from './GroupEntityV1alpha1';
export type {
GroupEntityV1alpha1 as GroupEntity,
GroupEntityV1alpha1,
} from './GroupEntityV1alpha1';
export { LocationEntityV1alpha1Policy } from './LocationEntityV1alpha1';
export type {
LocationEntityV1alpha1 as LocationEntity,
@@ -142,7 +142,12 @@ async function cleanUp(tempDir: string) {
}
async function buildPlugin(pluginFolder: string) {
const commands = ['yarn install', 'yarn tsc', 'yarn build'];
const commands = [
'yarn install',
'yarn lint --fix',
'yarn tsc',
'yarn build',
];
for (const command of commands) {
await Task.forItem('executing', command, async () => {
process.chdir(pluginFolder);
@@ -1,19 +1,3 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createDevApp } from '@backstage/dev-utils';
import { plugin } from '../src/plugin';
@@ -1,19 +1,3 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { render } from '@testing-library/react';
import mockFetch from 'jest-fetch-mock';
@@ -1,19 +1,3 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC } from 'react';
import { Typography, Grid } from '@material-ui/core';
import {
@@ -1,17 +1 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { default } from './ExampleComponent';
@@ -1,19 +1,3 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { render } from '@testing-library/react';
import mockFetch from 'jest-fetch-mock';
@@ -1,19 +1,3 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import { Table, TableColumn, Progress } from '@backstage/core';
@@ -1,17 +1 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { default } from './ExampleFetchComponent';
@@ -1,17 +1 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { plugin } from './plugin';
@@ -1,19 +1,3 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { plugin } from './plugin';
describe('{{ id }}', () => {
@@ -1,19 +1,3 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createPlugin, createRouteRef } from '@backstage/core';
import ExampleComponent from './components/ExampleComponent';
@@ -1,19 +1,3 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@testing-library/jest-dom';
require('jest-fetch-mock').enableMocks();
@@ -0,0 +1,41 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// @ts-check
/**
* @param {import('knex')} knex
*/
exports.up = async function up(knex) {
// Adds a single 'bootstrap' location that can be used to trigger work in processors.
// This is primarily here to fulfill foreign key constraints.
await knex('locations').insert({
id: 'bootstrap',
type: 'bootstrap',
target: 'bootstrap',
});
};
/**
* @param {import('knex')} knex
*/
exports.down = async function down(knex) {
await knex('locations')
.where({
id: 'bootstrap',
})
.del();
};
+1
View File
@@ -23,6 +23,7 @@
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.18",
"@backstage/catalog-model": "^0.1.1-alpha.18",
"@backstage/config": "^0.1.1-alpha.18",
"@types/express": "^4.17.6",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
@@ -17,6 +17,12 @@
import { DatabaseManager } from '../database';
import { DatabaseLocationsCatalog } from './DatabaseLocationsCatalog';
const bootstrapLocation = {
id: 'bootstrap',
type: 'bootstrap',
target: 'bootstrap',
};
describe('DatabaseLocationsCatalog', () => {
let catalog: DatabaseLocationsCatalog;
@@ -35,9 +41,12 @@ describe('DatabaseLocationsCatalog', () => {
await expect(
catalog.location('dd12620d-0436-422f-93bd-929aa0788123'),
).resolves.toEqual(expect.objectContaining({ data: location }));
await expect(catalog.locations()).resolves.toEqual([
expect.objectContaining({ data: location }),
]);
await expect(catalog.locations()).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({ data: location }),
expect.objectContaining({ data: bootstrapLocation }),
]),
);
});
it('does not return duplicates of rows because of logs', async () => {
@@ -60,11 +69,12 @@ describe('DatabaseLocationsCatalog', () => {
catalog.logUpdateSuccess(location1.id),
).resolves.toBeUndefined();
const locations = await catalog.locations();
expect(locations.length).toBe(2);
expect(locations.length).toBe(3);
expect(locations).toEqual(
expect.arrayContaining([
expect.objectContaining({ data: location1 }),
expect.objectContaining({ data: location2 }),
expect.objectContaining({ data: bootstrapLocation }),
]),
);
});
@@ -24,6 +24,15 @@ import type {
DbLocationsRowWithStatus,
} from './types';
const bootstrapLocation = {
id: 'bootstrap',
type: 'bootstrap',
target: 'bootstrap',
message: null,
status: null,
timestamp: null,
};
describe('CommonDatabase', () => {
let db: Database;
let entityRequest: DbEntityRequest;
@@ -85,8 +94,12 @@ describe('CommonDatabase', () => {
await db.addLocation(input);
const locations = await db.locations();
expect(locations).toEqual([output]);
const location = await db.location(locations[0].id);
expect(locations).toEqual(
expect.arrayContaining([output, bootstrapLocation]),
);
const location = await db.location(
locations.find(l => l.id !== 'bootstrap')!.id,
);
expect(location).toEqual(output);
// If we add 2 new update log events,
@@ -105,20 +118,21 @@ describe('CommonDatabase', () => {
DatabaseLocationUpdateLogStatus.FAIL,
);
expect(await db.locations()).toEqual([
{
...output,
status: DatabaseLocationUpdateLogStatus.FAIL,
timestamp: expect.any(String),
},
]);
await db.transaction(tx => db.removeLocation(tx, locations[0].id));
await expect(db.locations()).resolves.toEqual([]);
await expect(db.location(locations[0].id)).rejects.toThrow(
/Found no location/,
await expect(db.locations()).resolves.toEqual(
expect.arrayContaining([
bootstrapLocation,
{
...output,
status: DatabaseLocationUpdateLogStatus.FAIL,
timestamp: expect.any(String),
},
]),
);
await db.transaction(tx => db.removeLocation(tx, location.id));
await expect(db.locations()).resolves.toEqual([bootstrapLocation]);
await expect(db.location(location.id)).rejects.toThrow(/Found no location/);
});
describe('addEntity', () => {
@@ -15,6 +15,7 @@
*/
import { getVoidLogger } from '@backstage/backend-common';
import { Config, ConfigReader } from '@backstage/config';
import {
Entity,
EntityPolicies,
@@ -31,6 +32,7 @@ import { GitlabApiReaderProcessor } from './processors/GitlabApiReaderProcessor'
import { GitlabReaderProcessor } from './processors/GitlabReaderProcessor';
import { UrlReaderProcessor } from './processors/UrlReaderProcessor';
import { LocationRefProcessor } from './processors/LocationEntityProcessor';
import { StaticLocationProcessor } from './processors/StaticLocationProcessor';
import * as result from './processors/results';
import {
LocationProcessor,
@@ -47,6 +49,12 @@ import { LocationReader, ReadLocationResult } from './types';
// The max amount of nesting depth of generated work items
const MAX_DEPTH = 10;
type Options = {
logger?: Logger;
config?: Config;
processors?: LocationProcessor[];
};
/**
* Implements the reading of a location through a series of processor tasks.
*/
@@ -54,10 +62,16 @@ export class LocationReaders implements LocationReader {
private readonly logger: Logger;
private readonly processors: LocationProcessor[];
static defaultProcessors(
entityPolicy: EntityPolicy = new EntityPolicies(),
): LocationProcessor[] {
static defaultProcessors(options: {
config?: Config;
entityPolicy?: EntityPolicy;
}): LocationProcessor[] {
const {
config = new ConfigReader({}, 'missing-config'),
entityPolicy = new EntityPolicies(),
} = options;
return [
StaticLocationProcessor.fromConfig(config),
new FileReaderProcessor(),
new GithubReaderProcessor(),
new GithubApiReaderProcessor(),
@@ -71,10 +85,11 @@ export class LocationReaders implements LocationReader {
];
}
constructor(
logger: Logger = getVoidLogger(),
processors: LocationProcessor[] = LocationReaders.defaultProcessors(),
) {
constructor({
logger = getVoidLogger(),
config,
processors = LocationReaders.defaultProcessors({ config }),
}: Options) {
this.logger = logger;
this.processors = processors;
}
@@ -0,0 +1,53 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { LocationSpec } from '@backstage/catalog-model';
import * as result from './results';
import { Config } from '@backstage/config';
import { LocationProcessorEmit } from './types';
export class StaticLocationProcessor implements StaticLocationProcessor {
static fromConfig(config: Config): StaticLocationProcessor {
const locations: LocationSpec[] = [];
const lConfigs = config.getOptionalConfigArray('catalog.locations') ?? [];
for (const lConfig of lConfigs) {
const type = lConfig.getString('type');
const target = lConfig.getString('target');
locations.push({ type, target });
}
return new StaticLocationProcessor(locations);
}
constructor(private readonly staticLocations: LocationSpec[]) {}
async readLocation(
location: LocationSpec,
_optional: boolean,
emit: LocationProcessorEmit,
): Promise<boolean> {
if (location.type !== 'bootstrap') {
return false;
}
for (const staticLocation of this.staticLocations) {
emit(result.location(staticLocation, false));
}
return true;
}
}
@@ -14,7 +14,11 @@
* limitations under the License.
*/
import { createServiceBuilder } from '@backstage/backend-common';
import {
createServiceBuilder,
loadBackendConfig,
} from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import { Server } from 'http';
import { Logger } from 'winston';
import { HigherOrderOperations } from '..';
@@ -34,12 +38,13 @@ export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'catalog-backend' });
const config = ConfigReader.fromConfigs(await loadBackendConfig());
logger.debug('Creating application...');
const db = await DatabaseManager.createInMemoryDatabase({ logger });
const entitiesCatalog = new DatabaseEntitiesCatalog(db);
const locationsCatalog = new DatabaseLocationsCatalog(db);
const locationReader = new LocationReaders();
const locationReader = new LocationReaders({ logger, config });
const higherOrderOperation = new HigherOrderOperations(
entitiesCatalog,
locationsCatalog,
+1
View File
@@ -24,6 +24,7 @@
"@backstage/catalog-model": "^0.1.1-alpha.18",
"@backstage/core": "^0.1.1-alpha.18",
"@backstage/plugin-github-actions": "^0.1.1-alpha.18",
"@backstage/plugin-jenkins": "^0.1.1-alpha.18",
"@backstage/plugin-scaffolder": "^0.1.1-alpha.18",
"@backstage/plugin-sentry": "^0.1.1-alpha.18",
"@backstage/theme": "^0.1.1-alpha.18",
@@ -29,6 +29,10 @@ import {
} from '@backstage/core';
import { SentryIssuesWidget } from '@backstage/plugin-sentry';
import { Widget as GithubActionsWidget } from '@backstage/plugin-github-actions';
import {
JenkinsBuildsWidget,
JenkinsLastBuildWidget,
} from '@backstage/plugin-jenkins';
import { Grid, Box } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import React, { FC, useEffect, useState } from 'react';
@@ -187,12 +191,20 @@ export const EntityPage: FC<{}> = () => {
<Grid item sm={4}>
<EntityMetadataCard entity={entity} />
</Grid>
<Grid item sm={8}>
<SentryIssuesWidget
sentryProjectId="sample-sentry-project-id"
statsFor="24h"
/>
</Grid>
{entity.metadata?.annotations?.[
'backstage.io/jenkins-github-folder'
] && (
<Grid item sm={4}>
<JenkinsLastBuildWidget entity={entity} branch="master" />
</Grid>
)}
{entity.metadata?.annotations?.[
'backstage.io/jenkins-github-folder'
] && (
<Grid item sm={8}>
<JenkinsBuildsWidget entity={entity} />
</Grid>
)}
{entity.metadata?.annotations?.[
'backstage.io/github-actions-id'
] && (
@@ -201,6 +213,12 @@ export const EntityPage: FC<{}> = () => {
</Grid>
)}
</Grid>
<Grid item sm={8}>
<SentryIssuesWidget
sentryProjectId="sample-sentry-project-id"
statsFor="24h"
/>
</Grid>
</Content>
<UnregisterEntityDialog
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+87
View File
@@ -0,0 +1,87 @@
# Jenkins Plugin (Alpha)
Website: [https://jenkins.io/](https://jenkins.io/)
<img src="./src/assets/last-master-build.png" alt="Last master build"/>
<img src="./src/assets/folder-results.png" alt="Folder results"/>
<img src="./src/assets/build-details.png" alt="Build detials"/>
## Setup
1. If you have a standalone app (you didn't clone this repo), then do
```bash
yarn add @backstage/plugin-jenkins
```
2. Add plugin API to your Backstage instance:
```js
// packages/app/src/api.ts
import { JenkinsApi, jenkinsApiRef } from '@backstage/plugin-jenkins';
const builder = ApiRegistry.builder();
builder.add(jenkinsApiRef, new JenkinsApi(`${backendUrl}/proxy/jenkins/api`));
```
2. Add plugin itself:
```js
// packages/app/src/plugins.ts
export { plugin as Jenkins } from '@backstage/plugin-jenkins';
```
3. Add proxy configuration to `app-config.yaml`
```yaml
proxy:
'/jenkins/api':
target: 'http://localhost:8080' # your Jenkins URL
changeOrigin: true
headers:
Authorization:
$secret:
env: JENKINS_BASIC_AUTH_HEADER
pathRewrite:
'^/proxy/jenkins/api/': '/'
```
4. Add an environment variable which contains the Jenkins credentials, (note: use an API token not your password)
```shell
HEADER=$(echo -n user:api-token | base64)
export JENKINS_BASIC_AUTH_HEADER="Basic $HEADER"
```
5. Run app with `yarn start`
6. Add the Jenkins folder annotation to your `component-info.yaml`, (note: currently this plugin only supports folders and Git SCM)
```yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: 'your-component'
description: 'a description'
annotations:
backstage.io/jenkins-github-folder: 'folder-name/job-name'
spec:
type: service
lifecycle: experimental
owner: your-name
```
7. Register your component
8. Click the component in the catalog you should now see Jenkins builds, and a last build result for your master build.
## Features
- View all runs inside a folder
- Last build status for specified branch
- View summary of a build
## Limitations
- Only works with projects that use the Git SCM
- It requires jobs to be organised into folders
- No pagination support currently - don't run this on a Jenkins with lots of builds
+20
View File
@@ -0,0 +1,20 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createDevApp } from '@backstage/dev-utils';
import { plugin } from '../src/plugin';
createDevApp().registerPlugin(plugin).render();
+51
View File
@@ -0,0 +1,51 @@
{
"name": "@backstage/plugin-jenkins",
"version": "0.1.1-alpha.18",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": false,
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"scripts": {
"build": "backstage-cli plugin:build",
"start": "backstage-cli plugin:serve",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"diff": "backstage-cli plugin:diff",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.18",
"@backstage/core": "^0.1.1-alpha.18",
"@backstage/theme": "^0.1.1-alpha.18",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"jenkins": "^0.28.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.18",
"@backstage/dev-utils": "^0.1.1-alpha.18",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^12.0.0",
"@types/testing-library__jest-dom": "^5.9.1",
"jest-fetch-mock": "^3.0.3"
},
"files": [
"dist"
]
}
+198
View File
@@ -0,0 +1,198 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createApiRef } from '@backstage/core';
import { CITableBuildInfo } from '../pages/BuildsPage/lib/CITable';
const jenkins = require('jenkins');
export const jenkinsApiRef = createApiRef<JenkinsApi>({
id: 'plugin.jenkins.service',
description: 'Used by the Jenkins plugin to make requests',
});
export class JenkinsApi {
apiUrl: string;
jenkins: any;
constructor(apiUrl: string) {
this.apiUrl = apiUrl;
this.jenkins = jenkins({ baseUrl: apiUrl, promisify: true });
}
async retry(buildName: string) {
// looks like the current SDK only supports triggering a new build
// can't see any support for replay (re-running the specific build with the same SCM info)
return await this.jenkins.job.build(buildName);
}
async getLastBuild(jobName: string) {
const job = await this.jenkins.job.get(jobName);
const lastBuild = await this.jenkins.build.get(
jobName,
job.lastBuild.number,
);
return lastBuild;
}
extractScmDetailsFromJob(jobDetails: any): any {
const scmInfo = jobDetails.actions
.filter(
(action: any) =>
action._class === 'jenkins.scm.api.metadata.ObjectMetadataAction',
)
.map((action: any) => {
return {
url: action?.objectUrl,
// https://javadoc.jenkins.io/plugin/scm-api/jenkins/scm/api/metadata/ObjectMetadataAction.html
// branch name for regular builds, pull request title on pull requests
displayName: action?.objectDisplayName,
};
})
.pop();
return scmInfo;
}
async getJob(jobName: string) {
return this.jenkins.job.get({
name: jobName,
depth: 1,
});
}
async getFolder(folderName: string) {
const folder = await this.jenkins.job.get(folderName);
const results = [];
for (const jobSummary of folder.jobs) {
const jobDetails = await this.jenkins.job.get({
name: `${folderName}/${jobSummary.name}`,
depth: 1,
});
const jobScmInfo = this.extractScmDetailsFromJob(jobDetails);
if (jobDetails.jobs) {
// skipping folders inside folders for now
} else {
for (const buildDetails of jobDetails.builds) {
const build = await this.jenkins.build.get({
name: `${folderName}/${jobSummary.name}`,
number: buildDetails.number,
depth: 1,
});
const ciTable = this.mapJenkinsBuildToCITable(build, jobScmInfo);
results.push(ciTable);
}
}
}
return results;
}
private getTestReport(
jenkinsResult: any,
): {
total: number;
passed: number;
skipped: number;
failed: number;
testUrl: string;
} {
return jenkinsResult.actions
.filter(
(action: any) =>
action._class === 'hudson.tasks.junit.TestResultAction',
)
.map((action: any) => {
return {
total: action.totalCount,
passed: action.totalCount - action.failCount - action.skipCount,
skipped: action.skipCount,
failed: action.failCount,
testUrl: `${jenkinsResult.url}${action.urlName}/`,
};
})
.pop();
}
mapJenkinsBuildToCITable(
jenkinsResult: any,
jobScmInfo?: any,
): CITableBuildInfo {
const source =
jenkinsResult.actions
.filter(
(action: any) =>
action._class === 'hudson.plugins.git.util.BuildData',
)
.map((action: any) => {
const [first]: any = Object.values(action.buildsByBranchName);
const branch = first.revision.branch[0];
return {
branchName: branch.name,
commit: {
hash: branch.SHA1.substring(0, 8),
},
};
})
.pop() || {};
if (jobScmInfo) {
source.url = jobScmInfo?.url;
source.displayName = jobScmInfo?.displayName;
}
const path = new URL(jenkinsResult.url).pathname;
return {
id: path,
buildName: jenkinsResult.fullDisplayName,
status: jenkinsResult.building ? 'running' : jenkinsResult.result,
onRestartClick: () => {
// TODO: this won't handle non root context path, need a better way to get the job name
const { jobName } = this.extractJobDetailsFromBuildName(path);
return this.retry(jobName);
},
source: source,
tests: this.getTestReport(jenkinsResult),
};
}
async getBuild(buildName: string) {
const { jobName, buildNumber } = this.extractJobDetailsFromBuildName(
buildName,
);
const buildResult = await this.jenkins.build.get(jobName, buildNumber);
return buildResult;
}
extractJobDetailsFromBuildName(buildName: string) {
const trimmedBuild = buildName.replace(/\/job/g, '').replace(/\/$/, '');
const split = trimmedBuild.split('/');
const buildNumber = parseInt(split[split.length - 1], 10);
const jobName = trimmedBuild.slice(
0,
trimmedBuild.length - buildNumber.toString(10).length - 1,
);
return {
jobName,
buildNumber,
};
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,27 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Builds } from '../../pages/BuildsPage/lib/Builds';
import { Entity } from '@backstage/catalog-model';
export const JenkinsBuildsWidget = ({ entity }: { entity: Entity }) => {
const [owner, repo] = (
entity?.metadata.annotations?.['backstage.io/jenkins-github-folder'] ?? '/'
).split('/');
return <Builds owner={owner} repo={repo} />;
};
@@ -0,0 +1,82 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Entity } from '@backstage/catalog-model';
import { Link, Theme, makeStyles, LinearProgress } from '@material-ui/core';
import { InfoCard, StructuredMetadataTable } from '@backstage/core';
import ExternalLinkIcon from '@material-ui/icons/Launch';
import { useBuilds } from '../../state';
import { JenkinsRunStatus } from '../../pages/BuildsPage/lib/Status';
const useStyles = makeStyles<Theme>({
externalLinkIcon: {
fontSize: 'inherit',
verticalAlign: 'bottom',
},
});
const WidgetContent = ({
loading,
lastRun,
}: {
loading?: boolean;
lastRun: any;
branch: string;
}) => {
const classes = useStyles();
if (loading || !lastRun) return <LinearProgress />;
return (
<StructuredMetadataTable
metadata={{
status: (
<>
<JenkinsRunStatus
status={lastRun.building ? 'running' : lastRun.result}
/>
</>
),
build: lastRun.fullDisplayName,
url: (
<Link href={lastRun.url} target="_blank">
See more on Jenkins{' '}
<ExternalLinkIcon className={classes.externalLinkIcon} />
</Link>
),
}}
/>
);
};
export const JenkinsLastBuildWidget = ({
entity,
branch = 'master',
}: {
entity: Entity;
branch: string;
}) => {
const [owner, repo] = (
entity?.metadata.annotations?.['backstage.io/jenkins-github-folder'] ?? '/'
).split('/');
const [{ loading, value }] = useBuilds(owner, repo, branch);
const lastRun = value ?? {};
return (
<InfoCard title={`Last ${branch} build`}>
<WidgetContent loading={loading} branch={branch} lastRun={lastRun} />
</InfoCard>
);
};
@@ -0,0 +1,29 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Header, Page, pageTheme, HeaderLabel } from '@backstage/core';
export const Layout: React.FC = ({ children }) => {
return (
<Page theme={pageTheme.tool}>
<Header title="Jenkins" subtitle="See recent builds and their status">
<HeaderLabel label="Owner" value="Spotify" />
<HeaderLabel label="Lifecycle" value="Alpha" />
</Header>
{children}
</Page>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './Layout';
@@ -0,0 +1,36 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { ContentHeader, SupportButton } from '@backstage/core';
import { Box, Typography } from '@material-ui/core';
export type Props = { title?: string };
export const PluginHeader = ({ title = 'Jenkins' }) => {
return (
<ContentHeader
title={title}
titleComponent={() => (
<Box alignItems="center" display="flex">
<Typography variant="h4">{title}</Typography>
</Box>
)}
>
<SupportButton>
This plugin allows you to view and interact with your builds in Jenkins.
</SupportButton>
</ContentHeader>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './PluginHeader';
+18
View File
@@ -0,0 +1,18 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { plugin, JenkinsBuildsWidget, JenkinsLastBuildWidget } from './plugin';
export * from './api';
@@ -0,0 +1,164 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Content, InfoCard, Progress } from '@backstage/core';
import { Grid, Box, Link, IconButton } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import { PluginHeader } from '../../components/PluginHeader';
import { ActionOutput } from './lib/ActionOutput/ActionOutput';
import { Layout } from '../../components/Layout';
import LaunchIcon from '@material-ui/icons/Launch';
import GitHubIcon from '@material-ui/icons/GitHub';
import { useBuildWithSteps } from '../../state/useBuildWithSteps';
const IconLink = IconButton as typeof Link;
const BuildName: FC<{ build?: any }> = ({ build }) => (
<Box display="flex" alignItems="center">
{build?.buildName}
<IconLink href={build?.url} target="_blank" title="View on Jenkins">
<LaunchIcon /> {/* TODO use Jenkins logo*/}
</IconLink>
<IconLink href={build?.source.url} target="_blank" title="View on GitHub">
<GitHubIcon />
</IconLink>
</Box>
);
const useStyles = makeStyles(theme => ({
neutral: {},
failed: {
position: 'relative',
'&:after': {
pointerEvents: 'none',
content: '""',
position: 'absolute',
top: 0,
right: 0,
left: 0,
bottom: 0,
boxShadow: `inset 4px 0px 0px ${theme.palette.error.main}`,
},
},
running: {
position: 'relative',
'&:after': {
pointerEvents: 'none',
content: '""',
position: 'absolute',
top: 0,
right: 0,
left: 0,
bottom: 0,
boxShadow: `inset 4px 0px 0px ${theme.palette.info.main}`,
},
},
cardContent: {
backgroundColor: theme.palette.background.default,
},
success: {
position: 'relative',
'&:after': {
pointerEvents: 'none',
content: '""',
position: 'absolute',
top: 0,
right: 0,
left: 0,
bottom: 0,
boxShadow: `inset 4px 0px 0px ${theme.palette.success.main}`,
},
},
}));
const pickClassName = (
classes: ReturnType<typeof useStyles>,
build: any = {},
) => {
if (build.result === 'UNSTABLE') return classes.failed;
if (build.result === 'FAILURE') return classes.failed;
if (build.building) return classes.running;
if (build.status === 'SUCCESS') return classes.success;
return classes.neutral;
};
const Page = () => (
<Layout>
<Content>
<BuildWithStepsView />
</Content>
</Layout>
);
const BuildWithStepsView = () => {
const [searchParams] = useSearchParams();
const buildPath = searchParams.get('url') || '';
const classes = useStyles();
const [{ loading, value }, { startPolling, stopPolling }] = useBuildWithSteps(
buildPath,
);
useEffect(() => {
startPolling();
return () => stopPolling();
}, [buildPath, startPolling, stopPolling]);
return (
<>
<PluginHeader title={value?.source.displayName || 'Build details'} />
<Grid container spacing={3} direction="column">
<Grid item>
<InfoCard
className={pickClassName(classes, value)}
title={<BuildName build={value} />}
cardClassName={classes.cardContent}
>
{loading ? <Progress /> : <BuildsList build={value} />}
</InfoCard>
</Grid>
</Grid>
</>
);
};
const BuildsList: FC<{ build?: any }> = ({ build }) => (
<Box>
{build &&
build.steps &&
build.steps.map(({ name, actions }: { name: string; actions: any[] }) => (
<ActionsList name={name} actions={actions} />
))}
</Box>
);
const ActionsList: FC<{ actions: any[]; name: string }> = ({ actions }) => {
const classes = useStyles();
return (
<>
{actions.map((action: any) => (
<ActionOutput
className={action.failed ? classes.failed : classes.success}
action={action}
name={action.name}
url={action.output_url || ''}
/>
))}
</>
);
};
export default Page;
export { BuildWithStepsView as BuildWithSteps };
@@ -0,0 +1,19 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {
default as DetailedViewPage,
BuildWithSteps,
} from './BuildWithStepsPage';
@@ -0,0 +1,67 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useEffect, FC } from 'react';
import {
ExpansionPanel,
ExpansionPanelSummary,
Typography,
ExpansionPanelDetails,
} from '@material-ui/core';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles({
expansionPanelDetails: {
padding: 0,
},
button: {
order: -1,
marginRight: 0,
marginLeft: '-20px',
},
});
export const ActionOutput: FC<{
url: string;
name: string;
className?: string;
action: any;
}> = ({ url, name, className }) => {
const classes = useStyles();
useEffect(() => {}, [url]);
return (
<ExpansionPanel
TransitionProps={{ unmountOnExit: true }}
className={className}
>
<ExpansionPanelSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`panel-${name}-content`}
id={`panel-${name}-header`}
IconButtonProps={{
className: classes.button,
}}
>
<Typography variant="button">{name}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails className={classes.expansionPanelDetails}>
Nothing here...
</ExpansionPanelDetails>
</ExpansionPanel>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { ActionOutput } from './ActionOutput';
@@ -0,0 +1,38 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { CITable } from '../CITable';
import { useBuilds } from '../../../../state';
export const Builds = ({ owner, repo }: { owner: string; repo: string }) => {
const [
{ total, loading, value, projectName, page, pageSize },
{ setPage, retry, setPageSize },
] = useBuilds(owner, repo);
return (
<CITable
total={total}
loading={loading}
retry={retry}
builds={value ?? []}
projectName={projectName}
page={page}
onChangePage={setPage}
pageSize={pageSize}
onChangePageSize={setPageSize}
/>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { Builds } from './Builds';
@@ -0,0 +1,218 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC } from 'react';
import { Link, Typography, Box, IconButton } from '@material-ui/core';
import RetryIcon from '@material-ui/icons/Replay';
import GitHubIcon from '@material-ui/icons/GitHub';
import { Link as RouterLink } from 'react-router-dom';
import { Table, TableColumn } from '@backstage/core';
import { JenkinsRunStatus } from '../Status';
export type CITableBuildInfo = {
id: string;
buildName: string;
buildUrl?: string;
source: {
branchName: string;
url: string;
displayName: string;
commit: {
hash: string;
};
};
status: string;
tests?: {
total: number;
passed: number;
skipped: number;
failed: number;
testUrl: string;
};
onRestartClick: () => void;
};
const FailCount = ({ count }: { count: number }): JSX.Element | null => {
if (count !== 0) {
return <>{count} failed</>;
}
return null;
};
const SkippedCount = ({ count }: { count: number }): JSX.Element | null => {
if (count !== 0) {
return <>{count} skipped</>;
}
return null;
};
const FailSkippedWidget = ({
skipped,
failed,
}: {
skipped: number;
failed: number;
}): JSX.Element | null => {
if (skipped === 0 && failed === 0) {
return null;
}
if (skipped !== 0 && failed !== 0) {
return (
<>
{' '}
(<FailCount count={failed} />, <SkippedCount count={skipped} />)
</>
);
}
if (failed !== 0) {
return (
<>
{' '}
(<FailCount count={failed} />)
</>
);
}
if (skipped !== 0) {
return (
<>
{' '}
(<SkippedCount count={skipped} />)
</>
);
}
return null;
};
const generatedColumns: TableColumn[] = [
{
title: 'Build',
field: 'buildName',
highlight: true,
render: (row: Partial<CITableBuildInfo>) => (
<Link component={RouterLink} to={`/jenkins/job?url=${row.id}`}>
{row.buildName}
</Link>
),
},
{
title: 'Source',
render: (row: Partial<CITableBuildInfo>) => (
<>
<p>
<Link href={row.source?.url || ''} target="_blank">
{row.source?.branchName}
</Link>
</p>
<p>{row.source?.commit?.hash}</p>
</>
),
},
{
title: 'Status',
render: (row: Partial<CITableBuildInfo>) => {
return (
<Box display="flex" alignItems="center">
<JenkinsRunStatus status={row.status} />
</Box>
);
},
},
{
title: 'Tests',
render: (row: Partial<CITableBuildInfo>) => {
return (
<>
<p>
{row.tests && (
<Link href={row.tests.testUrl || ''} target="_blank">
{row.tests.passed} / {row.tests.total} passed
<FailSkippedWidget
skipped={row.tests.skipped}
failed={row.tests.failed}
/>
</Link>
)}
{!row.tests && 'n/a'}
</p>
</>
);
},
},
{
title: 'Actions',
render: (row: Partial<CITableBuildInfo>) => (
<IconButton onClick={row.onRestartClick}>
<RetryIcon />
</IconButton>
),
width: '10%',
},
];
type Props = {
loading: boolean;
retry: () => void;
builds: CITableBuildInfo[];
projectName: string;
page: number;
onChangePage: (page: number) => void;
total: number;
pageSize: number;
onChangePageSize: (pageSize: number) => void;
};
export const CITable: FC<Props> = ({
projectName,
loading,
pageSize,
page,
retry,
builds,
onChangePage,
onChangePageSize,
total,
}) => {
return (
<Table
isLoading={loading}
options={{ paging: true, pageSize }}
totalCount={total}
page={page}
actions={[
{
icon: () => <RetryIcon />,
tooltip: 'Refresh Data',
isFreeAction: true,
onClick: () => retry(),
},
]}
data={builds}
onChangePage={onChangePage}
onChangeRowsPerPage={onChangePageSize}
title={
<Box display="flex" alignItems="center">
<GitHubIcon />
<Box mr={1} />
<Typography variant="h6">{projectName}</Typography>
</Box>
}
columns={generatedColumns}
/>
);
};
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { CITable } from './CITable';
export type { CITableBuildInfo } from './CITable';
@@ -0,0 +1,78 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
StatusPending,
StatusRunning,
StatusOK,
StatusWarning,
StatusAborted,
StatusError,
} from '@backstage/core';
import React from 'react';
export const JenkinsRunStatus = ({
status,
}: {
status: string | undefined;
}) => {
if (status === undefined) return null;
switch (status.toLowerCase()) {
case 'queued':
case 'scheduled':
return (
<>
<StatusPending /> Queued
</>
);
case 'running':
return (
<>
<StatusRunning /> In progress
</>
);
case 'unstable':
return (
<>
<StatusWarning /> Unstable
</>
);
case 'failure':
return (
<>
<StatusError /> Failed
</>
);
case 'success':
return (
<>
<StatusOK /> Completed
</>
);
case 'aborted':
return (
<>
<StatusAborted /> Aborted
</>
);
default:
return (
<>
<StatusWarning /> {status}
</>
);
}
};
@@ -0,0 +1,16 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { JenkinsRunStatus } from './JenkinsRunStatus';
+23
View File
@@ -0,0 +1,23 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { plugin } from './plugin';
describe('jenkins', () => {
it('should export plugin', () => {
expect(plugin).toBeDefined();
});
});
+33
View File
@@ -0,0 +1,33 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createPlugin, createRouteRef } from '@backstage/core';
import { DetailedViewPage } from './pages/BuildWithStepsPage';
export const buildRouteRef = createRouteRef({
path: '/jenkins/job',
title: 'Jenkins run',
});
export const plugin = createPlugin({
id: 'jenkins',
register({ router }) {
router.addRoute(buildRouteRef, DetailedViewPage);
},
});
export { JenkinsBuildsWidget } from './components/JenkinsPluginWidget/JenkinsBuildsWidget';
export { JenkinsLastBuildWidget } from './components/JenkinsPluginWidget/JenkinsLastBuildWidget';
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@testing-library/jest-dom';
require('jest-fetch-mock').enableMocks();
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './useBuilds';
export * from './useBuildWithSteps';
@@ -0,0 +1,37 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useRef } from 'react';
export const useAsyncPolling = (
pollingFn: () => Promise<any>,
interval: number,
) => {
const isPolling = useRef<boolean>(false);
const startPolling = async () => {
if (isPolling.current === true) return;
isPolling.current = true;
while (isPolling.current === true) {
await pollingFn();
await new Promise(resolve => setTimeout(resolve, interval));
}
};
const stopPolling = () => {
isPolling.current = false;
};
return { startPolling, stopPolling };
};
@@ -0,0 +1,68 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { errorApiRef, useApi } from '@backstage/core';
import { useCallback } from 'react';
import { useAsyncRetry } from 'react-use';
import { jenkinsApiRef } from '../api/index';
import { useAsyncPolling } from './useAsyncPolling';
const INTERVAL_AMOUNT = 1500;
export function useBuildWithSteps(buildName: string) {
const api = useApi(jenkinsApiRef);
const errorApi = useApi(errorApiRef);
const getBuildWithSteps = useCallback(async () => {
try {
const build = await api.getBuild(buildName);
const { jobName } = api.extractJobDetailsFromBuildName(buildName);
const job = await api.getJob(jobName);
const jobInfo = api.extractScmDetailsFromJob(job);
return Promise.resolve(api.mapJenkinsBuildToCITable(build, jobInfo));
} catch (e) {
errorApi.post(e);
return Promise.reject(e);
}
}, [buildName, api, errorApi]);
const restartBuild = async () => {
try {
await api.retry(buildName);
} catch (e) {
errorApi.post(e);
}
};
const { loading, value, retry } = useAsyncRetry(() => getBuildWithSteps(), [
getBuildWithSteps,
]);
const { startPolling, stopPolling } = useAsyncPolling(
getBuildWithSteps,
INTERVAL_AMOUNT,
);
return [
{ loading, value, retry },
{
restartBuild,
getBuildWithSteps,
startPolling,
stopPolling,
},
] as const;
}
+82
View File
@@ -0,0 +1,82 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { errorApiRef, useApi } from '@backstage/core';
import { useCallback, useEffect, useState } from 'react';
import { useAsyncRetry } from 'react-use';
import { jenkinsApiRef } from '../api';
export function useBuilds(owner: string, repo: string, branch?: string) {
const api = useApi(jenkinsApiRef);
const errorApi = useApi(errorApiRef);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(5);
const getBuilds = useCallback(async () => {
try {
let build;
if (branch) {
build = await api.getLastBuild(`${owner}/${repo}/${branch}`);
} else {
build = await api.getFolder(`${owner}/${repo}`);
}
return build;
} catch (e) {
errorApi.post(e);
return Promise.reject(e);
}
}, [api, branch, errorApi, owner, repo]);
const restartBuild = async (buildName: string) => {
try {
await api.retry(buildName);
} catch (e) {
errorApi.post(e);
}
};
useEffect(() => {
getBuilds().then(b => {
const size = Array.isArray(b) ? b?.[0].build_num! : 1;
setTotal(size);
});
}, [repo, getBuilds]);
const { loading, value, retry } = useAsyncRetry(
() => getBuilds().then(builds => builds ?? [], restartBuild),
[page, pageSize, getBuilds],
);
const projectName = `${owner}/${repo}`;
return [
{
page,
pageSize,
loading,
value,
projectName,
total,
},
{
getBuilds,
setPage,
setPageSize,
restartBuild,
retry,
},
] as const;
}
+35 -3
View File
@@ -3183,6 +3183,26 @@
resolved "https://registry.npmjs.org/@rjsf/material-ui/-/material-ui-2.1.0.tgz#a361b125af3a383b7f671634b8d254345f9f9311"
integrity sha512-9mMttnPNP6GiP7BtZGimYcYsbWwjyviqg/PD8oxrkEtZykULXOIdC3WDMJ3nPSym8RvZsgSnB8bajCpa5iSYQQ==
"@roadiehq/backstage-plugin-travis-ci@^0.1.3":
version "0.1.3"
resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-travis-ci/-/backstage-plugin-travis-ci-0.1.3.tgz#597882bb6db119b9c8970f15cc506c5d70ba2c41"
integrity sha512-xTnc71VtQSp7qfiE/p92U2/FEAvsG32BHIELebYZGCIt+ZqY/fSiXZSGsOXFDlg9adTu0e8ro6HWnknXCdo2pw==
dependencies:
"@backstage/core" "0.1.1-alpha.18"
"@backstage/theme" "0.1.1-alpha.18"
"@material-ui/core" "^4.9.1"
"@material-ui/icons" "^4.9.1"
"@material-ui/lab" "4.0.0-alpha.45"
date-fns "^2.15.0"
history "^5.0.0"
moment "^2.27.0"
react "^16.13.1"
react-dom "^16.13.1"
react-lazylog "^4.5.2"
react-router "6.0.0-beta.0"
react-router-dom "6.0.0-beta.0"
react-use "^15.3.3"
"@rollup/plugin-commonjs@^13.0.0":
version "13.0.0"
resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-13.0.0.tgz#8a1d684ba6848afe8b9e3d85649d4b2f6f7217ec"
@@ -8676,7 +8696,7 @@ date-fns@^1.27.2:
resolved "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
date-fns@^2.0.0-alpha.27, date-fns@^2.0.1:
date-fns@^2.0.0-alpha.27, date-fns@^2.0.1, date-fns@^2.15.0:
version "2.15.0"
resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.15.0.tgz#424de6b3778e4e69d3ff27046ec136af58ae5d5f"
integrity sha512-ZCPzAMJZn3rNUvvQIMlXhDr4A+Ar07eLeGsGREoWU19a3Pqf5oYa+ccd+B3F6XVtQY6HANMFdOQ8A+ipFnvJdQ==
@@ -13286,6 +13306,13 @@ iterate-value@^1.0.0:
es-get-iterator "^1.0.2"
iterate-iterator "^1.0.1"
jenkins@^0.28.0:
version "0.28.0"
resolved "https://registry.npmjs.org/jenkins/-/jenkins-0.28.0.tgz#72d6fcc452145403b34f6d4ecbd877ee1ab77fca"
integrity sha512-EGzzZcyFwXBCPZZoDNvZTPmZOqaHALfAStNfCF37oh+Mc6G/e0MwIuQjx5kMEynTXR9bF5EwLiuMTiTf5kHk5g==
dependencies:
papi "^0.29.0"
jest-changed-files@^26.0.1:
version "26.0.1"
resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.0.1.tgz#1334630c6a1ad75784120f39c3aa9278e59f349f"
@@ -15412,12 +15439,12 @@ modify-values@^1.0.0:
resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"
integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==
moment@2.26.0, moment@^2.25.3, moment@^2.26.0:
moment@2.26.0:
version "2.26.0"
resolved "https://registry.npmjs.org/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a"
integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw==
moment@^2.27.0:
moment@^2.25.3, moment@^2.26.0, moment@^2.27.0:
version "2.27.0"
resolved "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d"
integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==
@@ -16553,6 +16580,11 @@ pako@~1.0.5:
resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
papi@^0.29.0:
version "0.29.1"
resolved "https://registry.npmjs.org/papi/-/papi-0.29.1.tgz#7373e2c527f5117d61fd2a0e6c6b1dd72bf7f180"
integrity sha512-Y9ipSMfWuuVFO3zY9PlxOmEg+bQ7CeJ28sa9/a0veYNynLf9fwjR3+3fld5otEy7okUaEOUuCHVH62MyTmACXQ==
parallel-transform@^1.1.0:
version "1.2.0"
resolved "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc"