From 58054ebe13fa6508f5151e6e41f7f75de32df2a9 Mon Sep 17 00:00:00 2001 From: abbyhu2000 Date: Mon, 5 Jun 2023 21:51:02 +0000 Subject: [PATCH] Add visualization Add and save visualization to dashboard Signed-off-by: abbyhu2000 Signed-off-by: abbyhu2000 --- package.json | 5 +- .../components/dashboard_top_nav.tsx | 55 +++++-- .../embeddable/dashboard_container.tsx | 1 + .../utils/dashboard_embeddable_editor.tsx | 44 ----- .../application/utils/get_nav_actions.tsx | 10 -- .../utils/use/use_dashboard_container.tsx | 152 ++++++++++++++++-- .../utils/use/use_editor_updates.ts | 18 ++- src/plugins/dashboard/public/plugin.tsx | 3 +- 8 files changed, 197 insertions(+), 91 deletions(-) delete mode 100644 src/plugins/dashboard/public/application/utils/dashboard_embeddable_editor.tsx diff --git a/package.json b/package.json index 30e6756ca108..cde5d7834150 100644 --- a/package.json +++ b/package.json @@ -170,7 +170,6 @@ "dns-sync": "^0.2.1", "elastic-apm-node": "^3.43.0", "elasticsearch": "^16.7.0", - "http-aws-es": "npm:@zhongnansu/http-aws-es@6.0.1", "execa": "^4.0.2", "expiry-js": "0.1.7", "fast-deep-equal": "^3.1.1", @@ -181,6 +180,7 @@ "globby": "^11.1.0", "handlebars": "4.7.7", "hjson": "3.2.1", + "http-aws-es": "npm:@zhongnansu/http-aws-es@6.0.1", "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^5.0.0", "inline-style": "^2.0.0", @@ -254,6 +254,7 @@ "@osd/utility-types": "1.0.0", "@percy/cli": "^1.0.0", "@percy/sdk-utils": "^1.0.0", + "@rxjs-insights/devtools": "^0.5.0", "@testing-library/dom": "^8.11.3", "@testing-library/jest-dom": "^5.16.2", "@testing-library/react": "^12.1.2", @@ -287,6 +288,7 @@ "@types/has-ansi": "^3.0.0", "@types/history": "^4.7.3", "@types/hjson": "^2.4.2", + "@types/http-aws-es": "6.0.2", "@types/jest": "^27.4.0", "@types/joi": "^13.4.2", "@types/jquery": "^3.3.31", @@ -339,7 +341,6 @@ "@types/zen-observable": "^0.8.0", "@typescript-eslint/eslint-plugin": "^3.10.0", "@typescript-eslint/parser": "^3.10.0", - "@types/http-aws-es": "6.0.2", "angular-aria": "^1.8.0", "angular-mocks": "^1.8.2", "angular-recursion": "^1.0.5", 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..4d3f1f6431e7 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,34 @@ 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() + console.log("index pattern", indexPatterns) + }, [dashboardContainer, stateContainer, currentAppState, services.data.indexPatterns]) + const shouldShowFilterBar = (forceHide: boolean): boolean => !forceHide && (filters!.length > 0 || !currentAppState?.fullScreenMode); @@ -111,19 +147,6 @@ 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); - }*/ - }, []); - 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/dashboard_embeddable_editor.tsx b/src/plugins/dashboard/public/application/utils/dashboard_embeddable_editor.tsx deleted file mode 100644 index 2a39ac6f3717..000000000000 --- a/src/plugins/dashboard/public/application/utils/dashboard_embeddable_editor.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import { useEffect } from 'react'; - -function DashboardEmbeddableEditor({ - timeRange, - filters, - query, - dom, - savedDashboardInstance, - eventEmitter, - dashboardContainer, -}: any) { - useEffect(() => { - if (!dom) { - return; - } - - dashboardContainer.render(dom); - setTimeout(() => { - eventEmitter.emit('embeddableRendered'); - }); - - return () => dashboardContainer.destroy(); - }, [dashboardContainer, eventEmitter, dom]); - - useEffect(() => { - dashboardContainer.updateInput({ - timeRange, - filters, - query, - }); - }, [dashboardContainer, timeRange, filters, query]); - - return
; -} - -// default export required for React.Lazy -// eslint-disable-next-line import/no-default-export -export { DashboardEmbeddableEditor as default }; 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..d3ae06c4ba6a 100644 --- a/src/plugins/dashboard/public/application/utils/get_nav_actions.tsx +++ b/src/plugins/dashboard/public/application/utils/get_nav_actions.tsx @@ -79,12 +79,10 @@ export const getNavActions = ( const appState = stateContainer.getState(); navActions[TopNavIds.FULL_SCREEN] = () => { stateContainer.transitions.set('fullScreenMode', true); - // updateNavBar(); }; 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!'); const currentTitle = appState.title; const currentDescription = appState.description; const currentTimeRestore = appState.timeRestore; @@ -362,12 +360,9 @@ export const getNavActions = ( revertChangesAndExitEditMode(); } }); - - // updateNavBar(); } async function save(saveOptions: SavedObjectSaveOpts) { - console.log('in the save function!'); const timefilter = queryService.timefilter.timefilter; try { const id = await saveDashboard(timefilter, stateContainer, savedDashboard, saveOptions); @@ -381,11 +376,6 @@ export const getNavActions = ( 'data-test-subj': 'saveDashboardSuccess', }); - const appPath = `${createDashboardEditUrl(id)}`; - - // Manually insert a new url so the back button will open the saved visualization. - history.replace(appPath); - // setActiveUrl(appPath); chrome.docTitle.change(savedDashboard.lastSavedTitle); stateContainer.transitions.set('viewMode', ViewMode.VIEW); } 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 effb081d864b..cbecc407e19c 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, @@ -57,12 +71,30 @@ export const useDashboardContainer = ( }; getDashboardContainer(); - }, [savedDashboardInstance, appState]); + }, [savedDashboardInstance, appState, 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 }; }; -const createDashboardEmbeddable = async ( +const createDashboardEmbeddable = ( savedDash: any, dashboardServices: DashboardServices, appState: DashboardAppStateContainer @@ -141,6 +173,7 @@ const createDashboardEmbeddable = async ( const getDashboardInput = () => { const appStateData = appState.getState(); + const embeddablesMap: { [key: string]: DashboardPanelState; } = {}; @@ -153,7 +186,7 @@ const createDashboardEmbeddable = async ( id: savedDash.id || '', filters: data.query.filterManager.getFilters(), hidePanelTitles: appStateData.options.hidePanelTitles, - query: savedDash.query, + query: data.query.queryString.getQuery(), timeRange: data.query.timefilter.timefilter.getTime(), refreshConfig: data.query.timefilter.timefilter.getRefreshInterval(), viewMode: appStateData.viewMode, @@ -189,6 +222,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( @@ -214,11 +282,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. @@ -232,15 +299,16 @@ const createDashboardEmbeddable = async ( // Add filters modifies the object passed to it, hence the clone deep. filterManager.addFilters(_.cloneDeep(container.getInput().filters)); + // TODO: investigate if this is needed /* 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; } @@ -248,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..11dd0e4a45ac 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 @@ -17,7 +17,7 @@ export const useEditorUpdates = ( ) => { const [isEmbeddableRendered, setIsEmbeddableRendered] = useState(false); const [currentAppState, setCurrentAppState] = useState(); - const dom = document.getElementById('dashboardViewport'); + const dashboardDom = document.getElementById('dashboardViewport'); const { timefilter: { timefilter }, @@ -33,7 +33,14 @@ 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 () => { @@ -47,20 +54,19 @@ export const useEditorUpdates = ( services, dashboardContainer, isEmbeddableRendered, - currentAppState, ]); useEffect(() => { - if (!dom || !dashboardContainer) { + if (!dashboardDom || !dashboardContainer) { return; } - dashboardContainer.render(dom); + dashboardContainer.render(dashboardDom); setIsEmbeddableRendered(true); return () => { setIsEmbeddableRendered(false); }; - }, [appState, dashboardInstance, currentAppState, dashboardContainer, state$, dom]); + }, [dashboardContainer, dashboardDom]); return { isEmbeddableRendered, currentAppState }; }; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 5a5df77c1310..3bc8b45942a5 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -435,7 +435,8 @@ export class DashboardPlugin }, }; - initAngularBootstrap(); + // TODO: need to add UI bootstrap + // initAngularBootstrap(); core.application.register(app); urlForwarding.forwardApp(