From 25d3de74f4452c40c05568ba6aebaddd2393a0dc Mon Sep 17 00:00:00 2001 From: Joanne Wang Date: Wed, 13 Nov 2024 10:33:12 -0800 Subject: [PATCH 01/18] Fix template queries loading and update getSampleQuery interface (#8848) * update sample query impl and fix saved query load Signed-off-by: Joanne Wang * Changeset file for PR #8848 created/updated * Changeset file for PR #8848 created/updated * add loading spinner Signed-off-by: Joanne Wang * check if tab is shown based on if sample query isTemplate Signed-off-by: Joanne Wang * added UTs for svaed queries flyout Signed-off-by: Riya Saxena --------- Signed-off-by: Joanne Wang Signed-off-by: Riya Saxena Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Co-authored-by: Riya Saxena --- changelogs/fragments/8848.yml | 2 + .../query_string/dataset_service/types.ts | 2 +- .../public/ui/filter_bar/filter_options.tsx | 4 +- .../open_saved_query_flyout.test.tsx | 228 ++++++++++++++++++ .../open_saved_query_flyout.tsx | 72 ++++-- .../saved_query_management_component.tsx | 5 +- .../ui/search_bar/create_search_bar.tsx | 1 + .../data/public/ui/search_bar/search_bar.tsx | 10 +- .../components/no_results/no_results.tsx | 57 +++-- 9 files changed, 342 insertions(+), 39 deletions(-) create mode 100644 changelogs/fragments/8848.yml create mode 100644 src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.test.tsx diff --git a/changelogs/fragments/8848.yml b/changelogs/fragments/8848.yml new file mode 100644 index 000000000000..f8cc51214cf5 --- /dev/null +++ b/changelogs/fragments/8848.yml @@ -0,0 +1,2 @@ +fix: +- Fix template queries loading and update getSampleQuery interface ([#8848](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8848)) \ No newline at end of file diff --git a/src/plugins/data/public/query/query_string/dataset_service/types.ts b/src/plugins/data/public/query/query_string/dataset_service/types.ts index f303fa6af56d..1e5af41ef785 100644 --- a/src/plugins/data/public/query/query_string/dataset_service/types.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/types.ts @@ -83,5 +83,5 @@ export interface DatasetTypeConfig { /** * Returns a list of sample queries for this dataset type */ - getSampleQueries?: (dataset: Dataset, language: string) => any; + getSampleQueries?: (dataset?: Dataset, language?: string) => Promise | any; } diff --git a/src/plugins/data/public/ui/filter_bar/filter_options.tsx b/src/plugins/data/public/ui/filter_bar/filter_options.tsx index 3cda39731fa7..4af53fa28df1 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_options.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_options.tsx @@ -59,7 +59,7 @@ import { import { FilterEditor } from './filter_editor'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { SavedQueryManagementComponent } from '../saved_query_management'; -import { SavedQuery, SavedQueryService } from '../../query'; +import { QueryStringManager, SavedQuery, SavedQueryService } from '../../query'; import { SavedQueryMeta } from '../saved_query_form'; import { getUseNewSavedQueriesUI } from '../../services'; @@ -79,6 +79,7 @@ interface Props { useSaveQueryMenu: boolean; isQueryEditorControl: boolean; saveQuery: (savedQueryMeta: SavedQueryMeta, saveAsNew?: boolean) => Promise; + queryStringManager: QueryStringManager; } const maxFilterWidth = 600; @@ -310,6 +311,7 @@ const FilterOptionsUI = (props: Props) => { key={'savedQueryManagement'} useNewSavedQueryUI={getUseNewSavedQueriesUI()} saveQuery={props.saveQuery} + queryStringManager={props.queryStringManager} />, ]} data-test-subj="save-query-panel" diff --git a/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.test.tsx b/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.test.tsx new file mode 100644 index 000000000000..8daaafe0fdcb --- /dev/null +++ b/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.test.tsx @@ -0,0 +1,228 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; +import { OpenSavedQueryFlyout } from './open_saved_query_flyout'; +import { createSavedQueryService } from '../../../public/query/saved_query/saved_query_service'; +import { applicationServiceMock, uiSettingsServiceMock } from '../../../../../core/public/mocks'; +import { SavedQueryAttributes } from '../../../public/query/saved_query/types'; +import '@testing-library/jest-dom'; +import { queryStringManagerMock } from '../../../../data/public/query/query_string/query_string_manager.mock'; + +const savedQueryAttributesWithTemplate: SavedQueryAttributes = { + title: 'foo', + description: 'bar', + query: { + language: 'kuery', + query: 'response:200', + dataset: 'my_dataset', + }, +}; + +const mockSavedObjectsClient = { + create: jest.fn(), + error: jest.fn(), + find: jest.fn(), + get: jest.fn(), + delete: jest.fn(), +}; + +mockSavedObjectsClient.create.mockReturnValue({ + id: 'foo', + attributes: { + ...savedQueryAttributesWithTemplate, + query: { + ...savedQueryAttributesWithTemplate.query, + }, + }, +}); + +jest.mock('./saved_query_card', () => ({ + SavedQueryCard: ({ + savedQuery = { + id: 'foo1', + attributes: savedQueryAttributesWithTemplate, + }, + onSelect, + handleQueryDelete, + }) => ( +
+
{savedQuery?.attributes?.title}
+ + +
+ ), +})); + +jest.mock('@osd/i18n', () => ({ + i18n: { + translate: jest.fn((id, { defaultMessage }) => defaultMessage), + }, +})); + +const mockSavedQueryService = createSavedQueryService( + // @ts-ignore + mockSavedObjectsClient, + { + application: applicationServiceMock.create(), + uiSettings: uiSettingsServiceMock.createStartContract(), + } +); + +const mockHandleQueryDelete = jest.fn(); +const mockOnQueryOpen = jest.fn(); +const mockOnClose = jest.fn(); + +const savedQueries = [ + { + id: '1', + attributes: { + title: 'Saved Query 1', + description: 'Description for Query 1', + query: { query: 'SELECT * FROM table1', language: 'sql' }, + }, + }, + { + id: '2', + attributes: { + title: 'Saved Query 2', + description: 'Description for Query 2', + query: { query: 'SELECT * FROM table2', language: 'sql' }, + }, + }, +]; + +jest.spyOn(mockSavedQueryService, 'getAllSavedQueries').mockResolvedValue(savedQueries); + +describe('OpenSavedQueryFlyout', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render the flyout with correct tabs and content', async () => { + render( + + ); + + const savedQueriesTextElements = screen.getAllByText('Saved queries'); + + expect(savedQueriesTextElements).toHaveLength(2); + + await waitFor(() => screen.getByPlaceholderText('Search')); + + await waitFor(() => screen.getByText('Saved Query 1')); + await waitFor(() => screen.getByText('Saved Query 2')); + + const openQueryButton = screen.getByText('Open query'); + + fireEvent.change(screen.getByPlaceholderText('Search'), { target: { value: 'Saved Query 1' } }); + + await waitFor(() => screen.getByText('Saved Query 1')); + expect(screen.queryByText('Saved Query 2')).not.toBeInTheDocument(); + + fireEvent.click(screen.getByText('Saved Query 1')); + + expect(openQueryButton).toBeEnabled(); + }); + + it('should filter saved queries based on search input', async () => { + render( + + ); + + await waitFor(() => screen.getByText('Saved Query 1')); + await waitFor(() => screen.getByText('Saved Query 2')); + + const searchBar = screen.getByPlaceholderText('Search'); + fireEvent.change(searchBar, { target: { value: 'Saved Query 1' } }); + + expect(screen.getByText('Saved Query 1')).toBeInTheDocument(); + expect(screen.queryByText('Saved Query 2')).toBeNull(); + }); + + it('should select a query when clicking on it and enable the "Open query" button', async () => { + render( + + ); + + await waitFor(() => screen.getByText('Saved Query 1')); + + fireEvent.click(screen.getByText('Saved Query 1')); + + expect(screen.getByText('Open query')).toBeEnabled(); + }); + + it('should call handleQueryDelete when deleting a query', async () => { + mockHandleQueryDelete.mockResolvedValueOnce(); + render( + + ); + + await waitFor(() => screen.getByText('Saved Query 1')); + + const deleteButtons = screen.getAllByText('Delete'); + + fireEvent.click(deleteButtons[0]); + + await waitFor(() => { + expect(mockHandleQueryDelete).toHaveBeenCalledWith({ + id: '1', + attributes: { + description: 'Description for Query 1', + query: { + language: 'sql', + query: 'SELECT * FROM table1', + }, + title: 'Saved Query 1', + }, + }); + }); + expect(mockHandleQueryDelete).toHaveBeenCalledTimes(1); + }); + + it('should handle pagination controls correctly', async () => { + render( + + ); + + await waitFor(() => screen.getByText('Saved Query 1')); + + const pageSizeButton = await screen.findByText(/10/); + fireEvent.click(pageSizeButton); + + expect(mockSavedQueryService.getAllSavedQueries).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx b/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx index c7f13f27db08..41aa344bbaef 100644 --- a/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx +++ b/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx @@ -13,6 +13,7 @@ import { EuiFlyoutBody, EuiFlyoutFooter, EuiFlyoutHeader, + EuiLoadingSpinner, EuiSearchBar, EuiSearchBarProps, EuiSpacer, @@ -23,14 +24,16 @@ import { } from '@elastic/eui'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { i18n } from '@osd/i18n'; -import { SavedQuery, SavedQueryService } from '../../query'; +import { QueryStringManager, SavedQuery, SavedQueryService } from '../../query'; import { SavedQueryCard } from './saved_query_card'; +import { Query } from '../../../common'; export interface OpenSavedQueryFlyoutProps { savedQueryService: SavedQueryService; onClose: () => void; onQueryOpen: (query: SavedQuery) => void; handleQueryDelete: (query: SavedQuery) => Promise; + queryStringManager: QueryStringManager; } interface SavedQuerySearchableItem { @@ -47,6 +50,7 @@ export function OpenSavedQueryFlyout({ onClose, onQueryOpen, handleQueryDelete, + queryStringManager, }: OpenSavedQueryFlyoutProps) { const [selectedTabId, setSelectedTabId] = useState('mutable-saved-queries'); const [savedQueries, setSavedQueries] = useState([]); @@ -59,18 +63,39 @@ export function OpenSavedQueryFlyout({ const [languageFilterOptions, setLanguageFilterOptions] = useState([]); const [selectedQuery, setSelectedQuery] = useState(undefined); const [searchQuery, setSearchQuery] = useState(EuiSearchBar.Query.MATCH_ALL); + const [isLoading, setIsLoading] = useState(false); + const currentTabIdRef = useRef(selectedTabId); const fetchAllSavedQueriesForSelectedTab = useCallback(async () => { - const allQueries = await savedQueryService.getAllSavedQueries(); - const templateQueriesPresent = allQueries.some((q) => q.attributes.isTemplate); - const queriesForSelectedTab = allQueries.filter( - (q) => - (selectedTabId === 'mutable-saved-queries' && !q.attributes.isTemplate) || - (selectedTabId === 'template-saved-queries' && q.attributes.isTemplate) - ); - setSavedQueries(queriesForSelectedTab); - setHasTemplateQueries(templateQueriesPresent); - }, [savedQueryService, selectedTabId, setSavedQueries]); + setIsLoading(true); + const query = queryStringManager.getQuery(); + let templateQueries: any[] = []; + + // fetch sample query based on dataset type + if (query?.dataset?.type) { + templateQueries = + (await queryStringManager + .getDatasetService() + ?.getType(query.dataset.type) + ?.getSampleQueries?.()) || []; + + // Check if any sample query has isTemplate set to true + const hasTemplates = templateQueries.some((q) => q?.attributes?.isTemplate); + setHasTemplateQueries(hasTemplates); + } + + // Set queries based on the current tab + if (currentTabIdRef.current === 'mutable-saved-queries') { + const allQueries = await savedQueryService.getAllSavedQueries(); + const mutableSavedQueries = allQueries.filter((q) => !q.attributes.isTemplate); + if (currentTabIdRef.current === 'mutable-saved-queries') { + setSavedQueries(mutableSavedQueries); + } + } else if (currentTabIdRef.current === 'template-saved-queries') { + setSavedQueries(templateQueries); + } + setIsLoading(false); + }, [savedQueryService, currentTabIdRef, setSavedQueries, queryStringManager]); const updatePageIndex = useCallback((index: number) => { pager.current.goToPageIndex(index); @@ -179,7 +204,13 @@ export function OpenSavedQueryFlyout({ onChange={onChange} /> - {queriesOnCurrentPage.length > 0 ? ( + {isLoading ? ( + + + + + + ) : queriesOnCurrentPage.length > 0 ? ( queriesOnCurrentPage.map((query) => ( )} - {queriesOnCurrentPage.length > 0 && ( + {!isLoading && queriesOnCurrentPage.length > 0 && ( { setSelectedTabId(tab.id); + currentTabIdRef.current = tab.id; }} /> @@ -268,7 +300,19 @@ export function OpenSavedQueryFlyout({ fill onClick={() => { if (selectedQuery) { - onQueryOpen(selectedQuery); + if ( + // Template queries are not associated with data sources. Apply data source from current query + selectedQuery.attributes.isTemplate + ) { + const updatedQuery: Query = { + ...queryStringManager?.getQuery(), + query: selectedQuery.attributes.query.query, + language: selectedQuery.attributes.query.language, + }; + queryStringManager.setQuery(updatedQuery); + } else { + onQueryOpen(selectedQuery); + } onClose(); } }} diff --git a/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx b/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx index 01f9b97e978f..44c5ef384966 100644 --- a/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx +++ b/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx @@ -45,7 +45,7 @@ import { import { i18n } from '@osd/i18n'; import React, { useCallback, useEffect, useState, Fragment, useRef } from 'react'; import { sortBy } from 'lodash'; -import { SavedQuery, SavedQueryService } from '../..'; +import { QueryStringManager, SavedQuery, SavedQueryService } from '../..'; import { SavedQueryListItem } from './saved_query_list_item'; import { toMountPoint, @@ -70,6 +70,7 @@ interface Props { onClearSavedQuery: () => void; closeMenuPopover: () => void; saveQuery: (savedQueryMeta: SavedQueryMeta, saveAsNew?: boolean) => Promise; + queryStringManager: QueryStringManager; } export function SavedQueryManagementComponent({ @@ -83,6 +84,7 @@ export function SavedQueryManagementComponent({ closeMenuPopover, useNewSavedQueryUI, saveQuery, + queryStringManager, }: Props) { const [savedQueries, setSavedQueries] = useState([] as SavedQuery[]); const [count, setTotalCount] = useState(0); @@ -256,6 +258,7 @@ export function SavedQueryManagementComponent({ onClose={() => openSavedQueryFlyout?.close().then()} onQueryOpen={onLoad} handleQueryDelete={handleDelete} + queryStringManager={queryStringManager} /> ) ); 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 f8b9694caabc..d3f89d0f559d 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 @@ -202,6 +202,7 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) isRefreshPaused={refreshInterval.pause} filters={filters} query={query} + queryStringManager={data.query.queryString} onFiltersUpdated={defaultFiltersUpdated(data.query)} onRefreshChange={defaultOnRefreshChange(data.query)} savedQuery={savedQuery} 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 1f1b20b8c952..3cd6cdcca25e 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -38,7 +38,13 @@ import { withOpenSearchDashboards, } from '../../../../opensearch_dashboards_react/public'; import { Filter, IIndexPattern, Query, TimeRange, UI_SETTINGS } from '../../../common'; -import { SavedQuery, SavedQueryAttributes, TimeHistoryContract, QueryStatus } from '../../query'; +import { + SavedQuery, + SavedQueryAttributes, + TimeHistoryContract, + QueryStatus, + QueryStringManager, +} from '../../query'; import { IDataPluginServices } from '../../types'; import { FilterBar } from '../filter_bar/filter_bar'; import { QueryEditorTopRow } from '../query_editor'; @@ -95,6 +101,7 @@ export interface SearchBarOwnProps { onRefresh?: (payload: { dateRange: TimeRange }) => void; indicateNoData?: boolean; queryStatus?: QueryStatus; + queryStringManager: QueryStringManager; } export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps; @@ -467,6 +474,7 @@ class SearchBarUI extends Component { useSaveQueryMenu={useSaveQueryMenu} isQueryEditorControl={isQueryEditorControl} saveQuery={this.onSave} + queryStringManager={this.props.queryStringManager} /> ) ); diff --git a/src/plugins/discover/public/application/components/no_results/no_results.tsx b/src/plugins/discover/public/application/components/no_results/no_results.tsx index b1ec382b4fd5..24a4b80c7204 100644 --- a/src/plugins/discover/public/application/components/no_results/no_results.tsx +++ b/src/plugins/discover/public/application/components/no_results/no_results.tsx @@ -174,6 +174,7 @@ export const DiscoverNoResults = ({ queryString, query, savedQuery, timeFieldNam // } const [savedQueries, setSavedQueries] = useState([]); + const [sampleQueries, setSampleQueries] = useState([]); useEffect(() => { const fetchSavedQueries = async () => { @@ -186,6 +187,39 @@ export const DiscoverNoResults = ({ queryString, query, savedQuery, timeFieldNam fetchSavedQueries(); }, [setSavedQueries, query, savedQuery]); + useEffect(() => { + // Samples for the language + const newSampleQueries: any = []; + if (query?.language) { + const languageSampleQueries = queryString.getLanguageService()?.getLanguage(query.language) + ?.sampleQueries; + if (Array.isArray(languageSampleQueries)) { + newSampleQueries.push(...languageSampleQueries); + } + } + + // Samples for the dataset type + if (query?.dataset?.type) { + const datasetType = queryString.getDatasetService()?.getType(query.dataset.type); + if (datasetType?.getSampleQueries) { + const sampleQueriesResponse = datasetType.getSampleQueries(query.dataset, query.language); + if (Array.isArray(sampleQueriesResponse)) { + setSampleQueries([...sampleQueriesResponse, ...newSampleQueries]); + } else if (sampleQueriesResponse instanceof Promise) { + sampleQueriesResponse + .then((datasetSampleQueries: any) => { + if (Array.isArray(datasetSampleQueries)) { + setSampleQueries([...datasetSampleQueries, ...newSampleQueries]); + } + }) + .catch((error: any) => { + // noop + }); + } + } + } + }, [queryString, query]); + const tabs = useMemo(() => { const buildSampleQueryBlock = (sampleTitle: string, sampleQuery: string) => { return ( @@ -197,25 +231,6 @@ export const DiscoverNoResults = ({ queryString, query, savedQuery, timeFieldNam ); }; - - const sampleQueries = []; - - // Samples for the dataset type - if (query?.dataset?.type) { - const datasetSampleQueries = queryString - .getDatasetService() - ?.getType(query.dataset.type) - ?.getSampleQueries?.(query.dataset, query.language); - if (Array.isArray(datasetSampleQueries)) sampleQueries.push(...datasetSampleQueries); - } - - // Samples for the language - if (query?.language) { - const languageSampleQueries = queryString.getLanguageService()?.getLanguage(query.language) - ?.sampleQueries; - if (Array.isArray(languageSampleQueries)) sampleQueries.push(...languageSampleQueries); - } - return [ ...(sampleQueries.length > 0 ? [ @@ -229,7 +244,7 @@ export const DiscoverNoResults = ({ queryString, query, savedQuery, timeFieldNam {sampleQueries .slice(0, 5) - .map((sampleQuery) => + .map((sampleQuery: any) => buildSampleQueryBlock(sampleQuery.title, sampleQuery.query) )} @@ -256,7 +271,7 @@ export const DiscoverNoResults = ({ queryString, query, savedQuery, timeFieldNam ] : []), ]; - }, [queryString, query, savedQueries]); + }, [savedQueries, sampleQueries]); return ( From c20c041627312e0239cf994eed3236a2340f790f Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Wed, 13 Nov 2024 16:09:02 -0800 Subject: [PATCH 02/18] [Discover] Fix discover query loading state (#8832) * [Discover] Fix loading status when uninitialized Signed-off-by: Joshua Li * fix discover loading state Signed-off-by: Joshua Li --------- Signed-off-by: Joshua Li --- .../lib/query_result.test.tsx | 11 +++++++ .../language_service/lib/query_result.tsx | 21 ++---------- .../view_components/canvas/index.tsx | 32 +++++++++---------- .../view_components/utils/use_search.test.tsx | 22 ++++++++----- .../view_components/utils/use_search.ts | 12 +++---- 5 files changed, 48 insertions(+), 50 deletions(-) diff --git a/src/plugins/data/public/query/query_string/language_service/lib/query_result.test.tsx b/src/plugins/data/public/query/query_string/language_service/lib/query_result.test.tsx index 9e735cd02d64..ea464f5ec68f 100644 --- a/src/plugins/data/public/query/query_string/language_service/lib/query_result.test.tsx +++ b/src/plugins/data/public/query/query_string/language_service/lib/query_result.test.tsx @@ -27,6 +27,17 @@ describe('Query Result', () => { expect(component).toMatchSnapshot(); }); + it('should not render if status is uninitialized', () => { + const props = { + queryStatus: { + status: ResultStatus.UNINITIALIZED, + startTime: Number.NEGATIVE_INFINITY, + }, + }; + const component = shallowWithIntl(); + expect(component.isEmptyRender()).toBe(true); + }); + it('shows ready status with complete message', () => { const props = { queryStatus: { diff --git a/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx b/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx index 2e8ab769e2e4..5378cf8a111c 100644 --- a/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx +++ b/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx @@ -58,24 +58,7 @@ export function QueryResult(props: { queryStatus: QueryStatus }) { }; }, [props.queryStatus.startTime]); - if (elapsedTime > BUFFER_TIME) { - if (props.queryStatus.status === ResultStatus.LOADING) { - const time = Math.floor(elapsedTime / 1000); - return ( - {}} - isLoading - data-test-subj="queryResultLoading" - > - {i18n.translate('data.query.languageService.queryResults.loadTime', { - defaultMessage: 'Loading {time} s', - values: { time }, - })} - - ); - } + if (elapsedTime > BUFFER_TIME && props.queryStatus.status === ResultStatus.LOADING) { const time = Math.floor(elapsedTime / 1000); return ( { const subscription = data$.subscribe((next) => { - if (next.status === ResultStatus.LOADING) return; - let shouldUpdateState = false; if (next.status !== fetchState.status) shouldUpdateState = true; @@ -86,7 +84,8 @@ export default function DiscoverCanvas({ setHeaderActionMenu, history, optionalR if (next.bucketInterval && next.bucketInterval !== fetchState.bucketInterval) shouldUpdateState = true; if (next.chartData && next.chartData !== fetchState.chartData) shouldUpdateState = true; - if (next.rows && next.rows !== fetchState.rows) { + // we still want to show rows from the previous query while current query is loading + if (next.status !== ResultStatus.LOADING && next.rows && next.rows !== fetchState.rows) { shouldUpdateState = true; setRows(next.rows); } @@ -164,19 +163,20 @@ export default function DiscoverCanvas({ setHeaderActionMenu, history, optionalR {fetchState.status === ResultStatus.UNINITIALIZED && ( refetch$.next()} /> )} - {fetchState.status === ResultStatus.LOADING && } - {fetchState.status === ResultStatus.READY && isEnhancementsEnabled && ( - <> - - - - )} - {fetchState.status === ResultStatus.READY && !isEnhancementsEnabled && ( - - - - - )} + {fetchState.status === ResultStatus.LOADING && !rows?.length && } + {(fetchState.status === ResultStatus.READY || + (fetchState.status === ResultStatus.LOADING && !!rows?.length)) && + (isEnhancementsEnabled ? ( + <> + + + + ) : ( + + + + + ))} ) : ( <> diff --git a/src/plugins/discover/public/application/view_components/utils/use_search.test.tsx b/src/plugins/discover/public/application/view_components/utils/use_search.test.tsx index 7f0d95296373..4a92bb5d37be 100644 --- a/src/plugins/discover/public/application/view_components/utils/use_search.test.tsx +++ b/src/plugins/discover/public/application/view_components/utils/use_search.test.tsx @@ -139,23 +139,29 @@ describe('useSearch', () => { wrapper, }); + act(() => { + mockDatasetUpdates$.next({ + dataset: { id: 'new-dataset-id', title: 'New Dataset', type: 'INDEX_PATTERN' }, + }); + }); + act(() => { result.current.data$.next({ status: ResultStatus.READY }); }); + act(() => { + mockDatasetUpdates$.next({ + dataset: { id: 'new-dataset-id', title: 'New Dataset', type: 'INDEX_PATTERN' }, + }); + }); + expect(result.current.data$.getValue()).toEqual( expect.objectContaining({ status: ResultStatus.READY }) ); act(() => { mockDatasetUpdates$.next({ - dataset: { id: 'new-dataset-id', title: 'New Dataset', type: 'INDEX_PATTERN' }, - }); - mockDatasetUpdates$.next({ - dataset: { id: 'new-dataset-id', title: 'New Dataset', type: 'INDEX_PATTERN' }, - }); - mockDatasetUpdates$.next({ - dataset: { id: 'new-dataset-id2', title: 'New Dataset', type: 'INDEX_PATTERN' }, + dataset: { id: 'different-dataset-id', title: 'New Dataset', type: 'INDEX_PATTERN' }, }); }); @@ -164,7 +170,7 @@ describe('useSearch', () => { }); expect(result.current.data$.getValue()).toEqual( - expect.objectContaining({ status: ResultStatus.LOADING }) + expect.objectContaining({ status: ResultStatus.LOADING, rows: [] }) ); }); }); diff --git a/src/plugins/discover/public/application/view_components/utils/use_search.ts b/src/plugins/discover/public/application/view_components/utils/use_search.ts index b8480bb03245..7f2270efc5a6 100644 --- a/src/plugins/discover/public/application/view_components/utils/use_search.ts +++ b/src/plugins/discover/public/application/view_components/utils/use_search.ts @@ -155,7 +155,7 @@ export const useSearch = (services: DiscoverViewServices) => { .getUpdates$() .pipe( pairwise(), - filter(([prev, curr]) => prev.dataset?.id === curr.dataset?.id) + filter(([prev, curr]) => prev.dataset?.id !== curr.dataset?.id) ) .subscribe(() => { data$.next({ @@ -164,6 +164,7 @@ export const useSearch = (services: DiscoverViewServices) => { ? ResultStatus.LOADING : ResultStatus.UNINITIALIZED, queryStatus: { startTime }, + rows: [], }); }); return () => subscription.unsubscribe(); @@ -186,11 +187,12 @@ export const useSearch = (services: DiscoverViewServices) => { const refetch$ = useMemo(() => new Subject(), []); const fetch = useCallback(async () => { + const currentTime = Date.now(); let dataset = indexPattern; if (!dataset) { data$.next({ status: shouldSearchOnPageLoad() ? ResultStatus.LOADING : ResultStatus.UNINITIALIZED, - queryStatus: { startTime }, + queryStatus: { startTime: currentTime }, }); return; } @@ -220,10 +222,7 @@ export const useSearch = (services: DiscoverViewServices) => { let elapsedMs; try { - // Only show loading indicator if we are fetching when the rows are empty - if (fetchStateRef.current.rows?.length === 0) { - data$.next({ status: ResultStatus.LOADING, queryStatus: { startTime } }); - } + data$.next({ status: ResultStatus.LOADING, queryStatus: { startTime: currentTime } }); // Initialize inspect adapter for search source inspectorAdapters.requests.reset(); @@ -341,7 +340,6 @@ export const useSearch = (services: DiscoverViewServices) => { services, sort, savedSearch?.searchSource, - startTime, data$, shouldSearchOnPageLoad, inspectorAdapters.requests, From 66ea0964f09dd2125d9fbbb2ccf09dac8c38f94b Mon Sep 17 00:00:00 2001 From: yuboluo Date: Thu, 14 Nov 2024 14:48:38 +0800 Subject: [PATCH 03/18] [Workspace][Bug] Fix inspect page url error (#8857) * Fix inspect page url error Signed-off-by: yubonluo * Changeset file for PR #8857 created/updated --------- Signed-off-by: yubonluo Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8857.yml | 2 ++ src/plugins/saved_objects_management/common/types.ts | 1 + .../saved_objects_management/public/utils.test.ts | 12 ++++++------ src/plugins/saved_objects_management/public/utils.ts | 3 ++- .../server/lib/find_relationships.test.ts | 6 ++++++ .../server/lib/find_relationships.ts | 1 + 6 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 changelogs/fragments/8857.yml diff --git a/changelogs/fragments/8857.yml b/changelogs/fragments/8857.yml new file mode 100644 index 000000000000..45d5fa5c5213 --- /dev/null +++ b/changelogs/fragments/8857.yml @@ -0,0 +1,2 @@ +fix: +- [Workspace][Bug] Fix inspect page url error. ([#8857](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8857)) \ No newline at end of file diff --git a/src/plugins/saved_objects_management/common/types.ts b/src/plugins/saved_objects_management/common/types.ts index e66fae37724e..cd560886f2e1 100644 --- a/src/plugins/saved_objects_management/common/types.ts +++ b/src/plugins/saved_objects_management/common/types.ts @@ -58,4 +58,5 @@ export interface SavedObjectRelation { type: string; relationship: 'child' | 'parent'; meta: SavedObjectMetadata; + workspaces?: SavedObject['workspaces']; } diff --git a/src/plugins/saved_objects_management/public/utils.test.ts b/src/plugins/saved_objects_management/public/utils.test.ts index bcaed2bb9417..fb9a8e328f04 100644 --- a/src/plugins/saved_objects_management/public/utils.test.ts +++ b/src/plugins/saved_objects_management/public/utils.test.ts @@ -52,7 +52,7 @@ describe('Utils', () => { attributes: {}, references: [], meta: { - editUrl: '/management/opensearch-dashboards/objects/savedDashboards/ID1', + editUrl: '/management/opensearch-dashboards/objects/dashboard/ID1', }, }; const savedObjectWithWorkspaces = { @@ -89,7 +89,7 @@ describe('Utils', () => { get: jest.fn().mockReturnValue(false), }; const result = formatInspectUrl(savedObject, mockCoreStart); - expect(result).toBe('/management/opensearch-dashboards/objects/savedDashboards/ID1'); + expect(result).toBe('/app/management/opensearch-dashboards/objects/dashboard/ID1'); }); it('formats URL correctly when useUpdatedUX is false, saved object does not belong to certain workspaces and not in current workspace', () => { @@ -98,7 +98,7 @@ describe('Utils', () => { get: jest.fn().mockReturnValue(false), }; const result = formatInspectUrl(savedObject, mockCoreStart); - expect(result).toBe('/management/opensearch-dashboards/objects/savedDashboards/ID1'); + expect(result).toBe('/app/management/opensearch-dashboards/objects/dashboard/ID1'); }); it('formats URL correctly when useUpdatedUX is true and in current workspace', () => { @@ -106,21 +106,21 @@ describe('Utils', () => { mockCoreStart.workspaces.currentWorkspace$.next(currentWorkspace); const result = formatInspectUrl(savedObjectWithWorkspaces, mockCoreStart); - expect(result).toBe('http://localhost/w/workspace1/app/objects/savedDashboards/ID1'); + expect(result).toBe('http://localhost/w/workspace1/app/objects/dashboard/ID1'); }); it('formats URL correctly when useUpdatedUX is true and saved object belongs to certain workspaces', () => { mockCoreStart.workspaces.workspaceList$.next([{ id: 'workspace1', name: 'workspace1' }]); const result = formatInspectUrl(savedObjectWithWorkspaces, mockCoreStart); - expect(result).toBe('http://localhost/w/workspace1/app/objects/savedDashboards/ID1'); + expect(result).toBe('http://localhost/w/workspace1/app/objects/dashboard/ID1'); }); it('formats URL correctly when useUpdatedUX is true and the object does not belong to any workspace', () => { mockCoreStart.workspaces.workspaceList$.next([{ id: 'workspace2', name: 'workspace2' }]); const result = formatInspectUrl(savedObjectWithWorkspaces, mockCoreStart); - expect(result).toBe('/app/objects/savedDashboards/ID1'); + expect(result).toBe('/app/objects/dashboard/ID1'); }); }); }); diff --git a/src/plugins/saved_objects_management/public/utils.ts b/src/plugins/saved_objects_management/public/utils.ts index 9dada18e8711..61f3670fed5c 100644 --- a/src/plugins/saved_objects_management/public/utils.ts +++ b/src/plugins/saved_objects_management/public/utils.ts @@ -25,9 +25,10 @@ export function formatInspectUrl( const useUpdatedUX = !!coreStart.uiSettings.get('home:useNewHomePage'); let finalEditUrl = editUrl; if (useUpdatedUX && finalEditUrl) { - finalEditUrl = finalEditUrl.replace(/^\/management\/opensearch-dashboards/, '/app'); + finalEditUrl = finalEditUrl.replace(/^\/management\/opensearch-dashboards/, ''); } if (finalEditUrl) { + finalEditUrl = `/app${finalEditUrl}`; const basePath = coreStart.http.basePath; let inAppUrl = basePath.prepend(finalEditUrl); const workspaceEnabled = coreStart.application.capabilities.workspaces.enabled; diff --git a/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts b/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts index c9ee0a1e766d..b34e65dbc6f6 100644 --- a/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts +++ b/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts @@ -72,12 +72,14 @@ describe('findRelationships', () => { id: 'ref-1', attributes: {}, references: [], + workspaces: ['workspace1'], }, { type: 'another-type', id: 'ref-2', attributes: {}, references: [], + workspaces: ['workspace1'], }, ], }); @@ -90,6 +92,7 @@ describe('findRelationships', () => { attributes: {}, score: 1, references: [], + workspaces: ['workspace1'], }, ], total: 1, @@ -130,18 +133,21 @@ describe('findRelationships', () => { relationship: 'child', type: 'some-type', meta: expect.any(Object), + workspaces: ['workspace1'], }, { id: 'ref-2', relationship: 'child', type: 'another-type', meta: expect.any(Object), + workspaces: ['workspace1'], }, { id: 'parent-id', relationship: 'parent', type: 'parent-type', meta: expect.any(Object), + workspaces: ['workspace1'], }, ]); }); diff --git a/src/plugins/saved_objects_management/server/lib/find_relationships.ts b/src/plugins/saved_objects_management/server/lib/find_relationships.ts index 4a4ed8155c8c..a2582ed7074f 100644 --- a/src/plugins/saved_objects_management/server/lib/find_relationships.ts +++ b/src/plugins/saved_objects_management/server/lib/find_relationships.ts @@ -95,5 +95,6 @@ function extractCommonProperties(savedObject: SavedObjectWithMetadata) { id: savedObject.id, type: savedObject.type, meta: savedObject.meta, + workspaces: savedObject.workspaces, }; } From 413697de27c2d39e2b1aed755de00e03b0cf4d2f Mon Sep 17 00:00:00 2001 From: Amardeepsingh Siglani Date: Thu, 14 Nov 2024 09:15:07 -0800 Subject: [PATCH 04/18] Indexed views framework (#8851) * indexed-view-working Signed-off-by: Amardeepsingh Siglani * working skeleton Signed-off-by: Amardeepsingh Siglani * Changeset file for PR #8851 created/updated * removed unwanted changes Signed-off-by: Amardeepsingh Siglani * fixed linter errors; failing UT Signed-off-by: Amardeepsingh Siglani * added some UTs Signed-off-by: Amardeepsingh Siglani * added more UTs Signed-off-by: Amardeepsingh Siglani * refactored code to address comments Signed-off-by: Amardeepsingh Siglani * minor updates Signed-off-by: Amardeepsingh Siglani * minor updates Signed-off-by: Amardeepsingh Siglani * updated tests Signed-off-by: Amardeepsingh Siglani * fixed UT Signed-off-by: Amardeepsingh Siglani --------- Signed-off-by: Amardeepsingh Siglani Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8851.yml | 3 + src/plugins/data/common/datasets/types.ts | 7 + src/plugins/data/public/index.ts | 1 + .../query_string/dataset_service/types.ts | 24 +- .../data/public/query/query_string/index.ts | 1 + .../query_string/query_string_manager.test.ts | 1 - .../ui/dataset_selector/configurator.test.tsx | 361 ++++++++++++++++++ .../ui/dataset_selector/configurator.tsx | 136 ++++++- .../ui/dataset_selector/dataset_selector.tsx | 3 +- .../public/ui/query_editor/query_editor.tsx | 9 +- .../query_editor_extension.test.tsx | 21 + .../query_editor_extension.tsx | 26 +- .../query_editor_extensions.test.tsx | 14 + .../query_editor_extensions.tsx | 3 + .../utils/create_extension.test.tsx | 3 + 15 files changed, 597 insertions(+), 16 deletions(-) create mode 100644 changelogs/fragments/8851.yml create mode 100644 src/plugins/data/public/ui/dataset_selector/configurator.test.tsx diff --git a/changelogs/fragments/8851.yml b/changelogs/fragments/8851.yml new file mode 100644 index 000000000000..b006f3eee529 --- /dev/null +++ b/changelogs/fragments/8851.yml @@ -0,0 +1,3 @@ +feat: +- Add framework to show banner at the top in discover results canvas ([#8851](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8851)) +- Show indexed views in dataset selector ([#8851](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8851)) \ No newline at end of file diff --git a/src/plugins/data/common/datasets/types.ts b/src/plugins/data/common/datasets/types.ts index e777eb8a45e8..ceffbd678218 100644 --- a/src/plugins/data/common/datasets/types.ts +++ b/src/plugins/data/common/datasets/types.ts @@ -247,6 +247,13 @@ export interface Dataset extends BaseDataset { timeFieldName?: string; /** Optional language to default to from the language selector */ language?: string; + /** Optional reference to the source dataset. Example usage is for indexed views to store the + * reference to the table dataset + */ + sourceDatasetRef?: { + id: string; + type: string; + }; } export interface DatasetField { diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index e39722118721..b120c69af694 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -493,6 +493,7 @@ export { QueryStart, PersistedLog, LanguageReference, + DatasetIndexedViewsService, } from './query'; export { AggsStart } from './search/aggs'; diff --git a/src/plugins/data/public/query/query_string/dataset_service/types.ts b/src/plugins/data/public/query/query_string/dataset_service/types.ts index 1e5af41ef785..0b8bcd402d15 100644 --- a/src/plugins/data/public/query/query_string/dataset_service/types.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/types.ts @@ -3,7 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ import { EuiIconProps } from '@elastic/eui'; -import { Dataset, DatasetField, DatasetSearchOptions, DataStructure } from '../../../../common'; +import { + Dataset, + DatasetField, + DatasetSearchOptions, + DataStructure, + SavedObject, +} from '../../../../common'; import { IDataPluginServices } from '../../../types'; /** @@ -16,6 +22,18 @@ export interface DataStructureFetchOptions { paginationToken?: string; } +export interface DatasetIndexedView { + name: string; +} + +export interface DatasetIndexedViewsService { + getIndexedViews: (dataset: Dataset) => Promise; + /** + * Returns the data source saved object connected with the data connection object + */ + getConnectedDataSource: (dataset: Dataset) => Promise; +} + /** * Configuration for handling dataset operations. */ @@ -84,4 +102,8 @@ export interface DatasetTypeConfig { * Returns a list of sample queries for this dataset type */ getSampleQueries?: (dataset?: Dataset, language?: string) => Promise | any; + /** + * Service used for indexedViews related operations + */ + indexedViewsService?: DatasetIndexedViewsService; } diff --git a/src/plugins/data/public/query/query_string/index.ts b/src/plugins/data/public/query/query_string/index.ts index 96f473a3aea5..9ec21a485663 100644 --- a/src/plugins/data/public/query/query_string/index.ts +++ b/src/plugins/data/public/query/query_string/index.ts @@ -34,6 +34,7 @@ export { DatasetService, DatasetServiceContract, DatasetTypeConfig, + DatasetIndexedViewsService, } from './dataset_service'; export { LanguageServiceContract, diff --git a/src/plugins/data/public/query/query_string/query_string_manager.test.ts b/src/plugins/data/public/query/query_string/query_string_manager.test.ts index da054574ff85..758d658864ab 100644 --- a/src/plugins/data/public/query/query_string/query_string_manager.test.ts +++ b/src/plugins/data/public/query/query_string/query_string_manager.test.ts @@ -44,7 +44,6 @@ describe('QueryStringManager', () => { storage = new DataStorage(window.localStorage, 'opensearchDashboards.'); sessionStorage = new DataStorage(window.sessionStorage, 'opensearchDashboards.'); mockSearchInterceptor = {} as jest.Mocked; - service = new QueryStringManager( storage, sessionStorage, diff --git a/src/plugins/data/public/ui/dataset_selector/configurator.test.tsx b/src/plugins/data/public/ui/dataset_selector/configurator.test.tsx new file mode 100644 index 000000000000..462c6298a0a3 --- /dev/null +++ b/src/plugins/data/public/ui/dataset_selector/configurator.test.tsx @@ -0,0 +1,361 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { Configurator } from './configurator'; +import '@testing-library/jest-dom'; +import React from 'react'; +import { setQueryService, setIndexPatterns } from '../../services'; +import { IntlProvider } from 'react-intl'; +import { Query } from '../../../../data/public'; +import { Dataset } from 'src/plugins/data/common'; + +const getQueryMock = jest.fn().mockReturnValue({ + query: '', + language: 'lucene', + dataset: undefined, +} as Query); + +const languageService = { + getDefaultLanguage: () => ({ id: 'lucene', title: 'Lucene' }), + getLanguages: () => [ + { id: 'lucene', title: 'Lucene' }, + { id: 'kuery', title: 'DQL' }, + ], + getLanguage: (languageId: string) => { + const languages = [ + { id: 'lucene', title: 'Lucene' }, + { id: 'kuery', title: 'DQL' }, + ]; + return languages.find((lang) => lang.id === languageId); + }, + setUserQueryLanguage: jest.fn(), +}; + +const datasetService = { + getType: jest.fn().mockReturnValue({ + fetchFields: jest.fn().mockResolvedValue([{ name: 'timestamp', type: 'date' }]), + supportedLanguages: jest.fn().mockReturnValue(['kuery', 'lucene']), + indexedViewsService: { + getIndexedViews: jest.fn().mockResolvedValue([ + { name: 'view1', type: 'type1' }, + { name: 'view2', type: 'type2' }, + ]), + getConnectedDataSource: jest.fn().mockResolvedValue({ + id: 'test-connected-data-source-saved-obj', + attributes: { + title: 'test-connected-data-source-saved-obj', + }, + }), + }, + }), + addRecentDataset: jest.fn(), +}; + +const fetchFieldsMock = jest.fn().mockResolvedValue([{ name: 'timestamp', type: 'date' }]); + +const mockServices = { + getQueryService: () => ({ + queryString: { + getQuery: getQueryMock, + getLanguageService: () => languageService, + getDatasetService: () => datasetService, + fetchFields: fetchFieldsMock, + getUpdates$: jest.fn().mockReturnValue({ + subscribe: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }), + }), + }, + }), + getIndexPatterns: jest.fn().mockResolvedValue([ + { + id: 'indexPattern1', + attributes: { + title: 'indexPattern1', + }, + }, + { + id: 'indexPattern2', + attributes: { + title: 'indexPattern2', + }, + }, + ]), +}; + +const mockBaseDataset: Dataset = { + id: 'mock-dataset', + title: 'Sample Dataset', + type: 'index-pattern', + timeFieldName: 'timestamp', + dataSource: { + id: 'test-connection-id', + meta: { supportsTimeFilter: true }, + title: 'mock-datasource', + type: 'DATA_SOURCE', + }, +}; + +const messages = { + 'app.welcome': 'Welcome to our application!', + 'app.logout': 'Log Out', +}; + +const mockOnConfirm = jest.fn(); +const mockOnCancel = jest.fn(); +const mockOnPrevious = jest.fn(); + +beforeEach(() => { + jest.clearAllMocks(); + setQueryService(mockServices.getQueryService()); + setIndexPatterns(mockServices.getIndexPatterns()); +}); + +describe('Configurator Component', () => { + it('should render the component with the correct title and description', () => { + render( + + {/* Wrap with IntlProvider */} + + + ); + + expect(screen.getByText('Step 2: Configure data')).toBeInTheDocument(); + expect( + screen.getByText('Configure selected data based on parameters available.') + ).toBeInTheDocument(); + }); + + it('should call onCancel when cancel button is clicked', () => { + render( + + + + ); + fireEvent.click(screen.getByText('Cancel')); + + expect(mockOnCancel).toHaveBeenCalledTimes(1); + }); + + it('should call onPrevious when previous button is clicked', () => { + render( + + + + ); + + fireEvent.click(screen.getByText('Back')); + + expect(mockOnPrevious).toHaveBeenCalledTimes(1); + }); + + it('should update state correctly when language is selected', async () => { + render( + + + + ); + const languageSelect = screen.getByText('Lucene'); + expect(languageSelect).toBeInTheDocument(); + expect(languageSelect.value).toBe('lucene'); + fireEvent.change(languageSelect, { target: { value: 'kuery' } }); + await waitFor(() => { + expect(languageSelect.value).toBe('kuery'); + }); + expect(mockOnConfirm).not.toHaveBeenCalled(); + }); + + it('should fetch indexed views on mount', async () => { + render( + + + + ); + + await waitFor(() => { + expect( + mockServices.getQueryService().queryString.getDatasetService().getType().indexedViewsService + .getIndexedViews + ).toHaveBeenCalledTimes(1); + }); + }); + + it('should display indexed views when query indexed view toggle is checked', async () => { + const container = render( + + + + ); + await waitFor(() => { + expect( + mockServices.getQueryService().queryString.getDatasetService().getType().indexedViewsService + .getIndexedViews + ).toHaveBeenCalledTimes(1); + }); + + fireEvent.click(container.getByText('Query indexed view')); + + await waitFor(() => { + expect(screen.getByText('view1')).toBeInTheDocument(); + expect(screen.getByText('view2')).toBeInTheDocument(); + }); + }); + + it('should update state correctly when indexed view is selected', async () => { + const container = render( + + + + ); + fireEvent.click(container.getByText('Query indexed view')); + await waitFor(() => { + expect( + mockServices.getQueryService().queryString.getDatasetService().getType().indexedViewsService + .getIndexedViews + ).toHaveBeenCalledTimes(1); + }); + const indexedViewSelector = screen.getByText('view1'); + expect(indexedViewSelector).toBeInTheDocument(); + expect(indexedViewSelector.value).toBe('view1'); + fireEvent.change(indexedViewSelector, { target: { value: 'view2' } }); + await waitFor(() => { + expect(indexedViewSelector.value).toBe('view2'); + }); + expect(mockOnConfirm).not.toHaveBeenCalled(); + }); + + it('should initialize selectedLanguage with the current language from queryString', async () => { + render( + + + + ); + + await waitFor(() => { + expect(screen.getByText('Lucene')).toBeInTheDocument(); + }); + }); + + it('should default selectedLanguage to the first language if currentLanguage is not supported', async () => { + mockServices.getQueryService().queryString.getQuery.mockReturnValue({ language: 'de' }); + + render( + + + + ); + + await waitFor(() => { + expect(screen.getByText('Lucene')).toBeInTheDocument(); // Should default to 'Lucene' + }); + }); + + it('should display the supported language dropdown correctly', async () => { + render( + + + + ); + + await waitFor(() => { + expect(screen.getByText('Lucene')).toBeInTheDocument(); + expect(screen.getByText('DQL')).toBeInTheDocument(); + }); + }); + + it('should disable the confirm button when submit is disabled', async () => { + const mockDataset = { + ...mockBaseDataset, + timeFieldName: undefined, + type: 'index', + }; + const { container } = render( + + + + ); + + const submitButton = container.querySelector( + `button[data-test-subj="advancedSelectorConfirmButton"]` + ); // screen.getAllByTestId() // screen.getByRole('button', { name: /select data/i }); + await waitFor(() => { + expect(submitButton).toBeDisabled(); + }); + + const timeFieldSelect = container.querySelector( + `[data-test-subj="advancedSelectorTimeFieldSelect"]` + ); + fireEvent.change(timeFieldSelect!, { target: { value: 'timestamp' } }); + + await waitFor(() => { + expect(submitButton).toBeEnabled(); + }); + }); +}); diff --git a/src/plugins/data/public/ui/dataset_selector/configurator.tsx b/src/plugins/data/public/ui/dataset_selector/configurator.tsx index 2940c6b2baf0..0dba9107934c 100644 --- a/src/plugins/data/public/ui/dataset_selector/configurator.tsx +++ b/src/plugins/data/public/ui/dataset_selector/configurator.tsx @@ -8,20 +8,24 @@ import { EuiButtonEmpty, EuiFieldText, EuiForm, + EuiFormLabel, EuiFormRow, EuiModalBody, EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, EuiSelect, + EuiSpacer, + EuiSwitch, EuiText, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from '@osd/i18n/react'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { BaseDataset, DEFAULT_DATA, Dataset, DatasetField, Query } from '../../../common'; import { getIndexPatterns, getQueryService } from '../../services'; import { IDataPluginServices } from '../../types'; +import { DatasetIndexedView } from '../../query/query_string/dataset_service'; export const Configurator = ({ services, @@ -42,6 +46,15 @@ export const Configurator = ({ const indexPatternsService = getIndexPatterns(); const type = queryString.getDatasetService().getType(baseDataset.type); const languages = type?.supportedLanguages(baseDataset) || []; + const [shouldSelectIndexedView, setShouldSelectIndexedView] = useState(false); + + const [language, setLanguage] = useState(() => { + const currentLanguage = queryString.getQuery().language; + if (languages.includes(currentLanguage)) { + return currentLanguage; + } + return languages[0]; + }); const [dataset, setDataset] = useState(baseDataset); const [timeFields, setTimeFields] = useState([]); @@ -52,13 +65,29 @@ export const Configurator = ({ defaultMessage: "I don't want to use the time filter", } ); - const [language, setLanguage] = useState(() => { - const currentLanguage = queryString.getQuery().language; - if (languages.includes(currentLanguage)) { - return currentLanguage; - } - return languages[0]; - }); + const indexedViewsService = type?.indexedViewsService; + const [selectedIndexedView, setSelectedIndexedView] = useState(); + const [indexedViews, setIndexedViews] = useState([]); + const [isLoadingIndexedViews, setIsLoadingIndexedViews] = useState(false); + + useEffect(() => { + let isMounted = true; + const getIndexedViews = async () => { + if (indexedViewsService) { + setIsLoadingIndexedViews(true); + const fetchedIndexedViews = await indexedViewsService.getIndexedViews(baseDataset); + if (isMounted) { + setIsLoadingIndexedViews(false); + setIndexedViews(fetchedIndexedViews || []); + } + } + }; + + getIndexedViews(); + return () => { + isMounted = false; + }; + }, [indexedViewsService, baseDataset]); const submitDisabled = useMemo(() => { return ( @@ -91,6 +120,38 @@ export const Configurator = ({ fetchFields(); }, [baseDataset, indexPatternsService, queryString, timeFields.length]); + const updateDatasetForIndexedView = useCallback(async () => { + if (!indexedViewsService || !selectedIndexedView) { + return dataset; + } + + let connectedDataSource; + if (dataset.dataSource?.id) { + const connectedDataSourceSavedObj: any = await indexedViewsService.getConnectedDataSource( + dataset + ); + if (connectedDataSourceSavedObj) { + connectedDataSource = { + id: connectedDataSourceSavedObj.id, + title: connectedDataSourceSavedObj.attributes?.title, + type: 'DATA_SOURCE', + }; + } + } + + return { + ...dataset, + id: `${dataset.id}.${selectedIndexedView}`, + title: selectedIndexedView, + type: DEFAULT_DATA.SET_TYPES.INDEX, + sourceDatasetRef: { + id: dataset.id, + type: dataset.type, + }, + dataSource: connectedDataSource ?? dataset.dataSource, + }; + }, [indexedViewsService, selectedIndexedView, dataset]); + return ( <> @@ -123,6 +184,57 @@ export const Configurator = ({ > + {indexedViewsService && ( + <> + + + {i18n.translate( + 'data.explorer.datasetSelector.advancedSelector.configurator.showAvailableIndexedViewsLabel', + { + defaultMessage: 'Query indexed view', + } + )} + + } + onChange={(e) => setShouldSelectIndexedView(e.target.checked)} + /> + + {shouldSelectIndexedView && ( + + ({ + text: name, + value: name, + }))} + value={selectedIndexedView} + onChange={async (e) => { + const value = e.target.value; + setSelectedIndexedView(value); + }} + hasNoInitialSelection + /> + + )} + + )} { - await queryString.getDatasetService().cacheDataset(dataset, services); - onConfirm({ dataset, language }); + let newDataset = dataset; + if (shouldSelectIndexedView && selectedIndexedView) { + newDataset = await updateDatasetForIndexedView(); + } + await queryString.getDatasetService().cacheDataset(newDataset, services); + onConfirm({ dataset: newDataset, language }); }} fill disabled={submitDisabled} diff --git a/src/plugins/data/public/ui/dataset_selector/dataset_selector.tsx b/src/plugins/data/public/ui/dataset_selector/dataset_selector.tsx index 75ea695a2083..a88aea528e7e 100644 --- a/src/plugins/data/public/ui/dataset_selector/dataset_selector.tsx +++ b/src/plugins/data/public/ui/dataset_selector/dataset_selector.tsx @@ -82,7 +82,8 @@ export const DatasetSelector = ({ const { overlays } = services; const datasetService = getQueryService().queryString.getDatasetService(); const datasetIcon = - datasetService.getType(selectedDataset?.type || '')?.meta.icon.type || 'database'; + datasetService.getType(selectedDataset?.sourceDatasetRef?.type || selectedDataset?.type || '') + ?.meta.icon.type || 'database'; useEffect(() => { isMounted.current = true; diff --git a/src/plugins/data/public/ui/query_editor/query_editor.tsx b/src/plugins/data/public/ui/query_editor/query_editor.tsx index 2223b577b513..20650cca6acc 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor.tsx @@ -15,7 +15,7 @@ import { PopoverAnchorPosition, } from '@elastic/eui'; import classNames from 'classnames'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { monaco } from '@osd/monaco'; import { IDataPluginServices, @@ -74,6 +74,7 @@ export const QueryEditorUI: React.FC = (props) => { const inputRef = useRef(null); const headerRef = useRef(null); const bannerRef = useRef(null); + const bottomPanelRef = useRef(null); const queryControlsContainer = useRef(null); // TODO: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/8801 const editorQuery = props.query; // local query state managed by the editor. Not to be confused by the app query state. @@ -113,6 +114,7 @@ export const QueryEditorUI: React.FC = (props) => { headerRef.current && bannerRef.current && queryControlsContainer.current && + bottomPanelRef.current && query.language && extensionMap && Object.keys(extensionMap).length > 0 @@ -130,6 +132,9 @@ export const QueryEditorUI: React.FC = (props) => { componentContainer={headerRef.current} bannerContainer={bannerRef.current} queryControlsContainer={queryControlsContainer.current} + bottomPanelContainer={bottomPanelRef.current} + query={query} + fetchStatus={props.queryStatus?.status} /> ); }; @@ -434,7 +439,7 @@ export const QueryEditorUI: React.FC = (props) => { queryString={queryString} onClickRecentQuery={onClickRecentQuery} /> - +
{renderQueryEditorExtensions()}
); diff --git a/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.test.tsx b/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.test.tsx index 289afadbac5e..13bb51ffea14 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.test.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.test.tsx @@ -15,9 +15,21 @@ jest.mock('react-dom', () => ({ type QueryEditorExtensionProps = ComponentProps; +const mockQuery = { + query: 'dummy query', + language: 'kuery', + dataset: { + id: 'db', + title: 'db', + type: 'index', + dataSource: { id: 'testId', type: 'DATA_SOURCE', title: 'testTitle' }, + }, +}; + describe('QueryEditorExtension', () => { const getComponentMock = jest.fn(); const getBannerMock = jest.fn(); + const getBottomPanelMock = jest.fn(); const isEnabledMock = jest.fn(); const defaultProps: QueryEditorExtensionProps = { @@ -27,15 +39,19 @@ describe('QueryEditorExtension', () => { isEnabled$: isEnabledMock, getComponent: getComponentMock, getBanner: getBannerMock, + getBottomPanel: getBottomPanelMock, }, dependencies: { language: 'Test', onSelectLanguage: jest.fn(), isCollapsed: false, setIsCollapsed: jest.fn(), + query: mockQuery, }, componentContainer: document.createElement('div'), bannerContainer: document.createElement('div'), + bottomPanelContainer: document.createElement('div'), + queryControlsContainer: document.createElement('div'), }; beforeEach(() => { @@ -46,26 +62,31 @@ describe('QueryEditorExtension', () => { isEnabledMock.mockReturnValue(of(true)); getComponentMock.mockReturnValue(
Test Component
); getBannerMock.mockReturnValue(
Test Banner
); + getBottomPanelMock.mockReturnValue(
Test Bottom panel
); const { getByText } = render(); await waitFor(() => { expect(getByText('Test Component')).toBeInTheDocument(); expect(getByText('Test Banner')).toBeInTheDocument(); + expect(getByText('Test Bottom panel')).toBeInTheDocument(); }); expect(isEnabledMock).toHaveBeenCalled(); expect(getComponentMock).toHaveBeenCalledWith(defaultProps.dependencies); + expect(getBottomPanelMock).toHaveBeenCalledWith(defaultProps.dependencies); }); it('does not render when isEnabled is false', async () => { isEnabledMock.mockReturnValue(of(false)); getComponentMock.mockReturnValue(
Test Component
); + getBottomPanelMock.mockReturnValue(
Test Bottom panel
); const { queryByText } = render(); await waitFor(() => { expect(queryByText('Test Component')).toBeNull(); + expect(queryByText('Test Bottom panel')).toBeNull(); }); expect(isEnabledMock).toHaveBeenCalled(); diff --git a/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.tsx b/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.tsx index 95af159c785f..bcfa95357040 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.tsx @@ -7,13 +7,15 @@ import { EuiErrorBoundary } from '@elastic/eui'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; import { Observable } from 'rxjs'; -import { DataStructureMeta } from '../../../../common'; +import { DataStructureMeta, Query } from '../../../../common'; +import { ResultStatus } from '../../../query/query_string/language_service/lib'; interface QueryEditorExtensionProps { config: QueryEditorExtensionConfig; dependencies: QueryEditorExtensionDependencies; componentContainer: Element; bannerContainer: Element; + bottomPanelContainer: Element; queryControlsContainer: Element; } @@ -34,6 +36,14 @@ export interface QueryEditorExtensionDependencies { * Set whether the query editor is collapsed. */ setIsCollapsed: (isCollapsed: boolean) => void; + /** + * Currently set Query + */ + query: Query; + /** + * Fetch status for the currently running query + */ + fetchStatus?: ResultStatus; } export interface QueryEditorExtensionConfig { @@ -74,6 +84,12 @@ export interface QueryEditorExtensionConfig { getSearchBarButton?: ( dependencies: QueryEditorExtensionDependencies ) => React.ReactElement | null; + /** + * Returns the footer element that is rendered at the bottom of the query editor. + * @param dependencies - The dependencies required for the extension. + * @returns The component the query editor extension. + */ + getBottomPanel?: (dependencies: QueryEditorExtensionDependencies) => React.ReactElement | null; } const QueryEditorExtensionPortal: React.FC<{ container: Element }> = (props) => { if (!props.children) return null; @@ -103,6 +119,11 @@ export const QueryEditorExtension: React.FC = (props) props.dependencies, ]); + const bottomPanel = useMemo(() => props.config.getBottomPanel?.(props.dependencies), [ + props.config, + props.dependencies, + ]); + useEffect(() => { isMounted.current = true; return () => { @@ -130,6 +151,9 @@ export const QueryEditorExtension: React.FC = (props) {queryControlButtons} + + {bottomPanel} + ); }; diff --git a/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extensions.test.tsx b/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extensions.test.tsx index ec67a3a52dfb..8a18a82d3714 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extensions.test.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extensions.test.tsx @@ -19,6 +19,17 @@ jest.mock('./query_editor_extension', () => ({ )), })); +const mockQuery = { + query: 'dummy query', + language: 'kuery', + dataset: { + id: 'db', + title: 'db', + type: 'index', + dataSource: { id: 'testId', type: 'DATA_SOURCE', title: 'testTitle' }, + }, +}; + describe('QueryEditorExtensions', () => { const defaultProps: QueryEditorExtensionsProps = { componentContainer: document.createElement('div'), @@ -28,6 +39,8 @@ describe('QueryEditorExtensions', () => { onSelectLanguage: jest.fn(), isCollapsed: false, setIsCollapsed: jest.fn(), + query: mockQuery, + bottomPanelContainer: document.createElement('div'), }; beforeEach(() => { @@ -78,6 +91,7 @@ describe('QueryEditorExtensions', () => { onSelectLanguage: expect.any(Function), isCollapsed: false, setIsCollapsed: expect.any(Function), + query: mockQuery, }, }), expect.anything() diff --git a/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extensions.tsx b/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extensions.tsx index 90c7fbf51666..4c420adc0312 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extensions.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extensions.tsx @@ -14,6 +14,7 @@ interface QueryEditorExtensionsProps extends QueryEditorExtensionDependencies { configMap?: Record; componentContainer: Element; bannerContainer: Element; + bottomPanelContainer: Element; queryControlsContainer: Element; } @@ -22,6 +23,7 @@ const QueryEditorExtensions: React.FC = React.memo(( configMap, componentContainer, bannerContainer, + bottomPanelContainer, queryControlsContainer, ...dependencies } = props; @@ -62,6 +64,7 @@ const QueryEditorExtensions: React.FC = React.memo(( dependencies={dependencies} componentContainer={extensionComponentContainer} bannerContainer={bannerContainer} + bottomPanelContainer={bottomPanelContainer} queryControlsContainer={extensionQueryControlsContainer} /> ); diff --git a/src/plugins/query_enhancements/public/query_assist/utils/create_extension.test.tsx b/src/plugins/query_enhancements/public/query_assist/utils/create_extension.test.tsx index 4b0a3b215db3..fc790cc79c11 100644 --- a/src/plugins/query_enhancements/public/query_assist/utils/create_extension.test.tsx +++ b/src/plugins/query_enhancements/public/query_assist/utils/create_extension.test.tsx @@ -12,6 +12,7 @@ import { QueryEditorExtensionDependencies, QueryStringContract } from '../../../ import { dataPluginMock } from '../../../../data/public/mocks'; import { ConfigSchema } from '../../../common/config'; import { clearCache, createQueryAssistExtension } from './create_extension'; +import { ResultStatus } from '../../../../discover/public'; const coreSetupMock = coreMock.createSetup({ pluginStartDeps: { @@ -54,6 +55,8 @@ describe('CreateExtension', () => { onSelectLanguage: jest.fn(), isCollapsed: false, setIsCollapsed: jest.fn(), + query: mockQueryWithIndexPattern, + fetchStatus: ResultStatus.NO_RESULTS, }; afterEach(() => { jest.clearAllMocks(); From 2eb162dc28244076d99441885e1e0a3014e55960 Mon Sep 17 00:00:00 2001 From: Joanne Wang Date: Thu, 14 Nov 2024 14:54:57 -0800 Subject: [PATCH 05/18] [Discover] Add max height and scroll to error message body (#8867) * add max height and scroll Signed-off-by: Joanne Wang * Changeset file for PR #8867 created/updated * comments and update snapshot Signed-off-by: Joanne Wang --------- Signed-off-by: Joanne Wang Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8867.yml | 2 ++ .../lib/__snapshots__/query_result.test.tsx.snap | 2 ++ .../query/query_string/language_service/lib/query_result.tsx | 5 ++++- 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/8867.yml diff --git a/changelogs/fragments/8867.yml b/changelogs/fragments/8867.yml new file mode 100644 index 000000000000..384f388393c4 --- /dev/null +++ b/changelogs/fragments/8867.yml @@ -0,0 +1,2 @@ +fix: +- Add max height and scroll to error message body ([#8867](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8867)) \ No newline at end of file diff --git a/src/plugins/data/public/query/query_string/language_service/lib/__snapshots__/query_result.test.tsx.snap b/src/plugins/data/public/query/query_string/language_service/lib/__snapshots__/query_result.test.tsx.snap index a0fd2861a2b4..f3d4e3df2c92 100644 --- a/src/plugins/data/public/query/query_string/language_service/lib/__snapshots__/query_result.test.tsx.snap +++ b/src/plugins/data/public/query/query_string/language_service/lib/__snapshots__/query_result.test.tsx.snap @@ -37,6 +37,8 @@ exports[`Query Result show error status with error message 2`] = ` className="eui-textBreakWord" style={ Object { + "maxHeight": "250px", + "overflowY": "auto", "width": "250px", } } diff --git a/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx b/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx index 5378cf8a111c..dff7faea36e3 100644 --- a/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx +++ b/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx @@ -143,7 +143,10 @@ export function QueryResult(props: { queryStatus: QueryStatus }) { data-test-subj="queryResultError" > ERRORS -
+

From bca4f5c637020b23c1b90c2a715f263f55807876 Mon Sep 17 00:00:00 2001 From: "Qingyang(Abby) Hu" Date: Thu, 14 Nov 2024 16:18:05 -0800 Subject: [PATCH 06/18] Keep previous query result if current query result in error (#8863) * keep previous result Signed-off-by: abbyhu2000 * Changeset file for PR #8863 created/updated * add some comment Signed-off-by: abbyhu2000 * invalid first query shows refresh data page Signed-off-by: abbyhu2000 --------- Signed-off-by: abbyhu2000 Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8863.yml | 2 ++ .../view_components/canvas/index.tsx | 23 ++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 changelogs/fragments/8863.yml diff --git a/changelogs/fragments/8863.yml b/changelogs/fragments/8863.yml new file mode 100644 index 000000000000..51dc8d37cc2f --- /dev/null +++ b/changelogs/fragments/8863.yml @@ -0,0 +1,2 @@ +fix: +- Keep previous query result if current query result in error ([#8863](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8863)) \ No newline at end of file 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 71d47446c75c..5fe1bac50891 100644 --- a/src/plugins/discover/public/application/view_components/canvas/index.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/index.tsx @@ -84,8 +84,13 @@ export default function DiscoverCanvas({ setHeaderActionMenu, history, optionalR if (next.bucketInterval && next.bucketInterval !== fetchState.bucketInterval) shouldUpdateState = true; if (next.chartData && next.chartData !== fetchState.chartData) shouldUpdateState = true; - // we still want to show rows from the previous query while current query is loading - if (next.status !== ResultStatus.LOADING && next.rows && next.rows !== fetchState.rows) { + // we still want to show rows from the previous query while current query is loading or the current query results in error + if ( + next.status !== ResultStatus.LOADING && + next.status !== ResultStatus.ERROR && + next.rows && + next.rows !== fetchState.rows + ) { shouldUpdateState = true; setRows(next.rows); } @@ -152,20 +157,16 @@ export default function DiscoverCanvas({ setHeaderActionMenu, history, optionalR timeFieldName={timeField} /> )} - {fetchState.status === ResultStatus.ERROR && ( - - )} {fetchState.status === ResultStatus.UNINITIALIZED && ( refetch$.next()} /> )} {fetchState.status === ResultStatus.LOADING && !rows?.length && } + {fetchState.status === ResultStatus.ERROR && !rows?.length && ( + refetch$.next()} /> + )} {(fetchState.status === ResultStatus.READY || - (fetchState.status === ResultStatus.LOADING && !!rows?.length)) && + (fetchState.status === ResultStatus.LOADING && !!rows?.length) || + (fetchState.status === ResultStatus.ERROR && !!rows?.length)) && (isEnhancementsEnabled ? ( <> From e993d2424706eaa0058244be511f1e03c26d9cb3 Mon Sep 17 00:00:00 2001 From: Miki Date: Fri, 15 Nov 2024 09:01:16 -0800 Subject: [PATCH 07/18] Fix a typo while inspecting values for large numerals in OSD and the JS client (#8839) * [@osd/std] Fix typo while inspecting values for large numerals Signed-off-by: Miki * Patch @opensearch-project/opensearch to fix a typo Ref: https://github.com/opensearch-project/opensearch-js/pull/889 Signed-off-by: Miki * Changeset file for PR #8839 created/updated --------- Signed-off-by: Miki Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8839.yml | 2 + packages/osd-std/src/json.test.ts | 84 +++++++++++++++++++++++++++++-- packages/osd-std/src/json.ts | 2 +- scripts/postinstall.js | 9 ++++ 4 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 changelogs/fragments/8839.yml diff --git a/changelogs/fragments/8839.yml b/changelogs/fragments/8839.yml new file mode 100644 index 000000000000..27477e376254 --- /dev/null +++ b/changelogs/fragments/8839.yml @@ -0,0 +1,2 @@ +fix: +- Fix a typo while inspecting values for large numerals in OSD and the JS client ([#8839](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8839)) \ No newline at end of file diff --git a/packages/osd-std/src/json.test.ts b/packages/osd-std/src/json.test.ts index 33abd71d91d2..0d4b900e0ca5 100644 --- a/packages/osd-std/src/json.test.ts +++ b/packages/osd-std/src/json.test.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import JSON11 from 'json11'; import { stringify, parse } from './json'; describe('json', () => { @@ -90,9 +91,55 @@ describe('json', () => { expect(stringify(input, replacer, 2)).toEqual(JSON.stringify(input, replacer, 2)); }); - it('can handle long numerals while parsing', () => { - const longPositive = BigInt(Number.MAX_SAFE_INTEGER) * 2n; - const longNegative = BigInt(Number.MIN_SAFE_INTEGER) * 2n; + it('can handle positive long numerals while parsing', () => { + const longPositiveA = BigInt(Number.MAX_SAFE_INTEGER) * 2n; + const longPositiveB = BigInt(Number.MAX_SAFE_INTEGER) * 2n + 1n; + const text = + `{` + + // The space before and after the values, and the lack of spaces before comma are intentional + `"\\":${longPositiveA}": "[ ${longPositiveB.toString()}, ${longPositiveA.toString()} ]", ` + + `"positive": ${longPositiveA.toString()}, ` + + `"array": [ ${longPositiveB.toString()}, ${longPositiveA.toString()} ], ` + + `"negative": ${longPositiveB.toString()},` + + `"number": 102931203123987` + + `}`; + + const result = parse(text); + expect(result.positive).toBe(longPositiveA); + expect(result.negative).toBe(longPositiveB); + expect(result.array).toEqual([longPositiveB, longPositiveA]); + expect(result['":' + longPositiveA]).toBe( + `[ ${longPositiveB.toString()}, ${longPositiveA.toString()} ]` + ); + expect(result.number).toBe(102931203123987); + }); + + it('can handle negative long numerals while parsing', () => { + const longNegativeA = BigInt(Number.MIN_SAFE_INTEGER) * 2n; + const longNegativeB = BigInt(Number.MIN_SAFE_INTEGER) * 2n - 1n; + const text = + `{` + + // The space before and after the values, and the lack of spaces before comma are intentional + `"\\":${longNegativeA}": "[ ${longNegativeB.toString()}, ${longNegativeA.toString()} ]", ` + + `"positive": ${longNegativeA.toString()}, ` + + `"array": [ ${longNegativeB.toString()}, ${longNegativeA.toString()} ], ` + + `"negative": ${longNegativeB.toString()},` + + `"number": 102931203123987` + + `}`; + + const result = parse(text); + expect(result.positive).toBe(longNegativeA); + expect(result.negative).toBe(longNegativeB); + expect(result.array).toEqual([longNegativeB, longNegativeA]); + expect(result['":' + longNegativeA]).toBe( + `[ ${longNegativeB.toString()}, ${longNegativeA.toString()} ]` + ); + expect(result.number).toBe(102931203123987); + }); + + it('can handle mixed long numerals while parsing', () => { + const longPositive = BigInt(Number.MAX_SAFE_INTEGER) * 2n + 1n; + const longNegative = BigInt(Number.MIN_SAFE_INTEGER) * 2n - 1n; const text = `{` + // The space before and after the values, and the lack of spaces before comma are intentional @@ -113,6 +160,37 @@ describe('json', () => { expect(result.number).toBe(102931203123987); }); + it('does not use JSON11 when not needed', () => { + const spyParse = jest.spyOn(JSON11, 'parse'); + + const longPositive = BigInt(Number.MAX_SAFE_INTEGER) * 2n + 1n; + const longNegative = BigInt(Number.MIN_SAFE_INTEGER) * 2n - 1n; + const text = + `{` + + `"\\":${longPositive}": "[ ${longNegative.toString()}, ${longPositive.toString()} ]", ` + + `"number": 102931203123987` + + `}`; + parse(text); + + expect(spyParse).not.toHaveBeenCalled(); + }); + + it('uses JSON11 when dealing with long numerals', () => { + const spyParse = jest.spyOn(JSON11, 'parse'); + + const longPositive = BigInt(Number.MAX_SAFE_INTEGER) * 2n + 1n; + const longNegative = BigInt(Number.MIN_SAFE_INTEGER) * 2n - 1n; + const text = + `{` + + `"\\":${longPositive}": "[ ${longNegative.toString()}, ${longPositive.toString()} ]", ` + + `"positive": ${longPositive.toString()}, ` + + `"number": 102931203123987` + + `}`; + parse(text); + + expect(spyParse).toHaveBeenCalled(); + }); + it('can handle BigInt values while stringifying', () => { const longPositive = BigInt(Number.MAX_SAFE_INTEGER) * 2n; const longNegative = BigInt(Number.MIN_SAFE_INTEGER) * 2n; diff --git a/packages/osd-std/src/json.ts b/packages/osd-std/src/json.ts index 4dcd3eb03e65..79a148f625f7 100644 --- a/packages/osd-std/src/json.ts +++ b/packages/osd-std/src/json.ts @@ -69,7 +69,7 @@ export const parse = ( numeralsAreNumbers && typeof val === 'number' && isFinite(val) && - (val < Number.MAX_SAFE_INTEGER || val > Number.MAX_SAFE_INTEGER) + (val < Number.MIN_SAFE_INTEGER || val > Number.MAX_SAFE_INTEGER) ) { numeralsAreNumbers = false; } diff --git a/scripts/postinstall.js b/scripts/postinstall.js index 7865473ee494..59be50284dca 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -84,6 +84,15 @@ const run = async () => { }, ]) ); + //ToDo: Remove when opensearch-js is released to include https://github.com/opensearch-project/opensearch-js/pull/889 + promises.push( + patchFile('node_modules/@opensearch-project/opensearch/lib/Serializer.js', [ + { + from: 'val < Number.MAX_SAFE_INTEGER', + to: 'val < Number.MIN_SAFE_INTEGER', + }, + ]) + ); await Promise.all(promises); }; From 42df421f5a4cc3158132cf680a8bb4722bac92d6 Mon Sep 17 00:00:00 2001 From: Kawika Avilla Date: Fri, 15 Nov 2024 11:56:49 -0800 Subject: [PATCH 08/18] [Discover] Dataset search on page load issues (#8871) * Dataset search on page load issues Required a double click on search and then also potentially loading issue. Issue n/a Signed-off-by: Kawika Avilla * Clean up dependencies Signed-off-by: Kawika Avilla * Addresses issue Signed-off-by: Kawika Avilla * add some tests Signed-off-by: Kawika Avilla * Changeset file for PR #8871 created/updated --------- Signed-off-by: Kawika Avilla Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8871.yml | 2 + .../view_components/utils/use_search.test.tsx | 42 +++++++++++++++++++ .../view_components/utils/use_search.ts | 26 +++++------- 3 files changed, 54 insertions(+), 16 deletions(-) create mode 100644 changelogs/fragments/8871.yml diff --git a/changelogs/fragments/8871.yml b/changelogs/fragments/8871.yml new file mode 100644 index 000000000000..032a928fd5c0 --- /dev/null +++ b/changelogs/fragments/8871.yml @@ -0,0 +1,2 @@ +fix: +- Search on page load out of sync state when clicking submit. ([#8871](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8871)) \ No newline at end of file diff --git a/src/plugins/discover/public/application/view_components/utils/use_search.test.tsx b/src/plugins/discover/public/application/view_components/utils/use_search.test.tsx index 4a92bb5d37be..b76651899b61 100644 --- a/src/plugins/discover/public/application/view_components/utils/use_search.test.tsx +++ b/src/plugins/discover/public/application/view_components/utils/use_search.test.tsx @@ -110,6 +110,48 @@ describe('useSearch', () => { }); }); + it('should initialize with uninitialized state when dataset type config search on page load is disabled', async () => { + const services = createMockServices(); + (services.uiSettings.get as jest.Mock).mockReturnValueOnce(true); + (services.data.query.queryString.getDatasetService as jest.Mock).mockReturnValue({ + meta: { searchOnLoad: false }, + }); + (services.data.query.timefilter.timefilter.getRefreshInterval as jest.Mock).mockReturnValue({ + pause: true, + value: 10, + }); + + const { result, waitForNextUpdate } = renderHook(() => useSearch(services), { wrapper }); + expect(result.current.data$.getValue()).toEqual( + expect.objectContaining({ status: ResultStatus.UNINITIALIZED }) + ); + + await act(async () => { + await waitForNextUpdate(); + }); + }); + + it('should initialize with uninitialized state when dataset type config search on page load is enabled but the UI setting is disabled', async () => { + const services = createMockServices(); + (services.uiSettings.get as jest.Mock).mockReturnValueOnce(false); + (services.data.query.queryString.getDatasetService as jest.Mock).mockReturnValue({ + meta: { searchOnLoad: true }, + }); + (services.data.query.timefilter.timefilter.getRefreshInterval as jest.Mock).mockReturnValue({ + pause: true, + value: 10, + }); + + const { result, waitForNextUpdate } = renderHook(() => useSearch(services), { wrapper }); + expect(result.current.data$.getValue()).toEqual( + expect.objectContaining({ status: ResultStatus.UNINITIALIZED }) + ); + + await act(async () => { + await waitForNextUpdate(); + }); + }); + it('should update startTime when hook rerenders', async () => { const services = createMockServices(); diff --git a/src/plugins/discover/public/application/view_components/utils/use_search.ts b/src/plugins/discover/public/application/view_components/utils/use_search.ts index 7f2270efc5a6..158a9cd46074 100644 --- a/src/plugins/discover/public/application/view_components/utils/use_search.ts +++ b/src/plugins/discover/public/application/view_components/utils/use_search.ts @@ -115,24 +115,23 @@ export const useSearch = (services: DiscoverViewServices) => { requests: new RequestAdapter(), }; - const getDatasetAutoSearchOnPageLoadPreference = () => { - // Checks the searchOnpageLoadPreference for the current dataset if not specifed defaults to true - const datasetType = data.query.queryString.getQuery().dataset?.type; - - const datasetService = data.query.queryString.getDatasetService(); - - return !datasetType || (datasetService?.getType(datasetType)?.meta?.searchOnLoad ?? true); - }; - const shouldSearchOnPageLoad = useCallback(() => { + // Checks the searchOnpageLoadPreference for the current dataset if not specifed defaults to UI Settings + const { queryString } = data.query; + const { dataset } = queryString.getQuery(); + const typeConfig = dataset ? queryString.getDatasetService().getType(dataset.type) : undefined; + const datasetPreference = + typeConfig?.meta?.searchOnLoad ?? uiSettings.get(SEARCH_ON_PAGE_LOAD_SETTING); + // A saved search is created on every page load, so we check the ID to see if we're loading a // previously saved search or if it is just transient return ( - services.uiSettings.get(SEARCH_ON_PAGE_LOAD_SETTING) || + datasetPreference || + uiSettings.get(SEARCH_ON_PAGE_LOAD_SETTING) || savedSearch?.id !== undefined || timefilter.getRefreshInterval().pause === false ); - }, [savedSearch, services.uiSettings, timefilter]); + }, [data.query, savedSearch, uiSettings, timefilter]); const startTime = Date.now(); const data$ = useMemo( @@ -346,9 +345,6 @@ export const useSearch = (services: DiscoverViewServices) => { ]); useEffect(() => { - if (!getDatasetAutoSearchOnPageLoadPreference()) { - skipInitialFetch.current = true; - } const fetch$ = merge( refetch$, filterManager.getFetches$(), @@ -379,8 +375,6 @@ export const useSearch = (services: DiscoverViewServices) => { return () => { subscription.unsubscribe(); }; - // disabling the eslint since we are not adding getDatasetAutoSearchOnPageLoadPreference since this changes when dataset changes and these chnages are already part of data.query.queryString - // eslint-disable-next-line react-hooks/exhaustive-deps }, [ data$, data.query.queryString, From b2b71e2e3686758566e4cb921e4a9df5807018ad Mon Sep 17 00:00:00 2001 From: Sean Li Date: Fri, 15 Nov 2024 13:58:34 -0800 Subject: [PATCH 09/18] [Discover] Hide Date Picker For Unsupported Types (#8866) * initial commit for hiding date picker Signed-off-by: Sean Li * Changeset file for PR #8866 created/updated * adding tests for query_editor_top_row.tsx Signed-off-by: Sean Li * updating conditional Signed-off-by: Sean Li --------- Signed-off-by: Sean Li Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8866.yml | 2 + .../language_service/language_service.mock.ts | 9 +- .../query_editor_top_row.test.tsx | 158 ++++++++++++++++++ .../ui/query_editor/query_editor_top_row.tsx | 14 +- 4 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 changelogs/fragments/8866.yml create mode 100644 src/plugins/data/public/ui/query_editor/query_editor_top_row.test.tsx diff --git a/changelogs/fragments/8866.yml b/changelogs/fragments/8866.yml new file mode 100644 index 000000000000..9d328bf54e5b --- /dev/null +++ b/changelogs/fragments/8866.yml @@ -0,0 +1,2 @@ +fix: +- Hide Date Picker for Unsupported Types ([#8866](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8866)) \ No newline at end of file diff --git a/src/plugins/data/public/query/query_string/language_service/language_service.mock.ts b/src/plugins/data/public/query/query_string/language_service/language_service.mock.ts index 936ff690353d..e481932883ca 100644 --- a/src/plugins/data/public/query/query_string/language_service/language_service.mock.ts +++ b/src/plugins/data/public/query/query_string/language_service/language_service.mock.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { createEditor, DQLBody, SingleLineInput } from '../../../ui'; import { LanguageServiceContract } from './language_service'; import { LanguageConfig } from './types'; @@ -14,7 +15,7 @@ const createSetupLanguageServiceMock = (): jest.Mocked title: 'DQL', search: {} as any, getQueryString: jest.fn(), - editor: {} as any, + editor: createEditor(SingleLineInput, SingleLineInput, [], DQLBody), fields: { filterable: true, visualizable: true, @@ -28,7 +29,7 @@ const createSetupLanguageServiceMock = (): jest.Mocked title: 'Lucene', search: {} as any, getQueryString: jest.fn(), - editor: {} as any, + editor: createEditor(SingleLineInput, SingleLineInput, [], DQLBody), fields: { filterable: true, visualizable: true, @@ -42,7 +43,9 @@ const createSetupLanguageServiceMock = (): jest.Mocked return { __enhance: jest.fn(), - registerLanguage: jest.fn(), + registerLanguage: jest.fn((language: LanguageConfig) => { + languages.set(language.id, language); + }), getLanguage: jest.fn((id: string) => languages.get(id)), getLanguages: jest.fn(() => Array.from(languages.values())), getDefaultLanguage: jest.fn(() => languages.get('kuery') || languages.values().next().value), diff --git a/src/plugins/data/public/ui/query_editor/query_editor_top_row.test.tsx b/src/plugins/data/public/ui/query_editor/query_editor_top_row.test.tsx new file mode 100644 index 000000000000..62fe653bfd45 --- /dev/null +++ b/src/plugins/data/public/ui/query_editor/query_editor_top_row.test.tsx @@ -0,0 +1,158 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Query, UI_SETTINGS } from '../../../common'; +import { coreMock } from '../../../../../core/public/mocks'; +import { dataPluginMock } from '../../mocks'; +import React from 'react'; +import { I18nProvider } from '@osd/i18n/react'; +import { createEditor, DQLBody, QueryEditorTopRow, SingleLineInput } from '../'; +import { OpenSearchDashboardsContextProvider } from 'src/plugins/opensearch_dashboards_react/public'; +import { cleanup, render, waitFor } from '@testing-library/react'; +import { LanguageConfig } from '../../query'; +import { getQueryService } from '../../services'; + +const startMock = coreMock.createStart(); + +jest.mock('../../services', () => ({ + getQueryService: jest.fn(), +})); + +startMock.uiSettings.get.mockImplementation((key: string) => { + switch (key) { + case UI_SETTINGS.TIMEPICKER_QUICK_RANGES: + return [ + { + from: 'now/d', + to: 'now/d', + display: 'Today', + }, + ]; + case 'dateFormat': + return 'MMM D, YYYY @ HH:mm:ss.SSS'; + case UI_SETTINGS.HISTORY_LIMIT: + return 10; + case UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS: + return { + from: 'now-15m', + to: 'now', + }; + case UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED: + return true; + case 'theme:darkMode': + return true; + default: + throw new Error(`Unexpected config key: ${key}`); + } +}); + +const createMockWebStorage = () => ({ + clear: jest.fn(), + getItem: jest.fn(), + key: jest.fn(), + removeItem: jest.fn(), + setItem: jest.fn(), + length: 0, +}); + +const createMockStorage = () => ({ + storage: createMockWebStorage(), + get: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + clear: jest.fn(), +}); + +const dataPlugin = dataPluginMock.createStartContract(true); + +function wrapQueryEditorTopRowInContext(testProps: any) { + const defaultOptions = { + onSubmit: jest.fn(), + onChange: jest.fn(), + isDirty: true, + screenTitle: 'Another Screen', + }; + + const mockLanguage: LanguageConfig = { + id: 'test-language', + title: 'Test Language', + search: {} as any, + getQueryString: jest.fn(), + editor: createEditor(SingleLineInput, SingleLineInput, [], DQLBody), + fields: {}, + showDocLinks: true, + editorSupportedAppNames: ['discover'], + hideDatePicker: true, + }; + dataPlugin.query.queryString.getLanguageService().registerLanguage(mockLanguage); + + const services = { + ...startMock, + data: dataPlugin, + appName: 'discover', + storage: createMockStorage(), + }; + + return ( + + + + + + ); +} + +describe('QueryEditorTopRow', () => { + const QUERY_EDITOR = '.osdQueryEditor'; + const DATE_PICKER = '.osdQueryEditor__datePickerWrapper'; + + beforeEach(() => { + jest.clearAllMocks(); + (getQueryService as jest.Mock).mockReturnValue(dataPlugin.query); + }); + + afterEach(() => { + cleanup(); + jest.resetModules(); + }); + + it('Should render query editor', async () => { + const { container } = render( + wrapQueryEditorTopRowInContext({ + showQueryEditor: true, + }) + ); + await waitFor(() => expect(container.querySelector(QUERY_EDITOR)).toBeTruthy()); + expect(container.querySelector(DATE_PICKER)).toBeTruthy(); + }); + + it('Should not render date picker if showDatePicker is false', async () => { + const { container } = render( + wrapQueryEditorTopRowInContext({ + showQueryEditor: true, + showDatePicker: false, + }) + ); + await waitFor(() => expect(container.querySelector(QUERY_EDITOR)).toBeTruthy()); + expect(container.querySelector(DATE_PICKER)).toBeFalsy(); + }); + + it('Should not render date picker if language does not support time field', async () => { + const query: Query = { + query: 'test query', + language: 'test-language', + }; + dataPlugin.query.queryString.getQuery = jest.fn().mockReturnValue(query); + const { container } = render( + wrapQueryEditorTopRowInContext({ + query, + showQueryEditor: false, + showDatePicker: true, + }) + ); + await waitFor(() => expect(container.querySelector(QUERY_EDITOR)).toBeTruthy()); + expect(container.querySelector(DATE_PICKER)).toBeFalsy(); + }); +}); 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 ab9b8c50e038..ea15fbfeeaa1 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 @@ -72,7 +72,7 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false); const [isQueryEditorFocused, setIsQueryEditorFocused] = useState(false); const opensearchDashboards = useOpenSearchDashboards(); - const { uiSettings, storage, appName } = opensearchDashboards.services; + const { uiSettings, storage, appName, data } = opensearchDashboards.services; const queryLanguage = props.query && props.query.language; const persistedLog: PersistedLog | undefined = React.useMemo( @@ -225,7 +225,17 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { } function shouldRenderDatePicker(): boolean { - return Boolean(props.showDatePicker ?? true) ?? (props.showAutoRefreshOnly && true); + return ( + Boolean((props.showDatePicker || props.showAutoRefreshOnly) ?? true) && + !( + queryLanguage && + data.query.queryString.getLanguageService().getLanguage(queryLanguage)?.hideDatePicker + ) && + (props.query?.dataset + ? data.query.queryString.getDatasetService().getType(props.query.dataset.type)?.meta + ?.supportsTimeFilter !== false + : true) + ); } function shouldRenderQueryEditor(): boolean { From ab4e6e8a0d45030a13c170bf8337fc06772c4ada Mon Sep 17 00:00:00 2001 From: Anan Zhuang Date: Fri, 15 Nov 2024 14:06:38 -0800 Subject: [PATCH 10/18] [Bug] Make release note generation more resilient by gracefully handling invalid changelog fragments (#8780) Signed-off-by: Anan Zhuang --- src/dev/generate_release_note.ts | 100 +++++++++++++++++++++---------- 1 file changed, 70 insertions(+), 30 deletions(-) diff --git a/src/dev/generate_release_note.ts b/src/dev/generate_release_note.ts index 1c85995f814b..5cfa4503537d 100644 --- a/src/dev/generate_release_note.ts +++ b/src/dev/generate_release_note.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { ToolingLog } from '@osd/dev-utils'; import { join, resolve } from 'path'; import { readFileSync, writeFileSync, Dirent, rm, rename, promises as fsPromises } from 'fs'; import { load as loadYaml } from 'js-yaml'; @@ -19,6 +20,11 @@ import { filePath, } from './generate_release_note_helper'; +const log = new ToolingLog({ + level: 'info', + writeTo: process.stdout, +}); + // Function to add content after the 'Unreleased' section in the changelog function addContentAfterUnreleased(path: string, newContent: string): void { let fileContent = readFileSync(path, 'utf8'); @@ -60,35 +66,63 @@ async function readFragments() { ) as unknown) as Changelog; const fragmentPaths = await readdir(fragmentDirPath, { withFileTypes: true }); + const failedFragments: string[] = []; + for (const fragmentFilename of fragmentPaths) { // skip non yml or yaml files if (!/\.ya?ml$/i.test(fragmentFilename.name)) { - // eslint-disable-next-line no-console - console.warn(`Skipping non yml or yaml file ${fragmentFilename.name}`); + log.info(`Skipping non yml or yaml file ${fragmentFilename.name}`); continue; } - const fragmentPath = join(fragmentDirPath, fragmentFilename.name); - const fragmentContents = readFileSync(fragmentPath, { encoding: 'utf-8' }); - - validateFragment(fragmentContents); - - const fragmentContentLines = fragmentContents.split('\n'); - // Adding a quotes to the second line and escaping exisiting " within the line - fragmentContentLines[1] = fragmentContentLines[1].replace(/-\s*(.*)/, (match, p1) => { - // Escape any existing quotes in the content - const escapedContent = p1.replace(/"/g, '\\"'); - return `- "${escapedContent}"`; - }); - - const processedFragmentContent = fragmentContentLines.join('\n'); - - const fragmentYaml = loadYaml(processedFragmentContent) as Changelog; - for (const [sectionKey, entries] of Object.entries(fragmentYaml)) { - sections[sectionKey as SectionKey].push(...entries); + try { + const fragmentPath = join(fragmentDirPath, fragmentFilename.name); + const fragmentContents = readFileSync(fragmentPath, { encoding: 'utf-8' }); + + try { + validateFragment(fragmentContents); + } catch (validationError) { + log.info(`Validation failed for ${fragmentFilename.name}: ${validationError.message}`); + failedFragments.push( + `${fragmentFilename.name} (Validation Error: ${validationError.message})` + ); + continue; + } + + const fragmentContentLines = fragmentContents.split('\n'); + // Adding a quotes to the second line and escaping existing " within the line + fragmentContentLines[1] = fragmentContentLines[1].replace(/-\s*(.*)/, (match, p1) => { + // Escape any existing quotes in the content + const escapedContent = p1.replace(/"/g, '\\"'); + return `- "${escapedContent}"`; + }); + + const processedFragmentContent = fragmentContentLines.join('\n'); + + try { + const fragmentYaml = loadYaml(processedFragmentContent) as Changelog; + for (const [sectionKey, entries] of Object.entries(fragmentYaml)) { + sections[sectionKey as SectionKey].push(...entries); + } + } catch (yamlError) { + log.info(`Failed to parse YAML in ${fragmentFilename.name}: ${yamlError.message}`); + failedFragments.push(`${fragmentFilename.name} (YAML Parse Error: ${yamlError.message})`); + continue; + } + } catch (error) { + log.info(`Failed to process ${fragmentFilename.name}: ${error.message}`); + failedFragments.push(`${fragmentFilename.name} (Processing Error: ${error.message})`); + continue; } } - return { sections, fragmentPaths }; + + if (failedFragments.length > 0) { + log.info('\nThe following changelog fragments were skipped due to errors:'); + failedFragments.forEach((fragment) => log.info(`- ${fragment}`)); + log.info('\nPlease review and fix these fragments for inclusion in the next release.\n'); + } + + return { sections, fragmentPaths, failedFragments }; } async function moveFragments(fragmentPaths: Dirent[], fragmentTempDirPath: string): Promise { @@ -128,16 +162,22 @@ function generateReleaseNote(changelogSections: string[]) { } (async () => { - const { sections, fragmentPaths } = await readFragments(); - // create folder for temp fragments - const fragmentTempDirPath = await fsPromises.mkdtemp(join(fragmentDirPath, 'tmp_fragments-')); - // move fragments to temp fragments folder - await moveFragments(fragmentPaths, fragmentTempDirPath); + const { sections, fragmentPaths, failedFragments } = await readFragments(); - const changelogSections = generateChangelog(sections); + // Only proceed if we have some valid fragments + if (Object.values(sections).some((section) => section.length > 0)) { + // create folder for temp fragments + const fragmentTempDirPath = await fsPromises.mkdtemp(join(fragmentDirPath, 'tmp_fragments-')); + // move fragments to temp fragments folder + await moveFragments(fragmentPaths, fragmentTempDirPath); - generateReleaseNote(changelogSections); + const changelogSections = generateChangelog(sections); + generateReleaseNote(changelogSections); - // remove temp fragments folder - await deleteFragments(fragmentTempDirPath); + // remove temp fragments folder + await deleteFragments(fragmentTempDirPath); + } else { + log.error('No valid changelog entries were found. Release notes generation aborted.'); + process.exit(1); + } })(); From 9262a3349dc8de70498e7eaade53e3eef3e66d70 Mon Sep 17 00:00:00 2001 From: Lu Yu Date: Mon, 18 Nov 2024 10:29:13 -0800 Subject: [PATCH 11/18] Fix openApi doc for bulk saved object API http method (#8885) * Fix openApi doc for bulk saved object API http method Signed-off-by: Lu Yu * Changeset file for PR #8885 created/updated --------- Signed-off-by: Lu Yu Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8885.yml | 2 ++ docs/openapi/saved_objects/saved_objects.yml | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 changelogs/fragments/8885.yml diff --git a/changelogs/fragments/8885.yml b/changelogs/fragments/8885.yml new file mode 100644 index 000000000000..5ba3f06558ca --- /dev/null +++ b/changelogs/fragments/8885.yml @@ -0,0 +1,2 @@ +doc: +- Fix OpenAPI documentation ([#8885](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8885)) \ No newline at end of file diff --git a/docs/openapi/saved_objects/saved_objects.yml b/docs/openapi/saved_objects/saved_objects.yml index bd1877545dc3..f54faa757072 100644 --- a/docs/openapi/saved_objects/saved_objects.yml +++ b/docs/openapi/saved_objects/saved_objects.yml @@ -423,7 +423,7 @@ paths: schema: type: object /api/saved_objects/_bulk_update: - post: + put: tags: - saved objects summary: Bulk update saved objects @@ -489,7 +489,7 @@ paths: schema: type: object /api/saved_objects/_bulk_get: - get: + post: tags: - saved objects summary: Bulk get saved objects From d94fad2f3b2fd7388a3ea08857d92b28bfb1866d Mon Sep 17 00:00:00 2001 From: Miki Date: Mon, 18 Nov 2024 12:31:49 -0800 Subject: [PATCH 12/18] [CVE-2024-21538] Bump `cross-spawn` from 6.0.5 and 7.0.3 to 7.0.5 (#8882) * Bump `cross-spawn` from 6.0.5 and 7.0.3 to 7.0.5 Signed-off-by: Miki * Changeset file for PR #8882 created/updated --------- Signed-off-by: Miki Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8882.yml | 2 ++ package.json | 1 + yarn.lock | 45 +++++------------------------------ 3 files changed, 9 insertions(+), 39 deletions(-) create mode 100644 changelogs/fragments/8882.yml diff --git a/changelogs/fragments/8882.yml b/changelogs/fragments/8882.yml new file mode 100644 index 000000000000..d6fe67ac7888 --- /dev/null +++ b/changelogs/fragments/8882.yml @@ -0,0 +1,2 @@ +security: +- [CVE-2024-21538] Bump `cross-spawn` from 6.0.5 and 7.0.3 to 7.0.5 ([#8882](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8882)) \ No newline at end of file diff --git a/package.json b/package.json index 7c8b3ec5cfa8..5bd2a4a5d09f 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "**/cpy/globby": "^10.0.1", "**/d3-color": "^3.1.0", "**/elasticsearch/agentkeepalive": "^4.5.0", + "**/eslint/cross-spawn": "^7.0.5", "**/es5-ext": "^0.10.63", "**/fetch-mock/path-to-regexp": "^3.3.0", "**/follow-redirects": "^1.15.4", diff --git a/yarn.lock b/yarn.lock index cc9f4490818d..5b3dec208a45 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6295,21 +6295,10 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" -cross-spawn@^6.0.5: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - -cross-spawn@^7.0.0, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== +cross-spawn@^6.0.5, cross-spawn@^7.0.0, cross-spawn@^7.0.3, cross-spawn@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.5.tgz#910aac880ff5243da96b728bc6521a5f6c2f2f82" + integrity sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" @@ -12753,11 +12742,6 @@ next-tick@1, next-tick@^1.1.0: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - nise@^1.5.2: version "1.5.3" resolved "https://registry.yarnpkg.com/nise/-/nise-1.5.3.tgz#9d2cfe37d44f57317766c6e9408a359c5d3ac1f7" @@ -13528,11 +13512,6 @@ path-is-inside@^1.0.2: resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= -path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" @@ -15376,7 +15355,7 @@ selenium-webdriver@^4.0.0-alpha.7: rimraf "^2.7.1" tmp "0.0.30" -"semver@2 || 3 || 4 || 5", semver@7.3.2, semver@^5.3.0, semver@^5.5.0, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1, semver@^5.7.2, semver@^6.0.0, semver@^6.1.0, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@~7.3.0: +"semver@2 || 3 || 4 || 5", semver@7.3.2, semver@^5.3.0, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1, semver@^5.7.2, semver@^6.0.0, semver@^6.1.0, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@~7.3.0: version "7.5.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e" integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ== @@ -15481,13 +15460,6 @@ shallowequal@^1.1.0: resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -15495,11 +15467,6 @@ shebang-command@^2.0.0: dependencies: shebang-regex "^3.0.0" -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - shebang-regex@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" @@ -18259,7 +18226,7 @@ which-typed-array@^1.1.11, which-typed-array@^1.1.13, which-typed-array@^1.1.2: gopd "^1.0.1" has-tostringtag "^1.0.0" -which@^1.2.14, which@^1.2.9, which@^1.3.1: +which@^1.2.14, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== From 5740259f54e6dbe5fa2199cbf3b141f5c7cd4da8 Mon Sep 17 00:00:00 2001 From: AWSHurneyt Date: Mon, 18 Nov 2024 14:01:19 -0800 Subject: [PATCH 13/18] Adjusted source of QueryStringManager functions for flyout. (#8864) * Change origin of query string management functions. Signed-off-by: AWSHurneyt * Added try/catch block. Signed-off-by: AWSHurneyt * Changed source of query string management functions. Signed-off-by: AWSHurneyt * Fixed typo. Signed-off-by: AWSHurneyt * Fixed import. Signed-off-by: AWSHurneyt * Fix lint errors. Signed-off-by: AWSHurneyt * Fixed lint error. Signed-off-by: AWSHurneyt * Fixed test mocks. Signed-off-by: AWSHurneyt --------- Signed-off-by: AWSHurneyt --- .../public/ui/filter_bar/filter_options.tsx | 4 +- .../open_saved_query_flyout.test.tsx | 15 +++--- .../open_saved_query_flyout.tsx | 54 ++++++++++--------- .../saved_query_management_component.tsx | 5 +- .../ui/search_bar/create_search_bar.tsx | 1 - .../data/public/ui/search_bar/search_bar.tsx | 10 +--- 6 files changed, 42 insertions(+), 47 deletions(-) diff --git a/src/plugins/data/public/ui/filter_bar/filter_options.tsx b/src/plugins/data/public/ui/filter_bar/filter_options.tsx index 4af53fa28df1..3cda39731fa7 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_options.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_options.tsx @@ -59,7 +59,7 @@ import { import { FilterEditor } from './filter_editor'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { SavedQueryManagementComponent } from '../saved_query_management'; -import { QueryStringManager, SavedQuery, SavedQueryService } from '../../query'; +import { SavedQuery, SavedQueryService } from '../../query'; import { SavedQueryMeta } from '../saved_query_form'; import { getUseNewSavedQueriesUI } from '../../services'; @@ -79,7 +79,6 @@ interface Props { useSaveQueryMenu: boolean; isQueryEditorControl: boolean; saveQuery: (savedQueryMeta: SavedQueryMeta, saveAsNew?: boolean) => Promise; - queryStringManager: QueryStringManager; } const maxFilterWidth = 600; @@ -311,7 +310,6 @@ const FilterOptionsUI = (props: Props) => { key={'savedQueryManagement'} useNewSavedQueryUI={getUseNewSavedQueriesUI()} saveQuery={props.saveQuery} - queryStringManager={props.queryStringManager} />, ]} data-test-subj="save-query-panel" diff --git a/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.test.tsx b/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.test.tsx index 8daaafe0fdcb..f004f6e7e5af 100644 --- a/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.test.tsx +++ b/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.test.tsx @@ -4,13 +4,14 @@ */ import React from 'react'; -import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { OpenSavedQueryFlyout } from './open_saved_query_flyout'; import { createSavedQueryService } from '../../../public/query/saved_query/saved_query_service'; import { applicationServiceMock, uiSettingsServiceMock } from '../../../../../core/public/mocks'; import { SavedQueryAttributes } from '../../../public/query/saved_query/types'; import '@testing-library/jest-dom'; import { queryStringManagerMock } from '../../../../data/public/query/query_string/query_string_manager.mock'; +import { getQueryService } from '../../services'; const savedQueryAttributesWithTemplate: SavedQueryAttributes = { title: 'foo', @@ -63,6 +64,10 @@ jest.mock('@osd/i18n', () => ({ }, })); +jest.mock('../../services', () => ({ + getQueryService: jest.fn(), +})); + const mockSavedQueryService = createSavedQueryService( // @ts-ignore mockSavedObjectsClient, @@ -100,6 +105,9 @@ jest.spyOn(mockSavedQueryService, 'getAllSavedQueries').mockResolvedValue(savedQ describe('OpenSavedQueryFlyout', () => { beforeEach(() => { jest.clearAllMocks(); + (getQueryService as jest.Mock).mockReturnValue({ + queryString: queryStringManagerMock.createSetupContract(), + }); }); it('should render the flyout with correct tabs and content', async () => { @@ -109,7 +117,6 @@ describe('OpenSavedQueryFlyout', () => { onClose={mockOnClose} onQueryOpen={mockOnQueryOpen} handleQueryDelete={mockHandleQueryDelete} - queryStringManager={queryStringManagerMock.createSetupContract()} /> ); @@ -141,7 +148,6 @@ describe('OpenSavedQueryFlyout', () => { onClose={mockOnClose} onQueryOpen={mockOnQueryOpen} handleQueryDelete={mockHandleQueryDelete} - queryStringManager={queryStringManagerMock.createSetupContract()} /> ); @@ -162,7 +168,6 @@ describe('OpenSavedQueryFlyout', () => { onClose={mockOnClose} onQueryOpen={mockOnQueryOpen} handleQueryDelete={mockHandleQueryDelete} - queryStringManager={queryStringManagerMock.createSetupContract()} /> ); @@ -181,7 +186,6 @@ describe('OpenSavedQueryFlyout', () => { onClose={mockOnClose} onQueryOpen={mockOnQueryOpen} handleQueryDelete={mockHandleQueryDelete} - queryStringManager={queryStringManagerMock.createSetupContract()} /> ); @@ -214,7 +218,6 @@ describe('OpenSavedQueryFlyout', () => { onClose={mockOnClose} onQueryOpen={mockOnQueryOpen} handleQueryDelete={mockHandleQueryDelete} - queryStringManager={queryStringManagerMock.createSetupContract()} /> ); diff --git a/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx b/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx index 41aa344bbaef..212e0228e626 100644 --- a/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx +++ b/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx @@ -24,16 +24,16 @@ import { } from '@elastic/eui'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { i18n } from '@osd/i18n'; -import { QueryStringManager, SavedQuery, SavedQueryService } from '../../query'; +import { SavedQuery, SavedQueryService } from '../../query'; import { SavedQueryCard } from './saved_query_card'; import { Query } from '../../../common'; +import { getQueryService } from '../../services'; export interface OpenSavedQueryFlyoutProps { savedQueryService: SavedQueryService; onClose: () => void; onQueryOpen: (query: SavedQuery) => void; handleQueryDelete: (query: SavedQuery) => Promise; - queryStringManager: QueryStringManager; } interface SavedQuerySearchableItem { @@ -50,7 +50,6 @@ export function OpenSavedQueryFlyout({ onClose, onQueryOpen, handleQueryDelete, - queryStringManager, }: OpenSavedQueryFlyoutProps) { const [selectedTabId, setSelectedTabId] = useState('mutable-saved-queries'); const [savedQueries, setSavedQueries] = useState([]); @@ -65,36 +64,43 @@ export function OpenSavedQueryFlyout({ const [searchQuery, setSearchQuery] = useState(EuiSearchBar.Query.MATCH_ALL); const [isLoading, setIsLoading] = useState(false); const currentTabIdRef = useRef(selectedTabId); + const queryStringManager = getQueryService().queryString; const fetchAllSavedQueriesForSelectedTab = useCallback(async () => { setIsLoading(true); - const query = queryStringManager.getQuery(); - let templateQueries: any[] = []; + try { + const query = queryStringManager.getQuery(); + let templateQueries: any[] = []; - // fetch sample query based on dataset type - if (query?.dataset?.type) { - templateQueries = - (await queryStringManager - .getDatasetService() - ?.getType(query.dataset.type) - ?.getSampleQueries?.()) || []; + // fetch sample query based on dataset type + if (query?.dataset?.type) { + templateQueries = + (await queryStringManager + .getDatasetService() + ?.getType(query.dataset.type) + ?.getSampleQueries?.()) || []; - // Check if any sample query has isTemplate set to true - const hasTemplates = templateQueries.some((q) => q?.attributes?.isTemplate); - setHasTemplateQueries(hasTemplates); - } + // Check if any sample query has isTemplate set to true + const hasTemplates = templateQueries.some((q) => q?.attributes?.isTemplate); + setHasTemplateQueries(hasTemplates); + } - // Set queries based on the current tab - if (currentTabIdRef.current === 'mutable-saved-queries') { - const allQueries = await savedQueryService.getAllSavedQueries(); - const mutableSavedQueries = allQueries.filter((q) => !q.attributes.isTemplate); + // Set queries based on the current tab if (currentTabIdRef.current === 'mutable-saved-queries') { - setSavedQueries(mutableSavedQueries); + const allQueries = await savedQueryService.getAllSavedQueries(); + const mutableSavedQueries = allQueries.filter((q) => !q.attributes.isTemplate); + if (currentTabIdRef.current === 'mutable-saved-queries') { + setSavedQueries(mutableSavedQueries); + } + } else if (currentTabIdRef.current === 'template-saved-queries') { + setSavedQueries(templateQueries); } - } else if (currentTabIdRef.current === 'template-saved-queries') { - setSavedQueries(templateQueries); + } catch (e) { + // eslint-disable-next-line no-console + console.error('Error occurred while retrieving saved queries.', e); + } finally { + setIsLoading(false); } - setIsLoading(false); }, [savedQueryService, currentTabIdRef, setSavedQueries, queryStringManager]); const updatePageIndex = useCallback((index: number) => { diff --git a/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx b/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx index 44c5ef384966..01f9b97e978f 100644 --- a/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx +++ b/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx @@ -45,7 +45,7 @@ import { import { i18n } from '@osd/i18n'; import React, { useCallback, useEffect, useState, Fragment, useRef } from 'react'; import { sortBy } from 'lodash'; -import { QueryStringManager, SavedQuery, SavedQueryService } from '../..'; +import { SavedQuery, SavedQueryService } from '../..'; import { SavedQueryListItem } from './saved_query_list_item'; import { toMountPoint, @@ -70,7 +70,6 @@ interface Props { onClearSavedQuery: () => void; closeMenuPopover: () => void; saveQuery: (savedQueryMeta: SavedQueryMeta, saveAsNew?: boolean) => Promise; - queryStringManager: QueryStringManager; } export function SavedQueryManagementComponent({ @@ -84,7 +83,6 @@ export function SavedQueryManagementComponent({ closeMenuPopover, useNewSavedQueryUI, saveQuery, - queryStringManager, }: Props) { const [savedQueries, setSavedQueries] = useState([] as SavedQuery[]); const [count, setTotalCount] = useState(0); @@ -258,7 +256,6 @@ export function SavedQueryManagementComponent({ onClose={() => openSavedQueryFlyout?.close().then()} onQueryOpen={onLoad} handleQueryDelete={handleDelete} - queryStringManager={queryStringManager} /> ) ); 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 d3f89d0f559d..f8b9694caabc 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 @@ -202,7 +202,6 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) isRefreshPaused={refreshInterval.pause} filters={filters} query={query} - queryStringManager={data.query.queryString} onFiltersUpdated={defaultFiltersUpdated(data.query)} onRefreshChange={defaultOnRefreshChange(data.query)} savedQuery={savedQuery} 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 3cd6cdcca25e..1f1b20b8c952 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -38,13 +38,7 @@ import { withOpenSearchDashboards, } from '../../../../opensearch_dashboards_react/public'; import { Filter, IIndexPattern, Query, TimeRange, UI_SETTINGS } from '../../../common'; -import { - SavedQuery, - SavedQueryAttributes, - TimeHistoryContract, - QueryStatus, - QueryStringManager, -} from '../../query'; +import { SavedQuery, SavedQueryAttributes, TimeHistoryContract, QueryStatus } from '../../query'; import { IDataPluginServices } from '../../types'; import { FilterBar } from '../filter_bar/filter_bar'; import { QueryEditorTopRow } from '../query_editor'; @@ -101,7 +95,6 @@ export interface SearchBarOwnProps { onRefresh?: (payload: { dateRange: TimeRange }) => void; indicateNoData?: boolean; queryStatus?: QueryStatus; - queryStringManager: QueryStringManager; } export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps; @@ -474,7 +467,6 @@ class SearchBarUI extends Component { useSaveQueryMenu={useSaveQueryMenu} isQueryEditorControl={isQueryEditorControl} saveQuery={this.onSave} - queryStringManager={this.props.queryStringManager} /> ) ); From 608911e3fa8576cc8078e49807e46de9e30eda56 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Tue, 19 Nov 2024 10:18:15 +0800 Subject: [PATCH 14/18] [chore]upgrade actions/upload-artifact to v4 (#8855) * upgrade actioins/upload-artifact to v4 Signed-off-by: Hailong Cui * Changeset file for PR #8855 created/updated --------- Signed-off-by: Hailong Cui Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- .github/workflows/build_and_test_workflow.yml | 8 +++--- .github/workflows/cypress_workflow.yml | 26 +++++++++---------- .../workflows/release_cypress_workflow.yml | 16 ++++++------ changelogs/fragments/8855.yml | 2 ++ 4 files changed, 27 insertions(+), 25 deletions(-) create mode 100644 changelogs/fragments/8855.yml diff --git a/.github/workflows/build_and_test_workflow.yml b/.github/workflows/build_and_test_workflow.yml index 49cdbe165961..40c335dcca9c 100644 --- a/.github/workflows/build_and_test_workflow.yml +++ b/.github/workflows/build_and_test_workflow.yml @@ -282,7 +282,7 @@ jobs: JOB: ci${{ matrix.group }} CACHE_DIR: ciGroup${{ matrix.group }} - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: failure() with: name: failure-artifacts-ci${{ matrix.group }} @@ -393,7 +393,7 @@ jobs: id: plugin-ftr-tests run: node scripts/functional_tests.js --config test/plugin_functional/config.ts - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: failure() with: name: failure-artifacts-plugin-functional-${{ matrix.os }} @@ -506,7 +506,7 @@ jobs: - name: Build `${{ matrix.name }}` run: yarn ${{ matrix.script }} --release - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: success() with: name: ${{ matrix.suffix }}-${{ env.VERSION }} @@ -595,7 +595,7 @@ jobs: run: | ./bwctest.sh -s false -o ${{ env.OPENSEARCH_URL }} -d ${{ steps.download.outputs.download-path }}/opensearch-dashboards-${{ env.VERSION }}-linux-x64.tar.gz - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: ${{ failure() && steps.verify-opensearch-exists.outputs.version-exists == 'true' }} with: name: ${{ matrix.version }}-test-failures diff --git a/.github/workflows/cypress_workflow.yml b/.github/workflows/cypress_workflow.yml index 3d3b0b79b027..c15edeac5e35 100644 --- a/.github/workflows/cypress_workflow.yml +++ b/.github/workflows/cypress_workflow.yml @@ -265,50 +265,50 @@ jobs: # Screenshots are only captured on failure, will change this once we do visual regression tests - name: Upload FT repo screenshots - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() && matrix.test_location == 'ftr' with: - name: ftr-cypress-screenshots + name: ftr-cypress-screenshots-${{ matrix.group }} path: ${{ env.FTR_PATH }}/cypress/screenshots retention-days: 1 - name: Upload FT repo videos - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() && matrix.test_location == 'ftr' with: - name: ftr-cypress-videos + name: ftr-cypress-videos-${{ matrix.group }} path: ${{ env.FTR_PATH }}/cypress/videos retention-days: 1 - name: Upload FT repo results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() && matrix.test_location == 'ftr' with: - name: ftr-cypress-results + name: ftr-cypress-results-${{ matrix.group }} path: ${{ env.FTR_PATH }}/cypress/results retention-days: 1 - name: Upload Dashboards screenshots if: failure() && matrix.test_location == 'source' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: dashboards-cypress-screenshots + name: dashboards-cypress-screenshots-${{ matrix.group }} path: cypress/screenshots retention-days: 1 - name: Upload Dashboards repo videos - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() && matrix.test_location == 'source' with: - name: dashboards-cypress-videos + name: dashboards-cypress-videos-${{ matrix.group }} path: cypress/videos retention-days: 1 - name: Upload Dashboards repo results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() && matrix.test_location == 'source' with: - name: dashboards-cypress-results + name: dashboards-cypress-results-${{ matrix.group }} path: cypress/results retention-days: 1 @@ -346,6 +346,6 @@ jobs: '${{ env.SPEC }}' ``` - #### Link to results: + #### Link to results: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} edit-mode: replace diff --git a/.github/workflows/release_cypress_workflow.yml b/.github/workflows/release_cypress_workflow.yml index f58757b23f9f..bb9895d9f048 100644 --- a/.github/workflows/release_cypress_workflow.yml +++ b/.github/workflows/release_cypress_workflow.yml @@ -72,7 +72,7 @@ jobs: CI: 1 # avoid warnings like "tput: No value for $TERM and no -T specified" TERM: xterm - name: Run cypress tests (osd:ciGroup${{ matrix.spec_group }}) ${{ inputs.UNIQUE_ID}} + name: Run cypress tests (osd:ciGroup${{ matrix.spec_group }}) ${{ inputs.UNIQUE_ID}} steps: - name: Checkout code uses: actions/checkout@v2 @@ -130,7 +130,7 @@ jobs: mkdir -p $CWD/${{ env.OPENSEARCH_DIR }} source ${{ env.OSD_PATH }}/scripts/common/utils.sh open_artifact $CWD/${{ env.OPENSEARCH_DIR }} ${{ env.OPENSEARCH }} - + - name: Download and extract OpenSearch Dashboards artifacts run: | CWD=$(pwd) @@ -138,22 +138,22 @@ jobs: source ${{ env.OSD_PATH }}/scripts/common/utils.sh open_artifact $CWD/${{ env.DASHBOARDS_DIR }} ${{ env.DASHBOARDS }} - - name: Run Cypress tests + - name: Run Cypress tests run: | chown -R 1000:1000 `pwd` su `id -un 1000` -c "source ${{ env.OSD_PATH }}/scripts/cypress_tests.sh && run_dashboards_cypress_tests" # Screenshots are only captured on failures - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: failure() with: - name: release-osd-cypress-screenshots + name: release-osd-cypress-screenshots-${{ matrix.spec_group }} path: ${{ env.OSD_PATH }}/cypress/screenshots retention-days: 1 - - - uses: actions/upload-artifact@v3 + + - uses: actions/upload-artifact@v4 if: always() with: - name: release-osd-cypress-videos + name: release-osd-cypress-videos-${{ matrix.spec_group }} path: ${{ env.OSD_PATH }}/cypress/videos retention-days: 1 diff --git a/changelogs/fragments/8855.yml b/changelogs/fragments/8855.yml new file mode 100644 index 000000000000..ad9835ebe292 --- /dev/null +++ b/changelogs/fragments/8855.yml @@ -0,0 +1,2 @@ +fix: +- Upgrade actions/upload-artifact to v4 ([#8855](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8855)) \ No newline at end of file From a8d383bc63adc477937acebd5664d748d671e8b9 Mon Sep 17 00:00:00 2001 From: Amardeepsingh Siglani Date: Tue, 19 Nov 2024 12:22:35 -0800 Subject: [PATCH 15/18] Use currently selected data source when no source attached to saved query (#8883) Opening a saved query that has no dataset stored with it, resets the currently selected dataset in the picker which breaks the query experience since the user will need to reselect the dataset which will then reset the query. * use currently selected data source when no source attached to saved query Signed-off-by: Amardeepsingh Siglani * Changeset file for PR #8883 created/updated * refactored fix Signed-off-by: Amardeepsingh Siglani * revert license change Signed-off-by: Amardeepsingh Siglani --------- Signed-off-by: Amardeepsingh Siglani Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8883.yml | 2 ++ .../open_saved_query_flyout.tsx | 24 +++++++-------- .../saved_query_flyouts/save_query_flyout.tsx | 1 - .../public/ui/saved_query_form/helpers.tsx | 30 +------------------ .../ui/saved_query_form/save_query_form.tsx | 4 --- .../populate_state_from_saved_query.test.ts | 12 ++++++-- .../lib/populate_state_from_saved_query.ts | 6 +++- .../data/public/ui/search_bar/search_bar.tsx | 6 +--- 8 files changed, 29 insertions(+), 56 deletions(-) create mode 100644 changelogs/fragments/8883.yml diff --git a/changelogs/fragments/8883.yml b/changelogs/fragments/8883.yml new file mode 100644 index 000000000000..d9254d81c3cf --- /dev/null +++ b/changelogs/fragments/8883.yml @@ -0,0 +1,2 @@ +fix: +- Retain currently selected dataset when opening saved query without dataset info ([#8883](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8883)) \ No newline at end of file diff --git a/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx b/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx index 212e0228e626..099f3e1f0420 100644 --- a/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx +++ b/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx @@ -26,7 +26,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { i18n } from '@osd/i18n'; import { SavedQuery, SavedQueryService } from '../../query'; import { SavedQueryCard } from './saved_query_card'; -import { Query } from '../../../common'; import { getQueryService } from '../../services'; export interface OpenSavedQueryFlyoutProps { @@ -306,19 +305,16 @@ export function OpenSavedQueryFlyout({ fill onClick={() => { if (selectedQuery) { - if ( - // Template queries are not associated with data sources. Apply data source from current query - selectedQuery.attributes.isTemplate - ) { - const updatedQuery: Query = { - ...queryStringManager?.getQuery(), - query: selectedQuery.attributes.query.query, - language: selectedQuery.attributes.query.language, - }; - queryStringManager.setQuery(updatedQuery); - } else { - onQueryOpen(selectedQuery); - } + onQueryOpen({ + ...selectedQuery, + attributes: { + ...selectedQuery.attributes, + query: { + ...selectedQuery.attributes.query, + dataset: queryStringManager.getQuery().dataset, + }, + }, + }); onClose(); } }} diff --git a/src/plugins/data/public/ui/saved_query_flyouts/save_query_flyout.tsx b/src/plugins/data/public/ui/saved_query_flyouts/save_query_flyout.tsx index c0356b864485..f62a60f7e9c7 100644 --- a/src/plugins/data/public/ui/saved_query_flyouts/save_query_flyout.tsx +++ b/src/plugins/data/public/ui/saved_query_flyouts/save_query_flyout.tsx @@ -43,7 +43,6 @@ export function SaveQueryFlyout({ savedQueryService={savedQueryService} showFilterOption={showFilterOption} showTimeFilterOption={showTimeFilterOption} - showDataSourceOption={true} setSaveAsNew={(shouldSaveAsNew) => setSaveAsNew(shouldSaveAsNew)} savedQuery={saveAsNew ? undefined : savedQuery} saveAsNew={saveAsNew} diff --git a/src/plugins/data/public/ui/saved_query_form/helpers.tsx b/src/plugins/data/public/ui/saved_query_form/helpers.tsx index 467eac2de475..ad3de3acde3f 100644 --- a/src/plugins/data/public/ui/saved_query_form/helpers.tsx +++ b/src/plugins/data/public/ui/saved_query_form/helpers.tsx @@ -57,7 +57,6 @@ interface Props { formUiType: 'Modal' | 'Flyout'; showFilterOption?: boolean; showTimeFilterOption?: boolean; - showDataSourceOption?: boolean; saveAsNew?: boolean; setSaveAsNew?: (shouldSaveAsNew: boolean) => void; cannotBeOverwritten?: boolean; @@ -70,7 +69,6 @@ export function useSaveQueryFormContent({ onClose, showFilterOption = true, showTimeFilterOption = true, - showDataSourceOption = false, formUiType, saveAsNew, setSaveAsNew, @@ -81,7 +79,6 @@ export function useSaveQueryFormContent({ const [description, setDescription] = useState(''); const [savedQueries, setSavedQueries] = useState([]); const [shouldIncludeFilters, setShouldIncludeFilters] = useState(true); - const [shouldIncludeDataSource, setShouldIncludeDataSource] = useState(true); // Defaults to false because saved queries are meant to be as portable as possible and loading // a saved query with a time filter will override whatever the current value of the global timepicker // is. We expect this option to be used rarely and only when the user knows they want this behavior. @@ -96,7 +93,6 @@ export function useSaveQueryFormContent({ setDescription(savedQuery?.description || ''); setShouldIncludeFilters(savedQuery ? !!savedQuery.filters : true); setIncludeTimefilter(!!savedQuery?.timefilter); - setShouldIncludeDataSource(savedQuery ? !!savedQuery.query.dataset : true); setFormErrors([]); }, [savedQuery]); @@ -147,18 +143,9 @@ export function useSaveQueryFormContent({ description, shouldIncludeFilters, shouldIncludeTimeFilter, - shouldIncludeDataSource, }); } - }, [ - validate, - onSave, - title, - description, - shouldIncludeFilters, - shouldIncludeTimeFilter, - shouldIncludeDataSource, - ]); + }, [validate, onSave, title, description, shouldIncludeFilters, shouldIncludeTimeFilter]); const onInputChange = useCallback((event) => { setEnabledSaveButton(Boolean(event.target.value)); @@ -229,21 +216,6 @@ export function useSaveQueryFormContent({ data-test-subj="saveQueryFormDescription" /> - {showDataSourceOption && ( - - { - setShouldIncludeDataSource(!shouldIncludeDataSource); - }} - data-test-subj="saveQueryFormIncludeDataSourceOption" - /> - - )} {showFilterOption && ( void; showFilterOption?: boolean; showTimeFilterOption?: boolean; - showDataSourceOption?: boolean; saveAsNew?: boolean; cannotBeOverwritten?: boolean; } @@ -63,7 +62,6 @@ export interface SavedQueryMeta { description: string; shouldIncludeFilters: boolean; shouldIncludeTimeFilter: boolean; - shouldIncludeDataSource: boolean; } export function SaveQueryForm({ @@ -74,7 +72,6 @@ export function SaveQueryForm({ onClose, showFilterOption = true, showTimeFilterOption = true, - showDataSourceOption = false, saveAsNew, setSaveAsNew, cannotBeOverwritten, @@ -87,7 +84,6 @@ export function SaveQueryForm({ onClose, showFilterOption, showTimeFilterOption, - showDataSourceOption, saveAsNew, setSaveAsNew, cannotBeOverwritten, diff --git a/src/plugins/data/public/ui/search_bar/lib/populate_state_from_saved_query.test.ts b/src/plugins/data/public/ui/search_bar/lib/populate_state_from_saved_query.test.ts index 52c3f981296b..b172d8c42a76 100644 --- a/src/plugins/data/public/ui/search_bar/lib/populate_state_from_saved_query.test.ts +++ b/src/plugins/data/public/ui/search_bar/lib/populate_state_from_saved_query.test.ts @@ -47,6 +47,11 @@ describe('populateStateFromSavedQuery', () => { query: { query: 'test', language: 'kuery', + dataset: { + id: 'saved-query-dataset', + title: 'saved-query-dataset', + type: 'INDEX', + }, }, }, }; @@ -57,12 +62,15 @@ describe('populateStateFromSavedQuery', () => { dataMock.query.filterManager.getGlobalFilters = jest.fn().mockReturnValue([]); }); - it('should set query', async () => { + it('should set query with current dataset', async () => { const savedQuery: SavedQuery = { ...baseSavedQuery, }; populateStateFromSavedQuery(dataMock.query, savedQuery); - expect(dataMock.query.queryString.setQuery).toHaveBeenCalled(); + expect(dataMock.query.queryString.setQuery).toHaveBeenCalledWith({ + ...savedQuery.attributes.query, + dataset: dataMock.query.queryString.getQuery().dataset, + }); }); it('should set filters', async () => { diff --git a/src/plugins/data/public/ui/search_bar/lib/populate_state_from_saved_query.ts b/src/plugins/data/public/ui/search_bar/lib/populate_state_from_saved_query.ts index 382fd382ac01..abab61dfe82e 100644 --- a/src/plugins/data/public/ui/search_bar/lib/populate_state_from_saved_query.ts +++ b/src/plugins/data/public/ui/search_bar/lib/populate_state_from_saved_query.ts @@ -48,7 +48,11 @@ export const populateStateFromSavedQuery = (queryService: QueryStart, savedQuery } // query string - queryString.setQuery(savedQuery.attributes.query); + queryString.setQuery({ + ...savedQuery.attributes.query, + // We should keep the currently selected dataset intact + dataset: queryString.getQuery().dataset, + }); // filters const savedQueryFilters = savedQuery.attributes.filters || []; 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 1f1b20b8c952..251a0dc86fa0 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -45,7 +45,6 @@ import { QueryEditorTopRow } from '../query_editor'; import QueryBarTopRow from '../query_string_input/query_bar_top_row'; import { SavedQueryMeta, SaveQueryForm } from '../saved_query_form'; import { FilterOptions } from '../filter_bar/filter_options'; -import { getUseNewSavedQueriesUI } from '../../services'; interface SearchBarInjectedDeps { opensearchDashboards: OpenSearchDashboardsReactContextValue; @@ -285,11 +284,8 @@ class SearchBarUI extends Component { public onSave = async (savedQueryMeta: SavedQueryMeta, saveAsNew = false) => { if (!this.state.query) return; - const query = cloneDeep(this.state.query); - if (getUseNewSavedQueriesUI() && !savedQueryMeta.shouldIncludeDataSource) { - delete query.dataset; - } + delete query.dataset; const savedQueryAttributes: SavedQueryAttributes = { title: savedQueryMeta.title, From d3f539c6495f233bf5dc207e727ca05a58c07494 Mon Sep 17 00:00:00 2001 From: Amardeepsingh Siglani Date: Tue, 19 Nov 2024 18:53:41 -0800 Subject: [PATCH 16/18] Added framework to get default query string using dataset and language combination (#8896) * added framework to get default query using dataset Signed-off-by: Amardeepsingh Siglani * Changeset file for PR #8896 created/updated * deduped code Signed-off-by: Amardeepsingh Siglani * added UTs; minor refactor Signed-off-by: Amardeepsingh Siglani --------- Signed-off-by: Amardeepsingh Siglani Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8896.yml | 2 ++ .../query_string/dataset_service/types.ts | 5 +++ .../query_string/query_string_manager.test.ts | 32 +++++++++++++++++++ .../query_string/query_string_manager.ts | 30 +++++++++++------ 4 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 changelogs/fragments/8896.yml diff --git a/changelogs/fragments/8896.yml b/changelogs/fragments/8896.yml new file mode 100644 index 000000000000..a1a03c05f257 --- /dev/null +++ b/changelogs/fragments/8896.yml @@ -0,0 +1,2 @@ +feat: +- Added framework to get default query string using dataset and language combination ([#8896](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8896)) \ No newline at end of file diff --git a/src/plugins/data/public/query/query_string/dataset_service/types.ts b/src/plugins/data/public/query/query_string/dataset_service/types.ts index 0b8bcd402d15..65c322acec6f 100644 --- a/src/plugins/data/public/query/query_string/dataset_service/types.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/types.ts @@ -8,6 +8,7 @@ import { DatasetField, DatasetSearchOptions, DataStructure, + Query, SavedObject, } from '../../../../common'; import { IDataPluginServices } from '../../../types'; @@ -106,4 +107,8 @@ export interface DatasetTypeConfig { * Service used for indexedViews related operations */ indexedViewsService?: DatasetIndexedViewsService; + /** + * Returns the initial query that is added to the query editor when a dataset is selected. + */ + getInitialQueryString?: (query: Query) => string | void; } diff --git a/src/plugins/data/public/query/query_string/query_string_manager.test.ts b/src/plugins/data/public/query/query_string/query_string_manager.test.ts index 758d658864ab..4bce5d7159db 100644 --- a/src/plugins/data/public/query/query_string/query_string_manager.test.ts +++ b/src/plugins/data/public/query/query_string/query_string_manager.test.ts @@ -308,5 +308,37 @@ describe('QueryStringManager', () => { expect(result.dataset).toEqual(currentDataset); expect(result.query).toBeDefined(); }); + + test('getInitialQueryByLanguage returns the initial query from the dataset config if present', () => { + service.getDatasetService().getType = jest.fn().mockReturnValue({ + supportedLanguages: jest.fn(), + getInitialQueryString: jest.fn().mockImplementation(({ language }) => { + switch (language) { + case 'sql': + return 'default sql dataset query'; + case 'ppl': + return 'default ppl dataset query'; + } + }), + }); + + const sqlQuery = service.getInitialQueryByLanguage('sql'); + expect(sqlQuery).toHaveProperty('query', 'default sql dataset query'); + + const pplQuery = service.getInitialQueryByLanguage('ppl'); + expect(pplQuery).toHaveProperty('query', 'default ppl dataset query'); + }); + + test('getInitialQueryByLanguage returns the initial query from the language config if dataset does not provide one', () => { + service.getDatasetService().getType = jest.fn().mockReturnValue({ + supportedLanguages: jest.fn(), + }); + service.getLanguageService().getLanguage = jest.fn().mockReturnValue({ + getQueryString: jest.fn().mockReturnValue('default-language-service-query'), + }); + + const sqlQuery = service.getInitialQueryByLanguage('sql'); + expect(sqlQuery).toHaveProperty('query', 'default-language-service-query'); + }); }); }); diff --git a/src/plugins/data/public/query/query_string/query_string_manager.ts b/src/plugins/data/public/query/query_string/query_string_manager.ts index 33bfc7d5d10b..47b6d536db6f 100644 --- a/src/plugins/data/public/query/query_string/query_string_manager.ts +++ b/src/plugins/data/public/query/query_string/query_string_manager.ts @@ -63,6 +63,21 @@ export class QueryStringManager { return this.storage.get('userQueryString') || ''; } + private getInitialDatasetQueryString(query: Query) { + const { language, dataset } = query; + + const languageConfig = this.languageService.getLanguage(language); + let typeConfig; + + if (dataset) { + typeConfig = this.datasetService.getType(dataset.type); + } + + return ( + typeConfig?.getInitialQueryString?.(query) ?? (languageConfig?.getQueryString(query) || '') + ); + } + public getDefaultQuery(): Query { const defaultLanguageId = this.getDefaultLanguage(); const defaultQuery = this.getDefaultQueryString(); @@ -79,13 +94,11 @@ export class QueryStringManager { defaultDataset && this.languageService ) { - const language = this.languageService.getLanguage(defaultLanguageId); const newQuery = { ...query, dataset: defaultDataset }; - const newQueryString = language?.getQueryString(newQuery) || ''; return { ...newQuery, - query: newQueryString, + query: this.getInitialDatasetQueryString(newQuery), }; } @@ -244,13 +257,12 @@ export class QueryStringManager { // Both language and dataset provided - generate fresh query if (language && dataset) { - const languageService = this.languageService.getLanguage(language); const newQuery = { language, dataset, query: '', }; - newQuery.query = languageService?.getQueryString(newQuery) || ''; + newQuery.query = this.getInitialDatasetQueryString(newQuery); return newQuery; } @@ -274,12 +286,12 @@ export class QueryStringManager { */ public getInitialQueryByLanguage = (languageId: string) => { const curQuery = this.query$.getValue(); - const language = this.languageService.getLanguage(languageId); const newQuery = { ...curQuery, language: languageId, }; - const queryString = language?.getQueryString(newQuery) || ''; + + const queryString = this.getInitialDatasetQueryString(newQuery); this.languageService.setUserQueryString(queryString); return { @@ -296,17 +308,15 @@ export class QueryStringManager { const curQuery = this.query$.getValue(); // Use dataset's preferred language or fallback to current language const languageId = newDataset.language || curQuery.language; - const language = this.languageService.getLanguage(languageId); const newQuery = { ...curQuery, language: languageId, dataset: newDataset, }; - const queryString = language?.getQueryString(newQuery) || ''; return { ...newQuery, - query: queryString, + query: this.getInitialDatasetQueryString(newQuery, newDataset), }; }; From a10750901857f838b1d92adfd27e4c46e6c7134b Mon Sep 17 00:00:00 2001 From: Amardeepsingh Siglani Date: Tue, 19 Nov 2024 20:50:57 -0800 Subject: [PATCH 17/18] Only support copy for query templates (#8899) * only support copy for query templates Signed-off-by: Amardeepsingh Siglani * Changeset file for PR #8899 created/updated * clear selected query on tab change; keep button disabled when query is not selected Signed-off-by: Amardeepsingh Siglani --------- Signed-off-by: Amardeepsingh Siglani Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8899.yml | 2 + .../open_saved_query_flyout.tsx | 78 +++++++++++++------ .../saved_query_management_component.tsx | 3 +- 3 files changed, 58 insertions(+), 25 deletions(-) create mode 100644 changelogs/fragments/8899.yml diff --git a/changelogs/fragments/8899.yml b/changelogs/fragments/8899.yml new file mode 100644 index 000000000000..11030aecb552 --- /dev/null +++ b/changelogs/fragments/8899.yml @@ -0,0 +1,2 @@ +fix: +- Only support copy action for query templates ([#8899](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8899)) \ No newline at end of file diff --git a/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx b/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx index 099f3e1f0420..d9c2941adc8d 100644 --- a/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx +++ b/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx @@ -21,15 +21,18 @@ import { EuiTablePagination, EuiTitle, Pager, + copyToClipboard, } from '@elastic/eui'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { i18n } from '@osd/i18n'; +import { NotificationsStart } from 'opensearch-dashboards/public'; import { SavedQuery, SavedQueryService } from '../../query'; import { SavedQueryCard } from './saved_query_card'; import { getQueryService } from '../../services'; export interface OpenSavedQueryFlyoutProps { savedQueryService: SavedQueryService; + notifications?: NotificationsStart; onClose: () => void; onQueryOpen: (query: SavedQuery) => void; handleQueryDelete: (query: SavedQuery) => Promise; @@ -44,13 +47,21 @@ interface SavedQuerySearchableItem { savedQuery: SavedQuery; } +enum OPEN_QUERY_TAB_ID { + SAVED_QUERIES = 'saved-queries', + QUERY_TEMPLATES = 'query-templates', +} + export function OpenSavedQueryFlyout({ savedQueryService, + notifications, onClose, onQueryOpen, handleQueryDelete, }: OpenSavedQueryFlyoutProps) { - const [selectedTabId, setSelectedTabId] = useState('mutable-saved-queries'); + const [selectedTabId, setSelectedTabId] = useState( + OPEN_QUERY_TAB_ID.SAVED_QUERIES + ); const [savedQueries, setSavedQueries] = useState([]); const [hasTemplateQueries, setHasTemplateQueries] = useState(false); const [itemsPerPage, setItemsPerPage] = useState(10); @@ -85,13 +96,13 @@ export function OpenSavedQueryFlyout({ } // Set queries based on the current tab - if (currentTabIdRef.current === 'mutable-saved-queries') { + if (currentTabIdRef.current === OPEN_QUERY_TAB_ID.SAVED_QUERIES) { const allQueries = await savedQueryService.getAllSavedQueries(); const mutableSavedQueries = allQueries.filter((q) => !q.attributes.isTemplate); - if (currentTabIdRef.current === 'mutable-saved-queries') { + if (currentTabIdRef.current === OPEN_QUERY_TAB_ID.SAVED_QUERIES) { setSavedQueries(mutableSavedQueries); } - } else if (currentTabIdRef.current === 'template-saved-queries') { + } else if (currentTabIdRef.current === OPEN_QUERY_TAB_ID.QUERY_TEMPLATES) { setSavedQueries(templateQueries); } } catch (e) { @@ -111,6 +122,7 @@ export function OpenSavedQueryFlyout({ fetchAllSavedQueriesForSelectedTab(); setSearchQuery(EuiSearchBar.Query.MATCH_ALL); updatePageIndex(0); + setSelectedQuery(undefined); }, [selectedTabId, fetchAllSavedQueriesForSelectedTab, updatePageIndex]); useEffect(() => { @@ -261,7 +273,7 @@ export function OpenSavedQueryFlyout({ const tabs = [ { - id: 'mutable-saved-queries', + id: OPEN_QUERY_TAB_ID.SAVED_QUERIES, name: 'Saved queries', content: flyoutBodyContent, }, @@ -269,12 +281,43 @@ export function OpenSavedQueryFlyout({ if (hasTemplateQueries) { tabs.push({ - id: 'template-saved-queries', + id: OPEN_QUERY_TAB_ID.QUERY_TEMPLATES, name: 'Templates', content: flyoutBodyContent, }); } + const onQueryAction = useCallback(() => { + if (!selectedQuery) { + return; + } + + if (selectedQuery?.attributes.isTemplate) { + copyToClipboard(selectedQuery.attributes.query.query as string); + notifications?.toasts.addSuccess({ + title: i18n.translate('data.openSavedQueryFlyout.queryCopied.title', { + defaultMessage: 'Query copied', + }), + text: i18n.translate('data.openSavedQueryFlyout.queryCopied.text', { + defaultMessage: 'Paste the query in the editor to modify and run.', + }), + }); + } else { + onQueryOpen({ + ...selectedQuery, + attributes: { + ...selectedQuery.attributes, + query: { + ...selectedQuery.attributes.query, + dataset: queryStringManager.getQuery().dataset, + }, + }, + }); + } + + onClose(); + }, [onClose, onQueryOpen, notifications, selectedQuery, queryStringManager]); + return ( @@ -287,8 +330,8 @@ export function OpenSavedQueryFlyout({ tabs={tabs} initialSelectedTab={tabs[0]} onTabClick={(tab) => { - setSelectedTabId(tab.id); - currentTabIdRef.current = tab.id; + setSelectedTabId(tab.id as OPEN_QUERY_TAB_ID); + currentTabIdRef.current = tab.id as OPEN_QUERY_TAB_ID; }} /> @@ -303,23 +346,10 @@ export function OpenSavedQueryFlyout({ { - if (selectedQuery) { - onQueryOpen({ - ...selectedQuery, - attributes: { - ...selectedQuery.attributes, - query: { - ...selectedQuery.attributes.query, - dataset: queryStringManager.getQuery().dataset, - }, - }, - }); - onClose(); - } - }} + onClick={onQueryAction} + data-testid="open-query-action-button" > - Open query + {selectedTabId === OPEN_QUERY_TAB_ID.SAVED_QUERIES ? 'Open' : 'Copy'} query diff --git a/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx b/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx index 01f9b97e978f..94898bfe57a2 100644 --- a/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx +++ b/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx @@ -89,7 +89,7 @@ export function SavedQueryManagementComponent({ const [activePage, setActivePage] = useState(0); const cancelPendingListingRequest = useRef<() => void>(() => {}); const { - services: { overlays }, + services: { overlays, notifications }, } = useOpenSearchDashboards(); useEffect(() => { @@ -253,6 +253,7 @@ export function SavedQueryManagementComponent({ toMountPoint( openSavedQueryFlyout?.close().then()} onQueryOpen={onLoad} handleQueryDelete={handleDelete} From c928aec215e9b2bf15a5eebf8b33b858740eaa3e Mon Sep 17 00:00:00 2001 From: Amardeepsingh Siglani Date: Wed, 20 Nov 2024 14:15:42 -0800 Subject: [PATCH 18/18] removed extra param getInitialDatasetQueryString in query_string_manager (#8902) * removed extra param Signed-off-by: Amardeepsingh Siglani * Changeset file for PR #8902 created/updated --------- Signed-off-by: Amardeepsingh Siglani Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8902.yml | 2 ++ .../data/public/query/query_string/query_string_manager.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/8902.yml diff --git a/changelogs/fragments/8902.yml b/changelogs/fragments/8902.yml new file mode 100644 index 000000000000..d4658d0296a7 --- /dev/null +++ b/changelogs/fragments/8902.yml @@ -0,0 +1,2 @@ +fix: +- Removed extra parameter ([#8902](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8902)) \ No newline at end of file diff --git a/src/plugins/data/public/query/query_string/query_string_manager.ts b/src/plugins/data/public/query/query_string/query_string_manager.ts index 47b6d536db6f..2bb5f41fbc19 100644 --- a/src/plugins/data/public/query/query_string/query_string_manager.ts +++ b/src/plugins/data/public/query/query_string/query_string_manager.ts @@ -316,7 +316,7 @@ export class QueryStringManager { return { ...newQuery, - query: this.getInitialDatasetQueryString(newQuery, newDataset), + query: this.getInitialDatasetQueryString(newQuery), }; };