diff --git a/package.json b/package.json index 30e6756ca108..be91136e831f 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "dashboarding" ], "private": true, - "version": "3.0.0", + "version": "2.8.0", "branch": "main", "types": "./opensearch_dashboards.d.ts", "tsdocMetadata": "./build/tsdoc-metadata.json", diff --git a/src/plugins/dashboard/public/application/components/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/components/dashboard_top_nav.tsx index 6b2e66cacfa3..2cfb289f7f74 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/components/dashboard_top_nav.tsx @@ -3,8 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +import _, { uniqBy } from 'lodash'; import React, { memo, useState, useEffect } from 'react'; -import { Filter } from 'src/plugins/data/public'; +import { Filter, IndexPattern } from 'src/plugins/data/public'; import { useCallback } from 'react'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { getTopNavConfig } from '../top_nav/get_top_nav_config'; @@ -12,10 +13,10 @@ import { DashboardAppStateContainer, DashboardAppState, DashboardServices, - NavAction, } from '../../types'; import { getNavActions } from '../utils/get_nav_actions'; import { DashboardContainer } from '../embeddable'; +import { isErrorEmbeddable } from '../../embeddable_plugin'; interface DashboardTopNavProps { isChromeVisible: boolean; @@ -45,12 +46,19 @@ const TopNav = ({ const [filters, setFilters] = useState([]); const [topNavMenu, setTopNavMenu] = useState(); const [isFullScreenMode, setIsFullScreenMode] = useState(); + const [indexPatterns, setIndexPatterns] = useState(); const { services } = useOpenSearchDashboards(); const { TopNavMenu } = services.navigation.ui; const { data, dashboardConfig, setHeaderActionMenu } = services; const { query: queryService } = data; + const handleRefresh = useCallback((_payload: any, isUpdate?: boolean) => { + if (isUpdate === false && dashboardContainer) { + dashboardContainer.reload() + } + }, [dashboardContainer]); + // TODO: this should base on URL const isEmbeddedExternally = false; @@ -97,6 +105,33 @@ const TopNav = ({ setIsFullScreenMode(currentAppState?.fullScreenMode); }, [currentAppState, services]); + useEffect(() => { + const asyncSetIndexPattern = async () => { + if(dashboardContainer){ + let panelIndexPatterns: IndexPattern[] = []; + Object.values(dashboardContainer.getChildIds()).forEach((id) => { + const embeddableInstance = dashboardContainer.getChild(id); + if (isErrorEmbeddable(embeddableInstance)) return; + const embeddableIndexPatterns = (embeddableInstance.getOutput() as any).indexPatterns; + if (!embeddableIndexPatterns) return; + panelIndexPatterns.push(...embeddableIndexPatterns); + }); + panelIndexPatterns = uniqBy(panelIndexPatterns, 'id'); + + if(panelIndexPatterns.length>0){ + setIndexPatterns(panelIndexPatterns) + } else { + const defaultIndex = await services.data.indexPatterns.getDefault() + if(defaultIndex){ + setIndexPatterns([defaultIndex]) + } + } + } + } + + asyncSetIndexPattern() + }, [currentAppState, services.data.indexPatterns]) + const shouldShowFilterBar = (forceHide: boolean): boolean => !forceHide && (filters!.length > 0 || !currentAppState?.fullScreenMode); @@ -111,19 +146,7 @@ const TopNav = ({ const showFilterBar = shouldShowFilterBar(forceHideFilterBar); const showSearchBar = showQueryBar || showFilterBar; - // TODO: implement handleRefresh - const handleRefresh = useCallback((_payload: any, isUpdate?: boolean) => { - /* if (isUpdate === false) { - // 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. - lastReloadRequestTime = new Date().getTime(); - const changes = getChangesFromAppStateForContainerState(); - if (changes && dashboardContainer) { - dashboardContainer.updateInput(changes); - }*/ - }, []); - + console.log("index pattern", indexPatterns) return isChromeVisible ? ( {}} diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index ffd50edbe119..76a11be560dd 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -111,6 +111,7 @@ export class DashboardContainer extends Container React.ReactNode); + public getChangesFromAppStateForContainerState?: (containerInput:any) => any; private embeddablePanel: EmbeddableStart['EmbeddablePanel']; diff --git a/src/plugins/dashboard/public/application/utils/get_nav_actions.tsx b/src/plugins/dashboard/public/application/utils/get_nav_actions.tsx index 333d2675388a..c7c152970cbc 100644 --- a/src/plugins/dashboard/public/application/utils/get_nav_actions.tsx +++ b/src/plugins/dashboard/public/application/utils/get_nav_actions.tsx @@ -84,7 +84,7 @@ export const getNavActions = ( navActions[TopNavIds.EXIT_EDIT_MODE] = () => onChangeViewMode(ViewMode.VIEW); navActions[TopNavIds.ENTER_EDIT_MODE] = () => onChangeViewMode(ViewMode.EDIT); navActions[TopNavIds.SAVE] = () => { - console.log('inside save top nav!'); + console.log("inside save top nav!") const currentTitle = appState.title; const currentDescription = appState.description; const currentTimeRestore = appState.timeRestore; @@ -362,12 +362,10 @@ export const getNavActions = ( revertChangesAndExitEditMode(); } }); - - // updateNavBar(); } async function save(saveOptions: SavedObjectSaveOpts) { - console.log('in the save function!'); + console.log("in the save function!") const timefilter = queryService.timefilter.timefilter; try { const id = await saveDashboard(timefilter, stateContainer, savedDashboard, saveOptions); diff --git a/src/plugins/dashboard/public/application/utils/use/use_dashboard_container.tsx b/src/plugins/dashboard/public/application/utils/use/use_dashboard_container.tsx index 8cb511e17620..eb953db76763 100644 --- a/src/plugins/dashboard/public/application/utils/use/use_dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/utils/use/use_dashboard_container.tsx @@ -4,12 +4,21 @@ */ import React, { useState } from 'react'; -import { EMPTY, Subscription, merge } from 'rxjs'; -import { catchError, distinctUntilChanged, map, mapTo, startWith, switchMap } from 'rxjs/operators'; +import _, { uniqBy } from 'lodash'; +import { EMPTY, Observable, Subscription, merge, of, pipe } from 'rxjs'; +import { + catchError, + distinctUntilChanged, + filter, + map, + mapTo, + startWith, + switchMap, +} from 'rxjs/operators'; import deepEqual from 'fast-deep-equal'; import { EventEmitter } from 'stream'; import { useEffect } from 'react'; -import { opensearchFilters } from '../../../../../data/public'; +import { IndexPattern, opensearchFilters } from '../../../../../data/public'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer, @@ -18,13 +27,18 @@ import { } from '../../embeddable'; import { ContainerOutput, + EmbeddableInput, ErrorEmbeddable, ViewMode, isErrorEmbeddable, } from '../../../embeddable_plugin'; -import { convertSavedDashboardPanelToPanelState } from '../../lib/embeddable_saved_object_converters'; +import { + convertPanelStateToSavedDashboardPanel, + convertSavedDashboardPanelToPanelState, +} from '../../lib/embeddable_saved_object_converters'; import { DashboardEmptyScreen, DashboardEmptyScreenProps } from '../../dashboard_empty_screen'; import { DashboardAppStateContainer, DashboardServices, SavedDashboardPanel } from '../../../types'; +import { migrateLegacyQuery } from '../../lib/migrate_legacy_query'; export const useDashboardContainer = ( services: DashboardServices, @@ -60,6 +74,24 @@ export const useDashboardContainer = ( getDashboardContainer(); }, [appState, dashboardDom, eventEmitter, isChromeVisible, savedDashboardInstance, services]); + useEffect(() => { + const incomingEmbeddable = services.embeddable + .getStateTransfer(services.scopedHistory()) + .getIncomingEmbeddablePackage(); + + if ( + incomingEmbeddable && + dashboardContainer && + (!incomingEmbeddable.embeddableId || + !dashboardContainer.getInput().panels[incomingEmbeddable.embeddableId]) + ) { + dashboardContainer.addNewEmbeddable( + incomingEmbeddable.type, + incomingEmbeddable.input + ); + } + }, [dashboardContainer, services.embeddable, services.scopedHistory]); + return { dashboardContainer }; }; @@ -142,6 +174,7 @@ const createDashboardEmbeddable = async ( const getDashboardInput = () => { const appStateData = appState.getState(); + const embeddablesMap: { [key: string]: DashboardPanelState; } = {}; @@ -190,6 +223,41 @@ const createDashboardEmbeddable = async ( ) : null; }; + dashboardContainer.getChangesFromAppStateForContainerState = (container: any) => { + const appStateDashboardInput = getDashboardInput(); + if (!dashboardContainer || isErrorEmbeddable(dashboardContainer)) { + return appStateDashboardInput; + } + + const containerInput = container.getInput(); + const differences: Partial = {}; + + // Filters shouldn't be compared using regular isEqual + if ( + !opensearchFilters.compareFilters( + containerInput.filters, + appStateDashboardInput.filters, + opensearchFilters.COMPARE_ALL_OPTIONS + ) + ) { + differences.filters = appStateDashboardInput.filters; + } + + Object.keys(_.omit(containerInput, ['filters'])).forEach((key) => { + const containerValue = (containerInput as { [key: string]: unknown })[key]; + const appStateValue = ((appStateDashboardInput as unknown) as { + [key: string]: unknown; + })[key]; + if (!_.isEqual(containerValue, appStateValue)) { + (differences as { [key: string]: unknown })[key] = appStateValue; + } + }); + + // cloneDeep hack is needed, as there are multiple place, where container's input mutated, + // but values from appStateValue are deeply frozen, as they can't be mutated directly + return Object.values(differences).length === 0 ? undefined : _.cloneDeep(differences); + }; + // TODO: handle dashboard container input and output subsciptions // issue: outputSubscription = merge( @@ -215,11 +283,10 @@ const createDashboardEmbeddable = async ( .pipe( mapTo(dashboardContainer), startWith(dashboardContainer) // to trigger initial index pattern update - // updateIndexPatternsOperator //TODO ) .subscribe(); - inputSubscription = dashboardContainer.getInput$().subscribe((foo) => { + inputSubscription = dashboardContainer.getInput$().subscribe(() => { // This has to be first because handleDashboardContainerChanges causes // appState.save which will cause refreshDashboardContainer to be called. @@ -236,12 +303,12 @@ const createDashboardEmbeddable = async ( /* dashboardStateManager.applyFilters( $scope.model.query, container.getInput().filters - );*/ + );*/ appState.transitions.set('query', queryStringManager.getQuery()); } - // TODO: triggered when dashboard embeddable container has changes, and update the appState - // handleDashboardContainerChanges(container, appState, dashboardServices); + // triggered when dashboard embeddable container has changes, and update the appState + handleDashboardContainerChanges(container, appState, dashboardServices); }); return dashboardContainer; } @@ -249,3 +316,63 @@ const createDashboardEmbeddable = async ( } return undefined; }; + +const handleDashboardContainerChanges = ( + dashboardContainer: DashboardContainer, + appState: DashboardAppStateContainer, + dashboardServices: DashboardServices +) => { + let dirty = false; + let dirtyBecauseOfInitialStateMigration = false; + const appStateData = appState.getState(); + const savedDashboardPanelMap: { [key: string]: SavedDashboardPanel } = {}; + const { opensearchDashboardsVersion } = dashboardServices; + const input = dashboardContainer.getInput(); + appStateData.panels.forEach((savedDashboardPanel) => { + if (input.panels[savedDashboardPanel.panelIndex] !== undefined) { + savedDashboardPanelMap[savedDashboardPanel.panelIndex] = savedDashboardPanel; + } else { + // A panel was deleted. + dirty = true; + } + }); + const convertedPanelStateMap: { [key: string]: SavedDashboardPanel } = {}; + Object.values(input.panels).forEach((panelState) => { + if (savedDashboardPanelMap[panelState.explicitInput.id] === undefined) { + dirty = true; + } + convertedPanelStateMap[panelState.explicitInput.id] = convertPanelStateToSavedDashboardPanel( + panelState, + opensearchDashboardsVersion + ); + if ( + !_.isEqual( + convertedPanelStateMap[panelState.explicitInput.id], + savedDashboardPanelMap[panelState.explicitInput.id] + ) + ) { + // A panel was changed + dirty = true; + const oldVersion = savedDashboardPanelMap[panelState.explicitInput.id]?.version; + const newVersion = convertedPanelStateMap[panelState.explicitInput.id]?.version; + if (oldVersion && newVersion && oldVersion !== newVersion) { + dirtyBecauseOfInitialStateMigration = true; + } + } + }); + if (dirty) { + appState.transitions.set('panels', Object.values(convertedPanelStateMap)); + if (dirtyBecauseOfInitialStateMigration) { + // this.saveState({ replace: true }); + } + } + if (input.isFullScreenMode !== appStateData.fullScreenMode) { + appState.transitions.set('fullScreenMode', input.isFullScreenMode); + } + if (input.expandedPanelId !== appStateData.expandedPanelId) { + appState.transitions.set('expandedPanelId', input.expandedPanelId); + } + if (!_.isEqual(input.query, migrateLegacyQuery(appState.get().query))) { + appState.transitions.set('query', input.query); + } +}; diff --git a/src/plugins/dashboard/public/application/utils/use/use_editor_updates.ts b/src/plugins/dashboard/public/application/utils/use/use_editor_updates.ts index 60dfb9ba927a..7eba0217995b 100644 --- a/src/plugins/dashboard/public/application/utils/use/use_editor_updates.ts +++ b/src/plugins/dashboard/public/application/utils/use/use_editor_updates.ts @@ -33,7 +33,12 @@ export const useEditorUpdates = ( const unsubscribeStateUpdates = appState.subscribe((state) => { setCurrentAppState(state); - dashboardContainer.reload(); + if(dashboardContainer.getChangesFromAppStateForContainerState){ + const changes = dashboardContainer.getChangesFromAppStateForContainerState(dashboardContainer); + if(changes){ + dashboardContainer.updateInput(changes) + } + } }); return () => {