Skip to content

Commit 9d7a981

Browse files
stephanwleedna2github
authored andcommitted
timeseries: show empty match warning text (tensorflow#5273)
When tag filter AND visualization filter matches no cards, TimeSeries should some some message. This change adds the warning when user inputed entries that would match no cards at all.
1 parent 044c026 commit 9d7a981

12 files changed

+283
-61
lines changed

tensorboard/webapp/metrics/views/_common.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,11 @@ $metrics-min-card-height: 320px;
6262
@include tb-theme-foreground-prop(color, secondary-text);
6363
white-space: nowrap;
6464
}
65+
66+
@mixin metrics-empty-message {
67+
@include tb-theme-foreground-prop(color, secondary-text);
68+
font-size: 13px;
69+
font-style: italic;
70+
padding: 16px;
71+
text-align: center;
72+
}

tensorboard/webapp/metrics/views/main_view/BUILD

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,30 @@ tf_sass_binary(
4747
],
4848
)
4949

50+
tf_ts_library(
51+
name = "common_selectors",
52+
srcs = ["common_selectors.ts"],
53+
deps = [
54+
"//tensorboard/webapp:app_state",
55+
"//tensorboard/webapp:selectors",
56+
"//tensorboard/webapp/metrics/data_source",
57+
"//tensorboard/webapp/metrics/store",
58+
"//tensorboard/webapp/metrics/views:types",
59+
"//tensorboard/webapp/metrics/views:utils",
60+
"//tensorboard/webapp/util:types",
61+
"@npm//@ngrx/store",
62+
],
63+
)
64+
5065
tf_ng_module(
5166
name = "main_view",
5267
srcs = [
5368
"card_grid_component.ts",
5469
"card_grid_container.ts",
5570
"card_groups_component.ts",
5671
"card_groups_container.ts",
72+
"empty_tag_match_message_component.ts",
73+
"empty_tag_match_message_container.ts",
5774
"filter_input_component.ts",
5875
"filter_input_container.ts",
5976
"filtered_view_component.ts",
@@ -76,6 +93,7 @@ tf_ng_module(
7693
":pinned_view_component_styles",
7794
],
7895
deps = [
96+
":common_selectors",
7997
"//tensorboard/webapp:app_state",
8098
"//tensorboard/webapp:selectors",
8199
"//tensorboard/webapp/angular:expect_angular_cdk_scrolling",

tensorboard/webapp/metrics/views/main_view/card_groups_container.ts

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,16 @@ See the License for the specific language governing permissions and
1313
limitations under the License.
1414
==============================================================================*/
1515
import {ChangeDetectionStrategy, Component, Input} from '@angular/core';
16-
import {createSelector, Store} from '@ngrx/store';
16+
import {Store} from '@ngrx/store';
1717
import {Observable} from 'rxjs';
1818
import {combineLatestWith, map} from 'rxjs/operators';
1919

2020
import {State} from '../../../app_state';
21-
import {getCurrentRouteRunSelection} from '../../../selectors';
22-
import {isSingleRunPlugin} from '../../data_source';
23-
import {
24-
getMetricsFilteredPluginTypes,
25-
getNonEmptyCardIdsWithMetadata,
26-
} from '../../store';
21+
import {getMetricsFilteredPluginTypes} from '../../store';
2722
import {CardObserver} from '../card_renderer/card_lazy_loader';
2823
import {CardGroup} from '../metrics_view_types';
2924
import {groupCardIdWithMetdata} from '../utils';
30-
31-
const getRenderableCardIdsWithMetadata = createSelector(
32-
getNonEmptyCardIdsWithMetadata,
33-
getCurrentRouteRunSelection,
34-
(cardList, runSelectionMap) => {
35-
return cardList.filter((card) => {
36-
if (!isSingleRunPlugin(card.plugin)) {
37-
return true;
38-
}
39-
return Boolean(runSelectionMap && runSelectionMap.get(card.runId!));
40-
});
41-
}
42-
);
25+
import {getSortedRenderableCardIdsWithMetadata} from './common_selectors';
4326

4427
@Component({
4528
selector: 'metrics-card-groups',
@@ -57,7 +40,7 @@ export class CardGroupsContainer {
5740
constructor(private readonly store: Store<State>) {}
5841

5942
readonly cardGroups$: Observable<CardGroup[]> = this.store
60-
.select(getRenderableCardIdsWithMetadata)
43+
.select(getSortedRenderableCardIdsWithMetadata)
6144
.pipe(
6245
combineLatestWith(this.store.select(getMetricsFilteredPluginTypes)),
6346
map(([cardList, filteredPlugins]) => {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/* Copyright 2021 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 {createSelector} from '@ngrx/store';
16+
17+
import {State} from '../../../app_state';
18+
import {getCurrentRouteRunSelection} from '../../../selectors';
19+
import {DeepReadonly} from '../../../util/types';
20+
import {isSingleRunPlugin} from '../../data_source';
21+
import {getNonEmptyCardIdsWithMetadata} from '../../store';
22+
import {CardIdWithMetadata} from '../metrics_view_types';
23+
import {compareTagNames} from '../utils';
24+
25+
const getRenderableCardIdsWithMetadata = createSelector<
26+
State,
27+
readonly DeepReadonly<CardIdWithMetadata>[],
28+
Map<string, boolean> | null,
29+
DeepReadonly<CardIdWithMetadata>[]
30+
>(
31+
getNonEmptyCardIdsWithMetadata,
32+
getCurrentRouteRunSelection,
33+
(cardList, runSelectionMap) => {
34+
return cardList.filter((card) => {
35+
if (!isSingleRunPlugin(card.plugin)) {
36+
return true;
37+
}
38+
return Boolean(runSelectionMap && runSelectionMap.get(card.runId!));
39+
});
40+
}
41+
);
42+
43+
export const getSortedRenderableCardIdsWithMetadata = createSelector<
44+
State,
45+
DeepReadonly<CardIdWithMetadata>[],
46+
DeepReadonly<CardIdWithMetadata>[]
47+
>(getRenderableCardIdsWithMetadata, (cardList) => {
48+
return cardList.sort((cardA, cardB) => {
49+
return compareTagNames(cardA.tag, cardB.tag);
50+
});
51+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/* Copyright 2021 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 {ChangeDetectionStrategy, Component, Input} from '@angular/core';
16+
17+
import {PluginType} from '../../data_source';
18+
19+
declare namespace Intl {
20+
class ListFormat {
21+
constructor(
22+
locale?: string,
23+
options?: {
24+
localeMatcher?: 'lookup' | 'best fit';
25+
style?: 'long' | 'short' | 'narrow';
26+
type?: 'unit' | 'conjunction' | 'disjunction';
27+
}
28+
);
29+
format: (items: string[]) => string;
30+
}
31+
}
32+
33+
@Component({
34+
selector: 'metrics-empty-tag-match-component',
35+
template: `No cards matches tag filter
36+
<code>/{{ tagFilterRegex }}/</code> among {{ tagCounts | number }} tags<span
37+
*ngIf="pluginTypes.size"
38+
>
39+
and {{ getPluginTypeFilterString(pluginTypes) }} visualization
40+
filter</span
41+
>.`,
42+
changeDetection: ChangeDetectionStrategy.OnPush,
43+
})
44+
export class EmptyTagMatchMessageComponent {
45+
readonly PluginType = PluginType;
46+
private readonly listFormatter = new Intl.ListFormat(undefined, {
47+
style: 'long',
48+
type: 'disjunction',
49+
});
50+
51+
@Input() pluginTypes!: Set<PluginType>;
52+
@Input() tagFilterRegex!: string;
53+
@Input() tagCounts!: number;
54+
55+
getPluginTypeFilterString(pluginTypes: Set<PluginType>): string {
56+
const humanReadableTypes = [...pluginTypes].map((type) => {
57+
switch (type) {
58+
case PluginType.SCALARS:
59+
return 'scalar';
60+
case PluginType.IMAGES:
61+
return 'image';
62+
case PluginType.HISTOGRAMS:
63+
return 'histogram';
64+
default:
65+
const _: never = type;
66+
throw new RangeError(
67+
`Please implement human readable name for plugin type: ${type}`
68+
);
69+
}
70+
});
71+
72+
return this.listFormatter.format(humanReadableTypes);
73+
}
74+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/* Copyright 2021 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 {ChangeDetectionStrategy, Component} from '@angular/core';
16+
import {Store} from '@ngrx/store';
17+
import {Observable} from 'rxjs';
18+
import {map} from 'rxjs/operators';
19+
20+
import {State} from '../../../app_state';
21+
import {PluginType} from '../../data_source';
22+
import {getMetricsFilteredPluginTypes, getMetricsTagFilter} from '../../store';
23+
import {getSortedRenderableCardIdsWithMetadata} from './common_selectors';
24+
25+
/**
26+
* Warning message that displays when no tags do not match filter query.
27+
*/
28+
@Component({
29+
selector: 'metrics-empty-tag-match',
30+
template: `
31+
<metrics-empty-tag-match-component
32+
[pluginTypes]="pluginTypes$ | async"
33+
[tagFilterRegex]="tagFilterRegex$ | async"
34+
[tagCounts]="tagCounts$ | async"
35+
></metrics-empty-tag-match-component>
36+
`,
37+
changeDetection: ChangeDetectionStrategy.OnPush,
38+
})
39+
export class EmptyTagMatchMessageContainer {
40+
constructor(private readonly store: Store<State>) {}
41+
42+
readonly pluginTypes$: Observable<Set<PluginType>> = this.store.select(
43+
getMetricsFilteredPluginTypes
44+
);
45+
readonly tagFilterRegex$: Observable<string> = this.store.select(
46+
getMetricsTagFilter
47+
);
48+
readonly tagCounts$: Observable<number> = this.store
49+
.select(getSortedRenderableCardIdsWithMetadata)
50+
.pipe(
51+
map((cardList) => {
52+
return new Set(cardList.map(({tag}) => tag)).size;
53+
})
54+
);
55+
}

tensorboard/webapp/metrics/views/main_view/filtered_view_component.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,8 @@ limitations under the License.
3232
@include metrics-card-group-count-text;
3333
margin-left: 6px;
3434
}
35+
36+
metrics-empty-tag-match {
37+
@include metrics-empty-message;
38+
display: block;
39+
}

tensorboard/webapp/metrics/views/main_view/filtered_view_component.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ import {CardIdWithMetadata} from '../metrics_view_types';
3030
>
3131
</span>
3232
</div>
33+
<metrics-empty-tag-match
34+
*ngIf="isEmptyMatch"
35+
class="warn"
36+
></metrics-empty-tag-match>
3337
<metrics-card-grid
3438
[cardIdsWithMetadata]="cardIdsWithMetadata"
3539
[cardObserver]="cardObserver"
@@ -39,6 +43,7 @@ import {CardIdWithMetadata} from '../metrics_view_types';
3943
changeDetection: ChangeDetectionStrategy.OnPush,
4044
})
4145
export class FilteredViewComponent {
46+
@Input() isEmptyMatch!: boolean;
4247
@Input() cardObserver!: CardObserver;
4348
@Input() cardIdsWithMetadata!: CardIdWithMetadata[];
4449
}

0 commit comments

Comments
 (0)