summaryrefslogtreecommitdiff
path: root/web
diff options
context:
space:
mode:
authorAntoine Musso <hashar@free.fr>2020-01-30 23:11:16 +0100
committerAntoine Musso <hashar@free.fr>2020-02-04 13:23:38 +0100
commit448596cfe0c59fee08f687efcdc01784f4cb36f9 (patch)
tree3fd0cd54e4c71c3884582e5d19ec4667c49a95d1 /web
parent3dd94345ac6d31faf4bcf64a5ae9bb1603c24479 (diff)
downloadzuul-448596cfe0c59fee08f687efcdc01784f4cb36f9.tar.gz
web: humanize time durations
At a lot of place, the duration for an item shows the raw value. It lacks an indication of the unit (ms, s or minutes??) and is not very human friendly. `480` is better understood as `8 minutes`. The `moment-duration-format` package enhance moment duration objects to ease formatting. It has better options than moment.duration.humanize(), notably it does not truncate value and trim extra tokens when they are not needed. The package does not have any extra dependencies. https://www.npmjs.com/package/moment-duration-format Format duration on the pages build, buildset and on the build summary. The Change panel had custom logic to workaround moment.duration.humanize over rounding (so that 100 minutes became '1 hour'). The new package does not have such behavior and offers tuning for all the features we had: * `largest: 2`, to only keep the two most significants token. 100 minutes and 30 seconds would thus render as '1 hour 40 minutes', stripping the extra '30 seconds'. * `minValue: 1`, based on the least significant token which is minute, it means any value less than one minute is rendered as: < 1 minute * `usePLural: false`, to disable pluralization since that was not handled previously. That reverts https://review.opendev.org/#/c/704191/ Make class="time" to not wrap, they would wrap on the Status page since the box containing is not large enough. In the Console, show the duration for each tasks, rounding to 1 second resolution. Would ease finding potential slowdown in a play execution. The tasks do not have a duration property, use moment to compute it based on start and end times. Since hostname length might vary, the duration spans might be misaligned. Use a flex to have them vertically aligned, credits to mcoker@github: https://github.com/patternfly/patternfly-react/issues/1393#issuecomment-515202640 Thanks to Tristan Cacqueray for the React guidances! Change-Id: I955583713778911a8e50f08dc6d077594da4ae44
Diffstat (limited to 'web')
-rw-r--r--web/package.json2
-rw-r--r--web/src/containers/build/Buildset.jsx4
-rw-r--r--web/src/containers/build/Console.jsx16
-rw-r--r--web/src/containers/build/Summary.jsx7
-rw-r--r--web/src/containers/status/ChangePanel.jsx77
-rw-r--r--web/src/index.css9
-rw-r--r--web/src/pages/Builds.jsx9
-rw-r--r--web/yarn.lock5
8 files changed, 67 insertions, 62 deletions
diff --git a/web/package.json b/web/package.json
index 4b11ac432..b811c7a24 100644
--- a/web/package.json
+++ b/web/package.json
@@ -12,6 +12,7 @@
"js-yaml": "^3.13.0",
"lodash": "^4.17.10",
"moment": "^2.22.2",
+ "moment-duration-format": "2.3.2",
"patternfly-react": "^2.13.1",
"prop-types": "^15.6.2",
"react": "^16.4.2",
@@ -36,6 +37,7 @@
"yarn": "^1.16.0"
},
"scripts": {
+ "start:opendev": "REACT_APP_ZUUL_API='https://zuul.opendev.org/api/' react-scripts start",
"start:openstack": "REACT_APP_ZUUL_API='https://zuul.openstack.org/api/' react-scripts start",
"start:multi": "REACT_APP_ZUUL_API='https://softwarefactory-project.io/zuul/api/' react-scripts start",
"start": "react-scripts start",
diff --git a/web/src/containers/build/Buildset.jsx b/web/src/containers/build/Buildset.jsx
index 803022a21..89f246c83 100644
--- a/web/src/containers/build/Buildset.jsx
+++ b/web/src/containers/build/Buildset.jsx
@@ -18,6 +18,7 @@ import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import { Panel } from 'react-bootstrap'
import * as moment from 'moment'
+import 'moment-duration-format'
class Buildset extends React.Component {
@@ -61,7 +62,8 @@ class Buildset extends React.Component {
if (column === 'job') {
row.push(build.job_name)
} else if (column === 'duration') {
- row.push(moment.duration(build.duration, 'seconds').humanize())
+ row.push(moment.duration(build.duration, 'seconds')
+ .format('h [hr] m [min] s [sec]'))
} else if (column === 'voting') {
row.push(build.voting ? 'true' : 'false')
} else if (column === 'result') {
diff --git a/web/src/containers/build/Console.jsx b/web/src/containers/build/Console.jsx
index 01b308bfe..d8810170e 100644
--- a/web/src/containers/build/Console.jsx
+++ b/web/src/containers/build/Console.jsx
@@ -12,6 +12,8 @@
// License for the specific language governing permissions and limitations
// under the License.
+import * as moment from 'moment'
+import 'moment-duration-format'
import * as React from 'react'
import PropTypes from 'prop-types'
import ReactJson from 'react-json-view'
@@ -206,6 +208,20 @@ class HostTask extends React.Component {
</ListView.InfoItem>
)
+ let duration = moment.duration(
+ moment(task.task.duration.end).diff(task.task.duration.start)
+ ).format({
+ template: 'h [hr] m [min] s [sec]',
+ largest: 2,
+ minValue: 1,
+ })
+
+ ai.push(
+ <ListView.InfoItem key="task-duration">
+ <span className="task-duration">{duration}</span>
+ </ListView.InfoItem>
+ )
+
const expand = errorIds.has(task.task.id)
let name = task.task.name
diff --git a/web/src/containers/build/Summary.jsx b/web/src/containers/build/Summary.jsx
index 90eea6af7..ef47b9ef9 100644
--- a/web/src/containers/build/Summary.jsx
+++ b/web/src/containers/build/Summary.jsx
@@ -20,6 +20,9 @@ import { Link } from 'react-router-dom'
import ArtifactList from './Artifact'
import BuildOutput from './BuildOutput'
+import * as moment from 'moment'
+import 'moment-duration-format'
+
class Summary extends React.Component {
static propTypes = {
@@ -66,6 +69,10 @@ class Summary extends React.Component {
value = 'false'
}
}
+ if (column === 'duration') {
+ value = moment.duration(value, 'seconds')
+ .format('h [hr] m [min] s [sec]')
+ }
if (value && (column === 'log_url' || column === 'ref_url')) {
value = <a href={value}>{value}</a>
}
diff --git a/web/src/containers/status/ChangePanel.jsx b/web/src/containers/status/ChangePanel.jsx
index def966e99..6600e6e8e 100644
--- a/web/src/containers/status/ChangePanel.jsx
+++ b/web/src/containers/status/ChangePanel.jsx
@@ -16,11 +16,8 @@ import * as React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
-
-const SECOND = 1000
-const MINUTE = SECOND * 60
-const HOUR = MINUTE * 60
-const DAY = HOUR * 24
+import * as moment from 'moment'
+import 'moment-duration-format'
class ChangePanel extends React.Component {
@@ -52,37 +49,13 @@ class ChangePanel extends React.Component {
this.setState({ expanded: !expanded })
}
- time (ms, words) {
- if (typeof (words) === 'undefined') {
- words = false
- }
- let seconds = (+ms) / 1000
- let minutes = Math.floor(seconds / 60)
- let hours = Math.floor(minutes / 60)
- seconds = Math.floor(seconds % 60)
- minutes = Math.floor(minutes % 60)
- let r = ''
- if (words) {
- if (hours) {
- r += hours
- r += ' hr '
- }
- r += minutes + ' min'
- } else {
- if (hours < 10) {
- r += '0'
- }
- r += hours + ':'
- if (minutes < 10) {
- r += '0'
- }
- r += minutes + ':'
- if (seconds < 10) {
- r += '0'
- }
- r += seconds
- }
- return r
+ time (ms) {
+ return moment.duration(ms).format({
+ template: 'h [hr] m [min]',
+ largest: 2,
+ minValue: 1,
+ usePlural: false,
+ })
}
enqueueTime (ms) {
@@ -91,7 +64,7 @@ class ChangePanel extends React.Component {
let now = Date.now()
let delta = now - ms
let status = 'text-success'
- let text = this.time(delta, true)
+ let text = this.time(delta)
if (delta > (4 * hours)) {
status = 'text-danger'
} else if (delta > (2 * hours)) {
@@ -175,7 +148,7 @@ class ChangePanel extends React.Component {
if (change.remaining_time === null) {
remainingTime = 'unknown'
} else {
- remainingTime = this.time(change.remaining_time, true)
+ remainingTime = this.time(change.remaining_time)
}
return (
<React.Fragment>
@@ -204,29 +177,11 @@ class ChangePanel extends React.Component {
className = 'progress-bar-striped progress-bar-animated'
}
if (remaining !== null) {
- title = 'Estimated time remaining: '
- if (remaining < MINUTE) {
- title = title + 'less than a minute'
- } else {
- let days = 0
- let hours = 0
- let minutes = 0
-
- days = Math.trunc(remaining/DAY)
- remaining = Math.trunc(remaining%DAY)
- hours = Math.trunc(remaining/HOUR)
- remaining = Math.trunc(remaining%HOUR)
- minutes = Math.trunc(remaining/MINUTE)
- if (days > 0) {
- title = title + days + ' days '
- }
- if (hours > 0) {
- title = title + hours + ' hours '
- }
- if (minutes > 0) {
- title = title + minutes + ' minutes '
- }
- }
+ title = 'Estimated time remaining: ' + moment.duration(remaining).format({
+ template: 'd [days] h [hours] m [minutes] s [seconds]',
+ largest: 2,
+ minValue: 30,
+ })
}
return (
diff --git a/web/src/index.css b/web/src/index.css
index b638a4600..476a55905 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -9,6 +9,10 @@ a.refresh {
text-decoration: none;
}
+.time {
+ white-space: nowrap;
+}
+
/* Notification bell color */
.fa-bell {
color: orange;
@@ -180,6 +184,11 @@ pre.version {
padding-top: 1px;
padding-bottom: 1px;
}
+.zuul-console .list-view-pf-additional-info-item {
+ flex-grow: 1;
+ flex-shrink: 1;
+ flex-basis: 0;
+}
.zuul-console .list-view-pf-expand
{
padding: 0;
diff --git a/web/src/pages/Builds.jsx b/web/src/pages/Builds.jsx
index 54c184d41..694608373 100644
--- a/web/src/pages/Builds.jsx
+++ b/web/src/pages/Builds.jsx
@@ -17,6 +17,8 @@ import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import { Table } from 'patternfly-react'
+import * as moment from 'moment'
+import 'moment-duration-format'
import { fetchBuilds } from '../api'
import TableFilters from '../containers/TableFilters'
@@ -77,6 +79,11 @@ class BuildsPage extends TableFilters {
<a href={rowdata.rowData.ref_url}>{value ? rowdata.rowData.change+','+rowdata.rowData.patchset : rowdata.rowData.newrev ? rowdata.rowData.newrev.substr(0, 7) : rowdata.rowData.branch}</a>
</Table.Cell>
)
+ const durationFormat = (value) => (
+ <Table.Cell>
+ {moment.duration(value, 'seconds').format('h [hr] m [min] s [sec]')}
+ </Table.Cell>
+ )
this.columns = []
this.filterTypes = []
const myColumns = [
@@ -101,6 +108,8 @@ class BuildsPage extends TableFilters {
formatter = linkChangeFormat
} else if (column === 'result') {
formatter = linkBuildFormat
+ } else if (column === 'duration') {
+ formatter = durationFormat
}
const label = column.charAt(0).toUpperCase() + column.slice(1)
this.columns.push({
diff --git a/web/yarn.lock b/web/yarn.lock
index 613645648..f572e43fc 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -6112,6 +6112,11 @@ mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@
dependencies:
minimist "0.0.8"
+moment-duration-format@2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/moment-duration-format/-/moment-duration-format-2.3.2.tgz#5fa2b19b941b8d277122ff3f87a12895ec0d6212"
+ integrity sha512-cBMXjSW+fjOb4tyaVHuaVE/A5TqkukDWiOfxxAjY+PEqmmBQlLwn+8OzwPiG3brouXKY5Un4pBjAeB6UToXHaQ==
+
moment-timezone@^0.4.0, moment-timezone@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.4.1.tgz#81f598c3ad5e22cdad796b67ecd8d88d0f5baa06"