diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 3a4e49968626f..7a19514eebe17 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -468,9 +468,14 @@ export class DashboardAppController { const explicitInput = { savedVis: input, }; + const embeddableId = + 'embeddableId' in incomingEmbeddable + ? incomingEmbeddable.embeddableId + : undefined; container.addOrUpdateEmbeddable( incomingEmbeddable.type, - explicitInput + explicitInput, + embeddableId ); } } diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index ff74580ba256b..036880a1d088b 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -171,6 +171,7 @@ export class DashboardContainer extends Container = IEmbeddable - >(type: string, explicitInput: Partial) { - if (explicitInput.id && this.input.panels[explicitInput.id]) { - this.replacePanel(this.input.panels[explicitInput.id], { + >(type: string, explicitInput: Partial, embeddableId?: string) { + const idToReplace = embeddableId || explicitInput.id; + if (idToReplace && this.input.panels[idToReplace]) { + this.replacePanel(this.input.panels[idToReplace], { type, explicitInput: { ...explicitInput, diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx index 594a7ad73c396..8c3d7ab9c30d0 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx @@ -56,12 +56,18 @@ test('is compatible when edit url is available, in edit mode and editable', asyn test('redirects to app using state transfer', async () => { applicationMock.currentAppId$ = of('superCoolCurrentApp'); const action = new EditPanelAction(getFactory, applicationMock, stateTransferMock); - const embeddable = new EditableEmbeddable({ id: '123', viewMode: ViewMode.EDIT }, true); + const input = { id: '123', viewMode: ViewMode.EDIT }; + const embeddable = new EditableEmbeddable(input, true); embeddable.getOutput = jest.fn(() => ({ editApp: 'ultraVisualize', editPath: '/123' })); await action.execute({ embeddable }); expect(stateTransferMock.navigateToEditor).toHaveBeenCalledWith('ultraVisualize', { path: '/123', - state: { originatingApp: 'superCoolCurrentApp' }, + state: { + originatingApp: 'superCoolCurrentApp', + byValueMode: true, + embeddableId: '123', + valueInput: input, + }, }); }); diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts index 9177a77d547b0..8d12ddd0299e7 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts @@ -24,7 +24,12 @@ import { take } from 'rxjs/operators'; import { ViewMode } from '../types'; import { EmbeddableFactoryNotFoundError } from '../errors'; import { EmbeddableStart } from '../../plugin'; -import { IEmbeddable, EmbeddableEditorState, EmbeddableStateTransfer } from '../..'; +import { + IEmbeddable, + EmbeddableEditorState, + EmbeddableStateTransfer, + SavedObjectEmbeddableInput, +} from '../..'; export const ACTION_EDIT_PANEL = 'editPanel'; @@ -109,8 +114,17 @@ export class EditPanelAction implements Action { const app = embeddable ? embeddable.getOutput().editApp : undefined; const path = embeddable ? embeddable.getOutput().editPath : undefined; if (app && path) { - const state = this.currentAppId ? { originatingApp: this.currentAppId } : undefined; - return { app, path, state }; + if (this.currentAppId) { + const byValueMode = !(embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId; + const state: EmbeddableEditorState = { + originatingApp: this.currentAppId, + byValueMode, + valueInput: byValueMode ? embeddable.getInput() : undefined, + embeddableId: embeddable.id, + }; + return { app, path, state }; + } + return { app, path }; } } diff --git a/src/plugins/embeddable/public/lib/state_transfer/types.ts b/src/plugins/embeddable/public/lib/state_transfer/types.ts index a6721784302ac..3f3456d914728 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/types.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/types.ts @@ -27,6 +27,7 @@ export interface EmbeddableEditorState { originatingApp: string; byValueMode?: boolean; valueInput?: EmbeddableInput; + embeddableId?: string; } export function isEmbeddableEditorState(state: unknown): state is EmbeddableEditorState { @@ -49,6 +50,7 @@ export interface EmbeddablePackageByReferenceState { export interface EmbeddablePackageByValueState { type: string; input: EmbeddableInput; + embeddableId?: string; } export type EmbeddablePackageState = diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx index 1c5642f9b75b7..b058ef0de448b 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx @@ -46,6 +46,7 @@ export function TopNavMenuItem(props: TopNavMenuData) { iconType: props.iconType, iconSide: props.iconSide, 'data-test-subj': props.testId, + className: props.className, }; const btn = props.emphasize ? ( diff --git a/src/plugins/vis_default_editor/public/default_editor_controller.tsx b/src/plugins/vis_default_editor/public/default_editor_controller.tsx index 3158582438558..56fb15ea8354a 100644 --- a/src/plugins/vis_default_editor/public/default_editor_controller.tsx +++ b/src/plugins/vis_default_editor/public/default_editor_controller.tsx @@ -69,7 +69,6 @@ class DefaultEditorController { ] : visType.editorConfig.optionTabs), ]; - this.state = { vis, optionTabs, diff --git a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts index 45c750de05ae1..194deef82a5f0 100644 --- a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts +++ b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts @@ -41,7 +41,8 @@ export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDe try { const visId = vis.id as string; - const editPath = visId ? savedVisualizations.urlFor(visId) : ''; + const editPath = visId ? savedVisualizations.urlFor(visId) : '#/edit_by_value'; + const editUrl = visId ? getHttp().basePath.prepend(`/app/visualize${savedVisualizations.urlFor(visId)}`) : ''; diff --git a/src/plugins/visualize/public/application/app.tsx b/src/plugins/visualize/public/application/app.tsx index 0e71d72a3d4c7..8dd6b2ace8413 100644 --- a/src/plugins/visualize/public/application/app.tsx +++ b/src/plugins/visualize/public/application/app.tsx @@ -24,7 +24,12 @@ import { Route, Switch, useLocation } from 'react-router-dom'; import { syncQueryStateWithUrl } from '../../../data/public'; import { useKibana } from '../../../kibana_react/public'; import { VisualizeServices } from './types'; -import { VisualizeEditor, VisualizeListing, VisualizeNoMatch } from './components'; +import { + VisualizeEditor, + VisualizeListing, + VisualizeNoMatch, + VisualizeByValueEditor, +} from './components'; import { VisualizeConstants } from './visualize_constants'; export const VisualizeApp = () => { @@ -48,6 +53,9 @@ export const VisualizeApp = () => { return ( + + + diff --git a/src/plugins/visualize/public/application/components/index.ts b/src/plugins/visualize/public/application/components/index.ts index a3a7fde1d6569..1666bae9b72e0 100644 --- a/src/plugins/visualize/public/application/components/index.ts +++ b/src/plugins/visualize/public/application/components/index.ts @@ -20,3 +20,4 @@ export { VisualizeListing } from './visualize_listing'; export { VisualizeEditor } from './visualize_editor'; export { VisualizeNoMatch } from './visualize_no_match'; +export { VisualizeByValueEditor } from './visualize_byvalue_editor'; diff --git a/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx new file mode 100644 index 0000000000000..a78633d6841e5 --- /dev/null +++ b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx @@ -0,0 +1,105 @@ +/* + * 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 './visualize_editor.scss'; +import React, { useEffect, useState } from 'react'; +import { EventEmitter } from 'events'; + +import { VisualizeInput } from 'src/plugins/visualizations/public'; +import { useKibana } from '../../../../kibana_react/public'; +import { + useChromeVisibility, + useVisByValue, + useVisualizeAppState, + useEditorUpdates, + useLinkedSearchUpdates, +} from '../utils'; +import { VisualizeServices } from '../types'; +import { VisualizeEditorCommon } from './visualize_editor_common'; + +export const VisualizeByValueEditor = () => { + const [originatingApp, setOriginatingApp] = useState(); + const { services } = useKibana(); + const [eventEmitter] = useState(new EventEmitter()); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(true); + const [embeddableId, setEmbeddableId] = useState(); + const [valueInput, setValueInput] = useState(); + + useEffect(() => { + const { originatingApp: value, embeddableId: embeddableIdValue, valueInput: valueInputValue } = + services.embeddable + .getStateTransfer(services.scopedHistory) + .getIncomingEditorState({ keysToRemoveAfterFetch: ['id', 'embeddableId', 'valueInput'] }) || + {}; + setOriginatingApp(value); + setValueInput(valueInputValue); + setEmbeddableId(embeddableIdValue); + if (!valueInputValue) { + history.back(); + } + }, [services]); + + const isChromeVisible = useChromeVisibility(services.chrome); + + const { byValueVisInstance, visEditorRef, visEditorController } = useVisByValue( + services, + eventEmitter, + isChromeVisible, + valueInput, + originatingApp + ); + const { appState, hasUnappliedChanges } = useVisualizeAppState( + services, + eventEmitter, + byValueVisInstance + ); + const { isEmbeddableRendered, currentAppState } = useEditorUpdates( + services, + eventEmitter, + setHasUnsavedChanges, + appState, + byValueVisInstance, + visEditorController + ); + useLinkedSearchUpdates(services, eventEmitter, appState, byValueVisInstance); + + useEffect(() => { + // clean up all registered listeners if any is left + return () => { + eventEmitter.removeAllListeners(); + }; + }, [eventEmitter]); + + return ( + + ); +}; diff --git a/src/plugins/visualize/public/application/components/visualize_editor.tsx b/src/plugins/visualize/public/application/components/visualize_editor.tsx index 516dcacfe5813..0bf5b26e1339f 100644 --- a/src/plugins/visualize/public/application/components/visualize_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_editor.tsx @@ -21,8 +21,6 @@ import './visualize_editor.scss'; import React, { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { EventEmitter } from 'events'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiScreenReaderOnly } from '@elastic/eui'; import { useKibana } from '../../../../kibana_react/public'; import { @@ -33,8 +31,7 @@ import { useLinkedSearchUpdates, } from '../utils'; import { VisualizeServices } from '../types'; -import { ExperimentalVisInfo } from './experimental_vis_info'; -import { VisualizeTopNav } from './visualize_top_nav'; +import { VisualizeEditorCommon } from './visualize_editor_common'; export const VisualizeEditor = () => { const { id: visualizationIdFromUrl } = useParams(); @@ -67,7 +64,9 @@ export const VisualizeEditor = () => { useEffect(() => { const { originatingApp: value } = - services.embeddable.getStateTransfer(services.scopedHistory).getIncomingEditorState() || {}; + services.embeddable + .getStateTransfer(services.scopedHistory) + .getIncomingEditorState({ keysToRemoveAfterFetch: ['id', 'input'] }) || {}; setOriginatingApp(value); }, [services]); @@ -79,38 +78,19 @@ export const VisualizeEditor = () => { }, [eventEmitter]); return ( -
- {savedVisInstance && appState && currentAppState && ( - - )} - {savedVisInstance?.vis?.type?.isExperimental && } - {savedVisInstance && ( - -

- -

-
- )} -
-
+ ); }; diff --git a/src/plugins/visualize/public/application/components/visualize_editor_common.tsx b/src/plugins/visualize/public/application/components/visualize_editor_common.tsx new file mode 100644 index 0000000000000..b811936c63b14 --- /dev/null +++ b/src/plugins/visualize/public/application/components/visualize_editor_common.tsx @@ -0,0 +1,110 @@ +/* + * 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 './visualize_editor.scss'; +import React, { RefObject } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiScreenReaderOnly } from '@elastic/eui'; +import { VisualizeTopNav } from './visualize_top_nav'; +import { ExperimentalVisInfo } from './experimental_vis_info'; +import { + SavedVisInstance, + VisualizeAppState, + VisualizeAppStateContainer, + VisualizeEditorVisInstance, +} from '../types'; + +interface VisualizeEditorCommonProps { + visInstance?: VisualizeEditorVisInstance; + appState: VisualizeAppStateContainer | null; + currentAppState?: VisualizeAppState; + isChromeVisible?: boolean; + hasUnsavedChanges: boolean; + setHasUnsavedChanges: (value: boolean) => void; + hasUnappliedChanges: boolean; + isEmbeddableRendered: boolean; + visEditorRef: RefObject; + originatingApp?: string; + setOriginatingApp?: (originatingApp: string | undefined) => void; + visualizationIdFromUrl?: string; + embeddableId?: string; +} + +export const VisualizeEditorCommon = ({ + visInstance, + appState, + currentAppState, + isChromeVisible, + hasUnsavedChanges, + setHasUnsavedChanges, + hasUnappliedChanges, + isEmbeddableRendered, + originatingApp, + setOriginatingApp, + visualizationIdFromUrl, + embeddableId, + visEditorRef, +}: VisualizeEditorCommonProps) => { + return ( +
+ {visInstance && appState && currentAppState && ( + + )} + {visInstance?.vis?.type?.isExperimental && } + {visInstance && ( + +

+ {'savedVis' in visInstance && visInstance.savedVis.id ? ( + + ) : ( + + )} +

+
+ )} +
+
+ ); +}; diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx index f2cb2d49f59b0..12a3d1cdf95b1 100644 --- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx +++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx @@ -25,7 +25,7 @@ import { VisualizeServices, VisualizeAppState, VisualizeAppStateContainer, - SavedVisInstance, + VisualizeEditorVisInstance, } from '../types'; import { APP_NAME } from '../visualize_constants'; import { getTopNavConfig } from '../utils'; @@ -38,10 +38,11 @@ interface VisualizeTopNavProps { setHasUnsavedChanges: (value: boolean) => void; hasUnappliedChanges: boolean; originatingApp?: string; + visInstance: VisualizeEditorVisInstance; setOriginatingApp?: (originatingApp: string | undefined) => void; - savedVisInstance: SavedVisInstance; stateContainer: VisualizeAppStateContainer; visualizationIdFromUrl?: string; + embeddableId?: string; } const TopNav = ({ @@ -53,26 +54,26 @@ const TopNav = ({ hasUnappliedChanges, originatingApp, setOriginatingApp, - savedVisInstance, + visInstance, stateContainer, visualizationIdFromUrl, + embeddableId, }: VisualizeTopNavProps) => { const { services } = useKibana(); const { TopNavMenu } = services.navigation.ui; - const { embeddableHandler, vis } = savedVisInstance; + const { embeddableHandler, vis } = visInstance; const [inspectorSession, setInspectorSession] = useState(); const openInspector = useCallback(() => { const session = embeddableHandler.openInspector(); setInspectorSession(session); }, [embeddableHandler]); - const handleRefresh = useCallback( (_payload: any, isUpdate?: boolean) => { if (isUpdate === false) { - savedVisInstance.embeddableHandler.reload(); + visInstance.embeddableHandler.reload(); } }, - [savedVisInstance.embeddableHandler] + [visInstance.embeddableHandler] ); const config = useMemo(() => { @@ -85,9 +86,10 @@ const TopNav = ({ openInspector, originatingApp, setOriginatingApp, - savedVisInstance, + visInstance, stateContainer, visualizationIdFromUrl, + embeddableId, }, services ); @@ -99,11 +101,12 @@ const TopNav = ({ hasUnappliedChanges, openInspector, originatingApp, + visInstance, setOriginatingApp, - savedVisInstance, stateContainer, visualizationIdFromUrl, services, + embeddableId, ]); const [indexPattern, setIndexPattern] = useState(vis.data.indexPattern); const showDatePicker = () => { diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index 02ae1cc155dd2..65b88485b2f06 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -121,6 +121,14 @@ export interface SavedVisInstance { embeddableHandler: VisualizeEmbeddableContract; } +export interface ByValueVisInstance { + vis: Vis; + savedSearch?: SavedObject; + embeddableHandler: VisualizeEmbeddableContract; +} + +export type VisualizeEditorVisInstance = SavedVisInstance | ByValueVisInstance; + export interface IEditorController { render(props: EditorRenderProps): void; destroy(): void; diff --git a/src/plugins/visualize/public/application/utils/breadcrumbs.ts b/src/plugins/visualize/public/application/utils/breadcrumbs.ts index a1e5a9e8912e1..a5c246c539c54 100644 --- a/src/plugins/visualize/public/application/utils/breadcrumbs.ts +++ b/src/plugins/visualize/public/application/utils/breadcrumbs.ts @@ -21,6 +21,18 @@ import { i18n } from '@kbn/i18n'; import { VisualizeConstants } from '../visualize_constants'; +const appPrefixes: Record = { + dashboards: { + text: i18n.translate('visualize.dashboard.prefix.breadcrumb', { + defaultMessage: 'Dashboard', + }), + }, +}; + +const defaultEditText = i18n.translate('visualize.editor.defaultEditBreadcrumbText', { + defaultMessage: 'Edit', +}); + export function getLandingBreadcrumbs() { return [ { @@ -43,7 +55,12 @@ export function getCreateBreadcrumbs() { ]; } -export function getEditBreadcrumbs(text: string) { +export function getBreadcrumbsPrefixedWithApp(originatingApp: string) { + const originatingAppBreadcrumb = appPrefixes[originatingApp]; + return [originatingAppBreadcrumb, ...getLandingBreadcrumbs(), { text: defaultEditText }]; +} + +export function getEditBreadcrumbs(text: string = defaultEditText) { return [ ...getLandingBreadcrumbs(), { diff --git a/src/plugins/visualize/public/application/utils/create_visualize_app_state.ts b/src/plugins/visualize/public/application/utils/create_visualize_app_state.ts index 52b7e3ede298b..7a7e04d78354b 100644 --- a/src/plugins/visualize/public/application/utils/create_visualize_app_state.ts +++ b/src/plugins/visualize/public/application/utils/create_visualize_app_state.ts @@ -32,6 +32,7 @@ const STATE_STORAGE_KEY = '_a'; interface Arguments { kbnUrlStateStorage: IKbnUrlStateStorage; stateDefaults: VisualizeAppState; + byValue?: boolean; } function toObject(state: PureVisState): PureVisState { @@ -40,55 +41,67 @@ function toObject(state: PureVisState): PureVisState { }) as PureVisState; } -export function createVisualizeAppState({ stateDefaults, kbnUrlStateStorage }: Arguments) { +const pureTransitions = { + set: (state) => (prop, value) => ({ ...state, [prop]: value }), + setVis: (state) => (vis) => ({ + ...state, + vis: { + ...state.vis, + ...vis, + }, + }), + unlinkSavedSearch: (state) => ({ query, parentFilters = [] }) => ({ + ...state, + query: query || state.query, + filters: union(state.filters, parentFilters), + linked: false, + }), + updateVisState: (state) => (newVisState) => ({ ...state, vis: toObject(newVisState) }), + updateSavedQuery: (state) => (savedQueryId) => { + const updatedState = { + ...state, + savedQuery: savedQueryId, + }; + + if (!savedQueryId) { + delete updatedState.savedQuery; + } + + return updatedState; + }, +} as VisualizeAppStateTransitions; + +function createVisualizeByValueAppState(stateDefaults: VisualizeAppState) { + const initialState = migrateAppState({ + ...stateDefaults, + ...stateDefaults, + }); + const stateContainer = createStateContainer( + initialState, + pureTransitions + ); + const stopStateSync = () => {}; + return { stateContainer, stopStateSync }; +} + +function createDefaultVisualizeAppState({ stateDefaults, kbnUrlStateStorage }: Arguments) { const urlState = kbnUrlStateStorage.get(STATE_STORAGE_KEY); const initialState = migrateAppState({ ...stateDefaults, ...urlState, }); - /* - make sure url ('_a') matches initial state - Initializing appState does two things - first it translates the defaults into AppState, - second it updates appState based on the url (the url trumps the defaults). This means if - we update the state format at all and want to handle BWC, we must not only migrate the - data stored with saved vis, but also any old state in the url. - */ + make sure url ('_a') matches initial state + Initializing appState does two things - first it translates the defaults into AppState, + second it updates appState based on the url (the url trumps the defaults). This means if + we update the state format at all and want to handle BWC, we must not only migrate the + data stored with saved vis, but also any old state in the url. + */ kbnUrlStateStorage.set(STATE_STORAGE_KEY, initialState, { replace: true }); - const stateContainer = createStateContainer( initialState, - { - set: (state) => (prop, value) => ({ ...state, [prop]: value }), - setVis: (state) => (vis) => ({ - ...state, - vis: { - ...state.vis, - ...vis, - }, - }), - unlinkSavedSearch: (state) => ({ query, parentFilters = [] }) => ({ - ...state, - query: query || state.query, - filters: union(state.filters, parentFilters), - linked: false, - }), - updateVisState: (state) => (newVisState) => ({ ...state, vis: toObject(newVisState) }), - updateSavedQuery: (state) => (savedQueryId) => { - const updatedState = { - ...state, - savedQuery: savedQueryId, - }; - - if (!savedQueryId) { - delete updatedState.savedQuery; - } - - return updatedState; - }, - } + pureTransitions ); - const { start: startStateSync, stop: stopStateSync } = syncState({ storageKey: STATE_STORAGE_KEY, stateContainer: { @@ -102,9 +115,14 @@ export function createVisualizeAppState({ stateDefaults, kbnUrlStateStorage }: A }, stateStorage: kbnUrlStateStorage, }); - // start syncing the appState with the ('_a') url startStateSync(); - return { stateContainer, stopStateSync }; } + +export function createVisualizeAppState({ stateDefaults, kbnUrlStateStorage, byValue }: Arguments) { + if (byValue) { + return createVisualizeByValueAppState(stateDefaults); + } + return createDefaultVisualizeAppState({ stateDefaults, kbnUrlStateStorage }); +} diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index da9ba66a914dd..87a6437192aa9 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -31,9 +31,14 @@ import { } from '../../../../saved_objects/public'; import { unhashUrl } from '../../../../kibana_utils/public'; -import { SavedVisInstance, VisualizeServices, VisualizeAppStateContainer } from '../types'; +import { + VisualizeServices, + VisualizeAppStateContainer, + VisualizeEditorVisInstance, +} from '../types'; import { VisualizeConstants } from '../visualize_constants'; import { getEditBreadcrumbs } from './breadcrumbs'; + interface TopNavConfigParams { hasUnsavedChanges: boolean; setHasUnsavedChanges: (value: boolean) => void; @@ -41,9 +46,10 @@ interface TopNavConfigParams { originatingApp?: string; setOriginatingApp?: (originatingApp: string | undefined) => void; hasUnappliedChanges: boolean; - savedVisInstance: SavedVisInstance; + visInstance: VisualizeEditorVisInstance; stateContainer: VisualizeAppStateContainer; visualizationIdFromUrl?: string; + embeddableId?: string; } export const getTopNavConfig = ( @@ -54,9 +60,10 @@ export const getTopNavConfig = ( originatingApp, setOriginatingApp, hasUnappliedChanges, - savedVisInstance: { embeddableHandler, savedVis, vis }, + visInstance, stateContainer, visualizationIdFromUrl, + embeddableId, }: TopNavConfigParams, { application, @@ -71,10 +78,15 @@ export const getTopNavConfig = ( featureFlagConfig, }: VisualizeServices ) => { + const { vis, embeddableHandler } = visInstance; + const savedVis = 'savedVis' in visInstance ? visInstance.savedVis : undefined; /** * Called when the user clicks "Save" button. */ async function doSave(saveOptions: SavedObjectSaveOpts) { + if (!savedVis) { + return {}; + } const newlyCreated = !Boolean(savedVis.id) || savedVis.copyOnSave; // vis.title was not bound and it's needed to reflect title into visState stateContainer.transitions.setVis({ @@ -147,8 +159,26 @@ export const getTopNavConfig = ( } } + const createVisReference = () => { + if (!originatingApp) { + return; + } + const state = { + input: { + ...vis.serialize(), + id: embeddableId ? embeddableId : uuid.v4(), + }, + type: VISUALIZE_EMBEDDABLE_TYPE, + embeddableId: '', + }; + if (embeddableId) { + state.embeddableId = embeddableId; + } + embeddable.getStateTransfer().navigateToWithEmbeddablePackage(originatingApp, { state }); + }; + const topNavMenu: TopNavMenuData[] = [ - ...(originatingApp && savedVis.id + ...(originatingApp && ((savedVis && savedVis.id) || embeddableId) ? [ { id: 'saveAndReturn', @@ -180,27 +210,35 @@ export const getTopNavConfig = ( confirmOverwrite: false, returnToOrigin: true, }; + if ( + originatingApp === 'dashboards' && + featureFlagConfig.showNewVisualizeFlow && + !savedVis + ) { + return createVisReference(); + } return doSave(saveOptions); }, }, ] : []), - ...(visualizeCapabilities.save + ...(visualizeCapabilities.save && !embeddableId ? [ { id: 'save', label: - savedVis.id && originatingApp + savedVis?.id && originatingApp ? i18n.translate('visualize.topNavMenu.saveVisualizationAsButtonLabel', { defaultMessage: 'save as', }) : i18n.translate('visualize.topNavMenu.saveVisualizationButtonLabel', { defaultMessage: 'save', }), - emphasize: !savedVis.id || !originatingApp, + emphasize: (savedVis && !savedVis.id) || !originatingApp, description: i18n.translate('visualize.topNavMenu.saveVisualizationButtonAriaLabel', { defaultMessage: 'Save Visualization', }), + className: savedVis?.id && originatingApp ? 'saveAsButton' : '', testId: 'visualizeSaveButton', disableButton: hasUnappliedChanges, tooltip() { @@ -213,7 +251,7 @@ export const getTopNavConfig = ( ); } }, - run: () => { + run: (anchorElement: HTMLElement) => { const onSave = async ({ newTitle, newCopyOnSave, @@ -222,6 +260,9 @@ export const getTopNavConfig = ( newDescription, returnToOrigin, }: OnSaveProps & { returnToOrigin: boolean }) => { + if (!savedVis) { + return; + } const currentTitle = savedVis.title; savedVis.title = newTitle; savedVis.copyOnSave = newCopyOnSave; @@ -239,32 +280,23 @@ export const getTopNavConfig = ( } return response; }; - - const createVisReference = () => { - if (!originatingApp) { - return; - } - const input = { - ...vis.serialize(), - id: uuid.v4(), - }; - embeddable.getStateTransfer().navigateToWithEmbeddablePackage(originatingApp, { - state: { input, type: VISUALIZE_EMBEDDABLE_TYPE }, - }); - }; - const saveModal = ( {}} originatingApp={originatingApp} /> ); - if (originatingApp === 'dashboards' && featureFlagConfig.showNewVisualizeFlow) { + const isSaveAsButton = anchorElement.classList.contains('saveAsButton'); + if ( + originatingApp === 'dashboards' && + featureFlagConfig.showNewVisualizeFlow && + !isSaveAsButton + ) { createVisReference(); - } else { + } else if (savedVis) { showSaveModal(saveModal, I18nContext); } }, @@ -281,23 +313,24 @@ export const getTopNavConfig = ( }), testId: 'shareTopNavButton', run: (anchorElement) => { - if (share) { + if (share && !embeddableId) { + // TODO: support sharing in by-value mode share.toggleShareContextMenu({ anchorElement, allowEmbed: true, allowShortUrl: visualizeCapabilities.createShortUrl, shareableUrl: unhashUrl(window.location.href), - objectId: savedVis.id, + objectId: savedVis?.id, objectType: 'visualization', sharingData: { - title: savedVis.title, + title: savedVis?.title, }, isDirty: hasUnappliedChanges || hasUnsavedChanges, }); } }, // disable the Share button if no action specified - disableButton: !share, + disableButton: !share || !!embeddableId, }, { id: 'inspector', diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts index a75c84cf0b71c..3ffca578f8052 100644 --- a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts +++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts @@ -18,46 +18,31 @@ */ import { i18n } from '@kbn/i18n'; -import { VisSavedObject, VisualizeEmbeddableContract } from 'src/plugins/visualizations/public'; +import { + SerializedVis, + Vis, + VisSavedObject, + VisualizeEmbeddableContract, + VisualizeInput, +} from 'src/plugins/visualizations/public'; import { SearchSourceFields } from 'src/plugins/data/public'; import { SavedObject } from 'src/plugins/saved_objects/public'; +import { cloneDeep } from 'lodash'; import { createSavedSearchesLoader } from '../../../../discover/public'; import { VisualizeServices } from '../types'; -export const getVisualizationInstance = async ( - { +const createVisualizeEmbeddableAndLinkSavedSearch = async ( + vis: Vis, + visualizeServices: VisualizeServices +) => { + const { chrome, data, overlays, - visualizations, createVisEmbeddableFromObject, savedObjects, - savedVisualizations, toastNotifications, - }: VisualizeServices, - /** - * opts can be either a saved visualization id passed as string, - * or an object of new visualization params. - * Both come from url search query - */ - opts?: Record | string -) => { - const savedVis: VisSavedObject = await savedVisualizations.get(opts); - - if (typeof opts !== 'string') { - savedVis.searchSourceFields = { index: opts?.indexPattern } as SearchSourceFields; - } - const serializedVis = visualizations.convertToSerializedVis(savedVis); - let vis = await visualizations.createVis(serializedVis.type, serializedVis); - - if (vis.type.setup) { - try { - vis = await vis.type.setup(vis); - } catch { - // skip this catch block - } - } - + } = visualizeServices; const embeddableHandler = (await createVisEmbeddableFromObject(vis, { timeRange: data.query.timefilter.timefilter.getTime(), filters: data.query.filterManager.getFilters(), @@ -86,5 +71,67 @@ export const getVisualizationInstance = async ( }).get(vis.data.savedSearchId); } - return { vis, savedVis, savedSearch, embeddableHandler }; + return { savedSearch, embeddableHandler }; +}; + +export const getVisualizationInstanceFromInput = async ( + visualizeServices: VisualizeServices, + input: VisualizeInput +) => { + const { visualizations } = visualizeServices; + const visState = input.savedVis as SerializedVis; + let vis = await visualizations.createVis(visState.type, cloneDeep(visState)); + if (vis.type.setup) { + try { + vis = await vis.type.setup(vis); + } catch { + // skip this catch block + } + } + const { embeddableHandler, savedSearch } = await createVisualizeEmbeddableAndLinkSavedSearch( + vis, + visualizeServices + ); + return { + vis, + embeddableHandler, + savedSearch, + }; +}; + +export const getVisualizationInstance = async ( + visualizeServices: VisualizeServices, + /** + * opts can be either a saved visualization id passed as string, + * or an object of new visualization params. + * Both come from url search query + */ + opts?: Record | string +) => { + const { visualizations, savedVisualizations } = visualizeServices; + const savedVis: VisSavedObject = await savedVisualizations.get(opts); + + if (typeof opts !== 'string') { + savedVis.searchSourceFields = { index: opts?.indexPattern } as SearchSourceFields; + } + const serializedVis = visualizations.convertToSerializedVis(savedVis); + let vis = await visualizations.createVis(serializedVis.type, serializedVis); + if (vis.type.setup) { + try { + vis = await vis.type.setup(vis); + } catch { + // skip this catch block + } + } + + const { embeddableHandler, savedSearch } = await createVisualizeEmbeddableAndLinkSavedSearch( + vis, + visualizeServices + ); + return { + vis, + embeddableHandler, + savedSearch, + savedVis, + }; }; diff --git a/src/plugins/visualize/public/application/utils/use/index.ts b/src/plugins/visualize/public/application/utils/use/index.ts index 8bd9456b10572..98d1f11d81a8e 100644 --- a/src/plugins/visualize/public/application/utils/use/index.ts +++ b/src/plugins/visualize/public/application/utils/use/index.ts @@ -22,3 +22,4 @@ export { useEditorUpdates } from './use_editor_updates'; export { useSavedVisInstance } from './use_saved_vis_instance'; export { useVisualizeAppState } from './use_visualize_app_state'; export { useLinkedSearchUpdates } from './use_linked_search_updates'; +export { useVisByValue } from './use_vis_byvalue'; diff --git a/src/plugins/visualize/public/application/utils/use/use_editor_updates.ts b/src/plugins/visualize/public/application/utils/use/use_editor_updates.ts index 0f4b2d34e8e87..c29f6337a6246 100644 --- a/src/plugins/visualize/public/application/utils/use/use_editor_updates.ts +++ b/src/plugins/visualize/public/application/utils/use/use_editor_updates.ts @@ -25,8 +25,8 @@ import { VisualizeServices, VisualizeAppState, VisualizeAppStateContainer, - SavedVisInstance, IEditorController, + VisualizeEditorVisInstance, } from '../../types'; export const useEditorUpdates = ( @@ -34,21 +34,22 @@ export const useEditorUpdates = ( eventEmitter: EventEmitter, setHasUnsavedChanges: (value: boolean) => void, appState: VisualizeAppStateContainer | null, - savedVisInstance: SavedVisInstance | undefined, + visInstance: VisualizeEditorVisInstance | undefined, visEditorController: IEditorController | undefined ) => { const [isEmbeddableRendered, setIsEmbeddableRendered] = useState(false); const [currentAppState, setCurrentAppState] = useState(); useEffect(() => { - if (appState && savedVisInstance) { + if (appState && visInstance) { const { timefilter: { timefilter }, filterManager, queryString, state$, } = services.data.query; - const { embeddableHandler, savedVis, savedSearch, vis } = savedVisInstance; + const { embeddableHandler, savedSearch, vis } = visInstance; + const savedVis = 'savedVis' in visInstance ? visInstance.savedVis : undefined; const initialState = appState.getState(); setCurrentAppState(initialState); @@ -79,15 +80,18 @@ export const useEditorUpdates = ( }); const handleLinkedSearch = (linked: boolean) => { - if (linked && !savedVis.savedSearchId && savedSearch) { + if (linked && savedVis && !savedVis.savedSearchId && savedSearch) { savedVis.savedSearchId = savedSearch.id; vis.data.savedSearchId = savedSearch.id; if (vis.data.searchSource) { vis.data.searchSource.setParent(savedSearch.searchSource); } - } else if (!linked && savedVis.savedSearchId) { + } else if (!linked && savedVis && savedVis.savedSearchId) { delete savedVis.savedSearchId; delete vis.data.savedSearchId; + } else if (!linked && !savedVis) { + // delete link when it's not a saved vis + delete vis.data.savedSearchId; } }; @@ -105,8 +109,7 @@ export const useEditorUpdates = ( const unsubscribeStateUpdates = appState.subscribe((state) => { setCurrentAppState(state); - - if (savedVis.id && !services.history.location.pathname.includes(savedVis.id)) { + if (savedVis && savedVis.id && !services.history.location.pathname.includes(savedVis.id)) { // this filters out the case when manipulating the browser history back/forward // and initializing different visualizations return; @@ -118,6 +121,7 @@ export const useEditorUpdates = ( // if the browser history was changed manually we need to reflect changes in the editor if ( + savedVis && !isEqual( { ...services.visualizations.convertFromSerializedVis(vis.serialize()).visState, @@ -160,14 +164,7 @@ export const useEditorUpdates = ( unsubscribeStateUpdates(); }; } - }, [ - appState, - eventEmitter, - savedVisInstance, - services, - setHasUnsavedChanges, - visEditorController, - ]); + }, [appState, eventEmitter, visInstance, services, setHasUnsavedChanges, visEditorController]); return { isEmbeddableRendered, currentAppState }; }; diff --git a/src/plugins/visualize/public/application/utils/use/use_linked_search_updates.ts b/src/plugins/visualize/public/application/utils/use/use_linked_search_updates.ts index e257b72ee751b..7bc38ba6e2842 100644 --- a/src/plugins/visualize/public/application/utils/use/use_linked_search_updates.ts +++ b/src/plugins/visualize/public/application/utils/use/use_linked_search_updates.ts @@ -22,24 +22,23 @@ import { i18n } from '@kbn/i18n'; import { EventEmitter } from 'events'; import { Filter } from 'src/plugins/data/public'; -import { VisualizeServices, VisualizeAppStateContainer, SavedVisInstance } from '../../types'; +import { + VisualizeServices, + VisualizeAppStateContainer, + VisualizeEditorVisInstance, +} from '../../types'; export const useLinkedSearchUpdates = ( services: VisualizeServices, eventEmitter: EventEmitter, appState: VisualizeAppStateContainer | null, - savedVisInstance: SavedVisInstance | undefined + visInstance: VisualizeEditorVisInstance | undefined ) => { useEffect(() => { - if ( - appState && - savedVisInstance && - savedVisInstance.savedSearch && - savedVisInstance.vis.data.searchSource - ) { - const { savedSearch } = savedVisInstance; + if (appState && visInstance && visInstance.savedSearch && visInstance.vis.data.searchSource) { + const { savedSearch } = visInstance; // SearchSource is a promise-based stream of search results that can inherit from other search sources. - const { searchSource } = savedVisInstance.vis.data; + const { searchSource } = visInstance.vis.data; const unlinkFromSavedSearch = () => { const searchSourceParent = savedSearch.searchSource; @@ -70,5 +69,5 @@ export const useLinkedSearchUpdates = ( eventEmitter.off('unlinkFromSavedSearch', unlinkFromSavedSearch); }; } - }, [appState, eventEmitter, savedVisInstance, services.toastNotifications]); + }, [appState, eventEmitter, visInstance, services.toastNotifications]); }; diff --git a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts index 764bcb4a327c0..ec815b8cfcbee 100644 --- a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts +++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts @@ -59,7 +59,6 @@ export const useSavedVisInstance = ( const getSavedVisInstance = async () => { try { let savedVisInstance: SavedVisInstance; - if (history.location.pathname === '/create') { const searchParams = parse(history.location.search); const visTypes = services.visualizations.all(); diff --git a/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts b/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts new file mode 100644 index 0000000000000..f2758d0cc01a4 --- /dev/null +++ b/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts @@ -0,0 +1,95 @@ +/* + * 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 { EventEmitter } from 'events'; +import { useEffect, useRef, useState } from 'react'; +import { VisualizeInput } from 'src/plugins/visualizations/public'; +import { ByValueVisInstance, IEditorController, VisualizeServices } from '../../types'; +import { getVisualizationInstanceFromInput } from '../get_visualization_instance'; +import { getBreadcrumbsPrefixedWithApp, getEditBreadcrumbs } from '../breadcrumbs'; +import { DefaultEditorController } from '../../../../../vis_default_editor/public'; + +export const useVisByValue = ( + services: VisualizeServices, + eventEmitter: EventEmitter, + isChromeVisible: boolean | undefined, + valueInput?: VisualizeInput, + originatingApp?: string +) => { + const [state, setState] = useState<{ + byValueVisInstance?: ByValueVisInstance; + visEditorController?: IEditorController; + }>({}); + const visEditorRef = useRef(null); + const loaded = useRef(false); + useEffect(() => { + const { chrome } = services; + const getVisInstance = async () => { + if (!valueInput || loaded.current) { + return; + } + const byValueVisInstance = await getVisualizationInstanceFromInput(services, valueInput); + const { embeddableHandler, vis } = byValueVisInstance; + const Editor = vis.type.editor || DefaultEditorController; + const visEditorController = new Editor( + visEditorRef.current, + vis, + eventEmitter, + embeddableHandler + ); + + if (chrome && originatingApp) { + chrome.setBreadcrumbs(getBreadcrumbsPrefixedWithApp(originatingApp)); + } else if (chrome) { + chrome.setBreadcrumbs(getEditBreadcrumbs()); + } + + loaded.current = true; + setState({ + byValueVisInstance, + visEditorController, + }); + }; + + getVisInstance(); + }, [ + eventEmitter, + isChromeVisible, + services, + state.byValueVisInstance, + state.visEditorController, + valueInput, + originatingApp, + ]); + + useEffect(() => { + return () => { + if (state.visEditorController) { + state.visEditorController.destroy(); + } else if (state.byValueVisInstance?.embeddableHandler) { + state.byValueVisInstance.embeddableHandler.destroy(); + } + }; + }, [state]); + + return { + ...state, + visEditorRef, + }; +}; diff --git a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts index 8bde9a049c492..39a2db12ffad1 100644 --- a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts +++ b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts @@ -90,6 +90,7 @@ describe('useVisualizeAppState', () => { expect(createVisualizeAppState).toHaveBeenCalledWith({ stateDefaults: visualizeAppStateStub, kbnUrlStateStorage: undefined, + byValue: false, }); expect(mockServices.data.query.filterManager.setAppFilters).toHaveBeenCalledWith( visualizeAppStateStub.filters diff --git a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx index c44f67df3729f..935d4b26c98c9 100644 --- a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx +++ b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx @@ -26,11 +26,14 @@ import { i18n } from '@kbn/i18n'; import { MarkdownSimple, toMountPoint } from '../../../../../kibana_react/public'; import { migrateLegacyQuery } from '../../../../../kibana_legacy/public'; import { esFilters, connectToQueryState } from '../../../../../data/public'; -import { VisualizeServices, VisualizeAppStateContainer, SavedVisInstance } from '../../types'; +import { + VisualizeServices, + VisualizeAppStateContainer, + VisualizeEditorVisInstance, +} from '../../types'; import { visStateToEditorState } from '../utils'; import { createVisualizeAppState } from '../create_visualize_app_state'; import { VisualizeConstants } from '../../visualize_constants'; - /** * This effect is responsible for instantiating the visualize app state container, * which is in sync with "_a" url param @@ -38,7 +41,7 @@ import { VisualizeConstants } from '../../visualize_constants'; export const useVisualizeAppState = ( services: VisualizeServices, eventEmitter: EventEmitter, - instance?: SavedVisInstance + instance?: VisualizeEditorVisInstance ) => { const [hasUnappliedChanges, setHasUnappliedChanges] = useState(false); const [appState, setAppState] = useState(null); @@ -46,10 +49,11 @@ export const useVisualizeAppState = ( useEffect(() => { if (instance) { const stateDefaults = visStateToEditorState(instance, services); - + const byValue = !('savedVis' in instance); const { stateContainer, stopStateSync } = createVisualizeAppState({ stateDefaults, kbnUrlStateStorage: services.kbnUrlStateStorage, + byValue, }); const onDirtyStateChange = ({ isDirty }: { isDirty: boolean }) => { diff --git a/src/plugins/visualize/public/application/utils/utils.ts b/src/plugins/visualize/public/application/utils/utils.ts index 532d87985a0b6..3d8d443d714a5 100644 --- a/src/plugins/visualize/public/application/utils/utils.ts +++ b/src/plugins/visualize/public/application/utils/utils.ts @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { ChromeStart, DocLinksStart } from 'kibana/public'; import { Filter } from '../../../../data/public'; -import { VisualizeServices, SavedVisInstance } from '../types'; +import { VisualizeServices, VisualizeEditorVisInstance } from '../types'; export const addHelpMenuToAppChrome = (chrome: ChromeStart, docLinks: DocLinksStart) => { chrome.setHelpExtension({ @@ -54,15 +54,18 @@ export const getDefaultQuery = ({ data }: VisualizeServices) => { }; export const visStateToEditorState = ( - { vis, savedVis }: SavedVisInstance, + visInstance: VisualizeEditorVisInstance, services: VisualizeServices ) => { + const vis = visInstance.vis; const savedVisState = services.visualizations.convertFromSerializedVis(vis.serialize()); + const savedVis = 'savedVis' in visInstance ? visInstance.savedVis : undefined; return { - uiState: savedVis.uiStateJSON ? JSON.parse(savedVis.uiStateJSON) : vis.uiState.toJSON(), + uiState: + savedVis && savedVis.uiStateJSON ? JSON.parse(savedVis.uiStateJSON) : vis.uiState.toJSON(), query: vis.data.searchSource?.getOwnField('query') || getDefaultQuery(services), filters: (vis.data.searchSource?.getOwnField('filter') as Filter[]) || [], vis: { ...savedVisState.visState, title: vis.title }, - linked: !!savedVis.savedSearchId, + linked: savedVis && savedVis.id ? !!savedVis.savedSearchId : !!savedVisState.savedSearchId, }; }; diff --git a/src/plugins/visualize/public/application/visualize_constants.ts b/src/plugins/visualize/public/application/visualize_constants.ts index adcf27f17dc25..1950fff2733d4 100644 --- a/src/plugins/visualize/public/application/visualize_constants.ts +++ b/src/plugins/visualize/public/application/visualize_constants.ts @@ -25,4 +25,5 @@ export const VisualizeConstants = { WIZARD_STEP_2_PAGE_PATH: '/new/configure', CREATE_PATH: '/create', EDIT_PATH: '/edit', + EDIT_BY_VALUE_PATH: '/edit_by_value', };