+
{row.lastBuild?.url && (
@@ -240,6 +261,17 @@ export const columnFactories = Object.freeze({
)}
+
+
+
+
+
+
+
);
};
diff --git a/plugins/jenkins/src/components/BuildsPage/lib/CITable/presets.ts b/plugins/jenkins/src/components/BuildsPage/lib/CITable/presets.ts
index 7c0dda9970..828e035c8e 100644
--- a/plugins/jenkins/src/components/BuildsPage/lib/CITable/presets.ts
+++ b/plugins/jenkins/src/components/BuildsPage/lib/CITable/presets.ts
@@ -24,5 +24,6 @@ export const defaultCITableColumns: TableColumn
[] = [
columnFactories.createBuildColumn(),
columnFactories.createTestColumn(),
columnFactories.createStatusColumn(),
+ columnFactories.createLastRunDuration(),
columnFactories.createActionsColumn(),
];
diff --git a/plugins/jenkins/src/components/JobRunsTable/JobRunsTable.tsx b/plugins/jenkins/src/components/JobRunsTable/JobRunsTable.tsx
new file mode 100644
index 0000000000..78b66b98eb
--- /dev/null
+++ b/plugins/jenkins/src/components/JobRunsTable/JobRunsTable.tsx
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2020 The Backstage Authors
+ *
+ * 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 { Link, Table, TableColumn } from '@backstage/core-components';
+import { Box, IconButton, Tooltip, Typography } from '@material-ui/core';
+import { default as React } from 'react';
+import JenkinsLogo from './../../assets/JenkinsLogo.svg';
+import { useJobRuns } from './../useJobRuns';
+import { Job, JobBuild } from './../../api/JenkinsApi';
+import { JenkinsRunStatus } from './../BuildsPage/lib/Status';
+import VisibilityIcon from '@material-ui/icons/Visibility';
+import { jobRunsRouteRef } from '../../plugin';
+import { useRouteRefParams } from '@backstage/core-plugin-api';
+
+const generatedColumns: TableColumn[] = [
+ {
+ title: 'Number',
+ field: 'number',
+ render: (row: Partial) => {
+ return (
+
+
+ {row.number}
+
+
+ );
+ },
+ },
+ {
+ title: 'Timestamp',
+ field: 'timestamp',
+ render: (row: Partial) => {
+ return (
+
+
+ {row?.timestamp ? new Date(row?.timestamp).toLocaleString() : ' '}
+
+
+ );
+ },
+ },
+ {
+ title: 'Result',
+ field: 'result',
+ render: (row: Partial) => {
+ return (
+
+ {row.inProgress ? (
+ In Progress
+ ) : (
+
+ )}
+
+ );
+ },
+ },
+ {
+ title: 'Duration',
+ field: 'duration',
+ render: (row: Partial) => {
+ return (
+
+
+ {row?.duration
+ ? (row.duration / 1000).toFixed(1).toString().concat(' s')
+ : ''}
+
+
+ );
+ },
+ },
+
+ {
+ title: 'Actions',
+ render: (row: Partial) => {
+ const ActionWrapper = () => {
+ return (
+
+ {row?.url && (
+
+
+
+
+
+ )}
+
+ );
+ };
+ return ;
+ },
+ width: '10%',
+ },
+];
+
+type Props = {
+ loading: boolean;
+ jobRuns?: Job;
+ page: number;
+ onChangePage: (page: number) => void;
+ pageSize: number;
+ onChangePageSize: (pageSize: number) => void;
+};
+
+export const JobRunsTableView = ({
+ loading,
+ pageSize,
+ page,
+ jobRuns,
+ onChangePage,
+ onChangePageSize,
+}: Props) => {
+ const builds = jobRuns?.builds.slice(
+ page * pageSize,
+ page * pageSize + pageSize,
+ );
+ let sumOfAllSuccessfulJobDuration = 0;
+
+ const successfulJobCount =
+ builds?.reduce((count, build) => {
+ if (!build.inProgress && build.result === 'SUCCESS') {
+ sumOfAllSuccessfulJobDuration += build.duration;
+ return count + 1;
+ }
+ return count;
+ }, 0) || 0;
+
+ let avgTime;
+
+ if (successfulJobCount > 0) {
+ avgTime = (sumOfAllSuccessfulJobDuration / successfulJobCount / 1000)
+ .toFixed(1)
+ .toString();
+ }
+
+ return (
+
+
+
+
+ {`${jobRuns?.displayName} Runs`}
+
+
+
+ Average Build Time For Last {successfulJobCount} Successful jobs :{' '}
+ {avgTime || 0}
+
+
+
+ }
+ columns={generatedColumns}
+ />
+ );
+};
+
+export const JobRunsTable = () => {
+ const { jobFullName } = useRouteRefParams(jobRunsRouteRef);
+ const [tableProps, { setPage, setPageSize }] = useJobRuns(jobFullName);
+
+ return (
+
+ );
+};
diff --git a/plugins/jenkins/src/components/JobRunsTable/index.ts b/plugins/jenkins/src/components/JobRunsTable/index.ts
new file mode 100644
index 0000000000..b74beeb315
--- /dev/null
+++ b/plugins/jenkins/src/components/JobRunsTable/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * 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 { JobRunsTable } from './JobRunsTable';
diff --git a/plugins/jenkins/src/components/Router.tsx b/plugins/jenkins/src/components/Router.tsx
index 364b5fde0b..30ce999f31 100644
--- a/plugins/jenkins/src/components/Router.tsx
+++ b/plugins/jenkins/src/components/Router.tsx
@@ -23,9 +23,10 @@ import { useEntity } from '@backstage/plugin-catalog-react';
import React from 'react';
import { Route, Routes } from 'react-router-dom';
import { JENKINS_ANNOTATION, LEGACY_JENKINS_ANNOTATION } from '../constants';
-import { buildRouteRef } from '../plugin';
+import { buildRouteRef, jobRunsRouteRef } from '../plugin';
import { CITable } from './BuildsPage/lib/CITable';
import { DetailedViewPage } from './BuildWithStepsPage/';
+import { JobRunsTable } from './JobRunsTable';
import { Project } from '../api';
/** @public */
@@ -46,6 +47,7 @@ export const Router = (props: { columns?: TableColumn[] }) => {
} />
} />
+ } />
);
};
diff --git a/plugins/jenkins/src/components/useJobRuns.ts b/plugins/jenkins/src/components/useJobRuns.ts
new file mode 100644
index 0000000000..d80e09b18b
--- /dev/null
+++ b/plugins/jenkins/src/components/useJobRuns.ts
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2020 The Backstage Authors
+ *
+ * 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 { useState } from 'react';
+import useAsyncRetry from 'react-use/lib/useAsyncRetry';
+import { jenkinsApiRef } from '../api';
+import { errorApiRef, useApi } from '@backstage/core-plugin-api';
+import { useEntity } from '@backstage/plugin-catalog-react';
+import { getCompoundEntityRef } from '@backstage/catalog-model';
+
+export enum ErrorType {
+ CONNECTION_ERROR,
+ NOT_FOUND,
+}
+
+export function useJobRuns(jobFullName: string) {
+ const { entity } = useEntity();
+ const api = useApi(jenkinsApiRef);
+ const errorApi = useApi(errorApiRef);
+
+ const [page, setPage] = useState(0);
+ const [pageSize, setPageSize] = useState(5);
+
+ const [error, setError] = useState<{
+ message: string;
+ errorType: ErrorType;
+ }>();
+
+ const { loading, value: jobRuns } = useAsyncRetry(async () => {
+ try {
+ const jobBuilds = await api.getJobBuilds({
+ entity: getCompoundEntityRef(entity),
+ jobFullName,
+ });
+ return jobBuilds;
+ } catch (e) {
+ const errorType = e.notFound
+ ? ErrorType.NOT_FOUND
+ : ErrorType.CONNECTION_ERROR;
+ setError({ message: e.message, errorType });
+ throw e;
+ }
+ }, [api, errorApi, entity]);
+
+ return [
+ {
+ page,
+ pageSize,
+ loading,
+ jobRuns,
+ error,
+ },
+ {
+ setPage,
+ setPageSize,
+ },
+ ] as const;
+}
diff --git a/plugins/jenkins/src/index.ts b/plugins/jenkins/src/index.ts
index 7562026bdc..9d092014a0 100644
--- a/plugins/jenkins/src/index.ts
+++ b/plugins/jenkins/src/index.ts
@@ -25,6 +25,7 @@ export {
jenkinsPlugin as plugin,
EntityJenkinsContent,
EntityLatestJenkinsRunCard,
+ EntityJobRunsTable,
} from './plugin';
export { LatestRunCard } from './components/Cards';
export {
diff --git a/plugins/jenkins/src/plugin.ts b/plugins/jenkins/src/plugin.ts
index 7dbb6bfde1..1b666da9a9 100644
--- a/plugins/jenkins/src/plugin.ts
+++ b/plugins/jenkins/src/plugin.ts
@@ -38,6 +38,13 @@ export const buildRouteRef = createSubRouteRef({
parent: rootRouteRef,
});
+/** @public */
+export const jobRunsRouteRef = createSubRouteRef({
+ id: 'jenkins/job/runs',
+ path: '/builds/:jobFullName/runs',
+ parent: rootRouteRef,
+});
+
/** @public */
export const jenkinsPlugin = createPlugin({
id: 'jenkins',
@@ -72,3 +79,13 @@ export const EntityLatestJenkinsRunCard = jenkinsPlugin.provide(
},
}),
);
+
+/** @public */
+export const EntityJobRunsTable = jenkinsPlugin.provide(
+ createComponentExtension({
+ name: 'EntityJobRunsTable',
+ component: {
+ lazy: () => import('./components/JobRunsTable').then(m => m.JobRunsTable),
+ },
+ }),
+);