diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index d96fe9841f..79ca074c35 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -14,23 +14,24 @@
* 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';
import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs';
import { Router as KubernetesRouter } from '@backstage/plugin-kubernetes';
-import React from 'react';
+import React, { ReactNode } from 'react';
import {
AboutCard,
EntityPageLayout,
@@ -60,16 +61,34 @@ const CICDSwitcher = ({ entity }: { entity: Entity }) => {
}
};
+const RecentCICDRunsSwitcher = ({ entity }: { entity: Entity }) => {
+ let content: ReactNode;
+ switch (true) {
+ case isJenkinsAvailable(entity):
+ content = ;
+ break;
+ case isGitHubActionsAvailable(entity):
+ content = ;
+ break;
+ default:
+ content = null;
+ }
+ if (!content) {
+ return null;
+ }
+ return (
+
+ {content}
+
+ );
+};
+
const OverviewContent = ({ entity }: { entity: 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..1d6001e038
--- /dev/null
+++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx
@@ -0,0 +1,131 @@
+/*
+ * 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 { ApiProvider, ApiRegistry, errorApiRef } 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(),
+}));
+
+const mockErrorApi: jest.Mocked = {
+ post: jest.fn(),
+ error$: jest.fn(),
+};
+
+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',
+ }));
+
+ beforeEach(() => {
+ (useWorkflowRuns as jest.Mock).mockReturnValue([{ runs: workflowRuns }]);
+ });
+
+ 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[]