diff --git a/app-config.yaml b/app-config.yaml index 5a5fdc1cab..c5c7a3674c 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -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 diff --git a/packages/app/package.json b/packages/app/package.json index 706c2d82bb..f42456709c 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -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", diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index de46418075..d9e3302146 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -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()); diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index 3e2b69a28f..7e803698fe 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -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'; diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index a0e69a647d..4f0e5df322 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -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", diff --git a/plugins/catalog/src/components/EntityPage/EntityPage.tsx b/plugins/catalog/src/components/EntityPage/EntityPage.tsx index c11844f722..5dd35bf8db 100644 --- a/plugins/catalog/src/components/EntityPage/EntityPage.tsx +++ b/plugins/catalog/src/components/EntityPage/EntityPage.tsx @@ -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<{}> = () => { - - - + {entity.metadata?.annotations?.[ + 'backstage.io/jenkins-github-folder' + ] && ( + + + + )} + {entity.metadata?.annotations?.[ + 'backstage.io/jenkins-github-folder' + ] && ( + + + + )} {entity.metadata?.annotations?.[ 'backstage.io/github-actions-id' ] && ( @@ -201,6 +213,12 @@ export const EntityPage: FC<{}> = () => { )} + + + +Folder results +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 diff --git a/plugins/jenkins/dev/index.tsx b/plugins/jenkins/dev/index.tsx new file mode 100644 index 0000000000..812a5585d4 --- /dev/null +++ b/plugins/jenkins/dev/index.tsx @@ -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(); diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json new file mode 100644 index 0000000000..dce577801e --- /dev/null +++ b/plugins/jenkins/package.json @@ -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" + ] +} diff --git a/plugins/jenkins/src/api/index.ts b/plugins/jenkins/src/api/index.ts new file mode 100644 index 0000000000..545a1da9cc --- /dev/null +++ b/plugins/jenkins/src/api/index.ts @@ -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({ + 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, + }; + } +} diff --git a/plugins/jenkins/src/assets/build-details.png b/plugins/jenkins/src/assets/build-details.png new file mode 100644 index 0000000000..396af428af Binary files /dev/null and b/plugins/jenkins/src/assets/build-details.png differ diff --git a/plugins/jenkins/src/assets/folder-results.png b/plugins/jenkins/src/assets/folder-results.png new file mode 100644 index 0000000000..2e14f3551f Binary files /dev/null and b/plugins/jenkins/src/assets/folder-results.png differ diff --git a/plugins/jenkins/src/assets/last-master-build.png b/plugins/jenkins/src/assets/last-master-build.png new file mode 100644 index 0000000000..517e7a4410 Binary files /dev/null and b/plugins/jenkins/src/assets/last-master-build.png differ diff --git a/plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsBuildsWidget.tsx b/plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsBuildsWidget.tsx new file mode 100644 index 0000000000..469869af69 --- /dev/null +++ b/plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsBuildsWidget.tsx @@ -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 ; +}; diff --git a/plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsLastBuildWidget.tsx b/plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsLastBuildWidget.tsx new file mode 100644 index 0000000000..9bd16bf9e6 --- /dev/null +++ b/plugins/jenkins/src/components/JenkinsPluginWidget/JenkinsLastBuildWidget.tsx @@ -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({ + externalLinkIcon: { + fontSize: 'inherit', + verticalAlign: 'bottom', + }, +}); + +const WidgetContent = ({ + loading, + lastRun, +}: { + loading?: boolean; + lastRun: any; + branch: string; +}) => { + const classes = useStyles(); + if (loading || !lastRun) return ; + return ( + + + + ), + build: lastRun.fullDisplayName, + url: ( + + See more on Jenkins{' '} + + + ), + }} + /> + ); +}; + +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 ( + + + + ); +}; diff --git a/plugins/jenkins/src/components/Layout/Layout.tsx b/plugins/jenkins/src/components/Layout/Layout.tsx new file mode 100644 index 0000000000..c6000437a3 --- /dev/null +++ b/plugins/jenkins/src/components/Layout/Layout.tsx @@ -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 ( + +
+ + +
+ {children} +
+ ); +}; diff --git a/plugins/jenkins/src/components/Layout/index.ts b/plugins/jenkins/src/components/Layout/index.ts new file mode 100644 index 0000000000..236fc98851 --- /dev/null +++ b/plugins/jenkins/src/components/Layout/index.ts @@ -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'; diff --git a/plugins/jenkins/src/components/PluginHeader/PluginHeader.tsx b/plugins/jenkins/src/components/PluginHeader/PluginHeader.tsx new file mode 100644 index 0000000000..501649e49a --- /dev/null +++ b/plugins/jenkins/src/components/PluginHeader/PluginHeader.tsx @@ -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 ( + ( + + {title} + + )} + > + + This plugin allows you to view and interact with your builds in Jenkins. + + + ); +}; diff --git a/plugins/jenkins/src/components/PluginHeader/index.ts b/plugins/jenkins/src/components/PluginHeader/index.ts new file mode 100644 index 0000000000..4de972f6f2 --- /dev/null +++ b/plugins/jenkins/src/components/PluginHeader/index.ts @@ -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'; diff --git a/plugins/jenkins/src/index.ts b/plugins/jenkins/src/index.ts new file mode 100644 index 0000000000..c7f2d8dc28 --- /dev/null +++ b/plugins/jenkins/src/index.ts @@ -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'; diff --git a/plugins/jenkins/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx b/plugins/jenkins/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx new file mode 100644 index 0000000000..ab4fc3f34f --- /dev/null +++ b/plugins/jenkins/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx @@ -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 }) => ( + + {build?.buildName} + + {/* TODO use Jenkins logo*/} + + + + + +); +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, + 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 = () => ( + + + + + +); + +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 ( + <> + + + + + } + cardClassName={classes.cardContent} + > + {loading ? : } + + + + + ); +}; + +const BuildsList: FC<{ build?: any }> = ({ build }) => ( + + {build && + build.steps && + build.steps.map(({ name, actions }: { name: string; actions: any[] }) => ( + + ))} + +); + +const ActionsList: FC<{ actions: any[]; name: string }> = ({ actions }) => { + const classes = useStyles(); + return ( + <> + {actions.map((action: any) => ( + + ))} + + ); +}; + +export default Page; +export { BuildWithStepsView as BuildWithSteps }; diff --git a/plugins/jenkins/src/pages/BuildWithStepsPage/index.ts b/plugins/jenkins/src/pages/BuildWithStepsPage/index.ts new file mode 100644 index 0000000000..fddff7088c --- /dev/null +++ b/plugins/jenkins/src/pages/BuildWithStepsPage/index.ts @@ -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'; diff --git a/plugins/jenkins/src/pages/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx b/plugins/jenkins/src/pages/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx new file mode 100644 index 0000000000..1143121a98 --- /dev/null +++ b/plugins/jenkins/src/pages/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx @@ -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 ( + + } + aria-controls={`panel-${name}-content`} + id={`panel-${name}-header`} + IconButtonProps={{ + className: classes.button, + }} + > + {name} + + + Nothing here... + + + ); +}; diff --git a/plugins/jenkins/src/pages/BuildWithStepsPage/lib/ActionOutput/index.ts b/plugins/jenkins/src/pages/BuildWithStepsPage/lib/ActionOutput/index.ts new file mode 100644 index 0000000000..7cf74c73f8 --- /dev/null +++ b/plugins/jenkins/src/pages/BuildWithStepsPage/lib/ActionOutput/index.ts @@ -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'; diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/Builds/Builds.tsx b/plugins/jenkins/src/pages/BuildsPage/lib/Builds/Builds.tsx new file mode 100644 index 0000000000..79f081c82d --- /dev/null +++ b/plugins/jenkins/src/pages/BuildsPage/lib/Builds/Builds.tsx @@ -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 ( + + ); +}; diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/Builds/index.ts b/plugins/jenkins/src/pages/BuildsPage/lib/Builds/index.ts new file mode 100644 index 0000000000..e91a9496b7 --- /dev/null +++ b/plugins/jenkins/src/pages/BuildsPage/lib/Builds/index.ts @@ -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'; diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx b/plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx new file mode 100644 index 0000000000..7f17107374 --- /dev/null +++ b/plugins/jenkins/src/pages/BuildsPage/lib/CITable/CITable.tsx @@ -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 ( + <> + {' '} + (, ) + + ); + } + + if (failed !== 0) { + return ( + <> + {' '} + () + + ); + } + + if (skipped !== 0) { + return ( + <> + {' '} + () + + ); + } + + return null; +}; + +const generatedColumns: TableColumn[] = [ + { + title: 'Build', + field: 'buildName', + highlight: true, + render: (row: Partial) => ( + + {row.buildName} + + ), + }, + { + title: 'Source', + render: (row: Partial) => ( + <> +

+ + {row.source?.branchName} + +

+

{row.source?.commit?.hash}

+ + ), + }, + { + title: 'Status', + render: (row: Partial) => { + return ( + + + + ); + }, + }, + { + title: 'Tests', + render: (row: Partial) => { + return ( + <> +

+ {row.tests && ( + + {row.tests.passed} / {row.tests.total} passed + + + )} + + {!row.tests && 'n/a'} +

+ + ); + }, + }, + { + title: 'Actions', + render: (row: Partial) => ( + + + + ), + 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 = ({ + projectName, + loading, + pageSize, + page, + retry, + builds, + onChangePage, + onChangePageSize, + total, +}) => { + return ( + , + tooltip: 'Refresh Data', + isFreeAction: true, + onClick: () => retry(), + }, + ]} + data={builds} + onChangePage={onChangePage} + onChangeRowsPerPage={onChangePageSize} + title={ + + + + {projectName} + + } + columns={generatedColumns} + /> + ); +}; diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/CITable/index.ts b/plugins/jenkins/src/pages/BuildsPage/lib/CITable/index.ts new file mode 100644 index 0000000000..358939e69f --- /dev/null +++ b/plugins/jenkins/src/pages/BuildsPage/lib/CITable/index.ts @@ -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'; diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/Status/JenkinsRunStatus.tsx b/plugins/jenkins/src/pages/BuildsPage/lib/Status/JenkinsRunStatus.tsx new file mode 100644 index 0000000000..66a58abe76 --- /dev/null +++ b/plugins/jenkins/src/pages/BuildsPage/lib/Status/JenkinsRunStatus.tsx @@ -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 ( + <> + Queued + + ); + case 'running': + return ( + <> + In progress + + ); + case 'unstable': + return ( + <> + Unstable + + ); + case 'failure': + return ( + <> + Failed + + ); + case 'success': + return ( + <> + Completed + + ); + case 'aborted': + return ( + <> + Aborted + + ); + default: + return ( + <> + {status} + + ); + } +}; diff --git a/plugins/jenkins/src/pages/BuildsPage/lib/Status/index.ts b/plugins/jenkins/src/pages/BuildsPage/lib/Status/index.ts new file mode 100644 index 0000000000..f5dd4a55e5 --- /dev/null +++ b/plugins/jenkins/src/pages/BuildsPage/lib/Status/index.ts @@ -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'; diff --git a/plugins/jenkins/src/plugin.test.ts b/plugins/jenkins/src/plugin.test.ts new file mode 100644 index 0000000000..8c3f057ecb --- /dev/null +++ b/plugins/jenkins/src/plugin.test.ts @@ -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(); + }); +}); diff --git a/plugins/jenkins/src/plugin.ts b/plugins/jenkins/src/plugin.ts new file mode 100644 index 0000000000..979b0a31cf --- /dev/null +++ b/plugins/jenkins/src/plugin.ts @@ -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'; diff --git a/plugins/jenkins/src/setupTests.ts b/plugins/jenkins/src/setupTests.ts new file mode 100644 index 0000000000..8553642152 --- /dev/null +++ b/plugins/jenkins/src/setupTests.ts @@ -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(); diff --git a/plugins/jenkins/src/state/index.ts b/plugins/jenkins/src/state/index.ts new file mode 100644 index 0000000000..d21a380c2a --- /dev/null +++ b/plugins/jenkins/src/state/index.ts @@ -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'; diff --git a/plugins/jenkins/src/state/useAsyncPolling.ts b/plugins/jenkins/src/state/useAsyncPolling.ts new file mode 100644 index 0000000000..7ea0755368 --- /dev/null +++ b/plugins/jenkins/src/state/useAsyncPolling.ts @@ -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, + interval: number, +) => { + const isPolling = useRef(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 }; +}; diff --git a/plugins/jenkins/src/state/useBuildWithSteps.ts b/plugins/jenkins/src/state/useBuildWithSteps.ts new file mode 100644 index 0000000000..617b2b1f4f --- /dev/null +++ b/plugins/jenkins/src/state/useBuildWithSteps.ts @@ -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; +} diff --git a/plugins/jenkins/src/state/useBuilds.ts b/plugins/jenkins/src/state/useBuilds.ts new file mode 100644 index 0000000000..deb3125637 --- /dev/null +++ b/plugins/jenkins/src/state/useBuilds.ts @@ -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; +} diff --git a/yarn.lock b/yarn.lock index 6e4af10f6c..557ced9add 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"