Use embeddable v2 (#39126) (#41042)

* Final Embeddable API V2 PR

* fix: import discover embeddable scss file

* address code review comments

* Add a functional test that would have caught the bug... will look to add a unit version once I discover the error.

* Fix bug cause by async loading calls and changes to parent input while child is being created. added jest test

* remove outdated readme in dashboard folder

* need to always refresh dashboard container, not just when "dirty"

* add a wait, this issue started appearing right when I added this to the test

* Remove test that kills kibana ci so it's not a blocker. jest test was added for this scenario

* fix issues when panel is added then removed before it completes loading

* fix logic error with maps embeddable and isLayerTOCOpen
This commit is contained in:
Stacey Gammon 2019-07-12 16:28:29 -04:00 committed by GitHub
parent 9ba01c9ab2
commit 458de86dd8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
159 changed files with 1754 additions and 7233 deletions

View file

@ -3,4 +3,4 @@
// TODO: uncomment once the duplicate styles are removed from the dashboard app itself.
// MUST STAY AT THE BOTTOM BECAUSE OF DARK THEME IMPORTS
// @import './embeddable/index';
@import './embeddable/index';

View file

@ -16,8 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
import 'uiExports/embeddableActions';
import 'uiExports/embeddableFactories';
export {
DASHBOARD_GRID_COLUMN_COUNT,

View file

@ -44,7 +44,6 @@ import {
FilterableEmbeddable,
} from '../test_samples/embeddables/filterable_embeddable';
import { ERROR_EMBEDDABLE_TYPE } from '../embeddables/error_embeddable';
import { Filter, FilterStateStore } from '@kbn/es-query';
import { PanelNotFoundError } from './panel_not_found_error';
const embeddableFactories = new Map<string, EmbeddableFactory>();
@ -417,67 +416,6 @@ test('Test nested reactions', async done => {
embeddable.updateInput({ nameTitle: 'Dr.' });
});
test('Explicit embeddable input mapped to undefined will default to inherited', async () => {
const derivedFilter: Filter = {
$state: { store: FilterStateStore.APP_STATE },
meta: { disabled: false, alias: 'name', negate: false },
query: { match: {} },
};
const container = new FilterableContainer(
{ id: 'hello', panels: {}, filters: [derivedFilter] },
embeddableFactories
);
const embeddable = await container.addNewEmbeddable<
FilterableEmbeddableInput,
EmbeddableOutput,
FilterableEmbeddable
>(FILTERABLE_EMBEDDABLE, {});
if (isErrorEmbeddable(embeddable)) {
throw new Error('Error adding embeddable');
}
embeddable.updateInput({ filters: [] });
expect(container.getInputForChild<FilterableEmbeddableInput>(embeddable.id).filters).toEqual([]);
embeddable.updateInput({ filters: undefined });
expect(container.getInputForChild<FilterableEmbeddableInput>(embeddable.id).filters).toEqual([
derivedFilter,
]);
});
test('Explicit embeddable input mapped to undefined with no inherited value will get passed to embeddable', async done => {
const container = new HelloWorldContainer({ id: 'hello', panels: {} }, embeddableFactories);
const embeddable = await container.addNewEmbeddable<
FilterableEmbeddableInput,
EmbeddableOutput,
FilterableEmbeddable
>(FILTERABLE_EMBEDDABLE, {});
if (isErrorEmbeddable(embeddable)) {
throw new Error('Error adding embeddable');
}
embeddable.updateInput({ filters: [] });
expect(container.getInputForChild<FilterableEmbeddableInput>(embeddable.id).filters).toEqual([]);
const subscription = embeddable
.getInput$()
.pipe(skip(1))
.subscribe(() => {
if (embeddable.getInput().filters === undefined) {
subscription.unsubscribe();
done();
}
});
embeddable.updateInput({ filters: undefined });
});
test('Panel removed from input state', async done => {
const container = new FilterableContainer(
{ id: 'hello', panels: {}, filters: [] },
@ -748,3 +686,47 @@ test('untilEmbeddableLoaded rejects with an error if child is subsequently remov
container.updateInput({ panels: {} });
});
test('adding a panel then subsequently removing it before its loaded removes the panel', async done => {
embeddableFactories.clear();
embeddableFactories.set(
CONTACT_CARD_EMBEDDABLE,
new SlowContactCardEmbeddableFactory({ loadTickCount: 1 })
);
const container = new HelloWorldContainer(
{
id: 'hello',
panels: {
'123': {
explicitInput: { id: '123', firstName: 'Sam', lastName: 'Tarley' },
type: CONTACT_CARD_EMBEDDABLE,
},
},
},
embeddableFactories
);
// Final state should be that the panel is removed.
Rx.merge(container.getInput$(), container.getOutput$()).subscribe(() => {
if (
container.getInput().panels['123'] === undefined &&
container.getOutput().embeddableLoaded['123'] === undefined &&
container.getInput().panels['456'] !== undefined &&
container.getOutput().embeddableLoaded['456'] === true
) {
done();
}
});
container.updateInput({ panels: {} });
container.updateInput({
panels: {
'456': {
explicitInput: { id: '456', firstName: 'a', lastName: 'b' },
type: CONTACT_CARD_EMBEDDABLE,
},
},
});
});

View file

@ -322,14 +322,13 @@ export abstract class Container<
// switch over to inline creation we can probably clean this up, and force EmbeddableFactory.create to always
// return an embeddable, or throw an error.
if (embeddable) {
// The factory creation process may ask the user for input to update or override any input coming
// from the container.
const input = embeddable.getInput();
const newOrChangedInput = getKeys(input)
.filter(key => input[key] !== inputForChild[key])
.reduce((res, key) => Object.assign(res, { [key]: input[key] }), {});
// make sure the panel wasn't removed in the mean time, since the embeddable creation is async
if (!this.input.panels[panel.explicitInput.id]) {
embeddable.destroy();
return;
}
if (embeddable.getOutput().savedObjectId || Object.keys(newOrChangedInput).length > 0) {
if (embeddable.getOutput().savedObjectId) {
this.updateInput({
panels: {
...this.input.panels,
@ -340,7 +339,6 @@ export abstract class Container<
: undefined),
explicitInput: {
...this.input.panels[panel.explicitInput.id].explicitInput,
...newOrChangedInput,
},
},
},

View file

@ -24,9 +24,9 @@ import React from 'react';
import { EuiLoadingChart } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { ErrorEmbeddable, IEmbeddable } from 'plugins/embeddable_api';
import { Subscription } from 'rxjs';
import { ErrorEmbeddable, IEmbeddable } from '../embeddables';
import { EmbeddablePanel } from '../panel';
import { IContainer } from './i_container';

View file

@ -0,0 +1,143 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 '../ui_capabilities.test.mocks';
import '../../../../../core/public/ui_new_platform.test.mocks';
import { skip } from 'rxjs/operators';
import {
CONTACT_CARD_EMBEDDABLE,
HelloWorldContainer,
FilterableContainer,
FILTERABLE_EMBEDDABLE,
FilterableEmbeddableFactory,
ContactCardEmbeddable,
SlowContactCardEmbeddableFactory,
HELLO_WORLD_EMBEDDABLE_TYPE,
HelloWorldEmbeddableFactory,
} from '../test_samples/index';
import { isErrorEmbeddable, EmbeddableOutput, EmbeddableFactory } from '../embeddables';
import {
FilterableEmbeddableInput,
FilterableEmbeddable,
} from '../test_samples/embeddables/filterable_embeddable';
import { Filter, FilterStateStore } from '@kbn/es-query';
jest.mock('ui/new_platform');
const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
embeddableFactories.set(
CONTACT_CARD_EMBEDDABLE,
new SlowContactCardEmbeddableFactory({ loadTickCount: 2 })
);
embeddableFactories.set(HELLO_WORLD_EMBEDDABLE_TYPE, new HelloWorldEmbeddableFactory());
test('Explicit embeddable input mapped to undefined will default to inherited', async () => {
const derivedFilter: Filter = {
$state: { store: FilterStateStore.APP_STATE },
meta: { disabled: false, alias: 'name', negate: false },
query: { match: {} },
};
const container = new FilterableContainer(
{ id: 'hello', panels: {}, filters: [derivedFilter] },
embeddableFactories
);
const embeddable = await container.addNewEmbeddable<
FilterableEmbeddableInput,
EmbeddableOutput,
FilterableEmbeddable
>(FILTERABLE_EMBEDDABLE, {});
if (isErrorEmbeddable(embeddable)) {
throw new Error('Error adding embeddable');
}
embeddable.updateInput({ filters: [] });
expect(container.getInputForChild<FilterableEmbeddableInput>(embeddable.id).filters).toEqual([]);
embeddable.updateInput({ filters: undefined });
expect(container.getInputForChild<FilterableEmbeddableInput>(embeddable.id).filters).toEqual([
derivedFilter,
]);
});
test('Explicit embeddable input mapped to undefined with no inherited value will get passed to embeddable', async done => {
const container = new HelloWorldContainer({ id: 'hello', panels: {} }, embeddableFactories);
const embeddable = await container.addNewEmbeddable<
FilterableEmbeddableInput,
EmbeddableOutput,
FilterableEmbeddable
>(FILTERABLE_EMBEDDABLE, {});
if (isErrorEmbeddable(embeddable)) {
throw new Error('Error adding embeddable');
}
embeddable.updateInput({ filters: [] });
expect(container.getInputForChild<FilterableEmbeddableInput>(embeddable.id).filters).toEqual([]);
const subscription = embeddable
.getInput$()
.pipe(skip(1))
.subscribe(() => {
if (embeddable.getInput().filters === undefined) {
subscription.unsubscribe();
done();
}
});
embeddable.updateInput({ filters: undefined });
});
// The goal is to make sure that if the container input changes after `onPanelAdded` is called
// but before the embeddable factory returns the embeddable, that the `inheritedChildInput` and
// embeddable input comparisons won't cause explicit input to be set when it shouldn't.
test('Explicit input tests in async situations', (done: () => void) => {
const container = new HelloWorldContainer(
{
id: 'hello',
panels: {
'123': {
explicitInput: { firstName: 'Sam', id: '123' },
type: CONTACT_CARD_EMBEDDABLE,
},
},
lastName: 'bar',
},
embeddableFactories
);
container.updateInput({ lastName: 'lolol' });
const subscription = container.getOutput$().subscribe(() => {
if (container.getOutput().embeddableLoaded['123']) {
expect(container.getInput().panels['123'].explicitInput.lastName).toBeUndefined();
const embeddable = container.getChild<ContactCardEmbeddable>('123');
expect(embeddable).toBeDefined();
expect(embeddable.getInput().lastName).toBe('lolol');
subscription.unsubscribe();
done();
}
});
});

View file

@ -104,12 +104,10 @@ function convertPanelActionToContextMenuItem({
'data-test-subj': `embeddablePanelAction-${action.id}`,
};
if (action.getHref(actionContext) === undefined) {
menuPanelItem.onClick = () => {
action.execute(actionContext);
closeMenu();
};
}
menuPanelItem.onClick = () => {
action.execute(actionContext);
closeMenu();
};
if (action.getHref(actionContext)) {
menuPanelItem.href = action.getHref(actionContext);

View file

@ -65,6 +65,9 @@ export abstract class Embeddable<
if (parent) {
this.parentSubscription = Rx.merge(parent.getInput$(), parent.getOutput$()).subscribe(() => {
// Make sure this panel hasn't been removed immediately after it was added, but before it finished loading.
if (!parent.getInput().panels[this.id]) return;
const newInput = parent.getInputForChild<TEmbeddableInput>(this.id);
this.onResetInput(newInput);
});

View file

@ -19,14 +19,13 @@
import { Action } from './actions';
import { IEmbeddable } from './embeddables';
import { IContainer } from './containers';
import { Trigger } from './types';
export async function getActionsForTrigger(
actionRegistry: Map<string, Action>,
triggerRegistry: Map<string, Trigger>,
triggerId: string,
context: { embeddable: IEmbeddable; container?: IContainer }
context: { embeddable: IEmbeddable; triggerContext?: { [key: string]: unknown } }
) {
const trigger = triggerRegistry.get(triggerId);

View file

@ -48,7 +48,7 @@ export class ContactCardEmbeddableFactory extends EmbeddableFactory<ContactCardE
modalSession.close();
resolve(undefined);
}}
onCreate={(input: { firstName: string; lastName: string }) => {
onCreate={(input: { firstName: string; lastName?: string }) => {
modalSession.close();
resolve(input);
}}

View file

@ -31,7 +31,7 @@ import {
import React, { Component } from 'react';
export interface ContactCardInitializerProps {
onCreate: (name: { lastName: string; firstName: string }) => void;
onCreate: (name: { lastName?: string; firstName: string }) => void;
onCancel: () => void;
}
@ -67,6 +67,7 @@ export class ContactCardInitializer extends Component<ContactCardInitializerProp
<EuiFieldText
name="popfirst"
value={this.state.lastName}
placeholder="optional"
onChange={e => this.setState({ lastName: e.target.value })}
/>
</EuiFormRow>
@ -77,12 +78,12 @@ export class ContactCardInitializer extends Component<ContactCardInitializerProp
<EuiButtonEmpty onClick={this.props.onCancel}>Cancel</EuiButtonEmpty>
<EuiButton
isDisabled={!this.state.lastName || !this.state.firstName}
isDisabled={!this.state.firstName}
onClick={() => {
if (this.state.lastName && this.state.firstName) {
if (this.state.firstName) {
this.props.onCreate({
firstName: this.state.firstName,
lastName: this.state.lastName,
...(this.state.lastName ? { lastName: this.state.lastName } : {}),
});
}
}}

View file

@ -37,7 +37,11 @@ type InheritedInput = {
lastName: string;
};
export class HelloWorldContainer extends Container<InheritedInput> {
interface HelloWorldContainerInput extends ContainerInput {
lastName?: string;
}
export class HelloWorldContainer extends Container<InheritedInput, HelloWorldContainerInput> {
public readonly type = HELLO_WORLD_CONTAINER;
constructor(input: ContainerInput, embeddableFactories: Map<string, EmbeddableFactory>) {
@ -48,7 +52,7 @@ export class HelloWorldContainer extends Container<InheritedInput> {
return {
id,
viewMode: this.input.viewMode || ViewMode.EDIT,
lastName: 'foo',
lastName: this.input.lastName || 'foo',
};
}

View file

@ -34,6 +34,7 @@ export async function executeTriggerActions(
) {
const actions = await getActionsForTrigger(actionRegistry, triggerRegistry, triggerId, {
embeddable,
triggerContext,
});
if (actions.length > 1) {

View file

@ -1,173 +0,0 @@
## Dashboard State Walkthrough
A high level walk through of types of dashboard state and how dashboard and
embeddables communicate with each other. An "embeddable" is anything that can be dropped
on a dashboard. It is a pluggable system so new embeddables can be created, as
long as they adhere to the communication protocol. Currently the only two embeddable types
are saved searches and visualizations. A truly pluggable embeddable system is still a
WIP - as the UI currently only supports adding visualizations and saved searches to a dashboard.
### Types of state
**Embeddable metadata** - Data the embeddable instance gives the dashboard once as a
return value of EmbeddableFactory.create. Data such as edit link and title go in
here. We may later decide to move some of this data to the dynamic embeddable state
(for instance, if we implemented inline editing a title could change), but we keep the
separation because it allows us to force some consistency in our UX. For example, we may
not want a visualization to all of a sudden go from supporting drilldown links to
not supporting it, as it would mean disappearing panel context menu items.
**Embeddable state** - Data the embeddable gives the dashboard throughout it's lifecycle as
things update and the user interacts with it. This is communicated to the dashboard via the
function `onEmbeddableStateChanged` that is passed in to the `EmbeddableFactory.create` call.
**Container state** - Data the dashboard gives to the embeddable throughout it's lifecycle
as things update and the user interacts with Kibana. This is communicated to the embeddable via
the function `onContainerStateChanged` which is returned from the `EmbeddableFactory.create` call
**Container metadata** - State that only needs to be given to the embeddable once,
and does not change thereafter. This will contain data given to dashboard when a new embeddable is
added to a dashboard. Currently, this is really only the saved object id.
**Dashboard storage data** - Data persisted in elasticsearch. Should not be coupled to the redux tree.
**Dashboard redux tree** - State stored in the dashboard redux tree.
**EmbeddableFactory metadata** - Data that is true for all instances of the given type and does not change.
I'm not sure if *any* data belongs here but we talked about it, so keeping it in here. We thought initially
it could be supportsDrillDowns but for type visualizations, for example, this depends on the visualization
"subtype" (e.g. input controls vs line chart).
### Dashboard/Embeddable communication psuedocode
```js
dashboard_panel.js:
// The Dashbaord Panel react component handles the lifecycle of the
// embeddable as well as rendering. If we ever need access to the embeddable
// object externally, we may need to rethink this.
class EmbeddableViewport extends Component {
componentDidMount() {
if (!initialized) {
this.props.embeddableFactory.create(panelMetadata, this.props.embeddableStateChanged)
.then(embeddable => {
this.embeddable = embeddable;
this.embeddable.onContainerStateChanged(this.props.containerState);
this.embeddable.render(this.panelElement);
}
}
}
componentWillUnmount() {
this.embeddable.destroy();
}
// We let react/redux tell us when to tell the embeddable that some container
// state changed.
componentDidUpdate(prevProps) {
if (this.embeddable && !_.isEqual(prevProps.containerState, this.props.containerState)) {
this.embeddable.onContainerStateChanged(this.props.containerState);
}
}
render() {
return (
<div>
<PanelHeaderContainer embeddable={this.embeddable} />
<div ref={panelElement => this.panelElement = panelElement}></div>
</div>
);
}
}
------
actions/embeddable.js:
/**
* This is the main communication point for embeddables to send state
* changes to dashboard.
* @param {EmbeddableState} newEmbeddableState
*/
function onEmbeddableStateChanged(newEmbeddableState) {
// Map embeddable state properties into our redux tree.
}
```
### Container state
State communicated to the embeddable.
```
{
// Contains per panel customizations like sort, columns, and color choices.
// This shape is defined by the embeddable. Dashboard stores it and tracks updates
// to it.
embeddableCustomization: Object,
hidePanelTitles: boolean,
title: string,
// TODO:
filters: FilterObject,
timeRange: TimeRangeObject,
}
```
### Container metadata
```
{
// Any shape needed to initialize an embeddable. Gets saved to storage. Created when
// a new embeddable is added. Currently just includes the object id.
embeddableConfiguration: Object,
}
```
### Embeddable Metadata
```
{
// Index patterns used by this embeddable. This information is currently
// used by the filter on a dashboard for which fields to show in the
// dropdown. Otherwise we'd have to show all fields over all indexes and
// if no embeddables use those index patterns, there really is no point
// to filtering on them.
indexPatterns: Array.<IndexPatterns>,
// Dashboard navigates to this url when the user clicks 'Edit visualization'
// in the panel context menu.
editUrl: string,
// Title to be shown in the panel. Can be overridden at the panel level.
title: string,
// TODO:
// If this is true, then dashboard will show a "configure drill down
// links" menu option in the context menu for the panel.
supportsDrillDowns: boolean,
}
```
### Embeddable State
Embeddable state is the data that the embeddable gives dashboard when something changes
```
{
// This will contain per panel embeddable state, such as pie colors and saved search columns.
embeddableCustomization: Object,
// If a filter action was initiated by a user action (e.g. clicking on a bar chart)
// This is how dashboard will know and update itself to match.
stagedFilters: FilterObject,
// TODO: More possible options to go in here:
error: Error,
isLoading: boolean,
renderComplete: boolean,
appliedtimeRange: TimeRangeObject,
stagedTimeRange: TimeRangeObject,
// This information will need to be exposed so other plugins (e.g. ML)
// can register panel actions.
esQuery: Object,
// Currently applied filters
appliedFilters: FilterObject,
}
```

View file

@ -3,9 +3,10 @@
/**
* Needs to correspond with the react root nested inside angular.
*/
dashboard-viewport-provider {
#dashboardViewport {
flex: 1;
display: flex;
flex-direction: column;
[data-reactroot] {
flex: 1;
}

View file

@ -16,6 +16,3 @@
// dshChart__legend-isLoading
@import './dashboard_app';
@import './grid/index';
@import './panel/index';
@import './viewport/index';

View file

@ -1,60 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { store } from '../../store';
import {
clearStagedFilters,
embeddableIsInitialized,
embeddableIsInitializing,
setStagedFilter,
} from '../actions';
import { getStagedFilters } from '../../selectors';
beforeAll(() => {
store.dispatch(embeddableIsInitializing('foo1'));
store.dispatch(embeddableIsInitializing('foo2'));
store.dispatch(embeddableIsInitialized({ panelId: 'foo1', metadata: {} }));
store.dispatch(embeddableIsInitialized({ panelId: 'foo2', metadata: {} }));
});
describe('staged filters', () => {
test('getStagedFilters initially is empty', () => {
const stagedFilters = getStagedFilters(store.getState());
expect(stagedFilters.length).toBe(0);
});
test('can set a staged filter', () => {
store.dispatch(setStagedFilter({ stagedFilter: ['imafilter'], panelId: 'foo1' }));
const stagedFilters = getStagedFilters(store.getState());
expect(stagedFilters.length).toBe(1);
});
test('getStagedFilters returns filters for all embeddables', () => {
store.dispatch(setStagedFilter({ stagedFilter: ['imafilter'], panelId: 'foo2' }));
const stagedFilters = getStagedFilters(store.getState());
expect(stagedFilters.length).toBe(2);
});
test('clearStagedFilters clears all filters', () => {
store.dispatch(clearStagedFilters());
const stagedFilters = getStagedFilters(store.getState());
expect(stagedFilters.length).toBe(0);
});
});

View file

@ -1,128 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/
/* eslint-disable @typescript-eslint/no-empty-interface */
import _ from 'lodash';
import { createAction } from 'redux-actions';
import { EmbeddableMetadata, EmbeddableState } from 'ui/embeddable';
import { getEmbeddableCustomization, getPanel } from '../../selectors';
import { PanelId } from '../selectors';
import { updatePanel } from './panels';
import { SavedDashboardPanel } from '../types';
import { KibanaAction, KibanaThunk } from '../../selectors/types';
export enum EmbeddableActionTypeKeys {
EMBEDDABLE_IS_INITIALIZING = 'EMBEDDABLE_IS_INITIALIZING',
EMBEDDABLE_IS_INITIALIZED = 'EMBEDDABLE_IS_INITIALIZED',
SET_STAGED_FILTER = 'SET_STAGED_FILTER',
CLEAR_STAGED_FILTERS = 'CLEAR_STAGED_FILTERS',
EMBEDDABLE_ERROR = 'EMBEDDABLE_ERROR',
REQUEST_RELOAD = 'REQUEST_RELOAD',
}
export interface EmbeddableIsInitializingAction
extends KibanaAction<EmbeddableActionTypeKeys.EMBEDDABLE_IS_INITIALIZING, PanelId> {}
export interface EmbeddableIsInitializedActionPayload {
panelId: PanelId;
metadata: EmbeddableMetadata;
}
export interface EmbeddableIsInitializedAction
extends KibanaAction<
EmbeddableActionTypeKeys.EMBEDDABLE_IS_INITIALIZED,
EmbeddableIsInitializedActionPayload
> {}
export interface SetStagedFilterActionPayload {
panelId: PanelId;
stagedFilter: object;
}
export interface SetStagedFilterAction
extends KibanaAction<EmbeddableActionTypeKeys.SET_STAGED_FILTER, SetStagedFilterActionPayload> {}
export interface ClearStagedFiltersAction
extends KibanaAction<EmbeddableActionTypeKeys.CLEAR_STAGED_FILTERS, undefined> {}
export interface RequestReload
extends KibanaAction<EmbeddableActionTypeKeys.REQUEST_RELOAD, undefined> {}
export interface EmbeddableErrorActionPayload {
error: string | object;
panelId: PanelId;
}
export interface EmbeddableErrorAction
extends KibanaAction<EmbeddableActionTypeKeys.EMBEDDABLE_ERROR, EmbeddableErrorActionPayload> {}
export type EmbeddableActions =
| EmbeddableIsInitializingAction
| EmbeddableIsInitializedAction
| ClearStagedFiltersAction
| SetStagedFilterAction
| EmbeddableErrorAction;
export const embeddableIsInitializing = createAction<PanelId>(
EmbeddableActionTypeKeys.EMBEDDABLE_IS_INITIALIZING
);
export const embeddableIsInitialized = createAction<EmbeddableIsInitializedActionPayload>(
EmbeddableActionTypeKeys.EMBEDDABLE_IS_INITIALIZED
);
export const setStagedFilter = createAction<SetStagedFilterActionPayload>(
EmbeddableActionTypeKeys.SET_STAGED_FILTER
);
export const clearStagedFilters = createAction(EmbeddableActionTypeKeys.CLEAR_STAGED_FILTERS);
export const embeddableError = createAction<EmbeddableErrorActionPayload>(
EmbeddableActionTypeKeys.EMBEDDABLE_ERROR
);
export const requestReload = createAction(EmbeddableActionTypeKeys.REQUEST_RELOAD);
/**
* The main point of communication from the embeddable to the dashboard. Any time state in the embeddable
* changes, this function will be called. The data is then extracted from EmbeddableState and stored in
* redux so the appropriate actions are taken and UI updated.
*
* @param changeData.panelId - the id of the panel whose state has changed.
* @param changeData.embeddableState - the new state of the embeddable.
*/
export function embeddableStateChanged(changeData: {
panelId: PanelId;
embeddableState: EmbeddableState;
}): KibanaThunk {
const { panelId, embeddableState } = changeData;
return (dispatch, getState) => {
// Translate embeddableState to things redux cares about.
const customization = getEmbeddableCustomization(getState(), panelId);
if (!_.isEqual(embeddableState.customization, customization)) {
const originalPanelState = getPanel(getState(), panelId);
const newPanelState: SavedDashboardPanel = {
...originalPanelState,
embeddableConfig: _.cloneDeep(embeddableState.customization) || {},
};
dispatch(updatePanel(newPanelState));
}
if (embeddableState.stagedFilter) {
dispatch(setStagedFilter({ stagedFilter: embeddableState.stagedFilter, panelId }));
}
};
}

View file

@ -1,23 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/
export * from './view';
export * from './panels';
export * from './embeddables';
export * from './metadata';

View file

@ -1,43 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/
/* eslint-disable @typescript-eslint/no-empty-interface */
import { createAction } from 'redux-actions';
import { KibanaAction } from '../../selectors/types';
export enum MetadataActionTypeKeys {
UPDATE_DESCRIPTION = 'UPDATE_DESCRIPTION',
UPDATE_TITLE = 'UPDATE_TITLE',
}
export type UpdateTitleActionPayload = string;
export interface UpdateTitleAction
extends KibanaAction<MetadataActionTypeKeys.UPDATE_TITLE, UpdateTitleActionPayload> {}
export type UpdateDescriptionActionPayload = string;
export interface UpdateDescriptionAction
extends KibanaAction<MetadataActionTypeKeys.UPDATE_DESCRIPTION, UpdateDescriptionActionPayload> {}
export type MetadataActions = UpdateDescriptionAction | UpdateTitleAction;
export const updateDescription = createAction<string>(MetadataActionTypeKeys.UPDATE_DESCRIPTION);
export const updateTitle = createAction<string>(MetadataActionTypeKeys.UPDATE_TITLE);

View file

@ -1,74 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/
/* eslint-disable @typescript-eslint/no-empty-interface */
import { createAction } from 'redux-actions';
import { KibanaAction } from '../../selectors/types';
import { PanelId } from '../selectors';
import { SavedDashboardPanel, SavedDashboardPanelMap } from '../types';
export enum PanelActionTypeKeys {
DELETE_PANEL = 'DELETE_PANEL',
UPDATE_PANEL = 'UPDATE_PANEL',
RESET_PANEL_TITLE = 'RESET_PANEL_TITLE',
SET_PANEL_TITLE = 'SET_PANEL_TITLE',
UPDATE_PANELS = 'UPDATE_PANELS',
SET_PANELS = 'SET_PANELS',
}
export interface DeletePanelAction
extends KibanaAction<PanelActionTypeKeys.DELETE_PANEL, PanelId> {}
export interface UpdatePanelAction
extends KibanaAction<PanelActionTypeKeys.UPDATE_PANEL, SavedDashboardPanel> {}
export interface UpdatePanelsAction
extends KibanaAction<PanelActionTypeKeys.UPDATE_PANELS, SavedDashboardPanelMap> {}
export interface ResetPanelTitleAction
extends KibanaAction<PanelActionTypeKeys.RESET_PANEL_TITLE, PanelId> {}
export interface SetPanelTitleActionPayload {
panelId: PanelId;
title?: string;
}
export interface SetPanelTitleAction
extends KibanaAction<PanelActionTypeKeys.SET_PANEL_TITLE, SetPanelTitleActionPayload> {}
export interface SetPanelsAction
extends KibanaAction<PanelActionTypeKeys.SET_PANELS, SavedDashboardPanelMap> {}
export type PanelActions =
| DeletePanelAction
| UpdatePanelAction
| ResetPanelTitleAction
| UpdatePanelsAction
| SetPanelTitleAction
| SetPanelsAction;
export const deletePanel = createAction<PanelId>(PanelActionTypeKeys.DELETE_PANEL);
export const updatePanel = createAction<SavedDashboardPanel>(PanelActionTypeKeys.UPDATE_PANEL);
export const resetPanelTitle = createAction<PanelId>(PanelActionTypeKeys.RESET_PANEL_TITLE);
export const setPanelTitle = createAction<SetPanelTitleActionPayload>(
PanelActionTypeKeys.SET_PANEL_TITLE
);
export const updatePanels = createAction<SavedDashboardPanelMap>(PanelActionTypeKeys.UPDATE_PANELS);
export const setPanels = createAction<SavedDashboardPanelMap>(PanelActionTypeKeys.SET_PANELS);

View file

@ -1,114 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/
/* eslint-disable @typescript-eslint/no-empty-interface */
import { createAction } from 'redux-actions';
import { RefreshInterval } from 'ui/timefilter/timefilter';
import { TimeRange } from 'ui/timefilter/time_history';
import { Filter } from '@kbn/es-query';
import { Query } from 'src/legacy/core_plugins/data/public';
import { KibanaAction } from '../../selectors/types';
import { DashboardViewMode } from '../dashboard_view_mode';
import { PanelId } from '../selectors';
export enum ViewActionTypeKeys {
UPDATE_VIEW_MODE = 'UPDATE_VIEW_MODE',
SET_VISIBLE_CONTEXT_MENU_PANEL_ID = 'SET_VISIBLE_CONTEXT_MENU_PANEL_ID',
MAXIMIZE_PANEL = 'MAXIMIZE_PANEL',
MINIMIZE_PANEL = 'MINIMIZE_PANEL',
UPDATE_IS_FULL_SCREEN_MODE = 'UPDATE_IS_FULL_SCREEN_MODE',
UPDATE_USE_MARGINS = 'UPDATE_USE_MARGINS',
UPDATE_HIDE_PANEL_TITLES = 'UPDATE_HIDE_PANEL_TITLES',
UPDATE_TIME_RANGE = 'UPDATE_TIME_RANGE',
UPDATE_REFRESH_CONFIG = 'UPDATE_REFRESH_CONFIG',
UPDATE_FILTERS = 'UPDATE_FILTERS',
UPDATE_QUERY = 'UPDATE_QUERY',
CLOSE_CONTEXT_MENU = 'CLOSE_CONTEXT_MENU',
}
export interface UpdateViewModeAction
extends KibanaAction<ViewActionTypeKeys.UPDATE_VIEW_MODE, DashboardViewMode> {}
export interface SetVisibleContextMenuPanelIdAction
extends KibanaAction<ViewActionTypeKeys.SET_VISIBLE_CONTEXT_MENU_PANEL_ID, PanelId> {}
export interface CloseContextMenuAction
extends KibanaAction<ViewActionTypeKeys.CLOSE_CONTEXT_MENU, undefined> {}
export interface MaximizePanelAction
extends KibanaAction<ViewActionTypeKeys.MAXIMIZE_PANEL, PanelId> {}
export interface MinimizePanelAction
extends KibanaAction<ViewActionTypeKeys.MINIMIZE_PANEL, undefined> {}
export interface UpdateIsFullScreenModeAction
extends KibanaAction<ViewActionTypeKeys.UPDATE_IS_FULL_SCREEN_MODE, boolean> {}
export interface UpdateUseMarginsAction
extends KibanaAction<ViewActionTypeKeys.UPDATE_USE_MARGINS, boolean> {}
export interface UpdateHidePanelTitlesAction
extends KibanaAction<ViewActionTypeKeys.UPDATE_HIDE_PANEL_TITLES, boolean> {}
export interface UpdateTimeRangeAction
extends KibanaAction<ViewActionTypeKeys.UPDATE_TIME_RANGE, TimeRange> {}
export interface UpdateRefreshConfigAction
extends KibanaAction<ViewActionTypeKeys.UPDATE_REFRESH_CONFIG, RefreshInterval> {}
export interface UpdateFiltersAction
extends KibanaAction<ViewActionTypeKeys.UPDATE_FILTERS, Filter[]> {}
export interface UpdateQueryAction extends KibanaAction<ViewActionTypeKeys.UPDATE_QUERY, Query> {}
export type ViewActions =
| UpdateViewModeAction
| SetVisibleContextMenuPanelIdAction
| CloseContextMenuAction
| MaximizePanelAction
| MinimizePanelAction
| UpdateIsFullScreenModeAction
| UpdateUseMarginsAction
| UpdateHidePanelTitlesAction
| UpdateTimeRangeAction
| UpdateRefreshConfigAction
| UpdateFiltersAction
| UpdateQueryAction;
export const updateViewMode = createAction<string>(ViewActionTypeKeys.UPDATE_VIEW_MODE);
export const closeContextMenu = createAction(ViewActionTypeKeys.CLOSE_CONTEXT_MENU);
export const setVisibleContextMenuPanelId = createAction<PanelId>(
ViewActionTypeKeys.SET_VISIBLE_CONTEXT_MENU_PANEL_ID
);
export const maximizePanel = createAction<PanelId>(ViewActionTypeKeys.MAXIMIZE_PANEL);
export const minimizePanel = createAction(ViewActionTypeKeys.MINIMIZE_PANEL);
export const updateIsFullScreenMode = createAction<boolean>(
ViewActionTypeKeys.UPDATE_IS_FULL_SCREEN_MODE
);
export const updateUseMargins = createAction<boolean>(ViewActionTypeKeys.UPDATE_USE_MARGINS);
export const updateHidePanelTitles = createAction<boolean>(
ViewActionTypeKeys.UPDATE_HIDE_PANEL_TITLES
);
export const updateTimeRange = createAction<TimeRange>(ViewActionTypeKeys.UPDATE_TIME_RANGE);
export const updateRefreshConfig = createAction<RefreshInterval>(
ViewActionTypeKeys.UPDATE_REFRESH_CONFIG
);
export const updateFilters = createAction<Filter[]>(ViewActionTypeKeys.UPDATE_FILTERS);
export const updateQuery = createAction<Query | string>(ViewActionTypeKeys.UPDATE_QUERY);

View file

@ -118,9 +118,6 @@
</div>
</div>
<dashboard-viewport-provider
get-embeddable-factory="getEmbeddableFactory"
>
</dashboard-viewport-provider>
<div id="dashboardViewport"></div>
</dashboard-app>

View file

@ -22,24 +22,12 @@ import _ from 'lodash';
// @ts-ignore
import { uiModules } from 'ui/modules';
import { IInjector } from 'ui/chrome';
import { wrapInI18nContext } from 'ui/i18n';
// @ts-ignore
import { ConfirmationButtonTypes } from 'ui/modals/confirm_modal';
// @ts-ignore
import { docTitle } from 'ui/doc_title';
// @ts-ignore
import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal';
// @ts-ignore
import * as filterActions from 'plugins/kibana/discover/doc_table/actions/filter';
// @ts-ignore
import { getFilterGenerator } from 'ui/filter_manager';
import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter';
import { EmbeddableFactory } from 'ui/embeddable';
import {
AppStateClass as TAppStateClass,
@ -51,16 +39,13 @@ import { Filter } from '@kbn/es-query';
import { TimeRange } from 'ui/timefilter/time_history';
import { IndexPattern } from 'ui/index_patterns';
import { IPrivate } from 'ui/private';
import { StaticIndexPattern, Query } from 'src/legacy/core_plugins/data/public';
import moment from 'moment';
import { StaticIndexPattern, Query } from '../../../data/public';
import { ViewMode } from '../../../embeddable_api/public';
import { SavedObjectDashboard } from './saved_dashboard/saved_dashboard';
import { DashboardAppState, SavedDashboardPanel, ConfirmModalFn, AddFilterFn } from './types';
import { DashboardAppState, SavedDashboardPanel, ConfirmModalFn } from './types';
// @ts-ignore -- going away soon
import { DashboardViewportProvider } from './viewport/dashboard_viewport_provider';
import { DashboardStateManager } from './dashboard_state_manager';
import { DashboardViewMode } from './dashboard_view_mode';
import { DashboardAppController } from './dashboard_app_controller';
export interface DashboardAppScope extends ng.IScope {
@ -68,7 +53,7 @@ export interface DashboardAppScope extends ng.IScope {
appState: TAppState;
screenTitle: string;
model: {
query: Query | string;
query: Query;
filters: Filter[];
timeRestore: boolean;
title: string;
@ -82,7 +67,7 @@ export interface DashboardAppScope extends ng.IScope {
panels: SavedDashboardPanel[];
indexPatterns: StaticIndexPattern[];
$evalAsync: any;
dashboardViewMode: DashboardViewMode;
dashboardViewMode: ViewMode;
expandedPanel?: string;
getShouldShowEditHelp: () => boolean;
getShouldShowViewHelp: () => boolean;
@ -104,9 +89,6 @@ export interface DashboardAppScope extends ng.IScope {
kbnTopNav: any;
enterEditMode: () => void;
$listen: any;
getEmbeddableFactory: (type: string) => EmbeddableFactory;
getDashboardState: () => DashboardStateManager;
refresh: () => void;
}
const app = uiModules.get('app/dashboard', [
@ -117,10 +99,6 @@ const app = uiModules.get('app/dashboard', [
'kibana/config',
]);
app.directive('dashboardViewportProvider', function(reactDirective: any) {
return reactDirective(wrapInI18nContext(DashboardViewportProvider));
});
app.directive('dashboardApp', function($injector: IInjector) {
const AppState = $injector.get<TAppStateClass<DashboardAppState>>('AppState');
const kbnUrl = $injector.get<KbnUrl>('kbnUrl');
@ -130,12 +108,6 @@ app.directive('dashboardApp', function($injector: IInjector) {
const Private = $injector.get<IPrivate>('Private');
const queryFilter = Private(FilterBarQueryFilterProvider);
const filterGen = getFilterGenerator(queryFilter);
const addFilter: AddFilterFn = ({ field, value, operator, index }, appState: TAppState) => {
filterActions.addFilter(field, value, operator, index, appState, filterGen);
};
const indexPatterns = $injector.get<{
getDefault: () => Promise<IndexPattern>;
}>('indexPatterns');
@ -145,7 +117,6 @@ app.directive('dashboardApp', function($injector: IInjector) {
controllerAs: 'dashboardApp',
controller: (
$scope: DashboardAppScope,
$rootScope: ng.IRootScopeService,
$route: any,
$routeParams: {
id?: string;
@ -156,11 +127,12 @@ app.directive('dashboardApp', function($injector: IInjector) {
dashboardConfig: {
getHideWriteControls: () => boolean;
},
localStorage: WindowLocalStorage
localStorage: {
get: (prop: string) => unknown;
}
) =>
new DashboardAppController({
$route,
$rootScope,
$scope,
$routeParams,
getAppState,
@ -172,7 +144,6 @@ app.directive('dashboardApp', function($injector: IInjector) {
indexPatterns,
config,
confirmModal,
addFilter,
courier,
}),
};

View file

@ -37,9 +37,6 @@ import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal';
import { showShareContextMenu, ShareContextMenuExtensionsRegistryProvider } from 'ui/share';
import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query';
import { EmbeddableFactoriesRegistryProvider } from 'ui/embeddable/embeddable_factories_registry';
import { ContextMenuActionsRegistryProvider } from 'ui/embeddable';
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
import { timefilter } from 'ui/timefilter';
import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing/get_unhashable_states_provider';
@ -55,17 +52,24 @@ import { IndexPattern } from 'ui/index_patterns';
import { IPrivate } from 'ui/private';
import { Query } from 'src/legacy/core_plugins/data/public';
import { SaveOptions } from 'ui/saved_objects/saved_object';
import { Subscription } from 'rxjs';
import {
DashboardAppState,
EmbeddableFactoryRegistry,
NavAction,
ConfirmModalFn,
AddFilterFn,
} from './types';
DashboardContainer,
DASHBOARD_CONTAINER_TYPE,
DashboardContainerFactory,
DashboardContainerInput,
DashboardPanelState,
} from '../../../dashboard_embeddable_container/public';
import {
isErrorEmbeddable,
embeddableFactories,
ErrorEmbeddable,
ViewMode,
openAddPanelFlyout,
} from '../../../embeddable_api/public';
import { DashboardAppState, NavAction, ConfirmModalFn, SavedDashboardPanel } from './types';
import { showNewVisModal } from '../visualize/wizard';
import { showOptionsPopover } from './top_nav/show_options_popover';
import { showAddPanel } from './top_nav/show_add_panel';
import { DashboardSaveModal } from './top_nav/save_modal';
import { showCloneModal } from './top_nav/show_clone_modal';
import { saveDashboard } from './lib';
@ -73,10 +77,10 @@ import { DashboardStateManager } from './dashboard_state_manager';
import { DashboardConstants, createDashboardEditUrl } from './dashboard_constants';
import { getTopNavConfig } from './top_nav/get_top_nav_config';
import { TopNavIds } from './top_nav/top_nav_ids';
import { DashboardViewMode } from './dashboard_view_mode';
import { getDashboardTitle } from './dashboard_strings';
import { panelActionsStore } from './store/panel_actions_store';
import { DashboardAppScope } from './dashboard_app';
import { VISUALIZE_EMBEDDABLE_TYPE } from '../visualize/embeddable';
import { convertSavedDashboardPanelToPanelState } from './lib/embeddable_saved_object_converters';
export class DashboardAppController {
// Part of the exposed plugin API - do not remove without careful consideration.
@ -86,7 +90,6 @@ export class DashboardAppController {
constructor({
$scope,
$rootScope,
$route,
$routeParams,
getAppState,
@ -98,13 +101,11 @@ export class DashboardAppController {
indexPatterns,
config,
confirmModal,
addFilter,
courier,
}: {
courier: { fetch: () => void };
$scope: DashboardAppScope;
$route: any;
$rootScope: ng.IRootScopeService;
$routeParams: any;
getAppState: {
previouslyStored: () => TAppState | undefined;
@ -113,27 +114,20 @@ export class DashboardAppController {
getDefault: () => Promise<IndexPattern>;
};
dashboardConfig: any;
localStorage: any;
localStorage: {
get: (prop: string) => unknown;
};
Private: IPrivate;
kbnUrl: KbnUrl;
AppStateClass: TAppStateClass<DashboardAppState>;
config: any;
confirmModal: ConfirmModalFn;
addFilter: AddFilterFn;
}) {
const queryFilter = Private(FilterBarQueryFilterProvider);
const embeddableFactories = Private(
EmbeddableFactoriesRegistryProvider
) as EmbeddableFactoryRegistry;
const panelActionsRegistry = Private(ContextMenuActionsRegistryProvider);
const getUnhashableStates = Private(getUnhashableStatesProvider);
const shareContextMenuExtensions = Private(ShareContextMenuExtensionsRegistryProvider);
// @ts-ignore This code is going away shortly.
panelActionsStore.initializeFromRegistry(panelActionsRegistry);
const visTypes = Private(VisTypesRegistryProvider);
$scope.getEmbeddableFactory = panelType => embeddableFactories.byName[panelType];
let lastReloadRequestTime = 0;
const dash = ($scope.dash = $route.current.locals.dash);
if (dash.id) {
@ -144,10 +138,8 @@ export class DashboardAppController {
savedDashboard: dash,
AppStateClass,
hideWriteControls: dashboardConfig.getHideWriteControls(),
addFilter,
});
$scope.getDashboardState = () => dashboardStateManager;
$scope.appState = dashboardStateManager.getAppState();
// The 'previouslyStored' check is so we only update the time filter on dashboard open, not during
@ -156,6 +148,58 @@ export class DashboardAppController {
dashboardStateManager.syncTimefilterWithDashboard(timefilter);
}
const updateIndexPatterns = (container?: DashboardContainer) => {
if (!container || isErrorEmbeddable(container)) {
return;
}
const panelIndexPatterns = container.getPanelIndexPatterns();
if (panelIndexPatterns && panelIndexPatterns.length > 0) {
$scope.$evalAsync(() => {
$scope.indexPatterns = panelIndexPatterns;
});
} else {
indexPatterns.getDefault().then(defaultIndexPattern => {
$scope.$evalAsync(() => {
$scope.indexPatterns = [defaultIndexPattern];
});
});
}
};
const getDashboardInput = (): DashboardContainerInput => {
const embeddablesMap: {
[key: string]: DashboardPanelState;
} = {};
dashboardStateManager.getPanels().forEach((panel: SavedDashboardPanel) => {
embeddablesMap[panel.panelIndex] = convertSavedDashboardPanelToPanelState(
panel,
dashboardStateManager.getUseMargins()
);
});
let expandedPanelId;
if (dashboardContainer && !isErrorEmbeddable(dashboardContainer)) {
expandedPanelId = dashboardContainer.getInput().expandedPanelId;
}
return {
id: dashboardStateManager.savedDashboard.id || '',
filters: queryFilter.getFilters(),
hidePanelTitles: dashboardStateManager.getHidePanelTitles(),
query: $scope.model.query,
timeRange: {
..._.cloneDeep(timefilter.getTime()),
},
refreshConfig: timefilter.getRefreshInterval(),
viewMode: dashboardStateManager.getViewMode(),
panels: embeddablesMap,
isFullScreenMode: dashboardStateManager.getFullScreenMode(),
useMargins: dashboardStateManager.getUseMargins(),
lastReloadRequestTime,
title: dashboardStateManager.getTitle(),
description: dashboardStateManager.getDescription(),
expandedPanelId,
};
};
const updateState = () => {
// Following the "best practice" of always have a '.' in your ng-models
// https://github.com/angular/angular.js/wiki/Understanding-Scopes
@ -170,19 +214,76 @@ export class DashboardAppController {
};
$scope.panels = dashboardStateManager.getPanels();
$scope.screenTitle = dashboardStateManager.getTitle();
const panelIndexPatterns = dashboardStateManager.getPanelIndexPatterns();
if (panelIndexPatterns && panelIndexPatterns.length > 0) {
$scope.indexPatterns = panelIndexPatterns;
} else {
indexPatterns.getDefault().then(defaultIndexPattern => {
$scope.$evalAsync(() => {
$scope.indexPatterns = [defaultIndexPattern];
});
});
}
};
updateState();
let dashboardContainer: DashboardContainer | undefined;
let inputSubscription: Subscription | undefined;
let outputSubscription: Subscription | undefined;
const dashboardDom = document.getElementById('dashboardViewport');
const dashboardFactory = embeddableFactories.get(
DASHBOARD_CONTAINER_TYPE
) as DashboardContainerFactory;
dashboardFactory
.create(getDashboardInput())
.then((container: DashboardContainer | ErrorEmbeddable) => {
if (!isErrorEmbeddable(container)) {
dashboardContainer = container;
updateIndexPatterns(dashboardContainer);
outputSubscription = dashboardContainer.getOutput$().subscribe(() => {
updateIndexPatterns(dashboardContainer);
});
inputSubscription = dashboardContainer.getInput$().subscribe(async () => {
let dirty = false;
// This has to be first because handleDashboardContainerChanges causes
// appState.save which will cause refreshDashboardContainer to be called.
// Add filters modifies the object passed to it, hence the clone deep.
if (!_.isEqual(container.getInput().filters, queryFilter.getFilters())) {
await queryFilter.addFilters(_.cloneDeep(container.getInput().filters));
dashboardStateManager.applyFilters($scope.model.query, container.getInput().filters);
dirty = true;
}
$scope.$evalAsync(() => {
dashboardStateManager.handleDashboardContainerChanges(container);
if (dirty) {
updateState();
}
});
});
dashboardStateManager.registerChangeListener(() => {
// we aren't checking dirty state because there are changes the container needs to know about
// that won't make the dashboard "dirty" - like a view mode change.
refreshDashboardContainer();
});
// This code needs to be replaced with a better mechanism for adding new embeddables of
// any type from the add panel. Likely this will happen via creating a visualization "inline",
// without navigating away from the UX.
if ($routeParams[DashboardConstants.NEW_VISUALIZATION_ID_PARAM]) {
container.addSavedObjectEmbeddable(
VISUALIZE_EMBEDDABLE_TYPE,
$routeParams[DashboardConstants.NEW_VISUALIZATION_ID_PARAM]
);
kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM);
kbnUrl.removeParam(DashboardConstants.NEW_VISUALIZATION_ID_PARAM);
}
}
if (dashboardDom) {
container.render(dashboardDom);
}
});
// Part of the exposed plugin API - do not remove without careful consideration.
this.appStatus = {
dirty: !dash.id,
@ -205,15 +306,6 @@ export class DashboardAppController {
timefilter.disableTimeRangeSelector();
timefilter.disableAutoRefreshSelector();
updateState();
$scope.refresh = () => {
$rootScope.$broadcast('fetch');
courier.fetch();
};
dashboardStateManager.handleTimeChange(timefilter.getTime());
dashboardStateManager.handleRefreshConfigChange(timefilter.getRefreshInterval());
const landingPageUrl = () => `#${DashboardConstants.LANDING_PAGE_PATH}`;
const getDashTitle = () =>
@ -248,6 +340,32 @@ export class DashboardAppController {
dashboardStateManager.getIsViewMode() &&
!dashboardConfig.getHideWriteControls();
const getChangesFromAppStateForContainerState = () => {
const appStateDashboardInput = getDashboardInput();
if (!dashboardContainer || isErrorEmbeddable(dashboardContainer)) {
return appStateDashboardInput;
}
const containerInput = dashboardContainer.getInput();
const differences: Partial<DashboardContainerInput> = {};
Object.keys(containerInput).forEach(key => {
const containerValue = (containerInput as { [key: string]: unknown })[key];
const appStateValue = (appStateDashboardInput as { [key: string]: unknown })[key];
if (!_.isEqual(containerValue, appStateValue)) {
(differences as { [key: string]: unknown })[key] = appStateValue;
}
});
return Object.values(differences).length === 0 ? undefined : differences;
};
const refreshDashboardContainer = () => {
const changes = getChangesFromAppStateForContainerState();
if (changes && dashboardContainer) {
dashboardContainer.updateInput(changes);
}
};
$scope.updateQueryAndFetch = function({ query, dateRange }) {
if (dateRange) {
timefilter.setTime(dateRange);
@ -258,12 +376,12 @@ export class DashboardAppController {
// The user can still request a reload in the query bar, even if the
// query is the same, and in that case, we have to explicitly ask for
// a reload, since no state changes will cause it.
dashboardStateManager.requestReload();
lastReloadRequestTime = new Date().getTime();
refreshDashboardContainer();
} else {
$scope.model.query = query;
dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters);
}
$scope.refresh();
};
$scope.onRefreshChange = function({ isPaused, refreshInterval }) {
@ -301,18 +419,24 @@ export class DashboardAppController {
});
$scope.$listenAndDigestAsync(timefilter, 'fetch', () => {
dashboardStateManager.handleTimeChange(timefilter.getTime());
// Currently discover relies on this logic to re-fetch. We need to refactor it to rely instead on the
// directly passed down time filter. Then we can get rid of this reliance on scope broadcasts.
$scope.refresh();
// The only reason this is here is so that search embeddables work on a dashboard with
// a refresh interval turned on. This kicks off the search poller. It should be
// refactored so no embeddables need to listen to the timefilter directly but instead
// the container tells it when to reload.
courier.fetch();
});
$scope.$listenAndDigestAsync(timefilter, 'refreshIntervalUpdate', () => {
dashboardStateManager.handleRefreshConfigChange(timefilter.getRefreshInterval());
updateState();
});
$scope.$listenAndDigestAsync(timefilter, 'timeUpdate', updateState);
function updateViewMode(newMode: DashboardViewMode) {
$scope.$listenAndDigestAsync(timefilter, 'refreshIntervalUpdate', () => {
updateState();
refreshDashboardContainer();
});
$scope.$listenAndDigestAsync(timefilter, 'timeUpdate', () => {
updateState();
refreshDashboardContainer();
});
function updateViewMode(newMode: ViewMode) {
$scope.topNavMenu = getTopNavConfig(
newMode,
navActions,
@ -321,9 +445,9 @@ export class DashboardAppController {
dashboardStateManager.switchViewMode(newMode);
}
const onChangeViewMode = (newMode: DashboardViewMode) => {
const onChangeViewMode = (newMode: ViewMode) => {
const isPageRefresh = newMode === dashboardStateManager.getViewMode();
const isLeavingEditMode = !isPageRefresh && newMode === DashboardViewMode.VIEW;
const isLeavingEditMode = !isPageRefresh && newMode === ViewMode.VIEW;
const willLoseChanges = isLeavingEditMode && dashboardStateManager.getIsDirty(timefilter);
if (!willLoseChanges) {
@ -337,7 +461,7 @@ export class DashboardAppController {
dash.id ? createDashboardEditUrl(dash.id) : DashboardConstants.CREATE_NEW_DASHBOARD_URL
);
// This is only necessary for new dashboards, which will default to Edit mode.
updateViewMode(DashboardViewMode.VIEW);
updateViewMode(ViewMode.VIEW);
// We need to do a hard reset of the timepicker. appState will not reload like
// it does on 'open' because it's been saved to the url and the getAppState.previouslyStored() check on
@ -398,7 +522,7 @@ export class DashboardAppController {
kbnUrl.change(createDashboardEditUrl(dash.id));
} else {
docTitle.change(dash.lastSavedTitle);
updateViewMode(DashboardViewMode.VIEW);
updateViewMode(ViewMode.VIEW);
}
}
return { id };
@ -433,8 +557,8 @@ export class DashboardAppController {
[key: string]: NavAction;
} = {};
navActions[TopNavIds.FULL_SCREEN] = () => dashboardStateManager.setFullScreenMode(true);
navActions[TopNavIds.EXIT_EDIT_MODE] = () => onChangeViewMode(DashboardViewMode.VIEW);
navActions[TopNavIds.ENTER_EDIT_MODE] = () => onChangeViewMode(DashboardViewMode.EDIT);
navActions[TopNavIds.EXIT_EDIT_MODE] = () => onChangeViewMode(ViewMode.VIEW);
navActions[TopNavIds.ENTER_EDIT_MODE] = () => onChangeViewMode(ViewMode.EDIT);
navActions[TopNavIds.SAVE] = () => {
const currentTitle = dashboardStateManager.getTitle();
const currentDescription = dashboardStateManager.getDescription();
@ -512,14 +636,11 @@ export class DashboardAppController {
showCloneModal(onClone, currentTitle);
};
navActions[TopNavIds.ADD] = () => {
const addNewVis = () => {
showNewVisModal(visTypes, {
editorParams: [DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM],
});
};
showAddPanel(dashboardStateManager.addNewPanel, addNewVis, embeddableFactories);
if (dashboardContainer && !isErrorEmbeddable(dashboardContainer)) {
openAddPanelFlyout(dashboardContainer);
}
};
navActions[TopNavIds.OPTIONS] = (menuItem, navController, anchorElement) => {
showOptionsPopover({
anchorElement,
@ -556,30 +677,24 @@ export class DashboardAppController {
next: () => {
$scope.model.filters = queryFilter.getFilters();
dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters);
if (dashboardContainer) {
dashboardContainer.updateInput({ filters: $scope.model.filters });
}
},
});
// update data when filters fire fetch event
const fetchSubscription = queryFilter.getFetches$().subscribe($scope.refresh);
$scope.$on('$destroy', () => {
updateSubscription.unsubscribe();
fetchSubscription.unsubscribe();
dashboardStateManager.destroy();
if (inputSubscription) {
inputSubscription.unsubscribe();
}
if (outputSubscription) {
outputSubscription.unsubscribe();
}
if (dashboardContainer) {
dashboardContainer.destroy();
}
});
if (
$route.current.params &&
$route.current.params[DashboardConstants.NEW_VISUALIZATION_ID_PARAM]
) {
dashboardStateManager.addNewPanel(
$route.current.params[DashboardConstants.NEW_VISUALIZATION_ID_PARAM],
'visualization'
);
kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM);
kbnUrl.removeParam(DashboardConstants.NEW_VISUALIZATION_ID_PARAM);
}
}
}

View file

@ -23,10 +23,6 @@ export const DashboardConstants = {
LANDING_PAGE_PATH: '/dashboards',
CREATE_NEW_DASHBOARD_URL: '/dashboard',
};
export const DASHBOARD_GRID_COLUMN_COUNT = 48;
export const DASHBOARD_GRID_HEIGHT = 20;
export const DEFAULT_PANEL_WIDTH = DASHBOARD_GRID_COLUMN_COUNT / 2;
export const DEFAULT_PANEL_HEIGHT = 15;
export function createDashboardEditUrl(id: string) {
return `/dashboard/${id}`;

View file

@ -17,17 +17,14 @@
* under the License.
*/
import './np_core.test.mocks';
import { DashboardStateManager } from './dashboard_state_manager';
import { DashboardViewMode } from './dashboard_view_mode';
import { embeddableIsInitialized, setPanels } from './actions';
import { getAppStateMock, getSavedDashboardMock } from './__tests__';
import { store } from '../store';
import { AppStateClass } from 'ui/state_management/app_state';
import { DashboardAppState } from './types';
import { IndexPattern } from 'ui/index_patterns';
import { Timefilter } from 'ui/timefilter';
jest.mock('ui/chrome', () => ({ getKibanaVersion: () => '6.0.0' }), { virtual: true });
import { ViewMode } from '../../../embeddable_api/public';
describe('DashboardState', function() {
let dashboardState: DashboardStateManager;
@ -52,14 +49,11 @@ describe('DashboardState', function() {
isAutoRefreshSelectorEnabled: true,
isTimeRangeSelectorEnabled: true,
};
const mockIndexPattern: IndexPattern = { id: 'index1', fields: [], title: 'hi' };
function initDashboardState() {
dashboardState = new DashboardStateManager({
savedDashboard,
AppStateClass: getAppStateMock() as AppStateClass<DashboardAppState>,
hideWriteControls: false,
addFilter: () => {},
});
}
@ -116,72 +110,15 @@ describe('DashboardState', function() {
});
test('getIsDirty is true if isDirty is true and editing', () => {
dashboardState.switchViewMode(DashboardViewMode.EDIT);
dashboardState.switchViewMode(ViewMode.EDIT);
dashboardState.isDirty = true;
expect(dashboardState.getIsDirty()).toBeTruthy();
});
test('getIsDirty is false if isDirty is true and editing', () => {
dashboardState.switchViewMode(DashboardViewMode.VIEW);
dashboardState.switchViewMode(ViewMode.VIEW);
dashboardState.isDirty = true;
expect(dashboardState.getIsDirty()).toBeFalsy();
});
});
describe('panelIndexPatternMapping', function() {
beforeAll(() => {
initDashboardState();
});
function simulateNewEmbeddableWithIndexPatterns({
panelId,
indexPatterns,
}: {
panelId: string;
indexPatterns?: IndexPattern[];
}) {
store.dispatch(
setPanels({
[panelId]: {
id: '123',
panelIndex: panelId,
version: '1',
type: 'hi',
embeddableConfig: {},
gridData: { x: 1, y: 1, h: 1, w: 1, i: '1' },
},
})
);
const metadata = { title: 'my embeddable title', editUrl: 'editme', indexPatterns };
store.dispatch(embeddableIsInitialized({ metadata, panelId }));
}
test('initially has no index patterns', () => {
expect(dashboardState.getPanelIndexPatterns().length).toBe(0);
});
test('registers index pattern when an embeddable is initialized with one', async () => {
simulateNewEmbeddableWithIndexPatterns({
panelId: 'foo1',
indexPatterns: [mockIndexPattern],
});
await new Promise(resolve => process.nextTick(resolve));
expect(dashboardState.getPanelIndexPatterns().length).toBe(1);
});
test('registers unique index patterns', async () => {
simulateNewEmbeddableWithIndexPatterns({
panelId: 'foo2',
indexPatterns: [mockIndexPattern],
});
await new Promise(resolve => process.nextTick(resolve));
expect(dashboardState.getPanelIndexPatterns().length).toBe(1);
});
test('does not register undefined index pattern for panels with no index pattern', async () => {
simulateNewEmbeddableWithIndexPatterns({ panelId: 'foo2' });
await new Promise(resolve => process.nextTick(resolve));
expect(dashboardState.getPanelIndexPatterns().length).toBe(1);
});
});
});

View file

@ -20,63 +20,23 @@
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import { stateMonitorFactory, StateMonitor } from 'ui/state_management/state_monitor_factory';
import { StaticIndexPattern } from 'ui/index_patterns';
import { AppStateClass as TAppStateClass } from 'ui/state_management/app_state';
import { Timefilter } from 'ui/timefilter';
import { RefreshInterval } from 'ui/timefilter/timefilter';
import { Filter } from '@kbn/es-query';
import moment from 'moment';
import { Query } from 'src/legacy/core_plugins/data/public';
import { TimeRange } from 'ui/timefilter/time_history';
import { DashboardViewMode } from './dashboard_view_mode';
import { FilterUtils } from './lib/filter_utils';
import { PanelUtils } from './panel/panel_utils';
import { store } from '../store';
import { stateMonitorFactory, StateMonitor } from 'ui/state_management/state_monitor_factory';
import { Timefilter } from 'ui/timefilter';
import { AppStateClass as TAppStateClass } from 'ui/state_management/app_state';
import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query';
import { Moment } from 'moment';
import { DashboardContainer } from '../../../dashboard_embeddable_container/public';
import { ViewMode } from '../../../embeddable_api/public';
import { Query } from '../../../data/public';
import {
updateViewMode,
setPanels,
updateUseMargins,
updateIsFullScreenMode,
minimizePanel,
updateTitle,
updateDescription,
updateHidePanelTitles,
updateTimeRange,
updateRefreshConfig,
clearStagedFilters,
updateFilters,
updateQuery,
closeContextMenu,
requestReload,
} from './actions';
import { createPanelState } from './panel';
import { getAppStateDefaults, migrateAppState } from './lib';
import {
getViewMode,
getFullScreenMode,
getPanels,
getPanel,
getTitle,
getDescription,
getUseMargins,
getHidePanelTitles,
getStagedFilters,
getEmbeddables,
getEmbeddableMetadata,
getQuery,
getFilters,
} from '../selectors';
import { convertPanelStateToSavedDashboardPanel } from './lib/embeddable_saved_object_converters';
import { FilterUtils } from './lib/filter_utils';
import { SavedObjectDashboard } from './saved_dashboard/saved_dashboard';
import {
DashboardAppState,
SavedDashboardPanel,
SavedDashboardPanelMap,
DashboardAppStateParameters,
AddFilterFn,
DashboardAppStateDefaults,
} from './types';
import { SavedDashboardPanel, DashboardAppState, DashboardAppStateDefaults } from './types';
/**
* Dashboard state manager handles connecting angular and redux state between the angular and react portions of the
@ -88,41 +48,34 @@ export class DashboardStateManager {
public savedDashboard: SavedObjectDashboard;
public appState: DashboardAppState;
public lastSavedDashboardFilters: {
timeTo?: string | moment.Moment;
timeFrom?: string | moment.Moment;
timeTo?: string | Moment;
timeFrom?: string | Moment;
filterBars: Filter[];
query: Query | string;
query: Query;
};
private stateDefaults: DashboardAppStateParameters;
private stateDefaults: DashboardAppStateDefaults;
private hideWriteControls: boolean;
public isDirty: boolean;
private changeListeners: Array<(status: { dirty: boolean }) => void>;
private stateMonitor: StateMonitor<DashboardAppStateDefaults>;
private panelIndexPatternMapping: { [key: string]: StaticIndexPattern[] } = {};
private addFilter: AddFilterFn;
private unsubscribe: () => void;
/**
*
* @param savedDashboard
* @param AppState The AppState class to use when instantiating a new AppState instance.
* @param hideWriteControls true if write controls should be hidden.
* @param addFilter a function that can be used to add a filter bar filter
*/
constructor({
savedDashboard,
AppStateClass,
hideWriteControls,
addFilter,
}: {
savedDashboard: SavedObjectDashboard;
AppStateClass: TAppStateClass<DashboardAppState>;
hideWriteControls: boolean;
addFilter: AddFilterFn;
}) {
this.savedDashboard = savedDashboard;
this.hideWriteControls = hideWriteControls;
this.addFilter = addFilter;
this.stateDefaults = getAppStateDefaults(this.savedDashboard, this.hideWriteControls);
@ -142,11 +95,6 @@ export class DashboardStateManager {
// in the 'lose changes' warning message.
this.lastSavedDashboardFilters = this.getFilterState();
// A mapping of panel index to the index pattern it uses.
this.panelIndexPatternMapping = {};
PanelUtils.initPanelIndexes(this.getPanels());
/**
* Creates a state monitor and saves it to this.stateMonitor. Used to track unsaved changes made to appState.
*/
@ -165,18 +113,10 @@ export class DashboardStateManager {
this.isDirty = status.dirty;
});
store.dispatch(closeContextMenu());
// Always start out with all panels minimized when a dashboard is first loaded.
store.dispatch(minimizePanel());
this.pushAppStateChangesToStore();
this.changeListeners = [];
this.unsubscribe = store.subscribe(() => this.handleStoreChanges());
this.stateMonitor.onChange((status: { dirty: boolean }) => {
this.changeListeners.forEach(listener => listener(status));
this.pushAppStateChangesToStore();
});
}
@ -184,152 +124,53 @@ export class DashboardStateManager {
this.changeListeners.push(callback);
}
private areStoreAndAppStatePanelsEqual() {
const state = store.getState();
const storePanels = getPanels(store.getState());
const appStatePanels = this.getPanels();
if (Object.values(storePanels).length !== appStatePanels.length) {
return false;
}
return appStatePanels.every(appStatePanel => {
const storePanel = getPanel(state, appStatePanel.panelIndex);
return _.isEqual(appStatePanel, storePanel);
});
}
/**
* Time is part of global state so we need to deal with it outside of pushAppStateChangesToStore.
*/
public handleTimeChange(newTimeRange: TimeRange) {
const from = FilterUtils.convertTimeToUTCString(newTimeRange.from);
const to = FilterUtils.convertTimeToUTCString(newTimeRange.to);
store.dispatch(
updateTimeRange({
from: from ? from.toString() : '',
to: to ? to.toString() : '',
})
);
}
public handleRefreshConfigChange(refreshInterval: RefreshInterval) {
store.dispatch(updateRefreshConfig(refreshInterval));
}
/**
* Changes made to app state outside of direct calls to this class will need to be propagated to the store.
* @private
*/
private pushAppStateChangesToStore() {
// We need these checks, or you can get into a loop where a change is triggered by the store, which updates
// AppState, which then dispatches the change here, which will end up triggering setState warnings.
if (!this.areStoreAndAppStatePanelsEqual()) {
// Translate appState panels data into the data expected by redux, copying the panel objects as we do so
// because the panels inside appState can be mutated, while redux state should never be mutated directly.
const panelsMap = this.getPanels().reduce((acc: SavedDashboardPanelMap, panel) => {
acc[panel.panelIndex] = _.cloneDeep(panel);
return acc;
}, {});
store.dispatch(setPanels(panelsMap));
}
const state = store.getState();
if (getTitle(state) !== this.getTitle()) {
store.dispatch(updateTitle(this.getTitle()));
}
if (getDescription(state) !== this.getDescription()) {
store.dispatch(updateDescription(this.getDescription()));
}
if (getViewMode(state) !== this.getViewMode()) {
store.dispatch(updateViewMode(this.getViewMode()));
}
if (getUseMargins(state) !== this.getUseMargins()) {
store.dispatch(updateUseMargins(this.getUseMargins()));
}
if (getHidePanelTitles(state) !== this.getHidePanelTitles()) {
store.dispatch(updateHidePanelTitles(this.getHidePanelTitles()));
}
if (getFullScreenMode(state) !== this.getFullScreenMode()) {
store.dispatch(updateIsFullScreenMode(this.getFullScreenMode()));
}
if (getTitle(state) !== this.getTitle()) {
store.dispatch(updateTitle(this.getTitle()));
}
if (getDescription(state) !== this.getDescription()) {
store.dispatch(updateDescription(this.getDescription()));
}
if (getQuery(state) !== this.getQuery()) {
store.dispatch(updateQuery(this.getQuery()));
}
this._pushFiltersToStore();
}
_pushFiltersToStore() {
const state = store.getState();
const dashboardFilters = this.savedDashboard.getFilters();
if (
!_.isEqual(
FilterUtils.cleanFiltersForComparison(dashboardFilters),
FilterUtils.cleanFiltersForComparison(getFilters(state))
)
) {
store.dispatch(updateFilters(dashboardFilters));
}
}
requestReload() {
store.dispatch(requestReload());
}
private handleStoreChanges() {
public handleDashboardContainerChanges(dashboardContainer: DashboardContainer) {
let dirty = false;
if (!this.areStoreAndAppStatePanelsEqual()) {
const panels: SavedDashboardPanelMap = getPanels(store.getState());
this.appState.panels = [];
this.panelIndexPatternMapping = {};
Object.values(panels).map((panel: SavedDashboardPanel) => {
this.appState.panels.push(_.cloneDeep(panel));
});
dirty = true;
}
_.forEach(getEmbeddables(store.getState()), (embeddable, panelId) => {
if (
panelId &&
embeddable.initialized &&
!this.panelIndexPatternMapping.hasOwnProperty(panelId)
) {
const embeddableMetadata = getEmbeddableMetadata(store.getState(), panelId);
if (embeddableMetadata && embeddableMetadata.indexPatterns) {
this.panelIndexPatternMapping[panelId] = _.compact(embeddableMetadata.indexPatterns);
dirty = true;
}
const savedDashboardPanelMap: { [key: string]: SavedDashboardPanel } = {};
const input = dashboardContainer.getInput();
this.getPanels().forEach(savedDashboardPanel => {
if (input.panels[savedDashboardPanel.panelIndex] !== undefined) {
savedDashboardPanelMap[savedDashboardPanel.panelIndex] = savedDashboardPanel;
} else {
// A panel was deleted.
dirty = true;
}
});
const stagedFilters = getStagedFilters(store.getState());
stagedFilters.forEach(filter => {
this.addFilter(filter, this.getAppState());
const convertedPanelStateMap: { [key: string]: SavedDashboardPanel } = {};
Object.values(input.panels).forEach(panelState => {
if (savedDashboardPanelMap[panelState.explicitInput.id] === undefined) {
dirty = true;
}
convertedPanelStateMap[panelState.explicitInput.id] = convertPanelStateToSavedDashboardPanel(
panelState
);
if (
!_.isEqual(
convertedPanelStateMap[panelState.explicitInput.id],
savedDashboardPanelMap[panelState.explicitInput.id]
)
) {
// A panel was changed
dirty = true;
}
});
if (stagedFilters.length > 0) {
this.saveState();
store.dispatch(clearStagedFilters());
if (dirty) {
this.appState.panels = Object.values(convertedPanelStateMap);
}
const fullScreen = getFullScreenMode(store.getState());
if (fullScreen !== this.getFullScreenMode()) {
this.setFullScreenMode(fullScreen);
if (input.isFullScreenMode !== this.getFullScreenMode()) {
this.setFullScreenMode(input.isFullScreenMode);
}
if (!_.isEqual(input.query, this.getQuery())) {
this.setQuery(input.query);
}
this.changeListeners.forEach(listener => listener({ dirty }));
@ -345,11 +186,6 @@ export class DashboardStateManager {
this.saveState();
}
public getPanelIndexPatterns() {
const indexPatterns = _.flatten(Object.values(this.panelIndexPatternMapping));
return _.uniq(indexPatterns, 'id');
}
/**
* Resets the state back to the last saved version of the dashboard.
*/
@ -379,7 +215,6 @@ export class DashboardStateManager {
/**
* Returns an object which contains the current filter state of this.savedDashboard.
* @returns {{timeTo: String, timeFrom: String, filterBars: Array, query: Object}}
*/
public getFilterState() {
return {
@ -413,8 +248,8 @@ export class DashboardStateManager {
return this.appState;
}
public getQuery() {
return this.appState.query;
public getQuery(): Query {
return migrateLegacyQuery(this.appState.query);
}
public getUseMargins() {
@ -447,9 +282,6 @@ export class DashboardStateManager {
this.saveState();
}
/**
* @returns {boolean}
*/
public getIsTimeSavedWithDashboard() {
return this.savedDashboard.timeRestore;
}
@ -458,29 +290,31 @@ export class DashboardStateManager {
return this.lastSavedDashboardFilters.filterBars;
}
public getLastSavedQuery(): Query | string {
public getLastSavedQuery() {
return this.lastSavedDashboardFilters.query;
}
/**
* @returns {boolean} True if the query changed since the last time the dashboard was saved, or if it's a
* @returns True if the query changed since the last time the dashboard was saved, or if it's a
* new dashboard, if the query differs from the default.
*/
public getQueryChanged() {
const currentQuery = this.appState.query;
const lastSavedQuery = this.getLastSavedQuery();
const query = migrateLegacyQuery(currentQuery);
const isLegacyStringQuery =
_.isString(lastSavedQuery) && _.isPlainObject(currentQuery) && _.has(currentQuery, 'query');
if (isLegacyStringQuery) {
return (lastSavedQuery as string) !== (currentQuery as Query).query;
return lastSavedQuery !== query.query;
}
return !_.isEqual(currentQuery, lastSavedQuery);
}
/**
* @returns {boolean} True if the filter bar state has changed since the last time the dashboard was saved,
* @returns True if the filter bar state has changed since the last time the dashboard was saved,
* or if it's a new dashboard, if the query differs from the default.
*/
public getFilterBarChanged() {
@ -492,7 +326,7 @@ export class DashboardStateManager {
/**
* @param timeFilter
* @returns {boolean} True if the time state has changed since the time saved with the dashboard.
* @returns True if the time state has changed since the time saved with the dashboard.
*/
public getTimeChanged(timeFilter: Timefilter) {
return (
@ -504,31 +338,21 @@ export class DashboardStateManager {
);
}
/**
*
* @returns {DashboardViewMode}
*/
public getViewMode() {
return this.hideWriteControls ? DashboardViewMode.VIEW : this.appState.viewMode;
return this.hideWriteControls ? ViewMode.VIEW : this.appState.viewMode;
}
/**
* @returns {boolean}
*/
public getIsViewMode() {
return this.getViewMode() === DashboardViewMode.VIEW;
return this.getViewMode() === ViewMode.VIEW;
}
/**
* @returns {boolean}
*/
public getIsEditMode() {
return this.getViewMode() === DashboardViewMode.EDIT;
return this.getViewMode() === ViewMode.EDIT;
}
/**
*
* @returns {boolean} True if the dashboard has changed since the last save (or, is new).
* @returns True if the dashboard has changed since the last save (or, is new).
*/
public getIsDirty(timeFilter?: Timefilter) {
// Filter bar comparison is done manually (see cleanFiltersForComparison for the reason) and time picker
@ -550,33 +374,9 @@ export class DashboardStateManager {
return foundPanel;
}
/**
* Creates and initializes a basic panel, adding it to the state.
* @param {number} id
* @param {string} type
*/
public addNewPanel = (id: string, type: string) => {
const maxPanelIndex = PanelUtils.getMaxPanelIndex(this.getPanels());
const newPanel = createPanelState(id, type, maxPanelIndex.toString(), this.getPanels());
this.getPanels().push(newPanel);
this.saveState();
};
public removePanel(panelIndex: string) {
_.remove(this.getPanels(), panel => {
if (panel.panelIndex === panelIndex) {
delete this.panelIndexPatternMapping[panelIndex];
return true;
} else {
return false;
}
});
this.saveState();
}
/**
* @param timeFilter
* @returns {Array.<string>} An array of user friendly strings indicating the filter types that have changed.
* @returns An array of user friendly strings indicating the filter types that have changed.
*/
public getChangedFilterTypes(timeFilter: Timefilter) {
const changedFilters = [];
@ -593,7 +393,7 @@ export class DashboardStateManager {
}
/**
* @return True if filters (query, filter bar filters, and time picker if time is stored
* @returns True if filters (query, filter bar filters, and time picker if time is stored
* with the dashboard) have changed since the last saved state (or if the dashboard hasn't been saved,
* the default state).
*/
@ -603,6 +403,9 @@ export class DashboardStateManager {
/**
* Updates timeFilter to match the time saved with the dashboard.
* @param timeFilter
* @param timeFilter.setTime
* @param timeFilter.setRefreshInterval
*/
public syncTimefilterWithDashboard(timeFilter: Timefilter) {
if (!this.getIsTimeSavedWithDashboard()) {
@ -632,23 +435,23 @@ export class DashboardStateManager {
this.appState.save();
}
public setQuery(query: Query) {
this.appState.query = query;
this.saveState();
}
/**
* Applies the current filter state to the dashboard.
* @param filter {Array.<Object>} An array of filter bar filters.
* @param filter An array of filter bar filters.
*/
public applyFilters(query: Query | string, filters: Filter[]) {
public applyFilters(query: Query, filters: Filter[]) {
this.appState.query = query;
this.savedDashboard.searchSource.setField('query', query);
this.savedDashboard.searchSource.setField('filter', filters);
this.saveState();
// pinned filters go on global state, therefore are not propagated to store via app state and have to be pushed manually.
this._pushFiltersToStore();
}
/**
* @param newMode {DashboardViewMode}
*/
public switchViewMode(newMode: DashboardViewMode) {
public switchViewMode(newMode: ViewMode) {
this.appState.viewMode = newMode;
this.saveState();
}
@ -661,6 +464,5 @@ export class DashboardStateManager {
this.stateMonitor.destroy();
}
this.savedDashboard.destroy();
this.unsubscribe();
}
}

View file

@ -18,7 +18,7 @@
*/
import { i18n } from '@kbn/i18n';
import { DashboardViewMode } from './dashboard_view_mode';
import { ViewMode } from '../../../embeddable_api/public';
/**
* @param title {string} the current title of the dashboard
@ -27,12 +27,8 @@ import { DashboardViewMode } from './dashboard_view_mode';
* end of the title.
* @returns {string} A title to display to the user based on the above parameters.
*/
export function getDashboardTitle(
title: string,
viewMode: DashboardViewMode,
isDirty: boolean
): string {
const isEditMode = viewMode === DashboardViewMode.EDIT;
export function getDashboardTitle(title: string, viewMode: ViewMode, isDirty: boolean): string {
const isEditMode = viewMode === ViewMode.EDIT;
let displayTitle: string;
if (isEditMode && isDirty) {

View file

@ -1,23 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/
export enum DashboardViewMode {
EDIT = 'edit',
VIEW = 'view',
}

View file

@ -1,77 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders DashboardGrid 1`] = `
<SizeMe(ResponsiveGrid)
isViewMode={false}
layout={
Array [
Object {
"h": 6,
"i": 1,
"w": 6,
"x": 0,
"y": 0,
},
Object {
"h": 6,
"i": 2,
"w": 6,
"x": 6,
"y": 6,
},
]
}
onLayoutChange={[Function]}
useMargins={true}
>
<div
className=""
key="1"
style={
Object {
"zIndex": "auto",
}
}
>
<Connect(InjectIntl(DashboardPanelUi))
embeddableFactory={
Object {
"create": [MockFunction],
}
}
onPanelBlurred={[Function]}
onPanelFocused={[Function]}
panelId="1"
/>
</div>
<div
className=""
key="2"
style={
Object {
"zIndex": "auto",
}
}
>
<Connect(InjectIntl(DashboardPanelUi))
embeddableFactory={
Object {
"create": [MockFunction],
}
}
onPanelBlurred={[Function]}
onPanelFocused={[Function]}
panelId="2"
/>
</div>
</SizeMe(ResponsiveGrid)>
`;
exports[`renders DashboardGrid with no visualizations 1`] = `
<SizeMe(ResponsiveGrid)
isViewMode={false}
layout={Array []}
onLayoutChange={[Function]}
useMargins={true}
/>
`;

View file

@ -1,127 +0,0 @@
// SASSTODO: Can't find this selector, but could break something if removed
.react-grid-layout .gs-w {
z-index: auto;
}
/**
* 1. Due to https://github.com/STRML/react-grid-layout/issues/240 we have to manually hide the resizable
* element.
*/
.dshLayout--viewing {
.react-resizable-handle {
display: none; /* 1 */
}
}
/**
* 1. If we don't give the resizable handler a larger z index value the layout will hide it.
*/
.dshLayout--editing {
.react-resizable-handle {
@include size($euiSizeL);
z-index: $euiZLevel1; /* 1 */
right: 0;
bottom: 0;
padding-right: $euiSizeS;
padding-bottom: $euiSizeS;
}
}
/**
* 1. Need to override the react grid layout height when a single panel is expanded. Important is required because
* otherwise the height is set inline.
*/
.dshLayout-isMaximizedPanel {
height: 100% !important; /* 1. */
width: 100%;
position: absolute;
}
/**
* .dshLayout-withoutMargins only affects the panel styles themselves, see ../panel
*/
/**
* When a single panel is expanded, all the other panels are hidden in the grid.
*/
.dshDashboardGrid__item--hidden {
display: none;
}
/**
* 1. We need to mark this as important because react grid layout sets the width and height of the panels inline.
*/
.dshDashboardGrid__item--expanded {
height: 100% !important; /* 1 */
width: 100% !important; /* 1 */
top: 0 !important; /* 1 */
left: 0 !important; /* 1 */
// Altered panel styles can be found in ../panel
}
// REACT-GRID
.react-grid-item {
/**
* Disable transitions from the library on each grid element.
*/
transition: none;
/**
* Copy over and overwrite the fill color with EUI color mixin (for theming)
*/
> .react-resizable-handle {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='6' viewBox='0 0 6 6'%3E%3Cpolygon fill='#{hexToRGB($euiColorDarkShade)}' points='6 6 0 6 0 4.2 4 4.2 4.2 4.2 4.2 0 6 0' /%3E%3C/svg%3E%0A");
&::after {
border: none;
}
&:hover,
&:focus {
background-color: $dshEditingModeHoverColor;
}
}
/**
* Dragged/Resized panels in dashboard should always appear above other panels
* and above the placeholder
*/
&.resizing,
&.react-draggable-dragging {
z-index: $euiZLevel2 !important;
}
&.react-draggable-dragging {
transition: box-shadow $euiAnimSpeedFast $euiAnimSlightResistance;
@include euiBottomShadowLarge;
border-radius: $euiBorderRadius; // keeps shadow within bounds
}
/**
* Overwrites red coloring that comes from this library by default.
*/
&.react-grid-placeholder {
border-radius: $euiBorderRadius;
background: $euiColorWarning;
}
}
// When in view-mode only, and on tiny mobile screens, just stack each of the grid-items
@include euiBreakpoint('xs', 's') {
.dshLayout--viewing {
.react-grid-item {
position: static !important;
width: calc(100% - #{$euiSize}) !important;
margin: $euiSizeS;
}
&.dshLayout-withoutMargins {
.react-grid-item {
width: 100% !important;
margin: 0;
}
}
}
}

View file

@ -1 +0,0 @@
@import './dashboard_grid';

View file

@ -1,93 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { shallowWithIntl } from 'test_utils/enzyme_helpers';
import sizeMe from 'react-sizeme';
import { DashboardViewMode } from '../dashboard_view_mode';
import { getEmbeddableFactoryMock } from '../__tests__';
import { DashboardGrid } from './dashboard_grid';
jest.mock('ui/chrome', () => ({ getKibanaVersion: () => '6.0.0' }), { virtual: true });
jest.mock('ui/notify',
() => ({
toastNotifications: {
addDanger: () => {},
}
}), { virtual: true });
function getProps(props = {}) {
const defaultTestProps = {
dashboardViewMode: DashboardViewMode.EDIT,
panels: {
'1': {
gridData: { x: 0, y: 0, w: 6, h: 6, i: 1 },
panelIndex: '1',
type: 'visualization',
id: '123',
version: '7.0.0',
},
'2': {
gridData: { x: 6, y: 6, w: 6, h: 6, i: 2 },
panelIndex: '2',
type: 'visualization',
id: '456',
version: '7.0.0',
}
},
getEmbeddableFactory: () => getEmbeddableFactoryMock(),
onPanelsUpdated: () => {},
useMargins: true,
};
return Object.assign(defaultTestProps, props);
}
beforeAll(() => {
// sizeme detects the width to be 0 in our test environment. noPlaceholder will mean that the grid contents will
// get rendered even when width is 0, which will improve our tests.
sizeMe.noPlaceholders = true;
});
afterAll(() => {
sizeMe.noPlaceholders = false;
});
test('renders DashboardGrid', () => {
const component = shallowWithIntl(<DashboardGrid.WrappedComponent {...getProps()} />);
expect(component).toMatchSnapshot();
const panelElements = component.find('Connect(InjectIntl(DashboardPanelUi))');
expect(panelElements.length).toBe(2);
});
test('renders DashboardGrid with no visualizations', () => {
const component = shallowWithIntl(<DashboardGrid.WrappedComponent {...getProps({ panels: {} })} />);
expect(component).toMatchSnapshot();
});
test('adjusts z-index of focused panel to be higher than siblings', () => {
const component = shallowWithIntl(<DashboardGrid.WrappedComponent {...getProps()} />);
const panelElements = component.find('Connect(InjectIntl(DashboardPanelUi))');
panelElements.first().prop('onPanelFocused')('1');
const [gridItem1, gridItem2] = component.update().findWhere(el => el.key() === '1' || el.key() === '2');
expect(gridItem1.props.style.zIndex).toEqual(2);
expect(gridItem2.props.style.zIndex).toEqual('auto');
});

View file

@ -1,283 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { injectI18n } from '@kbn/i18n/react';
import classNames from 'classnames';
import _ from 'lodash';
import React from 'react';
import ReactGridLayout, { Layout } from 'react-grid-layout';
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';
// @ts-ignore
import sizeMe from 'react-sizeme';
import { EmbeddableFactory } from 'ui/embeddable';
import { toastNotifications } from 'ui/notify';
import {
DASHBOARD_GRID_COLUMN_COUNT,
DASHBOARD_GRID_HEIGHT,
DashboardConstants,
} from '../dashboard_constants';
import { DashboardViewMode } from '../dashboard_view_mode';
import { DashboardPanel } from '../panel';
import { GridData, SavedDashboardPanel, SavedDashboardPanelMap } from '../types';
let lastValidGridSize = 0;
/**
* This is a fix for a bug that stopped the browser window from automatically scrolling down when panels were made
* taller than the current grid.
* see https://github.com/elastic/kibana/issues/14710.
*/
function ensureWindowScrollsToBottom(event: { clientY: number; pageY: number }) {
// The buffer is to handle the case where the browser is maximized and it's impossible for the mouse to move below
// the screen, out of the window. see https://github.com/elastic/kibana/issues/14737
const WINDOW_BUFFER = 10;
if (event.clientY > window.innerHeight - WINDOW_BUFFER) {
window.scrollTo(0, event.pageY + WINDOW_BUFFER - window.innerHeight);
}
}
function ResponsiveGrid({
size,
isViewMode,
layout,
onLayoutChange,
children,
maximizedPanelId,
useMargins,
}: {
size: { width: number };
isViewMode: boolean;
layout: Layout[];
onLayoutChange: () => void;
children: JSX.Element[];
maximizedPanelId: string;
useMargins: boolean;
}) {
// This is to prevent a bug where view mode changes when the panel is expanded. View mode changes will trigger
// the grid to re-render, but when a panel is expanded, the size will be 0. Minimizing the panel won't cause the
// grid to re-render so it'll show a grid with a width of 0.
lastValidGridSize = size.width > 0 ? size.width : lastValidGridSize;
const classes = classNames({
'dshLayout--viewing': isViewMode,
'dshLayout--editing': !isViewMode,
'dshLayout-isMaximizedPanel': maximizedPanelId !== undefined,
'dshLayout-withoutMargins': !useMargins,
});
const MARGINS = useMargins ? 8 : 0;
// We can't take advantage of isDraggable or isResizable due to performance concerns:
// https://github.com/STRML/react-grid-layout/issues/240
return (
<ReactGridLayout
width={lastValidGridSize}
className={classes}
isDraggable={true}
isResizable={true}
// There is a bug with d3 + firefox + elements using transforms.
// See https://github.com/elastic/kibana/issues/16870 for more context.
useCSSTransforms={false}
margin={[MARGINS, MARGINS]}
cols={DASHBOARD_GRID_COLUMN_COUNT}
rowHeight={DASHBOARD_GRID_HEIGHT}
// Pass the named classes of what should get the dragging handle
// (.doesnt-exist literally doesnt exist)
draggableHandle={isViewMode ? '.doesnt-exist' : '.dshPanel__dragger'}
layout={layout}
onLayoutChange={onLayoutChange}
onResize={({}, {}, {}, {}, event) => ensureWindowScrollsToBottom(event)}
>
{children}
</ReactGridLayout>
);
}
// Using sizeMe sets up the grid to be re-rendered automatically not only when the window size changes, but also
// when the container size changes, so it works for Full Screen mode switches.
const config = { monitorWidth: true };
const ResponsiveSizedGrid = sizeMe(config)(ResponsiveGrid);
interface Props extends ReactIntl.InjectedIntlProps {
panels: SavedDashboardPanelMap;
getEmbeddableFactory: (panelType: string) => EmbeddableFactory;
dashboardViewMode: DashboardViewMode.EDIT | DashboardViewMode.VIEW;
onPanelsUpdated: (updatedPanels: SavedDashboardPanelMap) => void;
maximizedPanelId?: string;
useMargins: boolean;
}
interface State {
focusedPanelIndex?: string;
isLayoutInvalid: boolean;
layout?: GridData[];
}
interface PanelLayout extends Layout {
i: string;
}
class DashboardGridUi extends React.Component<Props, State> {
// A mapping of panelIndexes to grid items so we can set the zIndex appropriately on the last focused
// item.
private gridItems = {} as { [key: string]: HTMLDivElement | null };
// A mapping of panel type to embeddable handlers. Because this function reaches out of react and into angular,
// if done in the render method, it appears to be triggering a scope.apply, which appears to be trigging a setState
// call inside TSVB visualizations. Moving the function out of render appears to fix the issue. See
// https://github.com/elastic/kibana/issues/14802 for more info.
// This is probably a better implementation anyway so the handlers are cached.
// @type {Object.<string, EmbeddableFactory>}
private embeddableFactoryMap: { [s: string]: EmbeddableFactory } = {};
constructor(props: Props) {
super(props);
let isLayoutInvalid = false;
let layout;
try {
layout = this.buildLayoutFromPanels();
} catch (error) {
isLayoutInvalid = true;
toastNotifications.addDanger({
title: props.intl.formatMessage({
id: 'kbn.dashboard.dashboardGrid.unableToLoadDashboardDangerMessage',
defaultMessage: 'Unable to load dashboard.',
}),
text: error.message,
});
window.location.hash = DashboardConstants.LANDING_PAGE_PATH;
}
this.state = {
focusedPanelIndex: undefined,
layout,
isLayoutInvalid,
};
}
public buildLayoutFromPanels(): GridData[] {
return _.map(this.props.panels, panel => {
return (panel as SavedDashboardPanel).gridData;
});
}
public createEmbeddableFactoriesMap(panels: SavedDashboardPanelMap) {
Object.values(panels).map(panel => {
if (!this.embeddableFactoryMap[panel.type]) {
this.embeddableFactoryMap[panel.type] = this.props.getEmbeddableFactory(panel.type);
}
});
}
public componentWillMount() {
this.createEmbeddableFactoriesMap(this.props.panels);
}
public componentWillReceiveProps(nextProps: Props) {
this.createEmbeddableFactoriesMap(nextProps.panels);
}
public onLayoutChange = (layout: PanelLayout[]) => {
const { onPanelsUpdated, panels } = this.props;
const updatedPanels = layout.reduce((updatedPanelsAcc: SavedDashboardPanelMap, panelLayout) => {
updatedPanelsAcc[panelLayout.i] = {
...panels[panelLayout.i],
panelIndex: panelLayout.i,
gridData: _.pick<GridData, PanelLayout>(panelLayout, ['x', 'y', 'w', 'h', 'i']),
};
return updatedPanelsAcc;
}, {});
onPanelsUpdated(updatedPanels);
};
public onPanelFocused = (focusedPanelIndex: string): void => {
this.setState({ focusedPanelIndex });
};
public onPanelBlurred = (blurredPanelIndex: string): void => {
if (this.state.focusedPanelIndex === blurredPanelIndex) {
this.setState({ focusedPanelIndex: undefined });
}
};
public renderDOM() {
const { panels, maximizedPanelId } = this.props;
const { focusedPanelIndex } = this.state;
// Part of our unofficial API - need to render in a consistent order for plugins.
const panelsInOrder = Object.keys(panels).map(
(key: string) => panels[key] as SavedDashboardPanel
);
panelsInOrder.sort((panelA, panelB) => {
if (panelA.gridData.y === panelB.gridData.y) {
return panelA.gridData.x - panelB.gridData.x;
} else {
return panelA.gridData.y - panelB.gridData.y;
}
});
return _.map(panelsInOrder, panel => {
const expandPanel = maximizedPanelId !== undefined && maximizedPanelId === panel.panelIndex;
const hidePanel = maximizedPanelId !== undefined && maximizedPanelId !== panel.panelIndex;
const classes = classNames({
'dshDashboardGrid__item--expanded': expandPanel,
'dshDashboardGrid__item--hidden': hidePanel,
});
return (
<div
style={{ zIndex: focusedPanelIndex === panel.panelIndex ? 2 : 'auto' }}
className={classes}
key={panel.panelIndex}
ref={reactGridItem => {
this.gridItems[panel.panelIndex] = reactGridItem;
}}
>
<DashboardPanel
panelId={panel.panelIndex}
embeddableFactory={this.embeddableFactoryMap[panel.type]}
onPanelFocused={this.onPanelFocused}
onPanelBlurred={this.onPanelBlurred}
/>
</div>
);
});
}
public render() {
if (this.state.isLayoutInvalid) {
return null;
}
const { dashboardViewMode, maximizedPanelId, useMargins } = this.props;
const isViewMode = dashboardViewMode === DashboardViewMode.VIEW;
return (
<ResponsiveSizedGrid
isViewMode={isViewMode}
layout={this.buildLayoutFromPanels()}
onLayoutChange={this.onLayoutChange}
maximizedPanelId={maximizedPanelId}
useMargins={useMargins}
>
{this.renderDOM()}
</ResponsiveSizedGrid>
);
}
}
export const DashboardGrid = injectI18n(DashboardGridUi);

View file

@ -1,54 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { updatePanels } from '../actions';
import { getPanels, getUseMargins, getViewMode } from '../selectors';
import { DashboardViewMode } from '../selectors/types';
import { DashboardGrid } from './dashboard_grid';
import { SavedDashboardPanelMap } from '../types';
interface DashboardGridContainerStateProps {
panels: SavedDashboardPanelMap;
dashboardViewMode: DashboardViewMode;
useMargins: boolean;
}
interface DashboardGridContainerDispatchProps {
onPanelsUpdated(updatedPanels: SavedDashboardPanelMap): void;
}
const mapStateToProps = ({ dashboard }: any): any => ({
panels: getPanels(dashboard),
dashboardViewMode: getViewMode(dashboard),
useMargins: getUseMargins(dashboard),
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
onPanelsUpdated: (updatedPanels: SavedDashboardPanelMap) => dispatch(updatePanels(updatedPanels)),
});
export const DashboardGridContainer = connect<
DashboardGridContainerStateProps,
DashboardGridContainerDispatchProps
>(
mapStateToProps,
mapDispatchToProps
)(DashboardGrid);

View file

@ -1,20 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/
export { DashboardGridContainer as DashboardGrid } from './dashboard_grid_container';

View file

@ -0,0 +1,142 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 '../np_core.test.mocks';
import {
convertSavedDashboardPanelToPanelState,
convertPanelStateToSavedDashboardPanel,
} from './embeddable_saved_object_converters';
import { SavedDashboardPanel } from '../types';
import { DashboardPanelState } from '../../../../dashboard_embeddable_container/public';
import { EmbeddableInput } from '../../../../embeddable_api/public';
interface CustomInput extends EmbeddableInput {
something: string;
}
test('convertSavedDashboardPanelToPanelState', () => {
const savedDashboardPanel: SavedDashboardPanel = {
type: 'search',
embeddableConfig: {
something: 'hi!',
},
id: 'savedObjectId',
panelIndex: '123',
gridData: {
x: 0,
y: 0,
h: 15,
w: 15,
i: '123',
},
version: '7.0.0',
};
expect(convertSavedDashboardPanelToPanelState(savedDashboardPanel, true)).toEqual({
gridData: {
x: 0,
y: 0,
h: 15,
w: 15,
i: '123',
},
explicitInput: {
something: 'hi!',
id: '123',
},
savedObjectId: 'savedObjectId',
type: 'search',
});
});
test('convertSavedDashboardPanelToPanelState does not include undefined id', () => {
const savedDashboardPanel: SavedDashboardPanel = {
type: 'search',
embeddableConfig: {
something: 'hi!',
},
panelIndex: '123',
gridData: {
x: 0,
y: 0,
h: 15,
w: 15,
i: '123',
},
version: '7.0.0',
};
const converted = convertSavedDashboardPanelToPanelState(savedDashboardPanel, false);
expect(converted.hasOwnProperty('savedObjectId')).toBe(false);
});
test('convertPanelStateToSavedDashboardPanel', () => {
const dashboardPanel: DashboardPanelState<CustomInput> = {
gridData: {
x: 0,
y: 0,
h: 15,
w: 15,
i: '123',
},
savedObjectId: 'savedObjectId',
explicitInput: {
something: 'hi!',
id: '123',
},
type: 'search',
};
expect(convertPanelStateToSavedDashboardPanel(dashboardPanel)).toEqual({
type: 'search',
embeddableConfig: {
something: 'hi!',
},
id: 'savedObjectId',
panelIndex: '123',
gridData: {
x: 0,
y: 0,
h: 15,
w: 15,
i: '123',
},
version: '6.3.0',
});
});
test('convertPanelStateToSavedDashboardPanel will not add an undefined id when not needed', () => {
const dashboardPanel: DashboardPanelState<CustomInput> = {
gridData: {
x: 0,
y: 0,
h: 15,
w: 15,
i: '123',
},
explicitInput: {
id: '123',
something: 'hi!',
},
type: 'search',
};
const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel);
expect(converted.hasOwnProperty('id')).toBe(false);
});

View file

@ -0,0 +1,55 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { omit } from 'lodash';
import { DashboardPanelState } from 'plugins/dashboard_embeddable_container';
import chrome from 'ui/chrome';
import { SavedDashboardPanel } from '../types';
export function convertSavedDashboardPanelToPanelState(
savedDashboardPanel: SavedDashboardPanel,
useMargins: boolean
): DashboardPanelState {
return {
type: savedDashboardPanel.type,
gridData: savedDashboardPanel.gridData,
...(savedDashboardPanel.id !== undefined && { savedObjectId: savedDashboardPanel.id }),
explicitInput: {
id: savedDashboardPanel.panelIndex,
...(savedDashboardPanel.title !== undefined && { title: savedDashboardPanel.title }),
...savedDashboardPanel.embeddableConfig,
},
};
}
export function convertPanelStateToSavedDashboardPanel(
panelState: DashboardPanelState
): SavedDashboardPanel {
const customTitle: string | undefined = panelState.explicitInput.title
? (panelState.explicitInput.title as string)
: undefined;
return {
version: chrome.getKibanaVersion(),
type: panelState.type,
gridData: panelState.gridData,
panelIndex: panelState.explicitInput.id,
embeddableConfig: omit(panelState.explicitInput, 'id'),
...(customTitle && { title: customTitle }),
...(panelState.savedObjectId !== undefined && { id: panelState.savedObjectId }),
};
}

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { DashboardViewMode } from '../dashboard_view_mode';
import { ViewMode } from '../../../../embeddable_api/public';
import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard';
import { DashboardAppStateDefaults } from '../types';
@ -34,7 +34,6 @@ export function getAppStateDefaults(
options: savedDashboard.optionsJSON ? JSON.parse(savedDashboard.optionsJSON) : {},
query: savedDashboard.getQuery(),
filters: savedDashboard.getFilters(),
viewMode:
savedDashboard.id || hideWriteControls ? DashboardViewMode.VIEW : DashboardViewMode.EDIT,
viewMode: savedDashboard.id || hideWriteControls ? ViewMode.VIEW : ViewMode.EDIT,
};
}

View file

@ -16,23 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
jest.mock(
'ui/chrome',
() => ({
getKibanaVersion: () => '6.3.0',
}),
{ virtual: true }
);
jest.mock(
'ui/notify',
() => ({
toastNotifications: {
addDanger: () => {},
},
}),
{ virtual: true }
);
import '../np_core.test.mocks';
import { SavedDashboardPanel } from '../types';
import { migrateAppState } from './migrate_app_state';

View file

@ -36,7 +36,6 @@ jest.mock(
import { migratePanelsTo730 } from './migrate_to_730_panels';
import { SavedDashboardPanelTo60, SavedDashboardPanel730ToLatest } from '../types';
import { DEFAULT_PANEL_WIDTH, DEFAULT_PANEL_HEIGHT } from '../dashboard_constants';
import {
RawSavedDashboardPanelTo60,
RawSavedDashboardPanel610,
@ -44,6 +43,10 @@ import {
RawSavedDashboardPanel630,
RawSavedDashboardPanel640To720,
} from './types';
import {
DEFAULT_PANEL_WIDTH,
DEFAULT_PANEL_HEIGHT,
} from '../../../../dashboard_embeddable_container/public';
test('6.0 migrates uiState, sort, scales, and gridData', async () => {
const uiState = {

View file

@ -19,7 +19,7 @@
import { i18n } from '@kbn/i18n';
import semver from 'semver';
import { GridData } from 'src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/types';
import { DEFAULT_PANEL_WIDTH, DEFAULT_PANEL_HEIGHT } from '../dashboard_constants';
import {
RawSavedDashboardPanelTo60,
RawSavedDashboardPanel630,
@ -112,7 +112,17 @@ function migratePre61PanelToLatest(
? PANEL_HEIGHT_SCALE_FACTOR_WITH_MARGINS
: PANEL_HEIGHT_SCALE_FACTOR;
// These are snapshotted here instead of imported form dashboard_embeddable_container because
// this function is called from both client and server side, and having an import from a public
// folder will cause errors for the server side version. Also, this is only run for the point in time
// from panels created in < 7.3 so maybe using a snapshot of the default values when this migration was
// written is more correct anyway.
const DASHBOARD_GRID_COLUMN_COUNT = 48;
const DEFAULT_PANEL_WIDTH = DASHBOARD_GRID_COLUMN_COUNT / 2;
const DEFAULT_PANEL_HEIGHT = 15;
const { columns, sort, row, col, size_x: sizeX, size_y: sizeY, ...rest } = panel;
return {
...rest,
version,

View file

@ -0,0 +1,65 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { fatalErrorsServiceMock, notificationServiceMock } from '../../../../../core/public/mocks';
let modalContents: React.Component;
export const getModalContents = () => modalContents;
jest.doMock('ui/new_platform', () => {
return {
npStart: {
core: {
overlays: {
openFlyout: jest.fn(),
openModal: (component: React.Component) => {
modalContents = component;
return {
close: jest.fn(),
};
},
},
},
},
npSetup: {
core: {
fatalErrors: fatalErrorsServiceMock.createSetupContract(),
notifications: notificationServiceMock.createSetupContract(),
},
},
};
});
jest.doMock('ui/metadata', () => ({
metadata: {
branch: 'my-metadata-branch',
version: 'my-metadata-version',
},
}));
jest.doMock('ui/capabilities', () => ({
uiCapabilities: {
visualize: {
save: true,
},
},
}));
jest.doMock('ui/chrome', () => ({ getKibanaVersion: () => '6.3.0', setVisible: () => {} }));

View file

@ -1,52 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DashboardPanel matches snapshot 1`] = `
<div
class="euiPanel dshPanel dshPanel--editing"
data-test-subj="dashboardPanel"
>
<div
class="dshPanel__header"
data-test-subj="dashboardPanelHeading-myembeddabletitle"
>
<div
aria-label="Dashboard panel: my embeddable title"
class="dshPanel__title dshPanel__dragger"
data-test-subj="dashboardPanelTitle"
title="my embeddable title"
>
my embeddable title
</div>
<div
class="euiPopover euiPopover--anchorDownRight euiPopover--withTitle dshPanel_optionsMenuPopover"
data-test-subj="dashboardPanelContextMenuClosed"
id="dashboardPanelContextMenu"
>
<div
class="euiPopover__anchor"
>
<button
aria-label="Panel options"
class="euiButtonIcon euiButtonIcon--text dshPanel_optionsMenuButton"
data-test-subj="dashboardPanelToggleMenuIcon"
type="button"
>
<svg
aria-hidden="true"
class="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon"
focusable="false"
height="16"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
/>
</button>
</div>
</div>
</div>
<div
class="panel-content"
id="embeddedPanel"
/>
</div>
`;

View file

@ -1,96 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PanelError renders given React Element 1`] = `
<div
class="dshPanel__error panel-content"
>
<div
class="euiText euiText--extraSmall"
>
<div
class="euiTextColor euiTextColor--subdued"
>
<svg
class="euiIcon euiIcon--medium euiIcon--danger euiIcon-isLoading"
focusable="false"
height="16"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
/>
<div
class="euiSpacer euiSpacer--s"
/>
<div>
test
</div>
</div>
</div>
</div>
`;
exports[`PanelError renders plain string 1`] = `
<div
class="dshPanel__error panel-content"
>
<div
class="euiText euiText--extraSmall"
>
<div
class="euiTextColor euiTextColor--subdued"
>
<svg
class="euiIcon euiIcon--medium euiIcon--danger euiIcon-isLoading"
focusable="false"
height="16"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
/>
<div
class="euiSpacer euiSpacer--s"
/>
<div>
<p>
test
</p>
</div>
</div>
</div>
</div>
`;
exports[`PanelError renders string with markdown link 1`] = `
<div
class="dshPanel__error panel-content"
>
<div
class="euiText euiText--extraSmall"
>
<div
class="euiTextColor euiTextColor--subdued"
>
<svg
class="euiIcon euiIcon--medium euiIcon--danger euiIcon-isLoading"
focusable="false"
height="16"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
/>
<div
class="euiSpacer euiSpacer--s"
/>
<div>
<p>
<a
href="http://www.elastic.co/"
>
test
</a>
</p>
</div>
</div>
</div>
</div>
`;

View file

@ -1,81 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 expect from '@kbn/expect';
import { createPanelState } from '../panel_state';
import { SavedDashboardPanel } from '../../types';
function createPanelWithDimensions(
x: number,
y: number,
w: number,
h: number
): SavedDashboardPanel {
return {
id: 'foo',
version: '6.3.0',
type: 'bar',
panelIndex: 'test',
title: 'test title',
gridData: {
x,
y,
w,
h,
i: 'an id',
},
embeddableConfig: {},
};
}
describe('Panel state', () => {
it('finds a spot on the right', () => {
// Default setup after a single panel, of default size, is on the grid
const panels = [createPanelWithDimensions(0, 0, 24, 30)];
const panel = createPanelState('1', 'a type', '1', panels);
expect(panel.gridData.x).to.equal(24);
expect(panel.gridData.y).to.equal(0);
});
it('finds a spot on the right when the panel is taller than any other panel on the grid', () => {
// Should be a little empty spot on the right.
const panels = [
createPanelWithDimensions(0, 0, 24, 45),
createPanelWithDimensions(24, 0, 24, 30),
];
const panel = createPanelState('1', 'a type', '1', panels);
expect(panel.gridData.x).to.equal(24);
expect(panel.gridData.y).to.equal(30);
});
it('finds an empty spot in the middle of the grid', () => {
const panels = [
createPanelWithDimensions(0, 0, 48, 5),
createPanelWithDimensions(0, 5, 4, 30),
createPanelWithDimensions(40, 5, 4, 30),
createPanelWithDimensions(0, 55, 48, 5),
];
const panel = createPanelState('1', 'a type', '1', panels);
expect(panel.gridData.x).to.equal(4);
expect(panel.gridData.y).to.equal(5);
});
});

View file

@ -1,190 +0,0 @@
/**
* EDITING MODE
* Use .dshLayout--editing to target editing state because
* .dshPanel--editing doesn't get updating without a hard refresh
*/
.dshPanel {
z-index: auto;
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
position: relative;
// SASSTODO: The inheritence factor stemming from embeddables makes this class hard to change
.panel-content {
display: flex;
flex: 1 1 100%;
height: auto;
z-index: 1;
min-height: 0; // Absolute must for Firefox to scroll contents
}
// SASSTODO: Pretty sure this doesn't do anything since the flex-basis 100%,
// but it MIGHT be fixing IE
.panel-content--fullWidth {
width: 100%;
}
.panel-content-isLoading {
// completely center the loading indicator
justify-content: center;
align-items: center;
}
/**
* 1. We want the kbnDocTable__container to scroll only when embedded in a dashboard panel
* 2. Fix overflow of vis's specifically for inside dashboard panels, lets the panel decide the overflow
* 3. Force a better looking scrollbar
*/
.kbnDocTable__container {
@include euiScrollBar; /* 3 */
flex: 1 1 0; /* 1 */
overflow: auto; /* 1 */
}
.visualization {
@include euiScrollBar; /* 3 */
}
.visualization .visChart__container {
overflow: visible; /* 2 */
}
.visLegend__toggle {
border-bottom-right-radius: 0;
border-top-left-radius: 0;
}
}
.dshLayout--editing .dshPanel {
border-style: dashed;
border-color: $euiColorMediumShade;
transition: all $euiAnimSpeedFast $euiAnimSlightResistance;
&:hover,
&:focus {
@include euiSlightShadowHover;
}
}
// LAYOUT MODES
// Adjust borders/etc... for non-spaced out and expanded panels
.dshLayout-withoutMargins,
.dshDashboardGrid__item--expanded {
.dshPanel {
box-shadow: none;
border-radius: 0;
}
}
// Remove border color unless in editing mode
.dshLayout-withoutMargins:not(.dshLayout--editing),
.dshDashboardGrid__item--expanded {
.dshPanel {
border-color: transparent;
}
}
// HEADER
.dshPanel__header {
flex: 0 0 auto;
display: flex;
// ensure menu button is on the right even if the title doesn't exist
justify-content: flex-end;
}
.dshPanel__title {
@include euiTextTruncate;
@include euiTitle('xxxs');
line-height: 1.5;
flex-grow: 1;
&:not(:empty) {
padding: ($euiSizeXS * 1.5) $euiSizeS 0;
}
}
.dshLayout--editing {
.dshPanel__dragger {
transition: background-color $euiAnimSpeedFast $euiAnimSlightResistance;
}
.dshPanel__dragger:hover {
background-color: $dshEditingModeHoverColor;
cursor: move;
}
}
.dshPanel__dragger:not(.dshPanel__title) {
flex-grow: 1;
}
.dshPanel__header--floater {
position: absolute;
right: 0;
top: 0;
left: 0;
z-index: $euiZLevel1;
}
// OPTIONS MENU
/**
* 1. Use opacity to make this element accessible to screen readers and keyboard.
* 2. Show on focus to enable keyboard accessibility.
* 3. Always show in editing mode
*/
.dshPanel_optionsMenuButton {
background-color: transparentize($euiColorDarkestShade, .9);
border-bottom-right-radius: 0;
border-top-left-radius: 0;
&:focus {
background-color: $euiFocusBackgroundColor;
}
}
.dshPanel .visLegend__toggle,
.dshPanel_optionsMenuButton {
opacity: 0; /* 1 */
&:focus {
opacity: 1; /* 2 */
}
}
.dshPanel_optionsMenuPopover[class*="-isOpen"],
.dshPanel:hover {
.dshPanel_optionsMenuButton,
.visLegend__toggle {
opacity: 1;
}
}
.dshLayout--editing {
.dshPanel_optionsMenuButton,
.dshPanel .visLegend__toggle {
opacity: 1; /* 3 */
}
}
// ERROR
.dshPanel__error {
text-align: center;
justify-content: center;
flex-direction: column;
overflow: auto;
text-align: center;
.fa-exclamation-triangle {
font-size: $euiFontSizeXL;
color: $euiColorDanger;
}
}

View file

@ -1,2 +0,0 @@
@import "./dashboard_panel";
@import 'panel_header/panel_options_menu_form';

View file

@ -1,93 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/
// TODO: remove this when EUI supports types for this.
// @ts-ignore: implicit any for JS file
import { takeMountedSnapshot } from '@elastic/eui/lib/test';
import _ from 'lodash';
import React from 'react';
import { Provider } from 'react-redux';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { store } from '../../store';
// @ts-ignore: implicit any for JS file
import { getEmbeddableFactoryMock } from '../__tests__/get_embeddable_factories_mock';
import { embeddableIsInitialized, setPanels, updateTimeRange, updateViewMode } from '../actions';
import { DashboardViewMode } from '../dashboard_view_mode';
import { DashboardPanel, DashboardPanelUiProps } from './dashboard_panel';
import { PanelError } from './panel_error';
function getProps(props = {}): DashboardPanelUiProps {
const defaultTestProps = {
panel: { panelIndex: 'foo1' },
viewOnlyMode: false,
initialized: true,
lastReloadRequestTime: 0,
embeddableFactory: getEmbeddableFactoryMock(),
};
return _.defaultsDeep(props, defaultTestProps);
}
beforeAll(() => {
store.dispatch(updateTimeRange({ to: 'now', from: 'now-15m' }));
store.dispatch(updateViewMode(DashboardViewMode.EDIT));
store.dispatch(
setPanels({
foo1: {
panelIndex: 'foo1',
id: 'hi',
version: '123',
type: 'viz',
embeddableConfig: {},
gridData: {
x: 1,
y: 1,
w: 1,
h: 1,
i: 'hi',
},
},
})
);
const metadata = { title: 'my embeddable title', editUrl: 'editme' };
store.dispatch(embeddableIsInitialized({ metadata, panelId: 'foo1' }));
});
test('DashboardPanel matches snapshot', () => {
const component = mountWithIntl(
<Provider store={store}>
<DashboardPanel.WrappedComponent {...getProps()} />
</Provider>
);
expect(takeMountedSnapshot(component)).toMatchSnapshot();
});
test('renders an error when error prop is passed', () => {
const props = getProps({
error: 'Simulated error',
});
const component = mountWithIntl(
<Provider store={store}>
<DashboardPanel.WrappedComponent {...props} />
</Provider>
);
const panelError = component.find(PanelError);
expect(panelError.length).toBe(1);
});

View file

@ -1,202 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { EuiLoadingChart, EuiPanel } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import classNames from 'classnames';
import _ from 'lodash';
import React from 'react';
import {
ContainerState,
Embeddable,
EmbeddableFactory,
EmbeddableMetadata,
EmbeddableState,
} from 'ui/embeddable';
import { EmbeddableErrorAction } from '../actions';
import { PanelId } from '../selectors';
import { PanelError } from './panel_error';
import { PanelHeader } from './panel_header';
import { SavedDashboardPanel } from '../types';
export interface DashboardPanelProps {
viewOnlyMode: boolean;
onPanelFocused?: (panelIndex: PanelId) => void;
onPanelBlurred?: (panelIndex: PanelId) => void;
error?: string | object;
destroy: () => void;
containerState: ContainerState;
embeddableFactory: EmbeddableFactory;
lastReloadRequestTime?: number;
embeddableStateChanged: (embeddableStateChanges: EmbeddableState) => void;
embeddableIsInitialized: (embeddableIsInitializing: EmbeddableMetadata) => void;
embeddableError: (errorMessage: EmbeddableErrorAction) => void;
embeddableIsInitializing: () => void;
initialized: boolean;
panel: SavedDashboardPanel;
className?: string;
}
export interface DashboardPanelUiProps extends DashboardPanelProps {
intl: InjectedIntl;
}
interface State {
error: string | null;
}
class DashboardPanelUi extends React.Component<DashboardPanelUiProps, State> {
[panel: string]: any;
public mounted: boolean;
public embeddable!: Embeddable;
private panelElement?: HTMLDivElement;
constructor(props: DashboardPanelUiProps) {
super(props);
this.state = {
error: props.embeddableFactory
? null
: props.intl.formatMessage({
id: 'kbn.dashboard.panel.noEmbeddableFactoryErrorMessage',
defaultMessage: 'The feature to render this panel is missing.',
}),
};
this.mounted = false;
}
public async componentDidMount() {
this.mounted = true;
const {
initialized,
embeddableFactory,
embeddableIsInitializing,
panel,
embeddableStateChanged,
embeddableIsInitialized,
embeddableError,
} = this.props;
if (!initialized) {
embeddableIsInitializing();
embeddableFactory
// @ts-ignore -- going away with Embeddable V2
.create(panel, embeddableStateChanged)
.then((embeddable: Embeddable) => {
if (this.mounted) {
this.embeddable = embeddable;
embeddableIsInitialized(embeddable.metadata);
this.embeddable.render(this.panelElement!, this.props.containerState);
} else {
embeddable.destroy();
}
})
.catch((error: { message: EmbeddableErrorAction }) => {
if (this.mounted) {
embeddableError(error.message);
}
});
}
}
public componentWillUnmount() {
this.props.destroy();
this.mounted = false;
if (this.embeddable) {
this.embeddable.destroy();
}
}
public onFocus = () => {
const { onPanelFocused, panel } = this.props;
if (onPanelFocused) {
onPanelFocused(panel.panelIndex);
}
};
public onBlur = () => {
const { onPanelBlurred, panel } = this.props;
if (onPanelBlurred) {
onPanelBlurred(panel.panelIndex);
}
};
public renderEmbeddableViewport() {
const classes = classNames('panel-content', {
'panel-content-isLoading': !this.props.initialized,
});
return (
<div
id="embeddedPanel"
className={classes}
ref={panelElement => (this.panelElement = panelElement || undefined)}
>
{!this.props.initialized && <EuiLoadingChart size="l" mono />}
</div>
);
}
public shouldComponentUpdate(nextProps: DashboardPanelUiProps) {
if (this.embeddable && !_.isEqual(nextProps.containerState, this.props.containerState)) {
this.embeddable.onContainerStateChanged(nextProps.containerState);
}
if (this.embeddable && nextProps.lastReloadRequestTime !== this.props.lastReloadRequestTime) {
this.embeddable.reload();
}
return nextProps.error !== this.props.error || nextProps.initialized !== this.props.initialized;
}
public renderEmbeddedError() {
return <PanelError error={this.props.error} />;
}
public renderContent() {
const { error } = this.props;
if (error) {
return this.renderEmbeddedError();
} else {
return this.renderEmbeddableViewport();
}
}
public render() {
const { viewOnlyMode, panel } = this.props;
const classes = classNames('dshPanel', this.props.className, {
'dshPanel--editing': !viewOnlyMode,
});
return (
<EuiPanel
className={classes}
data-test-subj="dashboardPanel"
onFocus={this.onFocus}
onBlur={this.onBlur}
paddingSize="none"
>
<PanelHeader panelId={panel.panelIndex} embeddable={this.embeddable} />
{this.renderContent()}
</EuiPanel>
);
}
}
export const DashboardPanel = injectI18n(DashboardPanelUi);

View file

@ -1,84 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 _ from 'lodash';
import React from 'react';
import { Provider } from 'react-redux';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { store } from '../../store';
// @ts-ignore: implicit for any JS file
import { getEmbeddableFactoryMock } from '../__tests__/get_embeddable_factories_mock';
import { setPanels, updateTimeRange, updateViewMode } from '../actions';
import { DashboardViewMode } from '../dashboard_view_mode';
import { PanelError } from '../panel/panel_error';
import {
DashboardPanelContainer,
DashboardPanelContainerOwnProps,
} from './dashboard_panel_container';
function getProps(props = {}): DashboardPanelContainerOwnProps {
const defaultTestProps = {
panelId: 'foo1',
embeddableFactory: getEmbeddableFactoryMock(),
};
return _.defaultsDeep(props, defaultTestProps);
}
beforeAll(() => {
store.dispatch(updateViewMode(DashboardViewMode.EDIT));
store.dispatch(updateTimeRange({ to: 'now', from: 'now-15m' }));
store.dispatch(
setPanels({
foo1: {
panelIndex: 'foo1',
id: 'hi',
version: '123',
type: 'viz',
embeddableConfig: {},
gridData: {
x: 1,
y: 1,
w: 1,
h: 1,
i: 'hi',
},
},
})
);
});
test('renders an error when embeddableFactory.create throws an error', done => {
const props = getProps();
props.embeddableFactory.create = () => {
return new Promise(() => {
throw new Error('simulated error');
});
};
const component = mountWithIntl(
<Provider store={store}>
<DashboardPanelContainer {...props} />
</Provider>
);
setTimeout(() => {
component.update();
const panelError = component.find(PanelError);
expect(panelError.length).toBe(1);
done();
}, 0);
});

View file

@ -1,125 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { i18n } from '@kbn/i18n';
import { connect } from 'react-redux';
import { Action } from 'redux-actions';
import { ThunkDispatch } from 'redux-thunk';
import {
ContainerState,
EmbeddableFactory,
EmbeddableMetadata,
EmbeddableState,
} from 'ui/embeddable';
import { CoreKibanaState } from '../../selectors';
import {
deletePanel,
embeddableError,
EmbeddableErrorAction,
embeddableIsInitialized,
embeddableIsInitializing,
embeddableStateChanged,
} from '../actions';
import { DashboardViewMode } from '../dashboard_view_mode';
import {
getContainerState,
getEmbeddable,
getEmbeddableError,
getEmbeddableInitialized,
getFullScreenMode,
getPanel,
getPanelType,
getViewMode,
PanelId,
} from '../selectors';
import { DashboardPanel } from './dashboard_panel';
import { SavedDashboardPanel } from '../types';
export interface DashboardPanelContainerOwnProps {
panelId: PanelId;
embeddableFactory: EmbeddableFactory;
}
interface DashboardPanelContainerStateProps {
error?: string | object;
viewOnlyMode: boolean;
containerState: ContainerState;
initialized: boolean;
panel: SavedDashboardPanel;
lastReloadRequestTime?: number;
}
export interface DashboardPanelContainerDispatchProps {
destroy: () => void;
embeddableIsInitializing: () => void;
embeddableIsInitialized: (metadata: EmbeddableMetadata) => void;
embeddableStateChanged: (embeddableState: EmbeddableState) => void;
embeddableError: (errorMessage: EmbeddableErrorAction) => void;
}
const mapStateToProps = (
{ dashboard }: CoreKibanaState,
{ embeddableFactory, panelId }: DashboardPanelContainerOwnProps
) => {
const embeddable = getEmbeddable(dashboard, panelId);
let error = null;
if (!embeddableFactory) {
const panelType = getPanelType(dashboard, panelId);
error = i18n.translate('kbn.dashboard.panel.noFoundEmbeddableFactoryErrorMessage', {
defaultMessage: 'No embeddable factory found for panel type {panelType}',
values: { panelType },
});
} else {
error = (embeddable && getEmbeddableError(dashboard, panelId)) || '';
}
const lastReloadRequestTime = embeddable ? embeddable.lastReloadRequestTime : 0;
const initialized = embeddable ? getEmbeddableInitialized(dashboard, panelId) : false;
return {
error,
viewOnlyMode: getFullScreenMode(dashboard) || getViewMode(dashboard) === DashboardViewMode.VIEW,
containerState: getContainerState(dashboard, panelId),
initialized,
panel: getPanel(dashboard, panelId),
lastReloadRequestTime,
};
};
const mapDispatchToProps = (
dispatch: ThunkDispatch<CoreKibanaState, {}, Action<any>>,
{ panelId }: DashboardPanelContainerOwnProps
): DashboardPanelContainerDispatchProps => ({
destroy: () => dispatch(deletePanel(panelId)),
embeddableIsInitializing: () => dispatch(embeddableIsInitializing(panelId)),
embeddableIsInitialized: (metadata: EmbeddableMetadata) =>
dispatch(embeddableIsInitialized({ panelId, metadata })),
embeddableStateChanged: (embeddableState: EmbeddableState) =>
dispatch(embeddableStateChanged({ panelId, embeddableState })),
embeddableError: (errorMessage: EmbeddableErrorAction) =>
dispatch(embeddableError({ panelId, error: errorMessage })),
});
export const DashboardPanelContainer = connect<
DashboardPanelContainerStateProps,
DashboardPanelContainerDispatchProps,
DashboardPanelContainerOwnProps,
CoreKibanaState
>(
mapStateToProps,
mapDispatchToProps
)(DashboardPanel);

View file

@ -1,40 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/
// TODO: remove this when EUI supports types for this.
// @ts-ignore: implicit any for JS file
import { takeMountedSnapshot } from '@elastic/eui/lib/test';
import React from 'react';
import { PanelError } from './panel_error';
import { mount } from 'enzyme';
test('PanelError renders plain string', () => {
const component = mount(<PanelError error="test" />);
expect(takeMountedSnapshot(component)).toMatchSnapshot();
});
test('PanelError renders string with markdown link', () => {
const component = mount(<PanelError error="[test](http://www.elastic.co/)" />);
expect(takeMountedSnapshot(component)).toMatchSnapshot();
});
test('PanelError renders given React Element ', () => {
const component = mount(<PanelError error={<div>test</div>} />);
expect(takeMountedSnapshot(component)).toMatchSnapshot();
});

View file

@ -1,38 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui';
import ReactMarkdown from 'react-markdown';
import React from 'react';
export interface PanelErrorProps {
error: string | React.ReactNode;
}
export function PanelError({ error }: PanelErrorProps) {
return (
<div className="dshPanel__error panel-content">
<EuiText color="subdued" size="xs">
<EuiIcon type="alert" color="danger" />
<EuiSpacer size="s" />
{typeof error === 'string' ? <ReactMarkdown source={error} /> : error}
</EuiText>
</div>
);
}

View file

@ -1,3 +0,0 @@
.dshPanel__optionsMenuForm {
padding: $euiSize;
}

View file

@ -1,71 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { ContextMenuAction, ContextMenuPanel } from 'ui/embeddable';
import { DashboardViewMode } from '../../../dashboard_view_mode';
import { PanelOptionsMenuForm } from '../panel_options_menu_form';
export function getCustomizePanelAction({
onResetPanelTitle,
onUpdatePanelTitle,
closeContextMenu,
title,
}: {
onResetPanelTitle: () => void;
onUpdatePanelTitle: (title: string) => void;
closeContextMenu: () => void;
title?: string;
}): ContextMenuAction {
return new ContextMenuAction(
{
id: 'customizePanel',
parentPanelId: 'mainMenu',
},
{
childContextMenuPanel: new ContextMenuPanel(
{
id: 'panelSubOptionsMenu',
title: i18n.translate('kbn.dashboard.panel.customizePanelTitle', {
defaultMessage: 'Customize panel',
}),
},
{
getContent: () => (
<PanelOptionsMenuForm
onReset={onResetPanelTitle}
onUpdatePanelTitle={onUpdatePanelTitle}
title={title}
onClose={closeContextMenu}
/>
),
}
),
icon: <EuiIcon type="pencil" />,
isVisible: ({ containerState }) => containerState.viewMode === DashboardViewMode.EDIT,
getDisplayName: () => {
return i18n.translate('kbn.dashboard.panel.customizePanel.displayName', {
defaultMessage: 'Customize panel',
});
},
}
);
}

View file

@ -1,65 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ContextMenuAction } from 'ui/embeddable';
import { DashboardViewMode } from '../../../dashboard_view_mode';
/**
*
* @return {ContextMenuAction}
*/
export function getEditPanelAction() {
return new ContextMenuAction(
{
id: 'editPanel',
parentPanelId: 'mainMenu',
},
{
icon: <EuiIcon type="pencil" />,
isDisabled: ({ embeddable }) =>
!embeddable || !embeddable.metadata || !embeddable.metadata.editUrl,
isVisible: ({ containerState, embeddable }) => {
const canEditEmbeddable = Boolean(
embeddable && embeddable.metadata && embeddable.metadata.editable
);
const inDashboardEditMode = containerState.viewMode === DashboardViewMode.EDIT;
return canEditEmbeddable && inDashboardEditMode;
},
getHref: ({ embeddable }) => {
if (embeddable && embeddable.metadata.editUrl) {
return embeddable.metadata.editUrl;
}
},
getDisplayName: ({ embeddable }) => {
if (embeddable && embeddable.metadata.editLabel) {
return embeddable.metadata.editLabel;
}
return i18n.translate('kbn.dashboard.panel.editPanel.defaultDisplayName', {
defaultMessage: 'Edit',
});
},
}
);
}

View file

@ -1,87 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { ContextMenuAction } from 'ui/embeddable';
import { Inspector } from 'ui/inspector';
/**
* Returns the dashboard panel action for opening an inspector for a specific panel.
* This will check if the embeddable inside the panel actually exposes inspector adapters
* via its embeddable.getInspectorAdapters() method. If so - and if an inspector
* could be shown for those adapters - the inspector icon will be visible.
* @return {ContextMenuAction}
*/
export function getInspectorPanelAction({
closeContextMenu,
panelTitle,
}: {
closeContextMenu: () => void;
panelTitle?: string;
}) {
return new ContextMenuAction(
{
id: 'openInspector',
parentPanelId: 'mainMenu',
},
{
getDisplayName: () => {
return i18n.translate('kbn.dashboard.panel.inspectorPanel.displayName', {
defaultMessage: 'Inspect',
});
},
icon: <EuiIcon type="inspect" />,
isVisible: ({ embeddable }) => {
if (!embeddable) {
return false;
}
return Inspector.isAvailable(embeddable.getInspectorAdapters());
},
onClick: ({ embeddable }) => {
if (!embeddable) {
return;
}
closeContextMenu();
const adapters = embeddable.getInspectorAdapters();
if (!adapters) {
return;
}
const session = Inspector.open(adapters, {
title: panelTitle,
});
// Overwrite the embeddables.destroy() function to close the inspector
// before calling the original destroy method
const originalDestroy = embeddable.destroy;
embeddable.destroy = () => {
session.close();
if (originalDestroy) {
originalDestroy.call(embeddable);
}
};
// In case the inspector gets closed (otherwise), restore the original destroy function
session.onClose.finally(() => {
embeddable.destroy = originalDestroy;
});
},
}
);
}

View file

@ -1,50 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { ContextMenuAction } from 'ui/embeddable';
import { DashboardViewMode } from '../../../dashboard_view_mode';
/**
*
* @param {function} onDeletePanel
* @return {ContextMenuAction}
*/
export function getRemovePanelAction(onDeletePanel: () => void) {
return new ContextMenuAction(
{
id: 'deletePanel',
parentPanelId: 'mainMenu',
},
{
getDisplayName: () => {
return i18n.translate('kbn.dashboard.panel.removePanel.displayName', {
defaultMessage: 'Delete from dashboard',
});
},
icon: <EuiIcon type="trash" />,
isVisible: ({ containerState }) =>
containerState.viewMode === DashboardViewMode.EDIT && !containerState.isPanelExpanded,
onClick: onDeletePanel,
}
);
}

View file

@ -1,59 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { EuiIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { ContextMenuAction } from 'ui/embeddable';
/**
* Returns an action that toggles the panel into maximized or minimized state.
* @param {boolean} isExpanded
* @param {function} toggleExpandedPanel
* @return {ContextMenuAction}
*/
export function getToggleExpandPanelAction({
isExpanded,
toggleExpandedPanel,
}: {
isExpanded: boolean;
toggleExpandedPanel: () => void;
}) {
return new ContextMenuAction(
{
id: 'togglePanel',
parentPanelId: 'mainMenu',
},
{
getDisplayName: () => {
return isExpanded
? i18n.translate('kbn.dashboard.panel.toggleExpandPanel.expandedDisplayName', {
defaultMessage: 'Minimize',
})
: i18n.translate('kbn.dashboard.panel.toggleExpandPanel.notExpandedDisplayName', {
defaultMessage: 'Full screen',
});
},
// TODO: Update to minimize icon when https://github.com/elastic/eui/issues/837 is complete.
icon: <EuiIcon type={isExpanded ? 'expand' : 'expand'} />,
onClick: toggleExpandedPanel,
}
);
}

View file

@ -1,24 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/
export { getEditPanelAction } from './get_edit_panel_action';
export { getRemovePanelAction } from './get_remove_panel_action';
export { getCustomizePanelAction } from './get_customize_panel_action';
export { getToggleExpandPanelAction } from './get_toggle_expand_panel_action';
export { getInspectorPanelAction } from './get_inspector_panel_action';

View file

@ -1,86 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import classNames from 'classnames';
import React from 'react';
import { Embeddable } from 'ui/embeddable';
import { PanelId } from '../../selectors';
import { PanelOptionsMenuContainer } from './panel_options_menu_container';
export interface PanelHeaderProps {
title?: string;
panelId: PanelId;
embeddable?: Embeddable;
isViewOnlyMode: boolean;
hidePanelTitles: boolean;
}
interface PanelHeaderUiProps extends PanelHeaderProps {
intl: InjectedIntl;
}
function PanelHeaderUi({
title,
panelId,
embeddable,
isViewOnlyMode,
hidePanelTitles,
intl,
}: PanelHeaderUiProps) {
const classes = classNames('dshPanel__header', {
'dshPanel__header--floater': !title || hidePanelTitles,
});
if (isViewOnlyMode && (!title || hidePanelTitles)) {
return (
<div className={classes}>
<PanelOptionsMenuContainer panelId={panelId} embeddable={embeddable} />
</div>
);
}
return (
<div
className={classes}
data-test-subj={`dashboardPanelHeading-${(title || '').replace(/\s/g, '')}`}
>
<div
data-test-subj="dashboardPanelTitle"
className="dshPanel__title dshPanel__dragger"
title={title}
aria-label={intl.formatMessage(
{
id: 'kbn.dashboard.panel.dashboardPanelAriaLabel',
defaultMessage: 'Dashboard panel: {title}',
},
{
title,
}
)}
>
{hidePanelTitles ? '' : title}
</div>
<PanelOptionsMenuContainer panelId={panelId} embeddable={embeddable} />
</div>
);
}
export const PanelHeader = injectI18n(PanelHeaderUi);

View file

@ -1,102 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { ReactWrapper } from 'enzyme';
import _ from 'lodash';
import React from 'react';
import { Provider } from 'react-redux';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
// TODO: remove this when EUI supports types for this.
// @ts-ignore: implicit any for JS file
import { findTestSubject } from '@elastic/eui/lib/test';
import { store } from '../../../store';
import {
embeddableIsInitialized,
resetPanelTitle,
setPanels,
setPanelTitle,
updateTimeRange,
updateViewMode,
} from '../../actions';
import { DashboardViewMode } from '../../dashboard_view_mode';
import { PanelHeaderContainer, PanelHeaderContainerOwnProps } from './panel_header_container';
function getProps(props = {}): PanelHeaderContainerOwnProps {
const defaultTestProps = {
panelId: 'foo1',
};
return _.defaultsDeep(props, defaultTestProps);
}
let component: ReactWrapper;
beforeAll(() => {
store.dispatch(updateTimeRange({ to: 'now', from: 'now-15m' }));
store.dispatch(updateViewMode(DashboardViewMode.EDIT));
store.dispatch(
setPanels({
foo1: {
panelIndex: 'foo1',
id: 'hi',
version: '123',
type: 'viz',
embeddableConfig: {},
gridData: {
x: 1,
y: 1,
w: 1,
h: 1,
i: 'hi',
},
},
})
);
const metadata = { title: 'my embeddable title', editUrl: 'editme' };
store.dispatch(embeddableIsInitialized({ metadata, panelId: 'foo1' }));
});
afterAll(() => {
component.unmount();
});
test('Panel header shows embeddable title when nothing is set on the panel', () => {
component = mountWithIntl(
<Provider store={store}>
<PanelHeaderContainer {...getProps()} />
</Provider>
);
expect(findTestSubject(component, 'dashboardPanelTitle').text()).toBe('my embeddable title');
});
test('Panel header shows panel title when it is set on the panel', () => {
store.dispatch(setPanelTitle({ title: 'my custom panel title', panelId: 'foo1' }));
expect(findTestSubject(component, 'dashboardPanelTitle').text()).toBe('my custom panel title');
});
test('Panel header shows no panel title when it is set to an empty string on the panel', () => {
store.dispatch(setPanelTitle({ title: '', panelId: 'foo1' }));
expect(findTestSubject(component, 'dashboardPanelTitle').text()).toBe('');
});
test('Panel header shows embeddable title when the panel title is reset', () => {
store.dispatch(resetPanelTitle('foo1'));
expect(findTestSubject(component, 'dashboardPanelTitle').text()).toBe('my embeddable title');
});

View file

@ -1,69 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { connect } from 'react-redux';
import { Embeddable } from 'ui/embeddable';
import { DashboardViewMode } from '../../dashboard_view_mode';
import { PanelHeader } from './panel_header';
import { CoreKibanaState } from '../../../selectors';
import {
getEmbeddableTitle,
getFullScreenMode,
getHidePanelTitles,
getMaximizedPanelId,
getPanel,
getViewMode,
PanelId,
} from '../../selectors';
export interface PanelHeaderContainerOwnProps {
panelId: PanelId;
embeddable?: Embeddable;
}
interface PanelHeaderContainerStateProps {
title?: string;
isExpanded: boolean;
isViewOnlyMode: boolean;
hidePanelTitles: boolean;
}
const mapStateToProps = (
{ dashboard }: CoreKibanaState,
{ panelId }: PanelHeaderContainerOwnProps
) => {
const panel = getPanel(dashboard, panelId);
const embeddableTitle = getEmbeddableTitle(dashboard, panelId);
return {
title: panel.title === undefined ? embeddableTitle : panel.title,
isExpanded: getMaximizedPanelId(dashboard) === panelId,
isViewOnlyMode:
getFullScreenMode(dashboard) || getViewMode(dashboard) === DashboardViewMode.VIEW,
hidePanelTitles: getHidePanelTitles(dashboard),
};
};
export const PanelHeaderContainer = connect<
PanelHeaderContainerStateProps,
{},
PanelHeaderContainerOwnProps,
CoreKibanaState
>(mapStateToProps)(PanelHeader);

View file

@ -1,83 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import {
EuiButtonIcon,
EuiContextMenu,
EuiContextMenuPanelDescriptor,
EuiPopover,
} from '@elastic/eui';
export interface PanelOptionsMenuProps {
toggleContextMenu: () => void;
isPopoverOpen: boolean;
closeContextMenu: () => void;
panels: EuiContextMenuPanelDescriptor[];
isViewMode: boolean;
}
interface PanelOptionsMenuUiProps extends PanelOptionsMenuProps {
intl: InjectedIntl;
}
function PanelOptionsMenuUi({
toggleContextMenu,
isPopoverOpen,
closeContextMenu,
panels,
isViewMode,
intl,
}: PanelOptionsMenuUiProps) {
const button = (
<EuiButtonIcon
iconType={isViewMode ? 'boxesHorizontal' : 'gear'}
color="text"
className="dshPanel_optionsMenuButton"
aria-label={intl.formatMessage({
id: 'kbn.dashboard.panel.optionsMenu.panelOptionsButtonAriaLabel',
defaultMessage: 'Panel options',
})}
data-test-subj="dashboardPanelToggleMenuIcon"
onClick={toggleContextMenu}
/>
);
return (
<EuiPopover
id="dashboardPanelContextMenu"
className="dshPanel_optionsMenuPopover"
button={button}
isOpen={isPopoverOpen}
closePopover={closeContextMenu}
panelPaddingSize="none"
anchorPosition="downRight"
data-test-subj={
isPopoverOpen ? 'dashboardPanelContextMenuOpen' : 'dashboardPanelContextMenuClosed'
}
withTitle
>
<EuiContextMenu initialPanelId="mainMenu" panels={panels} />
</EuiPopover>
);
}
export const PanelOptionsMenu = injectI18n(PanelOptionsMenuUi);

View file

@ -1,221 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { EuiContextMenuPanelDescriptor } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { connect } from 'react-redux';
import {
buildEuiContextMenuPanels,
ContainerState,
ContextMenuPanel,
Embeddable,
} from 'ui/embeddable';
import { Dispatch } from 'redux';
import { panelActionsStore } from '../../store/panel_actions_store';
import {
getCustomizePanelAction,
getEditPanelAction,
getInspectorPanelAction,
getRemovePanelAction,
getToggleExpandPanelAction,
} from './panel_actions';
import { PanelOptionsMenu, PanelOptionsMenuProps } from './panel_options_menu';
import {
closeContextMenu,
deletePanel,
maximizePanel,
minimizePanel,
resetPanelTitle,
setPanelTitle,
setVisibleContextMenuPanelId,
} from '../../actions';
import { CoreKibanaState } from '../../../selectors';
import { DashboardViewMode } from '../../dashboard_view_mode';
import {
getContainerState,
getEmbeddable,
getEmbeddableEditUrl,
getEmbeddableTitle,
getMaximizedPanelId,
getPanel,
getViewMode,
getVisibleContextMenuPanelId,
PanelId,
} from '../../selectors';
interface PanelOptionsMenuContainerDispatchProps {
onDeletePanel: () => void;
onCloseContextMenu: () => void;
openContextMenu: () => void;
onMaximizePanel: () => void;
onMinimizePanel: () => void;
onResetPanelTitle: () => void;
onUpdatePanelTitle: (title: string) => void;
}
interface PanelOptionsMenuContainerOwnProps {
panelId: PanelId;
embeddable?: Embeddable;
}
interface PanelOptionsMenuContainerStateProps {
panelTitle?: string;
editUrl: string | null | undefined;
isExpanded: boolean;
containerState: ContainerState;
visibleContextMenuPanelId: PanelId | undefined;
isViewMode: boolean;
}
const mapStateToProps = (
{ dashboard }: CoreKibanaState,
{ panelId }: PanelOptionsMenuContainerOwnProps
) => {
const embeddable = getEmbeddable(dashboard, panelId);
const panel = getPanel(dashboard, panelId);
const embeddableTitle = getEmbeddableTitle(dashboard, panelId);
const containerState = getContainerState(dashboard, panelId);
const visibleContextMenuPanelId = getVisibleContextMenuPanelId(dashboard);
const viewMode = getViewMode(dashboard);
return {
panelTitle: panel.title === undefined ? embeddableTitle : panel.title,
editUrl: embeddable ? getEmbeddableEditUrl(dashboard, panelId) : null,
isExpanded: getMaximizedPanelId(dashboard) === panelId,
containerState,
visibleContextMenuPanelId,
isViewMode: viewMode === DashboardViewMode.VIEW,
};
};
/**
* @param dispatch {Function}
* @param embeddableFactory {EmbeddableFactory}
* @param panelId {string}
*/
const mapDispatchToProps = (
dispatch: Dispatch,
{ panelId }: PanelOptionsMenuContainerOwnProps
) => ({
onDeletePanel: () => {
dispatch(deletePanel(panelId));
},
onCloseContextMenu: () => dispatch(closeContextMenu()),
openContextMenu: () => dispatch(setVisibleContextMenuPanelId(panelId)),
onMaximizePanel: () => dispatch(maximizePanel(panelId)),
onMinimizePanel: () => dispatch(minimizePanel()),
onResetPanelTitle: () => dispatch(resetPanelTitle(panelId)),
onUpdatePanelTitle: (newTitle: string) => dispatch(setPanelTitle({ title: newTitle, panelId })),
});
const mergeProps = (
stateProps: PanelOptionsMenuContainerStateProps,
dispatchProps: PanelOptionsMenuContainerDispatchProps,
ownProps: PanelOptionsMenuContainerOwnProps
) => {
const {
isExpanded,
panelTitle,
containerState,
visibleContextMenuPanelId,
isViewMode,
} = stateProps;
const isPopoverOpen = visibleContextMenuPanelId === ownProps.panelId;
const {
onMaximizePanel,
onMinimizePanel,
onDeletePanel,
onResetPanelTitle,
onUpdatePanelTitle,
onCloseContextMenu,
openContextMenu,
} = dispatchProps;
const toggleContextMenu = () => (isPopoverOpen ? onCloseContextMenu() : openContextMenu());
// Outside click handlers will trigger for every closed context menu, we only want to react to clicks external to
// the currently opened menu.
const closeMyContextMenuPanel = () => {
if (isPopoverOpen) {
onCloseContextMenu();
}
};
const toggleExpandedPanel = () => {
// eslint-disable-next-line no-unused-expressions
isExpanded ? onMinimizePanel() : onMaximizePanel();
closeMyContextMenuPanel();
};
let panels: EuiContextMenuPanelDescriptor[] = [];
// Don't build the panels if the pop over is not open, or this gets expensive - this function is called once for
// every panel, every time any state changes.
if (isPopoverOpen) {
const contextMenuPanel = new ContextMenuPanel({
title: i18n.translate('kbn.dashboard.panel.optionsMenu.optionsContextMenuTitle', {
defaultMessage: 'Options',
}),
id: 'mainMenu',
});
const actions = [
getInspectorPanelAction({
closeContextMenu: closeMyContextMenuPanel,
panelTitle,
}),
getEditPanelAction(),
getCustomizePanelAction({
onResetPanelTitle,
onUpdatePanelTitle,
title: panelTitle,
closeContextMenu: closeMyContextMenuPanel,
}),
getToggleExpandPanelAction({ isExpanded, toggleExpandedPanel }),
getRemovePanelAction(onDeletePanel),
].concat(panelActionsStore.actions);
panels = buildEuiContextMenuPanels({
contextMenuPanel,
actions,
embeddable: ownProps.embeddable,
containerState,
});
}
return {
panels,
toggleContextMenu,
closeContextMenu: closeMyContextMenuPanel,
isPopoverOpen,
isViewMode,
};
};
export const PanelOptionsMenuContainer = connect<
PanelOptionsMenuContainerStateProps,
PanelOptionsMenuContainerDispatchProps,
PanelOptionsMenuContainerOwnProps,
PanelOptionsMenuProps,
CoreKibanaState
>(
mapStateToProps,
mapDispatchToProps,
mergeProps
)(PanelOptionsMenu);

View file

@ -1,86 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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, { ChangeEvent, KeyboardEvent } from 'react';
import { EuiButtonEmpty, EuiFieldText, EuiFormRow, keyCodes } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
export interface PanelOptionsMenuFormProps {
title?: string;
onReset: () => void;
onUpdatePanelTitle: (newPanelTitle: string) => void;
onClose: () => void;
}
interface PanelOptionsMenuFormUiProps extends PanelOptionsMenuFormProps {
intl: InjectedIntl;
}
function PanelOptionsMenuFormUi({
title,
onReset,
onUpdatePanelTitle,
onClose,
intl,
}: PanelOptionsMenuFormUiProps) {
function onInputChange(event: ChangeEvent<HTMLInputElement>) {
onUpdatePanelTitle(event.target.value);
}
function onKeyDown(event: KeyboardEvent<HTMLInputElement>) {
if (event.keyCode === keyCodes.ENTER) {
onClose();
}
}
return (
<div className="dshPanel__optionsMenuForm" data-test-subj="dashboardPanelTitleInputMenuItem">
<EuiFormRow
label={intl.formatMessage({
id: 'kbn.dashboard.panel.optionsMenuForm.panelTitleFormRowLabel',
defaultMessage: 'Panel title',
})}
>
<EuiFieldText
id="panelTitleInput"
data-test-subj="customDashboardPanelTitleInput"
name="min"
type="text"
value={title}
onChange={onInputChange}
onKeyDown={onKeyDown}
aria-label={intl.formatMessage({
id: 'kbn.dashboard.panel.optionsMenuForm.panelTitleInputAriaLabel',
defaultMessage: 'Changes to this input are applied immediately. Press enter to exit.',
})}
/>
</EuiFormRow>
<EuiButtonEmpty data-test-subj="resetCustomDashboardPanelTitle" onClick={onReset}>
<FormattedMessage
id="kbn.dashboard.panel.optionsMenuForm.resetCustomDashboardButtonLabel"
defaultMessage="Reset title"
/>
</EuiButtonEmpty>
</div>
);
}
export const PanelOptionsMenuForm = injectI18n(PanelOptionsMenuFormUi);

View file

@ -1,66 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/
jest.mock('ui/chrome', () => ({ getKibanaVersion: () => '6.0.0' }), { virtual: true });
import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../dashboard_constants';
import { SavedDashboardPanel } from '../types';
import { createPanelState } from './panel_state';
const panels: SavedDashboardPanel[] = [];
test('createPanelState adds a new panel state in 0,0 position', () => {
const panelState = createPanelState('id', 'type', '1', panels);
expect(panelState.type).toBe('type');
expect(panelState.gridData.x).toBe(0);
expect(panelState.gridData.y).toBe(0);
expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT);
expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH);
panels.push(panelState);
});
test('createPanelState adds a second new panel state', () => {
const panelState = createPanelState('id2', 'type', '2', panels);
expect(panelState.gridData.x).toBe(DEFAULT_PANEL_WIDTH);
expect(panelState.gridData.y).toBe(0);
expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT);
expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH);
panels.push(panelState);
});
test('createPanelState adds a third new panel state', () => {
const panelState = createPanelState('id3', 'type', '3', panels);
expect(panelState.gridData.x).toBe(0);
expect(panelState.gridData.y).toBe(DEFAULT_PANEL_HEIGHT);
expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT);
expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH);
panels.push(panelState);
});
test('createPanelState adds a new panel state in the top most position', () => {
const panelsWithEmptySpace = panels.filter(panel => panel.gridData.x === 0);
const panelState = createPanelState('id3', 'type', '3', panelsWithEmptySpace);
expect(panelState.gridData.x).toBe(DEFAULT_PANEL_WIDTH);
expect(panelState.gridData.y).toBe(0);
expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT);
expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH);
});

View file

@ -1,123 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 chrome from 'ui/chrome';
import {
DASHBOARD_GRID_COLUMN_COUNT,
DEFAULT_PANEL_HEIGHT,
DEFAULT_PANEL_WIDTH,
} from '../dashboard_constants';
import { SavedDashboardPanel } from '../types';
// Look for the smallest y and x value where the default panel will fit.
function findTopLeftMostOpenSpace(
width: number,
height: number,
currentPanels: SavedDashboardPanel[]
) {
let maxY = -1;
currentPanels.forEach(panel => {
maxY = Math.max(panel.gridData.y + panel.gridData.h, maxY);
});
// Handle case of empty grid.
if (maxY < 0) {
return { x: 0, y: 0 };
}
const grid = new Array(maxY);
for (let y = 0; y < maxY; y++) {
grid[y] = new Array(DASHBOARD_GRID_COLUMN_COUNT).fill(0);
}
currentPanels.forEach(panel => {
for (let x = panel.gridData.x; x < panel.gridData.x + panel.gridData.w; x++) {
for (let y = panel.gridData.y; y < panel.gridData.y + panel.gridData.h; y++) {
const row = grid[y];
if (row === undefined) {
throw new Error(
`Attempted to access a row that doesn't exist at ${y} for panel ${JSON.stringify(
panel
)}`
);
}
grid[y][x] = 1;
}
}
});
for (let y = 0; y < maxY; y++) {
for (let x = 0; x < DASHBOARD_GRID_COLUMN_COUNT; x++) {
if (grid[y][x] === 1) {
// Space is filled
continue;
} else {
for (let h = y; h < Math.min(y + height, maxY); h++) {
for (let w = x; w < Math.min(x + width, DASHBOARD_GRID_COLUMN_COUNT); w++) {
const spaceIsEmpty = grid[h][w] === 0;
const fitsPanelWidth = w === x + width - 1;
// If the panel is taller than any other panel in the current grid, it can still fit in the space, hence
// we check the minimum of maxY and the panel height.
const fitsPanelHeight = h === Math.min(y + height - 1, maxY - 1);
if (spaceIsEmpty && fitsPanelWidth && fitsPanelHeight) {
// Found space
return { x, y };
} else if (grid[h][w] === 1) {
// x, y spot doesn't work, break.
break;
}
}
}
}
}
}
return { x: 0, y: maxY };
}
/**
* Creates and initializes a basic panel state.
*/
export function createPanelState(
id: string,
type: string,
panelIndex: string,
currentPanels: SavedDashboardPanel[]
) {
const { x, y } = findTopLeftMostOpenSpace(
DEFAULT_PANEL_WIDTH,
DEFAULT_PANEL_HEIGHT,
currentPanels
);
return {
gridData: {
w: DEFAULT_PANEL_WIDTH,
h: DEFAULT_PANEL_HEIGHT,
x,
y,
i: panelIndex.toString(),
},
version: chrome.getKibanaVersion(),
panelIndex: panelIndex.toString(),
type,
id,
embeddableConfig: {},
};
}

View file

@ -1,42 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 _ from 'lodash';
import { SavedDashboardPanel } from '../types';
export class PanelUtils {
public static initPanelIndexes(panels: SavedDashboardPanel[]): void {
// find the largest panelIndex in all the panels
let maxIndex = this.getMaxPanelIndex(panels);
// ensure that all panels have a panelIndex
panels.forEach(panel => {
if (!panel.panelIndex) {
panel.panelIndex = (maxIndex++).toString();
}
});
}
public static getMaxPanelIndex(panels: SavedDashboardPanel[]): number {
let maxId = panels.reduce((id, panel) => {
return Math.max(id, Number(panel.panelIndex || id));
}, 0);
return ++maxId;
}
}

View file

@ -1,53 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { getEmbeddableError, getEmbeddableInitialized } from '../../selectors';
import { store } from '../../store';
import { embeddableIsInitializing, setPanels } from '../actions';
beforeAll(() => {
const panelData = {
embeddableConfig: {},
gridData: {
h: 0,
i: '0',
w: 0,
x: 0,
y: 0,
},
id: '123',
panelIndex: 'foo1',
type: 'mySpecialType',
version: '123',
};
store.dispatch(setPanels({ foo1: panelData }));
});
describe('embeddableIsInitializing', () => {
test('clears the error', () => {
store.dispatch(embeddableIsInitializing('foo1'));
const initialized = getEmbeddableInitialized(store.getState(), 'foo1');
expect(initialized).toEqual(false);
});
test('and clears the error', () => {
const error = getEmbeddableError(store.getState(), 'foo1');
expect(error).toEqual(undefined);
});
});

View file

@ -1,124 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 _ from 'lodash';
import { Reducer } from 'redux';
import {
EmbeddableActionTypeKeys,
EmbeddableErrorActionPayload,
EmbeddableIsInitializedActionPayload,
PanelActionTypeKeys,
SetStagedFilterActionPayload,
} from '../actions';
import { EmbeddableReduxState, EmbeddablesMap, PanelId } from '../selectors/types';
const embeddableIsInitializing = (
embeddables: EmbeddablesMap,
panelId: PanelId
): EmbeddablesMap => ({
...embeddables,
[panelId]: {
error: undefined,
initialized: false,
metadata: {},
stagedFilter: undefined,
lastReloadRequestTime: 0,
},
});
const embeddableIsInitialized = (
embeddables: EmbeddablesMap,
{ panelId, metadata }: EmbeddableIsInitializedActionPayload
): EmbeddablesMap => ({
...embeddables,
[panelId]: {
...embeddables[panelId],
initialized: true,
metadata: { ...metadata },
},
});
const setStagedFilter = (
embeddables: EmbeddablesMap,
{ panelId, stagedFilter }: SetStagedFilterActionPayload
): EmbeddablesMap => ({
...embeddables,
[panelId]: {
...embeddables[panelId],
stagedFilter,
},
});
const embeddableError = (
embeddables: EmbeddablesMap,
payload: EmbeddableErrorActionPayload
): EmbeddablesMap => ({
...embeddables,
[payload.panelId]: {
...embeddables[payload.panelId],
error: payload.error,
},
});
const clearStagedFilters = (embeddables: EmbeddablesMap): EmbeddablesMap => {
const omitStagedFilters = (embeddable: EmbeddableReduxState): EmbeddablesMap =>
_.omit({ ...embeddable }, ['stagedFilter']);
return _.mapValues<EmbeddablesMap>(embeddables, omitStagedFilters);
};
const deleteEmbeddable = (embeddables: EmbeddablesMap, panelId: PanelId): EmbeddablesMap => {
const embeddablesCopy = { ...embeddables };
delete embeddablesCopy[panelId];
return embeddablesCopy;
};
const setReloadRequestTime = (
embeddables: EmbeddablesMap,
lastReloadRequestTime: number
): EmbeddablesMap => {
return _.mapValues<EmbeddablesMap>(embeddables, embeddable => ({
...embeddable,
lastReloadRequestTime,
}));
};
export const embeddablesReducer: Reducer<EmbeddablesMap> = (
embeddables = {},
action
): EmbeddablesMap => {
switch (action.type as EmbeddableActionTypeKeys | PanelActionTypeKeys.DELETE_PANEL) {
case EmbeddableActionTypeKeys.EMBEDDABLE_IS_INITIALIZING:
return embeddableIsInitializing(embeddables, action.payload);
case EmbeddableActionTypeKeys.EMBEDDABLE_IS_INITIALIZED:
return embeddableIsInitialized(embeddables, action.payload);
case EmbeddableActionTypeKeys.SET_STAGED_FILTER:
return setStagedFilter(embeddables, action.payload);
case EmbeddableActionTypeKeys.CLEAR_STAGED_FILTERS:
return clearStagedFilters(embeddables);
case EmbeddableActionTypeKeys.EMBEDDABLE_ERROR:
return embeddableError(embeddables, action.payload);
case PanelActionTypeKeys.DELETE_PANEL:
return deleteEmbeddable(embeddables, action.payload);
case EmbeddableActionTypeKeys.REQUEST_RELOAD:
return setReloadRequestTime(embeddables, new Date().getTime());
default:
return embeddables;
}
};

View file

@ -1,34 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { combineReducers } from 'redux';
import { embeddablesReducer } from './embeddables';
import { panelsReducer } from './panels';
import { viewReducer } from './view';
import { metadataReducer } from './metadata';
export const dashboard = combineReducers({
embeddables: embeddablesReducer,
metadata: metadataReducer,
panels: panelsReducer,
view: viewReducer,
});

View file

@ -1,57 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { Reducer } from 'redux';
import {
MetadataActions,
MetadataActionTypeKeys,
UpdateDescriptionActionPayload,
UpdateTitleActionPayload,
} from '../actions';
import { DashboardMetadata } from '../selectors';
const updateTitle = (metadata: DashboardMetadata, title: UpdateTitleActionPayload) => ({
...metadata,
title,
});
const updateDescription = (
metadata: DashboardMetadata,
description: UpdateDescriptionActionPayload
) => ({
...metadata,
description,
});
export const metadataReducer: Reducer<DashboardMetadata> = (
metadata = {
description: '',
title: '',
},
action
): DashboardMetadata => {
switch ((action as MetadataActions).type) {
case MetadataActionTypeKeys.UPDATE_TITLE:
return updateTitle(metadata, action.payload);
case MetadataActionTypeKeys.UPDATE_DESCRIPTION:
return updateDescription(metadata, action.payload);
default:
return metadata;
}
};

View file

@ -1,95 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { getPanel, getPanelType } from '../../selectors';
import { store } from '../../store';
import { updatePanel, updatePanels } from '../actions';
const originalPanelData = {
embeddableConfig: {},
gridData: {
h: 0,
i: '0',
w: 0,
x: 0,
y: 0,
},
id: '123',
panelIndex: '1',
type: 'mySpecialType',
version: '123',
};
beforeEach(() => {
// init store
// @ts-ignore all this is going away soon, just ignore type errors.
store.dispatch(updatePanels({ '1': originalPanelData }));
});
describe('UpdatePanel', () => {
test('updates a panel', () => {
const newPanelData = {
...originalPanelData,
gridData: {
h: 1,
i: '1',
w: 10,
x: 1,
y: 5,
},
};
// @ts-ignore all this is going away soon, just ignore type errors.
store.dispatch(updatePanel(newPanelData));
const panel = getPanel(store.getState(), '1');
expect(panel.gridData.x).toBe(1);
expect(panel.gridData.y).toBe(5);
expect(panel.gridData.w).toBe(10);
expect(panel.gridData.h).toBe(1);
expect(panel.gridData.i).toBe('1');
});
test('should allow updating an array that contains fewer values', () => {
const panelData = {
...originalPanelData,
embeddableConfig: {
columns: ['field1', 'field2', 'field3'],
},
};
// @ts-ignore all this is going away soon, just ignore type errors.
store.dispatch(updatePanels({ '1': panelData }));
const newPanelData = {
...originalPanelData,
embeddableConfig: {
columns: ['field2', 'field3'],
},
};
// @ts-ignore all this is going away soon, just ignore type errors.
store.dispatch(updatePanel(newPanelData));
const panel = getPanel(store.getState(), '1');
expect((panel.embeddableConfig as any).columns.length).toBe(2);
expect((panel.embeddableConfig as any).columns[0]).toBe('field2');
expect((panel.embeddableConfig as any).columns[1]).toBe('field3');
});
});
test('getPanelType', () => {
expect(getPanelType(store.getState(), '1')).toBe('mySpecialType');
});

View file

@ -1,84 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 _ from 'lodash';
import { Reducer } from 'redux';
import { PanelActions, PanelActionTypeKeys, SetPanelTitleActionPayload } from '../actions';
import { PanelId } from '../selectors';
import { SavedDashboardPanel } from '../types';
interface PanelStateMap {
[key: string]: SavedDashboardPanel;
}
const deletePanel = (panels: PanelStateMap, panelId: PanelId): PanelStateMap => {
const panelsCopy = { ...panels };
delete panelsCopy[panelId];
return panelsCopy;
};
const updatePanel = (panels: PanelStateMap, panelState: SavedDashboardPanel): PanelStateMap => ({
...panels,
[panelState.panelIndex]: panelState,
});
const updatePanels = (panels: PanelStateMap, updatedPanels: PanelStateMap): PanelStateMap => {
const panelsCopy = { ...panels };
Object.values(updatedPanels).forEach(panel => {
panelsCopy[panel.panelIndex] = panel;
});
return panelsCopy;
};
const resetPanelTitle = (panels: PanelStateMap, panelId: PanelId) => ({
...panels,
[panelId]: {
...panels[panelId],
title: undefined,
},
});
const setPanelTitle = (panels: PanelStateMap, payload: SetPanelTitleActionPayload) => ({
...panels,
[payload.panelId]: {
...panels[payload.panelId],
title: payload.title,
},
});
const setPanels = ({}, newPanels: PanelStateMap) => _.cloneDeep(newPanels);
export const panelsReducer: Reducer<PanelStateMap> = (panels = {}, action): PanelStateMap => {
switch ((action as PanelActions).type) {
case PanelActionTypeKeys.DELETE_PANEL:
return deletePanel(panels, action.payload);
case PanelActionTypeKeys.UPDATE_PANEL:
return updatePanel(panels, action.payload);
case PanelActionTypeKeys.UPDATE_PANELS:
return updatePanels(panels, action.payload);
case PanelActionTypeKeys.RESET_PANEL_TITLE:
return resetPanelTitle(panels, action.payload);
case PanelActionTypeKeys.SET_PANEL_TITLE:
return setPanelTitle(panels, action.payload);
case PanelActionTypeKeys.SET_PANELS:
return setPanels(panels, action.payload);
default:
return panels;
}
};

View file

@ -1,67 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { store } from '../../store';
import { maximizePanel, minimizePanel, updateIsFullScreenMode, updateViewMode } from '../actions';
import { getFullScreenMode, getMaximizedPanelId, getViewMode } from '../../selectors';
import { DashboardViewMode } from '../dashboard_view_mode';
describe('isFullScreenMode', () => {
test('updates to true', () => {
store.dispatch(updateIsFullScreenMode(true));
const fullScreenMode = getFullScreenMode(store.getState());
expect(fullScreenMode).toBe(true);
});
test('updates to false', () => {
store.dispatch(updateIsFullScreenMode(false));
const fullScreenMode = getFullScreenMode(store.getState());
expect(fullScreenMode).toBe(false);
});
});
describe('viewMode', () => {
test('updates to EDIT', () => {
store.dispatch(updateViewMode(DashboardViewMode.EDIT));
const viewMode = getViewMode(store.getState());
expect(viewMode).toBe(DashboardViewMode.EDIT);
});
test('updates to VIEW', () => {
store.dispatch(updateViewMode(DashboardViewMode.VIEW));
const viewMode = getViewMode(store.getState());
expect(viewMode).toBe(DashboardViewMode.VIEW);
});
});
describe('maximizedPanelId', () => {
test('updates to an id when maximized', () => {
store.dispatch(maximizePanel('1'));
const maximizedId = getMaximizedPanelId(store.getState());
expect(maximizedId).toBe('1');
});
test('updates to an id when minimized', () => {
store.dispatch(minimizePanel());
const maximizedId = getMaximizedPanelId(store.getState());
expect(maximizedId).toBe(undefined);
});
});

View file

@ -1,132 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { cloneDeep } from 'lodash';
import { Reducer } from 'redux';
import { RefreshInterval } from 'ui/timefilter/timefilter';
import { TimeRange } from 'ui/timefilter/time_history';
import { Filter } from '@kbn/es-query';
import { Query } from 'src/legacy/core_plugins/data/public';
import { ViewActions, ViewActionTypeKeys } from '../actions';
import { DashboardViewMode } from '../dashboard_view_mode';
import { PanelId, ViewState } from '../selectors';
const closeContextMenu = (view: ViewState) => ({
...view,
visibleContextMenuPanelId: undefined,
});
const setVisibleContextMenuPanelId = (view: ViewState, panelId: PanelId) => ({
...view,
visibleContextMenuPanelId: panelId,
});
const updateHidePanelTitles = (view: ViewState, hidePanelTitles: boolean) => ({
...view,
hidePanelTitles,
});
const minimizePanel = (view: ViewState) => ({
...view,
maximizedPanelId: undefined,
});
const maximizePanel = (view: ViewState, panelId: PanelId) => ({
...view,
maximizedPanelId: panelId,
});
const updateIsFullScreenMode = (view: ViewState, isFullScreenMode: boolean) => ({
...view,
isFullScreenMode,
});
const updateTimeRange = (view: ViewState, timeRange: TimeRange) => ({
...view,
timeRange,
});
const updateRefreshConfig = (view: ViewState, refreshConfig: RefreshInterval) => ({
...view,
refreshConfig,
});
const updateFilters = (view: ViewState, filters: Filter[]) => ({
...view,
filters: cloneDeep(filters),
});
const updateQuery = (view: ViewState, query: Query) => ({
...view,
query,
});
const updateUseMargins = (view: ViewState, useMargins: boolean) => ({
...view,
useMargins,
});
const updateViewMode = (view: ViewState, viewMode: DashboardViewMode) => ({
...view,
viewMode,
});
export const viewReducer: Reducer<ViewState> = (
view = {
filters: [],
hidePanelTitles: false,
isFullScreenMode: false,
query: { language: 'lucene', query: '' },
timeRange: { to: 'now', from: 'now-15m' },
refreshConfig: { pause: true, value: 0 },
useMargins: true,
viewMode: DashboardViewMode.VIEW,
},
action
): ViewState => {
switch ((action as ViewActions).type) {
case ViewActionTypeKeys.MINIMIZE_PANEL:
return minimizePanel(view);
case ViewActionTypeKeys.MAXIMIZE_PANEL:
return maximizePanel(view, action.payload);
case ViewActionTypeKeys.SET_VISIBLE_CONTEXT_MENU_PANEL_ID:
return setVisibleContextMenuPanelId(view, action.payload);
case ViewActionTypeKeys.CLOSE_CONTEXT_MENU:
return closeContextMenu(view);
case ViewActionTypeKeys.UPDATE_HIDE_PANEL_TITLES:
return updateHidePanelTitles(view, action.payload);
case ViewActionTypeKeys.UPDATE_TIME_RANGE:
return updateTimeRange(view, action.payload);
case ViewActionTypeKeys.UPDATE_REFRESH_CONFIG:
return updateRefreshConfig(view, action.payload);
case ViewActionTypeKeys.UPDATE_USE_MARGINS:
return updateUseMargins(view, action.payload);
case ViewActionTypeKeys.UPDATE_VIEW_MODE:
return updateViewMode(view, action.payload);
case ViewActionTypeKeys.UPDATE_IS_FULL_SCREEN_MODE:
return updateIsFullScreenMode(view, action.payload);
case ViewActionTypeKeys.UPDATE_FILTERS:
return updateFilters(view, action.payload);
case ViewActionTypeKeys.UPDATE_QUERY:
return updateQuery(view, action.payload);
default:
return view;
}
};

View file

@ -1,151 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 _ from 'lodash';
import { ContainerState, EmbeddableMetadata } from 'ui/embeddable';
import { EmbeddableCustomization } from 'ui/embeddable/types';
import { Filter } from '@kbn/es-query';
import { RefreshInterval } from 'ui/timefilter/timefilter';
import { Query } from 'src/legacy/core_plugins/data/public';
import { TimeRange } from 'ui/timefilter/time_history';
import { DashboardViewMode } from '../dashboard_view_mode';
import {
DashboardMetadata,
DashboardState,
EmbeddableReduxState,
EmbeddablesMap,
PanelId,
} from './types';
import { SavedDashboardPanel, SavedDashboardPanelMap, StagedFilter } from '../types';
export const getPanels = (dashboard: DashboardState): Readonly<SavedDashboardPanelMap> =>
dashboard.panels;
export const getPanel = (dashboard: DashboardState, panelId: PanelId): SavedDashboardPanel =>
getPanels(dashboard)[panelId] as SavedDashboardPanel;
export const getPanelType = (dashboard: DashboardState, panelId: PanelId): string =>
getPanel(dashboard, panelId).type;
export const getEmbeddables = (dashboard: DashboardState): EmbeddablesMap => dashboard.embeddables;
// TODO: rename panel.embeddableConfig to embeddableCustomization. Because it's on the panel that's stored on a
// dashboard, renaming this will require a migration step.
export const getEmbeddableCustomization = (
dashboard: DashboardState,
panelId: PanelId
): EmbeddableCustomization => getPanel(dashboard, panelId).embeddableConfig;
export const getEmbeddable = (dashboard: DashboardState, panelId: PanelId): EmbeddableReduxState =>
dashboard.embeddables[panelId];
export const getEmbeddableError = (
dashboard: DashboardState,
panelId: PanelId
): string | object | undefined => getEmbeddable(dashboard, panelId).error;
export const getEmbeddableTitle = (
dashboard: DashboardState,
panelId: PanelId
): string | undefined => {
const embeddable = getEmbeddable(dashboard, panelId);
return embeddable && embeddable.initialized && embeddable.metadata
? embeddable.metadata.title
: '';
};
export const getEmbeddableInitialized = (dashboard: DashboardState, panelId: PanelId): boolean =>
getEmbeddable(dashboard, panelId).initialized;
export const getEmbeddableStagedFilter = (
dashboard: DashboardState,
panelId: PanelId
): object | undefined => getEmbeddable(dashboard, panelId).stagedFilter;
export const getEmbeddableMetadata = (
dashboard: DashboardState,
panelId: PanelId
): EmbeddableMetadata | undefined => getEmbeddable(dashboard, panelId).metadata;
export const getEmbeddableEditUrl = (
dashboard: DashboardState,
panelId: PanelId
): string | undefined => {
const embeddable = getEmbeddable(dashboard, panelId);
return embeddable && embeddable.initialized && embeddable.metadata
? embeddable.metadata.editUrl
: '';
};
export const getVisibleContextMenuPanelId = (dashboard: DashboardState): PanelId | undefined =>
dashboard.view.visibleContextMenuPanelId;
export const getUseMargins = (dashboard: DashboardState): boolean => dashboard.view.useMargins;
export const getViewMode = (dashboard: DashboardState): DashboardViewMode =>
dashboard.view.viewMode;
export const getFullScreenMode = (dashboard: DashboardState): boolean =>
dashboard.view.isFullScreenMode;
export const getHidePanelTitles = (dashboard: DashboardState): boolean =>
dashboard.view.hidePanelTitles;
export const getMaximizedPanelId = (dashboard: DashboardState): PanelId | undefined =>
dashboard.view.maximizedPanelId;
export const getTimeRange = (dashboard: DashboardState): TimeRange => dashboard.view.timeRange;
export const getRefreshConfig = (dashboard: DashboardState): RefreshInterval =>
dashboard.view.refreshConfig;
export const getFilters = (dashboard: DashboardState): Filter[] => dashboard.view.filters;
export const getQuery = (dashboard: DashboardState): Query => dashboard.view.query;
export const getMetadata = (dashboard: DashboardState): DashboardMetadata => dashboard.metadata;
export const getTitle = (dashboard: DashboardState): string => dashboard.metadata.title;
export const getDescription = (dashboard: DashboardState): string | undefined =>
dashboard.metadata.description;
export const getContainerState = (dashboard: DashboardState, panelId: PanelId): ContainerState => {
const time = getTimeRange(dashboard);
return {
customTitle: getPanel(dashboard, panelId).title,
embeddableCustomization: _.cloneDeep(getEmbeddableCustomization(dashboard, panelId) || {}),
filters: getFilters(dashboard),
hidePanelTitles: getHidePanelTitles(dashboard),
isPanelExpanded: getMaximizedPanelId(dashboard) === panelId,
query: getQuery(dashboard),
timeRange: {
from: time.from,
to: time.to,
},
refreshConfig: getRefreshConfig(dashboard),
viewMode: getViewMode(dashboard),
};
};
/**
* @return an array of filters any embeddables wish dashboard to apply
*/
export const getStagedFilters = (dashboard: DashboardState): StagedFilter[] =>
_.compact(_.map(dashboard.embeddables, 'stagedFilter'));

View file

@ -1,70 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { EmbeddableMetadata } from 'ui/embeddable';
import { Filter } from '@kbn/es-query';
import { RefreshInterval } from 'ui/timefilter/timefilter';
import { TimeRange } from 'ui/timefilter/time_history';
import { Query } from 'src/legacy/core_plugins/data/public';
import { DashboardViewMode } from '../dashboard_view_mode';
import { SavedDashboardPanelMap } from '../types';
export type DashboardViewMode = DashboardViewMode;
export interface ViewState {
readonly viewMode: DashboardViewMode;
readonly isFullScreenMode: boolean;
readonly maximizedPanelId?: string;
readonly visibleContextMenuPanelId?: string;
readonly timeRange: TimeRange;
readonly refreshConfig: RefreshInterval;
readonly hidePanelTitles: boolean;
readonly useMargins: boolean;
readonly query: Query;
readonly filters: Filter[];
}
export type PanelId = string;
export type SavedObjectId = string;
export interface EmbeddableReduxState {
readonly metadata?: EmbeddableMetadata;
readonly error?: string | object;
readonly initialized: boolean;
readonly stagedFilter?: object;
/**
* Timestamp of the last time this embeddable was requested to reload.
*/
readonly lastReloadRequestTime: number;
}
export interface EmbeddablesMap {
readonly [panelId: string]: EmbeddableReduxState;
}
export interface DashboardMetadata {
readonly title: string;
readonly description?: string;
}
export interface DashboardState {
readonly view: ViewState;
readonly panels: SavedDashboardPanelMap;
readonly embeddables: EmbeddablesMap;
readonly metadata: DashboardMetadata;
}

View file

@ -1,38 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { ContextMenuAction } from 'ui/embeddable';
class PanelActionsStore {
public actions: ContextMenuAction[] = [];
/**
*
* @type {IndexedArray} panelActionsRegistry
*/
public initializeFromRegistry(panelActionsRegistry: ContextMenuAction[]) {
panelActionsRegistry.forEach(panelAction => {
if (!this.actions.includes(panelAction)) {
this.actions.push(panelAction);
}
});
}
}
export const panelActionsStore = new PanelActionsStore();

View file

@ -1,62 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render 1`] = `
<EuiFlyout
closeButtonAriaLabel="Closes this dialog"
data-test-subj="dashboardAddPanel"
hideCloseButton={false}
maxWidth={false}
onClose={[Function]}
ownFocus={true}
size="m"
>
<EuiFlyoutHeader
hasBorder={true}
>
<EuiTitle
size="m"
>
<h2>
<FormattedMessage
defaultMessage="Add panels"
id="kbn.dashboard.topNav.addPanelsTitle"
values={Object {}}
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<SavedObjectFinder
noItemsMessage="No matching objects found."
onChoose={[Function]}
savedObjectMetaData={Array []}
showFilter={true}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup
justifyContent="flexEnd"
>
<EuiFlexItem
grow={false}
>
<EuiButton
color="primary"
data-test-subj="addNewSavedObjectLink"
fill={true}
iconSide="left"
onClick={[Function]}
size="m"
type="button"
>
<FormattedMessage
defaultMessage="Create new visualization"
id="kbn.dashboard.topNav.addPanel.createNewVisualizationButtonLabel"
values={Object {}}
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
`;

View file

@ -1,129 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { capabilities } from 'ui/capabilities';
import { toastNotifications, Toast } from 'ui/notify';
import {
SavedObjectFinder,
SavedObjectMetaData,
} from 'ui/saved_objects/components/saved_object_finder';
import {
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutHeader,
EuiFlyoutFooter,
EuiFlyoutBody,
EuiButton,
EuiTitle,
} from '@elastic/eui';
import { SavedObjectAttributes } from 'src/core/server/saved_objects';
import { EmbeddableFactoryRegistry } from '../types';
interface Props {
onClose: () => void;
addNewPanel: (id: string, type: string) => void;
addNewVis: () => void;
embeddableFactories: EmbeddableFactoryRegistry;
}
export class DashboardAddPanel extends React.Component<Props> {
private lastToast?: Toast;
onAddPanel = (id: string, type: string, name: string) => {
this.props.addNewPanel(id, type);
// To avoid the clutter of having toast messages cover flyout
// close previous toast message before creating a new one
if (this.lastToast) {
toastNotifications.remove(this.lastToast);
}
this.lastToast = toastNotifications.addSuccess({
title: i18n.translate(
'kbn.dashboard.topNav.addPanel.savedObjectAddedToDashboardSuccessMessageTitle',
{
defaultMessage: '{savedObjectName} was added to your dashboard',
values: {
savedObjectName: name,
},
}
),
'data-test-subj': 'addObjectToDashboardSuccess',
});
};
render() {
return (
<EuiFlyout ownFocus onClose={this.props.onClose} data-test-subj="dashboardAddPanel">
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2>
<FormattedMessage
id="kbn.dashboard.topNav.addPanelsTitle"
defaultMessage="Add panels"
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<SavedObjectFinder
onChoose={this.onAddPanel}
savedObjectMetaData={
this.props.embeddableFactories
.filter(embeddableFactory => Boolean(embeddableFactory.savedObjectMetaData))
.map(({ savedObjectMetaData }) => savedObjectMetaData) as Array<
SavedObjectMetaData<SavedObjectAttributes>
>
}
showFilter={true}
noItemsMessage={i18n.translate(
'kbn.dashboard.topNav.addPanel.noMatchingObjectsMessage',
{
defaultMessage: 'No matching objects found.',
}
)}
/>
</EuiFlyoutBody>
{capabilities.get().visualize.save ? (
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton
fill
onClick={this.props.addNewVis}
data-test-subj="addNewSavedObjectLink"
>
<FormattedMessage
id="kbn.dashboard.topNav.addPanel.createNewVisualizationButtonLabel"
defaultMessage="Create new visualization"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
) : null}
</EuiFlyout>
);
}
}

View file

@ -18,25 +18,23 @@
*/
import { i18n } from '@kbn/i18n';
import { DashboardViewMode } from '../dashboard_view_mode';
import { ViewMode } from '../../../../embeddable_api/public';
import { TopNavIds } from './top_nav_ids';
import { NavAction } from '../types';
/**
* @param {DashboardMode} dashboardMode.
* @param actions {Object} - A mapping of TopNavIds to an action function that should run when the
* @param actions - A mapping of TopNavIds to an action function that should run when the
* corresponding top nav is clicked.
* @param hideWriteControls {boolean} if true, does not include any controls that allow editing or creating objects.
* @return {Array<kbnTopNavConfig>} - Returns an array of objects for a top nav configuration, based on the
* mode.
* @param hideWriteControls if true, does not include any controls that allow editing or creating objects.
* @return an array of objects for a top nav configuration, based on the mode.
*/
export function getTopNavConfig(
dashboardMode: DashboardViewMode,
dashboardMode: ViewMode,
actions: { [key: string]: NavAction },
hideWriteControls: boolean
) {
switch (dashboardMode) {
case DashboardViewMode.VIEW:
case ViewMode.VIEW:
return hideWriteControls
? [
getFullScreenConfig(actions[TopNavIds.FULL_SCREEN]),
@ -48,7 +46,7 @@ export function getTopNavConfig(
getCloneConfig(actions[TopNavIds.CLONE]),
getEditConfig(actions[TopNavIds.ENTER_EDIT_MODE]),
];
case DashboardViewMode.EDIT:
case ViewMode.EDIT:
return [
getSaveConfig(actions[TopNavIds.SAVE]),
getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]),

View file

@ -1,62 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { I18nContext } from 'ui/i18n';
import React from 'react';
import ReactDOM from 'react-dom';
import { DashboardAddPanel } from './add_panel';
import { EmbeddableFactoryRegistry } from '../types';
let isOpen = false;
export function showAddPanel(
addNewPanel: (id: string, type: string) => void,
addNewVis: () => void,
embeddableFactories: EmbeddableFactoryRegistry
) {
if (isOpen) {
return;
}
isOpen = true;
const container = document.createElement('div');
const onClose = () => {
ReactDOM.unmountComponentAtNode(container);
document.body.removeChild(container);
isOpen = false;
};
const addNewVisWithCleanup = () => {
onClose();
addNewVis();
};
document.body.appendChild(container);
const element = (
<I18nContext>
<DashboardAddPanel
onClose={onClose}
addNewPanel={addNewPanel}
addNewVis={addNewVisWithCleanup}
embeddableFactories={embeddableFactories}
/>
</I18nContext>
);
ReactDOM.render(element, container);
}

View file

@ -17,13 +17,10 @@
* under the License.
*/
import { EmbeddableFactory } from 'ui/embeddable';
import { AppState } from 'ui/state_management/app_state';
import { UIRegistry } from 'ui/registry/_registry';
import { Filter } from '@kbn/es-query';
import { Query } from 'src/legacy/core_plugins/data/public';
import { AppState as TAppState } from 'ui/state_management/app_state';
import { DashboardViewMode } from './dashboard_view_mode';
import {
RawSavedDashboardPanelTo60,
RawSavedDashboardPanel610,
@ -33,9 +30,7 @@ import {
RawSavedDashboardPanel730ToLatest,
} from './migrations/types';
export interface EmbeddableFactoryRegistry extends UIRegistry<EmbeddableFactory> {
byName: { [key: string]: EmbeddableFactory };
}
import { ViewMode } from '../../../embeddable_api/public';
export type NavAction = (menuItem: any, navController: any, anchorElement: any) => void;
@ -117,7 +112,7 @@ export interface DashboardAppStateParameters {
};
query: Query | string;
filters: Filter[];
viewMode: DashboardViewMode;
viewMode: ViewMode;
}
// This could probably be improved if we flesh out AppState more... though AppState will be going away

View file

@ -1,8 +0,0 @@
.dshDashboardViewport {
width: 100%;
background-color: $euiColorEmptyShade;
}
.dshDashboardViewport-withMargins {
width: 100%;
}

View file

@ -1 +0,0 @@
@import './dashboard_viewport';

View file

@ -1,59 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { EmbeddableFactory } from 'ui/embeddable';
import { ExitFullScreenButton } from 'ui/exit_full_screen';
import { DashboardGrid } from '../grid';
export function DashboardViewport({
maximizedPanelId,
getEmbeddableFactory,
panelCount,
title,
description,
useMargins,
isFullScreenMode,
onExitFullScreenMode,
}: {
maximizedPanelId: string;
getEmbeddableFactory: (panelType: string) => EmbeddableFactory;
panelCount: number;
title: string;
description: string;
useMargins: boolean;
isFullScreenMode: boolean;
onExitFullScreenMode: () => void;
}) {
return (
<div
data-shared-items-count={panelCount}
data-shared-items-container
data-title={title}
data-description={description}
className={useMargins ? 'dshDashboardViewport-withMargins' : 'dshDashboardViewport'}
>
{isFullScreenMode && <ExitFullScreenButton onExitFullScreenMode={onExitFullScreenMode} />}
<DashboardGrid
getEmbeddableFactory={getEmbeddableFactory}
maximizedPanelId={maximizedPanelId}
/>
</div>
);
}

View file

@ -1,51 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { connect } from 'react-redux';
import { DashboardViewport } from './dashboard_viewport';
import { updateIsFullScreenMode } from '../actions';
import {
getMaximizedPanelId,
getPanels,
getTitle,
getDescription,
getUseMargins,
getFullScreenMode,
} from '../selectors';
const mapStateToProps = ({ dashboard }) => {
const maximizedPanelId = getMaximizedPanelId(dashboard);
return {
maximizedPanelId,
panelCount: Object.keys(getPanels(dashboard)).length,
description: getDescription(dashboard),
title: getTitle(dashboard),
useMargins: getUseMargins(dashboard),
isFullScreenMode: getFullScreenMode(dashboard),
};
};
const mapDispatchToProps = (dispatch) => ({
onExitFullScreenMode: () => dispatch(updateIsFullScreenMode(false)),
});
export const DashboardViewportContainer = connect(
mapStateToProps,
mapDispatchToProps,
)(DashboardViewport);

View file

@ -1,36 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { store } from '../../store';
import { Provider } from 'react-redux';
import { DashboardViewportContainer } from './dashboard_viewport_container';
export function DashboardViewportProvider(props) {
return (
<Provider store={store}>
<DashboardViewportContainer {...props} />
</Provider>
);
}
DashboardViewportProvider.propTypes = {
getEmbeddableFactory: PropTypes.func.isRequired,
};

View file

@ -16,3 +16,5 @@
@import 'hacks';
@import 'discover';
@import 'embeddable/index';

View file

@ -0,0 +1,11 @@
/**
* 1. We want the kbnDocTable__container to scroll only when embedded in an embeddable panel
* 2. Force a better looking scrollbar
*/
.embPanel {
.kbnDocTable__container {
@include euiScrollBar; /* 2 */
flex: 1 1 0; /* 1 */
overflow: auto; /* 1 */
}
}

View file

@ -0,0 +1,2 @@
@import 'embeddables';

View file

@ -17,4 +17,6 @@
* under the License.
*/
export { PanelHeaderContainer as PanelHeader } from './panel_header_container';
export * from './types';
export * from './search_embeddable_factory';
export * from './search_embeddable';

View file

@ -17,25 +17,28 @@
* under the License.
*/
// @ts-ignore
import { getFilterGenerator } from 'ui/filter_manager';
import angular from 'angular';
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import { SearchSource } from 'ui/courier';
import {
ContainerState,
Embeddable,
EmbeddableState,
OnEmbeddableStateChanged,
} from 'ui/embeddable';
import { StaticIndexPattern } from 'ui/index_patterns';
import { RequestAdapter } from 'ui/inspector/adapters';
import { Adapters } from 'ui/inspector/types';
import { getTime } from 'ui/timefilter/get_time';
import { TimeRange } from 'ui/timefilter/time_history';
import { Filter } from '@kbn/es-query';
import { Query } from 'src/legacy/core_plugins/data/public';
import { Subscription } from 'rxjs';
import * as Rx from 'rxjs';
import { Filter, FilterStateStore } from '@kbn/es-query';
import {
APPLY_FILTER_TRIGGER,
Embeddable,
executeTriggerActions,
Container,
} from '../../../../embeddable_api/public';
import * as columnActions from '../doc_table/actions/columns';
import { SavedSearch } from '../types';
import searchTemplate from './search_template.html';
import { ISearchEmbeddable, SearchInput, SearchOutput } from './types';
interface SearchScope extends ng.IScope {
columns?: string[];
@ -48,95 +51,93 @@ interface SearchScope extends ng.IScope {
removeColumn?: (column: string) => void;
addColumn?: (column: string) => void;
moveColumn?: (column: string, index: number) => void;
filter?: (field: string, value: string, operator: string) => void;
filter?: (field: { name: string; scripted: boolean }, value: string[], operator: string) => void;
}
/**
* 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
type SearchEmbeddableCustomization = {
sort?: string[];
columns?: string[];
};
export interface FilterManager {
generate: (
field: {
name: string;
scripted: boolean;
},
values: string | string[],
operation: string,
index: number
) => Filter[];
}
interface SearchEmbeddableConfig {
onEmbeddableStateChanged: OnEmbeddableStateChanged;
savedSearch: SavedSearch;
editUrl: string;
editable: boolean;
$rootScope: ng.IRootScopeService;
$compile: ng.ICompileService;
courier: any;
savedSearch: SavedSearch;
editUrl: string;
indexPatterns?: StaticIndexPattern[];
editable: boolean;
queryFilter: unknown;
}
export class SearchEmbeddable extends Embeddable {
private readonly onEmbeddableStateChanged: OnEmbeddableStateChanged;
export const SEARCH_EMBEDDABLE_TYPE = 'search';
export class SearchEmbeddable extends Embeddable<SearchInput, SearchOutput>
implements ISearchEmbeddable {
private readonly savedSearch: SavedSearch;
private $rootScope: ng.IRootScopeService;
private $compile: ng.ICompileService;
private customization: SearchEmbeddableCustomization;
private inspectorAdaptors: Adapters;
private searchScope?: SearchScope;
private panelTitle: string = '';
private filtersSearchSource: SearchSource;
private timeRange?: TimeRange;
private filters?: Filter[];
private query?: Query;
private searchInstance?: JQLite;
private courier: any;
private subscription?: Subscription;
public readonly type = SEARCH_EMBEDDABLE_TYPE;
private filterGen: FilterManager;
constructor({
onEmbeddableStateChanged,
savedSearch,
editable,
editUrl,
$rootScope,
$compile,
}: SearchEmbeddableConfig) {
super({
title: savedSearch.title,
constructor(
{
$rootScope,
$compile,
courier,
savedSearch,
editUrl,
editLabel: i18n.translate('kbn.embeddable.search.editLabel', {
defaultMessage: 'Edit saved search',
}),
indexPatterns,
editable,
indexPatterns: _.compact([savedSearch.searchSource.getField('index')]),
});
this.onEmbeddableStateChanged = onEmbeddableStateChanged;
queryFilter,
}: SearchEmbeddableConfig,
initialInput: SearchInput,
parent?: Container
) {
super(
initialInput,
{ defaultTitle: savedSearch.title, editUrl, indexPatterns, editable },
parent
);
this.filterGen = getFilterGenerator(queryFilter);
this.courier = courier;
this.savedSearch = savedSearch;
this.$rootScope = $rootScope;
this.$compile = $compile;
this.customization = {};
this.inspectorAdaptors = {
requests: new RequestAdapter(),
};
this.subscription = Rx.merge(this.getOutput$(), this.getInput$()).subscribe(() => {
this.panelTitle = this.output.title || '';
if (this.searchScope) {
this.pushContainerStateParamsToScope(this.searchScope);
}
});
}
public getInspectorAdapters() {
return this.inspectorAdaptors;
}
public getPanelTitle() {
return this.panelTitle;
}
public onContainerStateChanged(containerState: ContainerState) {
this.customization = containerState.embeddableCustomization || {};
this.filters = containerState.filters;
this.query = containerState.query;
this.timeRange = containerState.timeRange;
this.panelTitle = '';
if (!containerState.hidePanelTitles) {
this.panelTitle =
containerState.customTitle !== undefined
? containerState.customTitle
: this.savedSearch.title;
}
if (this.searchScope) {
this.pushContainerStateParamsToScope(this.searchScope);
}
public getSavedSearch() {
return this.savedSearch;
}
/**
@ -144,8 +145,7 @@ export class SearchEmbeddable extends Embeddable {
* @param {Element} domNode
* @param {ContainerState} containerState
*/
public render(domNode: HTMLElement, containerState: ContainerState) {
this.onContainerStateChanged(containerState);
public render(domNode: HTMLElement) {
this.initializeSearchScope();
if (!this.searchScope) {
throw new Error('Search scope not defined');
@ -154,9 +154,12 @@ export class SearchEmbeddable extends Embeddable {
this.searchInstance = this.$compile(searchTemplate)(this.searchScope);
const rootNode = angular.element(domNode);
rootNode.append(this.searchInstance);
this.pushContainerStateParamsToScope(this.searchScope);
}
public destroy() {
super.destroy();
this.savedSearch.destroy();
if (this.searchInstance) {
this.searchInstance.remove();
@ -165,6 +168,9 @@ export class SearchEmbeddable extends Embeddable {
this.searchScope.$destroy();
delete this.searchScope;
}
if (this.subscription) {
this.subscription.unsubscribe();
}
}
private initializeSearchScope() {
@ -176,10 +182,10 @@ export class SearchEmbeddable extends Embeddable {
const timeRangeSearchSource = searchScope.searchSource.create();
timeRangeSearchSource.setField('filter', () => {
if (!this.searchScope || !this.timeRange) {
if (!this.searchScope || !this.input.timeRange) {
return;
}
return getTime(this.searchScope.searchSource.getField('index'), this.timeRange);
return getTime(this.searchScope.searchSource.getField('index'), this.input.timeRange);
});
this.filtersSearchSource = searchScope.searchSource.create();
@ -190,8 +196,8 @@ export class SearchEmbeddable extends Embeddable {
this.pushContainerStateParamsToScope(searchScope);
searchScope.setSortOrder = (columnName, direction) => {
searchScope.sort = this.customization.sort = [columnName, direction];
this.emitEmbeddableStateChange(this.getEmbeddableState());
searchScope.sort = [columnName, direction];
this.updateInput({ sort: searchScope.sort });
};
searchScope.addColumn = (columnName: string) => {
@ -200,8 +206,8 @@ export class SearchEmbeddable extends Embeddable {
}
this.savedSearch.searchSource.getField('index').popularizeField(columnName, 1);
columnActions.addColumn(searchScope.columns, columnName);
searchScope.columns = this.customization.columns = searchScope.columns;
this.emitEmbeddableStateChange(this.getEmbeddableState());
searchScope.columns = searchScope.columns;
this.updateInput({ columns: searchScope.columns });
};
searchScope.removeColumn = (columnName: string) => {
@ -210,8 +216,8 @@ export class SearchEmbeddable extends Embeddable {
}
this.savedSearch.searchSource.getField('index').popularizeField(columnName, 1);
columnActions.removeColumn(searchScope.columns, columnName);
this.customization.columns = searchScope.columns;
this.emitEmbeddableStateChange(this.getEmbeddableState());
this.updateInput({ columns: searchScope.columns });
};
searchScope.moveColumn = (columnName, newIndex: number) => {
@ -219,46 +225,44 @@ export class SearchEmbeddable extends Embeddable {
return;
}
columnActions.moveColumn(searchScope.columns, columnName, newIndex);
this.customization.columns = searchScope.columns;
this.emitEmbeddableStateChange(this.getEmbeddableState());
this.updateInput({ columns: searchScope.columns });
};
searchScope.filter = (field, value, operator) => {
searchScope.filter = async (field, value, operator) => {
const index = this.savedSearch.searchSource.getField('index').id;
const stagedFilter = {
field,
value,
operator,
index,
};
this.emitEmbeddableStateChange({
...this.getEmbeddableState(),
stagedFilter,
let filters = this.filterGen.generate(field, value, operator, index);
filters = filters.map(filter => ({
...filter,
$state: { store: FilterStateStore.APP_STATE },
}));
await executeTriggerActions(APPLY_FILTER_TRIGGER, {
embeddable: this,
triggerContext: {
filters,
},
});
};
this.searchScope = searchScope;
}
private emitEmbeddableStateChange(embeddableState: EmbeddableState) {
this.onEmbeddableStateChanged(embeddableState);
}
private getEmbeddableState(): EmbeddableState {
return {
customization: this.customization,
};
public reload() {
this.courier.fetch();
}
private pushContainerStateParamsToScope(searchScope: SearchScope) {
// If there is column or sort data on the panel, that means the original columns or sort settings have
// been overridden in a dashboard.
searchScope.columns = this.customization.columns || this.savedSearch.columns;
searchScope.sort = this.customization.sort || this.savedSearch.sort;
searchScope.columns = this.input.columns || this.savedSearch.columns;
searchScope.sort = this.input.sort || this.savedSearch.sort;
searchScope.sharedItemTitle = this.panelTitle;
this.filtersSearchSource.setField('filter', this.filters);
this.filtersSearchSource.setField('query', this.query);
this.filtersSearchSource.setField('filter', this.input.filters);
this.filtersSearchSource.setField('query', this.input.query);
// Sadly this is neccessary to tell the angular component to refetch the data.
this.courier.fetch();
}
}

View file

@ -20,22 +20,29 @@
import '../doc_table';
import { capabilities } from 'ui/capabilities';
import { i18n } from '@kbn/i18n';
import { EmbeddableFactory } from 'ui/embeddable';
import chrome from 'ui/chrome';
import { IPrivate } from 'ui/private';
import { TimeRange } from 'ui/timefilter/time_history';
import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter';
import {
EmbeddableInstanceConfiguration,
OnEmbeddableStateChanged,
} from 'ui/embeddable/embeddable_factory';
embeddableFactories,
EmbeddableFactory,
ErrorEmbeddable,
Container,
} from '../../../../embeddable_api/public/index';
import { SavedSearchLoader } from '../types';
import { SearchEmbeddable } from './search_embeddable';
import { SearchEmbeddable, SEARCH_EMBEDDABLE_TYPE } from './search_embeddable';
import { SearchInput, SearchOutput } from './types';
export class SearchEmbeddableFactory extends EmbeddableFactory {
constructor(
private $compile: ng.ICompileService,
private $rootScope: ng.IRootScopeService,
private searchLoader: SavedSearchLoader
) {
export class SearchEmbeddableFactory extends EmbeddableFactory<
SearchInput,
SearchOutput,
SearchEmbeddable
> {
public readonly type = SEARCH_EMBEDDABLE_TYPE;
constructor() {
super({
name: 'search',
savedObjectMetaData: {
name: i18n.translate('kbn.discover.savedSearch.savedObjectName', {
defaultMessage: 'Saved search',
@ -46,35 +53,61 @@ export class SearchEmbeddableFactory extends EmbeddableFactory {
});
}
public getEditPath(panelId: string) {
return this.searchLoader.urlFor(panelId);
public isEditable() {
return capabilities.get().discover.save as boolean;
}
/**
*
* @param {Object} panelMetadata. Currently just passing in panelState but it's more than we need, so we should
* decouple this to only include data given to us from the embeddable when it's added to the dashboard. Generally
* will be just the object id, but could be anything depending on the plugin.
* @param onEmbeddableStateChanged
* @return {Promise.<Embeddable>}
*/
public create(
{ id }: EmbeddableInstanceConfiguration,
onEmbeddableStateChanged: OnEmbeddableStateChanged
) {
const editUrl = this.getEditPath(id);
const editable = capabilities.get().discover.save as boolean;
public canCreateNew() {
return false;
}
// can't change this to be async / awayt, because an Anglular promise is expected to be returned.
return this.searchLoader.get(id).then(savedObject => {
return new SearchEmbeddable({
onEmbeddableStateChanged,
savedSearch: savedObject,
editUrl,
editable,
$rootScope: this.$rootScope,
$compile: this.$compile,
});
public getDisplayName() {
return i18n.translate('kbn.embeddable.search.displayName', {
defaultMessage: 'search',
});
}
public async createFromSavedObject(
savedObjectId: string,
input: Partial<SearchInput> & { id: string; timeRange: TimeRange },
parent?: Container
): Promise<SearchEmbeddable | ErrorEmbeddable> {
const $injector = await chrome.dangerouslyGetActiveInjector();
const $compile = $injector.get<ng.ICompileService>('$compile');
const $rootScope = $injector.get<ng.IRootScopeService>('$rootScope');
const courier = $injector.get<unknown>('courier');
const searchLoader = $injector.get<SavedSearchLoader>('savedSearches');
const editUrl = chrome.addBasePath(`/app/kibana${searchLoader.urlFor(savedObjectId)}`);
const Private = $injector.get<IPrivate>('Private');
const queryFilter = Private(FilterBarQueryFilterProvider);
try {
const savedObject = await searchLoader.get(savedObjectId);
return new SearchEmbeddable(
{
courier,
savedSearch: savedObject,
$rootScope,
$compile,
editUrl,
queryFilter,
editable: capabilities.get().discover.save as boolean,
indexPatterns: _.compact([savedObject.searchSource.getField('index')]),
},
input,
parent
);
} catch (e) {
console.error(e); // eslint-disable-line no-console
return new ErrorEmbeddable(e, input, parent);
}
}
public async create(input: SearchInput) {
return new ErrorEmbeddable('Saved searches can only be created from a saved object', input);
}
}
embeddableFactories.set(SEARCH_EMBEDDABLE_TYPE, new SearchEmbeddableFactory());

View file

@ -1,36 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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 { EmbeddableFactoriesRegistryProvider } from 'ui/embeddable/embeddable_factories_registry';
import { IPrivate } from 'ui/private';
import { SavedSearchLoader } from '../types';
import { SearchEmbeddableFactory } from './search_embeddable_factory';
export function searchEmbeddableFactoryProvider(Private: IPrivate) {
const SearchEmbeddableFactoryProvider = (
$compile: ng.ICompileService,
$rootScope: ng.IRootScopeService,
savedSearches: SavedSearchLoader
) => {
return new SearchEmbeddableFactory($compile, $rootScope, savedSearches);
};
return Private(SearchEmbeddableFactoryProvider);
}
EmbeddableFactoriesRegistryProvider.register(searchEmbeddableFactoryProvider);

View file

@ -17,45 +17,32 @@
* under the License.
*/
import React from 'react';
import sinon from 'sinon';
import { shallow } from 'enzyme';
import { StaticIndexPattern } from 'ui/index_patterns';
import { TimeRange } from 'ui/timefilter/time_history';
import { Query } from 'src/legacy/core_plugins/data/public';
import { Filter } from '@kbn/es-query';
import { SavedSearch } from '../types';
import {
DashboardAddPanel,
} from './add_panel';
EmbeddableInput,
EmbeddableOutput,
IEmbeddable,
} from '../../../../embeddable_api/public/index';
jest.mock('ui/capabilities',
() => ({
capabilities: {
get: () => ({
visualize: {
show: true,
save: true
}
})
}
}), { virtual: true });
export interface SearchInput extends EmbeddableInput {
timeRange: TimeRange;
query?: Query;
filters?: Filter[];
hidePanelTitles?: boolean;
columns?: string[];
sort?: string[];
}
jest.mock('ui/notify',
() => ({
toastNotifications: {
addDanger: () => {},
}
}), { virtual: true });
export interface SearchOutput extends EmbeddableOutput {
editUrl: string;
indexPatterns?: StaticIndexPattern[];
editable: boolean;
}
let onClose;
beforeEach(() => {
onClose = sinon.spy();
});
test('render', () => {
const component = shallow(<DashboardAddPanel
onClose={onClose}
visTypes={{}}
addNewPanel={() => {}}
addNewVis={() => {}}
embeddableFactories={[]}
/>);
expect(component).toMatchSnapshot();
});
export interface ISearchEmbeddable extends IEmbeddable<SearchInput, SearchOutput> {
getSavedSearch(): SavedSearch;
}

Some files were not shown because too many files have changed in this diff Show more