From 5af38f482e142a50a3f41e2725591fe5510a8222 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 21 Dec 2022 15:04:34 +0100 Subject: [PATCH] use url state package in ml plugin --- x-pack/packages/ml/url_state/index.ts | 2 +- .../ml/url_state/src}/url_state.test.tsx | 0 .../packages/ml/url_state/src/url_state.tsx | 148 ++++++-- .../change_point_detection_context.tsx | 4 +- .../explain_log_rate_spikes_page.tsx | 4 +- .../anomalies_table/anomalies_table.js | 2 +- .../anomaly_results_view_selector.tsx | 2 +- .../select_interval/select_interval.test.tsx | 2 +- .../select_interval/select_interval.tsx | 2 +- .../select_severity/select_severity.test.tsx | 2 +- .../select_severity/select_severity.tsx | 15 +- .../components/job_selector/job_selector.tsx | 2 +- .../job_selector/use_job_selection.ts | 3 +- .../components/ml_page/side_nav.tsx | 2 +- .../date_picker_wrapper.test.tsx | 5 +- .../date_picker_wrapper.tsx | 2 +- .../contexts/kibana/use_create_url.ts | 2 +- .../hooks/use_exploration_url_state.ts | 2 +- .../pages/analytics_exploration/page.tsx | 2 +- .../components/action_map/use_map_action.tsx | 2 +- .../analytics_list/use_refresh_interval.ts | 3 +- .../pages/analytics_management/page.tsx | 3 +- .../pages/job_map/page.tsx | 2 +- .../explorer/anomaly_charts_state_service.ts | 2 +- .../explorer/hooks/use_explorer_url_state.ts | 2 +- .../application/jobs/jobs_list/jobs.tsx | 2 +- .../components/notifications_list.tsx | 2 +- .../ml/public/application/routing/router.tsx | 2 +- .../analytics_job_exploration.tsx | 2 +- .../application/routing/routes/explorer.tsx | 2 +- .../routes/timeseriesexplorer.test.tsx | 2 +- .../routing/routes/timeseriesexplorer.tsx | 2 +- .../hooks/use_timeseriesexplorer_url_state.ts | 2 +- .../models_management/models_list.tsx | 2 +- .../nodes_overview/nodes_list.tsx | 2 +- .../application/util/__mocks__/url_state.tsx | 3 +- .../ml/public/application/util/url_state.tsx | 317 ------------------ 37 files changed, 166 insertions(+), 391 deletions(-) rename x-pack/{plugins/ml/public/application/util => packages/ml/url_state/src}/url_state.test.tsx (100%) delete mode 100644 x-pack/plugins/ml/public/application/util/url_state.tsx diff --git a/x-pack/packages/ml/url_state/index.ts b/x-pack/packages/ml/url_state/index.ts index 645bd12ad5243..c2b3f80c62a15 100644 --- a/x-pack/packages/ml/url_state/index.ts +++ b/x-pack/packages/ml/url_state/index.ts @@ -10,7 +10,7 @@ export { parseUrlState, usePageUrlState, useUrlState, - APP_STATE_KEY, + PageUrlStateService, Provider, UrlStateProvider, type Accessor, diff --git a/x-pack/plugins/ml/public/application/util/url_state.test.tsx b/x-pack/packages/ml/url_state/src/url_state.test.tsx similarity index 100% rename from x-pack/plugins/ml/public/application/util/url_state.test.tsx rename to x-pack/packages/ml/url_state/src/url_state.test.tsx diff --git a/x-pack/packages/ml/url_state/src/url_state.tsx b/x-pack/packages/ml/url_state/src/url_state.tsx index 70b343255a0ff..3e5f2f739016d 100644 --- a/x-pack/packages/ml/url_state/src/url_state.tsx +++ b/x-pack/packages/ml/url_state/src/url_state.tsx @@ -6,13 +6,25 @@ */ import { parse, stringify } from 'query-string'; -import React, { createContext, useCallback, useContext, useMemo, type FC } from 'react'; +import React, { + createContext, + useCallback, + useContext, + useMemo, + useRef, + useEffect, + type FC, +} from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { isEqual } from 'lodash'; import { getNestedProperty } from '@kbn/ml-nested-property'; import { decode, encode } from '@kbn/rison'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { distinctUntilChanged } from 'rxjs/operators'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; + export interface Dictionary { [id: string]: TValue; } @@ -71,18 +83,16 @@ export function parseUrlState(search: string): Dictionary { // This uses a context to be able to maintain only one instance // of the url state. It gets passed down with `UrlStateProvider` // and can be used via `useUrlState`. -export const dataVisualizerUrlStateStore = createContext({ +export const urlStateStore = createContext({ searchString: '', setUrlState: () => {}, }); -export const { Provider } = dataVisualizerUrlStateStore; +export const { Provider } = urlStateStore; export const UrlStateProvider: FC = ({ children }) => { - const { Provider: StateProvider } = dataVisualizerUrlStateStore; - const history = useHistory(); - const { search: urlSearchString } = useLocation(); + const { search: searchString } = useLocation(); const setUrlState: SetUrlState = useCallback( ( @@ -91,7 +101,7 @@ export const UrlStateProvider: FC = ({ children }) => { value?: any, replaceState?: boolean ) => { - const prevSearchString = urlSearchString; + const prevSearchString = searchString; const urlState = parseUrlState(prevSearchString); const parsedQueryString = parse(prevSearchString, { sort: false }); @@ -143,16 +153,20 @@ export const UrlStateProvider: FC = ({ children }) => { console.error('Could not save url state', error); } }, - [history, urlSearchString] + // eslint-disable-next-line react-hooks/exhaustive-deps + [searchString] ); - return ( - {children} - ); + return {children}; }; -export const useUrlState = (accessor: Accessor) => { - const { searchString, setUrlState: setUrlStateContext } = useContext(dataVisualizerUrlStateStore); +export const useUrlState = ( + accessor: Accessor +): [ + Record, + (attribute: string | Dictionary, value?: unknown, replaceState?: boolean) => void +] => { + const { searchString, setUrlState: setUrlStateContext } = useContext(urlStateStore); const urlState = useMemo(() => { const fullUrlState = parseUrlState(searchString); @@ -163,7 +177,7 @@ export const useUrlState = (accessor: Accessor) => { }, [searchString]); const setUrlState = useCallback( - (attribute: string | Dictionary, value?: any, replaceState?: boolean) => { + (attribute: string | Dictionary, value?: unknown, replaceState?: boolean) => { setUrlStateContext(accessor, attribute, value, replaceState); }, [accessor, setUrlStateContext] @@ -171,35 +185,103 @@ export const useUrlState = (accessor: Accessor) => { return [urlState, setUrlState]; }; -export const APP_STATE_KEY = { - AIOPS_INDEX_VIEWER: 'AIOPS_INDEX_VIEWER', - CHANGE_POINT_INDEX_VIEWER: 'CHANGE_POINT_INDEX_VIEWER', - DATA_VISUALIZER_INDEX_VIEWER: 'DATA_VISUALIZER_INDEX_VIEWER', -} as const; +/** + * Service for managing URL state of particular page. + */ +export class PageUrlStateService { + private _pageUrlState$ = new BehaviorSubject(null); + private _pageUrlStateCallback: ((update: Partial, replaceState?: boolean) => void) | null = + null; + + /** + * Provides updates for the page URL state. + */ + public getPageUrlState$(): Observable { + return this._pageUrlState$.pipe(distinctUntilChanged(isEqual)); + } -export type AppStateKey = keyof typeof APP_STATE_KEY; + public getPageUrlState(): T | null { + return this._pageUrlState$.getValue(); + } + + public updateUrlState(update: Partial, replaceState?: boolean): void { + if (!this._pageUrlStateCallback) { + throw new Error('Callback has not been initialized.'); + } + this._pageUrlStateCallback(update, replaceState); + } + + /** + * Populates internal subject with currently active state. + * @param currentState + */ + public setCurrentState(currentState: T): void { + this._pageUrlState$.next(currentState); + } + + /** + * Sets the callback for the state update. + * @param callback + */ + public setUpdateCallback(callback: (update: Partial, replaceState?: boolean) => void): void { + this._pageUrlStateCallback = callback; + } +} /** * Hook for managing the URL state of the page. */ -export const usePageUrlState = ( - pageKey: AppStateKey, +export const usePageUrlState = ( + pageKey: string, defaultState?: PageUrlState -): [PageUrlState, (update: Partial, replaceState?: boolean) => void] => { +): [ + PageUrlState, + (update: Partial, replaceState?: boolean) => void, + PageUrlStateService +] => { const [appState, setAppState] = useUrlState('_a'); const pageState = appState?.[pageKey]; + const setCallback = useRef(); + + useEffect(() => { + setCallback.current = setAppState; + }, [setAppState]); + + const prevPageState = useRef(); + const resultPageState: PageUrlState = useMemo(() => { - return { + const result = { ...(defaultState ?? {}), ...(pageState ?? {}), }; + + if (isEqual(result, prevPageState.current)) { + return prevPageState.current; + } + + // Compare prev and current states to only update changed values + if (isPopulatedObject(prevPageState.current)) { + for (const key in result) { + if (isEqual(result[key], prevPageState.current[key])) { + result[key] = prevPageState.current[key]; + } + } + } + + prevPageState.current = result; + + return result; // eslint-disable-next-line react-hooks/exhaustive-deps }, [pageState]); const onStateUpdate = useCallback( (update: Partial, replaceState?: boolean) => { - setAppState( + if (!setCallback?.current) { + throw new Error('Callback for URL state update has not been initialized.'); + } + + setCallback.current( pageKey, { ...resultPageState, @@ -208,10 +290,20 @@ export const usePageUrlState = ( replaceState ); }, - [pageKey, resultPageState, setAppState] + [pageKey, resultPageState] + ); + + const pageUrlStateService = useMemo(() => new PageUrlStateService(), []); + + useEffect( + function updatePageUrlService() { + pageUrlStateService.setCurrentState(resultPageState); + pageUrlStateService.setUpdateCallback(onStateUpdate); + }, + [pageUrlStateService, onStateUpdate, resultPageState] ); return useMemo(() => { - return [resultPageState, onStateUpdate]; - }, [resultPageState, onStateUpdate]); + return [resultPageState, onStateUpdate, pageUrlStateService]; + }, [resultPageState, onStateUpdate, pageUrlStateService]); }; diff --git a/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_context.tsx b/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_context.tsx index ca499ed267e5a..6d5b94a38e502 100644 --- a/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_context.tsx +++ b/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_context.tsx @@ -18,7 +18,7 @@ import { type DataViewField } from '@kbn/data-views-plugin/public'; import { startWith } from 'rxjs'; import useMount from 'react-use/lib/useMount'; import type { Query, Filter } from '@kbn/es-query'; -import { usePageUrlState, APP_STATE_KEY } from '@kbn/ml-url-state'; +import { usePageUrlState } from '@kbn/ml-url-state'; import { createMergedEsQuery, getEsQueryFromSavedSearch, @@ -157,7 +157,7 @@ export const ChangePointDetectionContextProvider: FC = ({ children }) => { }, [dataView]); const [requestParamsFromUrl, updateRequestParams] = - usePageUrlState(APP_STATE_KEY.CHANGE_POINT_INDEX_VIEWER); + usePageUrlState('change-point'); const resultQuery = useMemo(() => { return ( diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx index a2e90b9672f72..a5cfd981b803d 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx @@ -27,7 +27,7 @@ import { Filter, FilterStateStore, Query } from '@kbn/es-query'; import { FormattedMessage } from '@kbn/i18n-react'; import { SavedSearch } from '@kbn/discover-plugin/public'; -import { useUrlState, usePageUrlState, APP_STATE_KEY } from '@kbn/ml-url-state'; +import { useUrlState, usePageUrlState } from '@kbn/ml-url-state'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; import { SearchQueryLanguage, SavedSearchSavedObject } from '../../application/utils/search_utils'; import { useData } from '../../hooks/use_data'; @@ -80,7 +80,7 @@ export const ExplainLogRateSpikesPage: FC = ({ } = useSpikeAnalysisTableRowContext(); const [aiopsListState, setAiopsListState] = usePageUrlState( - APP_STATE_KEY.AIOPS_INDEX_VIEWER, + 'explain-log-rate-spikes', restorableDefaults ); const [globalState, setGlobalState] = useUrlState('_g'); diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.js index 78c2c705c29e9..38eccf76022ca 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.js @@ -17,6 +17,7 @@ import React, { Component } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { usePageUrlState } from '@kbn/ml-url-state'; import { getColumns } from './anomalies_table_columns'; @@ -26,7 +27,6 @@ import { mlTableService } from '../../services/table_service'; import { RuleEditorFlyout } from '../rule_editor'; import { ml } from '../../services/ml_api_service'; import { INFLUENCERS_LIMIT, ANOMALIES_TABLE_TABS, MAX_CHARS } from './anomalies_table_constants'; -import { usePageUrlState } from '../../util/url_state'; export class AnomaliesTableInternal extends Component { constructor(props) { diff --git a/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.tsx b/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.tsx index d0111c5aed438..e43f1a0c708e7 100644 --- a/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.tsx +++ b/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.tsx @@ -10,9 +10,9 @@ import React, { FC, useMemo } from 'react'; import { EuiButtonGroup, EuiButtonGroupProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useUrlState } from '@kbn/ml-url-state'; import type { ExplorerJob } from '../../explorer/explorer_utils'; -import { useUrlState } from '../../util/url_state'; import { useMlLocator, useNavigateToPath } from '../../contexts/kibana'; import { ML_PAGES } from '../../../../common/constants/locator'; diff --git a/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx b/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx index cfa9a30f4602e..288437fd367c7 100644 --- a/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx +++ b/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx @@ -12,7 +12,7 @@ import { mount } from 'enzyme'; import { EuiSelect } from '@elastic/eui'; -import { UrlStateProvider } from '../../../util/url_state'; +import { UrlStateProvider } from '@kbn/ml-url-state'; import { SelectInterval } from './select_interval'; diff --git a/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.tsx b/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.tsx index 75a51d439a73c..1d34e4e7c516b 100644 --- a/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.tsx +++ b/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.tsx @@ -8,7 +8,7 @@ import React, { FC } from 'react'; import { EuiIcon, EuiSelect, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { usePageUrlState } from '../../../util/url_state'; +import { usePageUrlState } from '@kbn/ml-url-state'; export interface TableInterval { display: string; diff --git a/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx b/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx index 59afbe1cb9f66..a2777867e3ab2 100644 --- a/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx +++ b/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx @@ -12,7 +12,7 @@ import { mount } from 'enzyme'; import { EuiSuperSelect } from '@elastic/eui'; -import { UrlStateProvider } from '../../../util/url_state'; +import { UrlStateProvider } from '@kbn/ml-url-state'; import { SelectSeverity } from './select_severity'; diff --git a/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.tsx b/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.tsx index 444ff0f0ab9ec..fc89b95f2db2d 100644 --- a/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.tsx +++ b/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.tsx @@ -9,25 +9,26 @@ * React component for rendering a select element with threshold levels. */ import React, { Fragment, FC, useMemo } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; import { EuiHealth, EuiSpacer, EuiSuperSelect, EuiText, EuiSuperSelectProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { usePageUrlState } from '@kbn/ml-url-state'; + import { getSeverityColor } from '../../../../../common/util/anomaly_utils'; -import { usePageUrlState } from '../../../util/url_state'; import { ANOMALY_THRESHOLD } from '../../../../../common'; -const warningLabel = i18n.translate('xpack.ml.controls.selectSeverity.warningLabel', { +const warningLabel: string = i18n.translate('xpack.ml.controls.selectSeverity.warningLabel', { defaultMessage: 'warning', }); -const minorLabel = i18n.translate('xpack.ml.controls.selectSeverity.minorLabel', { +const minorLabel: string = i18n.translate('xpack.ml.controls.selectSeverity.minorLabel', { defaultMessage: 'minor', }); -const majorLabel = i18n.translate('xpack.ml.controls.selectSeverity.majorLabel', { +const majorLabel: string = i18n.translate('xpack.ml.controls.selectSeverity.majorLabel', { defaultMessage: 'major', }); -const criticalLabel = i18n.translate('xpack.ml.controls.selectSeverity.criticalLabel', { +const criticalLabel: string = i18n.translate('xpack.ml.controls.selectSeverity.criticalLabel', { defaultMessage: 'critical', }); diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx index 2ca5320572da2..9c49099c45946 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx @@ -16,10 +16,10 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useUrlState } from '@kbn/ml-url-state'; import './_index.scss'; import { Dictionary } from '../../../../common/types/common'; -import { useUrlState } from '../../util/url_state'; import { IdBadges } from './id_badges'; import { BADGE_LIMIT, diff --git a/x-pack/plugins/ml/public/application/components/job_selector/use_job_selection.ts b/x-pack/plugins/ml/public/application/components/job_selector/use_job_selection.ts index 2af031f117e81..d5a5928845c6a 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/use_job_selection.ts +++ b/x-pack/plugins/ml/public/application/components/job_selector/use_job_selection.ts @@ -9,11 +9,10 @@ import { difference } from 'lodash'; import { useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; +import { useUrlState } from '@kbn/ml-url-state'; import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; -import { useUrlState } from '../../util/url_state'; - import { useNotifications } from '../../contexts/kibana'; import { useJobSelectionFlyout } from '../../contexts/ml/use_job_selection_flyout'; diff --git a/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx b/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx index 2d755d3cb1d54..6a928b2a365a8 100644 --- a/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx @@ -9,9 +9,9 @@ import { i18n } from '@kbn/i18n'; import type { EuiSideNavItemType } from '@elastic/eui'; import React, { ReactNode, useCallback, useMemo } from 'react'; import { AIOPS_ENABLED, CHANGE_POINT_DETECTION_ENABLED } from '@kbn/aiops-plugin/common'; +import { useUrlState } from '@kbn/ml-url-state'; import { NotificationsIndicator } from './notifications_indicator'; import type { MlLocatorParams } from '../../../../common/types/locator'; -import { useUrlState } from '../../util/url_state'; import { useMlLocator, useNavigateToPath } from '../../contexts/kibana'; import { isFullLicense } from '../../license'; import type { MlRoute } from '../../routing'; diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.test.tsx b/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.test.tsx index d1e125c412cb5..30304d78c234e 100644 --- a/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.test.tsx +++ b/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.test.tsx @@ -11,7 +11,8 @@ import React from 'react'; import { EuiSuperDatePicker } from '@elastic/eui'; -import { useUrlState } from '../../../util/url_state'; +import { useUrlState } from '@kbn/ml-url-state'; + import { mlTimefilterRefresh$ } from '../../../services/timefilter_refresh_service'; import { useToastNotificationService } from '../../../services/toast_notification_service'; @@ -34,7 +35,7 @@ jest.mock('@elastic/eui', () => { }; }); -jest.mock('../../../util/url_state', () => { +jest.mock('@kbn/ml-url-state', () => { return { useUrlState: jest.fn(() => { return [{ refreshInterval: { value: 0, pause: true } }, jest.fn()]; diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx b/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx index 75c503a139499..e8e42822ca885 100644 --- a/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx @@ -24,8 +24,8 @@ import { TimeHistoryContract } from '@kbn/data-plugin/public'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; import { wrapWithTheme, toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { useUrlState } from '@kbn/ml-url-state'; import { mlTimefilterRefresh$ } from '../../../services/timefilter_refresh_service'; -import { useUrlState } from '../../../util/url_state'; import { useMlKibana } from '../../../contexts/kibana'; import { useRefreshIntervalUpdates, diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts b/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts index cbd35193c8d94..572a179d2a7ea 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts @@ -7,10 +7,10 @@ import { useCallback, useEffect, useState } from 'react'; import { LocatorGetUrlParams } from '@kbn/share-plugin/common/url_service'; +import { useUrlState } from '@kbn/ml-url-state'; import { useMlKibana } from './kibana_context'; import { ML_APP_LOCATOR } from '../../../../common/constants/locator'; import { MlLocatorParams } from '../../../../common/types/locator'; -import { useUrlState } from '../../util/url_state'; export const useMlLocator = () => { const { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_exploration_url_state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_exploration_url_state.ts index 1a26cce465d85..655016a59263f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_exploration_url_state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_exploration_url_state.ts @@ -6,7 +6,7 @@ */ import { isPopulatedObject } from '@kbn/ml-is-populated-object'; -import { usePageUrlState } from '../../../../util/url_state'; +import { usePageUrlState } from '@kbn/ml-url-state'; import { ML_PAGES } from '../../../../../../common/constants/locator'; import { ExplorationPageUrlState } from '../../../../../../common/types/locator'; import { SEARCH_QUERY_LANGUAGE } from '../../../../../../common/constants/search'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx index 0550226599eb1..0d15450ff0163 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx @@ -9,6 +9,7 @@ import React, { FC, useState, useEffect } from 'react'; import { EuiEmptyPrompt } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useUrlState } from '@kbn/ml-url-state'; import { OutlierExploration } from './components/outlier_exploration'; import { RegressionExploration } from './components/regression_exploration'; import { ClassificationExploration } from './components/classification_exploration'; @@ -24,7 +25,6 @@ import { AnalyticsIdSelectorControls, } from '../components/analytics_selector'; import { AnalyticsEmptyPrompt } from '../analytics_management/components/empty_prompt'; -import { useUrlState } from '../../../util/url_state'; import { SavedObjectsWarning } from '../../../components/saved_objects_warning'; export const Page: FC<{ diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx index 54161fa8c801a..4b4ac38a304ef 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_map/use_map_action.tsx @@ -7,11 +7,11 @@ import React, { useCallback, useMemo } from 'react'; import { cloneDeep } from 'lodash'; +import { useUrlState } from '@kbn/ml-url-state'; import { useMlLocator, useNavigateToPath } from '../../../../../contexts/kibana'; import { DataFrameAnalyticsListAction, DataFrameAnalyticsListRow } from '../analytics_list/common'; import { ML_PAGES } from '../../../../../../../common/constants/locator'; import { getViewLinkStatus } from '../action_view/get_view_link_status'; -import { useUrlState } from '../../../../../util/url_state'; import { mapActionButtonText, MapButton } from './map_button'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts index b95358eb4c477..3e9e404146fd8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_refresh_interval.ts @@ -7,8 +7,9 @@ import React, { useEffect } from 'react'; +import { useUrlState } from '@kbn/ml-url-state'; + import { useMlKibana } from '../../../../../contexts/kibana'; -import { useUrlState } from '../../../../../util/url_state'; import { DEFAULT_REFRESH_INTERVAL_MS, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx index 26401c21af524..e53812da40a60 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx @@ -9,14 +9,13 @@ import React, { FC, useMemo, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useUrlState } from '../../../util/url_state'; +import { useUrlState, usePageUrlState } from '@kbn/ml-url-state'; import { DataFrameAnalyticsList } from './components/analytics_list'; import { useRefreshInterval } from './components/analytics_list/use_refresh_interval'; import { NodeAvailableWarning } from '../../../components/node_available_warning'; import { SavedObjectsWarning } from '../../../components/saved_objects_warning'; import { UpgradeWarning } from '../../../components/upgrade'; import { JobMap } from '../job_map'; -import { usePageUrlState } from '../../../util/url_state'; import { ListingPageUrlState } from '../../../../../common/types/common'; import { DataFrameAnalyticsListColumn } from './components/analytics_list/common'; import { ML_PAGES } from '../../../../../common/constants/locator'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/page.tsx index 084897cf61659..99f873d2d1125 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/job_map/page.tsx @@ -9,7 +9,7 @@ import React, { FC, useState, useEffect, useCallback } from 'react'; import { EuiEmptyPrompt } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useUrlState } from '../../../util/url_state'; +import { useUrlState } from '@kbn/ml-url-state'; import { NodeAvailableWarning } from '../../../components/node_available_warning'; import { SavedObjectsWarning } from '../../../components/saved_objects_warning'; import { UpgradeWarning } from '../../../components/upgrade'; diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_charts_state_service.ts b/x-pack/plugins/ml/public/application/explorer/anomaly_charts_state_service.ts index 1ffa93344ab03..752fa204efa4b 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomaly_charts_state_service.ts +++ b/x-pack/plugins/ml/public/application/explorer/anomaly_charts_state_service.ts @@ -7,6 +7,7 @@ import { BehaviorSubject, combineLatest, Observable, of, Subscription } from 'rxjs'; import { distinctUntilChanged, map, skipWhile, switchMap } from 'rxjs/operators'; +import type { PageUrlStateService } from '@kbn/ml-url-state'; import { StateService } from '../services/state_service'; import type { AnomalyExplorerCommonStateService } from './anomaly_explorer_common_state'; import type { AnomalyTimelineStateService } from './anomaly_timeline_state_service'; @@ -16,7 +17,6 @@ import { } from './explorer_charts/explorer_charts_container_service'; import { AnomalyExplorerChartsService } from '../services/anomaly_explorer_charts_service'; import { getSelectionInfluencers, getSelectionJobIds } from './explorer_utils'; -import type { PageUrlStateService } from '../util/url_state'; import type { TableSeverity } from '../components/controls/select_severity/select_severity'; import { AnomalyExplorerUrlStateService } from './hooks/use_explorer_url_state'; diff --git a/x-pack/plugins/ml/public/application/explorer/hooks/use_explorer_url_state.ts b/x-pack/plugins/ml/public/application/explorer/hooks/use_explorer_url_state.ts index 5af9684c3a09b..35e7760cd099d 100644 --- a/x-pack/plugins/ml/public/application/explorer/hooks/use_explorer_url_state.ts +++ b/x-pack/plugins/ml/public/application/explorer/hooks/use_explorer_url_state.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { PageUrlStateService, usePageUrlState } from '../../util/url_state'; +import { PageUrlStateService, usePageUrlState } from '@kbn/ml-url-state'; import { ExplorerAppState } from '../../../../common/types/locator'; import { ML_PAGES } from '../../../../common/constants/locator'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx index f6909b3d98f52..cd9bf60edb8cb 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx @@ -8,8 +8,8 @@ import React, { FC } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; // @ts-ignore +import { usePageUrlState } from '@kbn/ml-url-state'; import { JobsListView } from './components/jobs_list_view'; -import { usePageUrlState } from '../../util/url_state'; import { ML_PAGES } from '../../../../common/constants/locator'; import { ListingPageUrlState } from '../../../../common/types/common'; import { HelpMenu } from '../../components/help_menu'; diff --git a/x-pack/plugins/ml/public/application/notifications/components/notifications_list.tsx b/x-pack/plugins/ml/public/application/notifications/components/notifications_list.tsx index ffe66b8e5b9b6..86039672cbfd0 100644 --- a/x-pack/plugins/ml/public/application/notifications/components/notifications_list.tsx +++ b/x-pack/plugins/ml/public/application/notifications/components/notifications_list.tsx @@ -23,6 +23,7 @@ import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/bas import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; import useDebounce from 'react-use/lib/useDebounce'; import useMount from 'react-use/lib/useMount'; +import { usePageUrlState } from '@kbn/ml-url-state'; import { EntityFilter } from './entity_filter'; import { useMlNotifications } from '../../contexts/ml/ml_notifications_context'; import { ML_NOTIFICATIONS_MESSAGE_LEVEL } from '../../../../common/constants/notifications'; @@ -33,7 +34,6 @@ import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter'; import { useRefresh } from '../../routing/use_refresh'; import { useTableSettings } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings'; import { ListingPageUrlState } from '../../../../common/types/common'; -import { usePageUrlState } from '../../util/url_state'; import { ML_PAGES } from '../../../../common/constants/locator'; import type { MlNotificationMessageLevel, diff --git a/x-pack/plugins/ml/public/application/routing/router.tsx b/x-pack/plugins/ml/public/application/routing/router.tsx index abaac9bd05f9f..7a2d631e7a8ad 100644 --- a/x-pack/plugins/ml/public/application/routing/router.tsx +++ b/x-pack/plugins/ml/public/application/routing/router.tsx @@ -18,9 +18,9 @@ import type { import type { DataViewsContract } from '@kbn/data-views-plugin/public'; import { EuiLoadingContent } from '@elastic/eui'; +import { UrlStateProvider } from '@kbn/ml-url-state'; import { MlNotificationsContextProvider } from '../contexts/ml/ml_notifications_context'; import { MlContext, MlContextValue } from '../contexts/ml'; -import { UrlStateProvider } from '../util/url_state'; import { MlPage } from '../components/ml_page'; diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx index a226ec72cbf0c..eb05087a28770 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx @@ -9,6 +9,7 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; +import { useUrlState } from '@kbn/ml-url-state'; import { NavigateToPath } from '../../../contexts/kibana'; import { MlRoute, PageLoader, PageProps } from '../../router'; @@ -17,7 +18,6 @@ import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_exploration'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_frame_analytics'; -import { useUrlState } from '../../../util/url_state'; export const analyticsJobExplorationRouteFactory = ( navigateToPath: NavigateToPath, diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 5f4c71a909720..55225e08db991 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -13,6 +13,7 @@ import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiThemeProvider as StyledComponentsThemeProvider } from '@kbn/kibana-react-plugin/common'; +import { useUrlState } from '@kbn/ml-url-state'; import { NavigateToPath, useMlKibana, useTimefilter } from '../../contexts/kibana'; import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; @@ -30,7 +31,6 @@ import { getDateFormatTz } from '../../explorer/explorer_utils'; import { useJobSelection } from '../../components/job_selector/use_job_selection'; import { useTableInterval } from '../../components/controls/select_interval'; import { useTableSeverity } from '../../components/controls/select_severity'; -import { useUrlState } from '../../util/url_state'; import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; import { JOB_ID } from '../../../../common/constants/anomalies'; import { MlAnnotationUpdatesContext } from '../../contexts/ml/ml_annotation_updates_context'; diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx index 893aa6afda81e..eb09b386b322b 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx @@ -43,7 +43,7 @@ const MockedTimeseriesexplorerNoJobsFound = TimeseriesexplorerNoJobsFound as jes typeof TimeseriesexplorerNoJobsFound >; -jest.mock('../../util/url_state'); +jest.mock('@kbn/ml-url-state'); jest.mock('../../timeseriesexplorer/hooks/use_timeseriesexplorer_url_state'); diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index b25808d7b637b..68bc9eef1f70c 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -12,6 +12,7 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; +import { useUrlState } from '@kbn/ml-url-state'; import { getViewableDetectors } from '../../timeseriesexplorer/timeseriesexplorer_utils/get_viewable_detectors'; import { NavigateToPath, useNotifications } from '../../contexts/kibana'; import { useMlContext } from '../../contexts/ml'; @@ -31,7 +32,6 @@ import { } from '../../timeseriesexplorer/timeseriesexplorer_utils'; import { TimeSeriesExplorerPage } from '../../timeseriesexplorer/timeseriesexplorer_page'; import { TimeseriesexplorerNoJobsFound } from '../../timeseriesexplorer/components/timeseriesexplorer_no_jobs_found'; -import { useUrlState } from '../../util/url_state'; import { useTableInterval } from '../../components/controls/select_interval'; import { useTableSeverity } from '../../components/controls/select_severity'; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/hooks/use_timeseriesexplorer_url_state.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/hooks/use_timeseriesexplorer_url_state.ts index 614888fc005ad..6c473c3d6740d 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/hooks/use_timeseriesexplorer_url_state.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/hooks/use_timeseriesexplorer_url_state.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { usePageUrlState } from '../../util/url_state'; +import { usePageUrlState } from '@kbn/ml-url-state'; import { TimeSeriesExplorerAppState } from '../../../../common/types/locator'; import { ML_PAGES } from '../../../../common/constants/locator'; diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx index f37118f877936..c7aafca60f69f 100644 --- a/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx @@ -25,6 +25,7 @@ import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/bas import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import { usePageUrlState } from '@kbn/ml-url-state'; import { useModelActions } from './model_actions'; import { ModelsTableToConfigMapping } from '.'; import { ModelsBarStats, StatsBar } from '../../components/stats_bar'; @@ -39,7 +40,6 @@ import { BUILT_IN_MODEL_TAG } from '../../../../common/constants/data_frame_anal import { DeleteModelsModal } from './delete_models_modal'; import { ML_PAGES } from '../../../../common/constants/locator'; import { ListingPageUrlState } from '../../../../common/types/common'; -import { usePageUrlState } from '../../util/url_state'; import { ExpandedRow } from './expanded_row'; import { useTableSettings } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings'; import { useToastNotificationService } from '../../services/toast_notification_service'; diff --git a/x-pack/plugins/ml/public/application/trained_models/nodes_overview/nodes_list.tsx b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/nodes_list.tsx index 78dd6c8a017b2..81371345720f8 100644 --- a/x-pack/plugins/ml/public/application/trained_models/nodes_overview/nodes_list.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/nodes_list.tsx @@ -17,9 +17,9 @@ import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/bas import { i18n } from '@kbn/i18n'; import { cloneDeep } from 'lodash'; import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; +import { usePageUrlState } from '@kbn/ml-url-state'; import { ModelsBarStats, StatsBar } from '../../components/stats_bar'; import { NodeDeploymentStatsResponse } from '../../../../common/types/trained_models'; -import { usePageUrlState } from '../../util/url_state'; import { ML_PAGES } from '../../../../common/constants/locator'; import { useTrainedModelsApiService } from '../../services/ml_api_service/trained_models'; import { useTableSettings } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings'; diff --git a/x-pack/plugins/ml/public/application/util/__mocks__/url_state.tsx b/x-pack/plugins/ml/public/application/util/__mocks__/url_state.tsx index 5d6eb5c7e55ed..011765b972fe2 100644 --- a/x-pack/plugins/ml/public/application/util/__mocks__/url_state.tsx +++ b/x-pack/plugins/ml/public/application/util/__mocks__/url_state.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { AppStateKey } from '../url_state'; import { TABLE_INTERVAL_DEFAULT } from '../../components/controls/select_interval/select_interval'; export const useUrlState = jest.fn((accessor: '_a' | '_g') => { @@ -14,7 +13,7 @@ export const useUrlState = jest.fn((accessor: '_a' | '_g') => { } }); -export const usePageUrlState = jest.fn((pageKey: AppStateKey) => { +export const usePageUrlState = jest.fn((pageKey: string) => { let state: unknown; switch (pageKey) { case 'timeseriesexplorer': diff --git a/x-pack/plugins/ml/public/application/util/url_state.tsx b/x-pack/plugins/ml/public/application/util/url_state.tsx deleted file mode 100644 index 957fc6c2d6ea9..0000000000000 --- a/x-pack/plugins/ml/public/application/util/url_state.tsx +++ /dev/null @@ -1,317 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { parse, stringify } from 'query-string'; -import React, { - createContext, - useCallback, - useContext, - useMemo, - FC, - useRef, - useEffect, -} from 'react'; -import { isEqual } from 'lodash'; - -import { getNestedProperty } from '@kbn/ml-nested-property'; -import { decode, encode } from '@kbn/rison'; -import { useHistory, useLocation } from 'react-router-dom'; - -import { BehaviorSubject, Observable } from 'rxjs'; -import { distinctUntilChanged } from 'rxjs/operators'; -import { isPopulatedObject } from '@kbn/ml-is-populated-object'; -import { Dictionary } from '../../../common/types/common'; - -import { MlPages } from '../../../common/constants/locator'; - -type Accessor = '_a' | '_g'; -export type SetUrlState = ( - accessor: Accessor, - attribute: string | Dictionary, - value?: any, - replaceState?: boolean -) => void; -export interface UrlState { - searchString: string; - setUrlState: SetUrlState; -} - -/** - * Set of URL query parameters that require the rison serialization. - */ -const risonSerializedParams = new Set(['_a', '_g']); - -/** - * Checks if the URL query parameter requires rison serialization. - * @param queryParam - */ -function isRisonSerializationRequired(queryParam: string): boolean { - return risonSerializedParams.has(queryParam); -} - -export function parseUrlState(search: string): Dictionary { - const urlState: Dictionary = {}; - const parsedQueryString = parse(search, { sort: false }); - - try { - Object.keys(parsedQueryString).forEach((a) => { - if (isRisonSerializationRequired(a)) { - urlState[a] = decode(parsedQueryString[a] as string); - } else { - urlState[a] = parsedQueryString[a]; - } - }); - } catch (error) { - // eslint-disable-next-line no-console - console.error('Could not read url state', error); - } - - return urlState; -} - -// Compared to the original appState/globalState, -// this no longer makes use of fetch/save methods. -// - Reading from `location.search` is the successor of `fetch`. -// - `history.push()` is the successor of `save`. -// - The exposed state and set call make use of the above and make sure that -// different urlStates(e.g. `_a` / `_g`) don't overwrite each other. -// This uses a context to be able to maintain only one instance -// of the url state. It gets passed down with `UrlStateProvider` -// and can be used via `useUrlState`. -export const urlStateStore = createContext({ - searchString: '', - setUrlState: () => {}, -}); - -const { Provider } = urlStateStore; - -export const UrlStateProvider: FC = ({ children }) => { - const history = useHistory(); - const { search: searchString } = useLocation(); - - const setUrlState: SetUrlState = useCallback( - ( - accessor: Accessor, - attribute: string | Dictionary, - value?: any, - replaceState?: boolean - ) => { - const prevSearchString = searchString; - const urlState = parseUrlState(prevSearchString); - const parsedQueryString = parse(prevSearchString, { sort: false }); - - if (!Object.prototype.hasOwnProperty.call(urlState, accessor)) { - urlState[accessor] = {}; - } - - if (typeof attribute === 'string') { - if (isEqual(getNestedProperty(urlState, `${accessor}.${attribute}`), value)) { - return prevSearchString; - } - - urlState[accessor][attribute] = value; - } else { - const attributes = attribute; - Object.keys(attributes).forEach((a) => { - urlState[accessor][a] = attributes[a]; - }); - } - - try { - const oldLocationSearchString = stringify(parsedQueryString, { - sort: false, - encode: false, - }); - - Object.keys(urlState).forEach((a) => { - if (isRisonSerializationRequired(a)) { - parsedQueryString[a] = encode(urlState[a]); - } else { - parsedQueryString[a] = urlState[a]; - } - }); - const newLocationSearchString = stringify(parsedQueryString, { - sort: false, - encode: false, - }); - - if (oldLocationSearchString !== newLocationSearchString) { - const newSearchString = stringify(parsedQueryString, { sort: false }); - if (replaceState) { - history.replace({ search: newSearchString }); - } else { - history.push({ search: newSearchString }); - } - } - } catch (error) { - // eslint-disable-next-line no-console - console.error('Could not save url state', error); - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [searchString] - ); - - return {children}; -}; - -export const useUrlState = ( - accessor: Accessor -): [ - Record, - (attribute: string | Dictionary, value?: unknown, replaceState?: boolean) => void -] => { - const { searchString, setUrlState: setUrlStateContext } = useContext(urlStateStore); - - const urlState = useMemo(() => { - const fullUrlState = parseUrlState(searchString); - if (typeof fullUrlState === 'object') { - return fullUrlState[accessor]; - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchString]); - - const setUrlState = useCallback( - (attribute: string | Dictionary, value?: unknown, replaceState?: boolean) => { - setUrlStateContext(accessor, attribute, value, replaceState); - }, - [accessor, setUrlStateContext] - ); - return [urlState, setUrlState]; -}; - -type LegacyUrlKeys = 'mlExplorerSwimlane'; - -export type AppStateKey = - | 'mlSelectSeverity' - | 'mlSelectInterval' - | 'mlAnomaliesTable' - | MlPages - | LegacyUrlKeys; - -/** - * Service for managing URL state of particular page. - */ -export class PageUrlStateService { - private _pageUrlState$ = new BehaviorSubject(null); - private _pageUrlStateCallback: ((update: Partial, replaceState?: boolean) => void) | null = - null; - - /** - * Provides updates for the page URL state. - */ - public getPageUrlState$(): Observable { - return this._pageUrlState$.pipe(distinctUntilChanged(isEqual)); - } - - public getPageUrlState(): T | null { - return this._pageUrlState$.getValue(); - } - - public updateUrlState(update: Partial, replaceState?: boolean): void { - if (!this._pageUrlStateCallback) { - throw new Error('Callback has not been initialized.'); - } - this._pageUrlStateCallback(update, replaceState); - } - - /** - * Populates internal subject with currently active state. - * @param currentState - */ - public setCurrentState(currentState: T): void { - this._pageUrlState$.next(currentState); - } - - /** - * Sets the callback for the state update. - * @param callback - */ - public setUpdateCallback(callback: (update: Partial, replaceState?: boolean) => void): void { - this._pageUrlStateCallback = callback; - } -} - -/** - * Hook for managing the URL state of the page. - */ -export const usePageUrlState = ( - pageKey: AppStateKey, - defaultState?: PageUrlState -): [ - PageUrlState, - (update: Partial, replaceState?: boolean) => void, - PageUrlStateService -] => { - const [appState, setAppState] = useUrlState('_a'); - const pageState = appState?.[pageKey]; - - const setCallback = useRef(); - - useEffect(() => { - setCallback.current = setAppState; - }, [setAppState]); - - const prevPageState = useRef(); - - const resultPageState: PageUrlState = useMemo(() => { - const result = { - ...(defaultState ?? {}), - ...(pageState ?? {}), - }; - - if (isEqual(result, prevPageState.current)) { - return prevPageState.current; - } - - // Compare prev and current states to only update changed values - if (isPopulatedObject(prevPageState.current)) { - for (const key in result) { - if (isEqual(result[key], prevPageState.current[key])) { - result[key] = prevPageState.current[key]; - } - } - } - - prevPageState.current = result; - - return result; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pageState]); - - const onStateUpdate = useCallback( - (update: Partial, replaceState?: boolean) => { - if (!setCallback?.current) { - throw new Error('Callback for URL state update has not been initialized.'); - } - - setCallback.current( - pageKey, - { - ...resultPageState, - ...update, - }, - replaceState - ); - }, - [pageKey, resultPageState] - ); - - const pageUrlStateService = useMemo(() => new PageUrlStateService(), []); - - useEffect( - function updatePageUrlService() { - pageUrlStateService.setCurrentState(resultPageState); - pageUrlStateService.setUpdateCallback(onStateUpdate); - }, - [pageUrlStateService, onStateUpdate, resultPageState] - ); - - return useMemo(() => { - return [resultPageState, onStateUpdate, pageUrlStateService]; - }, [resultPageState, onStateUpdate, pageUrlStateService]); -};