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

EAH - Typescript state_monitor_factory #23945

Merged
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
3 changes: 2 additions & 1 deletion src/ui/public/state_management/app_state.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
import { State } from './state';

export type AppState = any;
export type AppState = State;
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
* under the License.
*/

export function getUnhashableStatesProvider(getAppState, globalState) {
return function getUnhashableStates() {
return [getAppState(), globalState].filter(Boolean);
};
}
import { State } from './state';

export type GlobalState = State;
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@
* under the License.
*/

import { mapValues } from 'lodash';

export function unhashQueryString(parsedQueryString, states) {
return mapValues(parsedQueryString, (val, key) => {
const state = states.find(s => key === s.getQueryParamName());
return state ? state.translateHashToRison(val) : val;
});
export interface State {
[key: string]: any;
translateHashToRison: (
stateHashOrRison: string | string[] | undefined
) => string | string[] | undefined;
getQueryParamName: () => string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { AppState } from '../app_state';
import { GlobalState } from '../global_state';
import { State } from '../state';

export function getUnhashableStatesProvider(getAppState: () => AppState, globalState: GlobalState) {
return function getUnhashableStates(): State[] {
return [getAppState(), globalState].filter(Boolean);
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { mapValues } from 'lodash';
import { ParsedUrlQuery } from 'querystring';
import { State } from '../state';

/**
* Takes in a parsed url query and state objects, finding the state objects that match the query parameters and expanding
* the hashed state. For example, a url query string like '?_a=@12353&_g=@19028df' will become
* '?_a=[expanded app state here]&_g=[expanded global state here]. This is used when storeStateInSessionStorage is turned on.
*/
export function unhashQueryString(
parsedQueryString: ParsedUrlQuery,
states: State[]
): ParsedUrlQuery {
return mapValues(parsedQueryString, (val, key) => {
const state = states.find(s => key === s.getQueryParamName());
return state ? state.translateHashToRison(val) : val;
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,41 @@
* specific language governing permissions and limitations
* under the License.
*/

import { cloneDeep, isEqual, set, isPlainObject } from 'lodash';
import { cloneDeep, isEqual, isPlainObject, set } from 'lodash';
import { State } from './state';

export const stateMonitorFactory = {
create: (state, customInitialState) => stateMonitor(state, customInitialState)
create: (state: State, customInitialState: State) => stateMonitor(state, customInitialState),
};

function stateMonitor(state, customInitialState) {
interface StateStatus {
clean: boolean;
dirty: boolean;
}

type ChangeHandlerFn = (status: StateStatus, type: string | null, keys: string[]) => void;

function stateMonitor(state: State, customInitialState: State) {
let destroyed = false;
let ignoredProps = [];
let changeHandlers = [];
let initialState;
let ignoredProps: string[] = [];
let changeHandlers: ChangeHandlerFn[] | undefined = [];
let initialState: State;

setInitialState(customInitialState);

function setInitialState(customInitialState) {
function setInitialState(innerCustomInitialState: State) {
// state.toJSON returns a reference, clone so we can mutate it safely
initialState = cloneDeep(customInitialState) || cloneDeep(state.toJSON());
initialState = cloneDeep(innerCustomInitialState) || cloneDeep(state.toJSON());
}

function removeIgnoredProps(state) {
function removeIgnoredProps(innerState: State) {
ignoredProps.forEach(path => {
set(state, path, true);
set(innerState, path, true);
});
return state;
return innerState;
}

function getStatus() {
function getStatus(): StateStatus {
// state.toJSON returns a reference, clone so we can mutate it safely
const currentState = removeIgnoredProps(cloneDeep(state.toJSON()));
const isClean = isEqual(currentState, initialState);
Expand All @@ -54,49 +61,60 @@ function stateMonitor(state, customInitialState) {
};
}

function dispatchChange(type = null, keys = []) {
function dispatchChange(type: string | null = null, keys: string[] = []) {
const status = getStatus();
if (!changeHandlers) {
throw new Error('Change handlers is undefined, this object has been destroyed');
}
changeHandlers.forEach(changeHandler => {
changeHandler(status, type, keys);
});
}

function dispatchFetch(keys) {
function dispatchFetch(keys: string[]) {
dispatchChange('fetch_with_changes', keys);
}

function dispatchSave(keys) {
function dispatchSave(keys: string[]) {
dispatchChange('save_with_changes', keys);
}

function dispatchReset(keys) {
function dispatchReset(keys: string[]) {
dispatchChange('reset_with_changes', keys);
}

return {
setInitialState(customInitialState) {
if (!isPlainObject(customInitialState)) throw new TypeError('The default state must be an object');
setInitialState(innerCustomInitialState: State) {
if (!isPlainObject(innerCustomInitialState)) {
throw new TypeError('The default state must be an object');
}

// check the current status
const previousStatus = getStatus();

// update the initialState and apply ignoredProps
setInitialState(customInitialState);
setInitialState(innerCustomInitialState);
removeIgnoredProps(initialState);

// fire the change handler if the status has changed
if (!isEqual(previousStatus, getStatus())) dispatchChange();
if (!isEqual(previousStatus, getStatus())) {
dispatchChange();
}
},

ignoreProps(props) {
ignoreProps(props: string[]) {
ignoredProps = ignoredProps.concat(props);
removeIgnoredProps(initialState);
return this;
},

onChange(callback) {
if (destroyed) throw new Error('Monitor has been destroyed');
if (typeof callback !== 'function') throw new Error('onChange handler must be a function');
onChange(callback: ChangeHandlerFn) {
if (destroyed || !changeHandlers) {
throw new Error('Monitor has been destroyed');
}
if (typeof callback !== 'function') {
throw new Error('onChange handler must be a function');
}

changeHandlers.push(callback);

Expand All @@ -107,7 +125,9 @@ function stateMonitor(state, customInitialState) {

// if the state is already dirty, fire the change handler immediately
const status = getStatus();
if (status.dirty) dispatchChange();
if (status.dirty) {
dispatchChange();
}

return this;
},
Expand All @@ -118,6 +138,6 @@ function stateMonitor(state, customInitialState) {
state.off('fetch_with_changes', dispatchFetch);
state.off('save_with_changes', dispatchSave);
state.off('reset_with_changes', dispatchReset);
}
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export class EmbeddedVisualizeHandler {
}, 100);

private dataLoaderParams: RequestHandlerParams;
private appState: AppState;
private readonly appState?: AppState;
private uiState: PersistedState;
private dataLoader: VisualizeDataLoader;

Expand Down