diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/index.scss b/src/legacy/core_plugins/dashboard_embeddable_container/public/index.scss index 21a82c782ee5a..713d554caf871 100644 --- a/src/legacy/core_plugins/dashboard_embeddable_container/public/index.scss +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/index.scss @@ -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'; diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/index.ts b/src/legacy/core_plugins/dashboard_embeddable_container/public/index.ts index ab594f858d7fe..3b9298183bd7e 100644 --- a/src/legacy/core_plugins/dashboard_embeddable_container/public/index.ts +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/index.ts @@ -16,8 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import 'uiExports/embeddableActions'; -import 'uiExports/embeddableFactories'; export { DASHBOARD_GRID_COLUMN_COUNT, diff --git a/src/legacy/core_plugins/embeddable_api/public/containers/container.test.ts b/src/legacy/core_plugins/embeddable_api/public/containers/container.test.ts index 2f5a8d42afa1f..acc7ece4f3d33 100644 --- a/src/legacy/core_plugins/embeddable_api/public/containers/container.test.ts +++ b/src/legacy/core_plugins/embeddable_api/public/containers/container.test.ts @@ -43,7 +43,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'; jest.mock('ui/new_platform'); @@ -418,67 +417,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(embeddable.id).filters).toEqual([]); - - embeddable.updateInput({ filters: undefined }); - - expect(container.getInputForChild(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(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: [] }, @@ -749,3 +687,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, + }, + }, + }); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/containers/container.ts b/src/legacy/core_plugins/embeddable_api/public/containers/container.ts index 0fd53e4f335af..fd3ee1b642262 100644 --- a/src/legacy/core_plugins/embeddable_api/public/containers/container.ts +++ b/src/legacy/core_plugins/embeddable_api/public/containers/container.ts @@ -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] }), {}); - - if (embeddable.getOutput().savedObjectId || Object.keys(newOrChangedInput).length > 0) { + // 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) { this.updateInput({ panels: { ...this.input.panels, @@ -340,7 +339,6 @@ export abstract class Container< : undefined), explicitInput: { ...this.input.panels[panel.explicitInput.id].explicitInput, - ...newOrChangedInput, }, }, }, diff --git a/src/legacy/core_plugins/embeddable_api/public/containers/embeddable_child_panel.tsx b/src/legacy/core_plugins/embeddable_api/public/containers/embeddable_child_panel.tsx index 0a5366e4b0a35..e6f69fb591c81 100644 --- a/src/legacy/core_plugins/embeddable_api/public/containers/embeddable_child_panel.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/containers/embeddable_child_panel.tsx @@ -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'; diff --git a/src/legacy/core_plugins/embeddable_api/public/containers/explicit_input.test.ts b/src/legacy/core_plugins/embeddable_api/public/containers/explicit_input.test.ts new file mode 100644 index 0000000000000..44987cb303995 --- /dev/null +++ b/src/legacy/core_plugins/embeddable_api/public/containers/explicit_input.test.ts @@ -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 '../ui_capabilities.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(); +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(embeddable.id).filters).toEqual([]); + + embeddable.updateInput({ filters: undefined }); + + expect(container.getInputForChild(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(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('123'); + expect(embeddable).toBeDefined(); + expect(embeddable.getInput().lastName).toBe('lolol'); + subscription.unsubscribe(); + done(); + } + }); +}); diff --git a/src/legacy/core_plugins/embeddable_api/public/context_menu_actions/build_eui_context_menu_panels.ts b/src/legacy/core_plugins/embeddable_api/public/context_menu_actions/build_eui_context_menu_panels.ts index a22d22615cf95..283ca4cf13002 100644 --- a/src/legacy/core_plugins/embeddable_api/public/context_menu_actions/build_eui_context_menu_panels.ts +++ b/src/legacy/core_plugins/embeddable_api/public/context_menu_actions/build_eui_context_menu_panels.ts @@ -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); diff --git a/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable.tsx b/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable.tsx index 02eb0cc639559..a6f7556a72045 100644 --- a/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable.tsx @@ -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(this.id); this.onResetInput(newInput); }); diff --git a/src/legacy/core_plugins/embeddable_api/public/get_actions_for_trigger.ts b/src/legacy/core_plugins/embeddable_api/public/get_actions_for_trigger.ts index eea740aca2767..922fea0ba310f 100644 --- a/src/legacy/core_plugins/embeddable_api/public/get_actions_for_trigger.ts +++ b/src/legacy/core_plugins/embeddable_api/public/get_actions_for_trigger.ts @@ -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, triggerRegistry: Map, triggerId: string, - context: { embeddable: IEmbeddable; container?: IContainer } + context: { embeddable: IEmbeddable; triggerContext?: { [key: string]: unknown } } ) { const trigger = triggerRegistry.get(triggerId); diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx index 87fb7b1169f24..e0949f181d243 100644 --- a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx @@ -48,7 +48,7 @@ export class ContactCardEmbeddableFactory extends EmbeddableFactory { + onCreate={(input: { firstName: string; lastName?: string }) => { modalSession.close(); resolve(input); }} diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card_initializer.tsx b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card_initializer.tsx index e90002261f159..44d1dac1d0eb8 100644 --- a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card_initializer.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card_initializer.tsx @@ -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 this.setState({ lastName: e.target.value })} /> @@ -77,12 +78,12 @@ export class ContactCardInitializer extends ComponentCancel { - 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 } : {}), }); } }} diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/hello_world_container.tsx b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/hello_world_container.tsx index c6636bd58cd8e..92c42a826c067 100644 --- a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/hello_world_container.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/hello_world_container.tsx @@ -37,7 +37,11 @@ type InheritedInput = { lastName: string; }; -export class HelloWorldContainer extends Container { +interface HelloWorldContainerInput extends ContainerInput { + lastName?: string; +} + +export class HelloWorldContainer extends Container { public readonly type = HELLO_WORLD_CONTAINER; constructor(input: ContainerInput, embeddableFactories: Map) { @@ -48,7 +52,7 @@ export class HelloWorldContainer extends Container { return { id, viewMode: this.input.viewMode || ViewMode.EDIT, - lastName: 'foo', + lastName: this.input.lastName || 'foo', }; } diff --git a/src/legacy/core_plugins/embeddable_api/public/triggers/execute_trigger_actions.ts b/src/legacy/core_plugins/embeddable_api/public/triggers/execute_trigger_actions.ts index f7dbe143645c9..78e87d577dd90 100644 --- a/src/legacy/core_plugins/embeddable_api/public/triggers/execute_trigger_actions.ts +++ b/src/legacy/core_plugins/embeddable_api/public/triggers/execute_trigger_actions.ts @@ -34,6 +34,7 @@ export async function executeTriggerActions( ) { const actions = await getActionsForTrigger(actionRegistry, triggerRegistry, triggerId, { embeddable, + triggerContext, }); if (actions.length > 1) { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/README.md b/src/legacy/core_plugins/kibana/public/dashboard/README.md deleted file mode 100644 index 01aef6db7dfa0..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/README.md +++ /dev/null @@ -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 ( -
- -
this.panelElement = panelElement}>
-
- ); - } -} - ------- -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., - - // 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, -} -``` diff --git a/src/legacy/core_plugins/kibana/public/dashboard/_hacks.scss b/src/legacy/core_plugins/kibana/public/dashboard/_hacks.scss index 474699d9cf25b..debcc78792de9 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/_hacks.scss +++ b/src/legacy/core_plugins/kibana/public/dashboard/_hacks.scss @@ -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; } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/_index.scss b/src/legacy/core_plugins/kibana/public/dashboard/_index.scss index a5037a64f067e..35d4127365f02 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/_index.scss +++ b/src/legacy/core_plugins/kibana/public/dashboard/_index.scss @@ -16,6 +16,3 @@ // dshChart__legend-isLoading @import './dashboard_app'; -@import './grid/index'; -@import './panel/index'; -@import './viewport/index'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/actions/embeddables.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/actions/embeddables.test.ts deleted file mode 100644 index 1a2f0bbbcd291..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/actions/embeddables.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/actions/embeddables.ts b/src/legacy/core_plugins/kibana/public/dashboard/actions/embeddables.ts deleted file mode 100644 index e284bc7b74c1f..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/actions/embeddables.ts +++ /dev/null @@ -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 {} - -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 {} - -export interface ClearStagedFiltersAction - extends KibanaAction {} -export interface RequestReload - extends KibanaAction {} - -export interface EmbeddableErrorActionPayload { - error: string | object; - panelId: PanelId; -} - -export interface EmbeddableErrorAction - extends KibanaAction {} - -export type EmbeddableActions = - | EmbeddableIsInitializingAction - | EmbeddableIsInitializedAction - | ClearStagedFiltersAction - | SetStagedFilterAction - | EmbeddableErrorAction; - -export const embeddableIsInitializing = createAction( - EmbeddableActionTypeKeys.EMBEDDABLE_IS_INITIALIZING -); -export const embeddableIsInitialized = createAction( - EmbeddableActionTypeKeys.EMBEDDABLE_IS_INITIALIZED -); -export const setStagedFilter = createAction( - EmbeddableActionTypeKeys.SET_STAGED_FILTER -); -export const clearStagedFilters = createAction(EmbeddableActionTypeKeys.CLEAR_STAGED_FILTERS); -export const embeddableError = createAction( - 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 })); - } - }; -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/actions/index.ts b/src/legacy/core_plugins/kibana/public/dashboard/actions/index.ts deleted file mode 100644 index 7a5a8e209684f..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/actions/index.ts +++ /dev/null @@ -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'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/actions/metadata.ts b/src/legacy/core_plugins/kibana/public/dashboard/actions/metadata.ts deleted file mode 100644 index eea16f02c9a4f..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/actions/metadata.ts +++ /dev/null @@ -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 {} - -export type UpdateDescriptionActionPayload = string; - -export interface UpdateDescriptionAction - extends KibanaAction {} - -export type MetadataActions = UpdateDescriptionAction | UpdateTitleAction; - -export const updateDescription = createAction(MetadataActionTypeKeys.UPDATE_DESCRIPTION); -export const updateTitle = createAction(MetadataActionTypeKeys.UPDATE_TITLE); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/actions/panels.ts b/src/legacy/core_plugins/kibana/public/dashboard/actions/panels.ts deleted file mode 100644 index c4c41e53a545c..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/actions/panels.ts +++ /dev/null @@ -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 {} - -export interface UpdatePanelAction - extends KibanaAction {} - -export interface UpdatePanelsAction - extends KibanaAction {} - -export interface ResetPanelTitleAction - extends KibanaAction {} - -export interface SetPanelTitleActionPayload { - panelId: PanelId; - title?: string; -} - -export interface SetPanelTitleAction - extends KibanaAction {} - -export interface SetPanelsAction - extends KibanaAction {} - -export type PanelActions = - | DeletePanelAction - | UpdatePanelAction - | ResetPanelTitleAction - | UpdatePanelsAction - | SetPanelTitleAction - | SetPanelsAction; - -export const deletePanel = createAction(PanelActionTypeKeys.DELETE_PANEL); -export const updatePanel = createAction(PanelActionTypeKeys.UPDATE_PANEL); -export const resetPanelTitle = createAction(PanelActionTypeKeys.RESET_PANEL_TITLE); -export const setPanelTitle = createAction( - PanelActionTypeKeys.SET_PANEL_TITLE -); -export const updatePanels = createAction(PanelActionTypeKeys.UPDATE_PANELS); -export const setPanels = createAction(PanelActionTypeKeys.SET_PANELS); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/actions/view.ts b/src/legacy/core_plugins/kibana/public/dashboard/actions/view.ts deleted file mode 100644 index 205479032b36e..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/actions/view.ts +++ /dev/null @@ -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 {} - -export interface SetVisibleContextMenuPanelIdAction - extends KibanaAction {} - -export interface CloseContextMenuAction - extends KibanaAction {} - -export interface MaximizePanelAction - extends KibanaAction {} - -export interface MinimizePanelAction - extends KibanaAction {} - -export interface UpdateIsFullScreenModeAction - extends KibanaAction {} - -export interface UpdateUseMarginsAction - extends KibanaAction {} - -export interface UpdateHidePanelTitlesAction - extends KibanaAction {} - -export interface UpdateTimeRangeAction - extends KibanaAction {} - -export interface UpdateRefreshConfigAction - extends KibanaAction {} - -export interface UpdateFiltersAction - extends KibanaAction {} - -export interface UpdateQueryAction extends KibanaAction {} - -export type ViewActions = - | UpdateViewModeAction - | SetVisibleContextMenuPanelIdAction - | CloseContextMenuAction - | MaximizePanelAction - | MinimizePanelAction - | UpdateIsFullScreenModeAction - | UpdateUseMarginsAction - | UpdateHidePanelTitlesAction - | UpdateTimeRangeAction - | UpdateRefreshConfigAction - | UpdateFiltersAction - | UpdateQueryAction; - -export const updateViewMode = createAction(ViewActionTypeKeys.UPDATE_VIEW_MODE); -export const closeContextMenu = createAction(ViewActionTypeKeys.CLOSE_CONTEXT_MENU); -export const setVisibleContextMenuPanelId = createAction( - ViewActionTypeKeys.SET_VISIBLE_CONTEXT_MENU_PANEL_ID -); -export const maximizePanel = createAction(ViewActionTypeKeys.MAXIMIZE_PANEL); -export const minimizePanel = createAction(ViewActionTypeKeys.MINIMIZE_PANEL); -export const updateIsFullScreenMode = createAction( - ViewActionTypeKeys.UPDATE_IS_FULL_SCREEN_MODE -); -export const updateUseMargins = createAction(ViewActionTypeKeys.UPDATE_USE_MARGINS); -export const updateHidePanelTitles = createAction( - ViewActionTypeKeys.UPDATE_HIDE_PANEL_TITLES -); -export const updateTimeRange = createAction(ViewActionTypeKeys.UPDATE_TIME_RANGE); -export const updateRefreshConfig = createAction( - ViewActionTypeKeys.UPDATE_REFRESH_CONFIG -); -export const updateFilters = createAction(ViewActionTypeKeys.UPDATE_FILTERS); -export const updateQuery = createAction(ViewActionTypeKeys.UPDATE_QUERY); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html index d73e15287077c..1e226ab804bdc 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html @@ -118,9 +118,6 @@ - - +
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx index 4605a49d18c14..eba3ac367aeb5 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx @@ -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 { SavedObjectDashboard } from './saved_dashboard/saved_dashboard'; -import { DashboardAppState, SavedDashboardPanel, ConfirmModalFn, AddFilterFn } from './types'; +import { StaticIndexPattern, Query } from '../../../data/public'; -// @ts-ignore -- going away soon -import { DashboardViewportProvider } from './viewport/dashboard_viewport_provider'; +import { ViewMode } from '../../../embeddable_api/public'; +import { SavedObjectDashboard } from './saved_dashboard/saved_dashboard'; +import { DashboardAppState, SavedDashboardPanel, ConfirmModalFn } from './types'; -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>('AppState'); const kbnUrl = $injector.get('kbnUrl'); @@ -130,12 +108,6 @@ app.directive('dashboardApp', function($injector: IInjector) { const Private = $injector.get('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; }>('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, }), }; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx index 557c780f6192e..ec6755e95c323 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx @@ -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'; - -import { showNewVisModal } from '../visualize/wizard'; + 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 { 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; }; dashboardConfig: any; - localStorage: any; + localStorage: { + get: (prop: string) => unknown; + }; Private: IPrivate; kbnUrl: KbnUrl; AppStateClass: TAppStateClass; 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,18 +214,75 @@ 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 = { @@ -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 = {}; + 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(); + refreshDashboardContainer(); + }); + + $scope.$listenAndDigestAsync(timefilter, 'timeUpdate', () => { + updateState(); + refreshDashboardContainer(); }); - $scope.$listenAndDigestAsync(timefilter, 'timeUpdate', updateState); - function updateViewMode(newMode: DashboardViewMode) { + 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); - } } } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_constants.ts b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_constants.ts index 2d0995179860e..b76b3f309874a 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_constants.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_constants.ts @@ -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}`; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts index 88312629da5d9..4719e39f7a67f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts @@ -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; @@ -48,14 +45,11 @@ describe('DashboardState', function() { off: jest.fn(), on: jest.fn(), }; - const mockIndexPattern: IndexPattern = { id: 'index1', fields: [], title: 'hi' }; - function initDashboardState() { dashboardState = new DashboardStateManager({ savedDashboard, AppStateClass: getAppStateMock() as AppStateClass, hideWriteControls: false, - addFilter: () => {}, }); } @@ -112,72 +106,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); - }); - }); }); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts index a0e9178de72d6..1cdce181e1b6e 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts @@ -20,63 +20,23 @@ import { i18n } from '@kbn/i18n'; import _ from 'lodash'; +import { Filter } from '@kbn/es-query'; 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 { - updateViewMode, - setPanels, - updateUseMargins, - updateIsFullScreenMode, - minimizePanel, - updateTitle, - updateDescription, - updateHidePanelTitles, - updateTimeRange, - updateRefreshConfig, - clearStagedFilters, - updateFilters, - updateQuery, - closeContextMenu, - requestReload, -} from './actions'; -import { createPanelState } from './panel'; +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 { 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; - 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; 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(); + public handleDashboardContainerChanges(dashboardContainer: DashboardContainer) { + let dirty = false; - if (Object.values(storePanels).length !== appStatePanels.length) { - return false; - } + const savedDashboardPanelMap: { [key: string]: SavedDashboardPanel } = {}; - return appStatePanels.every(appStatePanel => { - const storePanel = getPanel(state, appStatePanel.panelIndex); - return _.isEqual(appStatePanel, storePanel); + 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; + } }); - } - - /** - * 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())); - } + const convertedPanelStateMap: { [key: string]: SavedDashboardPanel } = {}; - 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()); - } + Object.values(input.panels).forEach(panelState => { + if (savedDashboardPanelMap[panelState.explicitInput.id] === undefined) { + dirty = true; + } - private handleStoreChanges() { - 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; - } + convertedPanelStateMap[panelState.explicitInput.id] = convertPanelStateToSavedDashboardPanel( + panelState + ); - _.forEach(getEmbeddables(store.getState()), (embeddable, panelId) => { if ( - panelId && - embeddable.initialized && - !this.panelIndexPatternMapping.hasOwnProperty(panelId) + !_.isEqual( + convertedPanelStateMap[panelState.explicitInput.id], + savedDashboardPanelMap[panelState.explicitInput.id] + ) ) { - const embeddableMetadata = getEmbeddableMetadata(store.getState(), panelId); - if (embeddableMetadata && embeddableMetadata.indexPatterns) { - this.panelIndexPatternMapping[panelId] = _.compact(embeddableMetadata.indexPatterns); - dirty = true; - } + // A panel was changed + dirty = true; } }); - const stagedFilters = getStagedFilters(store.getState()); - stagedFilters.forEach(filter => { - this.addFilter(filter, this.getAppState()); - }); - if (stagedFilters.length > 0) { - this.saveState(); - store.dispatch(clearStagedFilters()); + if (dirty) { + this.appState.panels = Object.values(convertedPanelStateMap); + } + + if (input.isFullScreenMode !== this.getFullScreenMode()) { + this.setFullScreenMode(input.isFullScreenMode); } - const fullScreen = getFullScreenMode(store.getState()); - if (fullScreen !== this.getFullScreenMode()) { - this.setFullScreenMode(fullScreen); + 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.} 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.} 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(); } } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_strings.ts b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_strings.ts index 3ea7cf593687c..7752177c16d44 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_strings.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_strings.ts @@ -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) { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_view_mode.ts b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_view_mode.ts deleted file mode 100644 index e9f968249090b..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_view_mode.ts +++ /dev/null @@ -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', -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/grid/__snapshots__/dashboard_grid.test.js.snap b/src/legacy/core_plugins/kibana/public/dashboard/grid/__snapshots__/dashboard_grid.test.js.snap deleted file mode 100644 index 806e11c557a02..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/grid/__snapshots__/dashboard_grid.test.js.snap +++ /dev/null @@ -1,77 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders DashboardGrid 1`] = ` - -
- -
-
- -
-
-`; - -exports[`renders DashboardGrid with no visualizations 1`] = ` - -`; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/grid/_dashboard_grid.scss b/src/legacy/core_plugins/kibana/public/dashboard/grid/_dashboard_grid.scss deleted file mode 100644 index 7acdf1ce5993a..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/grid/_dashboard_grid.scss +++ /dev/null @@ -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; - } - } - } -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/grid/_index.scss b/src/legacy/core_plugins/kibana/public/dashboard/grid/_index.scss deleted file mode 100644 index eb393d7603b8a..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/grid/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './dashboard_grid'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/grid/dashboard_grid.test.js b/src/legacy/core_plugins/kibana/public/dashboard/grid/dashboard_grid.test.js deleted file mode 100644 index 0d9b80763c136..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/grid/dashboard_grid.test.js +++ /dev/null @@ -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(); - expect(component).toMatchSnapshot(); - const panelElements = component.find('Connect(InjectIntl(DashboardPanelUi))'); - expect(panelElements.length).toBe(2); -}); - -test('renders DashboardGrid with no visualizations', () => { - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); -}); - -test('adjusts z-index of focused panel to be higher than siblings', () => { - const component = shallowWithIntl(); - 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'); -}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/grid/dashboard_grid.tsx b/src/legacy/core_plugins/kibana/public/dashboard/grid/dashboard_grid.tsx deleted file mode 100644 index 4ff38fd0fbfd3..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/grid/dashboard_grid.tsx +++ /dev/null @@ -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 ( - ensureWindowScrollsToBottom(event)} - > - {children} - - ); -} - -// 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 { - // 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.} - 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(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 ( -
{ - this.gridItems[panel.panelIndex] = reactGridItem; - }} - > - -
- ); - }); - } - - public render() { - if (this.state.isLayoutInvalid) { - return null; - } - - const { dashboardViewMode, maximizedPanelId, useMargins } = this.props; - const isViewMode = dashboardViewMode === DashboardViewMode.VIEW; - return ( - - {this.renderDOM()} - - ); - } -} - -export const DashboardGrid = injectI18n(DashboardGridUi); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/grid/dashboard_grid_container.ts b/src/legacy/core_plugins/kibana/public/dashboard/grid/dashboard_grid_container.ts deleted file mode 100644 index aaf994376759d..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/grid/dashboard_grid_container.ts +++ /dev/null @@ -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); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/grid/index.ts b/src/legacy/core_plugins/kibana/public/dashboard/grid/index.ts deleted file mode 100644 index b226168a31a6a..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/grid/index.ts +++ /dev/null @@ -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'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/embeddable_saved_object_converters.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/embeddable_saved_object_converters.test.ts new file mode 100644 index 0000000000000..4d8e7787cec4d --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/embeddable_saved_object_converters.test.ts @@ -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 = { + 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 = { + 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); +}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/embeddable_saved_object_converters.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/embeddable_saved_object_converters.ts new file mode 100644 index 0000000000000..112f6c7d833b1 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/embeddable_saved_object_converters.ts @@ -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 }), + }; +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/get_app_state_defaults.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/get_app_state_defaults.ts index 70a6799921723..1dbc7eb031eea 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/lib/get_app_state_defaults.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/get_app_state_defaults.ts @@ -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, }; } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.test.ts index 080334025a129..10c27226300a5 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.test.ts @@ -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'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.test.ts index c06ab33f093fd..f18a1b29f7181 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.test.ts @@ -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 = { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.ts index 6cc69013a4b1a..b52602df1b8b0 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.ts @@ -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, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_core.test.mocks.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_core.test.mocks.ts new file mode 100644 index 0000000000000..fff5aeab599ea --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_core.test.mocks.ts @@ -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: () => {} })); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/__snapshots__/dashboard_panel.test.tsx.snap b/src/legacy/core_plugins/kibana/public/dashboard/panel/__snapshots__/dashboard_panel.test.tsx.snap deleted file mode 100644 index 107bda0aea08b..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/__snapshots__/dashboard_panel.test.tsx.snap +++ /dev/null @@ -1,52 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DashboardPanel matches snapshot 1`] = ` -
-
-
- my embeddable title -
-
-
- -
-
-
-
-
-`; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/__snapshots__/panel_error.test.tsx.snap b/src/legacy/core_plugins/kibana/public/dashboard/panel/__snapshots__/panel_error.test.tsx.snap deleted file mode 100644 index e12c93f54cb00..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/__snapshots__/panel_error.test.tsx.snap +++ /dev/null @@ -1,96 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PanelError renders given React Element 1`] = ` -
-
-
- -
-
- test -
-
-
-
-`; - -exports[`PanelError renders plain string 1`] = ` -
-
-
- -
-
-

- test -

-
-
-
-
-`; - -exports[`PanelError renders string with markdown link 1`] = ` -
-
-
- -
-
-

- - test - -

-
-
-
-
-`; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/__tests__/panel_state.ts b/src/legacy/core_plugins/kibana/public/dashboard/panel/__tests__/panel_state.ts deleted file mode 100644 index 57a3d336c3dec..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/__tests__/panel_state.ts +++ /dev/null @@ -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); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/_dashboard_panel.scss b/src/legacy/core_plugins/kibana/public/dashboard/panel/_dashboard_panel.scss deleted file mode 100644 index 8f860151478f0..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/_dashboard_panel.scss +++ /dev/null @@ -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; - } -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/_index.scss b/src/legacy/core_plugins/kibana/public/dashboard/panel/_index.scss deleted file mode 100644 index 7fed11fe9db9d..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import "./dashboard_panel"; -@import 'panel_header/panel_options_menu_form'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/dashboard_panel.test.tsx b/src/legacy/core_plugins/kibana/public/dashboard/panel/dashboard_panel.test.tsx deleted file mode 100644 index e6e0cf292e655..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/dashboard_panel.test.tsx +++ /dev/null @@ -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( - - - - ); - expect(takeMountedSnapshot(component)).toMatchSnapshot(); -}); - -test('renders an error when error prop is passed', () => { - const props = getProps({ - error: 'Simulated error', - }); - - const component = mountWithIntl( - - - - ); - const panelError = component.find(PanelError); - expect(panelError.length).toBe(1); -}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/dashboard_panel.tsx b/src/legacy/core_plugins/kibana/public/dashboard/panel/dashboard_panel.tsx deleted file mode 100644 index 5aa0dbf1bd2fb..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/dashboard_panel.tsx +++ /dev/null @@ -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 { - [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 ( -
(this.panelElement = panelElement || undefined)} - > - {!this.props.initialized && } -
- ); - } - - 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 ; - } - - 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 ( - - - - {this.renderContent()} - - ); - } -} - -export const DashboardPanel = injectI18n(DashboardPanelUi); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/dashboard_panel_container.test.tsx b/src/legacy/core_plugins/kibana/public/dashboard/panel/dashboard_panel_container.test.tsx deleted file mode 100644 index 939bf4c04ae0e..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/dashboard_panel_container.test.tsx +++ /dev/null @@ -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( - - - - ); - setTimeout(() => { - component.update(); - const panelError = component.find(PanelError); - expect(panelError.length).toBe(1); - done(); - }, 0); -}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/dashboard_panel_container.ts b/src/legacy/core_plugins/kibana/public/dashboard/panel/dashboard_panel_container.ts deleted file mode 100644 index 100d7ba2806b7..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/dashboard_panel_container.ts +++ /dev/null @@ -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>, - { 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); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_error.test.tsx b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_error.test.tsx deleted file mode 100644 index 11b207edf9bcc..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_error.test.tsx +++ /dev/null @@ -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(); - expect(takeMountedSnapshot(component)).toMatchSnapshot(); -}); - -test('PanelError renders string with markdown link', () => { - const component = mount(); - expect(takeMountedSnapshot(component)).toMatchSnapshot(); -}); - -test('PanelError renders given React Element ', () => { - const component = mount(test
} />); - expect(takeMountedSnapshot(component)).toMatchSnapshot(); -}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_error.tsx b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_error.tsx deleted file mode 100644 index 83365e479e5b8..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_error.tsx +++ /dev/null @@ -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 ( -
- - - - {typeof error === 'string' ? : error} - -
- ); -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/_panel_options_menu_form.scss b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/_panel_options_menu_form.scss deleted file mode 100644 index e7fc3c59c3889..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/_panel_options_menu_form.scss +++ /dev/null @@ -1,3 +0,0 @@ -.dshPanel__optionsMenuForm { - padding: $euiSize; -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_customize_panel_action.tsx b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_customize_panel_action.tsx deleted file mode 100644 index 065f073b0177d..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_customize_panel_action.tsx +++ /dev/null @@ -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: () => ( - - ), - } - ), - icon: , - isVisible: ({ containerState }) => containerState.viewMode === DashboardViewMode.EDIT, - getDisplayName: () => { - return i18n.translate('kbn.dashboard.panel.customizePanel.displayName', { - defaultMessage: 'Customize panel', - }); - }, - } - ); -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_edit_panel_action.tsx b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_edit_panel_action.tsx deleted file mode 100644 index 4441b4101e961..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_edit_panel_action.tsx +++ /dev/null @@ -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: , - 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', - }); - }, - } - ); -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_inspector_panel_action.tsx b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_inspector_panel_action.tsx deleted file mode 100644 index af8918fe99609..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_inspector_panel_action.tsx +++ /dev/null @@ -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: , - 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; - }); - }, - } - ); -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_remove_panel_action.tsx b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_remove_panel_action.tsx deleted file mode 100644 index 113079aaeb103..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_remove_panel_action.tsx +++ /dev/null @@ -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: , - isVisible: ({ containerState }) => - containerState.viewMode === DashboardViewMode.EDIT && !containerState.isPanelExpanded, - onClick: onDeletePanel, - } - ); -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_toggle_expand_panel_action.tsx b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_toggle_expand_panel_action.tsx deleted file mode 100644 index 71dd5598cad26..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/get_toggle_expand_panel_action.tsx +++ /dev/null @@ -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: , - onClick: toggleExpandedPanel, - } - ); -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/index.ts b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/index.ts deleted file mode 100644 index b4cc5ea82e948..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_actions/index.ts +++ /dev/null @@ -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'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_header.tsx b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_header.tsx deleted file mode 100644 index 9f6d9bb309da0..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_header.tsx +++ /dev/null @@ -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 ( -
- -
- ); - } - - return ( -
-
- {hidePanelTitles ? '' : title} -
- - -
- ); -} - -export const PanelHeader = injectI18n(PanelHeaderUi); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_header_container.test.tsx b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_header_container.test.tsx deleted file mode 100644 index f395b17207be5..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_header_container.test.tsx +++ /dev/null @@ -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( - - - - ); - 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'); -}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_header_container.ts b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_header_container.ts deleted file mode 100644 index ba25db6acf8e1..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_header_container.ts +++ /dev/null @@ -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); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu.tsx b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu.tsx deleted file mode 100644 index a9ebf3ce42a51..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu.tsx +++ /dev/null @@ -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 = ( - - ); - - return ( - - - - ); -} - -export const PanelOptionsMenu = injectI18n(PanelOptionsMenuUi); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu_container.ts b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu_container.ts deleted file mode 100644 index 6b537a2a2c405..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu_container.ts +++ /dev/null @@ -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); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu_form.tsx b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu_form.tsx deleted file mode 100644 index c80ab00a46b77..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/panel_options_menu_form.tsx +++ /dev/null @@ -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) { - onUpdatePanelTitle(event.target.value); - } - - function onKeyDown(event: KeyboardEvent) { - if (event.keyCode === keyCodes.ENTER) { - onClose(); - } - } - - return ( -
- - - - - - - -
- ); -} - -export const PanelOptionsMenuForm = injectI18n(PanelOptionsMenuFormUi); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_state.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_state.test.ts deleted file mode 100644 index 3b821b005d07f..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_state.test.ts +++ /dev/null @@ -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); -}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_state.ts b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_state.ts deleted file mode 100644 index 105ae2ebf1fcb..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_state.ts +++ /dev/null @@ -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: {}, - }; -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_utils.ts b/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_utils.ts deleted file mode 100644 index 9abb916ca637f..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_utils.ts +++ /dev/null @@ -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; - } -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/reducers/embeddables.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/reducers/embeddables.test.ts deleted file mode 100644 index aa29740fa9d37..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/reducers/embeddables.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/reducers/embeddables.ts b/src/legacy/core_plugins/kibana/public/dashboard/reducers/embeddables.ts deleted file mode 100644 index f85e7649d8f9a..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/reducers/embeddables.ts +++ /dev/null @@ -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(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(embeddables, embeddable => ({ - ...embeddable, - lastReloadRequestTime, - })); -}; - -export const embeddablesReducer: Reducer = ( - 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; - } -}; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/reducers/index.ts b/src/legacy/core_plugins/kibana/public/dashboard/reducers/index.ts deleted file mode 100644 index 1f4a26d255d33..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/reducers/index.ts +++ /dev/null @@ -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, -}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/reducers/metadata.ts b/src/legacy/core_plugins/kibana/public/dashboard/reducers/metadata.ts deleted file mode 100644 index 3a2b9c8e1bae3..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/reducers/metadata.ts +++ /dev/null @@ -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 = ( - 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; - } -}; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/reducers/panels.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/reducers/panels.test.ts deleted file mode 100644 index 642bf6d4ce2e7..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/reducers/panels.test.ts +++ /dev/null @@ -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'); -}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/reducers/panels.ts b/src/legacy/core_plugins/kibana/public/dashboard/reducers/panels.ts deleted file mode 100644 index 735f636d8af3b..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/reducers/panels.ts +++ /dev/null @@ -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 = (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; - } -}; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/reducers/view.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/reducers/view.test.ts deleted file mode 100644 index 481e6c9a8555a..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/reducers/view.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/reducers/view.ts b/src/legacy/core_plugins/kibana/public/dashboard/reducers/view.ts deleted file mode 100644 index 8c1a1585caab7..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/reducers/view.ts +++ /dev/null @@ -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 = ( - 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; - } -}; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/selectors/dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/selectors/dashboard.ts deleted file mode 100644 index 47d771de41522..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/selectors/dashboard.ts +++ /dev/null @@ -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 => - 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')); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/selectors/types.ts b/src/legacy/core_plugins/kibana/public/dashboard/selectors/types.ts deleted file mode 100644 index befb6f6936105..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/selectors/types.ts +++ /dev/null @@ -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; -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/store/panel_actions_store.ts b/src/legacy/core_plugins/kibana/public/dashboard/store/panel_actions_store.ts deleted file mode 100644 index 69a9a93b1828a..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/store/panel_actions_store.ts +++ /dev/null @@ -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(); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/add_panel.test.js.snap b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/add_panel.test.js.snap deleted file mode 100644 index 493f4f65df776..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/add_panel.test.js.snap +++ /dev/null @@ -1,62 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`render 1`] = ` - - - -

- -

-
-
- - - - - - - - - - - - -
-`; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.tsx b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.tsx deleted file mode 100644 index 27595e2ecd548..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.tsx +++ /dev/null @@ -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 { - 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 ( - - - -

- -

-
-
- - Boolean(embeddableFactory.savedObjectMetaData)) - .map(({ savedObjectMetaData }) => savedObjectMetaData) as Array< - SavedObjectMetaData - > - } - showFilter={true} - noItemsMessage={i18n.translate( - 'kbn.dashboard.topNav.addPanel.noMatchingObjectsMessage', - { - defaultMessage: 'No matching objects found.', - } - )} - /> - - {capabilities.get().visualize.save ? ( - - - - - - - - - - ) : null} -
- ); - } -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.ts b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.ts index 32775101b8b34..955868f1f7ca8 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.ts @@ -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} - 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]), diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_add_panel.tsx b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_add_panel.tsx deleted file mode 100644 index 76df56acd1749..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_add_panel.tsx +++ /dev/null @@ -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 = ( - - - - ); - ReactDOM.render(element, container); -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/types.ts b/src/legacy/core_plugins/kibana/public/dashboard/types.ts index 4425496b7b554..ed2f4904580d1 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/types.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/types.ts @@ -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 { - 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 diff --git a/src/legacy/core_plugins/kibana/public/dashboard/viewport/_dashboard_viewport.scss b/src/legacy/core_plugins/kibana/public/dashboard/viewport/_dashboard_viewport.scss deleted file mode 100644 index 7cbe135115877..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/viewport/_dashboard_viewport.scss +++ /dev/null @@ -1,8 +0,0 @@ -.dshDashboardViewport { - width: 100%; - background-color: $euiColorEmptyShade; -} - -.dshDashboardViewport-withMargins { - width: 100%; -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/viewport/_index.scss b/src/legacy/core_plugins/kibana/public/dashboard/viewport/_index.scss deleted file mode 100644 index 56483d9d10195..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/viewport/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './dashboard_viewport'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/viewport/dashboard_viewport.tsx b/src/legacy/core_plugins/kibana/public/dashboard/viewport/dashboard_viewport.tsx deleted file mode 100644 index 8e34b50a0383d..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/viewport/dashboard_viewport.tsx +++ /dev/null @@ -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 ( -
- {isFullScreenMode && } - -
- ); -} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/viewport/dashboard_viewport_container.js b/src/legacy/core_plugins/kibana/public/dashboard/viewport/dashboard_viewport_container.js deleted file mode 100644 index 9c48241a390c1..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/viewport/dashboard_viewport_container.js +++ /dev/null @@ -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); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/viewport/dashboard_viewport_provider.js b/src/legacy/core_plugins/kibana/public/dashboard/viewport/dashboard_viewport_provider.js deleted file mode 100644 index a219f763dac28..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/viewport/dashboard_viewport_provider.js +++ /dev/null @@ -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 ( - - - - ); -} - -DashboardViewportProvider.propTypes = { - getEmbeddableFactory: PropTypes.func.isRequired, -}; diff --git a/src/legacy/core_plugins/kibana/public/discover/_index.scss b/src/legacy/core_plugins/kibana/public/discover/_index.scss index 34a7c593a6055..57a6b89c37722 100644 --- a/src/legacy/core_plugins/kibana/public/discover/_index.scss +++ b/src/legacy/core_plugins/kibana/public/discover/_index.scss @@ -16,3 +16,5 @@ @import 'hacks'; @import 'discover'; + +@import 'embeddable/index'; diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/_embeddables.scss b/src/legacy/core_plugins/kibana/public/discover/embeddable/_embeddables.scss new file mode 100644 index 0000000000000..e9d3843485ac2 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/_embeddables.scss @@ -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 */ + } +} \ No newline at end of file diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/_index.scss b/src/legacy/core_plugins/kibana/public/discover/embeddable/_index.scss new file mode 100644 index 0000000000000..6d64040e9e7a3 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/_index.scss @@ -0,0 +1,2 @@ + +@import 'embeddables'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/index.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/index.ts similarity index 88% rename from src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/index.ts rename to src/legacy/core_plugins/kibana/public/discover/embeddable/index.ts index 1ba52de585d1f..3138008f3e3a0 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/panel_header/index.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/index.ts @@ -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'; diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts index cfcc6c1519617..8591bb0ed7137 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts @@ -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; + $rootScope: ng.IRootScopeService; + $compile: ng.ICompileService; + courier: any; savedSearch: SavedSearch; editUrl: string; + indexPatterns?: StaticIndexPattern[]; editable: boolean; - $rootScope: ng.IRootScopeService; - $compile: ng.ICompileService; + queryFilter: unknown; } -export class SearchEmbeddable extends Embeddable { - private readonly onEmbeddableStateChanged: OnEmbeddableStateChanged; +export const SEARCH_EMBEDDABLE_TYPE = 'search'; + +export class SearchEmbeddable extends Embeddable + 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(); } } diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts index 1f041fc09dfc3..01c2d96f853de 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts @@ -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.} - */ - 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 & { id: string; timeRange: TimeRange }, + parent?: Container + ): Promise { + const $injector = await chrome.dangerouslyGetActiveInjector(); + + const $compile = $injector.get('$compile'); + const $rootScope = $injector.get('$rootScope'); + const courier = $injector.get('courier'); + const searchLoader = $injector.get('savedSearches'); + const editUrl = chrome.addBasePath(`/app/kibana${searchLoader.urlFor(savedObjectId)}`); + + const Private = $injector.get('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()); diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory_provider.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory_provider.ts deleted file mode 100644 index 717fdc717f1f9..0000000000000 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory_provider.ts +++ /dev/null @@ -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); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js b/src/legacy/core_plugins/kibana/public/discover/embeddable/types.ts similarity index 50% rename from src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js rename to src/legacy/core_plugins/kibana/public/discover/embeddable/types.ts index 96147b3852020..c75b58343666e 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/types.ts @@ -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'; - -jest.mock('ui/capabilities', - () => ({ - capabilities: { - get: () => ({ - visualize: { - show: true, - save: true - } - }) - } - }), { virtual: true }); + EmbeddableInput, + EmbeddableOutput, + IEmbeddable, +} from '../../../../embeddable_api/public/index'; -jest.mock('ui/notify', - () => ({ - toastNotifications: { - addDanger: () => {}, - } - }), { virtual: true }); +export interface SearchInput extends EmbeddableInput { + timeRange: TimeRange; + query?: Query; + filters?: Filter[]; + hidePanelTitles?: boolean; + columns?: string[]; + sort?: string[]; +} -let onClose; -beforeEach(() => { - onClose = sinon.spy(); -}); +export interface SearchOutput extends EmbeddableOutput { + editUrl: string; + indexPatterns?: StaticIndexPattern[]; + editable: boolean; +} -test('render', () => { - const component = shallow( {}} - addNewVis={() => {}} - embeddableFactories={[]} - />); - expect(component).toMatchSnapshot(); -}); +export interface ISearchEmbeddable extends IEmbeddable { + getSavedSearch(): SavedSearch; +} diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index 1bc91360eddb1..dbdc9e4c547e0 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -41,6 +41,7 @@ import 'uiExports/indexManagement'; import 'uiExports/devTools'; import 'uiExports/docViews'; import 'uiExports/embeddableFactories'; +import 'uiExports/embeddableActions'; import 'uiExports/inspectorViews'; import 'uiExports/search'; import 'uiExports/autocompleteProviders'; diff --git a/src/legacy/core_plugins/kibana/public/reducers.ts b/src/legacy/core_plugins/kibana/public/reducers.ts deleted file mode 100644 index 5097134087d61..0000000000000 --- a/src/legacy/core_plugins/kibana/public/reducers.ts +++ /dev/null @@ -1,30 +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 { dashboard } from './dashboard/reducers'; -import { CoreKibanaState } from './selectors'; - -/** - * Only a single reducer now, but eventually there should be one for each sub app that is part of the - * core kibana plugins. - */ -export const reducers = combineReducers({ - dashboard, -}); diff --git a/src/legacy/core_plugins/kibana/public/selectors/dashboard_selectors.ts b/src/legacy/core_plugins/kibana/public/selectors/dashboard_selectors.ts deleted file mode 100644 index 85a702cb93296..0000000000000 --- a/src/legacy/core_plugins/kibana/public/selectors/dashboard_selectors.ts +++ /dev/null @@ -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. - */ - -import { Filter } from '@kbn/es-query'; -import { TimeRange } from 'ui/timefilter/time_history'; -import { Query } from 'src/legacy/core_plugins/data/public'; -import { DashboardViewMode } from '../dashboard/dashboard_view_mode'; -import * as DashboardSelectors from '../dashboard/selectors'; -import { PanelId } from '../dashboard/selectors/types'; -import { CoreKibanaState } from './types'; -import { StagedFilter } from '../dashboard/types'; - -export const getDashboard = (state: CoreKibanaState): DashboardSelectors.DashboardState => - state.dashboard; - -export const getPanels = (state: CoreKibanaState) => - DashboardSelectors.getPanels(getDashboard(state)); -export const getPanel = (state: CoreKibanaState, panelId: PanelId) => - DashboardSelectors.getPanel(getDashboard(state), panelId); -export const getPanelType = (state: CoreKibanaState, panelId: PanelId) => - DashboardSelectors.getPanelType(getDashboard(state), panelId); - -export const getEmbeddables = (state: CoreKibanaState) => - DashboardSelectors.getEmbeddables(getDashboard(state)); -export const getEmbeddableError = (state: CoreKibanaState, panelId: PanelId) => - DashboardSelectors.getEmbeddableError(getDashboard(state), panelId); -export const getEmbeddableInitialized = (state: CoreKibanaState, panelId: PanelId) => - DashboardSelectors.getEmbeddableInitialized(getDashboard(state), panelId); -export const getEmbeddableCustomization = (state: CoreKibanaState, panelId: PanelId) => - DashboardSelectors.getEmbeddableCustomization(getDashboard(state), panelId); -export const getEmbeddableStagedFilter = (state: CoreKibanaState, panelId: PanelId) => - DashboardSelectors.getEmbeddableStagedFilter(getDashboard(state), panelId); -export const getEmbeddableMetadata = (state: CoreKibanaState, panelId: PanelId) => - DashboardSelectors.getEmbeddableMetadata(getDashboard(state), panelId); - -export const getStagedFilters = (state: CoreKibanaState): StagedFilter[] => - DashboardSelectors.getStagedFilters(getDashboard(state)); -export const getViewMode = (state: CoreKibanaState): DashboardViewMode => - DashboardSelectors.getViewMode(getDashboard(state)); -export const getFullScreenMode = (state: CoreKibanaState): boolean => - DashboardSelectors.getFullScreenMode(getDashboard(state)); -export const getMaximizedPanelId = (state: CoreKibanaState): PanelId | undefined => - DashboardSelectors.getMaximizedPanelId(getDashboard(state)); -export const getUseMargins = (state: CoreKibanaState): boolean => - DashboardSelectors.getUseMargins(getDashboard(state)); -export const getHidePanelTitles = (state: CoreKibanaState): boolean => - DashboardSelectors.getHidePanelTitles(getDashboard(state)); -export const getTimeRange = (state: CoreKibanaState): TimeRange => - DashboardSelectors.getTimeRange(getDashboard(state)); -export const getFilters = (state: CoreKibanaState): Filter[] => - DashboardSelectors.getFilters(getDashboard(state)); -export const getQuery = (state: CoreKibanaState): Query => - DashboardSelectors.getQuery(getDashboard(state)); - -export const getTitle = (state: CoreKibanaState): string => - DashboardSelectors.getTitle(getDashboard(state)); -export const getDescription = (state: CoreKibanaState): string | undefined => - DashboardSelectors.getDescription(getDashboard(state)); diff --git a/src/legacy/core_plugins/kibana/public/selectors/index.ts b/src/legacy/core_plugins/kibana/public/selectors/index.ts deleted file mode 100644 index 6c72362d160c4..0000000000000 --- a/src/legacy/core_plugins/kibana/public/selectors/index.ts +++ /dev/null @@ -1,21 +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 './dashboard_selectors'; -export { CoreKibanaState } from './types'; diff --git a/src/legacy/core_plugins/kibana/public/selectors/types.ts b/src/legacy/core_plugins/kibana/public/selectors/types.ts deleted file mode 100644 index 61ae8da5ed4bb..0000000000000 --- a/src/legacy/core_plugins/kibana/public/selectors/types.ts +++ /dev/null @@ -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 { Action } from 'redux'; -import { ThunkAction } from 'redux-thunk'; -import { DashboardState } from '../dashboard/selectors'; - -export interface CoreKibanaState { - readonly dashboard: DashboardState; -} - -export interface KibanaAction extends Action { - readonly type: T; - readonly payload: P; -} - -export type KibanaThunk< - R = Action | Promise | void, - S = CoreKibanaState, - E = any, - A extends Action = Action -> = ThunkAction; diff --git a/src/legacy/core_plugins/kibana/public/store.ts b/src/legacy/core_plugins/kibana/public/store.ts deleted file mode 100644 index 91aa57d8035f9..0000000000000 --- a/src/legacy/core_plugins/kibana/public/store.ts +++ /dev/null @@ -1,49 +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 { applyMiddleware, compose, createStore } from 'redux'; -import thunk from 'redux-thunk'; - -import { DashboardViewMode } from './dashboard/dashboard_view_mode'; -import { reducers } from './reducers'; - -const enhancers = [applyMiddleware(thunk)]; - -export const store = createStore( - reducers, - { - dashboard: { - embeddables: {}, - metadata: { - title: 'New Dashboard', - }, - panels: {}, - view: { - filters: [], - hidePanelTitles: false, - isFullScreenMode: false, - query: { language: 'lucene', query: '' }, - timeRange: { from: 'now-15m', to: 'now' }, - useMargins: true, - viewMode: DashboardViewMode.VIEW, - }, - }, - }, - compose(...enhancers) -); diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/_embeddables.scss b/src/legacy/core_plugins/kibana/public/visualize/embeddable/_embeddables.scss new file mode 100644 index 0000000000000..23d3e189767d7 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/_embeddables.scss @@ -0,0 +1,48 @@ +/* +* 1. Fix overflow of vis's specifically for inside embeddable panels, lets the panel decide the overflow +* 2. Force a better looking scrollbar +*/ + +.embPanel { + .visualization { + @include euiScrollBar; /* 2 */ + } + + .visualization .visChart__container { + overflow: visible; /* 1 */ + } + + .visLegend__toggle { + border-bottom-right-radius: 0; + border-top-left-radius: 0; + } +} + +// 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 + */ + +.embPanel_optionsMenuPopover[class*="-isOpen"], +.embPanel:hover { + .visLegend__toggle { + opacity: 1; + } +} + +.embPanel .visLegend__toggle { + opacity: 0; /* 1 */ + + &:focus { + opacity: 1; /* 2 */ + } +} + +.embPanel--editing { + .visLegend__toggle { + opacity: 1; /* 3 */ + } +} \ No newline at end of file diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/_index.scss b/src/legacy/core_plugins/kibana/public/visualize/embeddable/_index.scss index 1e273d18714d6..6b31803e7c8c5 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/_index.scss +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/_index.scss @@ -1 +1,2 @@ @import './visualize_lab_disabled'; +@import './embeddables'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/selectors/index.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/constants.ts similarity index 93% rename from src/legacy/core_plugins/kibana/public/dashboard/selectors/index.ts rename to src/legacy/core_plugins/kibana/public/visualize/embeddable/constants.ts index c3d90748976d4..b7967db289567 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/selectors/index.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/constants.ts @@ -17,6 +17,4 @@ * under the License. */ -export * from './types'; - -export * from './dashboard'; +export const VISUALIZE_EMBEDDABLE_TYPE = 'visualization'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/disabled_lab_embeddable.tsx b/src/legacy/core_plugins/kibana/public/visualize/embeddable/disabled_lab_embeddable.tsx index e119f7cfcf253..a9dd3b6be5a2d 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/disabled_lab_embeddable.tsx +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/disabled_lab_embeddable.tsx @@ -19,26 +19,24 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { Embeddable } from 'ui/embeddable'; -import { I18nContext } from 'ui/i18n'; +import { Embeddable, EmbeddableOutput } from 'plugins/embeddable_api'; import { DisabledLabVisualization } from './disabled_lab_visualization'; +import { VisualizeInput } from './visualize_embeddable'; +import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; -export class DisabledLabEmbeddable extends Embeddable { +export class DisabledLabEmbeddable extends Embeddable { private domNode?: HTMLElement; + public readonly type = VISUALIZE_EMBEDDABLE_TYPE; - constructor(title: string) { - super({ title }); + constructor(private readonly title: string, initialInput: VisualizeInput) { + super(initialInput, { title }); } + public reload() {} public render(domNode: HTMLElement) { - if (this.metadata.title) { + if (this.title) { this.domNode = domNode; - ReactDOM.render( - - - , - domNode - ); + ReactDOM.render(, domNode); } } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/panel/index.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/index.ts similarity index 74% rename from src/legacy/core_plugins/kibana/public/dashboard/panel/index.ts rename to src/legacy/core_plugins/kibana/public/visualize/embeddable/index.ts index e5a5ad18a94c2..a1cd31eebef20 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/panel/index.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/index.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - -export { DashboardPanelContainer as DashboardPanel } from './dashboard_panel_container'; -export { createPanelState } from './panel_state'; +export { DisabledLabEmbeddable } from './disabled_lab_embeddable'; +export { VisualizeEmbeddable, VisualizeInput } from './visualize_embeddable'; +export { VisualizeEmbeddableFactory } from './visualize_embeddable_factory'; +export { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts index 1895c7affca6a..a59029fc36ef1 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts @@ -18,8 +18,14 @@ */ import _ from 'lodash'; -import { ContainerState, Embeddable } from 'ui/embeddable'; -import { OnEmbeddableStateChanged } from 'ui/embeddable/embeddable_factory'; +import { + APPLY_FILTER_TRIGGER, + Embeddable, + EmbeddableInput, + EmbeddableOutput, + Trigger, + Container, +} from 'plugins/embeddable_api'; import { StaticIndexPattern } from 'ui/index_patterns'; import { PersistedState } from 'ui/persisted_state'; import { VisualizeLoader } from 'ui/visualize/loader'; @@ -29,50 +35,72 @@ import { VisualizeLoaderParams, VisualizeUpdateParams, } from 'ui/visualize/loader/types'; -import { i18n } from '@kbn/i18n'; +import { Subscription } from 'rxjs'; +import * as Rx from 'rxjs'; import { TimeRange } from 'ui/timefilter/time_history'; import { Query } from 'src/legacy/core_plugins/data/public'; import { Filter } from '@kbn/es-query'; +import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; + +const getKeys = (o: T): Array => Object.keys(o) as Array; export interface VisualizeEmbeddableConfiguration { - onEmbeddableStateChanged: OnEmbeddableStateChanged; savedVisualization: VisSavedObject; indexPatterns?: StaticIndexPattern[]; - editUrl?: string; - editable: boolean; + editUrl: string; loader: VisualizeLoader; + editable: boolean; +} + +export interface VisualizeInput extends EmbeddableInput { + timeRange?: TimeRange; + query?: Query; + filters?: Filter[]; + vis?: { + colors?: { [key: string]: string }; + }; } -export class VisualizeEmbeddable extends Embeddable { - private onEmbeddableStateChanged: OnEmbeddableStateChanged; +export interface VisualizeOutput extends EmbeddableOutput { + editUrl: string; + indexPatterns?: StaticIndexPattern[]; + savedObjectId: string; +} + +export class VisualizeEmbeddable extends Embeddable { private savedVisualization: VisSavedObject; private loader: VisualizeLoader; private uiState: PersistedState; private handler?: EmbeddedVisualizeHandler; - private customization?: { [key: string]: any }; - private panelTitle?: string; private timeRange?: TimeRange; private query?: Query; + private title?: string; private filters?: Filter[]; + private subscription: Subscription; + public readonly type = VISUALIZE_EMBEDDABLE_TYPE; - constructor({ - onEmbeddableStateChanged, - savedVisualization, - indexPatterns, - editUrl, - editable, - loader, - }: VisualizeEmbeddableConfiguration) { - super({ - title: savedVisualization.title, + constructor( + { + savedVisualization, + loader, editUrl, - editLabel: i18n.translate('kbn.embeddable.visualize.editLabel', { - defaultMessage: 'Edit visualization', - }), - editable, indexPatterns, - }); - this.onEmbeddableStateChanged = onEmbeddableStateChanged; + editable, + }: VisualizeEmbeddableConfiguration, + initialInput: VisualizeInput, + parent?: Container + ) { + super( + initialInput, + { + defaultTitle: savedVisualization.title, + editUrl, + indexPatterns, + editable, + savedObjectId: savedVisualization.id!, + }, + parent + ); this.savedVisualization = savedVisualization; this.loader = loader; @@ -82,6 +110,11 @@ export class VisualizeEmbeddable extends Embeddable { this.uiState = new PersistedState(parsedUiState); this.uiState.on('change', this.uiStateChangeHandler); + + this.subscription = Rx.merge(this.getOutput$(), this.getInput$()).subscribe(() => { + this.reload(); + this.handleChanges(); + }); } public getInspectorAdapters() { @@ -91,62 +124,61 @@ export class VisualizeEmbeddable extends Embeddable { return this.handler.inspectorAdapters; } - public getEmbeddableState() { - return { - customization: this.customization, - }; + public supportsTrigger(trigger: Trigger) { + return trigger.id !== APPLY_FILTER_TRIGGER; } /** - * Transfers all changes in the containerState.embeddableCustomization into + * Transfers all changes in the containerState.customization into * the uiState of this visualization. */ - public transferCustomizationsToUiState(containerState: ContainerState) { + public transferCustomizationsToUiState() { // Check for changes that need to be forwarded to the uiState // Since the vis has an own listener on the uiState we don't need to // pass anything from here to the handler.update method - const customization = containerState.embeddableCustomization; - if (customization && !_.isEqual(this.customization, customization)) { + const visCustomizations = this.input.vis; + if (visCustomizations) { // Turn this off or the uiStateChangeHandler will fire for every modification. this.uiState.off('change', this.uiStateChangeHandler); this.uiState.clearAllKeys(); - Object.getOwnPropertyNames(customization).forEach(key => { - this.uiState.set(key, customization[key]); + this.uiState.set('vis', visCustomizations); + getKeys(visCustomizations).forEach(key => { + this.uiState.set(key, visCustomizations[key]); }); - this.customization = customization; this.uiState.on('change', this.uiStateChangeHandler); + } else { + this.uiState.clearAllKeys(); } } - public onContainerStateChanged(containerState: ContainerState) { - this.transferCustomizationsToUiState(containerState); + public handleChanges() { + this.transferCustomizationsToUiState(); const updatedParams: VisualizeUpdateParams = {}; // Check if timerange has changed - if (containerState.timeRange !== this.timeRange) { - updatedParams.timeRange = containerState.timeRange; - this.timeRange = containerState.timeRange; + if (this.input.timeRange !== this.timeRange) { + this.timeRange = _.cloneDeep(this.input.timeRange); + updatedParams.timeRange = this.timeRange; } // Check if filters has changed - if (containerState.filters !== this.filters) { - updatedParams.filters = containerState.filters; - this.filters = containerState.filters; + if (this.input.filters !== this.filters) { + updatedParams.filters = this.input.filters; + this.filters = this.input.filters; } // Check if query has changed - if (containerState.query !== this.query) { - updatedParams.query = containerState.query; - this.query = containerState.query; + if (this.input.query !== this.query) { + updatedParams.query = this.input.query; + this.query = this.input.query; } - const derivedPanelTitle = this.getPanelTitle(containerState); - if (this.panelTitle !== derivedPanelTitle) { + if (this.output.title !== this.title) { + this.title = this.output.title; updatedParams.dataAttrs = { - title: derivedPanelTitle, + title: this.title || '', }; - this.panelTitle = derivedPanelTitle; } if (this.handler && !_.isEmpty(updatedParams)) { @@ -159,17 +191,16 @@ export class VisualizeEmbeddable extends Embeddable { * @param {Element} domNode * @param {ContainerState} containerState */ - public render(domNode: HTMLElement, containerState: ContainerState) { - this.panelTitle = this.getPanelTitle(containerState); - this.timeRange = containerState.timeRange; - this.query = containerState.query; - this.filters = containerState.filters; + public render(domNode: HTMLElement) { + this.timeRange = _.cloneDeep(this.input.timeRange); + this.query = this.input.query; + this.filters = this.input.filters; - this.transferCustomizationsToUiState(containerState); + this.transferCustomizationsToUiState(); const dataAttrs: { [key: string]: string } = { 'shared-item': '', - title: this.panelTitle, + title: this.output.title || '', }; if (this.savedVisualization.description) { dataAttrs.description = this.savedVisualization.description; @@ -179,10 +210,10 @@ export class VisualizeEmbeddable extends Embeddable { uiState: this.uiState, // Append visualization to container instead of replacing its content append: true, - timeRange: containerState.timeRange, - query: containerState.query, - filters: containerState.filters, - cssClass: `embPanel__content embPanel__content--fullWidth`, + timeRange: _.cloneDeep(this.input.timeRange), + query: this.input.query, + filters: this.input.filters, + cssClass: `panel-content panel-content--fullWidth`, dataAttrs, }; @@ -194,6 +225,10 @@ export class VisualizeEmbeddable extends Embeddable { } public destroy() { + super.destroy(); + if (this.subscription) { + this.subscription.unsubscribe(); + } this.uiState.off('change', this.uiStateChangeHandler); this.savedVisualization.destroy(); if (this.handler) { @@ -208,23 +243,9 @@ export class VisualizeEmbeddable extends Embeddable { } } - /** - * Retrieve the panel title for this panel from the container state. - * This will either return the overwritten panel title or the visualization title. - */ - private getPanelTitle(containerState: ContainerState) { - let derivedPanelTitle = ''; - if (!containerState.hidePanelTitles) { - derivedPanelTitle = - containerState.customTitle !== undefined - ? containerState.customTitle - : this.savedVisualization.title; - } - return derivedPanelTitle; - } - private uiStateChangeHandler = () => { - this.customization = this.uiState.toJSON(); - this.onEmbeddableStateChanged(this.getEmbeddableState()); + this.updateInput({ + ...this.uiState.toJSON(), + }); }; } diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.ts deleted file mode 100644 index d5b09dff2a65f..0000000000000 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.ts +++ /dev/null @@ -1,122 +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 chrome from 'ui/chrome'; -import { EmbeddableFactory } from 'ui/embeddable'; -import { getVisualizeLoader } from 'ui/visualize/loader'; - -import { Legacy } from 'kibana'; -import { capabilities } from 'ui/capabilities'; -import { - EmbeddableInstanceConfiguration, - OnEmbeddableStateChanged, -} from 'ui/embeddable/embeddable_factory'; -import { VisTypesRegistry } from 'ui/registry/vis_types'; -import { SavedObjectAttributes } from 'src/core/server'; -import { VisualizeEmbeddable } from './visualize_embeddable'; -import { SavedVisualizations } from '../types'; -import { DisabledLabEmbeddable } from './disabled_lab_embeddable'; -import { getIndexPattern } from './get_index_pattern'; - -export interface VisualizationAttributes extends SavedObjectAttributes { - visState: string; -} - -export class VisualizeEmbeddableFactory extends EmbeddableFactory { - private savedVisualizations: SavedVisualizations; - private config: Legacy.KibanaConfig; - - constructor( - savedVisualizations: SavedVisualizations, - config: Legacy.KibanaConfig, - visTypes: VisTypesRegistry - ) { - super({ - name: 'visualization', - savedObjectMetaData: { - name: i18n.translate('kbn.visualize.savedObjectName', { defaultMessage: 'Visualization' }), - type: 'visualization', - getIconForSavedObject: savedObject => { - return ( - visTypes.byName[JSON.parse(savedObject.attributes.visState).type].icon || 'visualizeApp' - ); - }, - getTooltipForSavedObject: savedObject => { - return `${savedObject.attributes.title} (${visTypes.byName[JSON.parse(savedObject.attributes.visState).type].title})`; - }, - showSavedObject: savedObject => { - const typeName: string = JSON.parse(savedObject.attributes.visState).type; - const visType = visTypes.byName[typeName]; - if (!visType) { - return false; - } - - if (chrome.getUiSettingsClient().get('visualize:enableLabs')) { - return true; - } - - return visType.stage !== 'experimental'; - }, - }, - }); - this.config = config; - this.savedVisualizations = savedVisualizations; - } - - public getEditPath(panelId: string) { - return this.savedVisualizations.urlFor(panelId); - } - - /** - * - * @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 {function} onEmbeddableStateChanged - * @return {Promise.<{ metadata, onContainerStateChanged, render, destroy }>} - */ - public async create( - panelMetadata: EmbeddableInstanceConfiguration, - onEmbeddableStateChanged: OnEmbeddableStateChanged - ) { - const visId = panelMetadata.id; - const editUrl = this.getEditPath(visId); - const editable: boolean = capabilities.get().visualize.save as boolean; - - const loader = await getVisualizeLoader(); - const savedObject = await this.savedVisualizations.get(visId); - const isLabsEnabled = this.config.get('visualize:enableLabs'); - - if (!isLabsEnabled && savedObject.vis.type.stage === 'experimental') { - return new DisabledLabEmbeddable(savedObject.title); - } - - const indexPattern = await getIndexPattern(savedObject); - const indexPatterns = indexPattern ? [indexPattern] : []; - return new VisualizeEmbeddable({ - onEmbeddableStateChanged, - savedVisualization: savedObject, - editUrl, - editable, - loader, - indexPatterns, - }); - } -} diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.tsx b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.tsx new file mode 100644 index 0000000000000..809f5ae84c482 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.tsx @@ -0,0 +1,191 @@ +/* + * 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/registry/field_formats'; +import 'uiExports/autocompleteProviders'; +import 'uiExports/contextMenuActions'; +import 'uiExports/devTools'; +import 'uiExports/docViews'; +import 'uiExports/embeddableFactories'; +import 'uiExports/embeddableActions'; +import 'uiExports/fieldFormatEditors'; +import 'uiExports/fieldFormats'; +import 'uiExports/home'; +import 'uiExports/indexManagement'; +import 'uiExports/inspectorViews'; +import 'uiExports/savedObjectTypes'; +import 'uiExports/search'; +import 'uiExports/shareContextMenuExtensions'; +import 'uiExports/visEditorTypes'; +import 'uiExports/visRequestHandlers'; +import 'uiExports/visResponseHandlers'; +import 'uiExports/visTypes'; +import 'uiExports/visualize'; + +import { i18n } from '@kbn/i18n'; + +import { capabilities } from 'ui/capabilities'; +// @ts-ignore +import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; +// @ts-ignore +import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; + +import { + embeddableFactories, + EmbeddableFactory, + ErrorEmbeddable, + Container, + EmbeddableOutput, +} from 'plugins/embeddable_api'; +import chrome from 'ui/chrome'; +import { getVisualizeLoader } from 'ui/visualize/loader'; + +import { Legacy } from 'kibana'; +import { VisTypesRegistry, VisTypesRegistryProvider } from 'ui/registry/vis_types'; + +import { IPrivate } from 'ui/private'; +import { SavedObjectAttributes } from 'kibana/server'; +import { showNewVisModal } from '../wizard'; +import { SavedVisualizations } from '../types'; +import { DisabledLabEmbeddable } from './disabled_lab_embeddable'; +import { getIndexPattern } from './get_index_pattern'; +import { VisualizeEmbeddable, VisualizeInput, VisualizeOutput } from './visualize_embeddable'; +import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; + +interface VisualizationAttributes extends SavedObjectAttributes { + visState: string; +} + +export class VisualizeEmbeddableFactory extends EmbeddableFactory< + VisualizeInput, + VisualizeOutput | EmbeddableOutput, + VisualizeEmbeddable | DisabledLabEmbeddable, + VisualizationAttributes +> { + private visTypes?: VisTypesRegistry; + public readonly type = VISUALIZE_EMBEDDABLE_TYPE; + + constructor() { + super({ + savedObjectMetaData: { + name: i18n.translate('kbn.visualize.savedObjectName', { defaultMessage: 'Visualization' }), + type: 'visualization', + getIconForSavedObject: savedObject => { + if (!this.visTypes) { + return 'visualizeApp'; + } + return ( + this.visTypes.byName[JSON.parse(savedObject.attributes.visState).type].icon || + 'visualizeApp' + ); + }, + getTooltipForSavedObject: savedObject => { + if (!this.visTypes) { + return ''; + } + return `${savedObject.attributes.title} (${this.visTypes.byName[JSON.parse(savedObject.attributes.visState).type].title})`; + }, + showSavedObject: savedObject => { + if (!this.visTypes) { + return false; + } + const typeName: string = JSON.parse(savedObject.attributes.visState).type; + const visType = this.visTypes.byName[typeName]; + if (!visType) { + return false; + } + if (chrome.getUiSettingsClient().get('visualize:enableLabs')) { + return true; + } + return visType.stage !== 'experimental'; + }, + }, + }); + this.initializeVisTypes(); + } + + public isEditable() { + return capabilities.get().visualize.save as boolean; + } + + public getDisplayName() { + return i18n.translate('kbn.embeddable.visualizations.displayName', { + defaultMessage: 'visualization', + }); + } + + public async initializeVisTypes() { + const $injector = await chrome.dangerouslyGetActiveInjector(); + const Private = $injector.get('Private'); + this.visTypes = Private(VisTypesRegistryProvider); + } + + public async createFromSavedObject( + savedObjectId: string, + input: Partial & { id: string }, + parent?: Container + ): Promise { + const $injector = await chrome.dangerouslyGetActiveInjector(); + const config = $injector.get('config'); + const savedVisualizations = $injector.get('savedVisualizations'); + + try { + const visId = savedObjectId; + + const editUrl = chrome.addBasePath(`/app/kibana${savedVisualizations.urlFor(visId)}`); + const loader = await getVisualizeLoader(); + const savedObject = await savedVisualizations.get(visId); + const isLabsEnabled = config.get('visualize:enableLabs'); + + if (!isLabsEnabled && savedObject.vis.type.stage === 'experimental') { + return new DisabledLabEmbeddable(savedObject.title, input); + } + + const indexPattern = await getIndexPattern(savedObject); + const indexPatterns = indexPattern ? [indexPattern] : []; + return new VisualizeEmbeddable( + { + savedVisualization: savedObject, + loader, + indexPatterns, + editUrl, + editable: this.isEditable(), + }, + input, + parent + ); + } catch (e) { + console.error(e); // eslint-disable-line no-console + return new ErrorEmbeddable(e, input, parent); + } + } + + public async create() { + // TODO: This is a bit of a hack to preserve the original functionality. Ideally we will clean this up + // to allow for in place creation of visualizations without having to navigate away to a new URL. + if (this.visTypes) { + showNewVisModal(this.visTypes, { + editorParams: ['addToDashboard'], + }); + } + return undefined; + } +} + +embeddableFactories.set(VISUALIZE_EMBEDDABLE_TYPE, new VisualizeEmbeddableFactory()); diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory_provider.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory_provider.ts deleted file mode 100644 index dcdf58a52d918..0000000000000 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory_provider.ts +++ /dev/null @@ -1,41 +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 { Legacy } from 'kibana'; -import { EmbeddableFactoriesRegistryProvider } from 'ui/embeddable/embeddable_factories_registry'; -import { IPrivate } from 'ui/private'; -import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; -import { SavedVisualizations } from '../types'; -import { VisualizeEmbeddableFactory } from './visualize_embeddable_factory'; - -export function visualizeEmbeddableFactoryProvider(Private: IPrivate) { - const VisualizeEmbeddableFactoryProvider = ( - savedVisualizations: SavedVisualizations, - config: Legacy.KibanaConfig - ) => { - return new VisualizeEmbeddableFactory( - savedVisualizations, - config, - Private(VisTypesRegistryProvider) - ); - }; - return Private(VisualizeEmbeddableFactoryProvider); -} - -EmbeddableFactoriesRegistryProvider.register(visualizeEmbeddableFactoryProvider); diff --git a/src/legacy/ui/public/embeddable/context_menu_actions/build_eui_context_menu_panels.ts b/src/legacy/ui/public/embeddable/context_menu_actions/build_eui_context_menu_panels.ts deleted file mode 100644 index 87c3d13c8de93..0000000000000 --- a/src/legacy/ui/public/embeddable/context_menu_actions/build_eui_context_menu_panels.ts +++ /dev/null @@ -1,164 +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, EuiContextMenuPanelItemDescriptor } from '@elastic/eui'; -import _ from 'lodash'; -import { ContainerState, ContextMenuAction, ContextMenuPanel, Embeddable } from 'ui/embeddable'; - -/** - * Loops through allActions and extracts those that belong on the given contextMenuPanelId - * @param {string} contextMenuPanelId - * @param {Array.} allActions - */ -function getActionsForPanel(contextMenuPanelId: string, allActions: ContextMenuAction[]) { - return allActions.filter(action => action.parentPanelId === contextMenuPanelId); -} - -/** - * @param {String} contextMenuPanelId - * @param {Array.} actions - * @param {Embeddable} embeddable - * @param {ContainerState} containerState - * @return {{ - * Array. items - panel actions converted into the items expected to be on an - * EuiContextMenuPanel, - * Array. childPanels - extracted child panels, if any actions also open a panel. They - * need to be moved to the top level for EUI. - * }} - */ -function buildEuiContextMenuPanelItemsAndChildPanels({ - contextMenuPanelId, - actions, - embeddable, - containerState, -}: { - contextMenuPanelId: string; - actions: ContextMenuAction[]; - embeddable?: Embeddable; - containerState: ContainerState; -}) { - const items: EuiContextMenuPanelItemDescriptor[] = []; - const childPanels: EuiContextMenuPanelDescriptor[] = []; - const actionsForPanel = getActionsForPanel(contextMenuPanelId, actions); - actionsForPanel.forEach(action => { - const isVisible = action.isVisible({ embeddable, containerState }); - if (!isVisible) { - return; - } - - if (action.childContextMenuPanel) { - childPanels.push( - ...buildEuiContextMenuPanels({ - contextMenuPanel: action.childContextMenuPanel, - actions, - embeddable, - containerState, - }) - ); - } - - items.push( - convertPanelActionToContextMenuItem({ - action, - containerState, - embeddable, - }) - ); - }); - - return { items, childPanels }; -} - -/** - * Transforms a DashboardContextMenuPanel to the shape EuiContextMenuPanel expects, inserting any registered pluggable - * panel actions. - * @param {ContextMenuPanel} contextMenuPanel - * @param {Array.} actions to build the context menu with - * @param {Embeddable} embeddable - * @param {ContainerState} containerState - * @return {EuiContextMenuPanelDescriptor[]} An array of context menu panels to be used in the eui react component. - */ -export function buildEuiContextMenuPanels({ - contextMenuPanel, - actions, - embeddable, - containerState, -}: { - contextMenuPanel: ContextMenuPanel; - actions: ContextMenuAction[]; - embeddable?: Embeddable; - containerState: ContainerState; -}): EuiContextMenuPanelDescriptor[] { - const euiContextMenuPanel: EuiContextMenuPanelDescriptor = { - id: contextMenuPanel.id, - title: contextMenuPanel.title, - items: [], - content: contextMenuPanel.getContent({ embeddable, containerState }), - }; - const contextMenuPanels = [euiContextMenuPanel]; - - const { items, childPanels } = buildEuiContextMenuPanelItemsAndChildPanels({ - contextMenuPanelId: contextMenuPanel.id, - actions, - embeddable, - containerState, - }); - - euiContextMenuPanel.items = items; - return contextMenuPanels.concat(childPanels); -} - -/** - * - * @param {ContextMenuAction} action - * @param {ContainerState} containerState - * @param {Embeddable} embeddable - * @return {EuiContextMenuPanelItemDescriptor} - */ -function convertPanelActionToContextMenuItem({ - action, - containerState, - embeddable, -}: { - action: ContextMenuAction; - containerState: ContainerState; - embeddable?: Embeddable; -}): EuiContextMenuPanelItemDescriptor { - const menuPanelItem: EuiContextMenuPanelItemDescriptor = { - name: action.getDisplayName({ embeddable, containerState }), - icon: action.icon, - panel: _.get(action, 'childContextMenuPanel.id'), - disabled: action.isDisabled({ embeddable, containerState }), - 'data-test-subj': `dashboardPanelAction-${action.id}`, - }; - - if (action.onClick) { - menuPanelItem.onClick = () => { - if (action.onClick) { - action.onClick({ embeddable, containerState }); - } - }; - } - - if (action.getHref) { - menuPanelItem.href = action.getHref({ embeddable, containerState }); - } - - return menuPanelItem; -} diff --git a/src/legacy/ui/public/embeddable/context_menu_actions/context_menu_action.ts b/src/legacy/ui/public/embeddable/context_menu_actions/context_menu_action.ts deleted file mode 100644 index d1c0af37e6bbf..0000000000000 --- a/src/legacy/ui/public/embeddable/context_menu_actions/context_menu_action.ts +++ /dev/null @@ -1,172 +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 { EuiContextMenuItemIcon } from '@elastic/eui'; -import { ContextMenuPanel } from './context_menu_panel'; -import { PanelActionAPI } from './types'; - -interface ContextMenuActionOptions { - /** - * An optional child context menu to display when the action is clicked. - */ - childContextMenuPanel?: ContextMenuPanel; - - /** - * Whether this action should be disabled based on the parameters given. - * @param {PanelActionAPI} panelActionAPI - * @return {boolean} - */ - isDisabled?: (actionAPI: PanelActionAPI) => boolean; - - /** - * Whether this action should be visible based on the parameters given. - * @param {PanelActionAPI} panelActionAPI - * @return {boolean} - */ - isVisible?: (panelActionAPI: PanelActionAPI) => boolean; - - /** - * Determines which ContextMenuPanel this action is displayed on. - */ - parentPanelId?: string; - - /** - * Optional icon to display to the left of the action. - */ - icon?: EuiContextMenuItemIcon; - - /** - * Return display name of the action in the menu - */ - getDisplayName: (actionAPI: PanelActionAPI) => string; -} - -interface ContextMenuButtonOptions extends ContextMenuActionOptions { - /** - * An optional action to take when the action is clicked on. Either this or childContextMenuPanel should be - * given. - */ - onClick?: (actionAPI: PanelActionAPI) => void; -} - -interface ContextMenuLinkOptions extends ContextMenuActionOptions { - /** - * An optional href to use as navigation when the action is clicked on. - */ - getHref?: (actionAPI: PanelActionAPI) => string | undefined; -} - -interface ContextMenuActionsConfig { - id: string; - - /** - * Determines which ContextMenuPanel this action is displayed on. - */ - parentPanelId: string; -} - -export class ContextMenuAction { - public readonly id: string; - - /** - * Optional icon to display to the left of the action. - */ - public readonly icon?: EuiContextMenuItemIcon; - - /** - * Optional child context menu to open when the action is clicked. - */ - public readonly childContextMenuPanel?: ContextMenuPanel; - - /** - * Determines which ContextMenuPanel this action is displayed on. - */ - public readonly parentPanelId: string; - - /** - * @param {PanelActionAPI} panelActionAPI - */ - public readonly onClick?: (panelActionAPI: PanelActionAPI) => void; - - /** - * @param {PanelActionAPI} panelActionAPI - */ - public readonly getHref?: (panelActionAPI: PanelActionAPI) => string | undefined; - - /** - * @param {PanelActionAPI} panelActionAPI - */ - public readonly getDisplayName: (panelActionAPI: PanelActionAPI) => string; - - /** - * - * @param {string} config.id - * @param {string} config.parentPanelId - set if this action belongs on a nested child panel - * @param {function} options.onClick - * @param {ContextMenuPanel} options.childContextMenuPanel - optional child panel to open when clicked. - * @param {function} options.isDisabled - optionally set a custom disabled function - * @param {function} options.isVisible - optionally set a custom isVisible function - * @param {function} options.getHref - * @param {function} options.getDisplayName - * @param {Element} options.icon - */ - public constructor( - config: ContextMenuActionsConfig, - options: ContextMenuButtonOptions | ContextMenuLinkOptions - ) { - this.id = config.id; - this.parentPanelId = config.parentPanelId; - - this.icon = options.icon; - this.childContextMenuPanel = options.childContextMenuPanel; - this.getDisplayName = options.getDisplayName; - - if ('onClick' in options) { - this.onClick = options.onClick; - } - - if (options.isDisabled) { - this.isDisabled = options.isDisabled; - } - - if (options.isVisible) { - this.isVisible = options.isVisible; - } - - if ('getHref' in options) { - this.getHref = options.getHref; - } - } - - /** - * Whether this action should be visible based on the parameters given. Defaults to always visible. - * @param {PanelActionAPI} panelActionAPI - * @return {boolean} - */ - public isVisible(panelActionAPI: PanelActionAPI): boolean { - return true; - } - - /** - * Whether this action should be disabled based on the parameters given. Defaults to always enabled. - * @param {PanelActionAPI} panelActionAPI - */ - public isDisabled(panelActionAPI: PanelActionAPI): boolean { - return false; - } -} diff --git a/src/legacy/ui/public/embeddable/context_menu_actions/context_menu_actions_registry.ts b/src/legacy/ui/public/embeddable/context_menu_actions/context_menu_actions_registry.ts deleted file mode 100644 index fe9782e2000eb..0000000000000 --- a/src/legacy/ui/public/embeddable/context_menu_actions/context_menu_actions_registry.ts +++ /dev/null @@ -1,26 +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. - */ - -// @ts-ignore: implicit any for JS file -import { uiRegistry } from 'ui/registry/_registry'; - -export const ContextMenuActionsRegistryProvider = uiRegistry({ - index: ['name'], - name: 'ContextMenuActions', -}); diff --git a/src/legacy/ui/public/embeddable/context_menu_actions/context_menu_panel.ts b/src/legacy/ui/public/embeddable/context_menu_actions/context_menu_panel.ts deleted file mode 100644 index 900125e9a5883..0000000000000 --- a/src/legacy/ui/public/embeddable/context_menu_actions/context_menu_panel.ts +++ /dev/null @@ -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 { ReactElement } from 'react'; -import { PanelActionAPI } from './types'; - -interface ContextMenuPanelOptions { - getContent?: (panelActionAPI: PanelActionAPI) => ReactElement | HTMLElement | undefined; -} - -interface ContextMenuPanelConfig { - id: string; - title: string; -} - -export class ContextMenuPanel { - public readonly id: string; - public readonly title: string; - - constructor(config: ContextMenuPanelConfig, options: ContextMenuPanelOptions = {}) { - this.id = config.id; - this.title = config.title; - - if (options.getContent) { - this.getContent = options.getContent; - } - } - - /** - * Optional, could be composed of actions instead of content. - */ - public getContent(panelActionAPI: PanelActionAPI): ReactElement | HTMLElement | undefined { - return; - } -} diff --git a/src/legacy/ui/public/embeddable/context_menu_actions/index.ts b/src/legacy/ui/public/embeddable/context_menu_actions/index.ts deleted file mode 100644 index d43f1d16aefd1..0000000000000 --- a/src/legacy/ui/public/embeddable/context_menu_actions/index.ts +++ /dev/null @@ -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 { ContextMenuPanel } from './context_menu_panel'; -export { ContextMenuAction } from './context_menu_action'; -export { ContextMenuActionsRegistryProvider } from './context_menu_actions_registry'; -export { buildEuiContextMenuPanels } from './build_eui_context_menu_panels'; -export { PanelActionAPI } from './types'; diff --git a/src/legacy/ui/public/embeddable/context_menu_actions/types.ts b/src/legacy/ui/public/embeddable/context_menu_actions/types.ts deleted file mode 100644 index a35c1f5b4480e..0000000000000 --- a/src/legacy/ui/public/embeddable/context_menu_actions/types.ts +++ /dev/null @@ -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 { ContainerState, Embeddable } from 'ui/embeddable'; - -/** - * Exposes information about the current state of the panel and the embeddable rendered internally. - */ -export interface PanelActionAPI { - /** - * The embeddable that resides inside this action. It's possible it's undefined if the embeddable has not been returned from - * the EmbeddableFactory yet. - */ - embeddable?: Embeddable; - - /** - * Information about the current state of the panel and dashboard. - */ - containerState: ContainerState; -} diff --git a/src/legacy/ui/public/embeddable/embeddable.ts b/src/legacy/ui/public/embeddable/embeddable.ts deleted file mode 100644 index 55ef4f3a6b15a..0000000000000 --- a/src/legacy/ui/public/embeddable/embeddable.ts +++ /dev/null @@ -1,85 +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 { Adapters } from 'ui/inspector'; -import { StaticIndexPattern } from 'ui/index_patterns'; -import { ContainerState } from './types'; - -export interface EmbeddableMetadata { - /** - * Should specify any index pattern the embeddable uses. This will be used by the container to list out - * available fields to filter on. - */ - indexPatterns?: StaticIndexPattern[]; - - /** - * The title, or name, of the embeddable. - */ - title?: string; - - /** - * A url to direct the user for managing the embeddable instance. We may want to eventually make this optional - * for non-instanced panels that can only be created and deleted but not edited. We also wish to eventually support - * in-place editing on the dashboard itself, so another option could be to supply an element, or fly out panel, to - * offer for editing directly on the dashboard. - */ - editUrl?: string; - - editLabel?: string; - - /** - * A flag indicating if this embeddable can be edited. - */ - editable?: boolean; -} - -export abstract class Embeddable { - public readonly metadata: EmbeddableMetadata = {}; - - // TODO: Make title and editUrl required and move out of options parameter. - constructor(metadata: EmbeddableMetadata = {}) { - this.metadata = metadata || {}; - } - - public onContainerStateChanged(containerState: ContainerState): void { - return; - } - - /** - * Embeddable should render itself at the given domNode. - */ - public abstract render(domNode: HTMLElement, containerState: ContainerState): void; - - /** - * An embeddable can return inspector adapters if it want the inspector to be - * available via the context menu of that panel. - * @return Inspector adapters that will be used to open an inspector for. - */ - public getInspectorAdapters(): Adapters | undefined { - return undefined; - } - - public destroy(): void { - return; - } - - public reload(): void { - return; - } -} diff --git a/src/legacy/ui/public/embeddable/embeddable_factories_registry.ts b/src/legacy/ui/public/embeddable/embeddable_factories_registry.ts deleted file mode 100644 index 8b227081ec6f9..0000000000000 --- a/src/legacy/ui/public/embeddable/embeddable_factories_registry.ts +++ /dev/null @@ -1,29 +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. - */ - -// @ts-ignore: implicit any for JS file -import { uiRegistry } from '../registry/_registry'; - -/** - * Registry of functions (EmbeddableFactoryProviders) which return an EmbeddableFactory. - */ -export const EmbeddableFactoriesRegistryProvider = uiRegistry({ - index: ['name'], - name: 'embeddableFactories', -}); diff --git a/src/legacy/ui/public/embeddable/embeddable_factory.ts b/src/legacy/ui/public/embeddable/embeddable_factory.ts deleted file mode 100644 index 782b8053498a9..0000000000000 --- a/src/legacy/ui/public/embeddable/embeddable_factory.ts +++ /dev/null @@ -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. - */ - -import { SavedObjectAttributes } from 'src/core/server'; -import { SavedObjectMetaData } from '../saved_objects/components/saved_object_finder'; -import { Embeddable } from './embeddable'; -import { EmbeddableState } from './types'; -export interface EmbeddableInstanceConfiguration { - id: string; -} - -export type OnEmbeddableStateChanged = (embeddableStateChanges: EmbeddableState) => void; - -/** - * The EmbeddableFactory creates and initializes an embeddable instance - */ -export abstract class EmbeddableFactory { - public readonly name: string; - public readonly savedObjectMetaData?: SavedObjectMetaData; - - /** - * - * @param name - a unique identified for this factory, which will be used to map an embeddable spec to - * a factory that can generate an instance of it. - */ - constructor({ - name, - savedObjectMetaData, - }: { - name: string; - savedObjectMetaData?: SavedObjectMetaData; - }) { - this.name = name; - this.savedObjectMetaData = savedObjectMetaData; - } - - /** - * - * @param {{ id: string }} containerMetadata. 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} onEmbeddableStateChanged - embeddable should call this function with updated - * state whenever something changes that the dashboard should know about. - * @return {Promise.} - */ - public abstract create( - containerMetadata: { id: string }, - onEmbeddableStateChanged: OnEmbeddableStateChanged - ): Promise; -} diff --git a/src/legacy/ui/public/embeddable/index.ts b/src/legacy/ui/public/embeddable/index.ts deleted file mode 100644 index a4d3b08bb1093..0000000000000 --- a/src/legacy/ui/public/embeddable/index.ts +++ /dev/null @@ -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 { EmbeddableFactory, OnEmbeddableStateChanged } from './embeddable_factory'; -export * from './embeddable'; -export * from './context_menu_actions'; -export { EmbeddableFactoriesRegistryProvider } from './embeddable_factories_registry'; -export { ContainerState, EmbeddableState } from './types'; diff --git a/src/legacy/ui/public/embeddable/types.ts b/src/legacy/ui/public/embeddable/types.ts deleted file mode 100644 index e682b6dda1822..0000000000000 --- a/src/legacy/ui/public/embeddable/types.ts +++ /dev/null @@ -1,73 +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 { 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'; - -export interface EmbeddableCustomization { - [key: string]: unknown; -} - -export interface ContainerState { - // 'view' or 'edit'. Should probably be an enum but I'm undecided where to define it, here or in dashboard code. - viewMode: string; - - timeRange: TimeRange; - - filters: Filter[]; - - refreshConfig: RefreshInterval; - - query: Query; - - // The shape will be up to the embeddable type. - embeddableCustomization?: EmbeddableCustomization; - - /** - * Whether or not panel titles are hidden. It is not the embeddable's responsibility to hide the title (the container - * handles that). This information is currently only used to determine the title for reporting (data-sharing-title - * attribute). If we move that out of the embeddables and push it to the container (as we probably should), then - * we shouldn't need to expose this information. - */ - hidePanelTitles: boolean; - - /** - * Is the current panel in expanded mode - */ - isPanelExpanded: boolean; - - /** - * A way to override the underlying embeddable title and supply a title at the panel level. - */ - customTitle?: string; -} - -export interface EmbeddableState { - /** - * Any customization data that should be stored at the panel level. For - * example, pie slice colors, or custom per panel sort order or columns. - */ - customization?: { [key: string]: unknown }; - /** - * A possible filter the embeddable wishes dashboard to apply. - */ - stagedFilter?: object; -} diff --git a/src/legacy/ui/public/saved_objects/saved_object.d.ts b/src/legacy/ui/public/saved_objects/saved_object.d.ts index 762cb9b6ee9c7..dc6496eacfcbe 100644 --- a/src/legacy/ui/public/saved_objects/saved_object.d.ts +++ b/src/legacy/ui/public/saved_objects/saved_object.d.ts @@ -26,4 +26,5 @@ export interface SaveOptions { export interface SavedObject { save: (saveOptions: SaveOptions) => Promise; copyOnSave: boolean; + id?: string; } diff --git a/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.test.ts b/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.test.ts index 5973ae60c14d0..b56a63c4c0928 100644 --- a/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.test.ts +++ b/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.test.ts @@ -68,6 +68,8 @@ describe('EmbeddedVisualizeHandler', () => { title: 'My Vis', searchSource: undefined, destroy: () => ({}), + copyOnSave: false, + save: () => Promise.resolve('123'), }, { autoFetch: true, @@ -98,6 +100,8 @@ describe('EmbeddedVisualizeHandler', () => { title: 'My Vis', searchSource: undefined, destroy: () => ({}), + copyOnSave: false, + save: () => Promise.resolve('123'), }, { autoFetch: false, diff --git a/src/legacy/ui/public/visualize/loader/types.ts b/src/legacy/ui/public/visualize/loader/types.ts index 4f34f6219621f..77e86ae063082 100644 --- a/src/legacy/ui/public/visualize/loader/types.ts +++ b/src/legacy/ui/public/visualize/loader/types.ts @@ -20,12 +20,14 @@ import { Filter } from '@kbn/es-query'; import { TimeRange } from 'ui/timefilter/time_history'; import { Query } from 'src/legacy/core_plugins/data/public'; +import { SavedObject } from 'ui/saved_objects/saved_object'; + import { SearchSource } from '../../courier'; import { PersistedState } from '../../persisted_state'; import { AppState } from '../../state_management/app_state'; import { Vis } from '../../vis'; -export interface VisSavedObject { +export interface VisSavedObject extends SavedObject { vis: Vis; description?: string; searchSource: SearchSource; diff --git a/src/legacy/ui/ui_exports/ui_export_defaults.js b/src/legacy/ui/ui_exports/ui_export_defaults.js index 01e998c945bfd..ffe43fa157808 100644 --- a/src/legacy/ui/ui_exports/ui_export_defaults.js +++ b/src/legacy/ui/ui_exports/ui_export_defaults.js @@ -63,8 +63,8 @@ export const UI_EXPORT_DEFAULTS = { 'ui/vis/editors/default/default', ], embeddableFactories: [ - 'plugins/kibana/visualize/embeddable/visualize_embeddable_factory_provider', - 'plugins/kibana/discover/embeddable/search_embeddable_factory_provider', + 'plugins/kibana/visualize/embeddable/visualize_embeddable_factory', + 'plugins/kibana/discover/embeddable/search_embeddable_factory', ], search: [ 'ui/courier/search_strategy/default_search_strategy', diff --git a/test/functional/apps/dashboard/create_and_add_embeddables.js b/test/functional/apps/dashboard/create_and_add_embeddables.js index dfc472c7af050..78486d24b4fca 100644 --- a/test/functional/apps/dashboard/create_and_add_embeddables.js +++ b/test/functional/apps/dashboard/create_and_add_embeddables.js @@ -39,7 +39,7 @@ export default function ({ getService, getPageObjects }) { const originalPanelCount = await PageObjects.dashboard.getPanelCount(); await PageObjects.dashboard.switchToEditMode(); await dashboardAddPanel.ensureAddPanelIsShowing(); - await dashboardAddPanel.clickAddNewEmbeddableLink(); + await dashboardAddPanel.clickAddNewEmbeddableLink('visualization'); await PageObjects.visualize.clickAreaChart(); await PageObjects.visualize.clickNewSearch(); await PageObjects.visualize.saveVisualizationExpectSuccess('visualization from add new link'); diff --git a/test/functional/apps/dashboard/dashboard_filtering.js b/test/functional/apps/dashboard/dashboard_filtering.js index e586443e4909d..716bdff4e8ca9 100644 --- a/test/functional/apps/dashboard/dashboard_filtering.js +++ b/test/functional/apps/dashboard/dashboard_filtering.js @@ -48,6 +48,7 @@ export default function ({ getService, getPageObjects }) { await dashboardAddPanel.addEverySavedSearch('"Filter Bytes Test"'); await dashboardAddPanel.closeAddPanel(); + await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.dashboard.waitForRenderComplete(); await filterBar.addFilter('bytes', 'is', '12345678'); diff --git a/test/functional/apps/dashboard/dashboard_state.js b/test/functional/apps/dashboard/dashboard_state.js index ca50e81d924e5..56c866d0aaccc 100644 --- a/test/functional/apps/dashboard/dashboard_state.js +++ b/test/functional/apps/dashboard/dashboard_state.js @@ -22,7 +22,7 @@ import expect from '@kbn/expect'; import { PIE_CHART_VIS_NAME, AREA_CHART_VIS_NAME } from '../../page_objects/dashboard_page'; import { DEFAULT_PANEL_WIDTH, -} from '../../../../src/legacy/core_plugins/kibana/public/dashboard/dashboard_constants'; +} from '../../../../src/legacy/core_plugins/dashboard_embeddable_container/public/embeddable/dashboard_constants'; export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['dashboard', 'visualize', 'header', 'discover']); diff --git a/test/functional/apps/dashboard/data_shared_attributes.js b/test/functional/apps/dashboard/data_shared_attributes.js index cf94b98c3d48d..12f85f1b5e46a 100644 --- a/test/functional/apps/dashboard/data_shared_attributes.js +++ b/test/functional/apps/dashboard/data_shared_attributes.js @@ -69,7 +69,7 @@ export default function ({ getService, getPageObjects }) { }); it('data-shared-item title is cleared with an empty panel title string', async () => { - await dashboardPanelActions.setCustomPanelTitle(''); + await dashboardPanelActions.toggleHidePanelTitle(); await retry.try(async () => { const sharedData = await PageObjects.dashboard.getPanelSharedItemData(); const foundSharedItemTitle = !!sharedData.find(item => { @@ -77,6 +77,7 @@ export default function ({ getService, getPageObjects }) { }); expect(foundSharedItemTitle).to.be(true); }); + await dashboardPanelActions.toggleHidePanelTitle(); }); it('data-shared-item title can be reset', async () => { diff --git a/test/functional/apps/dashboard/view_edit.js b/test/functional/apps/dashboard/view_edit.js index 2cc2cda0c46d5..f19066c9d0939 100644 --- a/test/functional/apps/dashboard/view_edit.js +++ b/test/functional/apps/dashboard/view_edit.js @@ -120,7 +120,7 @@ export default function ({ getService, getPageObjects }) { const originalPanelCount = await PageObjects.dashboard.getPanelCount(); await dashboardAddPanel.ensureAddPanelIsShowing(); - await dashboardAddPanel.clickAddNewEmbeddableLink(); + await dashboardAddPanel.clickAddNewEmbeddableLink('visualization'); await PageObjects.visualize.clickAreaChart(); await PageObjects.visualize.clickNewSearch(); await PageObjects.visualize.saveVisualizationExpectSuccess('new viz panel'); diff --git a/test/functional/page_objects/dashboard_page.js b/test/functional/page_objects/dashboard_page.js index c1aa593f64104..84560b743f32b 100644 --- a/test/functional/page_objects/dashboard_page.js +++ b/test/functional/page_objects/dashboard_page.js @@ -199,8 +199,8 @@ export function DashboardPageProvider({ getService, getPageObjects }) { // wait until the count of dashboard panels equals the count of toggle menu icons await retry.waitFor('in edit mode', async () => { const [panels, menuIcons] = await Promise.all([ - testSubjects.findAll('dashboardPanel'), - testSubjects.findAll('dashboardPanelToggleMenuIcon'), + testSubjects.findAll('embeddablePanel'), + testSubjects.findAll('embeddablePanelToggleMenuIcon'), ]); return panels.length === menuIcons.length; }); @@ -482,7 +482,7 @@ export function DashboardPageProvider({ getService, getPageObjects }) { async getPanelCount() { log.debug('getPanelCount'); - const panels = await testSubjects.findAll('dashboardPanel'); + const panels = await testSubjects.findAll('embeddablePanel'); return panels.length; } @@ -507,7 +507,7 @@ export function DashboardPageProvider({ getService, getPageObjects }) { } async getDashboardPanels() { - return await testSubjects.findAll('dashboardPanel'); + return await testSubjects.findAll('embeddablePanel'); } async addVisualizations(visualizations) { @@ -611,7 +611,7 @@ export function DashboardPageProvider({ getService, getPageObjects }) { const checkList = []; for (const name of vizList) { const isPresent = await testSubjects.exists( - `dashboardPanelHeading-${name.replace(/\s+/g, '')}`, + `embeddablePanelHeading-${name.replace(/\s+/g, '')}`, { timeout: 10000 } ); checkList.push({ name, isPresent }); diff --git a/test/functional/services/dashboard/add_panel.js b/test/functional/services/dashboard/add_panel.js index 79067b98ac2f7..89ec7b0a4c3c2 100644 --- a/test/functional/services/dashboard/add_panel.js +++ b/test/functional/services/dashboard/add_panel.js @@ -31,8 +31,10 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) { await testSubjects.click('dashboardAddPanelButton'); } - async clickAddNewEmbeddableLink() { - await testSubjects.click('addNewSavedObjectLink'); + async clickAddNewEmbeddableLink(type) { + await testSubjects.click('createNew'); + await testSubjects.click(`createNew-${type}`); + await testSubjects.missingOrFail(`createNew-${type}`); } async toggleFilterPopover() { diff --git a/test/functional/services/dashboard/panel_actions.js b/test/functional/services/dashboard/panel_actions.js index f6d15a501f27b..051074eb9b3d0 100644 --- a/test/functional/services/dashboard/panel_actions.js +++ b/test/functional/services/dashboard/panel_actions.js @@ -17,12 +17,12 @@ * under the License. */ -const REMOVE_PANEL_DATA_TEST_SUBJ = 'dashboardPanelAction-deletePanel'; -const EDIT_PANEL_DATA_TEST_SUBJ = 'dashboardPanelAction-editPanel'; -const TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ = 'dashboardPanelAction-togglePanel'; -const CUSTOMIZE_PANEL_DATA_TEST_SUBJ = 'dashboardPanelAction-customizePanel'; -const OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ = 'dashboardPanelToggleMenuIcon'; -const OPEN_INSPECTOR_TEST_SUBJ = 'dashboardPanelAction-openInspector'; +const REMOVE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-deletePanel'; +const EDIT_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-editPanel'; +const TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-togglePanel'; +const CUSTOMIZE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-CUSTOMIZE_PANEL_ACTION_ID'; +const OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ = 'embeddablePanelToggleMenuIcon'; +const OPEN_INSPECTOR_TEST_SUBJ = 'embeddablePanelAction-openInspector'; export function DashboardPanelActionsProvider({ getService, getPageObjects }) { const log = getService('log'); @@ -45,19 +45,13 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }) { async toggleContextMenu(parent) { log.debug('toggleContextMenu'); - // Sometimes Geckodriver throws MoveTargetOutOfBoundsError here - // https://github.com/mozilla/geckodriver/issues/776 - try { - await (parent ? browser.moveMouseTo(parent) : testSubjects.moveMouseTo('dashboardPanelTitle')); - } catch(err) { - log.error(err); - } + await (parent ? browser.moveMouseTo(parent) : testSubjects.moveMouseTo('dashboardPanelTitle')); const toggleMenuItem = await this.findContextMenu(parent); await toggleMenuItem.click(); } async expectContextMenuToBeOpen() { - await testSubjects.existOrFail('dashboardPanelContextMenuOpen'); + await testSubjects.existOrFail('embeddablePanelContextMenuOpen'); } async openContextMenu(parent) { @@ -130,7 +124,22 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }) { } async getPanelHeading(title) { - return await testSubjects.find(`dashboardPanelHeading-${title.replace(/\s/g, '')}`); + return await testSubjects.find(`embeddablePanelHeading-${title.replace(/\s/g, '')}`); + } + + async clickHidePanelTitleToggle() { + await testSubjects.click('customizePanelHideTitle'); + } + + async toggleHidePanelTitle(originalTitle) { + log.debug(`hidePanelTitle(${originalTitle})`); + let panelOptions = null; + if (originalTitle) { + panelOptions = await this.getPanelHeading(originalTitle); + } + await this.customizePanel(panelOptions); + await this.clickHidePanelTitleToggle(); + await testSubjects.click('saveNewTitleButton'); } /** @@ -146,24 +155,15 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }) { panelOptions = await this.getPanelHeading(originalTitle); } await this.customizePanel(panelOptions); - if (customTitle.length === 0) { - if (browser.isW3CEnabled) { - const input = await testSubjects.find('customDashboardPanelTitleInput'); - await input.clearValueWithKeyboard(); - } else { - // to clean in Chrome we trigger a change: put letter and delete it - await testSubjects.setValue('customDashboardPanelTitleInput', 'h\b'); - } - } else { - await testSubjects.setValue('customDashboardPanelTitleInput', customTitle); - } - await this.toggleContextMenu(panelOptions); + await testSubjects.setValue('customEmbeddablePanelTitleInput', customTitle); + await testSubjects.click('saveNewTitleButton'); } async resetCustomPanelTitle(panel) { log.debug('resetCustomPanelTitle'); await this.customizePanel(panel); - await testSubjects.click('resetCustomDashboardPanelTitle'); + await testSubjects.click('resetCustomEmbeddablePanelTitle'); + await testSubjects.click('saveNewTitleButton'); await this.toggleContextMenu(panel); } }; diff --git a/test/functional/services/dashboard/visualizations.js b/test/functional/services/dashboard/visualizations.js index cd55171ed919e..8cde98861ca88 100644 --- a/test/functional/services/dashboard/visualizations.js +++ b/test/functional/services/dashboard/visualizations.js @@ -33,7 +33,7 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }) { await PageObjects.dashboard.switchToEditMode(); } await dashboardAddPanel.ensureAddPanelIsShowing(); - await dashboardAddPanel.clickAddNewEmbeddableLink(); + await dashboardAddPanel.clickAddNewEmbeddableLink('visualization'); await PageObjects.visualize.clickVisualBuilder(); await PageObjects.visualize.saveVisualizationExpectSuccess(name); } @@ -80,7 +80,7 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }) { await PageObjects.dashboard.switchToEditMode(); } await dashboardAddPanel.ensureAddPanelIsShowing(); - await dashboardAddPanel.clickAddNewEmbeddableLink(); + await dashboardAddPanel.clickAddNewEmbeddableLink('visualization'); await PageObjects.visualize.clickMarkdownWidget(); await PageObjects.visualize.setMarkdownTxt(markdown); await PageObjects.visualize.clickGo(); diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_input.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_input.ts index 240911848269e..595e04cf56d38 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_input.ts +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/dashboard_input.ts @@ -54,49 +54,48 @@ export const dashboardInput: DashboardContainerInput = { firstName: 'Sue', }, }, - // TODO: Uncomment when saved objects are using the new Embeddable API - // '822cd0f0-ce7c-419d-aeaa-1171cf452745': { - // gridData: { - // w: 24, - // h: 15, - // x: 0, - // y: 0, - // i: '822cd0f0-ce7c-419d-aeaa-1171cf452745', - // }, - // type: 'visualization', - // explicitInput: { - // id: '822cd0f0-ce7c-419d-aeaa-1171cf452745', - // }, - // savedObjectId: '3fe22200-3dcb-11e8-8660-4d65aa086b3c', - // }, - // '66f0a265-7b06-4974-accd-d05f74f7aa82': { - // gridData: { - // w: 24, - // h: 15, - // x: 24, - // y: 0, - // i: '66f0a265-7b06-4974-accd-d05f74f7aa82', - // }, - // type: 'visualization', - // explicitInput: { - // id: '66f0a265-7b06-4974-accd-d05f74f7aa82', - // }, - // savedObjectId: '4c0f47e0-3dcd-11e8-8660-4d65aa086b3c', - // }, - // 'b2861741-40b9-4dc8-b82b-080c6e29a551': { - // gridData: { - // w: 24, - // h: 15, - // x: 0, - // y: 15, - // i: 'b2861741-40b9-4dc8-b82b-080c6e29a551', - // }, - // type: 'search', - // explicitInput: { - // id: 'b2861741-40b9-4dc8-b82b-080c6e29a551', - // }, - // savedObjectId: 'be5accf0-3dca-11e8-8660-4d65aa086b3c', - // }, + '822cd0f0-ce7c-419d-aeaa-1171cf452745': { + gridData: { + w: 24, + h: 15, + x: 0, + y: 0, + i: '822cd0f0-ce7c-419d-aeaa-1171cf452745', + }, + type: 'visualization', + explicitInput: { + id: '822cd0f0-ce7c-419d-aeaa-1171cf452745', + }, + savedObjectId: '3fe22200-3dcb-11e8-8660-4d65aa086b3c', + }, + '66f0a265-7b06-4974-accd-d05f74f7aa82': { + gridData: { + w: 24, + h: 15, + x: 24, + y: 0, + i: '66f0a265-7b06-4974-accd-d05f74f7aa82', + }, + type: 'visualization', + explicitInput: { + id: '66f0a265-7b06-4974-accd-d05f74f7aa82', + }, + savedObjectId: '4c0f47e0-3dcd-11e8-8660-4d65aa086b3c', + }, + 'b2861741-40b9-4dc8-b82b-080c6e29a551': { + gridData: { + w: 24, + h: 15, + x: 0, + y: 15, + i: 'b2861741-40b9-4dc8-b82b-080c6e29a551', + }, + type: 'search', + explicitInput: { + id: 'b2861741-40b9-4dc8-b82b-080c6e29a551', + }, + savedObjectId: 'be5accf0-3dca-11e8-8660-4d65aa086b3c', + }, }, isFullScreenMode: false, filters: [], diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/hello_world_container_example.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/hello_world_container_example.tsx index c614a5f0420ff..7b8542662ed11 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/hello_world_container_example.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/hello_world_container_example.tsx @@ -18,13 +18,9 @@ */ import React from 'react'; - -import { - EmbeddablePanel, - Container, - embeddableFactories, - EmbeddableFactory, -} from 'plugins/embeddable_api'; +import { EuiFieldText, EuiFormRow } from '@elastic/eui'; +import { EmbeddablePanel, embeddableFactories, EmbeddableFactory } from 'plugins/embeddable_api'; +import { Subscription } from 'rxjs'; import { HelloWorldContainer, CONTACT_CARD_EMBEDDABLE, @@ -35,8 +31,10 @@ interface Props { embeddableFactories: Map; } -export class HelloWorldContainerExample extends React.Component { - private container: Container; +export class HelloWorldContainerExample extends React.Component { + private container: HelloWorldContainer; + private mounted: boolean = false; + private subscription?: Subscription; public constructor(props: Props) { super(props); @@ -64,17 +62,43 @@ export class HelloWorldContainerExample extends React.Component { }, embeddableFactories ); + this.state = { + lastName: this.container.getInput().lastName, + }; + } + + public componentDidMount() { + this.mounted = true; + this.subscription = this.container.getInput$().subscribe(() => { + const { lastName } = this.container.getInput(); + if (this.mounted) { + this.setState({ + lastName, + }); + } + }); } public componentWillUnmount() { - if (this.container) { - this.container.destroy(); + this.mounted = false; + if (this.subscription) { + this.subscription.unsubscribe(); } + this.container.destroy(); } + public render() { return (

Hello World Container

+ + this.container.updateInput({ lastName: e.target.value })} + /> +
); diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/shim.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/shim.tsx index 7e430f6ea119b..bc2d19e0a4feb 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/shim.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/shim.tsx @@ -19,8 +19,8 @@ import { embeddableFactories, EmbeddableFactory } from 'plugins/embeddable_api'; import 'ui/autoload/all'; -import 'uiExports/embeddableActions'; import 'uiExports/embeddableFactories'; +import 'uiExports/embeddableActions'; import uiRoutes from 'ui/routes'; diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/index.ts b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/index.ts index dd069010e0742..13b7f8fe52fa0 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/index.ts +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/index.ts @@ -27,7 +27,7 @@ function samplePanelAction(kibana: KibanaPlugin) { return new kibana.Plugin({ publicDir: resolve(__dirname, './public'), uiExports: { - contextMenuActions: [ + embeddableActions: [ 'plugins/kbn_tp_sample_panel_action/sample_panel_action', 'plugins/kbn_tp_sample_panel_action/sample_panel_link', ], diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx index d8832a8ae46e2..b7d7daf8252aa 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_action.tsx @@ -21,26 +21,26 @@ import React from 'react'; import { npStart } from 'ui/new_platform'; import { - ContextMenuAction, - ContextMenuActionsRegistryProvider, - PanelActionAPI, -} from 'ui/embeddable'; + ActionContext, + actionRegistry, + Action, + triggerRegistry, + attachAction, + CONTEXT_MENU_TRIGGER, +} from 'plugins/embeddable_api'; + +class SamplePanelAction extends Action { + public readonly type = 'samplePanelAction'; -class SamplePanelAction extends ContextMenuAction { constructor() { - super( - { - id: 'samplePanelAction', - parentPanelId: 'mainMenu', - }, - { - getDisplayName: () => { - return 'Sample Panel Action'; - }, - } - ); + super('samplePanelAction'); + } + + public getDisplayName() { + return 'Sample Panel Action'; } - public onClick = ({ embeddable }: PanelActionAPI) => { + + public execute = ({ embeddable }: ActionContext) => { if (!embeddable) { return; } @@ -48,7 +48,7 @@ class SamplePanelAction extends ContextMenuAction { -

{embeddable.metadata.title}

+

{embeddable.getTitle()}

@@ -62,4 +62,5 @@ class SamplePanelAction extends ContextMenuAction { }; } -ContextMenuActionsRegistryProvider.register(() => new SamplePanelAction()); +actionRegistry.set('samplePanelAction', new SamplePanelAction()); +attachAction(triggerRegistry, { triggerId: CONTEXT_MENU_TRIGGER, actionId: 'samplePanelAction' }); diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_link.ts b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_link.ts index ea754dc269d03..4e59bbd896814 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_link.ts +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_link.ts @@ -16,21 +16,27 @@ * specific language governing permissions and limitations * under the License. */ -import { ContextMenuAction, ContextMenuActionsRegistryProvider } from 'ui/embeddable'; +import { + Action, + actionRegistry, + triggerRegistry, + CONTEXT_MENU_TRIGGER, + attachAction, +} from 'plugins/embeddable_api'; + +class SamplePanelLink extends Action { + public readonly type = 'samplePanelLink'; -class SamplePanelLink extends ContextMenuAction { constructor() { - super( - { - id: 'samplePanelLink', - parentPanelId: 'mainMenu', - }, - { - getDisplayName: () => { - return 'Sample Panel Link'; - }, - } - ); + super('samplePanelLink'); + } + + public getDisplayName() { + return 'Sample panel Link'; + } + + public execute() { + return; } public getHref = () => { @@ -38,4 +44,6 @@ class SamplePanelLink extends ContextMenuAction { }; } -ContextMenuActionsRegistryProvider.register(() => new SamplePanelLink()); +actionRegistry.set('samplePanelLink', new SamplePanelLink()); + +attachAction(triggerRegistry, { triggerId: CONTEXT_MENU_TRIGGER, actionId: 'samplePanelLink' }); diff --git a/test/plugin_functional/test_suites/embeddable_explorer/dashboard_container.js b/test/plugin_functional/test_suites/embeddable_explorer/dashboard_container.js index 4700fc8787e75..10ad41c012d46 100644 --- a/test/plugin_functional/test_suites/embeddable_explorer/dashboard_container.js +++ b/test/plugin_functional/test_suites/embeddable_explorer/dashboard_container.js @@ -22,6 +22,8 @@ import expect from '@kbn/expect'; export default function ({ getService }) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); + const pieChart = getService('pieChart'); + const dashboardExpect = getService('dashboardExpect'); describe('dashboard container', () => { before(async () => { @@ -39,17 +41,16 @@ export default function ({ getService }) { await testSubjects.existOrFail('embeddablePanelHeading-HelloSue'); }); - // TODO: uncomment when we add saved searches to the test dashboard. - // it('pie charts', async () => { - // await pieChart.expectPieSliceCount(5); - // }); + it('pie charts', async () => { + await pieChart.expectPieSliceCount(5); + }); - // it('markdown', async () => { - // await dashboardExpect.markdownWithValuesExists(['I\'m a markdown!']); - // }); + it('markdown', async () => { + await dashboardExpect.markdownWithValuesExists(['I\'m a markdown!']); + }); - // it('saved search', async () => { - // await dashboardExpect.savedSearchRowCount(50); - // }); + it('saved search', async () => { + await dashboardExpect.savedSearchRowCount(50); + }); }); } diff --git a/test/plugin_functional/test_suites/panel_actions/panel_actions.js b/test/plugin_functional/test_suites/panel_actions/panel_actions.js index 7e2f072d1a981..6a00fea7f5478 100644 --- a/test/plugin_functional/test_suites/panel_actions/panel_actions.js +++ b/test/plugin_functional/test_suites/panel_actions/panel_actions.js @@ -31,7 +31,7 @@ export default function ({ getService, getPageObjects }) { it('allows to register links into the context menu', async () => { await dashboardPanelActions.openContextMenu(); - const actionElement = await testSubjects.find('dashboardPanelAction-samplePanelLink'); + const actionElement = await testSubjects.find('embeddablePanelAction-samplePanelLink'); const actionElementTag = await actionElement.getTagName(); expect(actionElementTag).to.be('a'); const actionElementLink = await actionElement.getAttribute('href'); @@ -40,12 +40,12 @@ export default function ({ getService, getPageObjects }) { it('Sample action appears in context menu in view mode', async () => { await testSubjects.existOrFail( - 'dashboardPanelAction-samplePanelAction' + 'embeddablePanelAction-samplePanelAction' ); }); it('Clicking sample action shows a flyout', async () => { - await testSubjects.click('dashboardPanelAction-samplePanelAction'); + await testSubjects.click('embeddablePanelAction-samplePanelAction'); await testSubjects.existOrFail('samplePanelActionFlyout'); }); diff --git a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js index 4ffce3daa9828..89a265d81ed4b 100644 --- a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js +++ b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js @@ -21,6 +21,7 @@ import 'uiExports/visRequestHandlers'; import 'uiExports/visEditorTypes'; import 'uiExports/inspectorViews'; import 'uiExports/savedObjectTypes'; +import 'uiExports/embeddableActions'; import 'uiExports/embeddableFactories'; import 'uiExports/navbarExtensions'; import 'uiExports/docViews'; diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index 4e56ff5ff1958..2934a44504bcf 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -55,7 +55,7 @@ export function maps(kibana) { }; }, embeddableFactories: [ - 'plugins/maps/embeddable/map_embeddable_factory_provider' + 'plugins/maps/embeddable/map_embeddable_factory' ], inspectorViews: [ 'plugins/maps/inspector/views/register_views', diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js index 7e284c5121ee8..a9e574fdbf505 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js +++ b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js @@ -10,7 +10,7 @@ import { Provider } from 'react-redux'; import { render, unmountComponentAtNode } from 'react-dom'; import 'mapbox-gl/dist/mapbox-gl.css'; -import { Embeddable } from 'ui/embeddable'; +import { Embeddable } from '../../../../../../src/legacy/core_plugins/embeddable_api/public/index'; import { I18nContext } from 'ui/i18n'; import { GisMap } from '../connected_components/gis_map'; @@ -33,31 +33,26 @@ import { import { getIsLayerTOCOpen, getOpenTOCDetails } from '../selectors/ui_selectors'; import { getInspectorAdapters } from '../reducers/non_serializable_instances'; import { getMapCenter, getMapZoom } from '../selectors/map_selectors'; -import { i18n } from '@kbn/i18n'; +import { MAP_SAVED_OBJECT_TYPE } from '../../common/constants'; export class MapEmbeddable extends Embeddable { - - constructor({ - onEmbeddableStateChanged, - embeddableConfig, - savedMap, - editUrl, - editable, - indexPatterns = [] - }) { - super({ - title: savedMap.title, - editUrl, - editLabel: i18n.translate('xpack.maps.embeddable.editLabel', { - defaultMessage: 'Edit map', - }), - editable, - indexPatterns }); - - this._onEmbeddableStateChanged = onEmbeddableStateChanged; - this._embeddableConfig = _.cloneDeep(embeddableConfig); - this._savedMap = savedMap; + type = MAP_SAVED_OBJECT_TYPE; + + constructor(config, initialInput, parent) { + super( + initialInput, + { + editUrl: config.editUrl, + indexPatterns: config.indexPatterns, + editable: config.editable, + defaultTitle: config.savedMap.title + }, + parent); + + this._savedMap = config.savedMap; this._store = createMapStore(); + + this._subscription = this.getInput$().subscribe((input) => this.onContainerStateChanged(input)); } getInspectorAdapters() { @@ -71,11 +66,7 @@ export class MapEmbeddable extends Embeddable { this._dispatchSetQuery(containerState); } - const refreshConfig = { - isPaused: containerState.refreshConfig.pause, - interval: containerState.refreshConfig.value - }; - if (!_.isEqual(refreshConfig, this._prevRefreshConfig)) { + if (!_.isEqual(containerState.refreshConfig, this._prevRefreshConfig)) { this._dispatchSetRefreshConfig(containerState); } } @@ -92,12 +83,11 @@ export class MapEmbeddable extends Embeddable { } _dispatchSetRefreshConfig({ refreshConfig }) { - const internalRefreshConfig = { + this._prevRefreshConfig = refreshConfig; + this._store.dispatch(setRefreshConfig({ isPaused: refreshConfig.pause, - interval: refreshConfig.value - }; - this._prevRefreshConfig = internalRefreshConfig; - this._store.dispatch(setRefreshConfig(internalRefreshConfig)); + interval: refreshConfig.value, + })); } /** @@ -105,30 +95,30 @@ export class MapEmbeddable extends Embeddable { * @param {HTMLElement} domNode * @param {ContainerState} containerState */ - render(domNode, containerState) { + render(domNode) { this._store.dispatch(setReadOnly(true)); this._store.dispatch(setFilterable(true)); this._store.dispatch(disableScrollZoom()); - if (_.has(this._embeddableConfig, 'isLayerTOCOpen')) { - this._store.dispatch(setIsLayerTOCOpen(this._embeddableConfig.isLayerTOCOpen)); + if (_.has(this.input, 'isLayerTOCOpen')) { + this._store.dispatch(setIsLayerTOCOpen(this.input.isLayerTOCOpen)); } else if (this._savedMap.uiStateJSON) { const uiState = JSON.parse(this._savedMap.uiStateJSON); this._store.dispatch(setIsLayerTOCOpen(_.get(uiState, 'isLayerTOCOpen', DEFAULT_IS_LAYER_TOC_OPEN))); } - if (_.has(this._embeddableConfig, 'openTOCDetails')) { - this._store.dispatch(setOpenTOCDetails(this._embeddableConfig.openTOCDetails)); + if (_.has(this.input, 'openTOCDetails')) { + this._store.dispatch(setOpenTOCDetails(this.input.openTOCDetails)); } else if (this._savedMap.uiStateJSON) { const uiState = JSON.parse(this._savedMap.uiStateJSON); this._store.dispatch(setOpenTOCDetails(_.get(uiState, 'openTOCDetails', []))); } - if (this._embeddableConfig.mapCenter) { + if (this.input.mapCenter) { this._store.dispatch(setGotoWithCenter({ - lat: this._embeddableConfig.mapCenter.lat, - lon: this._embeddableConfig.mapCenter.lon, - zoom: this._embeddableConfig.mapCenter.zoom, + lat: this.input.mapCenter.lat, + lon: this.input.mapCenter.lon, + zoom: this.input.mapCenter.zoom, })); } else if (this._savedMap.mapStateJSON) { const mapState = JSON.parse(this._savedMap.mapStateJSON); @@ -140,8 +130,8 @@ export class MapEmbeddable extends Embeddable { } const layerList = getInitialLayers(this._savedMap.layerListJSON); this._store.dispatch(replaceLayerList(layerList)); - this._dispatchSetQuery(containerState); - this._dispatchSetRefreshConfig(containerState); + this._dispatchSetQuery(this.input); + this._dispatchSetRefreshConfig(this.input); render( @@ -158,6 +148,7 @@ export class MapEmbeddable extends Embeddable { } destroy() { + super.destroy(); if (this._unsubscribeFromStore) { this._unsubscribeFromStore(); } @@ -165,6 +156,10 @@ export class MapEmbeddable extends Embeddable { if (this._domNode) { unmountComponentAtNode(this._domNode); } + + if (this._subscription) { + this._subscription.unsubscribe(); + } } reload() { @@ -176,37 +171,36 @@ export class MapEmbeddable extends Embeddable { } _handleStoreChanges() { - let embeddableConfigChanged = false; const center = getMapCenter(this._store.getState()); const zoom = getMapZoom(this._store.getState()); - if (!this._embeddableConfig.mapCenter - || this._embeddableConfig.mapCenter.lat !== center.lat - || this._embeddableConfig.mapCenter.lon !== center.lon - || this._embeddableConfig.mapCenter.zoom !== zoom) { - embeddableConfigChanged = true; - this._embeddableConfig.mapCenter = { - lat: center.lat, - lon: center.lon, - zoom: zoom, - }; + + + const mapCenter = this.input.mapCenter || {}; + if (!mapCenter + || mapCenter.lat !== center.lat + || mapCenter.lon !== center.lon + || mapCenter.zoom !== zoom) { + this.updateInput({ + mapCenter: { + lat: center.lat, + lon: center.lon, + zoom: zoom, + } + }); } const isLayerTOCOpen = getIsLayerTOCOpen(this._store.getState()); - if (this._embeddableConfig.isLayerTOCOpen !== isLayerTOCOpen) { - embeddableConfigChanged = true; - this._embeddableConfig.isLayerTOCOpen = isLayerTOCOpen; + if (this.input.isLayerTOCOpen !== isLayerTOCOpen) { + this.updateInput({ + isLayerTOCOpen + }); } const openTOCDetails = getOpenTOCDetails(this._store.getState()); - if (!_.isEqual(this._embeddableConfig.openTOCDetails, openTOCDetails)) { - embeddableConfigChanged = true; - this._embeddableConfig.openTOCDetails = openTOCDetails; - } - - if (embeddableConfigChanged) { - this._onEmbeddableStateChanged({ - customization: this._embeddableConfig + if (!_.isEqual(this.input.openTOCDetails, openTOCDetails)) { + this.updateInput({ + openTOCDetails }); } } diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.js b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.js index 546249b9c21a8..f67f945f114d0 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.js +++ b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.js @@ -6,30 +6,48 @@ import _ from 'lodash'; import chrome from 'ui/chrome'; -import { EmbeddableFactory } from 'ui/embeddable'; +import { capabilities } from 'ui/capabilities'; +import { i18n } from '@kbn/i18n'; +import { + EmbeddableFactory, + embeddableFactories, + ErrorEmbeddable +} from '../../../../../../src/legacy/core_plugins/embeddable_api/public/index'; import { MapEmbeddable } from './map_embeddable'; import { indexPatternService } from '../kibana_services'; -import { i18n } from '@kbn/i18n'; + import { createMapPath, MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants'; import { createMapStore } from '../reducers/store'; import { addLayerWithoutDataSync } from '../actions/map_actions'; import { getQueryableUniqueIndexPatternIds } from '../selectors/map_selectors'; -import { capabilities } from 'ui/capabilities'; +import '../angular/services/gis_map_saved_object_loader'; +import 'ui/vis/map/service_settings'; export class MapEmbeddableFactory extends EmbeddableFactory { + type = MAP_SAVED_OBJECT_TYPE; - constructor(gisMapSavedObjectLoader) { + constructor() { super({ - name: 'map', savedObjectMetaData: { name: i18n.translate('xpack.maps.mapSavedObjectLabel', { defaultMessage: 'Map', }), type: MAP_SAVED_OBJECT_TYPE, - getIconForSavedObject: () => APP_ICON + getIconForSavedObject: () => APP_ICON, }, }); - this._savedObjectLoader = gisMapSavedObjectLoader; + } + isEditable() { + return capabilities.get().maps.save; + } + + // Not supported yet for maps types. + canCreateNew() { return false; } + + getDisplayName() { + return i18n.translate('xpack.maps.embeddableDisplayName', { + defaultMessage: 'map', + }); } async _getIndexPatterns(layerListJSON) { @@ -59,20 +77,33 @@ export class MapEmbeddableFactory extends EmbeddableFactory { return _.compact(indexPatterns); } - async create(panelMetadata, onEmbeddableStateChanged) { - const savedMap = await this._savedObjectLoader.get(panelMetadata.id); + async createFromSavedObject( + savedObjectId, + input, + parent + ) { + const $injector = await chrome.dangerouslyGetActiveInjector(); + const savedObjectLoader = $injector.get('gisMapSavedObjectLoader'); + const savedMap = await savedObjectLoader.get(savedObjectId); const indexPatterns = await this._getIndexPatterns(savedMap.layerListJSON); - const editable = capabilities.get().maps.save; + return new MapEmbeddable( + { + savedMap, + editUrl: chrome.addBasePath(createMapPath(savedObjectId)), + indexPatterns, + editable: this.isEditable(), + }, + input, + parent + ); + } - return new MapEmbeddable({ - onEmbeddableStateChanged, - embeddableConfig: panelMetadata.embeddableConfig, - savedMap, - editUrl: chrome.addBasePath(createMapPath(panelMetadata.id)), - editable, - indexPatterns, - }); + async create(input) { + window.location.href = chrome.addBasePath(createMapPath('')); + return new ErrorEmbeddable('Maps can only be created from a saved object', input); } } + +embeddableFactories.set(MAP_SAVED_OBJECT_TYPE, new MapEmbeddableFactory()); diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory_provider.js b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory_provider.js deleted file mode 100644 index 38be62a658f0f..0000000000000 --- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory_provider.js +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EmbeddableFactoriesRegistryProvider } from 'ui/embeddable/embeddable_factories_registry'; -import { MapEmbeddableFactory } from './map_embeddable_factory'; -import '../angular/services/gis_map_saved_object_loader'; - -function mapEmbeddableFactoryProvider(gisMapSavedObjectLoader) { - return new MapEmbeddableFactory(gisMapSavedObjectLoader); -} - -EmbeddableFactoriesRegistryProvider.register(mapEmbeddableFactoryProvider); diff --git a/x-pack/legacy/plugins/maps/public/index.js b/x-pack/legacy/plugins/maps/public/index.js index 85d519dddd6c4..4457ba459b092 100644 --- a/x-pack/legacy/plugins/maps/public/index.js +++ b/x-pack/legacy/plugins/maps/public/index.js @@ -15,6 +15,7 @@ import 'uiExports/fieldFormats'; import 'uiExports/inspectorViews'; import 'uiExports/search'; import 'uiExports/embeddableFactories'; +import 'uiExports/embeddableActions'; import 'ui/agg_types'; import { capabilities } from 'ui/capabilities'; diff --git a/x-pack/legacy/plugins/reporting/index.js b/x-pack/legacy/plugins/reporting/index.js index 98a903be061ee..09665055c09c6 100644 --- a/x-pack/legacy/plugins/reporting/index.js +++ b/x-pack/legacy/plugins/reporting/index.js @@ -36,7 +36,7 @@ export const reporting = (kibana) => { 'plugins/reporting/share_context_menu/register_csv_reporting', 'plugins/reporting/share_context_menu/register_reporting', ], - contextMenuActions: [ + embeddableActions: [ 'plugins/reporting/panel_actions/get_csv_panel_action', ], hacks: ['plugins/reporting/hacks/job_completion_notifier'], diff --git a/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index 939aef9586cc1..b30e0cdac58d4 100644 --- a/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -3,40 +3,62 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; import dateMath from '@elastic/datemath'; import { i18n } from '@kbn/i18n'; import moment from 'moment-timezone'; -import { ContextMenuAction, ContextMenuActionsRegistryProvider } from 'ui/embeddable'; -import { PanelActionAPI } from 'ui/embeddable/context_menu_actions/types'; import { kfetch } from 'ui/kfetch'; import { toastNotifications } from 'ui/notify'; import chrome from 'ui/chrome'; +import { EuiIcon } from '@elastic/eui'; +import { + ISearchEmbeddable, + SEARCH_EMBEDDABLE_TYPE, +} from '../../../../../../src/legacy/core_plugins/kibana/public/discover/embeddable'; + +import { + Action, + actionRegistry, + ActionContext, + ViewMode, + IncompatibleActionError, + IEmbeddable, + triggerRegistry, + attachAction, + CONTEXT_MENU_TRIGGER, +} from '../../../../../../src/legacy/core_plugins/embeddable_api/public'; import { API_BASE_URL_V1 } from '../../common/constants'; const API_BASE_URL = `${API_BASE_URL_V1}/generate/immediate/csv/saved-object`; -class GetCsvReportPanelAction extends ContextMenuAction { +const CSV_REPORTING_ACTION = 'downloadCsvReport'; + +function isSavedSearchEmbeddable( + embeddable: IEmbeddable | ISearchEmbeddable +): embeddable is ISearchEmbeddable { + return embeddable.type === SEARCH_EMBEDDABLE_TYPE; +} +class GetCsvReportPanelAction extends Action { private isDownloading: boolean; + public readonly type = CSV_REPORTING_ACTION; constructor() { - super( - { - id: 'downloadCsvReport', - parentPanelId: 'mainMenu', - }, - { - icon: 'document', - getDisplayName: () => - i18n.translate('xpack.reporting.dashboard.downloadCsvPanelTitle', { - defaultMessage: 'Download CSV', - }), - } - ); + super(CSV_REPORTING_ACTION); this.isDownloading = false; } + public getIcon() { + return ; + } + + public getDisplayName() { + return i18n.translate('xpack.reporting.dashboard.downloadCsvPanelTitle', { + defaultMessage: 'Download CSV', + }); + } + public async getSearchRequestBody({ searchEmbeddable }: { searchEmbeddable: any }) { const adapters = searchEmbeddable.getInspectorAdapters(); if (!adapters) { @@ -50,37 +72,40 @@ class GetCsvReportPanelAction extends ContextMenuAction { return searchEmbeddable.searchScope.searchSource.getSearchRequestBody(); } - public isVisible = (panelActionAPI: PanelActionAPI): boolean => { + public isCompatible = async (context: ActionContext) => { const enablePanelActionDownload = chrome.getInjected('enablePanelActionDownload'); if (!enablePanelActionDownload) { return false; } - const { embeddable, containerState } = panelActionAPI; + const { embeddable } = context; - return ( - containerState.viewMode !== 'edit' && !!embeddable && embeddable.hasOwnProperty('savedSearch') - ); + return embeddable.getInput().viewMode !== ViewMode.EDIT && embeddable.type === 'search'; }; - public onClick = async (panelActionAPI: PanelActionAPI) => { - const { embeddable } = panelActionAPI as any; - const { - timeRange: { from, to }, - } = embeddable; + public execute = async (context: ActionContext) => { + const { embeddable } = context; + + if (!isSavedSearchEmbeddable(embeddable)) { + throw new IncompatibleActionError(); + } - if (!embeddable || this.isDownloading) { + if (this.isDownloading) { return; } + const { + timeRange: { to, from }, + } = embeddable.getInput(); + const searchEmbeddable = embeddable; const searchRequestBody = await this.getSearchRequestBody({ searchEmbeddable }); const state = _.pick(searchRequestBody, ['sort', 'docvalue_fields', 'query']); const kibanaTimezone = chrome.getUiSettingsClient().get('dateFormat:tz'); - const id = `search:${embeddable.savedSearch.id}`; - const filename = embeddable.getPanelTitle(); + const id = `search:${embeddable.getSavedSearch().id}`; + const filename = embeddable.getTitle(); const timezone = kibanaTimezone === 'Browser' ? moment.tz.guess() : kibanaTimezone; const fromTime = dateMath.parse(from); const toTime = dateMath.parse(to); @@ -151,4 +176,6 @@ class GetCsvReportPanelAction extends ContextMenuAction { } } -ContextMenuActionsRegistryProvider.register(() => new GetCsvReportPanelAction()); +actionRegistry.set(CSV_REPORTING_ACTION, new GetCsvReportPanelAction()); + +attachAction(triggerRegistry, { triggerId: CONTEXT_MENU_TRIGGER, actionId: CSV_REPORTING_ACTION }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9866df017d675..313d46869b95c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1427,7 +1427,6 @@ "kbn.dashboard.changeViewModeConfirmModal.discardChangesTitle": "ダッシュボードへの変更を破棄しますか?", "kbn.dashboard.dashboardAppBreadcrumbsTitle": "ダッシュボード", "kbn.dashboard.dashboardBreadcrumbsTitle": "ダッシュボード", - "kbn.dashboard.dashboardGrid.unableToLoadDashboardDangerMessage": "ダッシュボードが読み込めません。", "kbn.dashboard.dashboardWasNotSavedDangerMessage": "ダッシュボード「{dashTitle}」は保存されませんでした。エラー: {errorMessage}", "kbn.dashboard.dashboardWasSavedSuccessMessage": "ダッシュボード「{dashTitle}」が保存されました。", "kbn.dashboard.featureCatalogue.dashboardDescription": "ビジュアライゼーションと保存された検索のコレクションの表示と共有を行います。", @@ -1447,31 +1446,12 @@ "kbn.dashboard.listing.table.entityName": "ダッシュボード", "kbn.dashboard.listing.table.entityNamePlural": "ダッシュボード", "kbn.dashboard.listing.table.titleColumnName": "タイトル", - "kbn.dashboard.panel.customizePanel.displayName": "パネルをカスタマイズ", - "kbn.dashboard.panel.customizePanelTitle": "パネルをカスタマイズ", - "kbn.dashboard.panel.dashboardPanelAriaLabel": "ダッシュボードパネル: {title}", - "kbn.dashboard.panel.editPanel.defaultDisplayName": "編集", - "kbn.dashboard.panel.inspectorPanel.displayName": "検査", - "kbn.dashboard.panel.noEmbeddableFactoryErrorMessage": "このパネルを並べ替える機能が欠けています。", - "kbn.dashboard.panel.noFoundEmbeddableFactoryErrorMessage": "パネルタイプ {panelType} をレンダリングする機能がありません", - "kbn.dashboard.panel.optionsMenu.optionsContextMenuTitle": "オプション", - "kbn.dashboard.panel.optionsMenu.panelOptionsButtonAriaLabel": "パネルオプション", - "kbn.dashboard.panel.optionsMenuForm.panelTitleFormRowLabel": "パネルタイトル", - "kbn.dashboard.panel.optionsMenuForm.panelTitleInputAriaLabel": "このインプットへの変更は直ちに適用されます。Enter を押して閉じます。", - "kbn.dashboard.panel.optionsMenuForm.resetCustomDashboardButtonLabel": "タイトルをリセット", - "kbn.dashboard.panel.removePanel.displayName": "ダッシュボードから削除", - "kbn.dashboard.panel.toggleExpandPanel.expandedDisplayName": "最小化", - "kbn.dashboard.panel.toggleExpandPanel.notExpandedDisplayName": "全画面", "kbn.dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "「6.3.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルに必要なフィールドがありません: {key}", "kbn.dashboard.savedDashboard.newDashboardTitle": "新規ダッシュボード", "kbn.dashboard.savedDashboardsTitle": "ダッシュボード", "kbn.dashboard.stateManager.timeNotSavedWithDashboardErrorMessage": "このダッシュボードに時刻が保存されていないため、同期できません。", "kbn.dashboard.strings.dashboardEditTitle": "{title} を編集中", "kbn.dashboard.strings.dashboardUnsavedEditTitle": "{title} を編集中 (未保存)", - "kbn.dashboard.topNav.addPanel.createNewVisualizationButtonLabel": "新規ビジュアライゼーションを追加", - "kbn.dashboard.topNav.addPanel.noMatchingObjectsMessage": "一致するオブジェクトが見つかりません。", - "kbn.dashboard.topNav.addPanel.savedObjectAddedToDashboardSuccessMessageTitle": "ダッシュボードに {savedObjectName} が追加されました", - "kbn.dashboard.topNav.addPanelsTitle": "パネルの追加", "kbn.dashboard.topNav.cloneModal.cancelButtonLabel": "キャンセル", "kbn.dashboard.topNav.cloneModal.cloneDashboardModalHeaderTitle": "ダッシュボードのクローンを作成", "kbn.dashboard.topNav.cloneModal.confirmButtonLabel": "クローンの確認", @@ -1631,8 +1611,6 @@ "kbn.docTable.tableRow.toggleRowDetailsButtonAriaLabel": "行の詳細を切り替える", "kbn.docTable.tableRow.viewSingleDocumentLinkText": "単一のドキュメントを表示", "kbn.docTable.tableRow.viewSurroundingDocumentsLinkText": "周りのドキュメントを表示", - "kbn.embeddable.search.editLabel": "保存された検索を編集", - "kbn.embeddable.visualize.editLabel": "ビジュアライゼーションを編集", "kbn.home.addData.addDataToKibanaDescription": "これらのソリューションで、データを作成済みのダッシュボードと監視システムへとすぐに変えることができます。", "kbn.home.addData.addDataToKibanaTitle": "Kibana にデータを追加", "kbn.home.addData.apm.addApmButtonLabel": "APM を追加", @@ -5573,7 +5551,6 @@ "xpack.maps.elasticsearch_geo_utils.unsupportedFieldTypeErrorMessage": "サポートされていないフィールドタイプ、期待値: geo_shape または geo_point、入力値: {geoFieldType}", "xpack.maps.elasticsearch_geo_utils.unsupportedGeoPointValueErrorMessage": "サポートされていない geo_point 値: {geoPointValue}", "xpack.maps.elasticsearch_geo_utils.wkt.invalidWKTErrorMessage": "{wkt} を Geojson に変換できません。有効な WKT が必要です。", - "xpack.maps.embeddable.editLabel": "マップを編集", "xpack.maps.esSearch.featureCountMsg": "{count} 件のドキュメントが見つかりました。", "xpack.maps.esSearch.resultsTrimmedMsg": "結果は初めの {count} 件のドキュメントに制限されています。", "xpack.maps.esSearch.topHitsEntitiesCountMsg": "{entityCount} 件のエントリーを発見.", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4896d3b96e0d3..84a2629996ecb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1427,7 +1427,6 @@ "kbn.dashboard.changeViewModeConfirmModal.discardChangesTitle": "放弃对仪表板的更改?", "kbn.dashboard.dashboardAppBreadcrumbsTitle": "仪表板", "kbn.dashboard.dashboardBreadcrumbsTitle": "仪表板", - "kbn.dashboard.dashboardGrid.unableToLoadDashboardDangerMessage": "无法加载仪表板。", "kbn.dashboard.dashboardWasNotSavedDangerMessage": "仪表板 “{dashTitle}” 未保存。错误:{errorMessage}", "kbn.dashboard.dashboardWasSavedSuccessMessage": "仪表板 “{dashTitle}” 已保存", "kbn.dashboard.featureCatalogue.dashboardDescription": "显示和共享可视化和已保存搜索的集合。", @@ -1447,31 +1446,12 @@ "kbn.dashboard.listing.table.entityName": "仪表板", "kbn.dashboard.listing.table.entityNamePlural": "仪表板", "kbn.dashboard.listing.table.titleColumnName": "标题", - "kbn.dashboard.panel.customizePanel.displayName": "定制面板", - "kbn.dashboard.panel.customizePanelTitle": "定制面板", - "kbn.dashboard.panel.dashboardPanelAriaLabel": "仪表板面板:{title}", - "kbn.dashboard.panel.editPanel.defaultDisplayName": "编辑", - "kbn.dashboard.panel.inspectorPanel.displayName": "检查", - "kbn.dashboard.panel.noEmbeddableFactoryErrorMessage": "用于呈现此面板的功能缺失。", - "kbn.dashboard.panel.noFoundEmbeddableFactoryErrorMessage": "未找到面板类型 {panelType} 的 Embeddable 工厂", - "kbn.dashboard.panel.optionsMenu.optionsContextMenuTitle": "选项", - "kbn.dashboard.panel.optionsMenu.panelOptionsButtonAriaLabel": "面板选项", - "kbn.dashboard.panel.optionsMenuForm.panelTitleFormRowLabel": "面板标题", - "kbn.dashboard.panel.optionsMenuForm.panelTitleInputAriaLabel": "对此输入的更改将立即应用。按 enter 键可退出。", - "kbn.dashboard.panel.optionsMenuForm.resetCustomDashboardButtonLabel": "重置标题", - "kbn.dashboard.panel.removePanel.displayName": "从仪表板删除", - "kbn.dashboard.panel.toggleExpandPanel.expandedDisplayName": "最小化", - "kbn.dashboard.panel.toggleExpandPanel.notExpandedDisplayName": "全屏", "kbn.dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "无法迁移用于“6.3.0”向后兼容的面板数据,面板不包含预期字段:{key}", "kbn.dashboard.savedDashboard.newDashboardTitle": "新建仪表板", "kbn.dashboard.savedDashboardsTitle": "仪表板", "kbn.dashboard.stateManager.timeNotSavedWithDashboardErrorMessage": "时间未随此仪表板保存,因此无法同步。", "kbn.dashboard.strings.dashboardEditTitle": "编辑 {title}", "kbn.dashboard.strings.dashboardUnsavedEditTitle": "编辑 {title}(未保存)", - "kbn.dashboard.topNav.addPanel.createNewVisualizationButtonLabel": "新建可视化", - "kbn.dashboard.topNav.addPanel.noMatchingObjectsMessage": "未找到任何匹配对象。", - "kbn.dashboard.topNav.addPanel.savedObjectAddedToDashboardSuccessMessageTitle": "“{savedObjectName}” 已添加到您的仪表板", - "kbn.dashboard.topNav.addPanelsTitle": "添加面板", "kbn.dashboard.topNav.cloneModal.cancelButtonLabel": "取消", "kbn.dashboard.topNav.cloneModal.cloneDashboardModalHeaderTitle": "克隆面板", "kbn.dashboard.topNav.cloneModal.confirmButtonLabel": "确认克隆", @@ -1631,8 +1611,6 @@ "kbn.docTable.tableRow.toggleRowDetailsButtonAriaLabel": "切换行详细信息", "kbn.docTable.tableRow.viewSingleDocumentLinkText": "查看单个文档", "kbn.docTable.tableRow.viewSurroundingDocumentsLinkText": "查看周围文档", - "kbn.embeddable.search.editLabel": "编辑保存的搜索", - "kbn.embeddable.visualize.editLabel": "编辑可视化", "kbn.home.addData.addDataToKibanaDescription": "使用这些解决方案可快速将您的数据转换成预建仪表板和监测系统。", "kbn.home.addData.addDataToKibanaTitle": "将数据添加到 Kibana", "kbn.home.addData.apm.addApmButtonLabel": "添加 APM", @@ -5573,7 +5551,6 @@ "xpack.maps.elasticsearch_geo_utils.unsupportedFieldTypeErrorMessage": "字段类型不受支持,应为:geo_shape 或 geo_point,而您提供的是:{geoFieldType}", "xpack.maps.elasticsearch_geo_utils.unsupportedGeoPointValueErrorMessage": "不受支持的 geo_point 值:{geoPointValue}", "xpack.maps.elasticsearch_geo_utils.wkt.invalidWKTErrorMessage": "无法将 {wkt} 转换成 geojson。需要有效的 WKT。", - "xpack.maps.embeddable.editLabel": "编辑地图", "xpack.maps.esSearch.featureCountMsg": "找到 {count} 个文档。", "xpack.maps.esSearch.resultsTrimmedMsg": "结果仅限于前 {count} 个文档。", "xpack.maps.esSearch.topHitsEntitiesCountMsg": "找到 {entityCount} 个实体。", diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts index 94b4eb534f593..84458d4c17223 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts @@ -116,7 +116,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa ensureCurrentUrl: false, shouldLoginIfPrompted: false, }); - await testSubjects.existOrFail('dashboardPanelHeading-APie', 10000); + await testSubjects.existOrFail('embeddablePanelHeading-APie', 10000); }); it(`does not allow a visualization to be edited`, async () => { @@ -267,7 +267,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa ensureCurrentUrl: false, shouldLoginIfPrompted: false, }); - await testSubjects.existOrFail('dashboardPanelHeading-APie', 10000); + await testSubjects.existOrFail('embeddablePanelHeading-APie', 10000); }); it(`Permalinks doesn't show create short-url button`, async () => { diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts index 227dd43da36fc..aa852d1ae8111 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts @@ -85,7 +85,7 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa ensureCurrentUrl: false, shouldLoginIfPrompted: false, }); - await testSubjects.existOrFail('dashboardPanelHeading-APie', 10000); + await testSubjects.existOrFail('embeddablePanelHeading-APie', 10000); }); });