diff --git a/frontend/__tests__/components/utils/datetime.ts b/frontend/__tests__/components/utils/datetime.ts index 93954748635..f1565ad851e 100644 --- a/frontend/__tests__/components/utils/datetime.ts +++ b/frontend/__tests__/components/utils/datetime.ts @@ -1,4 +1,4 @@ -import { fromNow, isValid, formatDuration } from '../../../public/components/utils/datetime'; +import { fromNow, isValid, formatDuration, formatPrometheusDuration, parsePrometheusDuration } from '../../../public/components/utils/datetime'; describe('fromNow', () => { it('prints past dates correctly', () => { @@ -92,3 +92,84 @@ describe('formatDuration', () => { }); }); +// Converts time durations to milliseconds +const ms = (s = 0, m = 0, h = 0, d = 0, w = 0) => ((((w * 7 + d) * 24 + h) * 60 + m) * 60 + s) * 1000; + +describe('formatPrometheusDuration', () => { + it('formats durations correctly', () => { + expect(formatPrometheusDuration(ms(1))).toEqual('1s'); + expect(formatPrometheusDuration(ms(2, 1))).toEqual('1m 2s'); + expect(formatPrometheusDuration(ms(3, 2, 1))).toEqual('1h 2m 3s'); + expect(formatPrometheusDuration(ms(4, 3, 2, 1))).toEqual('1d 2h 3m 4s'); + expect(formatPrometheusDuration(ms(5, 4, 3, 2, 1))).toEqual('1w 2d 3h 4m 5s'); + }); + + it('handles invalid values', () => { + [null, undefined, 0, -1, -9999].forEach(v => expect(formatPrometheusDuration(v)).toEqual('')); + }); +}); + +describe('parsePrometheusDuration', () => { + it('parses durations correctly', () => { + expect(parsePrometheusDuration('1s')).toEqual(ms(1)); + expect(parsePrometheusDuration('100s')).toEqual(ms(100)); + expect(parsePrometheusDuration('1m')).toEqual(ms(0, 1)); + expect(parsePrometheusDuration('90m')).toEqual(ms(0, 90)); + expect(parsePrometheusDuration('1h')).toEqual(ms(0, 0, 1)); + expect(parsePrometheusDuration('2h 0m 0s')).toEqual(ms(0, 0, 2)); + expect(parsePrometheusDuration('13h 10m 23s')).toEqual(ms(23, 10, 13)); + expect(parsePrometheusDuration('25h 61m 61s')).toEqual(ms(61, 61, 25)); + expect(parsePrometheusDuration('123h')).toEqual(ms(0, 0, 123)); + expect(parsePrometheusDuration('1d')).toEqual(ms(0, 0, 0, 1)); + expect(parsePrometheusDuration('2d 6h')).toEqual(ms(0, 0, 6, 2)); + expect(parsePrometheusDuration('8d 12h')).toEqual(ms(0, 0, 12, 8)); + expect(parsePrometheusDuration('10d 12h 30m 1s')).toEqual(ms(1, 30, 12, 10)); + expect(parsePrometheusDuration('1w')).toEqual(ms(0, 0, 0, 0, 1)); + expect(parsePrometheusDuration('5w 10d 12h 30m 1s')).toEqual(ms(1, 30, 12, 10, 5)); + expect(parsePrometheusDuration('999w 999h 999s')).toEqual(ms(999, 0, 999, 0, 999)); + }); + + it('handles 0 values', () => { + expect(parsePrometheusDuration('0s')).toEqual(0); + expect(parsePrometheusDuration('0w 0d 0h 0m 0s')).toEqual(0); + expect(parsePrometheusDuration('00h 000000m 0s')).toEqual(0); + }); + + it('handles invalid duration formats', () => { + [ + '', + null, + undefined, + '0', + '12', + 'z', + 'h', + 'abc', + '全角', + '0.5h', + '1hh', + '1h1m', + '1h h', + '1h 0', + '1h 0z', + '-1h', + ].forEach(v => expect(parsePrometheusDuration(v)).toEqual(0)); + }); + + it('mirrors formatPrometheusDuration()', () => { + [ + '1s', + '1m', + '1h', + '1m 40s', + '13h 10m 23s', + '2h 10s', + '1d', + '2d 6h', + '1w', + '5w 6d 12h 30m 1s', + '999w', + '', + ].forEach(v => expect(formatPrometheusDuration(parsePrometheusDuration(v))).toEqual(v)); + }); +}); diff --git a/frontend/package.json b/frontend/package.json index d64c8e83170..7c882c8e30a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -71,7 +71,7 @@ "patternfly": "^3.59.0", "patternfly-react": "^2.29.1", "patternfly-react-extensions": "2.14.1", - "plotly.js": "1.28.x", + "plotly.js": "1.44.4", "prop-types": "15.6.x", "react": "16.6.3", "react-copy-to-clipboard": "5.x", diff --git a/frontend/public/components/graphs/_graphs.scss b/frontend/public/components/graphs/_graphs.scss index 3bf1a3b9332..689efd69ab7 100644 --- a/frontend/public/components/graphs/_graphs.scss +++ b/frontend/public/components/graphs/_graphs.scss @@ -14,3 +14,43 @@ white-space: nowrap; line-height: 1.4; // so descenders don't clip } + +.query-browser__wrapper { + border: 1px solid $color-grey-background-border; + margin: 0 0 20px 0; + overflow: visible; + width: 100%; +} + +.query-browser__header { + display: inline-flex; + justify-content: space-between; + padding: 15px 10px 10px 10px; + width: 100%; +} + +.query-browser__controls { + display: inline-flex; +} + +.query-browser__span-text { + border-bottom-right-radius: 0; + border-right: none; + border-top-right-radius: 0; + width: 100px; +} + +.query-browser__span-text--error { + background-color: #fdd; +} + +.query-browser__span-dropdown { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + margin-right: 20px; + width: 30px; +} + +.query-browser__span-reset { + margin-right: 20px; +} diff --git a/frontend/public/components/graphs/base.jsx b/frontend/public/components/graphs/base.jsx index 6c401051c7e..84b7506e85a 100644 --- a/frontend/public/components/graphs/base.jsx +++ b/frontend/public/components/graphs/base.jsx @@ -32,7 +32,7 @@ export class BaseGraph extends SafetyFirst { } } - fetch() { + fetch(enablePolling = true) { const timeSpan = this.end - this.start || this.timeSpan; const end = this.end || Date.now(); const start = this.start || (end - timeSpan); @@ -45,8 +45,8 @@ export class BaseGraph extends SafetyFirst { } const basePath = this.props.basePath || (this.props.namespace ? prometheusTenancyBasePath : prometheusBasePath); - const pollInterval = timeSpan / 120 || 15000; - const stepSize = pollInterval / 1000; + const pollInterval = timeSpan ? Math.max(timeSpan / 120, 5000) : 15000; + const stepSize = (timeSpan && this.props.numSamples ? timeSpan / this.props.numSamples : pollInterval) / 1000; const promises = queries.map(q => { const nsParam = this.props.namespace ? `&namespace=${encodeURIComponent(this.props.namespace)}` : ''; const url = this.timeSpan @@ -64,11 +64,15 @@ export class BaseGraph extends SafetyFirst { } }) .catch(error => this.updateGraph(null, error)) - .then(() => this.interval = setTimeout(() => { - if (this.isMounted_) { - this.fetch(); + .then(() => { + if (enablePolling) { + this.interval = setTimeout(() => { + if (this.isMounted_) { + this.fetch(); + } + }, pollInterval); } - }, pollInterval)); + }); } componentWillMount() { @@ -132,7 +136,7 @@ export class BaseGraph extends SafetyFirst { const { title, className } = this.props; const url = this.props.query ? this.prometheusURL() : null; const graph =