diff --git a/.changeset/spicy-poets-sell.md b/.changeset/spicy-poets-sell.md new file mode 100644 index 0000000000..4bebd71344 --- /dev/null +++ b/.changeset/spicy-poets-sell.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-jenkins': patch +--- + +Extend EntityJenkinsContent to receive columns as prop diff --git a/plugins/jenkins/README.md b/plugins/jenkins/README.md index 2a0ec451c8..d21fee2b8a 100644 --- a/plugins/jenkins/README.md +++ b/plugins/jenkins/README.md @@ -5,7 +5,7 @@ Website: [https://jenkins.io/](https://jenkins.io/) Last master build Folder results Build details - +Modify Table Columns ## Setup 1. If you have a standalone app (you didn't clone this repo), then do @@ -97,3 +97,30 @@ spec: - Only works with organization folder projects backed by GitHub - No pagination support currently, limited to 50 projects - don't run this on a Jenkins instance with lots of builds + +## Modify Columns of EntityJenkinsContent + +- now you can pass down column props to show the columns/metadata as per your use case. + +```tsx +export const generatedColumns: TableColumn[] = [ + { + title: 'Timestamp', + field: 'lastBuild.timestamp', + render: (row: Partial) => ( + <> + + {` + ${new Date(row.lastBuild?.timestamp).toLocaleDateString()} + ${new Date(row.lastBuild?.timestamp).toLocaleTimeString()} + `} + + + ), + }, +] + +// ... + +// ... +``` diff --git a/plugins/jenkins/api-report.md b/plugins/jenkins/api-report.md index 611a56cd5f..2274c11e13 100644 --- a/plugins/jenkins/api-report.md +++ b/plugins/jenkins/api-report.md @@ -15,9 +15,12 @@ import { InfoCardVariants } from '@backstage/core-components'; import { JSX as JSX_2 } from 'react'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; +import { TableColumn } from '@backstage/core-components'; // @public (undocumented) -export const EntityJenkinsContent: () => JSX_2.Element; +export const EntityJenkinsContent: (props: { + columns?: TableColumn[] | undefined; +}) => JSX_2.Element; // @public (undocumented) export const EntityLatestJenkinsRunCard: (props: { @@ -45,7 +48,6 @@ export interface JenkinsApi { jobFullName: string; buildNumber: string; }): Promise; - // Warning: (ae-forgotten-export) The symbol "Project" needs to be exported by the entry point index.d.ts getProjects(options: { entity: CompoundEntityRef; filter: { @@ -117,8 +119,28 @@ export const LatestRunCard: (props: { // @public (undocumented) export const LEGACY_JENKINS_ANNOTATION = 'jenkins.io/github-folder'; +// @public (undocumented) +export interface Project { + // (undocumented) + displayName: string; + // (undocumented) + fullDisplayName: string; + // (undocumented) + fullName: string; + // (undocumented) + inQueue: string; + // (undocumented) + lastBuild: Build; + // (undocumented) + onRestartClick: () => Promise; + // (undocumented) + status: string; +} + // Warning: (ae-missing-release-tag) "Router" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const Router: () => React_2.JSX.Element; +export const Router: (props: { + columns?: TableColumn[]; +}) => React_2.JSX.Element; ``` diff --git a/plugins/jenkins/src/api/JenkinsApi.ts b/plugins/jenkins/src/api/JenkinsApi.ts index 04bd313227..83b38e63f0 100644 --- a/plugins/jenkins/src/api/JenkinsApi.ts +++ b/plugins/jenkins/src/api/JenkinsApi.ts @@ -57,6 +57,7 @@ export interface Build { status: string; // == building ? 'running' : result, } +/** @public */ export interface Project { // standard Jenkins lastBuild: Build; diff --git a/plugins/jenkins/src/api/index.ts b/plugins/jenkins/src/api/index.ts index af1829c0b4..32c98ae5a6 100644 --- a/plugins/jenkins/src/api/index.ts +++ b/plugins/jenkins/src/api/index.ts @@ -16,4 +16,4 @@ export { JenkinsClient, jenkinsApiRef } from './JenkinsApi'; -export type { JenkinsApi } from './JenkinsApi'; +export type { JenkinsApi, Project } from './JenkinsApi'; diff --git a/plugins/jenkins/src/assets/dynamic-columns.png b/plugins/jenkins/src/assets/dynamic-columns.png new file mode 100644 index 0000000000..1a85257403 Binary files /dev/null and b/plugins/jenkins/src/assets/dynamic-columns.png differ diff --git a/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx b/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx index f82ce2e7ec..1f5d272c61 100644 --- a/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx +++ b/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx @@ -13,222 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Link, Progress, Table, TableColumn } from '@backstage/core-components'; -import { alertApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; -import { useEntityPermission } from '@backstage/plugin-catalog-react/alpha'; -import { Box, IconButton, Tooltip, Typography } from '@material-ui/core'; +import { Table, TableColumn } from '@backstage/core-components'; +import { Box, Typography } from '@material-ui/core'; import RetryIcon from '@material-ui/icons/Replay'; -import VisibilityIcon from '@material-ui/icons/Visibility'; -import { default as React, useState } from 'react'; +import { default as React } from 'react'; import { Project } from '../../../../api/JenkinsApi'; import JenkinsLogo from '../../../../assets/JenkinsLogo.svg'; -import { buildRouteRef } from '../../../../plugin'; import { useBuilds } from '../../../useBuilds'; -import { JenkinsRunStatus } from '../Status'; -import { jenkinsExecutePermission } from '@backstage/plugin-jenkins-common'; - -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: 'Timestamp', - defaultSort: 'desc', - hidden: true, - field: 'lastBuild.timestamp', - }, - { - title: 'Build', - field: 'fullName', - highlight: true, - render: (row: Partial) => { - const LinkWrapper = () => { - const routeLink = useRouteRef(buildRouteRef); - if (!row.fullName || !row.lastBuild?.number) { - return ( - <> - {row.fullName || - row.fullDisplayName || - row.displayName || - 'Unknown'} - - ); - } - - return ( - - {row.fullDisplayName} - - ); - }; - - return ; - }, - }, - { - title: 'Source', - field: 'lastBuild.source.branchName', - render: (row: Partial) => ( - <> - - - {row.lastBuild?.source?.branchName} - - - {row.lastBuild?.source?.commit?.hash} - - ), - }, - { - title: 'Status', - field: 'status', - render: (row: Partial) => { - return ( - - - - ); - }, - }, - { - title: 'Tests', - sorting: false, - render: (row: Partial) => { - return ( - <> - - {row.lastBuild?.tests && ( - - {row.lastBuild?.tests.passed} / {row.lastBuild?.tests.total}{' '} - passed - - - )} - - {!row.lastBuild?.tests && 'n/a'} - - - ); - }, - }, - { - title: 'Actions', - sorting: false, - render: (row: Partial) => { - const ActionWrapper = () => { - const [isLoadingRebuild, setIsLoadingRebuild] = useState(false); - const { allowed, loading } = useEntityPermission( - jenkinsExecutePermission, - ); - - const alertApi = useApi(alertApiRef); - - const onRebuild = async () => { - if (row.onRestartClick) { - setIsLoadingRebuild(true); - try { - await row.onRestartClick(); - alertApi.post({ - message: 'Jenkins re-build has successfully executed', - severity: 'success', - display: 'transient', - }); - } catch (e) { - alertApi.post({ - message: `Jenkins re-build has failed. Error: ${e.message}`, - severity: 'error', - }); - } finally { - setIsLoadingRebuild(false); - } - } - }; - - return ( -
- {row.lastBuild?.url && ( - - - - - - )} - {isLoadingRebuild && } - {!isLoadingRebuild && ( - - - - - - )} -
- ); - }; - return ; - }, - width: '10%', - }, -]; +import { columnFactories } from './columns'; +import { defaultCITableColumns } from './presets'; type Props = { loading: boolean; @@ -239,6 +32,7 @@ type Props = { total: number; pageSize: number; onChangePageSize: (pageSize: number) => void; + columns: TableColumn[]; }; export const CITableView = ({ @@ -249,6 +43,7 @@ export const CITableView = ({ projects, onChangePage, onChangePageSize, + columns, total, }: Props) => { const projectsInPage = projects?.slice( @@ -279,20 +74,31 @@ export const CITableView = ({ Projects } - columns={generatedColumns} + columns={ + columns && columns.length !== 0 ? columns : defaultCITableColumns + } /> ); }; -export const CITable = () => { +type CITableProps = { + columns?: TableColumn[]; +}; + +export const CITable = ({ columns }: CITableProps) => { const [tableProps, { setPage, retry, setPageSize }] = useBuilds(); return ( [])} retry={retry} onChangePageSize={setPageSize} onChangePage={setPage} /> ); }; + +CITable.columns = columnFactories; + +CITable.defaultCITableColumns = defaultCITableColumns; diff --git a/plugins/jenkins/src/components/BuildsPage/lib/CITable/columns.tsx b/plugins/jenkins/src/components/BuildsPage/lib/CITable/columns.tsx new file mode 100644 index 0000000000..733a60c1b2 --- /dev/null +++ b/plugins/jenkins/src/components/BuildsPage/lib/CITable/columns.tsx @@ -0,0 +1,251 @@ +/* + * 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, Progress, TableColumn } from '@backstage/core-components'; +import { alertApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { useEntityPermission } from '@backstage/plugin-catalog-react/alpha'; +import { Box, IconButton, Tooltip, Typography } from '@material-ui/core'; +import RetryIcon from '@material-ui/icons/Replay'; +import VisibilityIcon from '@material-ui/icons/Visibility'; +import { default as React, useState } from 'react'; +import { Project } from '../../../../api/JenkinsApi'; +import { buildRouteRef } from '../../../../plugin'; +import { JenkinsRunStatus } from '../Status'; +import { jenkinsExecutePermission } from '@backstage/plugin-jenkins-common'; + +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; +}; + +export const columnFactories = Object.freeze({ + createTimestampColumn(): TableColumn { + return { + title: 'Timestamp', + defaultSort: 'desc', + hidden: true, + field: 'lastBuild.timestamp', + }; + }, + + createBuildColumn(): TableColumn { + return { + title: 'Build', + field: 'fullName', + highlight: true, + render: (row: Partial) => { + const LinkWrapper = () => { + const routeLink = useRouteRef(buildRouteRef); + if (!row.fullName || !row.lastBuild?.number) { + return ( + <> + {row.fullName || + row.fullDisplayName || + row.displayName || + 'Unknown'} + + ); + } + + return ( + + {row.fullDisplayName} + + ); + }; + + return ; + }, + }; + }, + + createSourceColumn(): TableColumn { + return { + title: 'Source', + field: 'lastBuild.source.branchName', + render: (row: Partial) => ( + <> + + + {row.lastBuild?.source?.branchName} + + + + {row.lastBuild?.source?.commit?.hash} + + + ), + }; + }, + + createStatusColumn(): TableColumn { + return { + title: 'Status', + field: 'status', + render: (row: Partial) => { + return ( + + + + ); + }, + }; + }, + + createTestColumn(): TableColumn { + return { + title: 'Tests', + sorting: false, + render: (row: Partial) => { + return ( + <> + + {row.lastBuild?.tests && ( + + {row.lastBuild?.tests.passed} / {row.lastBuild?.tests.total}{' '} + passed + + + )} + + {!row.lastBuild?.tests && 'n/a'} + + + ); + }, + }; + }, + + createActionsColumn(): TableColumn { + return { + title: 'Actions', + sorting: false, + render: (row: Partial) => { + const ActionWrapper = () => { + const [isLoadingRebuild, setIsLoadingRebuild] = useState(false); + const { allowed, loading } = useEntityPermission( + jenkinsExecutePermission, + ); + + const alertApi = useApi(alertApiRef); + + const onRebuild = async () => { + if (row.onRestartClick) { + setIsLoadingRebuild(true); + try { + await row.onRestartClick(); + alertApi.post({ + message: 'Jenkins re-build has successfully executed', + severity: 'success', + display: 'transient', + }); + } catch (e) { + alertApi.post({ + message: `Jenkins re-build has failed. Error: ${e.message}`, + severity: 'error', + }); + } finally { + setIsLoadingRebuild(false); + } + } + }; + + return ( +
+ {row.lastBuild?.url && ( + + + + + + )} + {isLoadingRebuild && } + {!isLoadingRebuild && ( + + + + + + )} +
+ ); + }; + return ; + }, + width: '10%', + }; + }, +}); diff --git a/plugins/jenkins/src/components/BuildsPage/lib/CITable/presets.ts b/plugins/jenkins/src/components/BuildsPage/lib/CITable/presets.ts new file mode 100644 index 0000000000..7c0dda9970 --- /dev/null +++ b/plugins/jenkins/src/components/BuildsPage/lib/CITable/presets.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2021 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 { Project } from '../../../../api'; +import { columnFactories } from './columns'; +import { TableColumn } from '@backstage/core-components'; + +export const defaultCITableColumns: TableColumn[] = [ + columnFactories.createTimestampColumn(), + columnFactories.createSourceColumn(), + columnFactories.createBuildColumn(), + columnFactories.createTestColumn(), + columnFactories.createStatusColumn(), + columnFactories.createActionsColumn(), +]; diff --git a/plugins/jenkins/src/components/Router.tsx b/plugins/jenkins/src/components/Router.tsx index af13575c62..364b5fde0b 100644 --- a/plugins/jenkins/src/components/Router.tsx +++ b/plugins/jenkins/src/components/Router.tsx @@ -15,7 +15,10 @@ */ import { Entity } from '@backstage/catalog-model'; -import { MissingAnnotationEmptyState } from '@backstage/core-components'; +import { + MissingAnnotationEmptyState, + TableColumn, +} from '@backstage/core-components'; import { useEntity } from '@backstage/plugin-catalog-react'; import React from 'react'; import { Route, Routes } from 'react-router-dom'; @@ -23,22 +26,25 @@ import { JENKINS_ANNOTATION, LEGACY_JENKINS_ANNOTATION } from '../constants'; import { buildRouteRef } from '../plugin'; import { CITable } from './BuildsPage/lib/CITable'; import { DetailedViewPage } from './BuildWithStepsPage/'; +import { Project } from '../api'; /** @public */ export const isJenkinsAvailable = (entity: Entity) => Boolean(entity.metadata.annotations?.[JENKINS_ANNOTATION]) || Boolean(entity.metadata.annotations?.[LEGACY_JENKINS_ANNOTATION]); -export const Router = () => { +export const Router = (props: { columns?: TableColumn[] }) => { const { entity } = useEntity(); if (!isJenkinsAvailable(entity)) { return ; } + const columns = props.columns; + return ( - } /> + } /> } /> );