Skip to content

Commit 0c2c195

Browse files
committed
Introduce alert snackbar
1 parent 29d4cd2 commit 0c2c195

28 files changed

+991
-7
lines changed

tensorboard/webapp/BUILD

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ ng_module(
3838
"selectors.ts",
3939
],
4040
deps = [
41+
"//tensorboard/webapp/alert/store",
4142
"//tensorboard/webapp/app_routing/store",
4243
"//tensorboard/webapp/experiments/store:selectors",
4344
"//tensorboard/webapp/metrics/store",
@@ -74,6 +75,8 @@ ng_module(
7475
":mat_icon",
7576
":oss_plugins_module",
7677
":reducer_config",
78+
"//tensorboard/webapp/alert",
79+
"//tensorboard/webapp/alert/views:alert_snackbar",
7780
"//tensorboard/webapp/angular:expect_angular_platform_browser_animations",
7881
"//tensorboard/webapp/app_routing",
7982
"//tensorboard/webapp/app_routing:route_registry",
@@ -106,6 +109,7 @@ ng_module(
106109
"app_state.ts",
107110
],
108111
deps = [
112+
"//tensorboard/webapp/alert/store:types",
109113
"//tensorboard/webapp/app_routing/store:types",
110114
"//tensorboard/webapp/core/store",
111115
"//tensorboard/webapp/experiments/store:types",
@@ -204,6 +208,9 @@ tensorboard_html_binary(
204208
tf_ng_web_test_suite(
205209
name = "karma_test",
206210
deps = [
211+
"//tensorboard/webapp/alert:test_lib",
212+
"//tensorboard/webapp/alert/store:test_lib",
213+
"//tensorboard/webapp/alert/views:views_test",
207214
"//tensorboard/webapp/app_routing:app_routing_test",
208215
"//tensorboard/webapp/app_routing:route_config_test",
209216
"//tensorboard/webapp/app_routing:testing",

tensorboard/webapp/alert/BUILD

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
load("//tensorboard/defs:defs.bzl", "tf_ts_library")
2+
load("@npm_angular_bazel//:index.bzl", "ng_module")
3+
4+
package(default_visibility = ["//tensorboard:internal"])
5+
6+
ng_module(
7+
name = "alert",
8+
srcs = [
9+
"alert_module.ts",
10+
],
11+
deps = [
12+
"//tensorboard/webapp/alert/effects",
13+
"//tensorboard/webapp/alert/store",
14+
"//tensorboard/webapp/alert/views:alert_snackbar",
15+
"@npm//@angular/core",
16+
"@npm//@ngrx/store",
17+
],
18+
)
19+
20+
ng_module(
21+
name = "alert_action",
22+
srcs = [
23+
"alert_action_module.ts",
24+
],
25+
deps = [
26+
":types",
27+
"@npm//@angular/core",
28+
"@npm//@ngrx/store",
29+
],
30+
)
31+
32+
tf_ts_library(
33+
name = "types",
34+
srcs = [
35+
"types.ts",
36+
],
37+
)
38+
39+
tf_ts_library(
40+
name = "test_lib",
41+
testonly = True,
42+
srcs = [
43+
"alert_action_test.ts",
44+
],
45+
deps = [
46+
":alert_action",
47+
"//tensorboard/webapp:app_state",
48+
"//tensorboard/webapp/alert/actions",
49+
"//tensorboard/webapp/alert/effects",
50+
"//tensorboard/webapp/alert/store",
51+
"@npm//@angular/common",
52+
"@npm//@angular/compiler",
53+
"@npm//@angular/core",
54+
"@npm//@ngrx/effects",
55+
"@npm//@ngrx/store",
56+
"@npm//@types/jasmine",
57+
"@npm//rxjs",
58+
],
59+
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
load("//tensorboard/defs:defs.bzl", "tf_ts_library")
2+
3+
package(default_visibility = ["//tensorboard:internal"])
4+
5+
tf_ts_library(
6+
name = "actions",
7+
srcs = [
8+
"index.ts",
9+
],
10+
deps = [
11+
"//tensorboard/webapp/alert:types",
12+
"@npm//@ngrx/store",
13+
],
14+
)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/* Copyright 2020 The TensorFlow Authors. All Rights Reserved.
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
==============================================================================*/
15+
import {createAction, props} from '@ngrx/store';
16+
17+
import {AlertReport} from '../types';
18+
19+
/** @typehack */ import * as _typeHackModels from '@ngrx/store/src/models';
20+
/** @typehack */ import * as _typeHackStore from '@ngrx/store';
21+
22+
/**
23+
* Fires when an alert is to be reported.
24+
*/
25+
export const alertReported = createAction(
26+
'[Alert] Alert Reported',
27+
props<AlertReport>()
28+
);
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/* Copyright 2020 The TensorFlow Authors. All Rights Reserved.
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
==============================================================================*/
15+
import {
16+
Inject,
17+
ModuleWithProviders,
18+
NgModule,
19+
Optional,
20+
InjectionToken,
21+
} from '@angular/core';
22+
import {Action, ActionCreator, Creator} from '@ngrx/store';
23+
import {AlertReport} from './types';
24+
25+
const ACTION_TO_ALERT_PROVIDER = new InjectionToken<ActionToAlertConfig[]>(
26+
'[Alert] Action-To-Alert Provider'
27+
);
28+
29+
export type ActionToAlertTransformer = (action: Action) => AlertReport | null;
30+
31+
export interface ActionToAlertConfig {
32+
/**
33+
* The action to listen for.
34+
*/
35+
actionCreator: ActionCreator<string, Creator>;
36+
37+
/**
38+
* A function that returns an alert report, or null, when the action is
39+
* received.
40+
*/
41+
alertFromAction: ActionToAlertTransformer;
42+
}
43+
44+
/**
45+
* An NgModule that provides alert-producing actions. These action configs are
46+
* collected by AlertModule, which tracks application alerts.
47+
*
48+
* When the configured action fires, the AlertModule may respond.
49+
*
50+
* @NgModule({
51+
* imports: [
52+
* AlertActionModule.registerAlertActions([
53+
* {
54+
* action: fetchKeysFailed,
55+
* alertFromAction: () => {details: "Keys failed to fetch."},
56+
* },
57+
* {
58+
* action: greenButtonClicked,
59+
* alertFromAction: (actionPayload) => {
60+
* if (!actionPayload.wasButtonEnabled) {
61+
* return {details: "Green button failed."};
62+
* }
63+
* return null;
64+
* }
65+
* }
66+
* ]),
67+
* ],
68+
* })
69+
*/
70+
@NgModule({})
71+
export class AlertActionModule {
72+
/**
73+
* Map from action creator type to transformer function.
74+
*/
75+
private readonly providers = new Map<string, ActionToAlertTransformer>();
76+
77+
constructor(
78+
@Optional()
79+
@Inject(ACTION_TO_ALERT_PROVIDER)
80+
providers: ActionToAlertConfig[][]
81+
) {
82+
for (const configs of providers || []) {
83+
for (const config of configs) {
84+
if (this.providers.has(config.actionCreator.type)) {
85+
throw new RangeError(
86+
`"${config.actionCreator.type}" is already registered for alerts.` +
87+
' Multiple alerts for the same action is not allowed.'
88+
);
89+
}
90+
this.providers.set(config.actionCreator.type, config.alertFromAction);
91+
}
92+
}
93+
}
94+
95+
getAlertFromAction(action: Action): AlertReport | null {
96+
const lambda = this.providers.get(action.type);
97+
if (!lambda) {
98+
return null;
99+
}
100+
return lambda(action);
101+
}
102+
103+
static registerAlertActions(
104+
configs: ActionToAlertConfig[]
105+
): ModuleWithProviders<AlertActionModule> {
106+
return {
107+
ngModule: AlertActionModule,
108+
providers: [
109+
{
110+
provide: ACTION_TO_ALERT_PROVIDER,
111+
multi: true,
112+
useValue: configs,
113+
},
114+
],
115+
};
116+
}
117+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/* Copyright 2020 The TensorFlow Authors. All Rights Reserved.
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
==============================================================================*/
15+
import {TestBed} from '@angular/core/testing';
16+
import {EffectsModule} from '@ngrx/effects';
17+
import {provideMockActions} from '@ngrx/effects/testing';
18+
import {Action, createAction, Store} from '@ngrx/store';
19+
import {MockStore, provideMockStore} from '@ngrx/store/testing';
20+
import {ReplaySubject} from 'rxjs';
21+
import {State} from '../app_state';
22+
import * as alertActions from './actions';
23+
import {AlertActionModule} from './alert_action_module';
24+
import {AlertEffects} from './effects';
25+
26+
const alertActionOccurred = createAction('[Test] Action Occurred (need alert)');
27+
const noAlertActionOccurred = createAction('[Test] Action Occurred (no alert)');
28+
29+
describe('alert_effects', () => {
30+
let actions$: ReplaySubject<Action>;
31+
let store: MockStore<Partial<State>>;
32+
let recordedActions: Action[] = [];
33+
let shouldReportAlert: boolean;
34+
35+
beforeEach(async () => {
36+
shouldReportAlert = false;
37+
actions$ = new ReplaySubject<Action>(1);
38+
39+
await TestBed.configureTestingModule({
40+
imports: [
41+
AlertActionModule.registerAlertActions([
42+
{
43+
actionCreator: alertActionOccurred,
44+
alertFromAction: (action: Action) => {
45+
if (shouldReportAlert) {
46+
return {details: 'alert details'};
47+
}
48+
return null;
49+
},
50+
},
51+
]),
52+
EffectsModule.forFeature([AlertEffects]),
53+
EffectsModule.forRoot([]),
54+
],
55+
providers: [provideMockActions(actions$), provideMockStore({})],
56+
}).compileComponents();
57+
58+
store = TestBed.inject<Store<State>>(Store) as MockStore<State>;
59+
recordedActions = [];
60+
spyOn(store, 'dispatch').and.callFake((action: Action) => {
61+
recordedActions.push(action);
62+
});
63+
});
64+
65+
it(`reports an alert when 'alertFromAction' returns a report`, () => {
66+
shouldReportAlert = true;
67+
actions$.next(alertActionOccurred);
68+
69+
expect(recordedActions).toEqual([
70+
alertActions.alertReported({details: 'alert details'}),
71+
]);
72+
});
73+
74+
it(`does not alert when 'alertFromAction' returns null`, () => {
75+
shouldReportAlert = false;
76+
actions$.next(alertActionOccurred);
77+
78+
expect(recordedActions).toEqual([]);
79+
});
80+
81+
it(`does not alert when a non-matching action is fired`, () => {
82+
shouldReportAlert = true;
83+
actions$.next(noAlertActionOccurred);
84+
85+
expect(recordedActions).toEqual([]);
86+
});
87+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/* Copyright 2020 The TensorFlow Authors. All Rights Reserved.
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
==============================================================================*/
15+
import {NgModule} from '@angular/core';
16+
import {EffectsModule} from '@ngrx/effects';
17+
import {StoreModule} from '@ngrx/store';
18+
import {AlertEffects} from './effects';
19+
import {reducers} from './store';
20+
import {ALERT_FEATURE_KEY} from './store/alert_types';
21+
import {AlertSnackbarModule} from './views/alert_snackbar_module';
22+
23+
@NgModule({
24+
imports: [
25+
StoreModule.forFeature(ALERT_FEATURE_KEY, reducers),
26+
EffectsModule.forFeature([AlertEffects]),
27+
AlertSnackbarModule,
28+
],
29+
})
30+
export class AlertModule {}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
load("@npm_angular_bazel//:index.bzl", "ng_module")
2+
3+
package(default_visibility = ["//tensorboard:internal"])
4+
5+
ng_module(
6+
name = "effects",
7+
srcs = [
8+
"index.ts",
9+
],
10+
deps = [
11+
"//tensorboard/webapp:app_state",
12+
"//tensorboard/webapp/alert:alert_action",
13+
"//tensorboard/webapp/alert/actions",
14+
"@npm//@angular/core",
15+
"@npm//@ngrx/effects",
16+
"@npm//@ngrx/store",
17+
"@npm//rxjs",
18+
],
19+
)

0 commit comments

Comments
 (0)