From b3234ddd9f8ad01137ca8bdcc7ee5fd4314d7993 Mon Sep 17 00:00:00 2001 From: Suchit Sahoo <38322563+LDrago27@users.noreply.github.com> Date: Tue, 20 Aug 2024 16:56:07 -0700 Subject: [PATCH] Add Application Page Header for Visualize Pages (#7712) * Add Application Header for Visualize Signed-off-by: Suchit Sahoo * Fix Application Header Layout for Discover Signed-off-by: Suchit Sahoo * Changeset file for PR #7712 created/updated * Add new Page Header in Vis Builder Signed-off-by: Suchit Sahoo --------- Signed-off-by: Suchit Sahoo Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/7712.yml | 2 + .../dashboard_listing/dashboard_listing.tsx | 63 +- .../public/components/app_container.tsx | 12 +- .../view_components/canvas/top_nav.tsx | 2 +- .../table_list_view/table_list_view.tsx | 21 +- .../vis_builder/public/application/app.tsx | 13 + .../public/application/components/top_nav.tsx | 47 +- .../application/utils/get_top_nav_config.tsx | 176 ++++-- .../visualize/public/application/app.tsx | 1 + .../components/visualize_byvalue_editor.tsx | 12 + .../components/visualize_editor.tsx | 12 + .../components/visualize_listing.tsx | 97 +++- .../components/visualize_top_nav.tsx | 86 ++- .../public/application/utils/constants.ts | 18 + .../application/utils/get_top_nav_config.tsx | 540 ++++++++++++------ 15 files changed, 771 insertions(+), 331 deletions(-) create mode 100644 changelogs/fragments/7712.yml create mode 100644 src/plugins/visualize/public/application/utils/constants.ts diff --git a/changelogs/fragments/7712.yml b/changelogs/fragments/7712.yml new file mode 100644 index 000000000000..faba262bcc6b --- /dev/null +++ b/changelogs/fragments/7712.yml @@ -0,0 +1,2 @@ +feat: +- Add New Page Header to Visualize ([#7712](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7712)) \ No newline at end of file diff --git a/src/plugins/dashboard/public/application/components/dashboard_listing/dashboard_listing.tsx b/src/plugins/dashboard/public/application/components/dashboard_listing/dashboard_listing.tsx index 00ce5a713b7f..e145f397d4c0 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_listing/dashboard_listing.tsx +++ b/src/plugins/dashboard/public/application/components/dashboard_listing/dashboard_listing.tsx @@ -34,6 +34,7 @@ export const DashboardListing = () => { dashboardProviders, data: { query }, osdUrlStateStorage, + navigation, }, } = useOpenSearchDashboards(); @@ -41,6 +42,9 @@ export const DashboardListing = () => { const queryParameters = useMemo(() => new URLSearchParams(location.search), [location]); const initialFiltersFromURL = queryParameters.get('filter'); const [initialFilter, setInitialFilter] = useState(initialFiltersFromURL); + const showUpdatedUx = uiSettings?.get('home:useNewHomePage'); + const { HeaderControl } = navigation.ui; + const { setAppRightControls } = application; useEffect(() => { // syncs `_g` portion of url with query services @@ -201,31 +205,40 @@ export const DashboardListing = () => { ); }); + const createButton = ; + return ( - - } - findItems={find} - deleteItems={hideWriteControls ? undefined : deleteItems} - editItem={hideWriteControls ? undefined : editItem} - tableColumns={tableColumns} - listingLimit={listingLimit} - initialFilter={initialFilter ?? ''} - initialPageSize={initialPageSize} - noItemsFragment={noItemsFragment} - entityName={i18n.translate('dashboard.listing.table.entityName', { - defaultMessage: 'dashboard', - })} - entityNamePlural={i18n.translate('dashboard.listing.table.entityNamePlural', { - defaultMessage: 'dashboards', - })} - tableListTitle={i18n.translate('dashboard.listing.dashboardsTitle', { - defaultMessage: 'Dashboards', - })} - toastNotifications={notifications.toasts} - /> + <> + {showUpdatedUx && !hideWriteControls && ( + + )} + + ); }; diff --git a/src/plugins/data_explorer/public/components/app_container.tsx b/src/plugins/data_explorer/public/components/app_container.tsx index d3dc6a97ef0d..b8749bf914e2 100644 --- a/src/plugins/data_explorer/public/components/app_container.tsx +++ b/src/plugins/data_explorer/public/components/app_container.tsx @@ -31,6 +31,7 @@ export const AppContainer = React.memo( const opensearchDashboards = useOpenSearchDashboards(); const { uiSettings } = opensearchDashboards.services; const isEnhancementsEnabled = uiSettings?.get(QUERY_ENHANCEMENT_ENABLED_SETTING); + const showActionsInGroup = uiSettings?.get('home:useNewHomePage'); const topLinkRef = useRef(null); const datePickerRef = useRef(null); @@ -47,21 +48,22 @@ export const AppContainer = React.memo( topLinkRef, datePickerRef, }; - // Render the application DOM. return (
{isEnhancementsEnabled && ( - -
- + {!showActionsInGroup && ( + +
+ + )}
diff --git a/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx b/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx index fa3ee3524994..8c4f748ca060 100644 --- a/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx @@ -126,6 +126,7 @@ export const TopNav = ({ opts, showSaveQuery, isEnhancementsEnabled }: TopNavPro <> {isEnhancementsEnabled && !!opts?.optionalRef?.topLinkRef?.current && + !showActionsInGroup && createPortal( {topNavLinks.map((topNavLink) => ( @@ -145,7 +146,6 @@ export const TopNav = ({ opts, showSaveQuery, isEnhancementsEnabled }: TopNavPro opts.optionalRef.topLinkRef.current )} {this.state.showDeleteModal && this.renderConfirmDeleteModal()} - - - -

{this.props.tableListTitle}

-
-
- - {this.props.createButton || defaultCreateButton} -
+ {!this.props.showUpdatedUx && ( + + + +

{this.props.tableListTitle}

+
+
+ + {this.props.createButton || defaultCreateButton} +
+ )} diff --git a/src/plugins/vis_builder/public/application/app.tsx b/src/plugins/vis_builder/public/application/app.tsx index 9a3367651fc2..515567ddcc4c 100644 --- a/src/plugins/vis_builder/public/application/app.tsx +++ b/src/plugins/vis_builder/public/application/app.tsx @@ -15,6 +15,7 @@ import { RightNav } from './components/right_nav'; import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; import { VisBuilderServices } from '../types'; import { syncQueryStateWithUrl } from '../../../data/public'; +import { HeaderVariant } from '../../../../core/public/index'; import './app.scss'; @@ -23,9 +24,21 @@ export const VisBuilderApp = () => { services: { data: { query }, osdUrlStateStorage, + chrome, + uiSettings, }, } = useOpenSearchDashboards(); const { pathname } = useLocation(); + const { setHeaderVariant } = chrome; + const showActionsInGroup = uiSettings.get('home:useNewHomePage'); + + useEffect(() => { + if (showActionsInGroup) setHeaderVariant?.(HeaderVariant.APPLICATION); + + return () => { + setHeaderVariant?.(); + }; + }, [setHeaderVariant, showActionsInGroup]); useEffect(() => { // syncs `_g` portion of url with query services diff --git a/src/plugins/vis_builder/public/application/components/top_nav.tsx b/src/plugins/vis_builder/public/application/components/top_nav.tsx index 8361073bcd14..3e8ff11254f7 100644 --- a/src/plugins/vis_builder/public/application/components/top_nav.tsx +++ b/src/plugins/vis_builder/public/application/components/top_nav.tsx @@ -8,7 +8,7 @@ import { isEqual } from 'lodash'; import { useParams } from 'react-router-dom'; import { useUnmount } from 'react-use'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; -import { getTopNavConfig } from '../utils/get_top_nav_config'; +import { getLegacyTopNavConfig, getNavActions, getTopNavConfig } from '../utils/get_top_nav_config'; import { VisBuilderServices } from '../../types'; import './top_nav.scss'; @@ -18,7 +18,7 @@ import { setSavedQuery } from '../utils/state_management/visualization_slice'; import { setEditorState } from '../utils/state_management/metadata_slice'; import { useCanSave } from '../utils/use/use_can_save'; import { saveStateToSavedObject } from '../../saved_visualizations/transforms'; -import { TopNavMenuData } from '../../../../navigation/public'; +import { TopNavMenuData, TopNavMenuItemRenderType } from '../../../../navigation/public'; import { opensearchFilters, connectStorageToQueryState } from '../../../../data/public'; import { RootState } from '../../../../data_explorer/public'; @@ -41,11 +41,13 @@ export const TopNav = () => { navigation: { ui: { TopNavMenu }, }, + uiSettings, appName, capabilities, } = services; const rootState = useTypedSelector((state: RootState) => state); const dispatch = useTypedDispatch(); + const showActionsInGroup = uiSettings.get('home:useNewHomePage'); useDeepEffect(() => { dispatch(setEditorState({ state: 'dirty' })); @@ -67,7 +69,7 @@ export const TopNav = () => { const getConfig = () => { if (!savedVisBuilderVis || !indexPattern) return; - return getTopNavConfig( + const navActions = getNavActions( { visualizationIdFromUrl, savedVisBuilderVis: saveStateToSavedObject(savedVisBuilderVis, rootState, indexPattern), @@ -77,6 +79,38 @@ export const TopNav = () => { }, services ); + + return showActionsInGroup + ? getTopNavConfig( + { + visualizationIdFromUrl, + savedVisBuilderVis: saveStateToSavedObject( + savedVisBuilderVis, + rootState, + indexPattern + ), + saveDisabledReason, + dispatch, + originatingApp, + }, + services, + navActions + ) + : getLegacyTopNavConfig( + { + visualizationIdFromUrl, + savedVisBuilderVis: saveStateToSavedObject( + savedVisBuilderVis, + rootState, + indexPattern + ), + saveDisabledReason, + dispatch, + originatingApp, + }, + services, + navActions + ); }; setConfig(getConfig()); @@ -89,6 +123,7 @@ export const TopNav = () => { dispatch, indexPattern, originatingApp, + showActionsInGroup, ]); // reset validity before component destroyed @@ -109,11 +144,15 @@ export const TopNav = () => { setMenuMountPoint={setHeaderActionMenu} indexPatterns={indexPattern ? [indexPattern] : []} showDatePicker={!!indexPattern?.timeFieldName ?? true} - showSearchBar + showSearchBar={TopNavMenuItemRenderType.IN_PORTAL} showSaveQuery={showSaveQuery} useDefaultBehaviors savedQueryId={rootState.visualization.savedQuery} onSavedQueryIdChange={updateSavedQueryId} + groupActions={showActionsInGroup} + screenTitle={ + savedVisBuilderVis?.title.length ? savedVisBuilderVis?.title : 'New visualization' + } />
); diff --git a/src/plugins/vis_builder/public/application/utils/get_top_nav_config.tsx b/src/plugins/vis_builder/public/application/utils/get_top_nav_config.tsx index 945e09a09734..42f3e68c3898 100644 --- a/src/plugins/vis_builder/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/vis_builder/public/application/utils/get_top_nav_config.tsx @@ -48,8 +48,11 @@ export interface TopNavConfigParams { dispatch: AppDispatch; originatingApp?: string; } +interface VisBuilderNavActionMap { + [key: string]: (anchorElement?: any) => void; +} -export const getTopNavConfig = ( +export const getLegacyTopNavConfig = ( { visualizationIdFromUrl, savedVisBuilderVis, @@ -57,15 +60,9 @@ export const getTopNavConfig = ( dispatch, originatingApp, }: TopNavConfigParams, - services: VisBuilderServices + services: VisBuilderServices, + navActions: VisBuilderNavActionMap ) => { - const { - i18n: { Context: I18nContext }, - embeddable, - } = services; - - const stateTransfer = embeddable.getStateTransfer(); - const topNavConfig: TopNavMenuData[] = [ { id: 'save', @@ -86,26 +83,7 @@ export const getTopNavConfig = ( testId: 'visBuilderSaveButton', disableButton: !!saveDisabledReason, tooltip: saveDisabledReason, - run: (_anchorElement) => { - const saveModal = ( - {}} - originatingApp={originatingApp} - getAppNameFromId={stateTransfer.getAppNameFromId} - /> - ); - - showSaveModal(saveModal, I18nContext); - }, + run: navActions.save, }, ...(originatingApp && savedVisBuilderVis && savedVisBuilderVis.id ? [ @@ -125,25 +103,7 @@ export const getTopNavConfig = ( testId: 'visBuilderSaveAndReturnButton', disableButton: !!saveDisabledReason, tooltip: saveDisabledReason, - run: async () => { - const saveOptions = { - newTitle: savedVisBuilderVis.title, - newCopyOnSave: false, - isTitleDuplicateConfirmed: false, - newDescription: savedVisBuilderVis.description, - returnToOrigin: true, - }; - - const onSave = getOnSave( - savedVisBuilderVis, - originatingApp, - visualizationIdFromUrl, - dispatch, - services - ); - - return onSave(saveOptions); - }, + run: navActions.saveAndReturn, }, ] : []), @@ -152,6 +112,61 @@ export const getTopNavConfig = ( return topNavConfig; }; +export const getTopNavConfig = ( + { + visualizationIdFromUrl, + savedVisBuilderVis, + saveDisabledReason, + dispatch, + originatingApp, + }: TopNavConfigParams, + services: VisBuilderServices, + navActions: VisBuilderNavActionMap +) => { + const topNavMenu = [ + { + tooltip: !!saveDisabledReason + ? saveDisabledReason + : savedVisBuilderVis?.id && originatingApp + ? i18n.translate('visBuilder.topNavMenu.saveVisualizationAsButtonLabel', { + defaultMessage: 'save as', + }) + : i18n.translate('visBuilder.topNavMenu.saveVisualizationButtonLabel', { + defaultMessage: 'save', + }), + ariaLabel: i18n.translate('visBuilder.topNavMenu.saveVisualizationAsButtonLabel', { + defaultMessage: 'save', + }), + testId: 'visBuilderSaveButton', + run: navActions.save, + iconType: 'save', + controlType: 'icon', + disabled: !!saveDisabledReason, + }, + ...(originatingApp && savedVisBuilderVis && savedVisBuilderVis.id + ? [ + { + tooltip: i18n.translate('visualize.topNavMenu.openInspectorTooltip', { + defaultMessage: 'Save and return', + }), + ariaLabel: i18n.translate( + 'visBuilder.topNavMenu.saveAndReturnVisualizationButtonAriaLabel', + { + defaultMessage: 'Finish editing visBuilder and return to the last app', + } + ), + testId: 'visBuilderSaveAndReturnButton', + run: navActions.saveAndReturn, + iconType: 'checkInCircleFilled', + controlType: 'icon', + disabled: !!saveDisabledReason, + }, + ] + : []), + ]; + return topNavMenu as TopNavMenuData[]; +}; + export const getOnSave = ( savedVisBuilderVis, originatingApp, @@ -258,3 +273,68 @@ export const getOnSave = ( }; return onSave; }; + +export const getNavActions = ( + { + visualizationIdFromUrl, + savedVisBuilderVis, + saveDisabledReason, + dispatch, + originatingApp, + }: TopNavConfigParams, + services: VisBuilderServices +): VisBuilderNavActionMap => { + const { + i18n: { Context: I18nContext }, + embeddable, + } = services; + + const stateTransfer = embeddable.getStateTransfer(); + + const navActions: any = {}; + + const saveAndReturnNavAction = async () => { + const saveOptions = { + newTitle: savedVisBuilderVis.title, + newCopyOnSave: false, + isTitleDuplicateConfirmed: false, + newDescription: savedVisBuilderVis.description, + returnToOrigin: true, + }; + + const onSave = getOnSave( + savedVisBuilderVis, + originatingApp, + visualizationIdFromUrl, + dispatch, + services + ); + + return onSave(saveOptions); + }; + navActions.saveAndReturn = saveAndReturnNavAction; + + const saveNavAction = (_anchorElement) => { + const saveModal = ( + {}} + originatingApp={originatingApp} + getAppNameFromId={stateTransfer.getAppNameFromId} + /> + ); + + showSaveModal(saveModal, I18nContext); + }; + + navActions.save = saveNavAction; + return navActions; +}; diff --git a/src/plugins/visualize/public/application/app.tsx b/src/plugins/visualize/public/application/app.tsx index 552fd86508e1..476d8aba05d2 100644 --- a/src/plugins/visualize/public/application/app.tsx +++ b/src/plugins/visualize/public/application/app.tsx @@ -35,6 +35,7 @@ import { Route, Switch, useLocation } from 'react-router-dom'; import { AppMountParameters } from 'opensearch-dashboards/public'; import { syncQueryStateWithUrl } from '../../../data/public'; import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; +import { HeaderVariant } from '../../../../core/public/index'; import { VisualizeServices } from './types'; import { VisualizeEditor, diff --git a/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx index 93a15b043c77..af52b010ae8f 100644 --- a/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx @@ -44,6 +44,7 @@ import { import { VisualizeServices } from '../types'; import { VisualizeEditorCommon } from './visualize_editor_common'; import { VisualizeAppProps } from '../app'; +import { HeaderVariant } from '../../../../../core/public/index'; export const VisualizeByValueEditor = ({ onAppLeave }: VisualizeAppProps) => { const [originatingApp, setOriginatingApp] = useState(); @@ -52,6 +53,17 @@ export const VisualizeByValueEditor = ({ onAppLeave }: VisualizeAppProps) => { const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [embeddableId, setEmbeddableId] = useState(); const [valueInput, setValueInput] = useState(); + const { chrome, uiSettings } = services; + const showActionsInGroup = uiSettings.get('home:useNewHomePage'); + const { setHeaderVariant } = chrome; + + useEffect(() => { + if (showActionsInGroup) setHeaderVariant?.(HeaderVariant.APPLICATION); + + return () => { + setHeaderVariant?.(); + }; + }, [setHeaderVariant, showActionsInGroup]); useEffect(() => { const { originatingApp: value, embeddableId: embeddableIdValue, valueInput: valueInputValue } = diff --git a/src/plugins/visualize/public/application/components/visualize_editor.tsx b/src/plugins/visualize/public/application/components/visualize_editor.tsx index c7e2378a35d6..df7491fb6487 100644 --- a/src/plugins/visualize/public/application/components/visualize_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_editor.tsx @@ -44,6 +44,7 @@ import { import { VisualizeServices } from '../types'; import { VisualizeEditorCommon } from './visualize_editor_common'; import { VisualizeAppProps } from '../app'; +import { HeaderVariant } from '../../../../../core/public/index'; export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { const { id: visualizationIdFromUrl } = useParams<{ id: string }>(); @@ -51,6 +52,9 @@ export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { const { services } = useOpenSearchDashboards(); const [eventEmitter] = useState(new EventEmitter()); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(!visualizationIdFromUrl); + const { chrome, uiSettings } = services; + const showActionsInGroup = uiSettings.get('home:useNewHomePage'); + const { setHeaderVariant } = chrome; const isChromeVisible = useChromeVisibility(services.chrome); const { savedVisInstance, visEditorRef, visEditorController } = useSavedVisInstance( @@ -74,6 +78,14 @@ export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { ); useLinkedSearchUpdates(services, eventEmitter, appState, savedVisInstance); + useEffect(() => { + if (showActionsInGroup) setHeaderVariant?.(HeaderVariant.APPLICATION); + + return () => { + setHeaderVariant?.(); + }; + }, [setHeaderVariant, showActionsInGroup]); + useEffect(() => { const { originatingApp: value } = services.embeddable diff --git a/src/plugins/visualize/public/application/components/visualize_listing.tsx b/src/plugins/visualize/public/application/components/visualize_listing.tsx index 5dab2f11051c..ead43a7b08ca 100644 --- a/src/plugins/visualize/public/application/components/visualize_listing.tsx +++ b/src/plugins/visualize/public/application/components/visualize_listing.tsx @@ -44,6 +44,7 @@ import { VisualizeConstants } from '../visualize_constants'; import { getTableColumns, getNoItemsMessage } from '../utils'; import { getUiActions } from '../../services'; import { SAVED_OBJECT_DELETE_TRIGGER } from '../../../../saved_objects_management/public'; +import { HeaderVariant } from '../../../../../core/public/index'; export const VisualizeListing = () => { const { @@ -58,11 +59,15 @@ export const VisualizeListing = () => { savedObjectsPublic, uiSettings, visualizeCapabilities, + navigation, }, } = useOpenSearchDashboards(); const { pathname } = useLocation(); const closeNewVisModal = useRef(() => {}); const listingLimit = savedObjectsPublic.settings.getListingLimit(); + const showUpdatedUx = uiSettings?.get('home:useNewHomePage'); + const { HeaderControl } = navigation.ui; + const { setAppRightControls } = application; useEffect(() => { if (pathname === '/new') { @@ -80,13 +85,24 @@ export const VisualizeListing = () => { }, [history, pathname, visualizations]); useMount(() => { - chrome.setBreadcrumbs([ - { - text: i18n.translate('visualize.visualizeListingBreadcrumbsTitle', { - defaultMessage: 'Visualize', - }), - }, - ]); + if (showUpdatedUx) { + chrome.setBreadcrumbs([ + { + text: i18n.translate('visualize.visualizeListingBreadcrumbsTitle', { + defaultMessage: 'Visualizations', + }), + }, + ]); + } else { + chrome.setBreadcrumbs([ + { + text: i18n.translate('visualize.visualizeListingBreadcrumbsTitle', { + defaultMessage: 'Visualize', + }), + }, + ]); + } + chrome.docTitle.change( i18n.translate('visualize.listingPageTitle', { defaultMessage: 'Visualize' }) ); @@ -163,29 +179,48 @@ export const VisualizeListing = () => { ); return ( - + <> + {showUpdatedUx && ( + + )} + + ); }; 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 d0a1755f275e..1c3ba3bbb702 100644 --- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx +++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx @@ -40,8 +40,9 @@ import { VisualizeEditorVisInstance, } from '../types'; import { APP_NAME } from '../visualize_constants'; -import { getTopNavConfig } from '../utils'; +import { getTopNavConfig, getNavActions, getLegacyTopNavConfig } from '../utils'; import type { IndexPattern } from '../../../../data/public'; +import { TopNavMenuItemRenderType } from '../../../../navigation/public'; interface VisualizeTopNavProps { currentAppState: VisualizeAppState; @@ -92,26 +93,66 @@ const TopNav = ({ [visInstance.embeddableHandler] ); const stateTransfer = services.embeddable.getStateTransfer(); + const showActionsInGroup = services.uiSettings.get('home:useNewHomePage'); + const navActions = getNavActions( + { + hasUnsavedChanges, + setHasUnsavedChanges, + hasUnappliedChanges, + openInspector, + originatingApp, + setOriginatingApp, + visInstance, + stateContainer, + visualizationIdFromUrl, + stateTransfer, + embeddableId, + onAppLeave, + }, + services + ); const config = useMemo(() => { if (isEmbeddableRendered) { - return getTopNavConfig( - { - hasUnsavedChanges, - setHasUnsavedChanges, - hasUnappliedChanges, - openInspector, - originatingApp, - setOriginatingApp, - visInstance, - stateContainer, - visualizationIdFromUrl, - stateTransfer, - embeddableId, - onAppLeave, - }, - services - ); + if (showActionsInGroup) { + return getTopNavConfig( + { + hasUnsavedChanges, + setHasUnsavedChanges, + hasUnappliedChanges, + openInspector, + originatingApp, + setOriginatingApp, + visInstance, + stateContainer, + visualizationIdFromUrl, + stateTransfer, + embeddableId, + onAppLeave, + }, + services, + navActions + ); + } else { + return getLegacyTopNavConfig( + { + hasUnsavedChanges, + setHasUnsavedChanges, + hasUnappliedChanges, + openInspector, + originatingApp, + setOriginatingApp, + visInstance, + stateContainer, + visualizationIdFromUrl, + stateTransfer, + embeddableId, + onAppLeave, + }, + services, + navActions + ); + } } }, [ isEmbeddableRendered, @@ -128,6 +169,8 @@ const TopNav = ({ embeddableId, stateTransfer, onAppLeave, + showActionsInGroup, + navActions, ]); const [indexPatterns, setIndexPatterns] = useState( vis.data.indexPattern ? [vis.data.indexPattern] : [] @@ -216,14 +259,15 @@ const TopNav = ({ savedQueryId={currentAppState.savedQuery} onSavedQueryIdChange={stateContainer.transitions.updateSavedQuery} indexPatterns={indexPatterns} - screenTitle={vis.title} + screenTitle={vis.title.length ?? '' ? vis.title : 'New visualization'} showAutoRefreshOnly={!showDatePicker()} showDatePicker={showDatePicker()} showFilterBar={showFilterBar} showQueryInput={showQueryInput} showSaveQuery={services.visualizeCapabilities.saveQuery} - showSearchBar + showSearchBar={TopNavMenuItemRenderType.IN_PORTAL} useDefaultBehaviors + groupActions={showActionsInGroup} /> ) : showFilterBar ? ( /** @@ -234,7 +278,7 @@ const TopNav = ({ appName={APP_NAME} setMenuMountPoint={setHeaderActionMenu} indexPatterns={indexPatterns} - showSearchBar + showSearchBar={TopNavMenuItemRenderType.IN_PORTAL} showSaveQuery={false} showDatePicker={false} showQueryInput={false} diff --git a/src/plugins/visualize/public/application/utils/constants.ts b/src/plugins/visualize/public/application/utils/constants.ts new file mode 100644 index 000000000000..0a62b13aff61 --- /dev/null +++ b/src/plugins/visualize/public/application/utils/constants.ts @@ -0,0 +1,18 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +export const VisualizeTopNavIds = { + SHARE: 'share', + INSPECT: 'inspect', + SAVE: 'save', + SAVEANDRETURN: 'saveAndReturn', + CANCEL: 'cancel', +}; 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 562f96b872ba..cffd251b3eb4 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 @@ -50,6 +50,7 @@ import { import { VisualizeConstants } from '../visualize_constants'; import { getEditBreadcrumbs } from './breadcrumbs'; import { EmbeddableStateTransfer } from '../../../../embeddable/public'; +import { VisualizeTopNavIds } from './constants'; interface TopNavConfigParams { hasUnsavedChanges: boolean; @@ -66,7 +67,170 @@ interface TopNavConfigParams { onAppLeave: AppMountParameters['onAppLeave']; } -export const getTopNavConfig = ( +interface VisualizeNavActionMap { + [key: string]: (anchorElement?: any) => void; +} + +export const getLegacyTopNavConfig = ( + { + hasUnsavedChanges, + setHasUnsavedChanges, + openInspector, + originatingApp, + setOriginatingApp, + hasUnappliedChanges, + visInstance, + stateContainer, + visualizationIdFromUrl, + stateTransfer, + embeddableId, + onAppLeave, + }: TopNavConfigParams, + { + application, + chrome, + history, + share, + setActiveUrl, + toastNotifications, + visualizeCapabilities, + i18n: { Context: I18nContext }, + dashboard, + }: VisualizeServices, + navActions: VisualizeNavActionMap +) => { + const { vis, embeddableHandler } = visInstance; + const savedVis = 'savedVis' in visInstance ? visInstance.savedVis : undefined; + + const topNavMenu: TopNavMenuData[] = [ + { + id: 'inspector', + label: i18n.translate('visualize.topNavMenu.openInspectorButtonLabel', { + defaultMessage: 'inspect', + }), + description: i18n.translate('visualize.topNavMenu.openInspectorButtonAriaLabel', { + defaultMessage: 'Open Inspector for visualization', + }), + testId: 'openInspectorButton', + disableButton() { + return !embeddableHandler.hasInspector || !embeddableHandler.hasInspector(); + }, + run: navActions[VisualizeTopNavIds.INSPECT], + tooltip() { + if (!embeddableHandler.hasInspector || !embeddableHandler.hasInspector()) { + return i18n.translate('visualize.topNavMenu.openInspectorDisabledButtonTooltip', { + defaultMessage: `This visualization doesn't support any inspectors.`, + }); + } + }, + }, + { + id: 'share', + label: i18n.translate('visualize.topNavMenu.shareVisualizationButtonLabel', { + defaultMessage: 'share', + }), + description: i18n.translate('visualize.topNavMenu.shareVisualizationButtonAriaLabel', { + defaultMessage: 'Share Visualization', + }), + testId: 'shareTopNavButton', + run: navActions[VisualizeTopNavIds.SHARE], + // disable the Share button if no action specified + disableButton: !share || !!embeddableId, + }, + ...(originatingApp === 'dashboards' || originatingApp === 'canvas' + ? [ + { + id: 'cancel', + label: i18n.translate('visualize.topNavMenu.cancelButtonLabel', { + defaultMessage: 'Cancel', + }), + emphasize: false, + description: i18n.translate('visualize.topNavMenu.cancelButtonAriaLabel', { + defaultMessage: 'Return to the last app without saving changes', + }), + testId: 'visualizeCancelAndReturnButton', + tooltip() { + if (hasUnappliedChanges || hasUnsavedChanges) { + return i18n.translate('visualize.topNavMenu.cancelAndReturnButtonTooltip', { + defaultMessage: 'Discard your changes before finishing', + }); + } + }, + run: navActions[VisualizeTopNavIds.CANCEL], + }, + ] + : []), + ...(visualizeCapabilities.save && !embeddableId + ? [ + { + id: 'save', + iconType: savedVis?.id && originatingApp ? undefined : ('save' as const), + label: + savedVis?.id && originatingApp + ? i18n.translate('visualize.topNavMenu.saveVisualizationAsButtonLabel', { + defaultMessage: 'save as', + }) + : i18n.translate('visualize.topNavMenu.saveVisualizationButtonLabel', { + defaultMessage: 'save', + }), + emphasize: (savedVis && !savedVis.id) || !originatingApp, + description: i18n.translate('visualize.topNavMenu.saveVisualizationButtonAriaLabel', { + defaultMessage: 'Save Visualization', + }), + className: savedVis?.id && originatingApp ? 'saveAsButton' : '', + testId: 'visualizeSaveButton', + disableButton: hasUnappliedChanges, + tooltip() { + if (hasUnappliedChanges) { + return i18n.translate( + 'visualize.topNavMenu.saveVisualizationDisabledButtonTooltip', + { + defaultMessage: 'Apply or Discard your changes before saving', + } + ); + } + }, + run: navActions[VisualizeTopNavIds.SAVE], + }, + ] + : []), + ...(originatingApp && ((savedVis && savedVis.id) || embeddableId) + ? [ + { + id: 'saveAndReturn', + label: i18n.translate('visualize.topNavMenu.saveAndReturnVisualizationButtonLabel', { + defaultMessage: 'Save and return', + }), + emphasize: true, + iconType: 'checkInCircleFilled' as const, + description: i18n.translate( + 'visualize.topNavMenu.saveAndReturnVisualizationButtonAriaLabel', + { + defaultMessage: 'Finish editing visualization and return to the last app', + } + ), + testId: 'visualizesaveAndReturnButton', + disableButton: hasUnappliedChanges, + tooltip() { + if (hasUnappliedChanges) { + return i18n.translate( + 'visualize.topNavMenu.saveAndReturnVisualizationDisabledButtonTooltip', + { + defaultMessage: 'Apply or Discard your changes before finishing', + } + ); + } + }, + run: navActions[VisualizeTopNavIds.SAVEANDRETURN], + }, + ] + : []), + ]; + + return topNavMenu; +}; + +export const getNavActions = ( { hasUnsavedChanges, setHasUnsavedChanges, @@ -95,9 +259,7 @@ export const getTopNavConfig = ( ) => { 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 {}; @@ -194,218 +356,222 @@ export const getTopNavConfig = ( } }; - const topNavMenu: TopNavMenuData[] = [ + const navActions: VisualizeNavActionMap = {}; + + const saveAction = (anchorElement: HTMLElement) => { + const onSave = async ({ + newTitle, + newCopyOnSave, + isTitleDuplicateConfirmed, + onTitleDuplicate, + newDescription, + returnToOrigin, + }: OnSaveProps & { returnToOrigin: boolean }) => { + if (!savedVis) { + return; + } + const currentTitle = savedVis.title; + savedVis.title = newTitle; + embeddableHandler.updateInput({ title: newTitle }); + savedVis.copyOnSave = newCopyOnSave; + savedVis.description = newDescription; + const saveOptions = { + confirmOverwrite: false, + isTitleDuplicateConfirmed, + onTitleDuplicate, + returnToOrigin, + }; + const response = await doSave(saveOptions); + // If the save wasn't successful, put the original values back. + if (!response.id || response.error) { + savedVis.title = currentTitle; + } + return response; + }; + + const saveModal = ( + {}} + originatingApp={originatingApp} + /> + ); + const isSaveAsButton = anchorElement.classList.contains('saveAsButton'); + onAppLeave((actions) => { + return actions.default(); + }); + if ( + originatingApp === 'dashboards' && + dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables && + !isSaveAsButton + ) { + createVisReference(); + } else if (savedVis) { + showSaveModal(saveModal, I18nContext); + } + }; + navActions[VisualizeTopNavIds.SAVE] = saveAction; + + const saveAndReturnAction = async () => { + const saveOptions = { + confirmOverwrite: false, + returnToOrigin: true, + }; + onAppLeave((actions) => { + return actions.default(); + }); + if ( + originatingApp === 'dashboards' && + dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables && + !savedVis + ) { + return createVisReference(); + } + return doSave(saveOptions); + }; + navActions[VisualizeTopNavIds.SAVEANDRETURN] = saveAndReturnAction; + + const cancelAction = async () => { + return navigateToOriginatingApp(); + }; + navActions[VisualizeTopNavIds.CANCEL] = cancelAction; + + const shareAction = (anchorElement: HTMLElement) => { + 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, + objectType: 'visualization', + sharingData: { + title: savedVis?.title, + }, + isDirty: hasUnappliedChanges || hasUnsavedChanges, + }); + } + }; + navActions[VisualizeTopNavIds.SHARE] = shareAction; + + navActions[VisualizeTopNavIds.INSPECT] = openInspector; + + return navActions; +}; + +export const getTopNavConfig = ( + { + hasUnsavedChanges, + setHasUnsavedChanges, + openInspector, + originatingApp, + setOriginatingApp, + hasUnappliedChanges, + visInstance, + stateContainer, + visualizationIdFromUrl, + stateTransfer, + embeddableId, + onAppLeave, + }: TopNavConfigParams, + { + application, + chrome, + history, + share, + setActiveUrl, + toastNotifications, + visualizeCapabilities, + i18n: { Context: I18nContext }, + dashboard, + }: VisualizeServices, + navActions: VisualizeNavActionMap +) => { + const { vis, embeddableHandler } = visInstance; + const savedVis = 'savedVis' in visInstance ? visInstance.savedVis : undefined; + + const topNavMenu = [ + ...(visualizeCapabilities.save && !embeddableId + ? [ + { + tooltip: i18n.translate('visualize.topNavMenu.saveVisualizationButtonLabel', { + defaultMessage: 'Save', + }), + ariaLabel: i18n.translate('visualize.topNavMenu.saveVisualizationAriaLabel', { + defaultMessage: 'Save visualization', + }), + testId: 'visualizeSaveButton', + run: navActions[VisualizeTopNavIds.SAVE], + iconType: 'save', + controlType: 'icon', + disableButton: hasUnappliedChanges, + }, + ] + : []), { - id: 'inspector', - label: i18n.translate('visualize.topNavMenu.openInspectorButtonLabel', { - defaultMessage: 'inspect', + tooltip: i18n.translate('visualize.topNavMenu.openInspectorTooltip', { + defaultMessage: 'Inspect', }), - description: i18n.translate('visualize.topNavMenu.openInspectorButtonAriaLabel', { - defaultMessage: 'Open Inspector for visualization', + ariaLabel: i18n.translate('visualize.topNavMenu.openInspectorButtonLabel', { + defaultMessage: 'Inspect', }), testId: 'openInspectorButton', - disableButton() { - return !embeddableHandler.hasInspector || !embeddableHandler.hasInspector(); - }, - run: openInspector, - tooltip() { - if (!embeddableHandler.hasInspector || !embeddableHandler.hasInspector()) { - return i18n.translate('visualize.topNavMenu.openInspectorDisabledButtonTooltip', { - defaultMessage: `This visualization doesn't support any inspectors.`, - }); - } - }, + run: navActions[VisualizeTopNavIds.INSPECT], + iconType: 'inspect', + controlType: 'icon', + disabled: !embeddableHandler.hasInspector || !embeddableHandler.hasInspector(), }, { - id: 'share', - label: i18n.translate('visualize.topNavMenu.shareVisualizationButtonLabel', { - defaultMessage: 'share', + tooltip: i18n.translate('visualize.topNavMenu.shareVisualizationButtonLabel', { + defaultMessage: 'Share', }), - description: i18n.translate('visualize.topNavMenu.shareVisualizationButtonAriaLabel', { + ariaLabel: i18n.translate('visualize.topNavMenu.shareVisualizationButtonAriaLabel', { defaultMessage: 'Share Visualization', }), testId: 'shareTopNavButton', - run: (anchorElement) => { - 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, - objectType: 'visualization', - sharingData: { - title: savedVis?.title, - }, - isDirty: hasUnappliedChanges || hasUnsavedChanges, - }); - } - }, - // disable the Share button if no action specified - disableButton: !share || !!embeddableId, + run: navActions[VisualizeTopNavIds.SHARE], + iconType: 'share', + controlType: 'icon', + disabled: !share || !!embeddableId, }, ...(originatingApp === 'dashboards' || originatingApp === 'canvas' ? [ { - id: 'cancel', - label: i18n.translate('visualize.topNavMenu.cancelButtonLabel', { - defaultMessage: 'Cancel', + tooltip: i18n.translate('visualize.topNavMenu.cancelAndReturnButtonTooltip', { + defaultMessage: 'Discard your changes before finishing', }), - emphasize: false, - description: i18n.translate('visualize.topNavMenu.cancelButtonAriaLabel', { + ariaLabel: i18n.translate('visualize.topNavMenu.cancelButtonAriaLabel', { defaultMessage: 'Return to the last app without saving changes', }), testId: 'visualizeCancelAndReturnButton', - tooltip() { - if (hasUnappliedChanges || hasUnsavedChanges) { - return i18n.translate('visualize.topNavMenu.cancelAndReturnButtonTooltip', { - defaultMessage: 'Discard your changes before finishing', - }); - } - }, - run: async () => { - return navigateToOriginatingApp(); - }, - }, - ] - : []), - ...(visualizeCapabilities.save && !embeddableId - ? [ - { - id: 'save', - iconType: savedVis?.id && originatingApp ? undefined : ('save' as const), - label: - savedVis?.id && originatingApp - ? i18n.translate('visualize.topNavMenu.saveVisualizationAsButtonLabel', { - defaultMessage: 'save as', - }) - : i18n.translate('visualize.topNavMenu.saveVisualizationButtonLabel', { - defaultMessage: 'save', - }), - emphasize: (savedVis && !savedVis.id) || !originatingApp, - description: i18n.translate('visualize.topNavMenu.saveVisualizationButtonAriaLabel', { - defaultMessage: 'Save Visualization', - }), - className: savedVis?.id && originatingApp ? 'saveAsButton' : '', - testId: 'visualizeSaveButton', - disableButton: hasUnappliedChanges, - tooltip() { - if (hasUnappliedChanges) { - return i18n.translate( - 'visualize.topNavMenu.saveVisualizationDisabledButtonTooltip', - { - defaultMessage: 'Apply or Discard your changes before saving', - } - ); - } - }, - run: (anchorElement: HTMLElement) => { - const onSave = async ({ - newTitle, - newCopyOnSave, - isTitleDuplicateConfirmed, - onTitleDuplicate, - newDescription, - returnToOrigin, - }: OnSaveProps & { returnToOrigin: boolean }) => { - if (!savedVis) { - return; - } - const currentTitle = savedVis.title; - savedVis.title = newTitle; - embeddableHandler.updateInput({ title: newTitle }); - savedVis.copyOnSave = newCopyOnSave; - savedVis.description = newDescription; - const saveOptions = { - confirmOverwrite: false, - isTitleDuplicateConfirmed, - onTitleDuplicate, - returnToOrigin, - }; - const response = await doSave(saveOptions); - // If the save wasn't successful, put the original values back. - if (!response.id || response.error) { - savedVis.title = currentTitle; - } - return response; - }; - - const saveModal = ( - {}} - originatingApp={originatingApp} - /> - ); - const isSaveAsButton = anchorElement.classList.contains('saveAsButton'); - onAppLeave((actions) => { - return actions.default(); - }); - if ( - originatingApp === 'dashboards' && - dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables && - !isSaveAsButton - ) { - createVisReference(); - } else if (savedVis) { - showSaveModal(saveModal, I18nContext); - } - }, + run: navActions[VisualizeTopNavIds.CANCEL], + iconType: 'cross', + controlType: 'icon', }, ] : []), ...(originatingApp && ((savedVis && savedVis.id) || embeddableId) ? [ { - id: 'saveAndReturn', - label: i18n.translate('visualize.topNavMenu.saveAndReturnVisualizationButtonLabel', { + tooltip: i18n.translate('visualize.topNavMenu.saveAndReturnVisualizationButtonLabel', { defaultMessage: 'Save and return', }), - emphasize: true, - iconType: 'checkInCircleFilled' as const, - description: i18n.translate( - 'visualize.topNavMenu.saveAndReturnVisualizationButtonAriaLabel', - { - defaultMessage: 'Finish editing visualization and return to the last app', - } - ), + ariaLabel: hasUnappliedChanges + ? i18n.translate('visualize.topNavMenu.saveAndReturnVisualizationButtonAriaLabel', { + defaultMessage: 'Finish editing visualization and return to the last app', + }) + : '', testId: 'visualizesaveAndReturnButton', - disableButton: hasUnappliedChanges, - tooltip() { - if (hasUnappliedChanges) { - return i18n.translate( - 'visualize.topNavMenu.saveAndReturnVisualizationDisabledButtonTooltip', - { - defaultMessage: 'Apply or Discard your changes before finishing', - } - ); - } - }, - run: async () => { - const saveOptions = { - confirmOverwrite: false, - returnToOrigin: true, - }; - onAppLeave((actions) => { - return actions.default(); - }); - if ( - originatingApp === 'dashboards' && - dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables && - !savedVis - ) { - return createVisReference(); - } - return doSave(saveOptions); - }, + run: navActions[VisualizeTopNavIds.SAVEANDRETURN], + iconType: 'checkInCircleFilled', + controlType: 'icon', }, ] : []), ]; - - return topNavMenu; + return topNavMenu as TopNavMenuData[]; };