From 27738481b9eff3457c057b4c51fa8b6fd538a47b Mon Sep 17 00:00:00 2001
From: Amardeepsingh Siglani
Date: Fri, 25 Oct 2024 08:44:56 -0700
Subject: [PATCH] Saved queries new UI (#8469)
* loading query functional
Signed-off-by: Amardeepsingh Siglani
* search bar working; save working
Signed-off-by: Amardeepsingh Siglani
* implemented pagination
Signed-off-by: Amardeepsingh Siglani
* Changeset file for PR #8469 created/updated
* implemented tab for template queries; updated UI elements for new mocks
Signed-off-by: Amardeepsingh Siglani
* added i18n; minor refactors
Signed-off-by: Amardeepsingh Siglani
* refactored css
Signed-off-by: Amardeepsingh Siglani
* addressed comments
Signed-off-by: Amardeepsingh Siglani
* fixed i18n error
Signed-off-by: Amardeepsingh Siglani
* show template queries tab only when templates present
Signed-off-by: Amardeepsingh Siglani
* addressed PR comments
Signed-off-by: Amardeepsingh Siglani
* fixed unit tests
Signed-off-by: Amardeepsingh Siglani
* introduced a new gate for the changes
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/8469.yml | 2 +
config/opensearch_dashboards.yml | 5 +-
src/plugins/data/config.ts | 3 +
src/plugins/data/public/plugin.ts | 10 +-
.../data/public/query/query_service.ts | 4 +-
.../saved_query/saved_query_service.test.ts | 7 +-
.../query/saved_query/saved_query_service.ts | 50 +--
.../data/public/query/saved_query/types.ts | 3 +
src/plugins/data/public/services.ts | 10 +
src/plugins/data/public/ui/_index.scss | 1 +
.../public/ui/filter_bar/filter_options.tsx | 24 +-
.../open_saved_query_flyout.tsx | 283 +++++++++++++++
.../saved_query_flyouts/save_query_flyout.tsx | 53 +++
.../saved_query_flyouts/saved_query_card.tsx | 196 ++++++++++
.../saved_query_flyouts.scss | 23 ++
.../public/ui/saved_query_form/helpers.tsx | 334 ++++++++++++++++++
.../data/public/ui/saved_query_form/index.ts | 2 +-
.../ui/saved_query_form/save_query_form.tsx | 246 +++----------
.../delete_saved_query_confirmation_modal.tsx | 72 ++++
.../saved_query_list_item.tsx | 23 +-
.../saved_query_management_component.test.tsx | 13 +-
.../saved_query_management_component.tsx | 103 +++++-
.../data/public/ui/search_bar/search_bar.tsx | 29 +-
src/plugins/data/server/index.ts | 1 +
.../data/server/saved_objects/query.ts | 31 +-
25 files changed, 1244 insertions(+), 284 deletions(-)
create mode 100644 changelogs/fragments/8469.yml
create mode 100644 src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx
create mode 100644 src/plugins/data/public/ui/saved_query_flyouts/save_query_flyout.tsx
create mode 100644 src/plugins/data/public/ui/saved_query_flyouts/saved_query_card.tsx
create mode 100644 src/plugins/data/public/ui/saved_query_flyouts/saved_query_flyouts.scss
create mode 100644 src/plugins/data/public/ui/saved_query_form/helpers.tsx
create mode 100644 src/plugins/data/public/ui/saved_query_management/delete_saved_query_confirmation_modal.tsx
diff --git a/changelogs/fragments/8469.yml b/changelogs/fragments/8469.yml
new file mode 100644
index 000000000000..3c7cfdb44e17
--- /dev/null
+++ b/changelogs/fragments/8469.yml
@@ -0,0 +1,2 @@
+feat:
+- Enhances the saved query UX ([#8469](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8469))
\ No newline at end of file
diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml
index a36d673349b7..7b21bbd5c0dc 100644
--- a/config/opensearch_dashboards.yml
+++ b/config/opensearch_dashboards.yml
@@ -374,4 +374,7 @@
# The default config is [], and no one will be dashboard admin.
# If the user config is set to wildcard ["*"], anyone will be dashboard admin.
# opensearchDashboards.dashboardAdmin.groups: ["dashboard_admin"]
-# opensearchDashboards.dashboardAdmin.users: ["dashboard_admin"]
\ No newline at end of file
+# opensearchDashboards.dashboardAdmin.users: ["dashboard_admin"]
+
+# Set the value to true to enable the new UI for savedQueries in Discover
+# data.savedQueriesNewUI.enabled: true
diff --git a/src/plugins/data/config.ts b/src/plugins/data/config.ts
index 186691f3c917..50cf5aef413b 100644
--- a/src/plugins/data/config.ts
+++ b/src/plugins/data/config.ts
@@ -57,6 +57,9 @@ export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: false }),
}),
}),
+ savedQueriesNewUI: schema.object({
+ enabled: schema.boolean({ defaultValue: false }),
+ }),
});
export type ConfigSchema = TypeOf;
diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts
index 70977deb5d1b..0354c33796fc 100644
--- a/src/plugins/data/public/plugin.ts
+++ b/src/plugins/data/public/plugin.ts
@@ -54,6 +54,7 @@ import {
} from './index_patterns';
import {
setApplication,
+ setUseNewSavedQueriesUI,
setFieldFormats,
setIndexPatterns,
setNotifications,
@@ -93,7 +94,7 @@ import { DEFAULT_DATA_SOURCE_TYPE } from './data_sources/constants';
import { getSuggestions as getSQLSuggestions } from './antlr/opensearch_sql/code_completion';
import { getSuggestions as getDQLSuggestions } from './antlr/dql/code_completion';
import { getSuggestions as getPPLSuggestions } from './antlr/opensearch_ppl/code_completion';
-import { createStorage, DataStorage } from '../common';
+import { createStorage, DataStorage, UI_SETTINGS } from '../common';
declare module '../../ui_actions/public' {
export interface ActionContextMapping {
@@ -118,6 +119,7 @@ export class DataPublicPlugin
private readonly queryService: QueryService;
private readonly storage: DataStorage;
private readonly sessionStorage: DataStorage;
+ private readonly config: ConfigSchema;
constructor(initializerContext: PluginInitializerContext) {
this.searchService = new SearchService(initializerContext);
@@ -130,6 +132,7 @@ export class DataPublicPlugin
engine: window.sessionStorage,
prefix: 'opensearchDashboards.',
});
+ this.config = initializerContext.config.get();
}
public setup(
@@ -178,6 +181,11 @@ export class DataPublicPlugin
autoComplete.addQuerySuggestionProvider('kuery', getDQLSuggestions);
autoComplete.addQuerySuggestionProvider('PPL', getPPLSuggestions);
+ const useNewSavedQueriesUI =
+ core.uiSettings.get(UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED) &&
+ this.config.savedQueriesNewUI.enabled;
+ setUseNewSavedQueriesUI(useNewSavedQueriesUI);
+
return {
// TODO: MQL
autocomplete: this.autocomplete.setup(core),
diff --git a/src/plugins/data/public/query/query_service.ts b/src/plugins/data/public/query/query_service.ts
index b009d4eeef01..9211ef4fc89b 100644
--- a/src/plugins/data/public/query/query_service.ts
+++ b/src/plugins/data/public/query/query_service.ts
@@ -111,8 +111,8 @@ export class QueryService {
queryString: this.queryStringManager,
savedQueries: createSavedQueryService(
savedObjectsClient,
- this.queryStringManager,
- application
+ { application, uiSettings },
+ this.queryStringManager
),
state$: this.state$,
timefilter: this.timefilter,
diff --git a/src/plugins/data/public/query/saved_query/saved_query_service.test.ts b/src/plugins/data/public/query/saved_query/saved_query_service.test.ts
index 9fc8cdf61016..b50c47f80408 100644
--- a/src/plugins/data/public/query/saved_query/saved_query_service.test.ts
+++ b/src/plugins/data/public/query/saved_query/saved_query_service.test.ts
@@ -31,6 +31,7 @@
import { createSavedQueryService } from './saved_query_service';
import { FilterStateStore } from '../../../common';
import { SavedQueryAttributes } from './types';
+import { applicationServiceMock, uiSettingsServiceMock } from '../../../../../core/public/mocks';
const savedQueryAttributes: SavedQueryAttributes = {
title: 'foo',
@@ -89,7 +90,11 @@ const {
getSavedQueryCount,
} = createSavedQueryService(
// @ts-ignore
- mockSavedObjectsClient
+ mockSavedObjectsClient,
+ {
+ application: applicationServiceMock.create(),
+ uiSettings: uiSettingsServiceMock.createStartContract(),
+ }
);
describe('saved query service', () => {
diff --git a/src/plugins/data/public/query/saved_query/saved_query_service.ts b/src/plugins/data/public/query/saved_query/saved_query_service.ts
index 8fba257fad27..92af9b7c024e 100644
--- a/src/plugins/data/public/query/saved_query/saved_query_service.ts
+++ b/src/plugins/data/public/query/saved_query/saved_query_service.ts
@@ -33,27 +33,24 @@ import { SavedObjectsClientContract, SavedObjectAttributes, CoreStart } from 'sr
import { first } from 'rxjs/operators';
import { SavedQueryAttributes, SavedQuery, SavedQueryService } from './types';
import { QueryStringContract } from '../query_string';
+import { getUseNewSavedQueriesUI } from '../../services';
-type SerializedSavedQueryAttributes = SavedObjectAttributes &
- SavedQueryAttributes & {
- query: {
- query: string;
- language: string;
- };
- };
+type SerializedSavedQueryAttributes = SavedObjectAttributes & SavedQueryAttributes;
export const createSavedQueryService = (
savedObjectsClient: SavedObjectsClientContract,
- queryStringManager?: QueryStringContract,
- application?: CoreStart['application']
+ coreStartServices: { application: CoreStart['application']; uiSettings: CoreStart['uiSettings'] },
+ queryStringManager?: QueryStringContract
): SavedQueryService => {
+ const { application } = coreStartServices;
+
const saveQuery = async (attributes: SavedQueryAttributes, { overwrite = false } = {}) => {
if (!attributes.title.length) {
// title is required extra check against circumventing the front end
throw new Error('Cannot create saved query without a title');
}
- const query = {
+ const query: SerializedSavedQueryAttributes['query'] = {
query:
typeof attributes.query.query === 'string'
? attributes.query.query
@@ -61,7 +58,11 @@ export const createSavedQueryService = (
language: attributes.query.language,
};
- const queryObject: SerializedSavedQueryAttributes = {
+ if (getUseNewSavedQueriesUI() && attributes.query.dataset) {
+ query.dataset = attributes.query.dataset;
+ }
+
+ const queryObject: SavedQueryAttributes = {
title: attributes.title.trim(), // trim whitespace before save as an extra precaution against circumventing the front end
description: attributes.description,
query,
@@ -75,6 +76,10 @@ export const createSavedQueryService = (
queryObject.timefilter = attributes.timefilter;
}
+ if (getUseNewSavedQueriesUI() && attributes.isTemplate) {
+ queryObject.isTemplate = true;
+ }
+
let rawQueryResponse;
if (!overwrite) {
rawQueryResponse = await savedObjectsClient.create('query', queryObject, {
@@ -126,8 +131,7 @@ export const createSavedQueryService = (
parseSavedQueryObject(savedObject)
);
- const currentAppId =
- (await application?.currentAppId$?.pipe(first()).toPromise()) ?? Promise.resolve(undefined);
+ const currentAppId = (await application?.currentAppId$?.pipe(first()).toPromise()) ?? undefined;
const languageService = queryStringManager?.getLanguageService();
// Filtering saved queries based on language supported by cirrent application
@@ -159,11 +163,8 @@ export const createSavedQueryService = (
return await savedObjectsClient.delete('query', id);
};
- const parseSavedQueryObject = (savedQuery: {
- id: string;
- attributes: SerializedSavedQueryAttributes;
- }) => {
- const queryString = savedQuery.attributes.query.query;
+ const parseSavedQueryObject = (savedQuery: SavedQuery) => {
+ const queryString = savedQuery.attributes.query.query as string;
let parsedQuery;
try {
parsedQuery = JSON.parse(queryString);
@@ -172,7 +173,7 @@ export const createSavedQueryService = (
parsedQuery = queryString;
}
- const savedQueryItems: SavedQueryAttributes = {
+ const savedQueryItem: SavedQueryAttributes = {
title: savedQuery.attributes.title || '',
description: savedQuery.attributes.description || '',
query: {
@@ -181,15 +182,20 @@ export const createSavedQueryService = (
},
};
+ if (getUseNewSavedQueriesUI()) {
+ savedQueryItem.query.dataset = savedQuery.attributes.query.dataset;
+ savedQueryItem.isTemplate = !!savedQuery.attributes.isTemplate;
+ }
+
if (savedQuery.attributes.filters) {
- savedQueryItems.filters = savedQuery.attributes.filters;
+ savedQueryItem.filters = savedQuery.attributes.filters;
}
if (savedQuery.attributes.timefilter) {
- savedQueryItems.timefilter = savedQuery.attributes.timefilter;
+ savedQueryItem.timefilter = savedQuery.attributes.timefilter;
}
return {
id: savedQuery.id,
- attributes: savedQueryItems,
+ attributes: savedQueryItem,
};
};
diff --git a/src/plugins/data/public/query/saved_query/types.ts b/src/plugins/data/public/query/saved_query/types.ts
index 8635547e0094..8b3b2cc1e5a0 100644
--- a/src/plugins/data/public/query/saved_query/types.ts
+++ b/src/plugins/data/public/query/saved_query/types.ts
@@ -42,6 +42,9 @@ export interface SavedQuery {
export interface SavedQueryAttributes {
title: string;
description: string;
+ // If isTemplate is true, then saved query cannot be updated/deleted from the UI and
+ // will show up under Templates tab for saved queries
+ isTemplate?: boolean;
query: Query;
filters?: Filter[];
timefilter?: SavedQueryTimeFilter;
diff --git a/src/plugins/data/public/services.ts b/src/plugins/data/public/services.ts
index c042c2b6d9b8..ef827a63405f 100644
--- a/src/plugins/data/public/services.ts
+++ b/src/plugins/data/public/services.ts
@@ -63,3 +63,13 @@ export const [getSearchService, setSearchService] = createGetterSetter<
export const [getUiService, setUiService] = createGetterSetter('Ui');
export const [getApplication, setApplication] = createGetterSetter('Application');
+
+let useNewSavedQueriesUI = false;
+
+export function getUseNewSavedQueriesUI() {
+ return useNewSavedQueriesUI;
+}
+
+export const setUseNewSavedQueriesUI = (value: boolean) => {
+ useNewSavedQueriesUI = value;
+};
diff --git a/src/plugins/data/public/ui/_index.scss b/src/plugins/data/public/ui/_index.scss
index 9ab4ca672b38..daace2e65281 100644
--- a/src/plugins/data/public/ui/_index.scss
+++ b/src/plugins/data/public/ui/_index.scss
@@ -5,3 +5,4 @@
@import "./dataset_selector/index";
@import "./query_editor/index";
@import "./shard_failure_modal/shard_failure_modal";
+@import "./saved_query_flyouts/saved_query_flyouts";
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 6de37d562ce1..3cda39731fa7 100644
--- a/src/plugins/data/public/ui/filter_bar/filter_options.tsx
+++ b/src/plugins/data/public/ui/filter_bar/filter_options.tsx
@@ -55,12 +55,13 @@ import {
unpinFilter,
UI_SETTINGS,
IIndexPattern,
- isQueryStringFilter,
} from '../../../common';
import { FilterEditor } from './filter_editor';
import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public';
import { SavedQueryManagementComponent } from '../saved_query_management';
import { SavedQuery, SavedQueryService } from '../../query';
+import { SavedQueryMeta } from '../saved_query_form';
+import { getUseNewSavedQueriesUI } from '../../services';
interface Props {
intl: InjectedIntl;
@@ -69,14 +70,15 @@ interface Props {
savedQueryService: SavedQueryService;
// Show when user has privileges to save
showSaveQuery?: boolean;
- onSave: () => void;
- onSaveAsNew: () => void;
+ onInitiateSave: () => void;
+ onInitiateSaveAsNew: () => void;
onLoad: (savedQuery: SavedQuery) => void;
onClearSavedQuery: () => void;
onFiltersUpdated?: (filters: Filter[]) => void;
loadedSavedQuery?: SavedQuery;
useSaveQueryMenu: boolean;
isQueryEditorControl: boolean;
+ saveQuery: (savedQueryMeta: SavedQueryMeta, saveAsNew?: boolean) => Promise;
}
const maxFilterWidth = 600;
@@ -285,8 +287,8 @@ const FilterOptionsUI = (props: Props) => {
];
const handleSave = () => {
- if (props.onSave) {
- props.onSave();
+ if (props.onInitiateSave) {
+ props.onInitiateSave();
}
setIsPopoverOpen(false);
};
@@ -297,8 +299,8 @@ const FilterOptionsUI = (props: Props) => {
{
setIsPopoverOpen(false);
}}
key={'savedQueryManagement'}
+ useNewSavedQueryUI={getUseNewSavedQueriesUI()}
+ saveQuery={props.saveQuery}
/>,
]}
data-test-subj="save-query-panel"
@@ -364,6 +368,10 @@ const FilterOptionsUI = (props: Props) => {
defaultMessage: 'See saved queries',
});
+ const iconForQueryEditorControlPopoverBtn = getUseNewSavedQueriesUI()
+ ? 'boxesHorizontal'
+ : 'folderOpen';
+
const savedQueryPopoverButton = (
{
title={label}
>
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
new file mode 100644
index 000000000000..c7f13f27db08
--- /dev/null
+++ b/src/plugins/data/public/ui/saved_query_flyouts/open_saved_query_flyout.tsx
@@ -0,0 +1,283 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiEmptyPrompt,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
+ EuiFlyoutHeader,
+ EuiSearchBar,
+ EuiSearchBarProps,
+ EuiSpacer,
+ EuiTabbedContent,
+ EuiTablePagination,
+ EuiTitle,
+ Pager,
+} from '@elastic/eui';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import { i18n } from '@osd/i18n';
+import { SavedQuery, SavedQueryService } from '../../query';
+import { SavedQueryCard } from './saved_query_card';
+
+export interface OpenSavedQueryFlyoutProps {
+ savedQueryService: SavedQueryService;
+ onClose: () => void;
+ onQueryOpen: (query: SavedQuery) => void;
+ handleQueryDelete: (query: SavedQuery) => Promise;
+}
+
+interface SavedQuerySearchableItem {
+ id: string;
+ title: string;
+ description: string;
+ language: string;
+ datasetType?: string;
+ savedQuery: SavedQuery;
+}
+
+export function OpenSavedQueryFlyout({
+ savedQueryService,
+ onClose,
+ onQueryOpen,
+ handleQueryDelete,
+}: OpenSavedQueryFlyoutProps) {
+ const [selectedTabId, setSelectedTabId] = useState('mutable-saved-queries');
+ const [savedQueries, setSavedQueries] = useState([]);
+ const [hasTemplateQueries, setHasTemplateQueries] = useState(false);
+ const [itemsPerPage, setItemsPerPage] = useState(10);
+ const pager = useRef(new Pager(savedQueries.length, itemsPerPage));
+ const [activePage, setActivePage] = useState(pager.current.getCurrentPageIndex());
+ const [queriesOnCurrentPage, setQueriesOnCurrentPage] = useState([]);
+ const [datasetTypeFilterOptions, setDatasetTypeFilterOptions] = useState([]);
+ const [languageFilterOptions, setLanguageFilterOptions] = useState([]);
+ const [selectedQuery, setSelectedQuery] = useState(undefined);
+ const [searchQuery, setSearchQuery] = useState(EuiSearchBar.Query.MATCH_ALL);
+
+ 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]);
+
+ const updatePageIndex = useCallback((index: number) => {
+ pager.current.goToPageIndex(index);
+ setActivePage(index);
+ }, []);
+
+ useEffect(() => {
+ fetchAllSavedQueriesForSelectedTab();
+ setSearchQuery(EuiSearchBar.Query.MATCH_ALL);
+ updatePageIndex(0);
+ }, [selectedTabId, fetchAllSavedQueriesForSelectedTab, updatePageIndex]);
+
+ useEffect(() => {
+ const queryLanguages = new Set();
+ const queryDatasetTypes = new Set();
+
+ savedQueries.forEach((q) => {
+ queryLanguages.add(q.attributes.query.language);
+ if (q.attributes.query.dataset?.type) {
+ queryDatasetTypes.add(q.attributes.query.dataset.type);
+ }
+ });
+ setLanguageFilterOptions(Array.from(queryLanguages));
+ setDatasetTypeFilterOptions(Array.from(queryDatasetTypes));
+ }, [savedQueries]);
+
+ useEffect(() => {
+ const searchableItems = savedQueries.map((q) => ({
+ id: q.id,
+ title: q.attributes.title,
+ description: q.attributes.description,
+ language: q.attributes.query.language,
+ datasetType: q.attributes.query.dataset?.type,
+ savedQuery: q,
+ }));
+
+ const filteredSavedQueries = EuiSearchBar.Query.execute(searchQuery, searchableItems, {
+ defaultFields: ['language', 'title', 'description', 'datasetType'],
+ });
+ pager.current.setTotalItems(filteredSavedQueries.length);
+ setQueriesOnCurrentPage(
+ filteredSavedQueries.slice(
+ pager.current.getFirstItemIndex(),
+ pager.current.getLastItemIndex() + 1
+ )
+ );
+ }, [savedQueries, searchQuery, activePage, itemsPerPage]);
+
+ const onChange: EuiSearchBarProps['onChange'] = ({ query, error }) => {
+ if (!error) {
+ setSearchQuery(query);
+ updatePageIndex(0);
+ }
+ };
+
+ const schema = {
+ strict: true,
+ fields: {
+ title: {
+ type: 'string',
+ },
+ description: {
+ type: 'string',
+ },
+ language: {
+ type: 'string',
+ },
+ },
+ };
+
+ const flyoutBodyContent = (
+ <>
+
+ ({
+ value: datasetType,
+ view: datasetType.toUpperCase(),
+ })),
+ },
+ {
+ type: 'field_value_selection',
+ field: 'language',
+ name: 'Query language',
+ multiSelect: 'or',
+ options: languageFilterOptions.map((language) => ({
+ value: language,
+ view: language.toUpperCase(),
+ })),
+ },
+ ]}
+ onChange={onChange}
+ />
+
+ {queriesOnCurrentPage.length > 0 ? (
+ queriesOnCurrentPage.map((query) => (
+ {
+ handleQueryDelete(queryToDelete).then(() => {
+ fetchAllSavedQueriesForSelectedTab();
+ });
+ }}
+ />
+ ))
+ ) : (
+
+ {i18n.translate('data.openSavedQueryFlyout.queryTable.noQueryFoundText', {
+ defaultMessage: 'No saved query found.',
+ })}
+
+ }
+ />
+ )}
+
+ {queriesOnCurrentPage.length > 0 && (
+ {
+ pager.current.setItemsPerPage(pageSize);
+ setItemsPerPage(pageSize);
+ }}
+ onChangePage={(pageIndex) => {
+ updatePageIndex(pageIndex);
+ }}
+ />
+ )}
+ >
+ );
+
+ const tabs = [
+ {
+ id: 'mutable-saved-queries',
+ name: 'Saved queries',
+ content: flyoutBodyContent,
+ },
+ ];
+
+ if (hasTemplateQueries) {
+ tabs.push({
+ id: 'template-saved-queries',
+ name: 'Templates',
+ content: flyoutBodyContent,
+ });
+ }
+
+ return (
+
+
+
+ Saved queries
+
+
+
+ {
+ setSelectedTabId(tab.id);
+ }}
+ />
+
+
+
+
+
+ Cancel
+
+
+
+ {
+ if (selectedQuery) {
+ onQueryOpen(selectedQuery);
+ onClose();
+ }
+ }}
+ >
+ Open query
+
+
+
+
+
+ );
+}
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
new file mode 100644
index 000000000000..c0356b864485
--- /dev/null
+++ b/src/plugins/data/public/ui/saved_query_flyouts/save_query_flyout.tsx
@@ -0,0 +1,53 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useState } from 'react';
+import { SavedQueryAttributes, SavedQueryService } from '../../query';
+import { SaveQueryForm, SavedQueryMeta } from '../saved_query_form';
+
+interface SaveQueryFlyoutProps {
+ savedQuery?: SavedQueryAttributes;
+ savedQueryService: SavedQueryService;
+ onSave: (savedQueryMeta: SavedQueryMeta, saveAsNew?: boolean) => Promise;
+ onClose: () => void;
+ showFilterOption: boolean | undefined;
+ showTimeFilterOption: boolean | undefined;
+}
+
+export function SaveQueryFlyout({
+ savedQuery,
+ savedQueryService,
+ onSave,
+ onClose,
+ showFilterOption,
+ showTimeFilterOption,
+}: SaveQueryFlyoutProps) {
+ const [saveAsNew, setSaveAsNew] = useState(!savedQuery || savedQuery.isTemplate);
+
+ return (
+ {
+ try {
+ await onSave(savedQueryMeta, saveAsNew);
+ onClose();
+ } catch (error) {
+ // error toast is already shown inside the onSave above,
+ // catching error to avoid UI crash
+ // adding comment to prevent no-empty lint error
+ }
+ }}
+ savedQueryService={savedQueryService}
+ showFilterOption={showFilterOption}
+ showTimeFilterOption={showTimeFilterOption}
+ showDataSourceOption={true}
+ setSaveAsNew={(shouldSaveAsNew) => setSaveAsNew(shouldSaveAsNew)}
+ savedQuery={saveAsNew ? undefined : savedQuery}
+ saveAsNew={saveAsNew}
+ cannotBeOverwritten={!!savedQuery?.isTemplate}
+ />
+ );
+}
diff --git a/src/plugins/data/public/ui/saved_query_flyouts/saved_query_card.tsx b/src/plugins/data/public/ui/saved_query_flyouts/saved_query_card.tsx
new file mode 100644
index 000000000000..7107a8906004
--- /dev/null
+++ b/src/plugins/data/public/ui/saved_query_flyouts/saved_query_card.tsx
@@ -0,0 +1,196 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ EuiBadge,
+ EuiButtonIcon,
+ EuiCheckableCard,
+ EuiCopy,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiLink,
+ EuiSpacer,
+ EuiText,
+ EuiTitle,
+} from '@elastic/eui';
+import React, { useRef, useState, useEffect } from 'react';
+import MonacoEditor from 'react-monaco-editor';
+import { monaco } from '@osd/monaco';
+import { i18n } from '@osd/i18n';
+import { SavedQuery } from '../../query';
+import { DeleteSavedQueryConfirmationModal } from '../saved_query_management/delete_saved_query_confirmation_modal';
+
+export interface SavedQueryCardProps {
+ savedQuery: SavedQuery;
+ selectedQuery?: SavedQuery;
+ onSelect: (query: SavedQuery) => void;
+ handleQueryDelete: (query: SavedQuery) => void;
+}
+
+export function SavedQueryCard({
+ savedQuery,
+ selectedQuery,
+ onSelect,
+ handleQueryDelete,
+}: SavedQueryCardProps) {
+ const [shouldTruncate, setShouldTruncate] = useState(false);
+ const [isTruncated, setIsTruncated] = useState(true);
+ const [editorHeight, setEditorHeight] = useState(60);
+ const customHTMLRef = useRef(null);
+ const [showDeletionConfirmationModal, setShowDeletionConfirmationModal] = useState(false);
+ const [lineCount, setLineCount] = useState(0);
+ const toggleView = () => {
+ setIsTruncated(!isTruncated);
+ };
+
+ useEffect(() => {
+ if (!shouldTruncate) {
+ return;
+ }
+
+ if (isTruncated) {
+ setEditorHeight(80);
+ return;
+ }
+
+ const editor = customHTMLRef.current;
+ if (!editor) {
+ return;
+ }
+
+ const editorElement = editor.getDomNode();
+
+ if (!editorElement) {
+ return;
+ }
+
+ const height = editor.getScrollHeight();
+ setEditorHeight(height);
+ }, [isTruncated, shouldTruncate]);
+
+ function handleHTMLEditorDidMount(editor: monaco.editor.IStandaloneCodeEditor) {
+ const scrollHeight = editor.getScrollHeight();
+ setEditorHeight(scrollHeight);
+
+ if (scrollHeight > 80) {
+ setShouldTruncate(true);
+ }
+
+ setLineCount(editor.getModel()?.getLineCount() || 0);
+ customHTMLRef.current = editor;
+ }
+
+ return (
+ <>
+ {
+ onSelect(savedQuery);
+ }}
+ label={
+ <>
+
+
+
+
+
+ {savedQuery.attributes.title}
+
+
+
+ {savedQuery.attributes.query.language}
+
+
+
+ {!savedQuery.attributes.isTemplate && (
+
+ {
+ setShowDeletionConfirmationModal(true);
+ }}
+ />
+
+ )}
+
+ {savedQuery.attributes.description && (
+
+ {savedQuery.attributes.description}
+
+ )}
+ {savedQuery.attributes.query.dataset?.title && (
+
+ {savedQuery.attributes.query.dataset.title}
+
+ )}
+ >
+ }
+ >
+
+
+
+
+ {shouldTruncate && (
+
+
+ {i18n.translate('data.saved_query.view_more_label', {
+ defaultMessage: '{viewMoreLabel}',
+ values: {
+ viewMoreLabel: `${
+ isTruncated ? `View full query (${lineCount} lines)` : 'View Less'
+ }`,
+ },
+ })}
+
+
+ )}
+
+
+
+
+ {(copy) => (
+
+ )}
+
+
+
+
+
+ {showDeletionConfirmationModal && (
+ {
+ handleQueryDelete(savedQuery);
+ setShowDeletionConfirmationModal(false);
+ }}
+ onCancel={() => {
+ setShowDeletionConfirmationModal(false);
+ }}
+ />
+ )}
+ >
+ );
+}
diff --git a/src/plugins/data/public/ui/saved_query_flyouts/saved_query_flyouts.scss b/src/plugins/data/public/ui/saved_query_flyouts/saved_query_flyouts.scss
new file mode 100644
index 000000000000..1ab1074b27a5
--- /dev/null
+++ b/src/plugins/data/public/ui/saved_query_flyouts/saved_query_flyouts.scss
@@ -0,0 +1,23 @@
+.editor-container {
+ position: relative;
+ overflow-y: clip;
+}
+
+.read-more-wrap {
+ /* stylelint-disable @osd/stylelint/no_restricted_values */
+ background: linear-gradient(rgba($euiColorEmptyShade, 5%) 10%, $euiColorEmptyShade 60%, $euiColorEmptyShade 100%);
+ height: 90px;
+ position: absolute;
+ z-index: 1;
+ left: 0;
+ bottom: 0;
+ text-align: center;
+ width: 100%;
+}
+
+.read-more-btn {
+ position: absolute;
+ left: 50%;
+ bottom: 0;
+ transform: translateX(-50%);
+}
diff --git a/src/plugins/data/public/ui/saved_query_form/helpers.tsx b/src/plugins/data/public/ui/saved_query_form/helpers.tsx
new file mode 100644
index 000000000000..467eac2de475
--- /dev/null
+++ b/src/plugins/data/public/ui/saved_query_form/helpers.tsx
@@ -0,0 +1,334 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ *
+ * Any modifications Copyright OpenSearch Contributors. See
+ * GitHub history for details.
+ */
+
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useEffect, useState, useCallback } from 'react';
+import {
+ EuiSmallButtonEmpty,
+ EuiSmallButton,
+ EuiForm,
+ EuiCompressedFormRow,
+ EuiCompressedFieldText,
+ EuiCompressedSwitch,
+ EuiText,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonEmpty,
+ EuiButton,
+ EuiCompressedCheckbox,
+} from '@elastic/eui';
+import { i18n } from '@osd/i18n';
+import { sortBy, isEqual } from 'lodash';
+import { SavedQuery, SavedQueryService } from '../..';
+import { SavedQueryAttributes } from '../../query';
+import { SavedQueryMeta } from './save_query_form';
+
+interface Props {
+ savedQuery?: SavedQueryAttributes;
+ savedQueryService: SavedQueryService;
+ onSave: (savedQueryMeta: SavedQueryMeta) => void;
+ onClose: () => void;
+ formUiType: 'Modal' | 'Flyout';
+ showFilterOption?: boolean;
+ showTimeFilterOption?: boolean;
+ showDataSourceOption?: boolean;
+ saveAsNew?: boolean;
+ setSaveAsNew?: (shouldSaveAsNew: boolean) => void;
+ cannotBeOverwritten?: boolean;
+}
+
+export function useSaveQueryFormContent({
+ savedQuery,
+ savedQueryService,
+ onSave,
+ onClose,
+ showFilterOption = true,
+ showTimeFilterOption = true,
+ showDataSourceOption = false,
+ formUiType,
+ saveAsNew,
+ setSaveAsNew,
+ cannotBeOverwritten,
+}: Props) {
+ const [title, setTitle] = useState('');
+ const [enabledSaveButton, setEnabledSaveButton] = useState(false);
+ 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.
+ const [shouldIncludeTimeFilter, setIncludeTimefilter] = useState(false);
+ const [formErrors, setFormErrors] = useState([]);
+
+ // Need this effect so that in case when user select "Save as new" in the flyout,
+ // the initial state resets since savedQuery will be undefined
+ useEffect(() => {
+ setTitle(savedQuery?.title || '');
+ setEnabledSaveButton(Boolean(savedQuery));
+ setDescription(savedQuery?.description || '');
+ setShouldIncludeFilters(savedQuery ? !!savedQuery.filters : true);
+ setIncludeTimefilter(!!savedQuery?.timefilter);
+ setShouldIncludeDataSource(savedQuery ? !!savedQuery.query.dataset : true);
+ setFormErrors([]);
+ }, [savedQuery]);
+
+ const titleConflictErrorText = i18n.translate(
+ 'data.search.searchBar.savedQueryForm.titleConflictText',
+ {
+ defaultMessage: 'Name conflicts with an existing saved query',
+ }
+ );
+
+ const savedQueryDescriptionText = i18n.translate(
+ 'data.search.searchBar.savedQueryDescriptionText',
+ {
+ defaultMessage: 'Save query text and filters that you want to use again.',
+ }
+ );
+
+ useEffect(() => {
+ const fetchQueries = async () => {
+ const allSavedQueries = await savedQueryService.getAllSavedQueries();
+ const sortedAllSavedQueries = sortBy(allSavedQueries, 'attributes.title') as SavedQuery[];
+ setSavedQueries(sortedAllSavedQueries);
+ };
+ fetchQueries();
+ }, [savedQueryService]);
+
+ const validate = useCallback(() => {
+ const errors = [];
+ if (
+ !savedQuery &&
+ savedQueries.some((existingSavedQuery) => existingSavedQuery.attributes.title === title)
+ ) {
+ errors.push(titleConflictErrorText);
+ }
+
+ if (!isEqual(errors, formErrors)) {
+ setFormErrors(errors);
+ return false;
+ }
+
+ return !formErrors.length;
+ }, [savedQueries, savedQuery, title, titleConflictErrorText, formErrors]);
+
+ const onClickSave = useCallback(() => {
+ if (validate()) {
+ onSave({
+ title,
+ description,
+ shouldIncludeFilters,
+ shouldIncludeTimeFilter,
+ shouldIncludeDataSource,
+ });
+ }
+ }, [
+ validate,
+ onSave,
+ title,
+ description,
+ shouldIncludeFilters,
+ shouldIncludeTimeFilter,
+ shouldIncludeDataSource,
+ ]);
+
+ const onInputChange = useCallback((event) => {
+ setEnabledSaveButton(Boolean(event.target.value));
+ setFormErrors([]);
+ setTitle(event.target.value);
+ }, []);
+
+ const autoTrim = useCallback(() => {
+ const trimmedTitle = title.trim();
+ if (title.length > trimmedTitle.length) {
+ setTitle(trimmedTitle);
+ }
+ }, [title]);
+
+ const hasErrors = formErrors.length > 0;
+
+ const saveQueryFormBody = (
+
+
+
+ {savedQueryDescriptionText}
+
+
+ {formUiType === 'Flyout' && (
+
+ setSaveAsNew?.(event.target.checked)}
+ checked={saveAsNew}
+ label={i18n.translate('data.search.searchBar.SaveAsNewLabelText', {
+ defaultMessage: 'Save as new query',
+ })}
+ disabled={cannotBeOverwritten}
+ />
+
+ )}
+
+
+
+
+
+ {
+ setDescription(event.target.value);
+ }}
+ data-test-subj="saveQueryFormDescription"
+ />
+
+ {showDataSourceOption && (
+
+ {
+ setShouldIncludeDataSource(!shouldIncludeDataSource);
+ }}
+ data-test-subj="saveQueryFormIncludeDataSourceOption"
+ />
+
+ )}
+ {showFilterOption && (
+
+ {
+ setShouldIncludeFilters(!shouldIncludeFilters);
+ }}
+ data-test-subj="saveQueryFormIncludeFiltersOption"
+ />
+
+ )}
+
+ {showTimeFilterOption && (
+
+ {
+ setIncludeTimefilter(!shouldIncludeTimeFilter);
+ }}
+ data-test-subj="saveQueryFormIncludeTimeFilterOption"
+ />
+
+ )}
+
+ );
+
+ const footer =
+ formUiType === 'Modal' ? (
+ <>
+
+ {i18n.translate('data.search.searchBar.savedQueryFormCancelButtonText', {
+ defaultMessage: 'Cancel',
+ })}
+
+
+
+ {i18n.translate('data.search.searchBar.savedQueryFormSaveButtonText', {
+ defaultMessage: 'Save',
+ })}
+
+ >
+ ) : (
+
+
+
+ {i18n.translate('data.search.searchBar.savedQueryFormCancelButtonText', {
+ defaultMessage: 'Cancel',
+ })}
+
+
+
+
+ {i18n.translate('data.search.searchBar.savedQueryFlyoutFormSaveButtonText', {
+ defaultMessage: '{saveText}',
+ values: { saveText: savedQuery ? 'Save changes' : 'Save' },
+ })}
+
+
+
+ );
+
+ return {
+ header: (
+
+
+ {i18n.translate('data.search.searchBar.savedQueryFormTitle', {
+ defaultMessage: 'Save query',
+ })}
+
+
+ ),
+ body: saveQueryFormBody,
+ footer,
+ };
+}
diff --git a/src/plugins/data/public/ui/saved_query_form/index.ts b/src/plugins/data/public/ui/saved_query_form/index.ts
index e9c62cf646df..6160e53b54e4 100644
--- a/src/plugins/data/public/ui/saved_query_form/index.ts
+++ b/src/plugins/data/public/ui/saved_query_form/index.ts
@@ -29,4 +29,4 @@
*/
// @internal
-export { SavedQueryMeta, SaveQueryForm } from '../saved_query_form/save_query_form';
+export { SavedQueryMeta, SaveQueryForm } from './save_query_form';
diff --git a/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx b/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx
index 9d1b5c29342d..e25f3229d88c 100644
--- a/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx
+++ b/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx
@@ -28,242 +28,86 @@
* under the License.
*/
-import React, { useEffect, useState, useCallback } from 'react';
+import React from 'react';
import {
- EuiSmallButtonEmpty,
EuiModal,
- EuiSmallButton,
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalBody,
EuiModalFooter,
- EuiForm,
- EuiCompressedFormRow,
- EuiCompressedFieldText,
- EuiCompressedSwitch,
- EuiText,
+ EuiFlyoutHeader,
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutFooter,
} from '@elastic/eui';
-import { i18n } from '@osd/i18n';
-import { sortBy, isEqual } from 'lodash';
-import { SavedQuery, SavedQueryService } from '../..';
+import { SavedQueryService } from '../..';
import { SavedQueryAttributes } from '../../query';
+import { useSaveQueryFormContent } from './helpers';
interface Props {
+ formUiType: 'Modal' | 'Flyout';
savedQuery?: SavedQueryAttributes;
savedQueryService: SavedQueryService;
onSave: (savedQueryMeta: SavedQueryMeta) => void;
onClose: () => void;
- showFilterOption: boolean | undefined;
- showTimeFilterOption: boolean | undefined;
+ setSaveAsNew?: (shouldSaveAsNew: boolean) => void;
+ showFilterOption?: boolean;
+ showTimeFilterOption?: boolean;
+ showDataSourceOption?: boolean;
+ saveAsNew?: boolean;
+ cannotBeOverwritten?: boolean;
}
export interface SavedQueryMeta {
title: string;
description: string;
shouldIncludeFilters: boolean;
- shouldIncludeTimefilter: boolean;
+ shouldIncludeTimeFilter: boolean;
+ shouldIncludeDataSource: boolean;
}
export function SaveQueryForm({
+ formUiType,
savedQuery,
savedQueryService,
onSave,
onClose,
showFilterOption = true,
showTimeFilterOption = true,
+ showDataSourceOption = false,
+ saveAsNew,
+ setSaveAsNew,
+ cannotBeOverwritten,
}: Props) {
- const [title, setTitle] = useState(savedQuery ? savedQuery.title : '');
- const [enabledSaveButton, setEnabledSaveButton] = useState(Boolean(savedQuery));
- const [description, setDescription] = useState(savedQuery ? savedQuery.description : '');
- const [savedQueries, setSavedQueries] = useState([]);
- const [shouldIncludeFilters, setShouldIncludeFilters] = useState(
- savedQuery ? !!savedQuery.filters : 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.
- const [shouldIncludeTimefilter, setIncludeTimefilter] = useState(
- savedQuery ? !!savedQuery.timefilter : false
- );
- const [formErrors, setFormErrors] = useState([]);
-
- const titleConflictErrorText = i18n.translate(
- 'data.search.searchBar.savedQueryForm.titleConflictText',
- {
- defaultMessage: 'Name conflicts with an existing saved query',
- }
- );
-
- const savedQueryDescriptionText = i18n.translate(
- 'data.search.searchBar.savedQueryDescriptionText',
- {
- defaultMessage: 'Save query text and filters that you want to use again.',
- }
- );
-
- useEffect(() => {
- const fetchQueries = async () => {
- const allSavedQueries = await savedQueryService.getAllSavedQueries();
- const sortedAllSavedQueries = sortBy(allSavedQueries, 'attributes.title') as SavedQuery[];
- setSavedQueries(sortedAllSavedQueries);
- };
- fetchQueries();
- }, [savedQueryService]);
-
- const validate = useCallback(() => {
- const errors = [];
- if (
- !!savedQueries.find(
- (existingSavedQuery) => !savedQuery && existingSavedQuery.attributes.title === title
- )
- ) {
- errors.push(titleConflictErrorText);
- }
-
- if (!isEqual(errors, formErrors)) {
- setFormErrors(errors);
- return false;
- }
-
- return !formErrors.length;
- }, [savedQueries, savedQuery, title, titleConflictErrorText, formErrors]);
-
- const onClickSave = useCallback(() => {
- if (validate()) {
- onSave({
- title,
- description,
- shouldIncludeFilters,
- shouldIncludeTimefilter,
- });
- }
- }, [validate, onSave, title, description, shouldIncludeFilters, shouldIncludeTimefilter]);
-
- const onInputChange = useCallback((event) => {
- setEnabledSaveButton(Boolean(event.target.value));
- setFormErrors([]);
- setTitle(event.target.value);
- }, []);
-
- const autoTrim = useCallback(() => {
- const trimmedTitle = title.trim();
- if (title.length > trimmedTitle.length) {
- setTitle(trimmedTitle);
- }
- }, [title]);
-
- const hasErrors = formErrors.length > 0;
-
- const saveQueryForm = (
-
-
-
- {savedQueryDescriptionText}
-
-
-
-
-
-
-
- {
- setDescription(event.target.value);
- }}
- data-test-subj="saveQueryFormDescription"
- />
-
- {showFilterOption && (
-
- {
- setShouldIncludeFilters(!shouldIncludeFilters);
- }}
- data-test-subj="saveQueryFormIncludeFiltersOption"
- />
-
- )}
-
- {showTimeFilterOption && (
-
- {
- setIncludeTimefilter(!shouldIncludeTimefilter);
- }}
- data-test-subj="saveQueryFormIncludeTimeFilterOption"
- />
-
- )}
-
- );
-
- return (
+ const { header, body, footer } = useSaveQueryFormContent({
+ formUiType,
+ savedQuery,
+ savedQueryService,
+ onSave,
+ onClose,
+ showFilterOption,
+ showTimeFilterOption,
+ showDataSourceOption,
+ saveAsNew,
+ setSaveAsNew,
+ cannotBeOverwritten,
+ });
+
+ return formUiType === 'Modal' ? (
-
-
-
- {i18n.translate('data.search.searchBar.savedQueryFormTitle', {
- defaultMessage: 'Save query',
- })}
-
-
-
+ {header}
- {saveQueryForm}
-
-
-
- {i18n.translate('data.search.searchBar.savedQueryFormCancelButtonText', {
- defaultMessage: 'Cancel',
- })}
-
+ {body}
-
- {i18n.translate('data.search.searchBar.savedQueryFormSaveButtonText', {
- defaultMessage: 'Save',
- })}
-
-
+ {footer}
+ ) : (
+
+ {header}
+ {body}
+ {footer}
+
);
}
diff --git a/src/plugins/data/public/ui/saved_query_management/delete_saved_query_confirmation_modal.tsx b/src/plugins/data/public/ui/saved_query_management/delete_saved_query_confirmation_modal.tsx
new file mode 100644
index 000000000000..035fc22ae3d0
--- /dev/null
+++ b/src/plugins/data/public/ui/saved_query_management/delete_saved_query_confirmation_modal.tsx
@@ -0,0 +1,72 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ *
+ * Any modifications Copyright OpenSearch Contributors. See
+ * GitHub history for details.
+ */
+
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import { EuiConfirmModal } from '@elastic/eui';
+import { i18n } from '@osd/i18n';
+import { SavedQuery } from '../..';
+
+export interface DeleteSavedQueryConfirmationModalProps {
+ savedQuery: SavedQuery;
+ onConfirm: () => void;
+ onCancel: () => void;
+}
+
+export const DeleteSavedQueryConfirmationModal = ({
+ savedQuery,
+ onConfirm,
+ onCancel,
+}: DeleteSavedQueryConfirmationModalProps) => {
+ return (
+
+ );
+};
diff --git a/src/plugins/data/public/ui/saved_query_management/saved_query_list_item.tsx b/src/plugins/data/public/ui/saved_query_management/saved_query_list_item.tsx
index 77711d6382a4..8e015adf1eb5 100644
--- a/src/plugins/data/public/ui/saved_query_management/saved_query_list_item.tsx
+++ b/src/plugins/data/public/ui/saved_query_management/saved_query_list_item.tsx
@@ -34,6 +34,7 @@ import React, { Fragment, useState } from 'react';
import classNames from 'classnames';
import { i18n } from '@osd/i18n';
import { SavedQuery } from '../..';
+import { DeleteSavedQueryConfirmationModal } from './delete_saved_query_confirmation_modal';
interface Props {
savedQuery: SavedQuery;
@@ -137,30 +138,12 @@ export const SavedQueryListItem = ({
/>
{showDeletionConfirmationModal && (
- {
onDelete(savedQuery);
setShowDeletionConfirmationModal(false);
}}
- buttonColor="danger"
onCancel={() => {
setShowDeletionConfirmationModal(false);
}}
diff --git a/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.test.tsx b/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.test.tsx
index 8b92609d419a..acafe2a444a3 100644
--- a/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.test.tsx
+++ b/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.test.tsx
@@ -18,8 +18,8 @@ const mockProps = () => ({
deleteSavedQuery: jest.fn(),
getSavedQueryCount: jest.fn(),
},
- onSave: jest.fn(),
- onSaveAsNew: jest.fn(),
+ onInitiateSave: jest.fn(),
+ onInitiateSaveAsNew: jest.fn(),
onLoad: jest.fn(),
onClearSavedQuery: jest.fn(),
closeMenuPopover: jest.fn(),
@@ -33,6 +33,7 @@ const mockProps = () => ({
query: { query: '', language: 'kuery' } as Query,
} as SavedQueryAttributes,
},
+ saveQuery: jest.fn(),
});
describe('Saved query management component', () => {
@@ -42,25 +43,25 @@ describe('Saved query management component', () => {
expect(wrapper.exists()).toBe(true);
});
- it('should call onSave when save button is clicked', () => {
+ it('should call onInitiateSave when save button is clicked', () => {
const props = mockProps();
const wrapper = shallowWithIntl();
const saveButton = wrapper
.find('[data-test-subj="saved-query-management-save-changes-button"]')
.at(0);
saveButton.simulate('click');
- expect(props.onSave).toHaveBeenCalled();
+ expect(props.onInitiateSave).toHaveBeenCalled();
expect(props.closeMenuPopover).toHaveBeenCalled();
});
- it('should call onSaveAsNew when save as new button is clicked', () => {
+ it('should call onInitiateSaveAsNew when save as new button is clicked', () => {
const props = mockProps();
const wrapper = shallowWithIntl();
const saveAsNewButton = wrapper
.find('[data-test-subj="saved-query-management-save-as-new-button"]')
.at(0);
saveAsNewButton.simulate('click');
- expect(props.onSaveAsNew).toHaveBeenCalled();
+ expect(props.onInitiateSaveAsNew).toHaveBeenCalled();
});
it('should call onClearSavedQuery when clear saved query button is clicked', () => {
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 3048554fe2fc..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
@@ -39,6 +39,7 @@ import {
EuiPagination,
EuiText,
EuiSpacer,
+ EuiListGroupItem,
} from '@elastic/eui';
import { i18n } from '@osd/i18n';
@@ -46,33 +47,50 @@ import React, { useCallback, useEffect, useState, Fragment, useRef } from 'react
import { sortBy } from 'lodash';
import { SavedQuery, SavedQueryService } from '../..';
import { SavedQueryListItem } from './saved_query_list_item';
+import {
+ toMountPoint,
+ useOpenSearchDashboards,
+} from '../../../../opensearch_dashboards_react/public';
+import { SaveQueryFlyout } from '../saved_query_flyouts/save_query_flyout';
+import {
+ OpenSavedQueryFlyout,
+ OpenSavedQueryFlyoutProps,
+} from '../saved_query_flyouts/open_saved_query_flyout';
+import { SavedQueryMeta } from '../saved_query_form';
const perPage = 50;
interface Props {
showSaveQuery?: boolean;
loadedSavedQuery?: SavedQuery;
savedQueryService: SavedQueryService;
- onSave: () => void;
- onSaveAsNew: () => void;
- onLoad: (savedQuery: SavedQuery) => void;
+ useNewSavedQueryUI?: boolean;
+ onInitiateSave: () => void;
+ onInitiateSaveAsNew: () => void;
+ onLoad: OpenSavedQueryFlyoutProps['onQueryOpen'];
onClearSavedQuery: () => void;
closeMenuPopover: () => void;
+ saveQuery: (savedQueryMeta: SavedQueryMeta, saveAsNew?: boolean) => Promise;
}
export function SavedQueryManagementComponent({
showSaveQuery,
loadedSavedQuery,
- onSave,
- onSaveAsNew,
+ onInitiateSave,
+ onInitiateSaveAsNew,
onLoad,
onClearSavedQuery,
savedQueryService,
closeMenuPopover,
+ useNewSavedQueryUI,
+ saveQuery,
}: Props) {
const [savedQueries, setSavedQueries] = useState([] as SavedQuery[]);
const [count, setTotalCount] = useState(0);
const [activePage, setActivePage] = useState(0);
const cancelPendingListingRequest = useRef<() => void>(() => {});
+ const {
+ services: { overlays },
+ } = useOpenSearchDashboards();
useEffect(() => {
const fetchCountAndSavedQueries = async () => {
@@ -102,13 +120,13 @@ export function SavedQueryManagementComponent({
const handleSave = useCallback(() => {
handleClosePopover();
- onSave();
- }, [handleClosePopover, onSave]);
+ onInitiateSave();
+ }, [handleClosePopover, onInitiateSave]);
const handleSaveAsNew = useCallback(() => {
handleClosePopover();
- onSaveAsNew();
- }, [handleClosePopover, onSaveAsNew]);
+ onInitiateSaveAsNew();
+ }, [handleClosePopover, onInitiateSaveAsNew]);
const handleSelect = useCallback(
(savedQueryToSelect) => {
@@ -134,10 +152,21 @@ export function SavedQueryManagementComponent({
setActivePage(0);
};
- onDeleteSavedQuery(savedQueryToDelete);
- handleClosePopover();
+ const deletePromise = onDeleteSavedQuery(savedQueryToDelete);
+ if (!useNewSavedQueryUI) {
+ handleClosePopover();
+ }
+
+ return deletePromise;
},
- [handleClosePopover, loadedSavedQuery, onClearSavedQuery, savedQueries, savedQueryService]
+ [
+ handleClosePopover,
+ loadedSavedQuery,
+ onClearSavedQuery,
+ savedQueries,
+ savedQueryService,
+ useNewSavedQueryUI,
+ ]
);
const savedQueryDescriptionText = i18n.translate(
@@ -186,7 +215,55 @@ export function SavedQueryManagementComponent({
));
};
- return (
+ return useNewSavedQueryUI ? (
+
+
+ {
+ closeMenuPopover();
+ const saveQueryFlyout = overlays?.openFlyout(
+ toMountPoint(
+ saveQueryFlyout?.close().then()}
+ onSave={saveQuery}
+ showFilterOption={true}
+ showTimeFilterOption={true}
+ savedQuery={loadedSavedQuery?.attributes}
+ />
+ )
+ );
+ }}
+ />
+ {
+ closeMenuPopover();
+ const openSavedQueryFlyout = overlays?.openFlyout(
+ toMountPoint(
+ openSavedQueryFlyout?.close().then()}
+ onQueryOpen={onLoad}
+ handleQueryDelete={handleDelete}
+ />
+ )
+ );
+ }}
+ />
+
+
+ ) : (
;
@@ -285,10 +286,15 @@ 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;
+ }
+
const savedQueryAttributes: SavedQueryAttributes = {
title: savedQueryMeta.title,
description: savedQueryMeta.description,
- query: this.state.query,
+ query,
};
if (savedQueryMeta.shouldIncludeFilters) {
@@ -296,7 +302,7 @@ class SearchBarUI extends Component {
}
if (
- savedQueryMeta.shouldIncludeTimefilter &&
+ savedQueryMeta.shouldIncludeTimeFilter &&
this.state.dateRangeTo !== undefined &&
this.state.dateRangeFrom !== undefined &&
this.props.refreshInterval !== undefined &&
@@ -334,9 +340,15 @@ class SearchBarUI extends Component {
if (this.props.onSaved) {
this.props.onSaved(response);
}
- } catch (error) {
+ } catch (error: any) {
this.services.notifications.toasts.addDanger(
- `An error occured while saving your query: ${error.message}`
+ this.props.intl.formatMessage(
+ {
+ id: 'data.search_bar.save_query.failedToSaveQuery',
+ defaultMessage: 'An error occured while saving your query{errorMessage}',
+ },
+ { errorMessage: error.message ? `: ${error.message}` : '' }
+ )
);
throw error;
}
@@ -447,13 +459,14 @@ class SearchBarUI extends Component {
indexPatterns={this.props.indexPatterns!}
showSaveQuery={this.props.showSaveQuery}
loadedSavedQuery={this.props.savedQuery}
- onSave={this.onInitiateSave}
- onSaveAsNew={this.onInitiateSaveNew}
+ onInitiateSave={this.onInitiateSave}
+ onInitiateSaveAsNew={this.onInitiateSaveNew}
onLoad={this.onLoadSavedQuery}
savedQueryService={this.savedQueryService}
onClearSavedQuery={this.props.onClearSavedQuery}
useSaveQueryMenu={useSaveQueryMenu}
isQueryEditorControl={isQueryEditorControl}
+ saveQuery={this.onSave}
/>
)
);
@@ -569,6 +582,7 @@ class SearchBarUI extends Component {
{this.state.showSaveQueryModal ? (
{
) : null}
{this.state.showSaveNewQueryModal ? (
this.onSave(savedQueryMeta, true)}
onClose={() => this.setState({ showSaveNewQueryModal: false })}
diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts
index 289e390070e2..b64c93372581 100644
--- a/src/plugins/data/server/index.ts
+++ b/src/plugins/data/server/index.ts
@@ -306,6 +306,7 @@ export const config: PluginConfigDescriptor = {
enhancements: true,
autocomplete: true,
search: true,
+ savedQueriesNewUI: true,
},
schema: configSchema,
};
diff --git a/src/plugins/data/server/saved_objects/query.ts b/src/plugins/data/server/saved_objects/query.ts
index 1b7b027a6121..67b45111a832 100644
--- a/src/plugins/data/server/saved_objects/query.ts
+++ b/src/plugins/data/server/saved_objects/query.ts
@@ -52,8 +52,37 @@ export const querySavedObjectType: SavedObjectsType = {
properties: {
title: { type: 'text' },
description: { type: 'text' },
+ isTemplate: { type: 'boolean' },
query: {
- properties: { language: { type: 'keyword' }, query: { type: 'keyword', index: false } },
+ properties: {
+ language: { type: 'keyword' },
+ query: { type: 'keyword', index: false },
+ dataset: {
+ type: 'object',
+ properties: {
+ id: { type: 'text' },
+ title: { type: 'text' },
+ type: { type: 'text' },
+ timeFieldName: { type: 'text' },
+ language: { type: 'text' },
+ dataSource: {
+ type: 'object',
+ properties: {
+ id: { type: 'text' },
+ title: { type: 'text' },
+ type: { type: 'text' },
+ meta: {
+ type: 'object',
+ properties: {
+ name: { type: 'text' },
+ sessionId: { type: 'text' },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
},
filters: { type: 'object', enabled: false },
timefilter: { type: 'object', enabled: false },