,
+ 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"