Skip to content

Conversation

@psybuzz
Copy link
Contributor

@psybuzz psybuzz commented Oct 16, 2020

Diffbase: #4221
Followup: #4245

Introduces an alert snackbar UI, which surfaces application
errors to the user in the corner of the screen.

Technical design discussion:

On the surface, there are many ways to architect an alert/error
system in ngrx, where our goal is to allow any feature in TB or
a TB embedder to trigger alerts in the snackbar UI.

Creating an 'Alert' feature store, whose reducer listens to other
features' actions would work for TB, but not embedders, since
TB core does not have access to ngrx actions from external
features.

One way to workaround this composability problem is to force
embedders to override the Alerts module and composeReducers.
Upon receiving an embedder-specific error, the embedder's
Alerts module can set the latest error accordingly. However,
this approach assumes that a reducer can determine whether
an action should produce an alert or not, based on the action's
payload.

In the followup, the 'cardPinStateToggled' action provides a
counterexample: it should only trigger an alert if there are too
many current pinned cards.

One way to workaround this is to introduce AlertEffects as a
layer that listens to actions from all features, and converts them
into generic 'alertReported' actions (assuming effects can be
composed, similar to reducers). However, this leads us to a world
where AlertModule depends heavily on other features. For
example, if AlertModule depends on feature F, it becomes
difficult to create a TB application / embedder that needs Alerts
without needing F. This is a design constraint we'd like to follow,
as it gives flexibility in bundling TB with only a subset of the
current features.

One might imagine that specific features can do the translation
themselves, using specific effects to convert actions into
'alertReported' actions. However, this synchronous dispatch of
composite actions goes against a best practice. In other
scenarios, it is known to lead to hard-to-reason-about in-between
states, and makes undoing or reverting state changes difficult
due to the processing of multiple actions.

Thus, we finally land on the existing patch. Essentially, it hides
away the composite action using Angular's DI framework. The
actions that should trigger alerts are registered once, and are
dispatched by the AlertEffects, rather than specific features.

Googlers, see test sync cl/338344258

@google-cla google-cla bot added the cla: yes label Oct 16, 2020
@psybuzz psybuzz marked this pull request as ready for review October 16, 2020 00:10
@psybuzz psybuzz requested a review from stephanwlee October 16, 2020 00:10
Copy link
Contributor

@stephanwlee stephanwlee left a comment

Choose a reason for hiding this comment

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

This PR introduces the concept of error "details", a string which
is defined in a TS module that will be surfaced in the UI.

This did not sufficiently describe your design choice; it looks like the change is recommending putting what should be localized in the action payload which kind of means that, for instance, there is a network error or whatever, you would be firing an action, presumably synchronously. That is conceptually regarding an action as a mutation/command, not event.

Are you willfully making the exception here? Does it need to be be done that way?

import {MatIconModule} from './mat_icon_module';
import {PluginsModule} from './plugins/plugins_module';
import {ROOT_REDUCERS, loggerMetaReducerFactory} from './reducer_config';
import {ReloaderModule} from './reloader/reloader_module';
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this an unused import cleanup?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. Although not related to the change, it is a small drive-by that fixes a lint warning upon sync, since this file is changed.

@psybuzz psybuzz changed the title Introduce error snackbar UI Introduce alert snackbar UI Oct 20, 2020
@psybuzz psybuzz requested a review from stephanwlee October 20, 2020 03:15
@psybuzz
Copy link
Contributor Author

psybuzz commented Oct 20, 2020

In the followup, #4245, this approach requires us to include 'canCreateNewPins' and 'wasPinned' to the cardPinStateToggled action payload.

Overall, my impression of this approach is that it's slightly more complex than it might deserve to be. I would be fine with this approach, or doing the simpler composite-action (noting that it would be an exception to our best practice. Also open to any other ideas, if we have any.

Comment on lines +3 to +6
package(default_visibility = ["//tensorboard:internal"])

tf_ts_library(
name = "actions",
Copy link
Contributor

Choose a reason for hiding this comment

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

make sure this is only usable by the effects.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Visibility reduced to alerts/.

* An alert structure used when creating newly reported alerts.
*/
export interface AlertReport {
details: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

I do not think this is sufficient to migrate existing code. We will need to have an optional actionType for an alert that can have actions (you may also want to have localized string for those actions).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would like to add further fields such as actionType in a separate PR, and keep this one scoped to things that will be used in the followup. Existing embedders will also want the snackbar to have an 'x' close button, which I believe may require upgrading the snackbar.open to .openFromComponent.

* An alert structure used when creating newly reported alerts.
*/
export interface AlertReport {
details: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

details -> localizedMessage?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Comment on lines 41 to 45
// return !isDevMode()
// ? (reducer) => (state, action) => {
// return reducer(state, action);
// }
// : logger;
Copy link
Contributor

Choose a reason for hiding this comment

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

revert please.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Whoops, done.

<main #main>
<router-outlet></router-outlet>
</main>
<alert-snackbar></alert-snackbar>
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, this is not great for composability but I have no good recommendation here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. Ideally these below (hash-storage, page-title, settings-polymer-interop) might also be added in some other way. I haven't thought of a way I like yet, but we could imagine that a more composable approach could be applied to all of them at once in the future.

store = TestBed.inject<Store<State>>(Store) as MockStore<State>;
});

it('should open the snackbar on each alert', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

omit the world "should" in test; it is redundant and not needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@psybuzz psybuzz requested a review from stephanwlee October 20, 2020 22:11
Copy link
Contributor

@stephanwlee stephanwlee left a comment

Choose a reason for hiding this comment

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

LGTM. I don't think we immediately in this PR require the actionable snackbar so I will be okay if you want to do a follow up.

* An alert structure used when creating newly reported alerts.
*/
export interface AlertReport {
localizedMessage: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

I may have left a comment in the other PR but I don't think I find any comment.

How would you plan on supporting different actions in the snackbar? i.e., in some cases, we want to have button to retry sending a request. In order to replace our current error handling, we would need that facility. Without it, we will have two ways to achieve the same thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd like to do this in a separate PR, if that works for you. This is what I had in mind:

extend AlertReport with a new field:

export interface AlertReport {
  localizedMessage: string;
  retryCallback?: (store: Store) => void;
}

used like so:

AlertActionModule.registerActionAlerts([
  {
    localizedMessage: "Fetch failed",
    retryCallback: (store) => {
      // ...
      store.dispatch(fetchAction(...));
    }
  },
])

with the AlertSnackbarContainer using .openFromComponent, instead of .open,
to open an AlertSnackbarComponent, which contains:

// template
<div *ngIf="!!alertInfo.retryCallback" (click)="onRetryClicked">Retry</div>

// component
@Input() alertInfo: AlertInfo

onRetryClicked() { this.alertInfo.retryCallback(); }

Copy link
Contributor

Choose a reason for hiding this comment

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

I wish it to be more generic than just retryCallback (and it, I don't think should be a callback since it opens the API for just about anything) but I will look forward to seeing the change afterwards.

Comment on lines 51 to 53
tap((alert) => {
this.showAlert(alert!.localizedMessage);
})
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe do this in the subscribe instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Copy link
Contributor Author

@psybuzz psybuzz left a comment

Choose a reason for hiding this comment

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

Thanks for the review!

* An alert structure used when creating newly reported alerts.
*/
export interface AlertReport {
localizedMessage: 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.

I'd like to do this in a separate PR, if that works for you. This is what I had in mind:

extend AlertReport with a new field:

export interface AlertReport {
  localizedMessage: string;
  retryCallback?: (store: Store) => void;
}

used like so:

AlertActionModule.registerActionAlerts([
  {
    localizedMessage: "Fetch failed",
    retryCallback: (store) => {
      // ...
      store.dispatch(fetchAction(...));
    }
  },
])

with the AlertSnackbarContainer using .openFromComponent, instead of .open,
to open an AlertSnackbarComponent, which contains:

// template
<div *ngIf="!!alertInfo.retryCallback" (click)="onRetryClicked">Retry</div>

// component
@Input() alertInfo: AlertInfo

onRetryClicked() { this.alertInfo.retryCallback(); }

Comment on lines 51 to 53
tap((alert) => {
this.showAlert(alert!.localizedMessage);
})
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

* An alert structure used when creating newly reported alerts.
*/
export interface AlertReport {
localizedMessage: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

I wish it to be more generic than just retryCallback (and it, I don't think should be a callback since it opens the API for just about anything) but I will look forward to seeing the change afterwards.

Base automatically changed from psybuzz-urlpins3 to master October 22, 2020 01:16
@psybuzz psybuzz merged commit cf53ff6 into master Oct 22, 2020
@psybuzz psybuzz deleted the psybuzz-urlpins4 branch October 22, 2020 02:48
psybuzz added a commit that referenced this pull request Oct 22, 2020
Diffbase: #4244

Prevents the "toggle pin" button from creating a new pin,
if we already have the max number of pins allowed.
Unresolved pins still count towards the quota.

This enforced limit prevents the URL from overflowing,
see #4242
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants