mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
* 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:
parent
9ba01c9ab2
commit
458de86dd8
159 changed files with 1754 additions and 7233 deletions
|
@ -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';
|
||||
|
|
|
@ -16,8 +16,6 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import 'uiExports/embeddableActions';
|
||||
import 'uiExports/embeddableFactories';
|
||||
|
||||
export {
|
||||
DASHBOARD_GRID_COLUMN_COUNT,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}}
|
||||
|
|
|
@ -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 } : {}),
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ export async function executeTriggerActions(
|
|||
) {
|
||||
const actions = await getActionsForTrigger(actionRegistry, triggerRegistry, triggerId, {
|
||||
embeddable,
|
||||
triggerContext,
|
||||
});
|
||||
|
||||
if (actions.length > 1) {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
```
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,3 @@
|
|||
// dshChart__legend-isLoading
|
||||
|
||||
@import './dashboard_app';
|
||||
@import './grid/index';
|
||||
@import './panel/index';
|
||||
@import './viewport/index';
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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 }));
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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';
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -118,9 +118,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<dashboard-viewport-provider
|
||||
get-embeddable-factory="getEmbeddableFactory"
|
||||
>
|
||||
</dashboard-viewport-provider>
|
||||
<div id="dashboardViewport"></div>
|
||||
|
||||
</dashboard-app>
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}`;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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',
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
`;
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import './dashboard_grid';
|
|
@ -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');
|
||||
});
|
|
@ -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);
|
|
@ -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);
|
|
@ -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';
|
|
@ -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);
|
||||
});
|
|
@ -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 }),
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: () => {} }));
|
|
@ -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>
|
||||
`;
|
|
@ -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>
|
||||
`;
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
@import "./dashboard_panel";
|
||||
@import 'panel_header/panel_options_menu_form';
|
|
@ -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);
|
||||
});
|
|
@ -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);
|
|
@ -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);
|
||||
});
|
|
@ -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);
|
|
@ -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();
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
.dshPanel__optionsMenuForm {
|
||||
padding: $euiSize;
|
||||
}
|
|
@ -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',
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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',
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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';
|
|
@ -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);
|
|
@ -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');
|
||||
});
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
||||
});
|
|
@ -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: {},
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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,
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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');
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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'));
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
|
@ -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>
|
||||
`;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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]),
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
.dshDashboardViewport {
|
||||
width: 100%;
|
||||
background-color: $euiColorEmptyShade;
|
||||
}
|
||||
|
||||
.dshDashboardViewport-withMargins {
|
||||
width: 100%;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
@import './dashboard_viewport';
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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);
|
|
@ -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,
|
||||
};
|
|
@ -16,3 +16,5 @@
|
|||
@import 'hacks';
|
||||
|
||||
@import 'discover';
|
||||
|
||||
@import 'embeddable/index';
|
||||
|
|
|
@ -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 */
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
@import 'embeddables';
|
|
@ -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';
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue