Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleanup OsdUrlStateStorage subscription in TopNav #9167

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelogs/fragments/9167.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
fix:
- Cleanup OsdUrlStateStorage subscription in TopNav ([#9167](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/9167))
1 change: 1 addition & 0 deletions src/plugins/data/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,7 @@ export { Filter, Query, RefreshInterval, TimeRange } from '../common';
export {
createSavedQueryService,
connectStorageToQueryState,
useConnectStorageToQueryState,
connectToQueryState,
syncQueryStateWithUrl,
QueryState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,15 @@ describe('connect_storage_to_query_state', () => {
sessionStorage: new DataStorage(window.sessionStorage, 'opensearch_dashboards.'),
defaultSearchInterceptor: mockSearchInterceptor,
application: setupMock.application,
notifications: setupMock.notifications,
});
queryServiceStart = queryService.start({
uiSettings: setupMock.uiSettings,
uiSettings: startMock.uiSettings,
storage: new DataStorage(window.localStorage, 'opensearch_dashboards.'),
savedObjectsClient: startMock.savedObjects.client,
indexPatterns: indexPatternsMock,
application: startMock.application,
notifications: startMock.notifications,
});
indexPatternsMock = ({
get: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
import { Subscription } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import _ from 'lodash';
import { CoreStart } from 'opensearch-dashboards/public';
import {
BaseStateContainer,
IOsdUrlStateStorage,
Expand All @@ -41,27 +40,28 @@ import { QueryState, QueryStateChange } from './types';
import { FilterStateStore, COMPARE_ALL_OPTIONS, compareFilters } from '../../../common';
import { validateTimeRange } from '../timefilter';

export interface ISyncConfig {
filters: FilterStateStore;
query: boolean;
dataset?: boolean;
}

/**
* Helper function to sync up filter and query services in data plugin
* with a URL state storage so plugins can persist the app filter and query
* values across refresh
* @param QueryService: either setup or start
* @param OsdUrlStateStorage to use for syncing and store data
* @param OsdUrlStateStorage to use for syncing and store data
* @param syncConfig app filter and query
*/
export const connectStorageToQueryState = async (
export const connectStorageToQueryState = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this function ever return anything other than the unsub callback function, besides it reaching the catch block? Several questions regarding that:

  • Based on the call pattern that you provided on the usage of the returning callback function, can't we just unsub from the end of this function instead of wrapping it with a useEffect by the caller?
  • If some subs were made and then getting to the catch block for some error, don't we want to unsub from the catch as well?

Copy link
Author

@Maosaic Maosaic Jan 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the intended behaviour hear is let the scope where invokes connectStorageToQueryState to maintain the life cycle of the subscription.

It make senses to me to unsub in the catch block, but currently the subscription is building at the end of the try block, it's unlikely to have any lingering subscription if it reach catch block.

{
filterManager,
queryString,
state$,
}: Pick<QueryStart | QuerySetup, 'timefilter' | 'filterManager' | 'queryString' | 'state$'>,
OsdUrlStateStorage: IOsdUrlStateStorage,
syncConfig: {
filters: FilterStateStore;
query: boolean;
dataSet?: boolean;
},
uiSettings?: CoreStart['uiSettings']
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about uiSettings?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a unused variable

syncConfig: ISyncConfig
) => {
try {
const syncKeys: Array<keyof QueryStateChange> = [];
Expand Down
1 change: 1 addition & 0 deletions src/plugins/data/public/query/state_sync/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@
*/

export { connectToQueryState, connectStorageToQueryState } from './connect_to_query_state';
export { useConnectStorageToQueryState } from './use_connect_to_query_state';
export { syncQueryStateWithUrl } from './sync_state_with_url';
export { QueryState, QueryStateChange } from './types';
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { Subscription } from 'rxjs';
import { FilterManager } from '../filter_manager';
import { getFilter } from '../filter_manager/test_helpers/get_stub_filter';
import {
DataStorage,
Filter,
FilterStateStore,
IndexPatternsService,
Query,
} from '../../../common';
import { coreMock } from '../../../../../core/public/mocks';
import {
IOsdUrlStateStorage,
createOsdUrlStateStorage,
} from '../../../../opensearch_dashboards_utils/public';
import { QueryService, QueryStart } from '../query_service';
import { connectStorageToQueryState } from './connect_to_query_state';
import { createBrowserHistory, History } from 'history';
import { QueryStringContract } from '../query_string';
import { ISearchInterceptor } from '../../search';
import { renderHook } from '@testing-library/react-hooks';
import { useConnectStorageToQueryState } from './use_connect_to_query_state';

jest.mock('./connect_to_query_state');

const setupMock = coreMock.createSetup();
const startMock = coreMock.createStart();

describe('use_connect_storage_to_query_state', () => {
let queryServiceStart: QueryStart;
let queryString: QueryStringContract;
let queryChangeSub: Subscription;
let queryChangeTriggered = jest.fn();
let filterManager: FilterManager;
let filterManagerChangeSub: Subscription;
let filterManagerChangeTriggered = jest.fn();
let osdUrlStateStorage: IOsdUrlStateStorage;
let indexPatternsMock: IndexPatternsService;
let history: History;
let gF1: Filter;
let gF2: Filter;
let aF1: Filter;
let aF2: Filter;
let q1: Query;
let mockSearchInterceptor: jest.Mocked<ISearchInterceptor>;

beforeEach(() => {
const queryService = new QueryService();
mockSearchInterceptor = {} as jest.Mocked<ISearchInterceptor>;
queryService.setup({
uiSettings: setupMock.uiSettings,
storage: new DataStorage(window.localStorage, 'opensearch_dashboards.'),
sessionStorage: new DataStorage(window.sessionStorage, 'opensearch_dashboards.'),
defaultSearchInterceptor: mockSearchInterceptor,
application: setupMock.application,
notifications: setupMock.notifications,
});
queryServiceStart = queryService.start({
uiSettings: startMock.uiSettings,
storage: new DataStorage(window.localStorage, 'opensearch_dashboards.'),
savedObjectsClient: startMock.savedObjects.client,
indexPatterns: indexPatternsMock,
application: startMock.application,
notifications: startMock.notifications,
});
indexPatternsMock = ({
get: jest.fn(),
} as unknown) as IndexPatternsService;

queryString = queryServiceStart.queryString;
queryChangeTriggered = jest.fn();
queryChangeSub = queryString.getUpdates$().subscribe(queryChangeTriggered);

filterManager = queryServiceStart.filterManager;
filterManagerChangeTriggered = jest.fn();
filterManagerChangeSub = filterManager.getUpdates$().subscribe(filterManagerChangeTriggered);

window.location.href = '/';
history = createBrowserHistory();
osdUrlStateStorage = createOsdUrlStateStorage({ useHash: false, history });

gF1 = getFilter(FilterStateStore.GLOBAL_STATE, true, true, 'key1', 'value1');
gF2 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'key2', 'value2');
aF1 = getFilter(FilterStateStore.APP_STATE, true, true, 'key3', 'value3');
aF2 = getFilter(FilterStateStore.APP_STATE, false, false, 'key4', 'value4');

q1 = {
query: 'count is less than 100',
language: 'kuery',
};
});

afterEach(() => {
filterManagerChangeSub.unsubscribe();
queryChangeSub.unsubscribe();
});

it('Should invoke connectStorageToQueryState', () => {
const { result } = renderHook(() =>
useConnectStorageToQueryState(queryServiceStart, osdUrlStateStorage, {
filters: FilterStateStore.APP_STATE,
query: true,
})
);
expect(connectStorageToQueryState).toHaveBeenCalledTimes(1);
expect(result.current).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { useEffect } from 'react';
import { QuerySetup, QueryStart } from '../query_service';
import { IOsdUrlStateStorage } from '../../../../opensearch_dashboards_utils/public';
import { connectStorageToQueryState, ISyncConfig } from './connect_to_query_state';

/**
* Hook version of connectStorageToQueryState, automatically unSub
* from OsdUrlStateStorage
* @param QueryService: either setup or start
* @param OsdUrlStateStorage to use for syncing and store data
* @param syncConfig app filter and query
*/
export const useConnectStorageToQueryState = (
QueryService: Pick<
QueryStart | QuerySetup,
'timefilter' | 'filterManager' | 'queryString' | 'state$'
>,
OsdUrlStateStorage: IOsdUrlStateStorage,
syncConfig: ISyncConfig
) => {
useEffect(() => {
const unSub = connectStorageToQueryState(QueryService, OsdUrlStateStorage, syncConfig);
return () => {
if (unSub) {
unSub();
}
};
}, [OsdUrlStateStorage, QueryService, syncConfig]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/e
import { i18n } from '@osd/i18n';
import { AppMountParameters } from '../../../../../../core/public';
import {
connectStorageToQueryState,
useConnectStorageToQueryState,
opensearchFilters,
QueryStatus,
} from '../../../../../data/public';
Expand Down Expand Up @@ -65,15 +65,10 @@ export const TopNav = ({ opts, showSaveQuery, isEnhancementsEnabled }: TopNavPro
? getTopNavLinks(services, inspectorAdapters, savedSearch, isEnhancementsEnabled)
: [];

connectStorageToQueryState(
services.data.query,
osdUrlStateStorage,
{
filters: opensearchFilters.FilterStateStore.APP_STATE,
query: true,
},
uiSettings
);
useConnectStorageToQueryState(services.data.query, osdUrlStateStorage, {
filters: opensearchFilters.FilterStateStore.APP_STATE,
query: true,
});

useEffect(() => {
const subscription = data$.subscribe((queryData) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { setEditorState } from '../utils/state_management/metadata_slice';
import { useCanSave } from '../utils/use/use_can_save';
import { saveStateToSavedObject } from '../../saved_visualizations/transforms';
import { TopNavMenuData, TopNavMenuItemRenderType } from '../../../../navigation/public';
import { opensearchFilters, connectStorageToQueryState } from '../../../../data/public';
import { opensearchFilters, useConnectStorageToQueryState } from '../../../../data/public';
import { RootState } from '../../../../data_explorer/public';

function useDeepEffect(callback, dependencies) {
Expand Down Expand Up @@ -56,16 +56,17 @@ export const TopNav = () => {

const saveDisabledReason = useCanSave();
const savedVisBuilderVis = useSavedVisBuilderVis(visualizationIdFromUrl);
connectStorageToQueryState(services.data.query, services.osdUrlStateStorage, {
filters: opensearchFilters.FilterStateStore.APP_STATE,
query: true,
});
const { selected: indexPattern } = useIndexPatterns();
const [config, setConfig] = useState<TopNavMenuData[] | undefined>();
const originatingApp = useTypedSelector((state) => {
return state.metadata.originatingApp;
});

useConnectStorageToQueryState(services.data.query, services.osdUrlStateStorage, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

without diving too deep, do we know if the query state is changed and navigate to visbuilder if this gets set to after the the index patterns does it cause a difference in the selected: indexPattern?

filters: opensearchFilters.FilterStateStore.APP_STATE,
query: true,
});

useEffect(() => {
const getConfig = () => {
if (!savedVisBuilderVis || !indexPattern) return;
Expand Down