Skip to content

Commit

Permalink
EAH - Typescript state_monitor_factory (elastic#23945)
Browse files Browse the repository at this point in the history
* Typescript state_monitor_factory

* Fix linter error with possibly undefined

* Expand typings to include hash stuff and expand the State type definition more.

* Mark readonly
  • Loading branch information
stacey-gammon committed Oct 19, 2018
1 parent 05c09a3 commit 2d8d56c
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 41 deletions.
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

0 comments on commit 2d8d56c

Please sign in to comment.