diff --git a/src/plugins/discover/public/application/components/data_grid/data_grid_table.tsx b/src/plugins/discover/public/application/components/data_grid/data_grid_table.tsx
index 3f99c009e56a..98943784237f 100644
--- a/src/plugins/discover/public/application/components/data_grid/data_grid_table.tsx
+++ b/src/plugins/discover/public/application/components/data_grid/data_grid_table.tsx
@@ -16,6 +16,7 @@ import { DocViewFilterFn } from '../../doc_views/doc_views_types';
import { DiscoverServices } from '../../../build_services';
import { OpenSearchSearchHit } from '../../doc_views/doc_views_types';
import { usePagination } from '../utils/use_pagination';
+import { SortOrder } from '../../../saved_searches/types';
export interface DataGridTableProps {
columns: string[];
@@ -23,10 +24,10 @@ export interface DataGridTableProps {
onAddColumn: (column: string) => void;
onFilter: DocViewFilterFn;
onRemoveColumn: (column: string) => void;
- onSort: (sort: Array<[string, 'asc' | 'desc']>) => void;
+ onSort: (sort: SortOrder[]) => void;
rows: OpenSearchSearchHit[];
onSetColumns: (columns: string[]) => void;
- sort: Array<[string, 'asc' | 'desc']>;
+ sort: SortOrder[];
displayTimeColumn: boolean;
services: DiscoverServices;
isToolbarVisible?: boolean;
diff --git a/src/plugins/discover/public/application/components/data_grid/data_grid_table_cell_actions.tsx b/src/plugins/discover/public/application/components/data_grid/data_grid_table_cell_actions.tsx
new file mode 100644
index 000000000000..3d81ba1afb33
--- /dev/null
+++ b/src/plugins/discover/public/application/components/data_grid/data_grid_table_cell_actions.tsx
@@ -0,0 +1,76 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { EuiDataGridColumnCellActionProps } from '@elastic/eui';
+import { i18n } from '@osd/i18n';
+import { IndexPatternField } from '../../../../../data/common';
+import { useDataGridContext } from './data_grid_table_context';
+
+export function getCellActions(field: IndexPatternField) {
+ const cellActions = field.filterable
+ ? [
+ ({ rowIndex, columnId, Component }: EuiDataGridColumnCellActionProps) => {
+ const { indexPattern, rows, onFilter } = useDataGridContext();
+
+ const filterForValueText = i18n.translate('discover.filterForValue', {
+ defaultMessage: 'Filter for value',
+ });
+ const filterForValueLabel = i18n.translate('discover.filterForValueLabel', {
+ defaultMessage: 'Filter for value: {value}',
+ values: { value: columnId },
+ });
+
+ return (
+ {
+ const row = rows[rowIndex];
+ const flattened = indexPattern.flattenHit(row);
+
+ if (flattened) {
+ onFilter(columnId, flattened[columnId], '+');
+ }
+ }}
+ iconType="plusInCircle"
+ aria-label={filterForValueLabel}
+ data-test-subj="filterForValue"
+ >
+ {filterForValueText}
+
+ );
+ },
+ ({ rowIndex, columnId, Component }: EuiDataGridColumnCellActionProps) => {
+ const { indexPattern, rows, onFilter } = useDataGridContext();
+
+ const filterOutValueText = i18n.translate('discover.filterOutValue', {
+ defaultMessage: 'Filter out value',
+ });
+ const filterOutValueLabel = i18n.translate('discover.filterOutValueLabel', {
+ defaultMessage: 'Filter out value: {value}',
+ values: { value: columnId },
+ });
+
+ return (
+ {
+ const row = rows[rowIndex];
+ const flattened = indexPattern.flattenHit(row);
+
+ if (flattened) {
+ onFilter(columnId, flattened[columnId], '-');
+ }
+ }}
+ iconType="minusInCircle"
+ aria-label={filterOutValueLabel}
+ data-test-subj="filterOutValue"
+ >
+ {filterOutValueText}
+
+ );
+ },
+ ]
+ : undefined;
+ return cellActions;
+}
diff --git a/src/plugins/discover/public/application/components/data_grid/data_grid_table_columns.tsx b/src/plugins/discover/public/application/components/data_grid/data_grid_table_columns.tsx
index 1561a7e838da..f38debbd7be3 100644
--- a/src/plugins/discover/public/application/components/data_grid/data_grid_table_columns.tsx
+++ b/src/plugins/discover/public/application/components/data_grid/data_grid_table_columns.tsx
@@ -6,6 +6,7 @@
import { EuiDataGridColumn } from '@elastic/eui';
import { i18n } from '@osd/i18n';
import { IndexPattern } from '../../../opensearch_dashboards_services';
+import { getCellActions } from './data_grid_table_cell_actions';
export function buildDataGridColumns(
columnNames: string[],
@@ -37,7 +38,7 @@ export function generateDataGridTableColumn(colName: string, idxPattern: IndexPa
showMoveLeft: false,
showMoveRight: false,
},
- cellActions: [],
+ cellActions: idxPatternField ? getCellActions(idxPatternField) : [],
};
if (dataGridCol.id === idxPattern.timeFieldName) {
diff --git a/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx b/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx
index 38c8092d5f73..73cc1af35e13 100644
--- a/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx
+++ b/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx
@@ -10,6 +10,7 @@ import { DiscoverServices } from '../../../build_services';
import { RootState, DefaultViewState } from '../../../../../data_explorer/public';
import { buildColumns } from '../columns';
import * as utils from './common';
+import { SortOrder } from '../../../saved_searches/types';
export interface DiscoverState {
/**
@@ -31,7 +32,7 @@ export interface DiscoverState {
/**
* Array of the used sorting [[field,direction],...]
*/
- sort: Array<[string, string]>;
+ sort: SortOrder[];
/**
* id of the used saved search
*/
@@ -129,7 +130,7 @@ export const discoverSlice = createSlice({
columns,
};
},
- setSort(state, action: PayloadAction>) {
+ setSort(state, action: PayloadAction) {
return {
...state,
sort: action.payload,
diff --git a/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx b/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx
index 814788406f7c..f4e28a59b08d 100644
--- a/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx
+++ b/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx
@@ -20,6 +20,7 @@ import {
import { ResultStatus, SearchData } from '../utils/use_search';
import { IndexPatternField, opensearchFilters } from '../../../../../data/public';
import { DocViewFilterFn } from '../../doc_views/doc_views_types';
+import { SortOrder } from '../../../saved_searches/types';
interface Props {
history: History;
@@ -40,7 +41,7 @@ export const DiscoverTable = ({ history }: Props) => {
const onRemoveColumn = (col: string) => dispatch(removeColumn(col));
const onSetColumns = (cols: string[]) =>
dispatch(setColumns({ timefield: indexPattern.timeFieldName, columns: cols }));
- const onSetSort = (s: Array<[string, string]>) => dispatch(setSort(s));
+ const onSetSort = (s: SortOrder[]) => dispatch(setSort(s));
const onAddFilter = useCallback(
(field: IndexPatternField, values: string, operation: '+' | '-') => {
const newFilters = opensearchFilters.generateFilters(
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 af845fb1ad2f..729439195dc4 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
@@ -30,6 +30,7 @@ import {
getResponseInspectorStats,
} from '../../../opensearch_dashboards_services';
import { SEARCH_ON_PAGE_LOAD_SETTING } from '../../../../common';
+import { SortOrder } from '../../../saved_searches/types';
export enum ResultStatus {
UNINITIALIZED = 'uninitialized',
@@ -67,7 +68,7 @@ export type RefetchSubject = Subject;
*/
export const useSearch = (services: DiscoverServices) => {
const [savedSearch, setSavedSearch] = useState(undefined);
- const savedSearchId = useSelector((state) => state.discover.savedSearch);
+ const { savedSearch: savedSearchId, sort } = useSelector((state) => state.discover);
const indexPattern = useIndexPattern(services);
const { data, filterManager, getSavedSearchById, core, toastNotifications } = services;
const timefilter = data.query.timefilter.timefilter;
@@ -101,122 +102,125 @@ export const useSearch = (services: DiscoverServices) => {
[shouldSearchOnPageLoad]
);
const refetch$ = useMemo(() => new Subject(), []);
+ const sort$ = useMemo(() => new Subject(), []);
- const fetch = useCallback(async () => {
- if (!indexPattern) {
- data$.next({
- status: shouldSearchOnPageLoad() ? ResultStatus.LOADING : ResultStatus.UNINITIALIZED,
- });
- return;
- }
-
- if (!validateTimeRange(timefilter.getTime(), toastNotifications)) {
- return data$.next({
- status: ResultStatus.NO_RESULTS,
- rows: [],
- });
- }
-
- // Abort any in-progress requests before fetching again
- if (fetchStateRef.current.abortController) fetchStateRef.current.abortController.abort();
- fetchStateRef.current.abortController = new AbortController();
- const sort = undefined;
- const histogramConfigs = indexPattern.timeFieldName
- ? createHistogramConfigs(indexPattern, 'auto', data)
- : undefined;
- const searchSource = await updateSearchSource({
- indexPattern,
- services,
- sort,
- searchSource: savedSearch?.searchSource,
- histogramConfigs,
- });
+ const fetch = useCallback(
+ async (sortArr: SortOrder[]) => {
+ if (!indexPattern) {
+ data$.next({
+ status: shouldSearchOnPageLoad() ? ResultStatus.LOADING : ResultStatus.UNINITIALIZED,
+ });
+ return;
+ }
- 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 });
+ if (!validateTimeRange(timefilter.getTime(), toastNotifications)) {
+ return data$.next({
+ status: ResultStatus.NO_RESULTS,
+ rows: [],
+ });
}
- // Initialize inspect adapter for search source
- inspectorAdapters.requests.reset();
- const title = i18n.translate('discover.inspectorRequestDataTitle', {
- defaultMessage: 'data',
- });
- const description = i18n.translate('discover.inspectorRequestDescription', {
- defaultMessage: 'This request queries OpenSearch to fetch the data for the search.',
- });
- const inspectorRequest = inspectorAdapters.requests.start(title, { description });
- inspectorRequest.stats(getRequestInspectorStats(searchSource));
- searchSource.getSearchRequestBody().then((body) => {
- inspectorRequest.json(body);
+ // Abort any in-progress requests before fetching again
+ if (fetchStateRef.current.abortController) fetchStateRef.current.abortController.abort();
+ fetchStateRef.current.abortController = new AbortController();
+ const histogramConfigs = indexPattern.timeFieldName
+ ? createHistogramConfigs(indexPattern, 'auto', data)
+ : undefined;
+ const searchSource = await updateSearchSource({
+ indexPattern,
+ services,
+ sort: sortArr,
+ searchSource: savedSearch?.searchSource,
+ histogramConfigs,
});
- // Execute the search
- const fetchResp = await searchSource.fetch({
- abortSignal: fetchStateRef.current.abortController.signal,
- });
+ 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 });
+ }
+
+ // Initialize inspect adapter for search source
+ inspectorAdapters.requests.reset();
+ const title = i18n.translate('discover.inspectorRequestDataTitle', {
+ defaultMessage: 'data',
+ });
+ const description = i18n.translate('discover.inspectorRequestDescription', {
+ defaultMessage: 'This request queries OpenSearch to fetch the data for the search.',
+ });
+ const inspectorRequest = inspectorAdapters.requests.start(title, { description });
+ inspectorRequest.stats(getRequestInspectorStats(searchSource));
+ searchSource.getSearchRequestBody().then((body) => {
+ inspectorRequest.json(body);
+ });
- inspectorRequest
- .stats(getResponseInspectorStats(fetchResp, searchSource))
- .ok({ json: fetchResp });
- const hits = fetchResp.hits.total as number;
- const rows = fetchResp.hits.hits;
- let bucketInterval = {};
- let chartData;
- for (const row of rows) {
- const fields = Object.keys(indexPattern.flattenHit(row));
- for (const fieldName of fields) {
- fetchStateRef.current.fieldCounts[fieldName] =
- (fetchStateRef.current.fieldCounts[fieldName] || 0) + 1;
+ // Execute the search
+ const fetchResp = await searchSource.fetch({
+ abortSignal: fetchStateRef.current.abortController.signal,
+ });
+
+ inspectorRequest
+ .stats(getResponseInspectorStats(fetchResp, searchSource))
+ .ok({ json: fetchResp });
+ const hits = fetchResp.hits.total as number;
+ const rows = fetchResp.hits.hits;
+ let bucketInterval = {};
+ let chartData;
+ for (const row of rows) {
+ const fields = Object.keys(indexPattern.flattenHit(row));
+ for (const fieldName of fields) {
+ fetchStateRef.current.fieldCounts[fieldName] =
+ (fetchStateRef.current.fieldCounts[fieldName] || 0) + 1;
+ }
}
- }
- if (histogramConfigs) {
- const bucketAggConfig = histogramConfigs.aggs[1];
- const tabifiedData = tabifyAggResponse(histogramConfigs, fetchResp);
- const dimensions = getDimensions(histogramConfigs, data);
- if (dimensions) {
- if (bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig)) {
- bucketInterval = bucketAggConfig.buckets?.getInterval();
+ if (histogramConfigs) {
+ const bucketAggConfig = histogramConfigs.aggs[1];
+ const tabifiedData = tabifyAggResponse(histogramConfigs, fetchResp);
+ const dimensions = getDimensions(histogramConfigs, data);
+ if (dimensions) {
+ if (bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig)) {
+ bucketInterval = bucketAggConfig.buckets?.getInterval();
+ }
+ // @ts-ignore tabifiedData is compatible but due to the way it is typed typescript complains
+ chartData = buildPointSeriesData(tabifiedData, dimensions);
}
- // @ts-ignore tabifiedData is compatible but due to the way it is typed typescript complains
- chartData = buildPointSeriesData(tabifiedData, dimensions);
}
- }
- fetchStateRef.current.fieldCounts = fetchStateRef.current.fieldCounts!;
- fetchStateRef.current.rows = rows;
- data$.next({
- status: rows.length > 0 ? ResultStatus.READY : ResultStatus.NO_RESULTS,
- fieldCounts: fetchStateRef.current.fieldCounts,
- hits,
- rows,
- bucketInterval,
- chartData,
- });
- } catch (error) {
- // If the request was aborted then no need to surface this error in the UI
- if (error instanceof Error && error.name === 'AbortError') return;
+ fetchStateRef.current.fieldCounts = fetchStateRef.current.fieldCounts!;
+ fetchStateRef.current.rows = rows;
+ data$.next({
+ status: rows.length > 0 ? ResultStatus.READY : ResultStatus.NO_RESULTS,
+ fieldCounts: fetchStateRef.current.fieldCounts,
+ hits,
+ rows,
+ bucketInterval,
+ chartData,
+ });
+ } catch (error) {
+ // If the request was aborted then no need to surface this error in the UI
+ if (error instanceof Error && error.name === 'AbortError') return;
- data$.next({
- status: ResultStatus.NO_RESULTS,
- rows: [],
- });
+ data$.next({
+ status: ResultStatus.NO_RESULTS,
+ rows: [],
+ });
- data.search.showError(error as Error);
- }
- }, [
- indexPattern,
- timefilter,
- toastNotifications,
- data,
- services,
- savedSearch?.searchSource,
- data$,
- shouldSearchOnPageLoad,
- inspectorAdapters.requests,
- ]);
+ data.search.showError(error as Error);
+ }
+ },
+ [
+ indexPattern,
+ timefilter,
+ toastNotifications,
+ data,
+ services,
+ savedSearch?.searchSource,
+ data$,
+ shouldSearchOnPageLoad,
+ inspectorAdapters.requests,
+ ]
+ );
useEffect(() => {
const fetch$ = merge(
@@ -225,13 +229,14 @@ export const useSearch = (services: DiscoverServices) => {
timefilter.getFetch$(),
timefilter.getTimeUpdate$(),
timefilter.getAutoRefreshFetch$(),
- data.query.queryString.getUpdates$()
+ data.query.queryString.getUpdates$(),
+ sort$
).pipe(debounceTime(100));
const subscription = fetch$.subscribe(() => {
(async () => {
try {
- await fetch();
+ await fetch(sort);
} catch (error) {
core.fatalErrors.add(error as Error);
}
@@ -244,7 +249,17 @@ export const useSearch = (services: DiscoverServices) => {
return () => {
subscription.unsubscribe();
};
- }, [data$, data.query.queryString, filterManager, refetch$, timefilter, fetch, core.fatalErrors]);
+ }, [
+ data$,
+ data.query.queryString,
+ filterManager,
+ refetch$,
+ sort,
+ sort$,
+ timefilter,
+ fetch,
+ core.fatalErrors,
+ ]);
// Get savedSearch if it exists
useEffect(() => {
@@ -256,6 +271,10 @@ export const useSearch = (services: DiscoverServices) => {
return () => {};
}, [getSavedSearchById, savedSearchId]);
+ useEffect(() => {
+ sort$.next(sort);
+ }, [sort, sort$]);
+
return {
data$,
refetch$,
diff --git a/src/plugins/discover/public/saved_searches/types.ts b/src/plugins/discover/public/saved_searches/types.ts
index 97bd4966e94c..112d7d998c97 100644
--- a/src/plugins/discover/public/saved_searches/types.ts
+++ b/src/plugins/discover/public/saved_searches/types.ts
@@ -31,7 +31,7 @@
import { SavedObject } from '../../../saved_objects/public';
import { ISearchSource } from '../../../data/public';
-export type SortOrder = [string, string];
+export type SortOrder = [string, 'asc' | 'desc'];
export interface SavedSearch
extends Pick {
searchSource: ISearchSource; // This is optional in SavedObject, but required for SavedSearch