zuul/web/src/containers/build/BuildsetTable.jsx
James E. Blair 2e4b605722 Add option to show overall duration in buildset table
Change the "duration" colubm header in the buildset table to a
dropdown which allows the user to display either the buildset
duration (end-start) or overall duration (end-trigger event).

Also update the label on the buildset page to say "Buildset duration"
to match.

Change-Id: Ib208bcf6b9673d2bba52de90ea6682ec650986a9
2022-03-02 12:44:41 -08:00

297 lines
8.4 KiB
JavaScript

// Copyright 2020 BMW Group
//
// 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 from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import {
Button,
Dropdown,
DropdownItem,
DropdownPosition,
DropdownToggle,
EmptyState,
EmptyStateBody,
EmptyStateIcon,
EmptyStateSecondaryActions,
Spinner,
Title,
} from '@patternfly/react-core'
import {
BuildIcon,
CodeBranchIcon,
CodeIcon,
CogIcon,
CubeIcon,
OutlinedCalendarAltIcon,
OutlinedClockIcon,
PollIcon,
StreamIcon,
} from '@patternfly/react-icons'
import {
Table,
TableHeader,
TableBody,
TableVariant,
truncate,
breakWord,
cellWidth,
} from '@patternfly/react-table'
import 'moment-duration-format'
import * as moment from 'moment'
import { BuildResult, BuildResultWithIcon } from './Misc'
import { buildExternalTableLink, IconProperty } from '../../Misc'
function BuildsetTable({
buildsets,
fetching,
onClearFilters,
tenant,
timezone,
history,
}) {
const [isDurationOpen, setIsDurationOpen] = React.useState(false)
const [currentDuration, setCurrentDuration] = React.useState(
'Buildset Duration'
)
const handleDurationSelect = (event) => {
setIsDurationOpen(!isDurationOpen)
setCurrentDuration(event.target.innerText)
}
const columns = [
{
title: <IconProperty icon={<CubeIcon />} value="Project" />,
dataLabel: 'Project',
cellTransforms: [breakWord],
},
{
title: <IconProperty icon={<CodeBranchIcon />} value="Branch" />,
dataLabel: 'Branch',
cellTransforms: [breakWord],
},
{
title: <IconProperty icon={<StreamIcon />} value="Pipeline" />,
dataLabel: 'Pipeline',
cellTransforms: [breakWord],
},
{
title: <IconProperty icon={<CodeIcon />} value="Change" />,
dataLabel: 'Change',
transforms: [cellWidth(10)],
cellTransforms: [truncate],
},
{
title: <Dropdown
isText
isPlain
position={DropdownPosition.left}
onSelect={handleDurationSelect}
toggle={
<DropdownToggle
onToggle={next => setIsDurationOpen(next)}
// Use a gear instead of a caret which looks likea sort indicator.
toggleIndicator={CogIcon}
style={{
padding: 0,
fontWeight: 'var(--pf-c-table--cell-FontWeight)',
fontSize: 'var(--pf-c-table--cell-FontSize)',
color: 'var(--pf-c-table--cell-Color)'
}}
id="toggle-id-duration"
>
<OutlinedClockIcon /> {currentDuration}
</DropdownToggle>
}
isOpen={isDurationOpen}
dropdownItems={[
<DropdownItem key="buildset">Buildset Duration</DropdownItem>,
<DropdownItem key="overall">Overall Duration</DropdownItem>,
]}
style={{ width: '100%', padding: 0 }}
/>,
dataLabel: 'Duration',
props: {style: {overflowY: 'visible'}},
},
{
title: (
<IconProperty icon={<OutlinedCalendarAltIcon />} value="Start time" />
),
dataLabel: 'Start time',
},
{
title: <IconProperty icon={<PollIcon />} value="Result" />,
dataLabel: 'Result',
},
]
function createBuildsetRow(buildset) {
const changeOrRefLink = buildExternalTableLink(buildset)
let duration
if (currentDuration === 'Buildset Duration') {
duration = moment.utc(buildset.last_build_end_time) -
moment.utc(buildset.first_build_start_time)
} else {
duration = moment.utc(buildset.last_build_end_time) -
moment.utc(buildset.event_timestamp)
}
return {
// Pass the buildset's uuid as row id, so we can use it later on in the
// action handler to build the link to the build result page for each row.
id: buildset.uuid,
cells: [
{
// To allow passing anything else than simple string values to a table
// cell, we must use the title attribute.
title: (
<BuildResultWithIcon
result={buildset.result}
link={`${tenant.linkPrefix}/buildset/${buildset.uuid}`}>
{buildset.project}
</BuildResultWithIcon>
),
},
{
title: buildset.branch ? buildset.branch : buildset.ref,
},
{
title: buildset.pipeline,
},
{
title: changeOrRefLink && changeOrRefLink,
},
{
title: moment
.duration(duration, 'ms')
.format('h [hr] m [min] s [sec]'),
},
{
title: moment
.utc(buildset.first_build_start_time)
.tz(timezone)
.format('YYYY-MM-DD HH:mm:ss'),
},
{
title: (
<BuildResult
result={buildset.result}
link={`${tenant.linkPrefix}/buildset/${buildset.uuid}`}
>
</BuildResult>
),
},
],
}
}
function createFetchingRow() {
const rows = [
{
heightAuto: true,
cells: [
{
props: { colSpan: 8 },
title: (
<center>
<Spinner size="xl" />
</center>
),
},
],
},
]
return rows
}
let rows = []
// For the fetching row we don't need any actions, so we keep them empty by
// default.
let actions = []
if (fetching) {
rows = createFetchingRow()
// The dataLabel property is used to show the column header in a list-like
// format for smaller viewports. When we are fetching, we don't want the
// fetching row to be prepended by a "Project" column header. The other
// column headers are not relevant here since we only have a single cell in
// the fetching row.
columns[0].dataLabel = ''
} else {
rows = buildsets.map((buildset) => createBuildsetRow(buildset))
// This list of actions will be applied to each row in the table. For
// row-specific actions we must evaluate the individual row data provided to
// the onClick handler.
actions = [
{
title: 'Show buildset result',
onClick: (event, rowId, rowData) =>
// The row's id contains the buildset's uuid, so we can use it to
// build the correct link.
history.push(`${tenant.linkPrefix}/buildset/${rowData.id}`),
},
]
}
return (
<>
<Table
aria-label="Builds Table"
variant={TableVariant.compact}
cells={columns}
rows={rows}
actions={actions}
className="zuul-table"
>
<TableHeader />
<TableBody />
</Table>
{/* Show an empty state in case we don't have any buildsets but are also
not fetching */}
{!fetching && buildsets.length === 0 && (
<EmptyState>
<EmptyStateIcon icon={BuildIcon} />
<Title headingLevel="h1">No buildsets found</Title>
<EmptyStateBody>
No buildsets match this filter criteria. Remove some filters or
clear all to show results.
</EmptyStateBody>
<EmptyStateSecondaryActions>
<Button variant="link" onClick={onClearFilters}>
Clear all filters
</Button>
</EmptyStateSecondaryActions>
</EmptyState>
)}
</>
)
}
BuildsetTable.propTypes = {
buildsets: PropTypes.array.isRequired,
fetching: PropTypes.bool.isRequired,
onClearFilters: PropTypes.func.isRequired,
tenant: PropTypes.object.isRequired,
timezone: PropTypes.string.isRequired,
history: PropTypes.object.isRequired,
}
export default connect((state) => ({
tenant: state.tenant,
timezone: state.timezone,
}))(BuildsetTable)