diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index d96fe9841f..28fb012c12 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -14,17 +14,18 @@ * limitations under the License. */ import { - Router as GitHubActionsRouter, isPluginApplicableToEntity as isGitHubActionsAvailable, + RecentWorkflowRunsCard, + Router as GitHubActionsRouter, } from '@backstage/plugin-github-actions'; import { - Router as JenkinsRouter, isPluginApplicableToEntity as isJenkinsAvailable, LatestRunCard as JenkinsLatestRunCard, + Router as JenkinsRouter, } from '@backstage/plugin-jenkins'; import { - Router as CircleCIRouter, isPluginApplicableToEntity as isCircleCIAvailable, + Router as CircleCIRouter, } from '@backstage/plugin-circleci'; import { Router as ApiDocsRouter } from '@backstage/plugin-api-docs'; import { Router as SentryRouter } from '@backstage/plugin-sentry'; @@ -62,14 +63,15 @@ const CICDSwitcher = ({ entity }: { entity: Entity }) => { const OverviewContent = ({ entity }: { entity: Entity }) => ( - + + {isJenkinsAvailable(entity) && } + {isGitHubActionsAvailable(entity) && ( + + )} + + - {isJenkinsAvailable(entity) && ( - - - - )} ); diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx new file mode 100644 index 0000000000..e5cb931ec1 --- /dev/null +++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx @@ -0,0 +1,127 @@ +/* + * 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 type { Props as RecentWorkflowRunsCardProps } from './RecentWorkflowRunsCard'; +import React from 'react'; +import { render } from '@testing-library/react'; +import { RecentWorkflowRunsCard } from './RecentWorkflowRunsCard'; +import { useApi } from '@backstage/core-api'; +import { useWorkflowRuns } from '../useWorkflowRuns'; +import { ThemeProvider } from '@material-ui/core'; +import { lightTheme } from '@backstage/theme'; +import { MemoryRouter } from 'react-router'; + +jest.mock('../useWorkflowRuns', () => ({ + useWorkflowRuns: jest.fn(), +})); +jest.mock('@backstage/core-api'); + +describe('', () => { + const entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + annotations: { + 'github.com/project-slug': 'theorg/the-service', + }, + }, + spec: { + owner: 'guest', + type: 'service', + lifecycle: 'production', + }, + }; + + const workflowRuns = [1, 2, 3, 4, 5].map(n => ({ + id: `run-id-${n}`, + message: `Commit message for workflow ${n}`, + source: { branchName: `branch-${n}` }, + status: 'completed', + })); + const mockErrorApi = { post: jest.fn() }; + + beforeEach(() => { + (useWorkflowRuns as jest.Mock).mockReturnValue([{ runs: workflowRuns }]); + (useApi as jest.Mock).mockReturnValue(mockErrorApi); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + const renderSubject = (props: RecentWorkflowRunsCardProps = { entity }) => + render( + + + + + , + ); + + it('renders a table with a row for each workflow', async () => { + const subject = renderSubject(); + + workflowRuns.forEach(run => { + expect(subject.getByText(run.message)).toBeInTheDocument(); + }); + }); + + it('renders a workflow row correctly', async () => { + const subject = renderSubject(); + const [run] = workflowRuns; + expect(subject.getByText(run.message).closest('a')).toHaveAttribute( + 'href', + `/ci-cd/${run.id}`, + ); + expect(subject.getByText(run.source.branchName)).toBeInTheDocument(); + }); + + it('requests only the required number of workflow runs', async () => { + const limit = 3; + renderSubject({ entity, limit }); + expect(useWorkflowRuns).toHaveBeenCalledWith( + expect.objectContaining({ initialPageSize: limit }), + ); + }); + + it('uses the github repo and owner from the entity annotation', async () => { + renderSubject(); + expect(useWorkflowRuns).toHaveBeenCalledWith( + expect.objectContaining({ owner: 'theorg', repo: 'the-service' }), + ); + }); + + it('filters workflows by branch if one is specified', async () => { + const branch = 'master'; + renderSubject({ entity, branch }); + expect(useWorkflowRuns).toHaveBeenCalledWith( + expect.objectContaining({ branch }), + ); + }); + + describe('where there is an error fetching workflows', () => { + const error = 'error getting workflows'; + beforeEach(() => { + (useWorkflowRuns as jest.Mock).mockReturnValue([{ runs: [], error }]); + }); + + it('sends the error to the errorApi', async () => { + renderSubject(); + expect(mockErrorApi.post).toHaveBeenCalledWith(error); + }); + }); +}); diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx new file mode 100644 index 0000000000..014aed1633 --- /dev/null +++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx @@ -0,0 +1,88 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { errorApiRef, useApi } from '@backstage/core-api'; +import { GITHUB_ACTIONS_ANNOTATION } from '../useProjectName'; +import { useWorkflowRuns } from '../useWorkflowRuns'; +import React, { useEffect } from 'react'; +import { Table } from '@backstage/core'; +import { WorkflowRunStatus } from '../WorkflowRunStatus'; +import { Card, Link, TableContainer } from '@material-ui/core'; +import { generatePath, Link as RouterLink } from 'react-router-dom'; + +const firstLine = (message: string): string => message.split('\n')[0]; + +export type Props = { + entity: Entity; + branch?: string; + dense?: boolean; + limit?: number; +}; + +export const RecentWorkflowRunsCard = ({ + entity, + branch, + dense = false, + limit = 5, +}: Props) => { + const errorApi = useApi(errorApiRef); + const [owner, repo] = ( + entity?.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION] ?? '/' + ).split('/'); + const [{ runs = [], loading, error }] = useWorkflowRuns({ + owner, + repo, + branch, + initialPageSize: limit, + }); + useEffect(() => { + if (error) { + errorApi.post(error); + } + }, [error, errorApi]); + + return ( + + ( + + {firstLine(data.message)} + + ), + }, + { title: 'Branch', field: 'source.branchName' }, + { title: 'Status', field: 'status', render: WorkflowRunStatus }, + ]} + data={runs} + /> + + ); +}; diff --git a/plugins/github-actions/src/components/Cards/index.ts b/plugins/github-actions/src/components/Cards/index.ts index 8c987ea1d5..ab918d3b77 100644 --- a/plugins/github-actions/src/components/Cards/index.ts +++ b/plugins/github-actions/src/components/Cards/index.ts @@ -14,3 +14,4 @@ * limitations under the License. */ export { LatestWorkflowRunCard, LatestWorkflowsForBranchCard } from './Cards'; +export { RecentWorkflowRunsCard } from './RecentWorkflowRunsCard'; diff --git a/plugins/github-actions/src/components/useWorkflowRuns.ts b/plugins/github-actions/src/components/useWorkflowRuns.ts index f935d57810..25ede3f997 100644 --- a/plugins/github-actions/src/components/useWorkflowRuns.ts +++ b/plugins/github-actions/src/components/useWorkflowRuns.ts @@ -24,10 +24,12 @@ export function useWorkflowRuns({ owner, repo, branch, + initialPageSize = 5, }: { owner: string; repo: string; branch?: string; + initialPageSize?: number; }) { const api = useApi(githubActionsApiRef); const auth = useApi(githubAuthApiRef); @@ -36,7 +38,7 @@ export function useWorkflowRuns({ const [total, setTotal] = useState(0); const [page, setPage] = useState(0); - const [pageSize, setPageSize] = useState(5); + const [pageSize, setPageSize] = useState(initialPageSize); const { loading, value: runs, retry, error } = useAsyncRetry< WorkflowRun[]