feat: GH Workflow Runs as Card View (#20939)
* feat: GH Workflow Runs as Card View Signed-off-by: blbose <lillian.bose@philips.com> * feat: GH Workflow Runs as Card View Signed-off-by: blbose <lillian.bose@philips.com> * feat: GH Workflow Runs as Card View Signed-off-by: blbose <lillian.bose@philips.com> * fix dagda1 github link in OWNERS.md Signed-off-by: Paul Cowan <paul.cowan@cutting.scot> Signed-off-by: blbose <lillian.bose@philips.com> * Added description for publish:gerrit scaffolder actions Signed-off-by: Andy Muldoon <andy.muldoon@ericsson.com> Signed-off-by: blbose <lillian.bose@philips.com> * Add existing OpenCost plugin to the Backstage Plugin Directory Signed-off-by: Matt Ray <github@mattray.dev> Signed-off-by: blbose <lillian.bose@philips.com> * Squared Penny the Pig logo Signed-off-by: Matt Ray <github@mattray.dev> Signed-off-by: blbose <lillian.bose@philips.com> * chore(deps): update dependency @types/supertest to v2.0.15 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: blbose <lillian.bose@philips.com> * chore(deps): update dependency @types/stoppable to v1.1.2 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: blbose <lillian.bose@philips.com> * chore(deps): update dependency @types/rollup-plugin-peer-deps-external to v2.2.3 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: blbose <lillian.bose@philips.com> * chore(deps): update dependency @types/regression to v2.0.4 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: blbose <lillian.bose@philips.com> * chore(deps): update dependency @types/recursive-readdir to v2.2.3 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: blbose <lillian.bose@philips.com> * chore(deps): update dependency @types/recharts to v1.8.26 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: blbose <lillian.bose@philips.com> * chore(deps): update dependency @types/luxon to v3.3.3 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: blbose <lillian.bose@philips.com> * chore(deps): update dependency @types/swagger-ui-react to v4.18.2 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: blbose <lillian.bose@philips.com> * chore(deps): update dependency @types/tar to v6.1.7 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: blbose <lillian.bose@philips.com> * chore(deps): update dependency @types/xml2js to v0.4.13 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: blbose <lillian.bose@philips.com> * feat: GH Workflow Runs as Card View Signed-off-by: blbose <lillian.bose@philips.com> * feat: GH Workflow Runs as Card View Signed-off-by: blbose <lillian.bose@philips.com> * feat: GH Workflow Runs as Card View Signed-off-by: blbose <lillian.bose@philips.com> * feat: GH Workflow Runs as Card View Signed-off-by: blbose <lillian.bose@philips.com> * feat: GH Workflow Runs as Card View Signed-off-by: blbose <lillian.bose@philips.com> * feat: GH Workflow Runs as Card View Signed-off-by: blbose <lillian.bose@philips.com> * file mode changes revert Signed-off-by: blbose <lillian.bose@philips.com> * api-report warning fix Signed-off-by: blbose <lillian.bose@philips.com> * fix: UX perspective changes Signed-off-by: blbose <lillian.bose@philips.com> * fix:eslint warning removal Signed-off-by: blbose <lillian.bose@philips.com> * fix:avoiding of id based on acierto review Signed-off-by: blbose <lillian.bose@philips.com> * fix: latest review comments Signed-off-by: blbose <lillian.bose@philips.com> * fix: review comment changes from Freben Signed-off-by: blbose <lillian.bose@philips.com> * fix: review comment changes from Freben Signed-off-by: blbose <lillian.bose@philips.com> * fix: rebase conflict Signed-off-by: blbose <lillian.bose@philips.com> * fix: rebase conflicts Signed-off-by: blbose <lillian.bose@philips.com> * fix: rebase conflict Signed-off-by: blbose <lillian.bose@philips.com> * fix: rebase conflicts Signed-off-by: blbose <lillian.bose@philips.com> * fix: rebase conflict Signed-off-by: blbose <lillian.bose@philips.com> * fix: rebase conflicts Signed-off-by: blbose <lillian.bose@philips.com> * fix: rebase conflict Signed-off-by: blbose <lillian.bose@philips.com> * fix: rebase conflicts Signed-off-by: blbose <lillian.bose@philips.com> * fix: rebase conflict Signed-off-by: blbose <lillian.bose@philips.com> * fix: rebase conflicts Signed-off-by: blbose <lillian.bose@philips.com> * fix: rebase conflict Signed-off-by: blbose <lillian.bose@philips.com> * fix: rebase conflicts Signed-off-by: blbose <lillian.bose@philips.com> * fix: rebase conflict Signed-off-by: blbose <lillian.bose@philips.com> * fix: rebase conflicts Signed-off-by: blbose <lillian.bose@philips.com> --------- Signed-off-by: blbose <lillian.bose@philips.com> Signed-off-by: Paul Cowan <paul.cowan@cutting.scot> Signed-off-by: Andy Muldoon <andy.muldoon@ericsson.com> Signed-off-by: Matt Ray <github@mattray.dev> Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Paul Cowan <paul.cowan@cutting.scot> Co-authored-by: Andy Muldoon <andy.muldoon@ericsson.com> Co-authored-by: Matt Ray <github@mattray.dev> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-github-actions': patch
|
||||
---
|
||||
|
||||
Github Workflow Runs UI is modified to show in optional Card view instead of table, with branch selection option
|
||||
@@ -92,3 +92,19 @@ integrations:
|
||||
- There is a limit of 100 apps for one OAuth client/token pair
|
||||
- The OAuth application must be at the GitHub organization level in order to display the workflows. If you do
|
||||
not see any workflows, confirm the OAuth application was created in the organization and not a specific user account.
|
||||
|
||||
## Optional Workflow Runs Card View
|
||||
|
||||
Github Workflow Runs optional UI to show in Card view instead of table, with branch selection option
|
||||
|
||||
```tsx
|
||||
|
||||
// You can add the tab to any number of pages, the service page is shown as an
|
||||
// example given here
|
||||
const serviceEntityPage = (
|
||||
<EntityLayout>
|
||||
{/* other tabs... */}
|
||||
<EntityLayout.Route path="/github-actions" title="GitHub Actions">
|
||||
<EntityGithubActionsContent view='cards' />
|
||||
</EntityLayout.Route>
|
||||
```
|
||||
|
||||
@@ -16,6 +16,17 @@ import { RestEndpointMethodTypes } from '@octokit/rest';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
import { ScmAuthApi } from '@backstage/integration-react';
|
||||
|
||||
// @public (undocumented)
|
||||
export type Branch = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type Branches = {
|
||||
default_branch: string;
|
||||
branches: Branch[];
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export enum BuildStatus {
|
||||
// (undocumented)
|
||||
@@ -29,7 +40,7 @@ export enum BuildStatus {
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const EntityGithubActionsContent: () => JSX_2.Element;
|
||||
export const EntityGithubActionsContent: (props: RouterProps) => JSX_2.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const EntityLatestGithubActionRunCard: (props: {
|
||||
@@ -106,6 +117,21 @@ export type GithubActionsApi = {
|
||||
}) => Promise<
|
||||
RestEndpointMethodTypes['actions']['downloadJobLogsForWorkflowRun']['response']['data']
|
||||
>;
|
||||
listBranches: (options: {
|
||||
hostname?: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
page: number;
|
||||
}) => Promise<
|
||||
RestEndpointMethodTypes['repos']['listBranches']['response']['data']
|
||||
>;
|
||||
getDefaultBranch: (options: {
|
||||
hostname?: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
}) => Promise<
|
||||
RestEndpointMethodTypes['repos']['get']['response']['data']['default_branch']
|
||||
>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -124,6 +150,14 @@ export class GithubActionsClient implements GithubActionsApi {
|
||||
RestEndpointMethodTypes['actions']['downloadJobLogsForWorkflowRun']['response']['data']
|
||||
>;
|
||||
// (undocumented)
|
||||
getDefaultBranch(options: {
|
||||
hostname?: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
}): Promise<
|
||||
RestEndpointMethodTypes['repos']['get']['response']['data']['default_branch']
|
||||
>;
|
||||
// (undocumented)
|
||||
getWorkflow(options: {
|
||||
hostname?: string;
|
||||
owner: string;
|
||||
@@ -142,6 +176,15 @@ export class GithubActionsClient implements GithubActionsApi {
|
||||
RestEndpointMethodTypes['actions']['getWorkflowRun']['response']['data']
|
||||
>;
|
||||
// (undocumented)
|
||||
listBranches(options: {
|
||||
hostname?: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
page?: number;
|
||||
}): Promise<
|
||||
RestEndpointMethodTypes['repos']['listBranches']['response']['data']
|
||||
>;
|
||||
// (undocumented)
|
||||
listJobsForWorkflowRun(options: {
|
||||
hostname?: string;
|
||||
owner: string;
|
||||
@@ -226,7 +269,13 @@ export const RecentWorkflowRunsCard: (props: {
|
||||
}) => React_2.JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const Router: () => React_2.JSX.Element;
|
||||
export const Router: (props: RouterProps) => React_2.JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface RouterProps {
|
||||
// (undocumented)
|
||||
view?: 'cards' | 'table';
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type Step = {
|
||||
|
||||
@@ -63,6 +63,12 @@ const mockGithubActionsApi: GithubActionsApi = {
|
||||
async reRunWorkflow() {
|
||||
return {} as any;
|
||||
},
|
||||
async listBranches() {
|
||||
return {} as any;
|
||||
},
|
||||
async getDefaultBranch() {
|
||||
return {} as any;
|
||||
},
|
||||
};
|
||||
|
||||
createDevApp()
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 206 KiB |
@@ -83,4 +83,21 @@ export type GithubActionsApi = {
|
||||
}) => Promise<
|
||||
RestEndpointMethodTypes['actions']['downloadJobLogsForWorkflowRun']['response']['data']
|
||||
>;
|
||||
|
||||
listBranches: (options: {
|
||||
hostname?: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
page: number;
|
||||
}) => Promise<
|
||||
RestEndpointMethodTypes['repos']['listBranches']['response']['data']
|
||||
>;
|
||||
|
||||
getDefaultBranch: (options: {
|
||||
hostname?: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
}) => Promise<
|
||||
RestEndpointMethodTypes['repos']['get']['response']['data']['default_branch']
|
||||
>;
|
||||
};
|
||||
|
||||
@@ -174,4 +174,41 @@ export class GithubActionsClient implements GithubActionsApi {
|
||||
|
||||
return workflow.data;
|
||||
}
|
||||
|
||||
async listBranches(options: {
|
||||
hostname?: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
page?: number;
|
||||
}): Promise<
|
||||
RestEndpointMethodTypes['repos']['listBranches']['response']['data']
|
||||
> {
|
||||
const { hostname, owner, repo, page = 0 } = options;
|
||||
const octokit = await this.getOctokit(hostname);
|
||||
const response = await octokit.rest.repos.listBranches({
|
||||
owner,
|
||||
repo,
|
||||
per_page: 100,
|
||||
page,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getDefaultBranch(options: {
|
||||
hostname?: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
}): Promise<
|
||||
RestEndpointMethodTypes['repos']['get']['response']['data']['default_branch']
|
||||
> {
|
||||
const { hostname, owner, repo } = options;
|
||||
const octokit = await this.getOctokit(hostname);
|
||||
const response = await octokit.rest.repos.get({
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
|
||||
return response.data.default_branch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,3 +49,19 @@ export enum BuildStatus {
|
||||
'pending',
|
||||
'running',
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type Branch = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type Branches = {
|
||||
default_branch: string;
|
||||
branches: Branch[];
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export interface RouterProps {
|
||||
view?: 'cards' | 'table';
|
||||
}
|
||||
|
||||
@@ -23,15 +23,18 @@ import {
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { buildRouteRef } from '../routes';
|
||||
import { WorkflowRunDetails } from './WorkflowRunDetails';
|
||||
import { WorkflowRunsCard } from './WorkflowRunsCard';
|
||||
import { WorkflowRunsTable } from './WorkflowRunsTable';
|
||||
import { GITHUB_ACTIONS_ANNOTATION } from './getProjectNameFromEntity';
|
||||
import { RouterProps } from '../api/types';
|
||||
|
||||
/** @public */
|
||||
export const isGithubActionsAvailable = (entity: Entity) =>
|
||||
Boolean(entity.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION]);
|
||||
|
||||
/** @public */
|
||||
export const Router = () => {
|
||||
export const Router = (props: RouterProps) => {
|
||||
const { view = 'table' } = props;
|
||||
const { entity } = useEntity();
|
||||
|
||||
if (!isGithubActionsAvailable(entity)) {
|
||||
@@ -39,14 +42,21 @@ export const Router = () => {
|
||||
<MissingAnnotationEmptyState annotation={GITHUB_ACTIONS_ANNOTATION} />
|
||||
);
|
||||
}
|
||||
|
||||
const workflowRunsComponent =
|
||||
view === 'cards' ? (
|
||||
<WorkflowRunsCard entity={entity} />
|
||||
) : (
|
||||
<WorkflowRunsTable entity={entity} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<WorkflowRunsTable entity={entity} />} />
|
||||
<Route path="/" element={workflowRunsComponent} />
|
||||
<Route
|
||||
path={`${buildRouteRef.path}`}
|
||||
element={<WorkflowRunDetails entity={entity} />}
|
||||
/>
|
||||
)
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,412 @@
|
||||
/*
|
||||
* 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 React, { ChangeEvent, useEffect, useState } from 'react';
|
||||
import {
|
||||
Typography,
|
||||
Box,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Button,
|
||||
Chip,
|
||||
ButtonGroup,
|
||||
Grid,
|
||||
makeStyles,
|
||||
createStyles,
|
||||
Theme,
|
||||
TablePagination,
|
||||
Select,
|
||||
MenuItem,
|
||||
TextField,
|
||||
CircularProgress,
|
||||
} from '@material-ui/core';
|
||||
import {
|
||||
EmptyState,
|
||||
Link,
|
||||
MarkdownContent,
|
||||
InfoCard,
|
||||
} from '@backstage/core-components';
|
||||
import GitHubIcon from '@material-ui/icons/GitHub';
|
||||
import RetryIcon from '@material-ui/icons/Replay';
|
||||
import SyncIcon from '@material-ui/icons/Sync';
|
||||
import ExternalLinkIcon from '@material-ui/icons/Launch';
|
||||
import { useRouteRef } from '@backstage/core-plugin-api';
|
||||
import { useWorkflowRuns, WorkflowRun } from '../useWorkflowRuns';
|
||||
import { WorkflowRunStatus } from '../WorkflowRunStatus';
|
||||
import { buildRouteRef } from '../../routes';
|
||||
import { getProjectNameFromEntity } from '../getProjectNameFromEntity';
|
||||
import { getHostnameFromEntity } from '../getHostnameFromEntity';
|
||||
|
||||
import { Alert, Color } from '@material-ui/lab';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
card: {
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
boxShadow: theme.shadows[2],
|
||||
borderRadius: '4px',
|
||||
overflow: 'visible',
|
||||
position: 'relative',
|
||||
margin: theme.spacing(4, 1, 1),
|
||||
flex: '1',
|
||||
minWidth: '0px',
|
||||
},
|
||||
externalLinkIcon: {
|
||||
fontSize: 'inherit',
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
bottomline: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: '-5px',
|
||||
},
|
||||
pagination: {
|
||||
width: '100%',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
type WorkflowRunsCardViewProps = {
|
||||
runs?: WorkflowRun[];
|
||||
searchTerm: string;
|
||||
loading: boolean;
|
||||
onChangePageSize: (pageSize: number) => void;
|
||||
onChangePage: (page: number) => void;
|
||||
page: number;
|
||||
total: number;
|
||||
pageSize: number;
|
||||
projectName: string;
|
||||
};
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
skipped: 'warning',
|
||||
canceled: 'info',
|
||||
timed_out: 'error',
|
||||
failure: 'error',
|
||||
success: 'success',
|
||||
};
|
||||
|
||||
const matchesSearchTerm = (run: WorkflowRun, searchTerm: string) => {
|
||||
const lowerCaseSearchTerm = searchTerm.toLocaleLowerCase();
|
||||
return (
|
||||
run.workflowName?.toLocaleLowerCase().includes(lowerCaseSearchTerm) ||
|
||||
run.source.branchName?.toLocaleLowerCase().includes(lowerCaseSearchTerm) ||
|
||||
run.status?.toLocaleLowerCase().includes(lowerCaseSearchTerm) ||
|
||||
run.id?.toLocaleLowerCase().includes(lowerCaseSearchTerm)
|
||||
);
|
||||
};
|
||||
|
||||
export const WorkflowRunsCardView = ({
|
||||
runs,
|
||||
searchTerm,
|
||||
loading,
|
||||
onChangePageSize,
|
||||
onChangePage,
|
||||
page,
|
||||
total,
|
||||
pageSize,
|
||||
}: WorkflowRunsCardViewProps) => {
|
||||
const classes = useStyles();
|
||||
const routeLink = useRouteRef(buildRouteRef);
|
||||
|
||||
const filteredRuns = runs?.filter(run => matchesSearchTerm(run, searchTerm));
|
||||
|
||||
return (
|
||||
<Grid container spacing={3}>
|
||||
{filteredRuns && runs?.length !== 0 ? (
|
||||
filteredRuns.map(run => (
|
||||
<Grid key={run.id} item container xs={12} sm={4} md={4} xl={4}>
|
||||
<Box className={classes.card}>
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
m={3}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Box pt={2} sx={{ width: '100%' }} textAlign="center">
|
||||
<Tooltip
|
||||
title={run.status ?? 'No Status'}
|
||||
placement="top-start"
|
||||
>
|
||||
<Alert
|
||||
variant="outlined"
|
||||
severity={
|
||||
statusColors[
|
||||
run.conclusion as keyof typeof statusColors
|
||||
] as Color
|
||||
}
|
||||
style={{ alignItems: 'center' }}
|
||||
>
|
||||
<Typography variant="h6">
|
||||
<Link to={routeLink({ id: run.id })}>
|
||||
<Typography color="primary" variant="h6">
|
||||
{run.workflowName}
|
||||
</Typography>
|
||||
</Link>
|
||||
</Typography>
|
||||
</Alert>
|
||||
</Tooltip>
|
||||
<Tooltip title={run.message ?? 'No run message'}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
component="span"
|
||||
style={{ fontSize: 'smaller' }}
|
||||
>
|
||||
<MarkdownContent
|
||||
content={`Commit ID : ${run.source.commit.hash!}`}
|
||||
/>
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
|
||||
{run.source.branchName && (
|
||||
<MarkdownContent
|
||||
content={`Branch : ${run.source.branchName}`}
|
||||
/>
|
||||
)}
|
||||
<Chip
|
||||
key={run.id}
|
||||
size="small"
|
||||
label={`Workflow ID : ${run.id}`}
|
||||
/>
|
||||
<Chip
|
||||
size="small"
|
||||
label={
|
||||
<Box display="flex" alignItems="center">
|
||||
<WorkflowRunStatus
|
||||
status={run.status}
|
||||
conclusion={run.conclusion}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<div className={classes.bottomline}>
|
||||
{run.githubUrl && (
|
||||
<Link to={run.githubUrl}>
|
||||
Workflow runs on GitHub{' '}
|
||||
<ExternalLinkIcon
|
||||
className={classes.externalLinkIcon}
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
<ButtonGroup>
|
||||
<Tooltip title="Rerun workflow">
|
||||
<IconButton
|
||||
onClick={run.onReRunClick}
|
||||
style={{ fontSize: '12px' }}
|
||||
>
|
||||
<RetryIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
))
|
||||
) : (
|
||||
<Box p={2}>
|
||||
{loading ? <CircularProgress /> : 'No matching runs found.'}
|
||||
</Box>
|
||||
)}
|
||||
<div className={classes.pagination}>
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={total}
|
||||
page={page}
|
||||
rowsPerPage={pageSize}
|
||||
onPageChange={(_, newPage) => onChangePage(newPage)}
|
||||
onRowsPerPageChange={event =>
|
||||
onChangePageSize(parseInt(event.target.value, 6))
|
||||
}
|
||||
labelRowsPerPage="Workflows per page"
|
||||
rowsPerPageOptions={[6, 12, 18, { label: 'All', value: -1 }]}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
type WorkflowRunsCardProps = {
|
||||
entity: Entity;
|
||||
};
|
||||
|
||||
const WorkflowRunsCardSearch = ({
|
||||
searchTerm,
|
||||
handleSearch,
|
||||
retry,
|
||||
}: {
|
||||
searchTerm: string;
|
||||
handleSearch: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
retry: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<Box flexGrow={1} />
|
||||
<TextField
|
||||
type="search"
|
||||
label="Search"
|
||||
value={searchTerm}
|
||||
onChange={handleSearch}
|
||||
data-testid="search-control"
|
||||
style={{ marginRight: '20px' }}
|
||||
/>
|
||||
<ButtonGroup>
|
||||
<Tooltip title="Reload workflow runs">
|
||||
<IconButton onClick={retry}>
|
||||
<SyncIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const WorkflowRunsCard = ({ entity }: WorkflowRunsCardProps) => {
|
||||
const projectName = getProjectNameFromEntity(entity);
|
||||
const hostname = getHostnameFromEntity(entity);
|
||||
const [owner, repo] = (projectName ?? '/').split('/');
|
||||
const [branch, setBranch] = useState<string | undefined>('default');
|
||||
const [runs, setRuns] = useState<WorkflowRun[] | undefined>([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const handleSearch = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchTerm(event.target.value);
|
||||
};
|
||||
|
||||
const [
|
||||
{ runs: runsData, branches, defaultBranch, ...cardProps },
|
||||
{ retry, setPage, setPageSize },
|
||||
] = useWorkflowRuns({
|
||||
hostname,
|
||||
owner,
|
||||
repo,
|
||||
branch: branch === 'all' ? undefined : branch,
|
||||
});
|
||||
|
||||
const githubHost = hostname || 'github.com';
|
||||
const hasNoRuns = !cardProps.loading && !runsData;
|
||||
|
||||
const handleMenuChange = (
|
||||
event: ChangeEvent<{ name?: string; value: unknown }>,
|
||||
) => {
|
||||
const selectedValue = event.target.value as string;
|
||||
setBranch(selectedValue);
|
||||
setPage(0);
|
||||
retry();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setRuns(runsData);
|
||||
}, [runsData, branch]);
|
||||
|
||||
useEffect(() => {
|
||||
setBranch(defaultBranch);
|
||||
}, [defaultBranch]);
|
||||
|
||||
return (
|
||||
<Grid item>
|
||||
{hasNoRuns ? (
|
||||
<EmptyState
|
||||
missing="data"
|
||||
title="No Workflow Data"
|
||||
description="This component has GitHub Actions enabled, but no data was found. Have you created any Workflows? Click the button below to create a new Workflow."
|
||||
action={
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
href={`https://${githubHost}/${projectName}/actions/new`}
|
||||
>
|
||||
Create new Workflow
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<InfoCard
|
||||
title={
|
||||
<Box display="flex" alignItems="center">
|
||||
<GitHubIcon />
|
||||
<Box mr={1} />
|
||||
<Typography variant="h6">{projectName}</Typography>
|
||||
|
||||
<Select
|
||||
value={branch}
|
||||
key={branch}
|
||||
label="Branch"
|
||||
onChange={handleMenuChange}
|
||||
data-testid="menu-control"
|
||||
style={{
|
||||
marginLeft: '30px',
|
||||
marginRight: '20px',
|
||||
width: '230px',
|
||||
}}
|
||||
>
|
||||
{branches.map(branchItem => (
|
||||
<MenuItem key={branchItem.name} value={branchItem.name}>
|
||||
{branchItem.name === defaultBranch ? (
|
||||
<Typography variant="body2" component="span">
|
||||
{branchItem.name}{' '}
|
||||
<Typography
|
||||
variant="body2"
|
||||
component="span"
|
||||
style={{ color: 'lightgray', fontSize: 'x-small' }}
|
||||
>
|
||||
(default)
|
||||
</Typography>
|
||||
</Typography>
|
||||
) : (
|
||||
branchItem.name
|
||||
)}
|
||||
</MenuItem>
|
||||
))}
|
||||
|
||||
<MenuItem
|
||||
value="all"
|
||||
key="all"
|
||||
style={{ color: 'lightGrey', fontSize: 'small' }}
|
||||
>
|
||||
select all branches
|
||||
</MenuItem>
|
||||
</Select>
|
||||
|
||||
<WorkflowRunsCardSearch
|
||||
searchTerm={searchTerm}
|
||||
handleSearch={handleSearch}
|
||||
retry={retry}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<WorkflowRunsCardView
|
||||
runs={runs}
|
||||
loading={cardProps.loading}
|
||||
onChangePageSize={setPageSize}
|
||||
onChangePage={setPage}
|
||||
page={cardProps.page}
|
||||
total={cardProps.total}
|
||||
pageSize={cardProps.pageSize}
|
||||
searchTerm={searchTerm}
|
||||
projectName={projectName}
|
||||
/>
|
||||
</InfoCard>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowRunsCard;
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
export { WorkflowRunsCard, WorkflowRunsCardView } from './WorkflowRunsCard';
|
||||
@@ -17,6 +17,7 @@ import { useState } from 'react';
|
||||
import useAsyncRetry from 'react-use/lib/useAsyncRetry';
|
||||
import { githubActionsApiRef } from '../api/GithubActionsApi';
|
||||
import { useApi, errorApiRef } from '@backstage/core-plugin-api';
|
||||
import { Branch } from '../api';
|
||||
|
||||
export type WorkflowRun = {
|
||||
workflowName?: string;
|
||||
@@ -41,12 +42,12 @@ export function useWorkflowRuns({
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
initialPageSize = 5,
|
||||
initialPageSize = 6,
|
||||
}: {
|
||||
hostname?: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
branch?: string;
|
||||
branch?: string | undefined;
|
||||
initialPageSize?: number;
|
||||
}) {
|
||||
const api = useApi(githubActionsApiRef);
|
||||
@@ -56,6 +57,8 @@ export function useWorkflowRuns({
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(initialPageSize);
|
||||
const [branches, setBranches] = useState<Branch[]>([]);
|
||||
const [defaultBranch, setDefaultBranch] = useState<string>('');
|
||||
|
||||
const {
|
||||
loading,
|
||||
@@ -63,6 +66,43 @@ export function useWorkflowRuns({
|
||||
retry,
|
||||
error,
|
||||
} = useAsyncRetry<WorkflowRun[]>(async () => {
|
||||
const fetchedDefaultBranch = await api.getDefaultBranch({
|
||||
hostname,
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
|
||||
setDefaultBranch(fetchedDefaultBranch);
|
||||
let selectedBranch = branch;
|
||||
if (branch === 'default') {
|
||||
selectedBranch = fetchedDefaultBranch;
|
||||
}
|
||||
|
||||
const fetchBranches = async () => {
|
||||
let next = true;
|
||||
let iteratePage = 0;
|
||||
const branchSet: Branch[] = [];
|
||||
|
||||
while (next) {
|
||||
const branchesData = await api.listBranches({
|
||||
hostname,
|
||||
owner,
|
||||
repo,
|
||||
page: iteratePage,
|
||||
});
|
||||
if (branchesData.length === 0) {
|
||||
next = false;
|
||||
}
|
||||
iteratePage++;
|
||||
branchSet.push(...branchesData);
|
||||
}
|
||||
|
||||
return branchSet;
|
||||
};
|
||||
|
||||
const branchSet = await fetchBranches();
|
||||
setBranches(branchSet);
|
||||
|
||||
// GitHub API pagination count starts from 1
|
||||
const workflowRunsData = await api.listWorkflowRuns({
|
||||
hostname,
|
||||
@@ -70,7 +110,7 @@ export function useWorkflowRuns({
|
||||
repo,
|
||||
pageSize,
|
||||
page: page + 1,
|
||||
branch,
|
||||
branch: selectedBranch,
|
||||
});
|
||||
setTotal(workflowRunsData.total_count);
|
||||
// Transformation here
|
||||
@@ -88,7 +128,7 @@ export function useWorkflowRuns({
|
||||
});
|
||||
} catch (e) {
|
||||
errorApi.post(
|
||||
new Error(`Failed to rerun the workflow: ${e.message}`),
|
||||
new Error(`Failed to rerun the workflow: ${(e as Error).message}`),
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -115,6 +155,8 @@ export function useWorkflowRuns({
|
||||
pageSize,
|
||||
loading,
|
||||
runs,
|
||||
branches,
|
||||
defaultBranch,
|
||||
projectName: `${owner}/${repo}`,
|
||||
total,
|
||||
error,
|
||||
|
||||
Reference in New Issue
Block a user