Skip to content

Commit

Permalink
feat(StoreDevtools): implement actionsBlacklist/Whitelist & predicate
Browse files Browse the repository at this point in the history
  • Loading branch information
Wykks committed Apr 14, 2018
1 parent 15b472d commit 89b7262
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 12 deletions.
30 changes: 24 additions & 6 deletions docs/store-devtools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Devtools for [@ngrx/store](../store/README.md).

### Installation

Install @ngrx/store-devtools from npm:

`npm install @ngrx/store-devtools --save` OR `yarn add @ngrx/store-devtools`
Expand All @@ -12,11 +13,12 @@ Install @ngrx/store-devtools from npm:
`npm install github:ngrx/store-devtools-builds` OR `yarn add github:ngrx/store-devtools-builds`

## Instrumentation

### Instrumentation with the Chrome / Firefox Extension

1. Download the [Redux Devtools Extension](http://zalmoxisus.github.io/redux-devtools-extension/)
1. Download the [Redux Devtools Extension](http://zalmoxisus.github.io/redux-devtools-extension/)

2. In your `AppModule` add instrumentation to the module imports using `StoreDevtoolsModule.instrument`:
2. In your `AppModule` add instrumentation to the module imports using `StoreDevtoolsModule.instrument`:

```ts
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
Expand All @@ -28,33 +30,49 @@ import { environment } from '../environments/environment'; // Angular CLI enviro
// Instrumentation must be imported after importing StoreModule (config is optional)
StoreDevtoolsModule.instrument({
maxAge: 25, // Retains last 25 states
logOnly: environment.production // Restrict extension to log-only mode
})
]
logOnly: environment.production, // Restrict extension to log-only mode
}),
],
})
export class AppModule { }
export class AppModule {}
```

### Instrumentation options

When you call the instrumentation, you can give an optional configuration object:

#### `maxAge`

number (>1) | false - maximum allowed actions to be stored in the history tree. The oldest actions are removed once maxAge is reached. It's critical for performance. Default is `false` (infinite).

#### `logOnly`

boolean - connect to the Devtools Extension in log-only mode. Default is `false` which enables all extension [features](/~https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md#features).

#### `name`

string - the instance name to be showed on the monitor page. Default value is _NgRx Store DevTools_.

#### `monitor`:

function - the monitor function configuration that you want to hook.

#### `actionSanitizer`

function which takes `action` object and id number as arguments, and should return `action` object back.

#### `stateSanitizer`

function which takes `state` object and index as arguments, and should return `state` object back.

#### `serialize`

false | configuration object - Handle the way you want to serialize your state, [more information here](/~https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md#serialize).

#### `actionsBlacklist / actionsWhitelist`

array of strings as regex - actions types to be hidden / shown in the monitors (while passed to the reducers), [more information here](/~https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md#actionsblacklist--actionswhitelist).

#### `predicate`

function - called for every action before sending, takes state and action object, and returns true in case it allows sending the current data to the monitor, [more information here](/~https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md#predicate).
118 changes: 118 additions & 0 deletions modules/store-devtools/spec/extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,5 +339,123 @@ describe('DevtoolsExtension', () => {
});
});
});

describe('with Action and actionsBlacklist', () => {
const NORMAL_ACTION = 'NORMAL_ACTION';
const BLACKLISTED_ACTION = 'BLACKLISTED_ACTION';

beforeEach(() => {
devtoolsExtension = new DevtoolsExtension(
reduxDevtoolsExtension,
createConfig({
actionsBlacklist: [BLACKLISTED_ACTION],
})
);
// Subscription needed or else extension connection will not be established.
devtoolsExtension.actions$.subscribe(() => null);
});

it('should ignore blacklisted action', () => {
const options = createOptions();
const state = createState();

devtoolsExtension.notify(
new PerformAction({ type: NORMAL_ACTION }, 1234567),
state
);
devtoolsExtension.notify(
new PerformAction({ type: NORMAL_ACTION }, 1234567),
state
);
devtoolsExtension.notify(
new PerformAction({ type: BLACKLISTED_ACTION }, 1234567),
state
);
expect(extensionConnection.send).toHaveBeenCalledTimes(2);
});
});

describe('with Action and actionsWhitelist', () => {
const NORMAL_ACTION = 'NORMAL_ACTION';
const WHITELISTED_ACTION = 'WHITELISTED_ACTION';

beforeEach(() => {
devtoolsExtension = new DevtoolsExtension(
reduxDevtoolsExtension,
createConfig({
actionsWhitelist: [WHITELISTED_ACTION],
})
);
// Subscription needed or else extension connection will not be established.
devtoolsExtension.actions$.subscribe(() => null);
});

it('should only keep whitelisted action', () => {
const options = createOptions();
const state = createState();

devtoolsExtension.notify(
new PerformAction({ type: NORMAL_ACTION }, 1234567),
state
);
devtoolsExtension.notify(
new PerformAction({ type: NORMAL_ACTION }, 1234567),
state
);
devtoolsExtension.notify(
new PerformAction({ type: WHITELISTED_ACTION }, 1234567),
state
);
expect(extensionConnection.send).toHaveBeenCalledTimes(1);
});
});

describe('with Action and predicate', () => {
const NORMAL_ACTION = 'NORMAL_ACTION';
const RANDOM_ACTION = 'RANDOM_ACTION';

const predicate = jasmine
.createSpy('predicate', (state: any, action: Action) => {
if (action.type === RANDOM_ACTION) {
return false;
}
return true;
})
.and.callThrough();

beforeEach(() => {
devtoolsExtension = new DevtoolsExtension(
reduxDevtoolsExtension,
createConfig({
predicate,
})
);
// Subscription needed or else extension connection will not be established.
devtoolsExtension.actions$.subscribe(() => null);
});

it('should ignore action according to predicate', () => {
const options = createOptions();
const state = createState();

devtoolsExtension.notify(
new PerformAction({ type: NORMAL_ACTION }, 1234567),
state
);
expect(predicate).toHaveBeenCalledWith(unliftState(state), {
type: NORMAL_ACTION,
});
devtoolsExtension.notify(
new PerformAction({ type: NORMAL_ACTION }, 1234567),
state
);
devtoolsExtension.notify(
new PerformAction({ type: RANDOM_ACTION }, 1234567),
state
);
expect(predicate).toHaveBeenCalledTimes(3);
expect(extensionConnection.send).toHaveBeenCalledTimes(2);
});
});
});
});
4 changes: 4 additions & 0 deletions modules/store-devtools/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { InjectionToken, Type } from '@angular/core';

export type ActionSanitizer = (action: Action, id: number) => Action;
export type StateSanitizer = (state: any, index: number) => any;
export type Predicate = (state: any, action: Action) => boolean;

export class StoreDevtoolsConfig {
maxAge: number | false;
Expand All @@ -13,6 +14,9 @@ export class StoreDevtoolsConfig {
serialize?: boolean;
logOnly?: boolean;
features?: any;
actionsBlacklist?: string[];
actionsWhitelist?: string[];
predicate?: Predicate;
}

export const STORE_DEVTOOLS_CONFIG = new InjectionToken<StoreDevtoolsConfig>(
Expand Down
38 changes: 33 additions & 5 deletions modules/store-devtools/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
sanitizeState,
sanitizeStates,
unliftState,
isActionFiltered,
filterLiftedState,
} from './utils';

export const ExtensionActionTypes = {
Expand Down Expand Up @@ -73,7 +75,10 @@ export class DevtoolsExtension {
if (!this.devtoolsExtension) {
return;
}

const shouldFilter =
this.config.predicate ||
this.config.actionsWhitelist ||
this.config.actionsBlacklist;
// Check to see if the action requires a full update of the liftedState.
// If it is a simple action generated by the user's app, only send the
// action and the current state (fast).
Expand All @@ -89,6 +94,19 @@ export class DevtoolsExtension {
// caution.
if (action.type === PERFORM_ACTION) {
const currentState = unliftState(state);
if (shouldFilter) {
if (
isActionFiltered(
currentState,
action,
this.config.predicate,
this.config.actionsWhitelist,
this.config.actionsBlacklist
)
) {
return;
}
}
const sanitizedState = this.config.stateSanitizer
? sanitizeState(
this.config.stateSanitizer,
Expand All @@ -105,15 +123,25 @@ export class DevtoolsExtension {
: action;
this.extensionConnection.send(sanitizedAction, sanitizedState);
} else {
let newState = state;
if (shouldFilter) {
newState = filterLiftedState(
state,
this.config.predicate,
this.config.actionsWhitelist,
this.config.actionsBlacklist
);
}
// Requires full state update
const sanitizedLiftedState = {
...state,
stagedActionIds: newState.stagedActionIds,
actionsById: this.config.actionSanitizer
? sanitizeActions(this.config.actionSanitizer, state.actionsById)
: state.actionsById,
? sanitizeActions(this.config.actionSanitizer, newState.actionsById)
: newState.actionsById,
computedStates: this.config.stateSanitizer
? sanitizeStates(this.config.stateSanitizer, state.computedStates)
: state.computedStates,
? sanitizeStates(this.config.stateSanitizer, newState.computedStates)
: newState.computedStates,
};
this.devtoolsExtension.send(
null,
Expand Down
59 changes: 58 additions & 1 deletion modules/store-devtools/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Action } from '@ngrx/store';
import { Observable } from 'rxjs';

import * as Actions from './actions';
import { ActionSanitizer, StateSanitizer } from './config';
import { ActionSanitizer, StateSanitizer, Predicate } from './config';
import {
ComputedState,
LiftedAction,
Expand Down Expand Up @@ -93,3 +93,60 @@ export function sanitizeState(
) {
return stateSanitizer(state, stateIdx);
}

/**
* Return a full filtered lifted state
*/
export function filterLiftedState(
liftedState: LiftedState,
predicate?: Predicate,
whitelist?: string[],
blacklist?: string[]
): LiftedState {
const filteredStagedActionIds: number[] = [];
const filteredActionsById: LiftedActions = {};
const filteredComputedStates: ComputedState[] = [];
liftedState.stagedActionIds.forEach((id, idx) => {
const liftedAction = liftedState.actionsById[id];
if (!liftedAction) return;
if (idx) {
if (
isActionFiltered(
liftedState.computedStates[idx],
liftedAction,
predicate,
whitelist,
blacklist
)
) {
return;
}
}
filteredActionsById[id] = liftedAction;
filteredStagedActionIds.push(id);
filteredComputedStates.push(liftedState.computedStates[idx]);
});
return {
...liftedState,
stagedActionIds: filteredStagedActionIds,
actionsById: filteredActionsById,
computedStates: filteredComputedStates,
};
}

/**
* Return true is the action should be ignored
*/
export function isActionFiltered(
state: any,
action: LiftedAction,
predicate?: Predicate,
whitelist?: string[],
blacklist?: string[]
) {
return (
(predicate && !predicate(state, action.action)) ||
(whitelist && !action.action.type.match(whitelist.join('|'))) ||
(blacklist && action.action.type.match(blacklist.join('|')))
);
}

0 comments on commit 89b7262

Please sign in to comment.