Skip to content

Commit

Permalink
use url state package in ml plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
walterra committed Dec 21, 2022
1 parent 2d71f46 commit 5af38f4
Show file tree
Hide file tree
Showing 37 changed files with 166 additions and 391 deletions.
2 changes: 1 addition & 1 deletion x-pack/packages/ml/url_state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export {
parseUrlState,
usePageUrlState,
useUrlState,
APP_STATE_KEY,
PageUrlStateService,
Provider,
UrlStateProvider,
type Accessor,
Expand Down
148 changes: 120 additions & 28 deletions x-pack/packages/ml/url_state/src/url_state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TValue> {
[id: string]: TValue;
}
Expand Down Expand Up @@ -71,18 +83,16 @@ export function parseUrlState(search: string): Dictionary<any> {
// 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<UrlState>({
export const urlStateStore = createContext<UrlState>({
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(
(
Expand All @@ -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 });

Expand Down Expand Up @@ -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 (
<StateProvider value={{ searchString: urlSearchString, setUrlState }}>{children}</StateProvider>
);
return <Provider value={{ searchString, setUrlState }}>{children}</Provider>;
};

export const useUrlState = (accessor: Accessor) => {
const { searchString, setUrlState: setUrlStateContext } = useContext(dataVisualizerUrlStateStore);
export const useUrlState = (
accessor: Accessor
): [
Record<string, any>,
(attribute: string | Dictionary<unknown>, value?: unknown, replaceState?: boolean) => void
] => {
const { searchString, setUrlState: setUrlStateContext } = useContext(urlStateStore);

const urlState = useMemo(() => {
const fullUrlState = parseUrlState(searchString);
Expand All @@ -163,43 +177,111 @@ export const useUrlState = (accessor: Accessor) => {
}, [searchString]);

const setUrlState = useCallback(
(attribute: string | Dictionary<any>, value?: any, replaceState?: boolean) => {
(attribute: string | Dictionary<unknown>, value?: unknown, replaceState?: boolean) => {
setUrlStateContext(accessor, attribute, value, replaceState);
},
[accessor, setUrlStateContext]
);
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<T> {
private _pageUrlState$ = new BehaviorSubject<T | null>(null);
private _pageUrlStateCallback: ((update: Partial<T>, replaceState?: boolean) => void) | null =
null;

/**
* Provides updates for the page URL state.
*/
public getPageUrlState$(): Observable<T> {
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<T>, 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<T>, replaceState?: boolean) => void): void {
this._pageUrlStateCallback = callback;
}
}

/**
* Hook for managing the URL state of the page.
*/
export const usePageUrlState = <PageUrlState extends {}>(
pageKey: AppStateKey,
export const usePageUrlState = <PageUrlState extends object>(
pageKey: string,
defaultState?: PageUrlState
): [PageUrlState, (update: Partial<PageUrlState>, replaceState?: boolean) => void] => {
): [
PageUrlState,
(update: Partial<PageUrlState>, replaceState?: boolean) => void,
PageUrlStateService<PageUrlState>
] => {
const [appState, setAppState] = useUrlState('_a');
const pageState = appState?.[pageKey];

const setCallback = useRef<typeof setAppState>();

useEffect(() => {
setCallback.current = setAppState;
}, [setAppState]);

const prevPageState = useRef<PageUrlState | undefined>();

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<PageUrlState>, replaceState?: boolean) => {
setAppState(
if (!setCallback?.current) {
throw new Error('Callback for URL state update has not been initialized.');
}

setCallback.current(
pageKey,
{
...resultPageState,
Expand All @@ -208,10 +290,20 @@ export const usePageUrlState = <PageUrlState extends {}>(
replaceState
);
},
[pageKey, resultPageState, setAppState]
[pageKey, resultPageState]
);

const pageUrlStateService = useMemo(() => new PageUrlStateService<PageUrlState>(), []);

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]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -157,7 +157,7 @@ export const ChangePointDetectionContextProvider: FC = ({ children }) => {
}, [dataView]);

const [requestParamsFromUrl, updateRequestParams] =
usePageUrlState<ChangePointDetectionRequestParams>(APP_STATE_KEY.CHANGE_POINT_INDEX_VIEWER);
usePageUrlState<ChangePointDetectionRequestParams>('change-point');

const resultQuery = useMemo<Query>(() => {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -80,7 +80,7 @@ export const ExplainLogRateSpikesPage: FC<ExplainLogRateSpikesPageProps> = ({
} = useSpikeAnalysisTableRowContext();

const [aiopsListState, setAiopsListState] = usePageUrlState(
APP_STATE_KEY.AIOPS_INDEX_VIEWER,
'explain-log-rate-spikes',
restorableDefaults
);
const [globalState, setGlobalState] = useUrlState('_g');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Loading

0 comments on commit 5af38f4

Please sign in to comment.