@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -62,6 +62,7 @@ import {
|
||||
GithubActionsClient,
|
||||
githubActionsApiRef,
|
||||
} from '@backstage/plugin-github-actions';
|
||||
import { jenkinsApiRef, JenkinsApi } from '@backstage/plugin-jenkins';
|
||||
|
||||
import { TravisCIApi, travisCIApiRef } from '@roadiehq/backstage-plugin-travis-ci';
|
||||
|
||||
@@ -85,6 +86,8 @@ 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());
|
||||
|
||||
@@ -29,3 +29,4 @@ export { plugin as GithubActions } from '@backstage/plugin-github-actions';
|
||||
export { plugin as Rollbar } from '@backstage/plugin-rollbar';
|
||||
export { plugin as Newrelic } from '@backstage/plugin-newrelic';
|
||||
export { plugin as TravisCI } from '@roadiehq/backstage-plugin-travis-ci';
|
||||
export { plugin as Jenkins } from '@backstage/plugin-jenkins';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -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
|
||||
@@ -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();
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -12309,6 +12309,13 @@ iterate-value@^1.0.0:
|
||||
es-get-iterator "^1.0.2"
|
||||
iterate-iterator "^1.0.1"
|
||||
|
||||
jenkins@^0.28.0:
|
||||
version "0.28.0"
|
||||
resolved "https://registry.npmjs.org/jenkins/-/jenkins-0.28.0.tgz#72d6fcc452145403b34f6d4ecbd877ee1ab77fca"
|
||||
integrity sha512-EGzzZcyFwXBCPZZoDNvZTPmZOqaHALfAStNfCF37oh+Mc6G/e0MwIuQjx5kMEynTXR9bF5EwLiuMTiTf5kHk5g==
|
||||
dependencies:
|
||||
papi "^0.29.0"
|
||||
|
||||
jest-changed-files@^26.0.1:
|
||||
version "26.0.1"
|
||||
resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.0.1.tgz#1334630c6a1ad75784120f39c3aa9278e59f349f"
|
||||
@@ -15325,6 +15332,11 @@ pako@~1.0.5:
|
||||
resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
||||
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
|
||||
|
||||
papi@^0.29.0:
|
||||
version "0.29.1"
|
||||
resolved "https://registry.npmjs.org/papi/-/papi-0.29.1.tgz#7373e2c527f5117d61fd2a0e6c6b1dd72bf7f180"
|
||||
integrity sha512-Y9ipSMfWuuVFO3zY9PlxOmEg+bQ7CeJ28sa9/a0veYNynLf9fwjR3+3fld5otEy7okUaEOUuCHVH62MyTmACXQ==
|
||||
|
||||
parallel-transform@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc"
|
||||
|
||||
Reference in New Issue
Block a user