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

Redux types #63413

Merged
merged 6 commits into from
Apr 15, 2020
Merged

Redux types #63413

merged 6 commits into from
Apr 15, 2020

Conversation

oatkiller
Copy link
Contributor

@oatkiller oatkiller commented Apr 13, 2020

Summary

Changes some types around so modifying redux state or actions will cause type errors

  • the state and action types aren't using Immutable directly. This means that you can instantiate them and mutate them
  • the state and action params received by all reducers will be automatically cast to Immutable
  • reducers can return either Immutable or regular versions of state
  • some code may be mutating redux state directly, this is being ignored (at least for now)

TODO

  • state and action received in middleware should be Immutable (next PR, since this got big)

Checklist

Delete any items that are not applicable to this PR.

For maintainers

@@ -13,6 +13,8 @@ import { alertingIndexGetQuerySchema } from './schema/alert_index';
*/
export type Immutable<T> = T extends undefined | null | boolean | string | number
? T
: unknown extends T
? unknown
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is safe because anything union'ed w/ unknown is unknown
image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need to do : unknown extends T ? unknown

Copy link
Contributor Author

@oatkiller oatkiller Apr 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Immutable is meant to be able to accept anything. If you have a type such as:

type nerds = {
  a: string,
  b: unknown
}

Then when we recurse to b, the value will be Immutable<unknown>. Without special handling this will fallback to ImmutableObject<unknown>. ImmutableObject<unknown> doesn't work (maybe ImmutableObject should have stricter type parameter constraints.)

"Anything is assignable to unknown, but unknown isn’t assignable to anything but itself and any without a type assertion or a control flow based narrowing."

Therefore T extends unknown wouldn't be a good way to handle unknown as it will always be true. e.g.:

type itsTrue = 1 extends unknown ? true : false

If you have any union with unknown, it is the same as unknown. This is because that union can accept anything (everything is assignable to unknown.) So if we want to have Immutable<unknown> alias unknown, we can check if unknown can be assigned to T.

This use case arose because this PR uses Immutable on a data structure from the data plugin which has unknown as the value of a key of an object.

@@ -142,7 +141,7 @@ export const searchBarIndexPatterns = (state: AlertListState) => state.searchBar
* query params to use when requesting alert data.
*/
export const apiQueryParams: (
state: AlertListState
state: Immutable<AlertListState>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its not generally necessary to add Immutable everywhere, but it can be useful when debugging. i may or may not remove these. they dont hurt anything

@@ -21,7 +20,7 @@ const initialState = (): HostListState => {
};
};

export const hostListReducer: Reducer<HostListState, AppAction> = (
export const hostListReducer: ImmutableReducer<HostListState, AppAction> = (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another case where its probably not necessary, but it is helpful for debugging and stuff

return newState;
return {
...newState,
isLoading: true,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newState isn't easy to mutate because its kinda a mix between immutable and not. Its created by spreading an immutable value, so all of its nonprimivite keys are immutable. We could come up w/ a type for this if we need it

const newPolicy: PolicyConfig = { ...fullPolicy(state) };

/**
* This is directly changing redux state because `policyItem.inputs` was copied over and not cloned.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not 100% sure that this is mutating redux state, but im pretty sure its not safe

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, you are right here. Missed that. I'm thinking that we should just start to use lodash cloneDeep especially when working with these deep object structures.

/**
* this is not safe because `action.payload.policyConfig` may have excess keys
*/
// @ts-ignore
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: store the action.payload on state plain and simple

return {
  userModifiedPolicyConfig: action.payload
}

Then in the middleware when you need to make a request, create a new object that is a combination of the policy (as it was received from the server) and the user modified parts.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My initial goal was actually the opposite - ensure the state.policyItem is alway a full representation of the datasource/policy and create selectors that return back a sub-section of that for use in the UI.
I'm good with changing the approach if that makes things easier to work with from a TS standpoint.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gotcha. My suggestion wasn't really to get around any TS issues. I try to model redux state as a normalized representation of the application state. In this case, the form dispatches a series of actions, each including the entire state of the form. That's why I recommended making the state be simply the payload of the action. This also keeps writing to the redux store cheap, which is not a primary goal, but doesn't hurt.

I would say that merging the form state with other metadata to make a request to another plugin would be the concern of the data layer, that's why I recommend doing it in the middleware.

Anyway, just my 2c

/**
* this is not safe because `action.payload.policyConfig` may have excess keys
*/
// @ts-ignore
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My initial goal was actually the opposite - ensure the state.policyItem is alway a full representation of the datasource/policy and create selectors that return back a sub-section of that for use in the UI.
I'm good with changing the approach if that makes things easier to work with from a TS standpoint.

const newPolicy: PolicyConfig = { ...fullPolicy(state) };

/**
* This is directly changing redux state because `policyItem.inputs` was copied over and not cloned.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, you are right here. Missed that. I'm thinking that we should just start to use lodash cloneDeep especially when working with these deep object structures.

@oatkiller oatkiller force-pushed the redux-types branch 2 times, most recently from fe18fae to 43cf87a Compare April 14, 2020 18:17
@@ -6,11 +6,8 @@

import uuid from 'uuid';
import seedrandom from 'seedrandom';
import { AlertEvent, EndpointEvent, HostMetadata, OSFields, HostFields } from './types';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enabling lint

*/
export const generatePolicy = (): PolicyConfig => {
export const factory = (): PolicyConfig => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing generate from name, as this is a pure function and 'generate' is being used by the 'data generator' scripts and related code

/**
* Endpoint Policy configuration
*/
export interface PolicyConfig {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used by code in common, so moved to common

* Model for the `IIndexPattern` interface exported by the `data` plugin.
*/
export function clone(value: IIndexPattern | Immutable<IIndexPattern>): IIndexPattern {
return all([value]) as IIndexPattern;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@paul-tavares i'm on the same page as you re: using libraries to clone and/or support other immutable patterns. I'm using all from deepmerge here. The one in lodash had too much code for me to read and understand

policyDetails,
policyData => {
return policyData?.inputs[0]?.config?.policy?.value ?? generatePolicy();
return policyData?.inputs[0]?.config?.policy?.value ?? defaultFullPolicy;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any time we return a default policy, return the same (immutable) object reference

/**
* A description for the component.
*/
description: string;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

description was being passed an empty string.

/**
* Types of supported operating systems.
*/
supportedOss: React.ReactNode;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was an array of translated strings. it was being concat'd with , but that should be done via i18n. taking a react node instead.

/**
* The `data-test-subj` attribute to append to a certain child element.
*/
dataTestSubj: string;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renaming from id because its just used for data-test-subj and id already has other meanings

@@ -123,7 +123,7 @@ export const MalwareProtections = React.memo(() => {
[dispatch, policyDetailsConfig]
);

const RadioButtons = () => {
const radioButtons = useMemo(() => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

capital functions in react are conventionally reserved for components

@@ -44,8 +44,8 @@ const PolicyLink: React.FC<{ name: string; route: string }> = ({ name, route })
);
};

const renderPolicyNameLink = (value: string, _item: PolicyData) => {
return <PolicyLink name={value} route={`/policy/${_item.id}`} />;
const renderPolicyNameLink = (value: string, item: Immutable<PolicyData>) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_ prefix in typescript bindings is used to tell tsserver to exclude it from unused-bindings checks. this is used

@@ -160,7 +160,7 @@ export const PolicyList = React.memo(() => {
}
>
<EuiBasicTable
items={policyItems}
items={useMemo(() => [...policyItems], [policyItems])}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EuiBasicTable doesn't accept readonly arrays. We could probably get that changed in EUI, as it probably doesn't actually mutate items

@oatkiller oatkiller marked this pull request as ready for review April 14, 2020 18:32
@oatkiller oatkiller requested a review from a team as a code owner April 14, 2020 18:32
@oatkiller oatkiller added release_note:skip Skip the PR/issue when compiling release notes v7.8.0 v8.0.0 labels Apr 14, 2020
@kibanamachine
Copy link
Contributor

💚 Build Succeeded

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

export interface PolicyConfig {
windows: {
events: {
dll_and_driver_load: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

were we thinking about making these events keys part of another enum?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you mean. This was moved to common because it was used by (or was related to a type used by) the generator code (which is in common.)

export type HostAction =
| ServerReturnedHostList
| ServerReturnedHostDetails
| ServerFailedToReturnHostDetails
| UserPaginatedHostList
| FakeActionWithNoPayload;
| UserPaginatedHostList;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what helped us get around this issue?

Copy link
Contributor Author

@oatkiller oatkiller Apr 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure

description={i18n.translate('xpack.endpoint.policy.details.eventCollectionLabel', {
defaultMessage: 'Event Collection',
})}
supportedOss={i18n.translate('xpack.endpoint.policy.details.linux', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did you get rid of useMemo here again?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

supportedOss was taking an array of translated strings and then concat'ing them with ', '. Since we do i18n, the translator needs to be able to specify that text. supportedOss now takes a React node. Since i18n.translate is returning a string, there is no need to memoize.

@oatkiller oatkiller merged commit 08aa214 into elastic:master Apr 15, 2020
oatkiller pushed a commit to oatkiller/kibana that referenced this pull request Apr 15, 2020
## Summary

Changes some types around so modifying redux state or actions will cause type errors

* the state and action types aren't using `Immutable` directly. This means that you can instantiate them and mutate them
* the `state` and `action` params received by all reducers will be automatically cast to `Immutable`
* reducers can return either `Immutable` or regular versions of `state`
* some code may be mutating redux state directly, this is being ignored (at least for now)
wayneseymour pushed a commit that referenced this pull request Apr 15, 2020
## Summary

Changes some types around so modifying redux state or actions will cause type errors

* the state and action types aren't using `Immutable` directly. This means that you can instantiate them and mutate them
* the `state` and `action` params received by all reducers will be automatically cast to `Immutable`
* reducers can return either `Immutable` or regular versions of `state`
* some code may be mutating redux state directly, this is being ignored (at least for now)
oatkiller pushed a commit that referenced this pull request Apr 15, 2020
## Summary

Changes some types around so modifying redux state or actions will cause type errors

* the state and action types aren't using `Immutable` directly. This means that you can instantiate them and mutate them
* the `state` and `action` params received by all reducers will be automatically cast to `Immutable`
* reducers can return either `Immutable` or regular versions of `state`
* some code may be mutating redux state directly, this is being ignored (at least for now)
@oatkiller oatkiller deleted the redux-types branch March 31, 2022 11:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release_note:skip Skip the PR/issue when compiling release notes v7.8.0 v8.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants