Reporting: Fix missing time range in the PDF page header (#47916)

* layout selector refactoring

* remove some any types

* Expose the time duration for sharing apps

* switch Reporting to use timeRange.duration

* fix mock

* up the logs for platform bug

* add jest snapshot test

* add functional ui test

* Revert "up the logs for platform bug"

This reverts commit b128c35e15.

* fix snapshot test
This commit is contained in:
Tim Sullivan 2019-10-16 21:39:55 -07:00 committed by GitHub
parent 2335902ffa
commit 1939b8c55c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 109 additions and 51 deletions

View file

@ -47,9 +47,14 @@ startMock.uiSettings.get.mockImplementation((key: string) => {
},
];
case 'dateFormat':
return 'YY';
return 'MMM D, YYYY @ HH:mm:ss.SSS';
case 'history:limit':
return 10;
case 'timepicker:timeDefaults':
return {
from: 'now-15m',
to: 'now',
};
default:
throw new Error(`Unexpected config key: ${key}`);
}
@ -122,6 +127,7 @@ function wrapQueryBarTopRowInContext(testProps: any) {
describe('QueryBarTopRowTopRow', () => {
const QUERY_INPUT_SELECTOR = 'QueryBarInputUI';
const TIMEPICKER_SELECTOR = 'EuiSuperDatePicker';
const TIMEPICKER_DURATION = '[data-shared-timefilter-duration]';
beforeEach(() => {
jest.clearAllMocks();
@ -198,6 +204,24 @@ describe('QueryBarTopRowTopRow', () => {
expect(component.find(TIMEPICKER_SELECTOR).length).toBe(1);
});
it('Should render the timefilter duration container for sharing', () => {
const component = mount(
wrapQueryBarTopRowInContext({
isDirty: false,
screenTitle: 'Another Screen',
showDatePicker: true,
dateRangeFrom: 'now-7d',
dateRangeTo: 'now',
timeHistory: timefilterSetupMock.history,
})
);
// match the data attribute rendered in the in the ReactHTML object
expect(component.find(TIMEPICKER_DURATION)).toMatchObject(
/<div\b.*\bdata-shared-timefilter-duration\b/
);
});
it('Should render only query input bar', () => {
const component = mount(
wrapQueryBarTopRowInContext({

View file

@ -17,12 +17,20 @@
* under the License.
*/
import dateMath from '@elastic/datemath';
import { doesKueryExpressionHaveLuceneSyntaxError } from '@kbn/es-query';
import classNames from 'classnames';
import React, { useState, useEffect } from 'react';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLink, EuiSuperDatePicker } from '@elastic/eui';
import {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiSuperDatePicker,
prettyDuration,
} from '@elastic/eui';
// @ts-ignore
import { EuiSuperUpdateButton, OnRefreshProps } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
@ -156,6 +164,14 @@ function QueryBarTopRowUI(props: Props) {
});
}
function toAbsoluteString(value: string, roundUp = false) {
const valueAsMoment = dateMath.parse(value, { roundUp });
if (!valueAsMoment) {
return value;
}
return valueAsMoment.toISOString();
}
function renderQueryInput() {
if (!shouldRenderQueryInput()) return;
return (
@ -174,6 +190,22 @@ function QueryBarTopRowUI(props: Props) {
);
}
function renderSharingMetaFields() {
const { from, to } = getDateRange();
const dateRangePretty = prettyDuration(
toAbsoluteString(from),
toAbsoluteString(to),
[],
uiSettings.get('dateFormat')
);
return (
<div
data-shared-timefilter-duration={dateRangePretty}
data-test-subj="dataSharedTimefilterDuration"
/>
);
}
function shouldRenderDatePicker(): boolean {
return Boolean(props.showDatePicker || props.showAutoRefreshOnly);
}
@ -322,6 +354,7 @@ function QueryBarTopRowUI(props: Props) {
justifyContent="flexEnd"
>
{renderQueryInput()}
{renderSharingMetaFields()}
<EuiFlexItem grow={false}>{renderUpdateButton()}</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -22,7 +22,7 @@ import expect from '@kbn/expect';
export default function ({ getService, getPageObjects }) {
const retry = getService('retry');
const dashboardPanelActions = getService('dashboardPanelActions');
const PageObjects = getPageObjects(['dashboard']);
const PageObjects = getPageObjects(['dashboard', 'timePicker']);
describe('dashboard data-shared attributes', function describeIndexTests() {
let originalPanelTitles;
@ -32,6 +32,13 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.dashboard.waitForRenderComplete();
});
it('should have time picker with data-shared-timefilter-duration', async () => {
await retry.try(async () => {
const sharedData = await PageObjects.timePicker.getTimeDurationForSharing();
expect(sharedData).to.not.be(null);
});
});
it('should have data-shared-items-count set to the number of embeddables on the dashboard', async () => {
await retry.try(async () => {
const sharedItemsCount = await PageObjects.dashboard.getSharedItemsCount();

View file

@ -184,6 +184,14 @@ export function TimePickerPageProvider({ getService, getPageObjects }) {
};
}
async getTimeDurationForSharing() {
return await retry.try(async () => {
const element = await testSubjects.find('dataSharedTimefilterDuration');
const data = await element.getAttribute('data-shared-timefilter-duration');
return data;
});
}
async getTimeConfigAsAbsoluteTimes() {
await this.showStartEndTimes();

View file

@ -26,8 +26,7 @@ export interface LayoutSelectorDictionary {
screenshot: string;
renderComplete: string;
itemsCountAttribute: string;
timefilterFromAttribute: string;
timefilterToAttribute: string;
timefilterDurationAttribute: string;
toastHeader: string;
}
@ -36,6 +35,14 @@ export interface PdfImageSize {
height?: number;
}
export const getDefaultLayoutSelectors = (): LayoutSelectorDictionary => ({
screenshot: '[data-shared-items-container]',
renderComplete: '[data-shared-item]',
itemsCountAttribute: 'data-shared-items-count',
timefilterDurationAttribute: 'data-shared-timefilter-duration',
toastHeader: '[data-test-subj="euiToastHeader"]',
});
export abstract class Layout {
public id: string = '';

View file

@ -5,21 +5,19 @@
*/
import path from 'path';
import { LayoutTypes } from '../constants';
import { Layout, LayoutSelectorDictionary, PageSizeParams, Size } from './layout';
import {
getDefaultLayoutSelectors,
Layout,
LayoutSelectorDictionary,
PageSizeParams,
Size,
} from './layout';
// We use a zoom of two to bump up the resolution of the screenshot a bit.
const ZOOM: number = 2;
export class PreserveLayout extends Layout {
public readonly selectors: LayoutSelectorDictionary = {
screenshot: '[data-shared-items-container]',
renderComplete: '[data-shared-item]',
itemsCountAttribute: 'data-shared-items-count',
timefilterFromAttribute: 'data-shared-timefilter-from',
timefilterToAttribute: 'data-shared-timefilter-to',
toastHeader: '[data-test-subj="euiToastHeader"]',
};
public readonly selectors: LayoutSelectorDictionary = getDefaultLayoutSelectors();
public readonly groupCount = 1;
private readonly height: number;
private readonly width: number;

View file

@ -9,21 +9,15 @@ import { LevelLogger } from '../../../server/lib';
import { HeadlessChromiumDriver } from '../../../server/browsers/chromium/driver';
import { KbnServer } from '../../../types';
import { LayoutTypes } from '../constants';
import { Layout, LayoutSelectorDictionary, Size } from './layout';
import { getDefaultLayoutSelectors, Layout, LayoutSelectorDictionary, Size } from './layout';
import { CaptureConfig } from './types';
export class PrintLayout extends Layout {
public readonly selectors: LayoutSelectorDictionary = {
...getDefaultLayoutSelectors(),
screenshot: '[data-shared-item]',
renderComplete: '[data-shared-item]',
itemsCountAttribute: 'data-shared-items-count',
timefilterFromAttribute: 'data-shared-timefilter-from',
timefilterToAttribute: 'data-shared-timefilter-to',
toastHeader: '[data-test-subj="euiToastHeader"]',
};
public readonly groupCount = 2;
private captureConfig: CaptureConfig;
constructor(server: KbnServer) {

View file

@ -17,27 +17,25 @@ export const getTimeRange = async (
logger.debug('getting timeRange');
const timeRange: TimeRange | null = await browser.evaluate({
fn: (fromAttribute, toAttribute) => {
const fromElement = document.querySelector(`[${fromAttribute}]`);
const toElement = document.querySelector(`[${toAttribute}]`);
fn: durationAttribute => {
const durationElement = document.querySelector(`[${durationAttribute}]`);
if (!fromElement || !toElement) {
if (!durationElement) {
return null;
}
const from = fromElement.getAttribute(fromAttribute);
const to = toElement.getAttribute(toAttribute);
if (!to || !from) {
const duration = durationElement.getAttribute(durationAttribute);
if (!duration) {
return null;
}
return { from, to };
return { duration };
},
args: [layout.selectors.timefilterFromAttribute, layout.selectors.timefilterToAttribute],
args: [layout.selectors.timefilterDurationAttribute],
});
if (timeRange) {
logger.debug(`timeRange from ${timeRange.from} to ${timeRange.to}`);
logger.info(`timeRange: ${timeRange.duration}`);
} else {
logger.debug('no timeRange');
}

View file

@ -17,8 +17,7 @@ export interface ScreenshotObservableOpts {
}
export interface TimeRange {
from: any;
to: any;
duration: string;
}
export interface AttributesMap {
@ -31,7 +30,7 @@ export interface ElementsPositionAndAttribute {
}
export interface Screenshot {
base64EncodedData: any;
title: any;
description: any;
base64EncodedData: string;
title: string;
description: string;
}

View file

@ -26,7 +26,7 @@ function generatePngObservableFn(server: KbnServer) {
const captureConcurrency = 1;
// prettier-ignore
const createPngWithScreenshots = async ({ urlScreenshots }: { urlScreenshots: UrlScreenshot[] }) => {
const createPngWithScreenshots = async ({ urlScreenshots }: { urlScreenshots: UrlScreenshot[] }): Promise<string> => {
if (urlScreenshots.length !== 1) {
throw new Error(
`Expected there to be 1 URL screenshot, but there are ${urlScreenshots.length}`
@ -47,7 +47,7 @@ function generatePngObservableFn(server: KbnServer) {
browserTimezone: string,
conditionalHeaders: ConditionalHeaders,
layoutParams: LayoutParams
) {
): Rx.Observable<string> {
if (!layoutParams || !layoutParams.dimensions) {
throw new Error(`LayoutParams.Dimensions is undefined.`);
}

View file

@ -6,7 +6,6 @@
import * as Rx from 'rxjs';
import { toArray, mergeMap } from 'rxjs/operators';
import moment from 'moment-timezone';
import { groupBy } from 'lodash';
import { LevelLogger } from '../../../../server/lib';
import { KbnServer, ConditionalHeaders } from '../../../../types';
@ -39,10 +38,6 @@ const getTimeRange = (urlScreenshots: UrlScreenshot[]) => {
return null;
};
const formatDate = (date: Date, timezone: string) => {
return moment.tz(date, timezone).format('llll');
};
function generatePdfObservableFn(server: KbnServer) {
const screenshotsObservable = screenshotsObservableFactory(server);
const captureConcurrency = 1;
@ -72,12 +67,7 @@ function generatePdfObservableFn(server: KbnServer) {
if (title) {
const timeRange = getTimeRange(urlScreenshots);
title += timeRange
? `${formatDate(timeRange.from, browserTimezone)} to ${formatDate(
timeRange.to,
browserTimezone
)}`
: '';
title += timeRange ? ` - ${timeRange.duration}` : '';
pdfOutput.setTitle(title);
}