diff --git a/CHANGELOG.md b/CHANGELOG.md index 1580a367da8a..5ab7c00e0896 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Discover] Fix missing index pattern field from breaking Discover [#5626](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5626) - [BUG] Remove duplicate sample data as id 90943e30-9a47-11e8-b64d-95841ca0b247 ([5668](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5668)) - [BUG][Multiple Datasource] Fix datasource testing connection unexpectedly passed with wrong endpoint [#5663](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5663) +- [BUG] Resolve display issue and improve performance in Discover ([#5514](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5514)) ### 🚞 Infrastructure diff --git a/src/plugins/discover/public/application/components/chart/chart.tsx b/src/plugins/discover/public/application/components/chart/chart.tsx index f21ce3e4ef19..8fd83fa6435e 100644 --- a/src/plugins/discover/public/application/components/chart/chart.tsx +++ b/src/plugins/discover/public/application/components/chart/chart.tsx @@ -17,19 +17,20 @@ import { TimechartHeader, TimechartHeaderBucketInterval } from './timechart_head import { DiscoverHistogram } from './histogram/histogram'; import { DiscoverServices } from '../../../build_services'; import { Chart } from './utils'; -import { useDiscoverContext } from '../../view_components/context'; import { setInterval, useDispatch, useSelector } from '../../utils/state_management'; +import { RefetchSubject } from '../../view_components/utils/use_search'; interface DiscoverChartProps { - bucketInterval?: TimechartHeaderBucketInterval; - chartData?: Chart; + services: DiscoverServices; config: IUiSettingsClient; data: DataPublicPluginStart; hits: number; resetQuery: () => void; + bucketInterval?: TimechartHeaderBucketInterval; + chartData?: Chart; showResetButton?: boolean; isTimeBased?: boolean; - services: DiscoverServices; + refetch?: RefetchSubject; } export const DiscoverChart = ({ @@ -42,8 +43,8 @@ export const DiscoverChart = ({ isTimeBased, services, showResetButton = false, + refetch, }: DiscoverChartProps) => { - const { refetch$ } = useDiscoverContext(); const { from, to } = data.query.timefilter.timefilter.getTime(); const timeRange = { from: dateMath.parse(from)?.format('YYYY-MM-DDTHH:mm:ss.SSSZ') || '', @@ -53,7 +54,7 @@ export const DiscoverChart = ({ const dispatch = useDispatch(); const onChangeInterval = (newInterval: string) => { dispatch(setInterval(newInterval)); - refetch$.next(); + refetch?.next(); }; const timefilterUpdateHandler = useCallback( (ranges: { from: number; to: number }) => { diff --git a/src/plugins/discover/public/application/view_components/canvas/discover_canvas.scss b/src/plugins/discover/public/application/view_components/canvas/discover_canvas.scss index 92e1131a05e3..ac44d6e59e6b 100644 --- a/src/plugins/discover/public/application/view_components/canvas/discover_canvas.scss +++ b/src/plugins/discover/public/application/view_components/canvas/discover_canvas.scss @@ -34,3 +34,7 @@ } } } + +.trigger-reflow { + padding-right: 1px; /* Or any other property that affects layout */ +} diff --git a/src/plugins/discover/public/application/view_components/canvas/discover_chart_container.tsx b/src/plugins/discover/public/application/view_components/canvas/discover_chart_container.tsx index bade3ecae7a5..669a5d25aa41 100644 --- a/src/plugins/discover/public/application/view_components/canvas/discover_chart_container.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/discover_chart_container.tsx @@ -5,16 +5,34 @@ import './discover_chart_container.scss'; import React, { useMemo } from 'react'; -import { DiscoverViewServices } from '../../../build_services'; -import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; -import { useDiscoverContext } from '../context'; -import { SearchData } from '../utils/use_search'; import { DiscoverChart } from '../../components/chart/chart'; +import { DiscoverViewServices } from '../../../build_services'; +import { IndexPattern } from '../../../opensearch_dashboards_services'; +import { SavedSearch } from '../../../saved_searches'; +import { TimechartHeaderBucketInterval } from '../../components/chart/timechart_header'; +import { Chart } from '../../components/chart/utils'; +import { RefetchSubject } from '../utils/use_search'; + +interface DiscoverChartContainerProps { + services: DiscoverViewServices; + hits?: number; + bucketInterval?: TimechartHeaderBucketInterval | {}; + chartData?: Chart; + indexPattern?: IndexPattern; + savedSearch?: SavedSearch; + refetch?: RefetchSubject; +} -export const DiscoverChartContainer = ({ hits, bucketInterval, chartData }: SearchData) => { - const { services } = useOpenSearchDashboards(); +export const DiscoverChartContainer = ({ + hits, + bucketInterval, + chartData, + services, + indexPattern, + savedSearch, + refetch, +}: DiscoverChartContainerProps) => { const { uiSettings, data, core } = services; - const { indexPattern, savedSearch } = useDiscoverContext(); const isTimeBased = useMemo(() => (indexPattern ? indexPattern.isTimeBased() : false), [ indexPattern, @@ -35,6 +53,7 @@ export const DiscoverChartContainer = ({ hits, bucketInterval, chartData }: Sear services={services} showResetButton={!!savedSearch && !!savedSearch.id} isTimeBased={isTimeBased} + refetch={refetch} /> ); }; 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 3f7bbf4ec7b8..4f484497df7c 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 @@ -5,9 +5,7 @@ import React, { useCallback, useMemo } from 'react'; import { DiscoverViewServices } from '../../../build_services'; -import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; import { DataGridTable } from '../../components/data_grid/data_grid_table'; -import { useDiscoverContext } from '../context'; import { addColumn, removeColumn, @@ -22,13 +20,27 @@ import { SortOrder } from '../../../saved_searches/types'; import { DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../common'; import { OpenSearchSearchHit } from '../../doc_views/doc_views_types'; import { popularizeField } from '../../helpers/popularize_field'; +import { IndexPattern } from '../../../opensearch_dashboards_services'; +import { SavedSearch } from '../../../saved_searches'; +import { RefetchSubject } from '../../view_components/utils/use_search'; -interface Props { +interface DiscoverTableProps { + services: DiscoverViewServices; + refetch: RefetchSubject; + setShouldFetchOnColumnChange: React.Dispatch>; + indexPattern: IndexPattern; rows?: OpenSearchSearchHit[]; + savedSearch?: SavedSearch; } -export const DiscoverTable = ({ rows }: Props) => { - const { services } = useOpenSearchDashboards(); +export const DiscoverTable = ({ + services, + refetch, + setShouldFetchOnColumnChange, + indexPattern, + rows, + savedSearch, +}: DiscoverTableProps) => { const { uiSettings, data: { @@ -38,27 +50,29 @@ export const DiscoverTable = ({ rows }: Props) => { indexPatterns, } = services; - const { refetch$, indexPattern, savedSearch } = useDiscoverContext(); const { columns, sort } = useSelector((state) => state.discover); const dispatch = useDispatch(); const onAddColumn = (col: string) => { if (indexPattern && capabilities.discover?.save) { popularizeField(indexPattern, col, indexPatterns); } - + setShouldFetchOnColumnChange(false); dispatch(addColumn({ column: col })); }; const onRemoveColumn = (col: string) => { if (indexPattern && capabilities.discover?.save) { popularizeField(indexPattern, col, indexPatterns); } - + setShouldFetchOnColumnChange(false); dispatch(removeColumn(col)); }; - const onSetColumns = (cols: string[]) => dispatch(setColumns({ columns: cols })); + const onSetColumns = (cols: string[]) => { + setShouldFetchOnColumnChange(false); + dispatch(setColumns({ columns: cols })); + }; const onSetSort = (s: SortOrder[]) => { dispatch(setSort(s)); - refetch$.next(); + refetch.next(); }; const onAddFilter = useCallback( (field: string | IndexPatternField, values: string, operation: '+' | '-') => { @@ -80,11 +94,6 @@ export const DiscoverTable = ({ rows }: Props) => { [indexPattern, uiSettings] ); - if (indexPattern === undefined) { - // TODO: handle better - return null; - } - if (!rows || rows.length === 0) { // TODO: handle better return
{'loading...'}
; 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 d5c54158e997..ff12509e0686 100644 --- a/src/plugins/discover/public/application/view_components/canvas/index.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/index.tsx @@ -19,15 +19,21 @@ import { DiscoverViewServices } from '../../../build_services'; import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; import { filterColumns } from '../utils/filter_columns'; import { DEFAULT_COLUMNS_SETTING, MODIFY_COLUMNS_ON_SWITCH } from '../../../../common'; -import { OpenSearchSearchHit } from '../../../application/doc_views/doc_views_types'; import './discover_canvas.scss'; // eslint-disable-next-line import/no-default-export export default function DiscoverCanvas({ setHeaderActionMenu, history }: ViewProps) { - const { data$, refetch$, indexPattern } = useDiscoverContext(); const { - services: { uiSettings }, - } = useOpenSearchDashboards(); + data$, + refetch$, + indexPattern, + savedSearch, + inspectorAdapters, + shouldFetchOnColumnChange, + setShouldFetchOnColumnChange, + } = useDiscoverContext(); + const { services } = useOpenSearchDashboards(); + const { uiSettings } = services; const { columns } = useSelector((state) => state.discover); const filteredColumns = filterColumns( columns, @@ -52,7 +58,6 @@ export default function DiscoverCanvas({ setHeaderActionMenu, history }: ViewPro }, [refetch$] ); - const [rows, setRows] = useState(undefined); useEffect(() => { const subscription = data$.subscribe((next) => { @@ -67,11 +72,11 @@ export default function DiscoverCanvas({ setHeaderActionMenu, history }: ViewPro if (next.chartData && next.chartData !== fetchState.chartData) shouldUpdateState = true; if (next.rows && next.rows !== fetchState.rows) { shouldUpdateState = true; - setRows(next.rows); } // Update the state if any condition is met. if (shouldUpdateState) { + setShouldFetchOnColumnChange(true); setFetchState({ ...fetchState, ...next }); } }); @@ -79,7 +84,7 @@ export default function DiscoverCanvas({ setHeaderActionMenu, history }: ViewPro return () => { subscription.unsubscribe(); }; - }, [data$, fetchState]); + }, [data$, fetchState, setShouldFetchOnColumnChange]); useEffect(() => { if (indexPattern !== prevIndexPattern.current) { @@ -90,6 +95,24 @@ export default function DiscoverCanvas({ setHeaderActionMenu, history }: ViewPro const timeField = indexPattern?.timeFieldName ? indexPattern.timeFieldName : undefined; + // Add a reflow to ensure that updated content is properly laid out on the page. + useEffect(() => { + if (fetchState.status === ResultStatus.READY) { + const element = document.querySelector('.dscCanvas') as HTMLElement; + if (element) { + element.classList.add('trigger-reflow'); + setTimeout(() => { + element.classList.remove('trigger-reflow'); + }, 10); + } + } + }, [fetchState.status]); + + if (indexPattern === undefined || shouldFetchOnColumnChange === false) { + // TODO: handle better + return null; + } + return ( - {fetchState.status === ResultStatus.NO_RESULTS && ( @@ -115,15 +142,31 @@ export default function DiscoverCanvas({ setHeaderActionMenu, history }: ViewPro <> - + - + )} ); } +const MemorizedTopNav = React.memo(TopNav); const MemoizedDiscoverTable = React.memo(DiscoverTable); const MemoizedDiscoverChartContainer = React.memo(DiscoverChartContainer); diff --git a/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx b/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx index feb7b91e7c5e..4a51e32859af 100644 --- a/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx @@ -7,24 +7,32 @@ import React, { useEffect, useMemo, useState } from 'react'; import { TimeRange, Query } from 'src/plugins/data/common'; import { AppMountParameters } from '../../../../../../core/public'; import { PLUGIN_ID } from '../../../../common'; -import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; import { DiscoverViewServices } from '../../../build_services'; import { IndexPattern } from '../../../opensearch_dashboards_services'; import { getTopNavLinks } from '../../components/top_nav/get_top_nav_links'; -import { useDiscoverContext } from '../context'; import { getRootBreadcrumbs } from '../../helpers/breadcrumbs'; import { opensearchFilters, connectStorageToQueryState } from '../../../../../data/public'; +import { SavedSearch } from '../../../saved_searches'; +import { InspectorAdapters } from '../utils/use_search'; export interface TopNavProps { opts: { setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; onQuerySubmit: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; }; + services: DiscoverViewServices; + inspectorAdapters: InspectorAdapters; + indexPattern?: IndexPattern; + savedSearch?: SavedSearch; } -export const TopNav = ({ opts }: TopNavProps) => { - const { services } = useOpenSearchDashboards(); - const { inspectorAdapters, savedSearch, indexPattern } = useDiscoverContext(); +export const TopNav = ({ + opts, + services, + inspectorAdapters, + savedSearch, + indexPattern, +}: TopNavProps) => { const [indexPatterns, setIndexPatterns] = useState(undefined); const { diff --git a/src/plugins/discover/public/application/view_components/panel/index.tsx b/src/plugins/discover/public/application/view_components/panel/index.tsx index 6b4cd2a87c91..8e22a8b6ca7c 100644 --- a/src/plugins/discover/public/application/view_components/panel/index.tsx +++ b/src/plugins/discover/public/application/view_components/panel/index.tsx @@ -32,7 +32,7 @@ export default function DiscoverPanel(props: ViewProps) { capabilities, indexPatterns, } = services; - const { data$, indexPattern } = useDiscoverContext(); + const { data$, indexPattern, setShouldFetchOnColumnChange } = useDiscoverContext(); const [fetchState, setFetchState] = useState(data$.getValue()); const { columns } = useSelector((state) => ({ @@ -95,7 +95,7 @@ export default function DiscoverPanel(props: ViewProps) { if (indexPattern && capabilities.discover?.save) { popularizeField(indexPattern, fieldName, indexPatterns); } - + setShouldFetchOnColumnChange(false); dispatch( addColumn({ column: fieldName, @@ -107,7 +107,7 @@ export default function DiscoverPanel(props: ViewProps) { if (indexPattern && capabilities.discover?.save) { popularizeField(indexPattern, fieldName, indexPatterns); } - + setShouldFetchOnColumnChange(false); dispatch(removeColumn(fieldName)); }} onReorderFields={(source, destination) => { diff --git a/src/plugins/discover/public/application/view_components/utils/index_pattern_helper.ts b/src/plugins/discover/public/application/view_components/utils/index_pattern_helper.ts deleted file mode 100644 index 2db2b6111868..000000000000 --- a/src/plugins/discover/public/application/view_components/utils/index_pattern_helper.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { i18n } from '@osd/i18n'; -import { SearchSource, IndexPattern } from 'src/plugins/data/public'; -import { SavedObject, ToastsStart } from 'opensearch-dashboards/public'; -import { redirectWhenMissing, getUrlTracker } from '../../../opensearch_dashboards_services'; -import { getIndexPatternId } from '../../helpers/get_index_pattern_id'; - -export type IndexPatternSavedObject = SavedObject & { title: string }; -export interface IndexPatternData { - loaded: IndexPattern; - stateVal: string; - stateValFound: boolean; -} - -export const fetchIndexPattern = async (data, config) => { - await data.indexPatterns.ensureDefaultIndexPattern(); - const indexPatternList = await data.indexPatterns.getCache(); - const id = getIndexPatternId('', indexPatternList, config.get('defaultIndex')); - const indexPatternData = await data.indexPatterns.get(id); - const ip: IndexPatternData = { - loaded: indexPatternData, - stateVal: '', // TODO: get stateVal from appStateContainer - stateValFound: false, // TODO: get stateValFound from appStateContainer - }; - return ip; -}; - -export const fetchSavedSearch = async ( - core, - basePath, - history, - savedSearchId, - services, - toastNotifications -) => { - try { - const savedSearch = await services.getSavedSearchById(savedSearchId); - return savedSearch; - } catch (error) { - // TODO: handle redirect with Data Explorer - redirectWhenMissing({ - history, - navigateToApp: core.application.navigateToApp, - basePath, - mapping: { - search: '/', - 'index-pattern': { - app: 'management', - path: `opensearch-dashboards/objects/savedSearches/${savedSearchId}`, - }, - }, - toastNotifications, - onBeforeRedirect() { - getUrlTracker().setTrackedUrl('/'); - }, - }); - } -}; - -export function resolveIndexPattern( - ip: IndexPatternData, - searchSource: SearchSource, - toastNotifications: ToastsStart -) { - const { loaded: loadedIndexPattern, stateVal, stateValFound } = ip; - - const ownIndexPattern = searchSource.getOwnField('index'); - - if (ownIndexPattern && !stateVal) { - return ownIndexPattern; - } - - if (stateVal && !stateValFound) { - const warningTitle = i18n.translate('discover.valueIsNotConfiguredIndexPatternIDWarningTitle', { - defaultMessage: '{id} is not a configured index pattern ID', - values: { - id: `"${stateVal}"`, - }, - }); - - if (ownIndexPattern) { - toastNotifications.addWarning({ - title: warningTitle, - text: i18n.translate('discover.showingSavedIndexPatternWarningDescription', { - defaultMessage: - 'Showing the saved index pattern: "{ownIndexPatternTitle}" ({ownIndexPatternId})', - values: { - ownIndexPatternTitle: ownIndexPattern.title, - ownIndexPatternId: ownIndexPattern.id, - }, - }), - }); - return ownIndexPattern; - } - - toastNotifications.addWarning({ - title: warningTitle, - text: i18n.translate('discover.showingDefaultIndexPatternWarningDescription', { - defaultMessage: - 'Showing the default index pattern: "{loadedIndexPatternTitle}" ({loadedIndexPatternId})', - values: { - loadedIndexPatternTitle: loadedIndexPattern.title, - loadedIndexPatternId: loadedIndexPattern.id, - }, - }), - }); - } - - return loadedIndexPattern; -} diff --git a/src/plugins/discover/public/application/view_components/utils/use_index_pattern.ts b/src/plugins/discover/public/application/view_components/utils/use_index_pattern.ts index e8a81234278e..6b96f101ba92 100644 --- a/src/plugins/discover/public/application/view_components/utils/use_index_pattern.ts +++ b/src/plugins/discover/public/application/view_components/utils/use_index_pattern.ts @@ -62,7 +62,9 @@ export const useIndexPattern = (services: DiscoverViewServices) => { data.indexPatterns.getCache().then((indexPatternList) => { const newId = getIndexPatternId('', indexPatternList, config.get('defaultIndex')); store!.dispatch(updateIndexPattern(newId)); - fetchIndexPatternDetails(newId); + if (indexPatternList && indexPatternList.length !== 0) { + fetchIndexPatternDetails(newId); + } }); } else { fetchIndexPatternDetails(indexPatternIdFromState); 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 06eabb1e139f..753b138d77cc 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 @@ -49,6 +49,10 @@ export interface SearchData { chartData?: Chart; } +export interface InspectorAdapters { + requests: RequestAdapter; +} + export type SearchRefetch = 'refetch' | undefined; export type DataSubject = BehaviorSubject; @@ -56,7 +60,7 @@ export type RefetchSubject = Subject; /** * A hook that provides functionality for fetching and managing discover search data. - * @returns { data: DataSubject, refetch$: RefetchSubject, indexPattern: IndexPattern, savedSearch?: SavedSearch, inspectorAdapters } - data is a BehaviorSubject that emits the current search data, refetch$ is a Subject that can be used to trigger a refetch, savedSearch is the saved search object if it exists + * @returns { data: DataSubject, refetch$: RefetchSubject, indexPattern: IndexPattern, savedSearch?: SavedSearch, inspectorAdapters: InspectorAdapters } - data is a BehaviorSubject that emits the current search data, refetch$ is a Subject that can be used to trigger a refetch, savedSearch is the saved search object if it exists * @example * const { data$, refetch$ } = useSearch(); * useEffect(() => { @@ -83,7 +87,9 @@ export const useSearch = (services: DiscoverViewServices) => { }); const inspectorAdapters = { requests: new RequestAdapter(), - }; + } as InspectorAdapters; + + const [shouldFetchOnColumnChange, setShouldFetchOnColumnChange] = useState(true); const shouldSearchOnPageLoad = useCallback(() => { // A saved search is created on every page load, so we check the ID to see if we're loading a @@ -103,14 +109,29 @@ export const useSearch = (services: DiscoverViewServices) => { [shouldSearchOnPageLoad] ); const refetch$ = useMemo(() => new Subject(), []); + const resetState = useCallback(() => { + // Reset fetchStateRef fields + fetchStateRef.current = { + abortController: new AbortController(), + fieldCounts: {}, + rows: [], + }; + + // Reset the data$ observable + data$.next({ + status: ResultStatus.UNINITIALIZED, + fetchCounter: 0, + fieldCounts: {}, + rows: [], + bucketInterval: {}, + chartData: undefined, + }); + }, [data$]); const fetch = useCallback(async () => { - if (!indexPattern) { - data$.next({ - status: shouldSearchOnPageLoad() ? ResultStatus.LOADING : ResultStatus.UNINITIALIZED, - }); - return; - } + // reset state + resetState(); + if (!indexPattern) return; if (!validateTimeRange(timefilter.getTime(), toastNotifications)) { return data$.next({ @@ -134,11 +155,6 @@ export const useSearch = (services: DiscoverViewServices) => { }); 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', { @@ -220,8 +236,8 @@ export const useSearch = (services: DiscoverViewServices) => { savedSearch?.searchSource, data$, sort, - shouldSearchOnPageLoad, inspectorAdapters.requests, + resetState, ]); useEffect(() => { @@ -229,11 +245,9 @@ export const useSearch = (services: DiscoverViewServices) => { refetch$, filterManager.getFetches$(), timefilter.getFetch$(), - timefilter.getTimeUpdate$(), timefilter.getAutoRefreshFetch$(), data.query.queryString.getUpdates$() ).pipe(debounceTime(100)); - const subscription = fetch$.subscribe(() => { (async () => { try { @@ -253,7 +267,6 @@ export const useSearch = (services: DiscoverViewServices) => { subscription.unsubscribe(); }; }, [ - data$, data.query.queryString, filterManager, refetch$, @@ -307,6 +320,8 @@ export const useSearch = (services: DiscoverViewServices) => { indexPattern, savedSearch, inspectorAdapters, + shouldFetchOnColumnChange, + setShouldFetchOnColumnChange, }; };