jenkins JobRunTable added

Signed-off-by: Abhay-soni-developer <abhaysoni.developer@gmail.com>
This commit is contained in:
Abhay-soni-developer
2023-09-11 19:54:31 +05:30
parent cef192ebe3
commit 831c6c711f
9 changed files with 397 additions and 0 deletions
@@ -100,6 +100,7 @@ import {
EntityJenkinsContent,
EntityLatestJenkinsRunCard,
isJenkinsAvailable,
EntityJobRunsTable,
} from '@backstage/plugin-jenkins';
import { EntityKafkaContent } from '@backstage/plugin-kafka';
import { EntityKubernetesContent } from '@backstage/plugin-kubernetes';
@@ -245,6 +246,9 @@ export const cicdContent = (
<EntitySwitch>
<EntitySwitch.Case if={isJenkinsAvailable}>
<EntityJenkinsContent />
<hr />
<hr />
<EntityJobRunsTable />
</EntitySwitch.Case>
<EntitySwitch.Case if={isBuildkiteAvailable}>
@@ -64,6 +64,16 @@ export class JenkinsApiImpl {
${JenkinsApiImpl.jobTreeSpec}
]{0,50}`;
private static readonly jobBuildsTreeSpec = `
name,
description,
url,
fullName,
displayName,
fullDisplayName,
inQueue,
builds[*]`;
constructor(private readonly permissionApi?: PermissionEvaluator) {}
/**
@@ -329,4 +339,15 @@ export class JenkinsApiImpl {
const jobs = jobFullName.split('/');
return `${jenkinsInfo.baseUrl}/job/${jobs.join('/job/')}/${buildId}`;
}
async getJobBuilds(jenkinsInfo: JenkinsInfo) {
const client = await JenkinsApiImpl.getClient(jenkinsInfo);
const jobBuilds = await client.job.get({
name: jenkinsInfo.jobFullName,
tree: JenkinsApiImpl.jobBuildsTreeSpec.replace(/\s/g, ''),
});
return jobBuilds;
}
}
@@ -151,6 +151,32 @@ export async function createRouter(
},
);
router.get(
'/v1/entity/:namespace/:kind/:name/job/:jobFullName',
async (request, response) => {
const token = getBearerTokenFromAuthorizationHeader(
request.header('authorization'),
);
const { namespace, kind, name, jobFullName } = request.params;
const jenkinsInfo = await jenkinsInfoProvider.getInstance({
entityRef: {
kind,
namespace,
name,
},
jobFullName,
backstageToken: token,
});
const build = await jenkinsApi.getJobBuilds(jenkinsInfo);
response.json({
build: build,
});
},
);
router.post(
'/v1/entity/:namespace/:kind/:name/job/:jobFullName/:buildNumber',
async (request, response) => {
+52
View File
@@ -56,6 +56,30 @@ export interface Build {
};
status: string; // == building ? 'running' : result,
}
export interface JobBuild {
timestamp: number;
building: boolean;
duration: number;
result?: string;
fullDisplayName: string;
displayName: string;
url: string;
number: number;
inProgress: boolean;
queueId: number;
id: number;
}
export interface Job {
name: string;
displayName: string;
description: string;
fullDisplayName: string;
inQueue: boolean;
fullName: string;
url: string;
builds: JobBuild[];
}
export interface Project {
// standard Jenkins
@@ -67,6 +91,10 @@ export interface Project {
// added by us
status: string; // == inQueue ? 'queued' : lastBuild.building ? 'running' : lastBuild.result,
onRestartClick: () => Promise<void>; // TODO rename to handle.* ? also, should this be on lastBuild?
getJobBuilds(options: {
entity: CompoundEntityRef;
jobFullName: string;
}): Promise<Job>;
}
export interface JenkinsApi {
@@ -212,4 +240,28 @@ export class JenkinsClient implements JenkinsApi {
const { token } = await this.identityApi.getCredentials();
return token;
}
async getJobBuilds(options: {
entity: CompoundEntityRef;
jobFullName: string;
}): Promise<Job> {
const { entity, jobFullName } = options;
const url = `${await this.discoveryApi.getBaseUrl(
'jenkins',
)}/v1/entity/${encodeURIComponent(entity.namespace)}/${encodeURIComponent(
entity.kind,
)}/${encodeURIComponent(entity.name)}/job/${encodeURIComponent(
jobFullName,
)}`;
const idToken = await this.getToken();
const response = await fetch(url, {
method: 'GET',
headers: {
...(idToken && { Authorization: `Bearer ${idToken}` }),
},
});
return (await response.json()).build;
}
}
@@ -0,0 +1,186 @@
/*
* 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';
const generatedColumns: TableColumn[] = [
{
title: 'Number',
field: 'number',
render: (row: Partial<JobBuild>) => {
return (
<Box display="flex" alignItems="center">
<Typography paragraph>
<Link to={row.url ?? ''}>{row.number}</Link>
</Typography>
</Box>
);
},
},
{
title: 'Timestamp',
field: 'timestamp',
render: (row: Partial<JobBuild>) => {
return (
<Box display="flex" alignItems="center">
<Typography>
{row?.timestamp ? new Date(row?.timestamp).toLocaleString() : ' '}
</Typography>
</Box>
);
},
},
{
title: 'Result',
field: 'result',
render: (row: Partial<JobBuild>) => {
return (
<Box display="flex" alignItems="center">
{row.inProgress ? (
<Typography>In Progress</Typography>
) : (
<JenkinsRunStatus status={row?.result} />
)}
</Box>
);
},
},
{
title: 'Duration',
field: 'duration',
render: (row: Partial<JobBuild>) => {
return (
<Box display="flex" alignItems="center">
<Typography>
{row?.duration
? (row.duration / 1000).toFixed(1).toString().concat(' s')
: ''}
</Typography>
</Box>
);
},
},
{
title: 'Actions',
render: (row: Partial<JobBuild>) => {
const ActionWrapper = () => {
return (
<div style={{ width: '98px' }}>
{row?.url && (
<Tooltip title="View build">
<IconButton href={row.url} target="_blank">
<VisibilityIcon />
</IconButton>
</Tooltip>
)}
</div>
);
};
return <ActionWrapper />;
},
width: '10%',
},
];
type Props = {
loading: boolean;
jobRuns?: Job;
page: number;
onChangePage: (page: number) => void;
total: number;
pageSize: number;
onChangePageSize: (pageSize: number) => void;
};
export const JobRunsTableView = ({
loading,
pageSize,
page,
jobRuns,
onChangePage,
onChangePageSize,
total,
}: Props) => {
const builds = jobRuns?.builds.slice(
page * pageSize,
page * pageSize + pageSize,
);
let sumOfAllSuccessfullJobDuration = 0;
const successfullJobCount =
builds?.reduce((count, build) => {
if (!build.inProgress && build.result === 'SUCCESS') {
sumOfAllSuccessfullJobDuration += build.duration;
return count + 1;
}
return count;
}, 0) || 0;
let avgTime;
if (successfullJobCount > 0) {
avgTime = (sumOfAllSuccessfullJobDuration / successfullJobCount / 1000)
.toFixed(1)
.toString();
}
return (
<Table
isLoading={loading}
options={{ paging: true, pageSize, padding: 'dense' }}
totalCount={total}
page={page}
data={builds ?? []}
onPageChange={onChangePage}
onRowsPerPageChange={onChangePageSize}
title={
<Box>
<Box display="flex" alignItems="center">
<img src={JenkinsLogo} alt="Jenkins logo" height="50px" />
<Box mr={2} />
<Typography variant="h6">Job Runs</Typography>
</Box>
<Box display="flex" alignItems="center" mt={2}>
<Typography variant="h6">
Average Build Time For Last {successfullJobCount} Successfull jobs
: {avgTime || 0}
</Typography>
</Box>
</Box>
}
columns={generatedColumns}
/>
);
};
export const JobRunsTable = () => {
const [tableProps, { setPage, setPageSize }] = useJobRuns();
return (
<JobRunsTableView
{...tableProps}
onChangePageSize={setPageSize}
onChangePage={setPage}
/>
);
};
@@ -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';
@@ -0,0 +1,81 @@
/*
* 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';
import { JENKINS_ANNOTATION, LEGACY_JENKINS_ANNOTATION } from '../constants';
export enum ErrorType {
CONNECTION_ERROR,
NOT_FOUND,
}
export function useJobRuns() {
const { entity } = useEntity();
const api = useApi(jenkinsApiRef);
const errorApi = useApi(errorApiRef);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(5);
const [error, setError] = useState<{
message: string;
errorType: ErrorType;
}>();
const jobFullName =
entity.metadata.annotations?.[JENKINS_ANNOTATION] ||
entity.metadata.annotations?.[LEGACY_JENKINS_ANNOTATION] ||
'';
const { loading, value: jobRuns } = useAsyncRetry(async () => {
try {
const jobBuilds = await api.getJobBuilds({
entity: getCompoundEntityRef(entity),
jobFullName,
});
setTotal(jobBuilds.builds.length);
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,
total,
error,
},
{
setPage,
setPageSize,
},
] as const;
}
+1
View File
@@ -23,6 +23,7 @@
export {
jenkinsPlugin,
jenkinsPlugin as plugin,
EntityJobRunsTable,
EntityJenkinsContent,
EntityLatestJenkinsRunCard,
} from './plugin';
+10
View File
@@ -72,3 +72,13 @@ export const EntityLatestJenkinsRunCard = jenkinsPlugin.provide(
},
}),
);
/** @public */
export const EntityJobRunsTable = jenkinsPlugin.provide(
createComponentExtension({
name: 'EntityLatestJenkinsRunCard',
component: {
lazy: () => import('./components/JobRunsTable').then(m => m.JobRunsTable),
},
}),
);