From e46e54a85fac05684c4a052325331e4f7331b6ce Mon Sep 17 00:00:00 2001 From: Suchit Sahoo <38322563+LDrago27@users.noreply.github.com> Date: Tue, 23 Jul 2024 00:33:09 -0700 Subject: [PATCH] Move TopNavLinks to new Nav Bar Discover (#7326) This PR aims to introduce a new Nav bar in Discover that would contain the Date Picker as well as the Navigation links that are currently part of headers. The Navigation Links have been replaced with the corresponding Icons to provide a modern UX. These setting are currently controlled by query:enhancements:enabled Advanced Setting Flag and would be visible when the above flag is turned on. Signed-off-by: Suchit Sahoo --- src/core/public/application/types.ts | 2 + .../ui/query_editor/query_editor_top_row.tsx | 20 +++- .../query_string_input/query_bar_top_row.tsx | 10 +- .../ui/search_bar/create_search_bar.tsx | 1 + .../data/public/ui/search_bar/search_bar.tsx | 3 + .../__snapshots__/app_container.test.tsx.snap | 10 +- .../public/components/app_container.scss | 10 ++ .../public/components/app_container.tsx | 108 ++++++++++++------ .../public/components/constants.ts | 6 + src/plugins/discover/common/index.ts | 1 + .../components/top_nav/get_top_nav_links.tsx | 28 ++++- .../canvas/discover_canvas.scss | 6 + .../view_components/canvas/index.tsx | 12 +- .../view_components/canvas/top_nav.tsx | 69 ++++++++--- .../public/top_nav_menu/top_nav_menu.tsx | 1 + .../public/top_nav_menu/top_nav_menu_data.tsx | 1 - 16 files changed, 218 insertions(+), 70 deletions(-) create mode 100644 src/plugins/data_explorer/public/components/constants.ts diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 1e84ae309bda..7546b49620a4 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -539,6 +539,8 @@ export interface AppMountParameters { * Optional datasource id to pass while mounting app */ dataSourceId?: string; + + optionalRef?: Record>; } /** diff --git a/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx index 8304fdc252ee..a482d7416418 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx @@ -15,6 +15,7 @@ import { import classNames from 'classnames'; import { compact, isEqual } from 'lodash'; import React, { useState } from 'react'; +import { createPortal } from 'react-dom'; import { DataSource, IDataPluginServices, @@ -64,6 +65,7 @@ export interface QueryEditorTopRowProps { isDirty: boolean; timeHistory?: TimeHistoryContract; indicateNoData?: boolean; + datePickerRef?: React.RefObject; } // Needed for React.lazy @@ -294,7 +296,7 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { return ( - + {renderDatePicker()} {button} @@ -359,6 +361,14 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { 'osdQueryEditor--withDatePicker': props.showDatePicker, }); + const datePicker = ( + + + {renderUpdateButton()} + + + ); + return ( - - - {renderUpdateButton()} - - + {props?.datePickerRef?.current && uiSettings.get(UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED) + ? createPortal(datePicker, props.datePickerRef.current) + : datePicker} {renderQueryEditor()} diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx index 72b28830652b..3383df9d4e66 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx @@ -46,6 +46,7 @@ import { import { EuiSuperUpdateButton, OnRefreshProps, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@osd/i18n/react'; import { Toast } from 'src/core/public'; +import { createPortal } from 'react-dom'; import { IDataPluginServices, IIndexPattern, TimeRange, TimeHistoryContract, Query } from '../..'; import { useOpenSearchDashboards, @@ -83,6 +84,7 @@ export interface QueryBarTopRowProps { isDirty: boolean; timeHistory?: TimeHistoryContract; indicateNoData?: boolean; + datePickerRef?: React.RefObject; } // Needed for React.lazy @@ -262,7 +264,7 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) { return ( - + {renderDatePicker()} {button} @@ -393,7 +395,11 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) { > {renderQueryInput()} {renderSharingMetaFields()} - {renderUpdateButton()} + + {props?.datePickerRef?.current && uiSettings.get(UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED) + ? createPortal(renderUpdateButton(), props.datePickerRef.current) + : renderUpdateButton()} + ); diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index 244f4296216c..31f3401dc76f 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -237,6 +237,7 @@ export function createSearchBar({ onClearSavedQuery={defaultOnClearSavedQuery(props, clearSavedQuery)} onSavedQueryUpdated={defaultOnSavedQueryUpdated(props, setSavedQuery)} onSaved={defaultOnSavedQueryUpdated(props, setSavedQuery)} + datePickerRef={props.datePickerRef} {...overrideDefaultBehaviors(props)} /> diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index 11914f134443..b2ff6766e81c 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -77,6 +77,7 @@ export interface SearchBarOwnProps { refreshInterval?: number; dateRangeFrom?: string; dateRangeTo?: string; + datePickerRef?: React.RefObject; // Query bar - should be in SearchBarInjectedDeps query?: Query; settings?: Settings; @@ -482,6 +483,7 @@ class SearchBarUI extends Component { } dataTestSubj={this.props.dataTestSubj} indicateNoData={this.props.indicateNoData} + datePickerRef={this.props.datePickerRef} /> ); } @@ -518,6 +520,7 @@ class SearchBarUI extends Component { filterBar={filterBar} dataTestSubj={this.props.dataTestSubj} indicateNoData={this.props.indicateNoData} + datePickerRef={this.props.datePickerRef} /> ); } diff --git a/src/plugins/data_explorer/public/components/__snapshots__/app_container.test.tsx.snap b/src/plugins/data_explorer/public/components/__snapshots__/app_container.test.tsx.snap index 29b8e5ab54e9..6e79926aa727 100644 --- a/src/plugins/data_explorer/public/components/__snapshots__/app_container.test.tsx.snap +++ b/src/plugins/data_explorer/public/components/__snapshots__/app_container.test.tsx.snap @@ -3,10 +3,14 @@ exports[`DataExplorerApp should render the canvas and panel when selected 1`] = `
-
- Context +
+
+ Context +
diff --git a/src/plugins/data_explorer/public/components/app_container.scss b/src/plugins/data_explorer/public/components/app_container.scss index 7bd5ed6f69f6..07f070be3b17 100644 --- a/src/plugins/data_explorer/public/components/app_container.scss +++ b/src/plugins/data_explorer/public/components/app_container.scss @@ -20,3 +20,13 @@ $osdHeaderOffset: $euiHeaderHeightCompensation; .headerIsExpanded .deLayout { height: calc(100vh - #{$osdHeaderOffset * 2}); } + +.mainPage { + overflow-x: hidden; + overflow-y: auto; + + .navBar { + padding: $euiSizeXS $euiSizeXS $euiSizeXS $euiSizeM; + border-bottom: $euiBorderThin; + } +} diff --git a/src/plugins/data_explorer/public/components/app_container.tsx b/src/plugins/data_explorer/public/components/app_container.tsx index bf4a02bd223b..d334fb2ae0d3 100644 --- a/src/plugins/data_explorer/public/components/app_container.tsx +++ b/src/plugins/data_explorer/public/components/app_container.tsx @@ -3,8 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { memo } from 'react'; -import { EuiPage, EuiPageBody, EuiResizableContainer, useIsWithinBreakpoints } from '@elastic/eui'; +import React, { memo, useRef } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPage, + EuiPageBody, + EuiResizableContainer, + useIsWithinBreakpoints, +} from '@elastic/eui'; import { Suspense } from 'react'; import { AppMountParameters } from '../../../../core/public'; import { Sidebar } from './sidebar'; @@ -12,11 +19,19 @@ import { NoView } from './no_view'; import { View } from '../services/view_service/view'; import { shallowEqual } from '../utils/use/shallow_equal'; import './app_container.scss'; +import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; +import { IDataPluginServices } from '../../../data/public'; +import { QUERY_ENHANCEMENT_ENABLED_SETTING } from './constants'; export const AppContainer = React.memo( ({ view, params }: { view?: View; params: AppMountParameters }) => { const isMobile = useIsWithinBreakpoints(['xs', 's', 'm']); - // TODO: Make this more robust. + + const opensearchDashboards = useOpenSearchDashboards(); + const { uiSettings } = opensearchDashboards.services; + + const topLinkRef = useRef(null); + const datePickerRef = useRef(null); if (!view) { return ; } @@ -26,38 +41,67 @@ export const AppContainer = React.memo( const MemoizedPanel = memo(Panel); const MemoizedCanvas = memo(Canvas); + params.optionalRef = { + topLinkRef, + datePickerRef, + }; + // Render the application DOM. return ( - - {/* TODO: improve fallback state */} - Loading...
}> - - - {(EuiResizablePanel, EuiResizableButton) => ( - <> - - - - - - +
+ {uiSettings?.get(QUERY_ENHANCEMENT_ENABLED_SETTING) && ( + + +
+ + +
+ + + )} + + + {/* TODO: improve fallback state */} + Loading...
}> + + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + + + + + - - - - - - - )} - - - - + + + + + + + )} + + + + +
); }, (prevProps, nextProps) => { diff --git a/src/plugins/data_explorer/public/components/constants.ts b/src/plugins/data_explorer/public/components/constants.ts new file mode 100644 index 000000000000..85d0f9ec146b --- /dev/null +++ b/src/plugins/data_explorer/public/components/constants.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const QUERY_ENHANCEMENT_ENABLED_SETTING = 'query:enhancements:enabled'; diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts index 45887df880ae..b47e163a6c54 100644 --- a/src/plugins/discover/common/index.ts +++ b/src/plugins/discover/common/index.ts @@ -15,3 +15,4 @@ export const CONTEXT_DEFAULT_SIZE_SETTING = 'context:defaultSize'; export const CONTEXT_STEP_SETTING = 'context:step'; export const CONTEXT_TIE_BREAKER_FIELDS_SETTING = 'context:tieBreakerFields'; export const MODIFY_COLUMNS_ON_SWITCH = 'discover:modifyColumnsOnSwitch'; +export const QUERY_ENHANCEMENT_ENABLED_SETTING = 'query:enhancements:enabled'; diff --git a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.tsx b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.tsx index 592cc23afffc..3e0b00846930 100644 --- a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.tsx +++ b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.tsx @@ -29,7 +29,8 @@ import { OpenSearchPanel } from './open_search_panel'; export const getTopNavLinks = ( services: DiscoverViewServices, inspectorAdapters: Adapters, - savedSearch: SavedSearch + savedSearch: SavedSearch, + isEnhancementEnabled: boolean = false ) => { const { history, @@ -44,7 +45,7 @@ export const getTopNavLinks = ( osdUrlStateStorage, } = services; - const newSearch = { + const newSearch: TopNavMenuData = { id: 'new', label: i18n.translate('discover.localMenu.localMenu.newSearchTitle', { defaultMessage: 'New', @@ -61,6 +62,7 @@ export const getTopNavLinks = ( ariaLabel: i18n.translate('discover.topNav.discoverNewButtonLabel', { defaultMessage: `New Search`, }), + iconType: 'plusInCircle', }; const saveSearch: TopNavMenuData = { @@ -160,9 +162,10 @@ export const getTopNavLinks = ( ); showSaveModal(saveModal, core.i18n.Context); }, + iconType: 'save', }; - const openSearch = { + const openSearch: TopNavMenuData = { id: 'open', label: i18n.translate('discover.localMenu.openTitle', { defaultMessage: 'Open', @@ -190,6 +193,7 @@ export const getTopNavLinks = ( ) ); }, + iconType: 'folderOpen', }; const shareSearch: TopNavMenuData = { @@ -225,9 +229,10 @@ export const getTopNavLinks = ( isDirty: !savedSearch.id || state.isDirty || false, }); }, + iconType: 'share', }; - const inspectSearch = { + const inspectSearch: TopNavMenuData = { id: 'inspect', label: i18n.translate('discover.localMenu.inspectTitle', { defaultMessage: 'Inspect', @@ -244,15 +249,28 @@ export const getTopNavLinks = ( title: savedSearch?.title || undefined, }); }, + iconType: 'inspect', }; - return [ + const topNavLinksArray = [ newSearch, ...(capabilities.discover?.save ? [saveSearch] : []), openSearch, ...(share ? [shareSearch] : []), // Show share option only if share plugin is available inspectSearch, ]; + + if (!isEnhancementEnabled) { + return topNavLinksArray.map((topNavLink) => { + if (topNavLink) { + const { iconType, ...rest } = topNavLink; // Removing the Icon Type property to maintain consistency with older Nav Bar + return rest; + } + return topNavLink; + }); + } + + return topNavLinksArray; }; // TODO: This does not seem to affect the share menu. need to look into it in future diff --git a/src/plugins/discover/public/application/view_components/canvas/discover_canvas.scss b/src/plugins/discover/public/application/view_components/canvas/discover_canvas.scss index 2c2c8dfe8ebb..e0ab20a15296 100644 --- a/src/plugins/discover/public/application/view_components/canvas/discover_canvas.scss +++ b/src/plugins/discover/public/application/view_components/canvas/discover_canvas.scss @@ -45,3 +45,9 @@ } } } + +.topNav { + .hidden { + display: none; + } +} diff --git a/src/plugins/discover/public/application/view_components/canvas/index.tsx b/src/plugins/discover/public/application/view_components/canvas/index.tsx index 5c5fcb358600..54489824227e 100644 --- a/src/plugins/discover/public/application/view_components/canvas/index.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/index.tsx @@ -25,14 +25,18 @@ import { setColumns, useDispatch, useSelector } from '../../utils/state_manageme import { DiscoverViewServices } from '../../../build_services'; import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; import { filterColumns } from '../utils/filter_columns'; -import { DEFAULT_COLUMNS_SETTING, MODIFY_COLUMNS_ON_SWITCH } from '../../../../common'; +import { + DEFAULT_COLUMNS_SETTING, + MODIFY_COLUMNS_ON_SWITCH, + QUERY_ENHANCEMENT_ENABLED_SETTING, +} from '../../../../common'; import { OpenSearchSearchHit } from '../../../application/doc_views/doc_views_types'; import { buildColumns } from '../../utils/columns'; import './discover_canvas.scss'; import { getNewDiscoverSetting, setNewDiscoverSetting } from '../../components/utils/local_storage'; // eslint-disable-next-line import/no-default-export -export default function DiscoverCanvas({ setHeaderActionMenu, history }: ViewProps) { +export default function DiscoverCanvas({ setHeaderActionMenu, history, optionalRef }: ViewProps) { const panelRef = useRef(null); const { data$, refetch$, indexPattern } = useDiscoverContext(); const { @@ -46,6 +50,7 @@ export default function DiscoverCanvas({ setHeaderActionMenu, history }: ViewPro columns: stateColumns !== undefined ? stateColumns : buildColumns([]), }; }); + const isEnhancementsEnabled = uiSettings.get(QUERY_ENHANCEMENT_ENABLED_SETTING); const filteredColumns = filterColumns( columns, indexPattern, @@ -171,12 +176,15 @@ export default function DiscoverCanvas({ setHeaderActionMenu, history }: ViewPro className="dscCanvas" > + {fetchState.status === ResultStatus.NO_RESULTS && ( )} 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 adb4152cc40f..b6547e1b00a4 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 @@ -5,6 +5,8 @@ import React, { useEffect, useMemo, useState } from 'react'; import { Query, TimeRange } from 'src/plugins/data/common'; +import { createPortal } from 'react-dom'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { AppMountParameters } from '../../../../../../core/public'; import { connectStorageToQueryState, opensearchFilters } from '../../../../../data/public'; import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; @@ -15,16 +17,19 @@ import { getTopNavLinks } from '../../components/top_nav/get_top_nav_links'; import { getRootBreadcrumbs } from '../../helpers/breadcrumbs'; import { useDiscoverContext } from '../context'; import { useDispatch, setSavedQuery, useSelector } from '../../utils/state_management'; +import './discover_canvas.scss'; export interface TopNavProps { opts: { setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; onQuerySubmit: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; + optionalRef?: Record>; }; showSaveQuery: boolean; + isEnhancementsEnabled?: boolean; } -export const TopNav = ({ opts, showSaveQuery }: TopNavProps) => { +export const TopNav = ({ opts, showSaveQuery, isEnhancementsEnabled }: TopNavProps) => { const { services } = useOpenSearchDashboards(); const { inspectorAdapters, savedSearch, indexPattern } = useDiscoverContext(); const [indexPatterns, setIndexPatterns] = useState(undefined); @@ -43,7 +48,9 @@ export const TopNav = ({ opts, showSaveQuery }: TopNavProps) => { osdUrlStateStorage, } = services; - const topNavLinks = savedSearch ? getTopNavLinks(services, inspectorAdapters, savedSearch) : []; + const topNavLinks = savedSearch + ? getTopNavLinks(services, inspectorAdapters, savedSearch, isEnhancementsEnabled) + : []; connectStorageToQueryState(services.data.query, osdUrlStateStorage, { filters: opensearchFilters.FilterStateStore.APP_STATE, @@ -88,22 +95,46 @@ export const TopNav = ({ opts, showSaveQuery }: TopNavProps) => { }; return ( - + <> + {isEnhancementsEnabled && + !!opts?.optionalRef?.topLinkRef?.current && + createPortal( + + {topNavLinks.map((topNavLink) => ( + + + { + topNavLink.run(event.currentTarget); + }} + iconType={topNavLink.iconType} + aria-label={topNavLink.ariaLabel} + /> + + + ))} + , + opts.optionalRef.topLinkRef.current + )} + + ); }; diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx index c7aaa2763695..7bce0e01470d 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx @@ -55,6 +55,7 @@ export type TopNavMenuProps = StatefulSearchBarProps & showDataSourceMenu?: boolean; data?: DataPublicPluginStart; className?: string; + datePickerRef?: any; /** * If provided, the menu part of the component will be rendered as a portal inside the given mount point. * diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx index 322ed11d6390..c7a3220a896e 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx @@ -30,7 +30,6 @@ import { EuiButtonProps } from '@elastic/eui'; import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; -import { string } from 'mathjs'; export type TopNavMenuAction = (anchorElement: HTMLElement) => void;