Per panel time range (#43153) (#43297)

* Per panel time range

* Added tests and fixed lack of await check for incompatibility

* Remove a couple more unneccessary `anys`
This commit is contained in:
Stacey Gammon 2019-08-14 15:20:59 -04:00 committed by GitHub
parent fc997b9518
commit 5eb8cfb243
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1364 additions and 17 deletions

View file

@ -70,7 +70,7 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => {
<I18nProvider>
<EmbeddablePanel
embeddable={embeddable}
getActions={(() => []) as any}
getActions={() => Promise.resolve([])}
getAllEmbeddableFactories={(() => []) as any}
getEmbeddableFactory={(() => null) as any}
notifications={{} as any}

View file

@ -18,7 +18,12 @@
*/
import { EmbeddableApi } from './api/types';
import { CONTEXT_MENU_TRIGGER, APPLY_FILTER_TRIGGER, ApplyFilterAction } from './lib';
import {
CONTEXT_MENU_TRIGGER,
APPLY_FILTER_TRIGGER,
ApplyFilterAction,
PANEL_BADGE_TRIGGER,
} from './lib';
/**
* This method initializes Embeddable plugin with initial set of
@ -39,10 +44,17 @@ export const bootstrap = (api: EmbeddableApi) => {
description: 'Triggered when user applies filter to an embeddable.',
actionIds: [],
};
const triggerBadge = {
id: PANEL_BADGE_TRIGGER,
title: 'Panel badges',
description: 'Actions appear in title bar when an embeddable loads in a panel',
actionIds: [],
};
const actionApplyFilter = new ApplyFilterAction();
api.registerTrigger(triggerContext);
api.registerTrigger(triggerFilter);
api.registerAction(actionApplyFilter);
api.registerTrigger(triggerBadge);
api.attachAction(triggerFilter.id, actionApplyFilter.id);
};

View file

@ -24,6 +24,7 @@ export {
ADD_PANEL_ACTION_ID,
APPLY_FILTER_ACTION,
APPLY_FILTER_TRIGGER,
PANEL_BADGE_TRIGGER,
Action,
ActionContext,
Adapters,

View file

@ -35,10 +35,10 @@ const getKeys = <T extends {}>(o: T): Array<keyof T> => Object.keys(o) as Array<
export abstract class Container<
TChildInput extends Partial<EmbeddableInput> = {},
TContainerInput extends ContainerInput = ContainerInput,
TContainerInput extends ContainerInput<TChildInput> = ContainerInput<TChildInput>,
TContainerOutput extends ContainerOutput = ContainerOutput
> extends Embeddable<TContainerInput, TContainerOutput>
implements IContainer<TContainerInput, TContainerOutput> {
implements IContainer<TChildInput, TContainerInput, TContainerOutput> {
public readonly isContainer: boolean = true;
protected readonly children: {
[key: string]: IEmbeddable<any, any> | ErrorEmbeddable;

View file

@ -63,7 +63,7 @@ test('EmbeddableChildPanel renders an embeddable when it is done loading', async
intl={null as any}
container={container}
embeddableId={newEmbeddable.id}
getActions={(() => undefined) as any}
getActions={() => Promise.resolve([])}
getAllEmbeddableFactories={(() => []) as any}
getEmbeddableFactory={(() => undefined) as any}
notifications={{} as any}
@ -102,7 +102,7 @@ test(`EmbeddableChildPanel renders an error message if the factory doesn't exist
intl={null as any}
container={container}
embeddableId={'1'}
getActions={(() => undefined) as any}
getActions={() => Promise.resolve([])}
getAllEmbeddableFactories={(() => []) as any}
getEmbeddableFactory={(() => undefined) as any}
notifications={{} as any}

View file

@ -50,7 +50,8 @@ export interface ContainerInput<PanelExplicitInput = {}> extends EmbeddableInput
}
export interface IContainer<
I extends ContainerInput = ContainerInput,
Inherited extends {} = {},
I extends ContainerInput<Inherited> = ContainerInput<Inherited>,
O extends ContainerOutput = ContainerOutput
> extends IEmbeddable<I, O> {
/**

View file

@ -156,7 +156,7 @@ test('HelloWorldContainer in view mode hides edit mode actions', async () => {
<I18nProvider>
<EmbeddablePanel
embeddable={embeddable}
getActions={(() => undefined) as any}
getActions={() => Promise.resolve([])}
getAllEmbeddableFactories={(() => []) as any}
getEmbeddableFactory={(() => undefined) as any}
notifications={{} as any}
@ -193,7 +193,7 @@ test('HelloWorldContainer in edit mode shows edit mode actions', async () => {
<I18nProvider>
<EmbeddablePanel
embeddable={embeddable}
getActions={(() => undefined) as any}
getActions={() => Promise.resolve([])}
getAllEmbeddableFactories={(() => []) as any}
getEmbeddableFactory={(() => undefined) as any}
notifications={{} as any}
@ -255,7 +255,7 @@ test('Updates when hidePanelTitles is toggled', async () => {
<I18nProvider>
<EmbeddablePanel
embeddable={embeddable}
getActions={(() => undefined) as any}
getActions={() => Promise.resolve([])}
getAllEmbeddableFactories={(() => []) as any}
getEmbeddableFactory={(() => undefined) as any}
notifications={{} as any}

View file

@ -23,7 +23,7 @@ import { Subscription } from 'rxjs';
import { CoreStart } from '../../../../../../../../core/public';
import { buildContextMenuForActions } from '../context_menu_actions';
import { CONTEXT_MENU_TRIGGER } from '../triggers';
import { CONTEXT_MENU_TRIGGER, PANEL_BADGE_TRIGGER } from '../triggers';
import { IEmbeddable } from '../embeddables/i_embeddable';
import {
ViewMode,
@ -58,6 +58,7 @@ interface State {
viewMode: ViewMode;
hidePanelTitles: boolean;
closeContextMenu: boolean;
badges: Action[];
}
export class EmbeddablePanel extends React.Component<Props, State> {
@ -80,11 +81,24 @@ export class EmbeddablePanel extends React.Component<Props, State> {
viewMode,
hidePanelTitles,
closeContextMenu: false,
badges: [],
};
this.embeddableRoot = React.createRef();
}
private async refreshBadges() {
const badges = await this.props.getActions(PANEL_BADGE_TRIGGER, {
embeddable: this.props.embeddable,
});
if (this.mounted) {
this.setState({
badges,
});
}
}
public componentWillMount() {
this.mounted = true;
const { embeddable } = this.props;
@ -95,6 +109,8 @@ export class EmbeddablePanel extends React.Component<Props, State> {
this.setState({
viewMode: embeddable.getInput().viewMode ? embeddable.getInput().viewMode : ViewMode.EDIT,
});
this.refreshBadges();
}
});
@ -104,6 +120,8 @@ export class EmbeddablePanel extends React.Component<Props, State> {
this.setState({
hidePanelTitles: Boolean(parent.getInput().hidePanelTitles),
});
this.refreshBadges();
}
});
}
@ -144,6 +162,8 @@ export class EmbeddablePanel extends React.Component<Props, State> {
isViewMode={viewOnlyMode}
closeContextMenu={this.state.closeContextMenu}
title={title}
badges={this.state.badges}
embeddable={this.props.embeddable}
/>
<div className="embPanel__content" ref={this.embeddableRoot} />
</EuiPanel>

View file

@ -29,9 +29,9 @@ interface ExpandedPanelInput extends ContainerInput {
}
function hasExpandedPanelInput(
container: IContainer | IContainer<ExpandedPanelInput>
): container is IContainer<ExpandedPanelInput> {
return (container as IContainer<ExpandedPanelInput>).getInput().expandedPanelId !== undefined;
container: IContainer
): container is IContainer<{}, ExpandedPanelInput> {
return (container as IContainer<{}, ExpandedPanelInput>).getInput().expandedPanelId !== undefined;
}
export class RemovePanelAction extends Action {

View file

@ -17,11 +17,13 @@
* under the License.
*/
import { EuiContextMenuPanelDescriptor } from '@elastic/eui';
import { EuiContextMenuPanelDescriptor, EuiBadge } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import classNames from 'classnames';
import React from 'react';
import { PanelOptionsMenu } from './panel_options_menu';
import { Action } from '../../actions';
import { IEmbeddable } from '../../embeddables';
export interface PanelHeaderProps {
title?: string;
@ -29,12 +31,27 @@ export interface PanelHeaderProps {
hidePanelTitles: boolean;
getActionContextMenuPanel: () => Promise<EuiContextMenuPanelDescriptor>;
closeContextMenu: boolean;
badges: Action[];
embeddable: IEmbeddable;
}
interface PanelHeaderUiProps extends PanelHeaderProps {
intl: InjectedIntl;
}
function renderBadges(badges: Action[], embeddable: IEmbeddable) {
return badges.map(badge => (
<EuiBadge
key={badge.id}
iconType={badge.getIconType({ embeddable })}
onClick={() => badge.execute({ embeddable })}
onClickAriaLabel={badge.getDisplayName({ embeddable })}
>
{badge.getDisplayName({ embeddable })}
</EuiBadge>
));
}
function PanelHeaderUi({
title,
isViewMode,
@ -42,12 +59,17 @@ function PanelHeaderUi({
getActionContextMenuPanel,
intl,
closeContextMenu,
badges,
embeddable,
}: PanelHeaderUiProps) {
const classes = classNames('embPanel__header', {
'embPanel__header--floater': !title || hidePanelTitles,
});
if (isViewMode && (!title || hidePanelTitles)) {
const showTitle = !isViewMode || (title && !hidePanelTitles);
const showPanelBar = badges.length > 0 || showTitle;
if (!showPanelBar) {
return (
<div className={classes}>
<PanelOptionsMenu
@ -78,7 +100,8 @@ function PanelHeaderUi({
}
)}
>
{hidePanelTitles ? '' : title}
{showTitle ? `${title} ` : ''}
{renderBadges(badges, embeddable)}
</div>
<PanelOptionsMenu

View file

@ -19,3 +19,4 @@
export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER';
export const APPLY_FILTER_TRIGGER = 'FILTER_TRIGGER';
export const PANEL_BADGE_TRIGGER = 'PANEL_BADGE_TRIGGER';

View file

@ -65,6 +65,7 @@ export interface VisualizeOutput extends EmbeddableOutput {
editUrl: string;
indexPatterns?: StaticIndexPattern[];
savedObjectId: string;
visTypeName: string;
}
export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOutput> {
@ -99,6 +100,7 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
indexPatterns,
editable,
savedObjectId: savedVisualization.id!,
visTypeName: savedVisualization.vis.type.name,
},
parent
);

View file

@ -2,6 +2,7 @@
"prefix": "xpack",
"paths": {
"xpack.actions": "legacy/plugins/actions",
"xpack.advancedUiActions": "legacy/plugins/advanced_ui_actions",
"xpack.alerting": "legacy/plugins/alerting",
"xpack.apm": "legacy/plugins/apm",
"xpack.beatsManagement": "legacy/plugins/beats_management",

View file

@ -43,6 +43,7 @@ import { encryptedSavedObjects } from './legacy/plugins/encrypted_saved_objects'
import { snapshotRestore } from './legacy/plugins/snapshot_restore';
import { actions } from './legacy/plugins/actions';
import { alerting } from './legacy/plugins/alerting';
import { advancedUiActions } from './legacy/plugins/advanced_ui_actions';
module.exports = function (kibana) {
return [
@ -85,5 +86,6 @@ module.exports = function (kibana) {
snapshotRestore(kibana),
actions(kibana),
alerting(kibana),
advancedUiActions(kibana),
];
};

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { resolve } from 'path';
export const advancedUiActions = (kibana: any) =>
new kibana.Plugin({
id: 'advanced_ui_actions',
publicDir: resolve(__dirname, 'public'),
uiExports: {
hacks: 'plugins/advanced_ui_actions/np_ready/public/legacy',
},
});

View file

@ -0,0 +1,9 @@
{
"id": "advanced_ui_actions",
"version": "kibana",
"requiredPlugins": [
"embeddable"
],
"server": false,
"ui": true
}

View file

@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { canInheritTimeRange } from './can_inherit_time_range';
import {
HelloWorldEmbeddable,
HelloWorldContainer,
} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples';
import { TimeRangeEmbeddable, TimeRangeContainer } from './test_helpers';
jest.mock('ui/new_platform');
test('canInheritTimeRange returns false if embeddable is inside container without a time range', () => {
const embeddable = new TimeRangeEmbeddable(
{ id: '1234', timeRange: { from: 'noxw-15m', to: 'now' } },
new HelloWorldContainer({ id: '123', panels: {} }, (() => null) as any)
);
expect(canInheritTimeRange(embeddable)).toBe(false);
});
test('canInheritTimeRange returns false if embeddable is without a time range', () => {
const embeddable = new HelloWorldEmbeddable(
{ id: '1234' },
new HelloWorldContainer({ id: '123', panels: {} }, (() => null) as any)
);
// @ts-ignore
expect(canInheritTimeRange(embeddable)).toBe(false);
});
test('canInheritTimeRange returns true if embeddable is inside a container with a time range', () => {
const embeddable = new TimeRangeEmbeddable(
{ id: '1234', timeRange: { from: 'noxw-15m', to: 'now' } },
new TimeRangeContainer(
{ id: '123', panels: {}, timeRange: { from: 'noxw-15m', to: 'now' } },
(() => null) as any
)
);
expect(canInheritTimeRange(embeddable)).toBe(true);
});

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { TimeRange } from '../../../../../../../src/plugins/data/public';
import {
Embeddable,
IContainer,
ContainerInput,
} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public';
import { TimeRangeInput } from './custom_time_range_action';
interface ContainerTimeRangeInput extends ContainerInput<TimeRangeInput> {
timeRange: TimeRange;
}
export function canInheritTimeRange(embeddable: Embeddable<TimeRangeInput>) {
if (!embeddable.parent) {
return false;
}
const parent = embeddable.parent as IContainer<TimeRangeInput, ContainerTimeRangeInput>;
return parent.getInput().timeRange !== undefined;
}

View file

@ -0,0 +1,345 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// @ts-ignore
import { findTestSubject } from '@elastic/eui/lib/test';
import { skip } from 'rxjs/operators';
import * as Rx from 'rxjs';
import { mount } from 'enzyme';
import { EmbeddableFactory } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public';
import { TimeRangeEmbeddable, TimeRangeContainer, TIME_RANGE_EMBEDDABLE } from './test_helpers';
import { TimeRangeEmbeddableFactory } from './test_helpers/time_range_embeddable_factory';
import { CustomTimeRangeAction } from './custom_time_range_action';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import {
HelloWorldEmbeddableFactory,
HELLO_WORLD_EMBEDDABLE_TYPE,
HelloWorldEmbeddable,
HelloWorldContainer,
} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples';
import { nextTick } from 'test_utils/enzyme_helpers';
import { ReactElement } from 'react';
jest.mock('ui/new_platform');
test('Custom time range action prevents embeddable from using container time', async done => {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
panels: {
'1': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '1',
},
},
'2': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '2',
},
},
},
id: '123',
},
(() => {}) as any
);
await container.untilEmbeddableLoaded('1');
await container.untilEmbeddableLoaded('2');
const child1 = container.getChild<TimeRangeEmbeddable>('1');
expect(child1).toBeDefined();
expect(child1.getInput().timeRange).toEqual({ from: 'now-15m', to: 'now' });
const child2 = container.getChild<TimeRangeEmbeddable>('2');
expect(child2).toBeDefined();
expect(child2.getInput().timeRange).toEqual({ from: 'now-15m', to: 'now' });
const start = coreMock.createStart();
const overlayMock = start.overlays;
overlayMock.openModal.mockClear();
new CustomTimeRangeAction({
openModal: start.overlays.openModal,
commonlyUsedRanges: [],
dateFormat: 'MM YYY',
}).execute({
embeddable: child1,
});
await nextTick();
const openModal = overlayMock.openModal.mock.calls[0][0] as ReactElement;
const wrapper = mount(openModal);
wrapper.setState({ timeRange: { from: 'now-30days', to: 'now-29days' } });
findTestSubject(wrapper, 'addPerPanelTimeRangeButton').simulate('click');
const subscription = Rx.merge(container.getOutput$(), container.getInput$())
.pipe(skip(2))
.subscribe(() => {
expect(child1.getInput().timeRange).toEqual({ from: 'now-30days', to: 'now-29days' });
expect(child2.getInput().timeRange).toEqual({ from: 'now-30m', to: 'now-1m' });
subscription.unsubscribe();
done();
});
container.updateInput({ timeRange: { from: 'now-30m', to: 'now-1m' } });
});
test('Removing custom time range action resets embeddable back to container time', async done => {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
panels: {
'1': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '1',
},
},
'2': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '2',
},
},
},
id: '123',
},
(() => {}) as any
);
await container.untilEmbeddableLoaded('1');
await container.untilEmbeddableLoaded('2');
const child1 = container.getChild<TimeRangeEmbeddable>('1');
const child2 = container.getChild<TimeRangeEmbeddable>('2');
const start = coreMock.createStart();
const overlayMock = start.overlays;
overlayMock.openModal.mockClear();
new CustomTimeRangeAction({
openModal: start.overlays.openModal,
commonlyUsedRanges: [],
dateFormat: 'MM YYY',
}).execute({
embeddable: child1,
});
await nextTick();
const openModal = overlayMock.openModal.mock.calls[0][0] as ReactElement;
const wrapper = mount(openModal);
wrapper.setState({ timeRange: { from: 'now-30days', to: 'now-29days' } });
findTestSubject(wrapper, 'addPerPanelTimeRangeButton').simulate('click');
container.updateInput({ timeRange: { from: 'now-30m', to: 'now-1m' } });
new CustomTimeRangeAction({
openModal: start.overlays.openModal,
commonlyUsedRanges: [],
dateFormat: 'MM YYY',
}).execute({
embeddable: child1,
});
await nextTick();
const openModal2 = (overlayMock.openModal as any).mock.calls[1][0];
const wrapper2 = mount(openModal2);
findTestSubject(wrapper2, 'removePerPanelTimeRangeButton').simulate('click');
const subscription = Rx.merge(container.getOutput$(), container.getInput$())
.pipe(skip(2))
.subscribe(() => {
expect(child1.getInput().timeRange).toEqual({ from: 'now-10m', to: 'now-5m' });
expect(child2.getInput().timeRange).toEqual({ from: 'now-10m', to: 'now-5m' });
subscription.unsubscribe();
done();
});
container.updateInput({ timeRange: { from: 'now-10m', to: 'now-5m' } });
});
test('Cancelling custom time range action leaves state alone', async done => {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
panels: {
'1': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '1',
timeRange: { to: '2', from: '1' },
},
},
'2': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '2',
},
},
},
id: '123',
},
(() => {}) as any
);
await container.untilEmbeddableLoaded('1');
await container.untilEmbeddableLoaded('2');
const child1 = container.getChild<TimeRangeEmbeddable>('1');
const child2 = container.getChild<TimeRangeEmbeddable>('2');
const start = coreMock.createStart();
const overlayMock = start.overlays;
overlayMock.openModal.mockClear();
new CustomTimeRangeAction({
openModal: start.overlays.openModal,
commonlyUsedRanges: [],
dateFormat: 'MM YYY',
}).execute({
embeddable: child1,
});
await nextTick();
const openModal = overlayMock.openModal.mock.calls[0][0] as ReactElement;
const wrapper = mount(openModal);
wrapper.setState({ timeRange: { from: 'now-300m', to: 'now-400m' } });
findTestSubject(wrapper, 'cancelPerPanelTimeRangeButton').simulate('click');
const subscription = Rx.merge(container.getOutput$(), container.getInput$())
.pipe(skip(2))
.subscribe(() => {
expect(child1.getInput().timeRange).toEqual({ from: '1', to: '2' });
expect(child2.getInput().timeRange).toEqual({ from: 'now-30m', to: 'now-1m' });
subscription.unsubscribe();
done();
});
container.updateInput({ timeRange: { from: 'now-30m', to: 'now-1m' } });
});
test(`badge is compatible with embeddable that inherits from parent`, async () => {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
panels: {
'1': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '1',
},
},
},
id: '123',
},
(() => {}) as any
);
await container.untilEmbeddableLoaded('1');
const child = container.getChild<TimeRangeEmbeddable>('1');
const start = coreMock.createStart();
const compatible = await new CustomTimeRangeAction({
openModal: start.overlays.openModal,
commonlyUsedRanges: [],
dateFormat: 'MM YYY',
}).isCompatible({
embeddable: child,
});
expect(compatible).toBe(true);
});
// TODO: uncomment when https://github.com/elastic/kibana/issues/43271 is fixed.
// test('Embeddable that does not use time range in a container that has time range is incompatible', async () => {
// const embeddableFactories = new Map<string, EmbeddableFactory>();
// embeddableFactories.set(HELLO_WORLD_EMBEDDABLE_TYPE, new HelloWorldEmbeddableFactory());
// const container = new TimeRangeContainer(
// {
// timeRange: { from: 'now-15m', to: 'now' },
// panels: {
// '1': {
// type: HELLO_WORLD_EMBEDDABLE_TYPE,
// explicitInput: {
// id: '1',
// },
// },
// },
// id: '123',
// },
// (() => null) as any
// );
// await container.untilEmbeddableLoaded('1');
// const child = container.getChild<HelloWorldEmbeddable>('1');
// const start = coreMock.createStart();
// const action = await new CustomTimeRangeAction({
// openModal: start.overlays.openModal,
// dateFormat: 'MM YYYY',
// commonlyUsedRanges: [],
// });
// async function check() {
// await action.execute({ embeddable: child });
// }
// await expect(check()).rejects.toThrow(Error);
// });
test('Attempting to execute on incompatible embeddable throws an error', async () => {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(HELLO_WORLD_EMBEDDABLE_TYPE, new HelloWorldEmbeddableFactory());
const container = new HelloWorldContainer(
{
panels: {
'1': {
type: HELLO_WORLD_EMBEDDABLE_TYPE,
explicitInput: {
id: '1',
},
},
},
id: '123',
},
(() => null) as any
);
await container.untilEmbeddableLoaded('1');
const child = container.getChild<HelloWorldEmbeddable>('1');
const start = coreMock.createStart();
const action = await new CustomTimeRangeAction({
openModal: start.overlays.openModal,
dateFormat: 'MM YYYY',
commonlyUsedRanges: [],
});
async function check() {
await action.execute({ embeddable: child });
}
await expect(check()).rejects.toThrow(Error);
});

View file

@ -0,0 +1,112 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import { TimeRange } from '../../../../../../../src/plugins/data/public';
import { SEARCH_EMBEDDABLE_TYPE } from '../../../../../../../src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable';
import { VisualizeEmbeddable } from '../../../../../../../src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable';
import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../../../../src/legacy/core_plugins/kibana/public/visualize/embeddable/constants';
import {
Action,
IEmbeddable,
ActionContext,
IncompatibleActionError,
Embeddable,
EmbeddableInput,
} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public';
import { CustomizeTimeRangeModal } from './customize_time_range_modal';
import { OpenModal, CommonlyUsedRange } from './types';
const CUSTOM_TIME_RANGE = 'CUSTOM_TIME_RANGE';
export interface TimeRangeInput extends EmbeddableInput {
timeRange: TimeRange;
}
function hasTimeRange(
embeddable: IEmbeddable | Embeddable<TimeRangeInput>
): embeddable is Embeddable<TimeRangeInput> {
return (embeddable as Embeddable<TimeRangeInput>).getInput().timeRange !== undefined;
}
function isVisualizeEmbeddable(
embeddable: IEmbeddable | VisualizeEmbeddable
): embeddable is VisualizeEmbeddable {
return embeddable.type === VISUALIZE_EMBEDDABLE_TYPE;
}
export class CustomTimeRangeAction extends Action {
public readonly type = CUSTOM_TIME_RANGE;
private openModal: OpenModal;
private dateFormat?: string;
private commonlyUsedRanges: CommonlyUsedRange[];
constructor({
openModal,
dateFormat,
commonlyUsedRanges,
}: {
openModal: OpenModal;
dateFormat: string;
commonlyUsedRanges: CommonlyUsedRange[];
}) {
super(CUSTOM_TIME_RANGE);
this.order = 7;
this.openModal = openModal;
this.dateFormat = dateFormat;
this.commonlyUsedRanges = commonlyUsedRanges;
}
public getDisplayName() {
return i18n.translate('xpack.advancedUiActions.customizeTimeRangeMenuItem.displayName', {
defaultMessage: 'Customize time range',
});
}
public getIconType() {
return 'calendar';
}
public async isCompatible({ embeddable }: ActionContext) {
const isInputControl =
isVisualizeEmbeddable(embeddable) &&
embeddable.getOutput().visTypeName === 'input_control_vis';
const isMarkdown =
isVisualizeEmbeddable(embeddable) && embeddable.getOutput().visTypeName === 'markdown';
return Boolean(
embeddable &&
hasTimeRange(embeddable) &&
// Saved searches don't listen to the time range from the container that is passed down to them so it
// won't work without a fix. For now, just leave them out.
embeddable.type !== SEARCH_EMBEDDABLE_TYPE &&
!isInputControl &&
!isMarkdown
);
}
public async execute({ embeddable }: ActionContext) {
const isCompatible = await this.isCompatible({ embeddable });
if (!isCompatible) {
throw new IncompatibleActionError();
}
// Only here for typescript
if (hasTimeRange(embeddable)) {
const modalSession = this.openModal(
<CustomizeTimeRangeModal
onClose={() => modalSession.close()}
embeddable={embeddable}
dateFormat={this.dateFormat}
commonlyUsedRanges={this.commonlyUsedRanges}
/>
);
}
}
}

View file

@ -0,0 +1,186 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// @ts-ignore
import { findTestSubject } from '@elastic/eui/lib/test';
import { skip } from 'rxjs/operators';
import * as Rx from 'rxjs';
import { mount } from 'enzyme';
import { EmbeddableFactory } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public';
import { TimeRangeEmbeddable, TimeRangeContainer, TIME_RANGE_EMBEDDABLE } from './test_helpers';
import { TimeRangeEmbeddableFactory } from './test_helpers/time_range_embeddable_factory';
import { CustomTimeRangeBadge } from './custom_time_range_badge';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { ReactElement } from 'react';
import { nextTick } from 'test_utils/enzyme_helpers';
test('Removing custom time range from badge resets embeddable back to container time', async done => {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
panels: {
'1': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '1',
timeRange: { from: '1', to: '2' },
},
},
'2': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '2',
},
},
},
id: '123',
},
(() => null) as any
);
await container.untilEmbeddableLoaded('1');
await container.untilEmbeddableLoaded('2');
const child1 = container.getChild<TimeRangeEmbeddable>('1');
const child2 = container.getChild<TimeRangeEmbeddable>('2');
const start = coreMock.createStart();
const overlayMock = start.overlays;
overlayMock.openModal.mockClear();
new CustomTimeRangeBadge({
openModal: start.overlays.openModal,
dateFormat: 'MM YYYY',
commonlyUsedRanges: [],
}).execute({
embeddable: child1,
});
await nextTick();
const openModal = overlayMock.openModal.mock.calls[0][0] as ReactElement;
const wrapper = mount(openModal);
findTestSubject(wrapper, 'removePerPanelTimeRangeButton').simulate('click');
const subscription = Rx.merge(child1.getInput$(), container.getOutput$(), container.getInput$())
.pipe(skip(4))
.subscribe(() => {
expect(child1.getInput().timeRange).toEqual({ from: 'now-10m', to: 'now-5m' });
expect(child2.getInput().timeRange).toEqual({ from: 'now-10m', to: 'now-5m' });
subscription.unsubscribe();
done();
});
container.updateInput({ timeRange: { from: 'now-10m', to: 'now-5m' } });
});
test(`badge is not compatible with embeddable that inherits from parent`, async () => {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
panels: {
'1': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '1',
},
},
},
id: '123',
},
(() => null) as any
);
await container.untilEmbeddableLoaded('1');
const child = container.getChild<TimeRangeEmbeddable>('1');
const start = coreMock.createStart();
const compatible = await new CustomTimeRangeBadge({
openModal: start.overlays.openModal,
dateFormat: 'MM YYYY',
commonlyUsedRanges: [],
}).isCompatible({
embeddable: child,
});
expect(compatible).toBe(false);
});
test(`badge is compatible with embeddable that has custom time range`, async () => {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
panels: {
'1': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '1',
timeRange: { to: '123', from: '456' },
},
},
},
id: '123',
},
(() => null) as any
);
await container.untilEmbeddableLoaded('1');
const child = container.getChild<TimeRangeEmbeddable>('1');
const start = coreMock.createStart();
const compatible = await new CustomTimeRangeBadge({
openModal: start.overlays.openModal,
dateFormat: 'MM YYYY',
commonlyUsedRanges: [],
}).isCompatible({
embeddable: child,
});
expect(compatible).toBe(true);
});
test('Attempting to execute on incompatible embeddable throws an error', async () => {
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(TIME_RANGE_EMBEDDABLE, new TimeRangeEmbeddableFactory());
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
panels: {
'1': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '1',
},
},
},
id: '123',
},
(() => null) as any
);
await container.untilEmbeddableLoaded('1');
const child = container.getChild<TimeRangeEmbeddable>('1');
const start = coreMock.createStart();
const badge = await new CustomTimeRangeBadge({
openModal: start.overlays.openModal,
dateFormat: 'MM YYYY',
commonlyUsedRanges: [],
});
async function check() {
await badge.execute({ embeddable: child });
}
await expect(check()).rejects.toThrow(Error);
});

View file

@ -0,0 +1,93 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { prettyDuration, commonDurationRanges } from '@elastic/eui';
import { TimeRange } from '../../../../../../../src/plugins/data/public';
import {
Action,
IEmbeddable,
ActionContext,
IncompatibleActionError,
Embeddable,
EmbeddableInput,
} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public';
import { CustomizeTimeRangeModal } from './customize_time_range_modal';
import { doesInheritTimeRange } from './does_inherit_time_range';
import { OpenModal, CommonlyUsedRange } from './types';
const CUSTOM_TIME_RANGE_BADGE = 'CUSTOM_TIME_RANGE_BADGE';
export interface TimeRangeInput extends EmbeddableInput {
timeRange: TimeRange;
}
function hasTimeRange(
embeddable: IEmbeddable | Embeddable<TimeRangeInput>
): embeddable is Embeddable<TimeRangeInput> {
return (embeddable as Embeddable<TimeRangeInput>).getInput().timeRange !== undefined;
}
export class CustomTimeRangeBadge extends Action {
public readonly type = CUSTOM_TIME_RANGE_BADGE;
private openModal: OpenModal;
private dateFormat: string;
private commonlyUsedRanges: CommonlyUsedRange[];
constructor({
openModal,
dateFormat,
commonlyUsedRanges,
}: {
openModal: OpenModal;
dateFormat: string;
commonlyUsedRanges: CommonlyUsedRange[];
}) {
super(CUSTOM_TIME_RANGE_BADGE);
this.order = 7;
this.openModal = openModal;
this.dateFormat = dateFormat;
this.commonlyUsedRanges = commonlyUsedRanges;
}
public getDisplayName({ embeddable }: ActionContext<Embeddable<TimeRangeInput>>) {
return prettyDuration(
embeddable.getInput().timeRange.from,
embeddable.getInput().timeRange.to,
commonDurationRanges,
this.dateFormat
);
}
public getIconType() {
return 'calendar';
}
public async isCompatible({ embeddable }: ActionContext) {
return Boolean(embeddable && hasTimeRange(embeddable) && !doesInheritTimeRange(embeddable));
}
public async execute({ embeddable }: ActionContext) {
const isCompatible = await this.isCompatible({ embeddable });
if (!isCompatible) {
throw new IncompatibleActionError();
}
// Only here for typescript
if (hasTimeRange(embeddable)) {
const modalSession = this.openModal(
<CustomizeTimeRangeModal
onClose={() => modalSession.close()}
embeddable={embeddable}
dateFormat={this.dateFormat}
commonlyUsedRanges={this.commonlyUsedRanges}
/>
);
}
}
}

View file

@ -0,0 +1,185 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Component } from 'react';
import {
EuiFormRow,
EuiButton,
EuiButtonEmpty,
EuiModalHeader,
EuiModalFooter,
EuiModalBody,
EuiModalHeaderTitle,
EuiSuperDatePicker,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { TimeRange } from '../../../../../../../src/plugins/data/public';
import {
Embeddable,
IContainer,
ContainerInput,
} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public';
import { TimeRangeInput } from './custom_time_range_action';
import { doesInheritTimeRange } from './does_inherit_time_range';
import { CommonlyUsedRange } from './types';
interface CustomizeTimeRangeProps {
embeddable: Embeddable<TimeRangeInput>;
onClose: () => void;
dateFormat?: string;
commonlyUsedRanges: CommonlyUsedRange[];
}
interface State {
timeRange?: TimeRange;
inheritTimeRange: boolean;
}
export class CustomizeTimeRangeModal extends Component<CustomizeTimeRangeProps, State> {
constructor(props: CustomizeTimeRangeProps) {
super(props);
this.state = {
timeRange: props.embeddable.getInput().timeRange,
inheritTimeRange: doesInheritTimeRange(props.embeddable),
};
}
onTimeChange = ({ start, end }: { start: string; end: string }) => {
this.setState({ timeRange: { from: start, to: end } });
};
cancel = () => {
this.props.onClose();
};
onInheritToggle = () => {
this.setState(prevState => ({
inheritTimeRange: !prevState.inheritTimeRange,
}));
};
addToPanel = () => {
const { embeddable } = this.props;
embeddable.updateInput({ timeRange: this.state.timeRange });
this.props.onClose();
};
inheritFromParent = () => {
const { embeddable } = this.props;
const parent = embeddable.parent as IContainer<{}, ContainerInput<TimeRangeInput>>;
const parentPanels = parent!.getInput().panels;
// Remove any explicit input to this child from the parent.
parent!.updateInput({
panels: {
...parentPanels,
[embeddable.id]: {
...parentPanels[embeddable.id],
explicitInput: {
...parentPanels[embeddable.id].explicitInput,
timeRange: undefined,
},
},
},
});
this.props.onClose();
};
public render() {
return (
<React.Fragment>
<EuiModalHeader>
<EuiModalHeaderTitle data-test-subj="customizePanelTitle">
{i18n.translate('xpack.advancedUiActions.customizeTimeRange.modal.headerTitle', {
defaultMessage: 'Customize panel time range',
})}
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiFormRow
label={i18n.translate(
'xpack.advancedUiActions.customizePanelTimeRange.modal.optionsMenuForm.panelTitleFormRowLabel',
{
defaultMessage: 'Time range',
}
)}
>
<EuiSuperDatePicker
start={this.state.timeRange ? this.state.timeRange.from : undefined}
end={this.state.timeRange ? this.state.timeRange.to : undefined}
isPaused={false}
onTimeChange={this.onTimeChange}
showUpdateButton={false}
dateFormat={this.props.dateFormat}
commonlyUsedRanges={this.props.commonlyUsedRanges.map(
({ from, to, display }: { from: string; to: string; display: string }) => {
return {
start: from,
end: to,
label: display,
};
}
)}
/>
</EuiFormRow>
</EuiModalBody>
<EuiModalFooter>
<EuiFlexGroup gutterSize="s" responsive={false}>
<EuiFlexItem>
<EuiButtonEmpty
onClick={this.inheritFromParent}
color="danger"
data-test-subj="removePerPanelTimeRangeButton"
disabled={this.state.inheritTimeRange}
>
{i18n.translate(
'xpack.advancedUiActions.customizePanelTimeRange.modal.removeButtonTitle',
{
defaultMessage: 'Remove',
}
)}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem>
<EuiButtonEmpty onClick={this.cancel} data-test-subj="cancelPerPanelTimeRangeButton">
{i18n.translate(
'xpack.advancedUiActions.customizePanelTimeRange.modal.cancelButtonTitle',
{
defaultMessage: 'Cancel',
}
)}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem>
<EuiButton data-test-subj="addPerPanelTimeRangeButton" onClick={this.addToPanel} fill>
{this.state.inheritTimeRange
? i18n.translate(
'xpack.advancedUiActions.customizePanelTimeRange.modal.addToPanelButtonTitle',
{
defaultMessage: 'Add to panel',
}
)
: i18n.translate(
'xpack.advancedUiActions.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle',
{
defaultMessage: 'Update',
}
)}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalFooter>
</React.Fragment>
);
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
Embeddable,
IContainer,
ContainerInput,
} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public';
import { TimeRangeInput } from './custom_time_range_action';
export function doesInheritTimeRange(embeddable: Embeddable<TimeRangeInput>) {
if (!embeddable.parent) {
return false;
}
const parent = embeddable.parent as IContainer<{}, ContainerInput<TimeRangeInput>>;
// Note: this logic might not work in a container nested world... the explicit input
// may be on the root... or any of the interim parents.
// If there is no explicit input defined on the parent then this embeddable inherits the
// time range from whatever the time range of the parent is.
return parent.getInput().panels[embeddable.id].explicitInput.timeRange === undefined;
}

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { PluginInitializerContext } from 'src/core/public';
import { AdvancedUiActionsPublicPlugin } from './plugin';
export function plugin(initializerContext: PluginInitializerContext) {
return new AdvancedUiActionsPublicPlugin(initializerContext);
}
export { AdvancedUiActionsPublicPlugin as Plugin };

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable @kbn/eslint/no-restricted-paths */
import { npSetup, npStart } from 'ui/new_platform';
/* eslint-enable @kbn/eslint/no-restricted-paths */
import { plugin } from '.';
import {
setup as embeddableSetup,
start as embeddableStart,
} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy';
const pluginInstance = plugin({} as any);
export const setup = pluginInstance.setup(npSetup.core, {
embeddable: embeddableSetup,
});
export const start = pluginInstance.start(npStart.core, {
embeddable: embeddableStart,
});

View file

@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public';
import {
Plugin as EmbeddablePlugin,
CONTEXT_MENU_TRIGGER,
PANEL_BADGE_TRIGGER,
} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public';
import { CustomTimeRangeAction } from './custom_time_range_action';
import { CustomTimeRangeBadge } from './custom_time_range_badge';
import { CommonlyUsedRange } from './types';
interface SetupDependencies {
embeddable: ReturnType<EmbeddablePlugin['setup']>;
}
interface StartDependencies {
embeddable: ReturnType<EmbeddablePlugin['start']>;
}
export type Setup = void;
export type Start = void;
export class AdvancedUiActionsPublicPlugin
implements Plugin<Setup, Start, SetupDependencies, StartDependencies> {
constructor(initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup, { embeddable }: SetupDependencies): Setup {}
public start(core: CoreStart, { embeddable }: StartDependencies): Start {
const dateFormat = core.uiSettings.get('dateFormat') as string;
const commonlyUsedRanges = core.uiSettings.get('timepicker:quickRanges') as CommonlyUsedRange[];
const timeRangeAction = new CustomTimeRangeAction({
openModal: core.overlays.openModal,
dateFormat,
commonlyUsedRanges,
});
embeddable.registerAction(timeRangeAction);
embeddable.attachAction(CONTEXT_MENU_TRIGGER, timeRangeAction.id);
const timeRangeBadge = new CustomTimeRangeBadge({
openModal: core.overlays.openModal,
dateFormat,
commonlyUsedRanges,
});
embeddable.registerAction(timeRangeBadge);
embeddable.attachAction(PANEL_BADGE_TRIGGER, timeRangeBadge.id);
}
public stop() {}
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { TimeRangeEmbeddable, TIME_RANGE_EMBEDDABLE } from './time_range_embeddable';
export { TimeRangeContainer } from './time_range_container';

View file

@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { TimeRange } from '../../../../../../../../src/plugins/data/public';
import {
ContainerInput,
Container,
ContainerOutput,
GetEmbeddableFactory,
} from '../../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public';
/**
* interfaces are not allowed to specify a sub-set of the required types until
* https://github.com/microsoft/TypeScript/issues/15300 is fixed so we use a type
* here instead
*/
// eslint-disable-next-line @typescript-eslint/prefer-interface
export type InheritedChildrenInput = {
timeRange: TimeRange;
id?: string;
};
interface ContainerTimeRangeInput extends ContainerInput<InheritedChildrenInput> {
timeRange: TimeRange;
}
const TIME_RANGE_CONTAINER = 'TIME_RANGE_CONTAINER';
export class TimeRangeContainer extends Container<
InheritedChildrenInput,
ContainerTimeRangeInput,
ContainerOutput
> {
public readonly type = TIME_RANGE_CONTAINER;
constructor(
initialInput: ContainerTimeRangeInput,
getFactory: GetEmbeddableFactory,
parent?: Container
) {
super(initialInput, { embeddableLoaded: {} }, getFactory, parent);
}
public getInheritedInput() {
return { timeRange: this.input.timeRange };
}
public render() {}
public reload() {}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { TimeRange } from '../../../../../../../../src/plugins/data/public';
import {
EmbeddableOutput,
Embeddable,
EmbeddableInput,
IContainer,
} from '../../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public';
interface EmbeddableTimeRangeInput extends EmbeddableInput {
timeRange: TimeRange;
}
export const TIME_RANGE_EMBEDDABLE = 'TIME_RANGE_EMBEDDABLE';
export class TimeRangeEmbeddable extends Embeddable<EmbeddableTimeRangeInput, EmbeddableOutput> {
public readonly type = TIME_RANGE_EMBEDDABLE;
constructor(initialInput: EmbeddableTimeRangeInput, parent?: IContainer) {
super(initialInput, {}, parent);
}
public render() {}
public reload() {}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { TimeRange } from '../../../../../../../../src/plugins/data/public';
import {
EmbeddableInput,
IContainer,
EmbeddableFactory,
} from '../../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public';
import { TIME_RANGE_EMBEDDABLE, TimeRangeEmbeddable } from './time_range_embeddable';
interface EmbeddableTimeRangeInput extends EmbeddableInput {
timeRange: TimeRange;
}
export class TimeRangeEmbeddableFactory extends EmbeddableFactory<EmbeddableTimeRangeInput> {
public readonly type = TIME_RANGE_EMBEDDABLE;
public isEditable() {
return true;
}
public async create(initialInput: EmbeddableTimeRangeInput, parent?: IContainer) {
return new TimeRangeEmbeddable(initialInput, parent);
}
public getDisplayName() {
return 'time range';
}
}

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { OverlayRef } from 'src/core/public';
export interface CommonlyUsedRange {
from: string;
to: string;
display: string;
}
export type OpenModal = (
modalChildren: React.ReactNode,
modalProps?: {
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}
) => OverlayRef;

View file

@ -191,3 +191,5 @@ export const mountHook = <Args extends {}, HookValue extends any>(
hookValueCallback,
};
};
export const nextTick = () => new Promise(res => process.nextTick(res));