-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Feature: Add NgRx Actions, Reducers, Selectors for the Feature Flag Modal #5868
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
Feature: Add NgRx Actions, Reducers, Selectors for the Feature Flag Modal #5868
Conversation
dfcce72 to
ab64fb8
Compare
b821342 to
4153c5d
Compare
JamesHollyer
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still need to review the tests. Here is what I have so far.
| }>() | ||
| ); | ||
|
|
||
| export const resetFeatureFlagOverrides = createAction( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have been told the name of the action should state what action has been taken as opposed to the effect that will take place. This is a subtle difference. Maybe change this to FlagSetToDefault or something along those lines.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think of featureFlagOverridesReset?
tensorboard/webapp/feature_flag/actions/feature_flag_actions.ts
Outdated
Show resolved
Hide resolved
tensorboard/webapp/feature_flag/effects/feature_flag_effects.ts
Outdated
Show resolved
Hide resolved
7d5df11 to
31853ed
Compare
bmd3k
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks pretty good. I feel like there is something slightly off with how you've modeled your Actions (or I misunderstand the UI). And there are some missing tests.
| describe('resetAllPersistedFeatureFlags', () => { | ||
| it('removes entry from localStorage', () => { | ||
| const mockStorage: Record<string, string> = {}; | ||
| spyOn(localStorage, 'getItem').and.callFake((key: string) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If James also prefers this test pattern (essentially using a fake instead of a mock) then it could be worth writing a cleanup PR to rewrite the existing tests to use this pattern, too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer to not have a bunch of test infrastructure which simulates local storage and just test that the expected calls are made.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I felt like my version was a bit more thorough, but I've gone ahead and refactored it to make it consistent with the existing tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW I preferred the use of fakes but +1 for consistency.
tensorboard/webapp/feature_flag/actions/feature_flag_actions.ts
Outdated
Show resolved
Hide resolved
tensorboard/webapp/feature_flag/actions/feature_flag_actions.ts
Outdated
Show resolved
Hide resolved
| }>() | ||
| ); | ||
|
|
||
| export const featureFlagOverridesReset = createAction( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little suspicious about featureFlagOverrideChanged and featureFlagOverridesReset living side x side. It either points to something wrong with your UI or something wrong with how you're modeling actions.
I am guessing that:
- "featureFlagOverrideChanged" is fired by clicking on some sort of "Save" button at the bottom of the dialog.
- "featureFlagsOverridesAllSetToDefault" is fired by clickign on some sort of "Reset All" button at the bottom of the dialog.
Is that right? If so, where does "featureFlagOverridesReset" fit in? Is there a third "Reset Selected" button or similar? That doesn't seem quite right.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
featureFlagOverrideChanged is used when the user sets a flag to override as true or as false. However, the Partial is not able to indicate that a flag is set to default. So featureFlagOverridesReset handles the setting to default.
I was under the impression we would save these individually as the user changes the setting(ie there is no "save" button). However, if we do have a save button then featureFlagOverrideChanged(which should be named featureFlagOverrideSaved in this case) should be all that is necessary and the flags that are missing from the Partial object can all be set to default.
I think I prefer the option where we call these actions on individual changes and we do not require a save button.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a bit of background I wanted to make featureFlagOverrideChanged have a single key and value pair but I could not get the type to work the way I wanted. We decided to use Partial as a compromise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation, that makes sense. Yes agreed that it would be fine/preferable to omit the "Save" button.
I assumed there is a "Save" button because these actions specify lists of flags. Should they instead specify a single flag?
props<{
flag: keyof FeatureFlags
}>()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was trying to keep my new actions consistent with the existing ones. I believe the logic for using Partial<FeatureFlags> originally was to persist the key and value of the flag which lead to otherwise complicated typing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible, then, to also change the existing action? You could consider doing it in a followup PR if it seems out of scope.
This will work, keeps the typing sane, simplifies your effects and reducer code, and better models the user behavior:
export const featureFlagOverrideChanged = createAction(
'[FEATURE FLAG] Store the feature flags in persistent localStorage',
props<{
flag: keyof FeatureFlags;
value: FeatureFlagType;
}>()
);
export const featureFlagOverridesReset = createAction(
'[FEATURE FLAG] Resetting feature flag overrides',
props<{
flag: keyof FeatureFlags;
}>()
);
(and, note, if you do change the props of featureFlagOverridesReset then it should be renamed to featureFlagOverrideReset (ie get rid of the plural from Overrides).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am ok with this change if you feel strongly about it. We had discussed this as an option. The problem is that there is no way to guarantee the FeatureFlagType(which is basically a big OR statement with all the possible types) matches the type associated with the given key. It was all a lot cleaner and more strongly typed when we just used the Partial. I see the confusion this causes but, I still have a slight preference to the way it is now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, FeatureFlagType is unfortunately necessary to workaround some (relatively minor) limitations in the TypeScript type system.
I think in this case we should prefer the more-accurate data model over more-accurate type checks.
tensorboard/webapp/feature_flag/actions/feature_flag_actions.ts
Outdated
Show resolved
Hide resolved
JamesHollyer
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
tensorboard/webapp/feature_flag/actions/feature_flag_actions.ts
Outdated
Show resolved
Hide resolved
| }>() | ||
| ); | ||
|
|
||
| export const featureFlagOverridesReset = createAction( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible, then, to also change the existing action? You could consider doing it in a followup PR if it seems out of scope.
This will work, keeps the typing sane, simplifies your effects and reducer code, and better models the user behavior:
export const featureFlagOverrideChanged = createAction(
'[FEATURE FLAG] Store the feature flags in persistent localStorage',
props<{
flag: keyof FeatureFlags;
value: FeatureFlagType;
}>()
);
export const featureFlagOverridesReset = createAction(
'[FEATURE FLAG] Resetting feature flag overrides',
props<{
flag: keyof FeatureFlags;
}>()
);
(and, note, if you do change the props of featureFlagOverridesReset then it should be renamed to featureFlagOverrideReset (ie get rid of the plural from Overrides).
tensorboard/webapp/feature_flag/store/feature_flag_reducers_test.ts
Outdated
Show resolved
Hide resolved
tensorboard/webapp/feature_flag/store/feature_flag_reducers_test.ts
Outdated
Show resolved
Hide resolved
| describe('resetAllPersistedFeatureFlags', () => { | ||
| it('removes entry from localStorage', () => { | ||
| const mockStorage: Record<string, string> = {}; | ||
| spyOn(localStorage, 'getItem').and.callFake((key: string) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW I preferred the use of fakes but +1 for consistency.
d7d0edc to
9f7fecc
Compare
…odal (tensorflow#5868) * add ngrx infrastructure for the feature-flags-modal
…odal (tensorflow#5868) * add ngrx infrastructure for the feature-flags-modal
Motivation for features / changes
We want to have a modal to control the state of feature flags and persist their state to localStorage.
Creating such a modal is a large change and so, I'm breaking its creation into two PRs, this one and #5800.
This PR is just intended to prepare the NgRx and data source to be used the ui in #5800.
Technical description of changes
There are existing actions to handle getting and setting feature flag overrides, but none to remove an override. To resolve this, I'm adding two actions, one to reset a set number of overrides, and another to reset all overrides.
I'm also adding a selector to get the default state of all the feature flags. This should allow the modal to work in other flavors of TensorBoard which have different sets of feature flags without worrying about their specifics.
No ui changes yet
Here's an example of what it may look like
