Use DetailsShell in ftrace panel, logs panel, and query panel.
Provides a more consistent look and feel to details panels.
Also fixes bug where horizontal scrolling in analyse page was broken.
Bug: 286842471
Change-Id: I789cdc0bc6117b8e522ae5bfb0d3fd75c1b17e95
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index 94a9c0e..ad91f01 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -262,14 +262,22 @@
}
}
-.query-table {
- width: 100%;
+.pf-query-panel {
+ display: contents;
+ .pf-query-warning {
+ padding: 4px;
+ position: sticky;
+ left: 0;
+ }
+}
+
+.pf-query-table {
+ min-width: 100%;
font-size: 14px;
border: 0;
thead td {
- // TODO(stevegolton): Get sticky working again.
- // position: sticky;
- // top: 0;
+ position: sticky;
+ top: 0;
background-color: hsl(214, 22%, 90%);
color: #262f3c;
text-align: center;
diff --git a/ui/src/assets/perfetto.scss b/ui/src/assets/perfetto.scss
index 95ed422..0b1e705 100644
--- a/ui/src/assets/perfetto.scss
+++ b/ui/src/assets/perfetto.scss
@@ -44,3 +44,5 @@
@import "widgets/switch";
@import "widgets/text_input";
@import "widgets/tree";
+@import "widgets/virtual_scroll_container";
+@import "widgets/callout";
diff --git a/ui/src/assets/widgets/callout.scss b/ui/src/assets/widgets/callout.scss
new file mode 100644
index 0000000..da47edf
--- /dev/null
+++ b/ui/src/assets/widgets/callout.scss
@@ -0,0 +1,33 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+@import "theme";
+
+.pf-callout {
+ display: flex;
+ font-family: inherit;
+ font-size: inherit;
+ line-height: 1;
+ color: $pf-minimal-foreground;
+ background: rgb(239, 239, 239);
+ border-radius: $pf-border-radius;
+ padding: 4px;
+ border: solid lightgray 1px;
+
+ & > .pf-left-icon {
+ font-size: inherit;
+ margin-right: 6px;
+ align-items: baseline;
+ }
+}
diff --git a/ui/src/assets/widgets/details_shell.scss b/ui/src/assets/widgets/details_shell.scss
index 75c6af9..a71f237 100644
--- a/ui/src/assets/widgets/details_shell.scss
+++ b/ui/src/assets/widgets/details_shell.scss
@@ -18,17 +18,10 @@
font-family: $pf-font;
display: flex;
flex-direction: column;
- min-height: 100%;
-
- &.pf-match-parent {
- height: 100%;
- }
.pf-header-bar {
- z-index: 1; // HACK: Make the header bar appear above the content
- position: sticky;
- top: 0;
- left: 0;
+ z-index: 1;
+
display: flex;
flex-direction: row;
align-items: baseline;
@@ -36,7 +29,6 @@
background-color: white;
color: black;
padding: 8px 8px 5px 8px;
- box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.2);
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
.pf-header-title {
@@ -58,6 +50,7 @@
display: flex;
min-width: min-content;
gap: 4px;
+ align-items: baseline;
}
}
@@ -65,45 +58,13 @@
font-size: smaller;
flex-grow: 1;
font-weight: 300;
+ overflow-x: auto;
+ }
- table {
- @include transition(0.1s);
- @include table-font-size;
- width: 100%;
- // Aggregation panel uses multiple table elements that need to be aligned,
- // which is done by using fixed table layout.
- table-layout: fixed;
- word-wrap: break-word;
- padding: 0 10px;
- tr:hover {
- td,
- th {
- background-color: $table-hover-color;
-
- &.no-highlight {
- background-color: white;
- }
- }
- }
- th {
- text-align: left;
- width: 30%;
- font-weight: normal;
- vertical-align: top;
- }
- td.value {
- white-space: pre-wrap;
- }
- td.padding {
- min-width: 10px;
- }
- .array-index {
- text-align: right;
- }
- }
-
- .auto-layout {
- table-layout: auto;
+ &.pf-fill-parent {
+ height: 100%;
+ .pf-content {
+ overflow-y: auto;
}
}
}
diff --git a/ui/src/assets/widgets/details_table.scss b/ui/src/assets/widgets/details_table.scss
new file mode 100644
index 0000000..297c7ae
--- /dev/null
+++ b/ui/src/assets/widgets/details_table.scss
@@ -0,0 +1,53 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+table.pf-details-table {
+ @include transition(0.1s);
+ @include table-font-size;
+ width: 100%;
+ // Aggregation panel uses multiple table elements that need to be aligned,
+ // which is done by using fixed table layout.
+ table-layout: fixed;
+ word-wrap: break-word;
+ padding: 0 10px;
+ tr:hover {
+ td,
+ th {
+ background-color: $table-hover-color;
+
+ &.no-highlight {
+ background-color: white;
+ }
+ }
+ }
+ th {
+ text-align: left;
+ width: 30%;
+ font-weight: normal;
+ vertical-align: top;
+ }
+ td.value {
+ white-space: pre-wrap;
+ }
+ td.padding {
+ min-width: 10px;
+ }
+ .array-index {
+ text-align: right;
+ }
+}
+
+.auto-layout {
+ table-layout: auto;
+}
diff --git a/ui/src/assets/widgets/virtual_scroll_container.scss b/ui/src/assets/widgets/virtual_scroll_container.scss
new file mode 100644
index 0000000..d48fbeb
--- /dev/null
+++ b/ui/src/assets/widgets/virtual_scroll_container.scss
@@ -0,0 +1,20 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+@import "theme";
+
+.pf-virtual-scroll-container {
+ overflow: auto;
+ height: 100%;
+}
diff --git a/ui/src/assets/widgets_page.scss b/ui/src/assets/widgets_page.scss
index 96286bc..4d3309a 100644
--- a/ui/src/assets/widgets_page.scss
+++ b/ui/src/assets/widgets_page.scss
@@ -58,6 +58,7 @@
.widget-container {
display: flex;
min-width: 300px;
+ max-width: 600px;
min-height: 250px;
border-radius: 3px;
box-shadow: inset 2px 2px 10px #00000020;
diff --git a/ui/src/frontend/analyze_page.ts b/ui/src/frontend/analyze_page.ts
index 85ad308..dc0025f 100644
--- a/ui/src/frontend/analyze_page.ts
+++ b/ui/src/frontend/analyze_page.ts
@@ -220,6 +220,7 @@
state.queryResult = undefined;
globals.rafScheduler.scheduleFullRedraw();
},
+ fillParent: false,
}),
m(QueryHistoryComponent));
},
diff --git a/ui/src/frontend/ftrace_panel.ts b/ui/src/frontend/ftrace_panel.ts
index 84337a9..1fb0c2c 100644
--- a/ui/src/frontend/ftrace_panel.ts
+++ b/ui/src/frontend/ftrace_panel.ts
@@ -15,7 +15,6 @@
import m from 'mithril';
import {StringListPatch} from 'src/common/state';
-import {assertExists} from '../base/logging';
import {Actions} from '../common/actions';
import {colorForString} from '../common/colorizer';
import {TPTime} from '../common/time';
@@ -31,6 +30,7 @@
} from './widgets/multiselect';
import {PopupPosition} from './widgets/popup';
import {Timestamp} from './widgets/timestamp';
+import {VirtualScrollContainer} from './widgets/virtual_scroll_container';
const ROW_H = 20;
const PAGE_SIZE = 250;
@@ -63,23 +63,14 @@
title: this.renderTitle(),
buttons: this.renderFilterPanel(),
},
- m('.ftrace-panel', this.renderRows()));
- }
-
- private scrollContainer(dom: Element): HTMLElement {
- const el = dom.parentElement;
- return assertExists(el);
- }
-
- oncreate({dom}: m.CVnodeDOM) {
- const sc = this.scrollContainer(dom);
- sc.addEventListener('scroll', this.onScroll);
- this.recomputeVisibleRowsAndUpdate(sc);
- }
-
- onupdate({dom}: m.CVnodeDOM) {
- const sc = this.scrollContainer(dom);
- this.recomputeVisibleRowsAndUpdate(sc);
+ m(
+ VirtualScrollContainer,
+ {
+ onScroll: this.onScroll,
+ },
+ m('.ftrace-panel', this.renderRows()),
+ ),
+ );
}
recomputeVisibleRowsAndUpdate(scrollContainer: HTMLElement) {
@@ -101,19 +92,15 @@
}
}
- onremove({dom}: m.CVnodeDOM) {
- const sc = this.scrollContainer(dom);
- sc.removeEventListener('scroll', this.onScroll);
-
+ onremove(_: m.CVnodeDOM) {
globals.dispatch(Actions.updateFtracePagination({
offset: 0,
count: 0,
}));
}
- onScroll = (e: Event) => {
- const scrollContainer = e.target as HTMLElement;
- this.recomputeVisibleRowsAndUpdate(scrollContainer);
+ onScroll = (container: HTMLElement) => {
+ this.recomputeVisibleRowsAndUpdate(container);
};
onRowOver(ts: TPTime) {
@@ -153,7 +140,6 @@
{
label: 'Filter',
minimal: true,
- compact: true,
icon: 'filter_list_alt',
popupPosition: PopupPosition.Top,
options,
diff --git a/ui/src/frontend/logs_filters.ts b/ui/src/frontend/logs_filters.ts
index 432913f..dff5ef3 100644
--- a/ui/src/frontend/logs_filters.ts
+++ b/ui/src/frontend/logs_filters.ts
@@ -15,7 +15,11 @@
import m from 'mithril';
import {Actions} from '../common/actions';
+
import {globals} from './globals';
+import {Button} from './widgets/button';
+import {Select} from './widgets/select';
+import {TextInput} from './widgets/text_input';
export const LOG_PRIORITIES =
['-', '-', 'Verbose', 'Debug', 'Info', 'Warn', 'Error', 'Fatal'];
@@ -50,7 +54,7 @@
m('option', {value: i, selected}, attrs.options[i]));
}
return m(
- 'select',
+ Select,
{
onchange: (e: InputEvent) => {
const selectionValue = (e.target as HTMLSelectElement).value;
@@ -64,16 +68,11 @@
class LogTagChip implements m.ClassComponent<LogTagChipAttrs> {
view({attrs}: m.CVnode<LogTagChipAttrs>) {
- return m(
- '.chip',
- m('.chip-text', attrs.name),
- m('button.chip-button',
- {
- onclick: () => {
- attrs.removeTag(attrs.name);
- },
- },
- '×'));
+ return m(Button, {
+ label: attrs.name,
+ rightIcon: 'close',
+ onclick: () => attrs.removeTag(attrs.name),
+ });
}
}
@@ -84,13 +83,14 @@
view(vnode: m.Vnode<LogTagsWidgetAttrs>) {
const tags = vnode.attrs.tags;
- return m(
- '.tag-container',
- m('.chips', tags.map((tag) => m(LogTagChip, {
- name: tag,
- removeTag: this.removeTag.bind(this),
- }))),
- m(`input.chip-input[placeholder='Add new tag']`, {
+ return [
+ tags.map((tag) => m(LogTagChip, {
+ name: tag,
+ removeTag: this.removeTag.bind(this),
+ })),
+ m(TextInput,
+ {
+ placeholder: 'Filter by tag...',
onkeydown: (e: KeyboardEvent) => {
// This is to avoid zooming on 'w'(and other unexpected effects
// of key presses in this input field).
@@ -115,14 +115,16 @@
Actions.addLogTag({tag: htmlElement.value.trim()}));
htmlElement.value = '';
},
- }));
+ }),
+ ];
}
}
class LogTextWidget implements m.ClassComponent {
view() {
return m(
- '.tag-container', m(`input.chip-input[placeholder='Search log text']`, {
+ TextInput, {
+ placeholder: 'Search logs...',
onkeydown: (e: KeyboardEvent) => {
// This is to avoid zooming on 'w'(and other unexpected effects
// of key presses in this input field).
@@ -136,7 +138,7 @@
globals.dispatch(
Actions.updateLogFilterText({textEntry: htmlElement.value}));
},
- }));
+ });
}
}
@@ -145,35 +147,32 @@
const icon = attrs.hideNonMatching ? 'unfold_less' : 'unfold_more';
const tooltip = attrs.hideNonMatching ? 'Expand all and view highlighted' :
'Collapse all';
- return m(
- '.filter-widget',
- m('.tooltip', tooltip),
- m('i.material-icons',
- {
- onclick: () => {
- globals.dispatch(Actions.toggleCollapseByTextEntry({}));
- },
- },
- icon));
+ return m(Button, {
+ icon,
+ title: tooltip,
+ disabled: globals.state.logFilteringCriteria.textEntry === '',
+ minimal: true,
+ onclick: () => globals.dispatch(Actions.toggleCollapseByTextEntry({})),
+ });
}
}
export class LogsFilters implements m.ClassComponent {
view(_: m.CVnode<{}>) {
- return m(
- '.log-filters',
- m('.log-label', 'Log Level'),
- m(LogPriorityWidget, {
- options: LOG_PRIORITIES,
- selectedIndex: globals.state.logFilteringCriteria.minimumLevel,
- onSelect: (minimumLevel) => {
- globals.dispatch(Actions.setMinimumLogLevel({minimumLevel}));
- },
- }),
- m(LogTagsWidget, {tags: globals.state.logFilteringCriteria.tags}),
- m(LogTextWidget),
- m(FilterByTextWidget, {
- hideNonMatching: globals.state.logFilteringCriteria.hideNonMatching,
- }));
+ return [
+ m('.log-label', 'Log Level'),
+ m(LogPriorityWidget, {
+ options: LOG_PRIORITIES,
+ selectedIndex: globals.state.logFilteringCriteria.minimumLevel,
+ onSelect: (minimumLevel) => {
+ globals.dispatch(Actions.setMinimumLogLevel({minimumLevel}));
+ },
+ }),
+ m(LogTagsWidget, {tags: globals.state.logFilteringCriteria.tags}),
+ m(LogTextWidget),
+ m(FilterByTextWidget, {
+ hideNonMatching: globals.state.logFilteringCriteria.hideNonMatching,
+ }),
+ ];
}
}
diff --git a/ui/src/frontend/logs_panel.ts b/ui/src/frontend/logs_panel.ts
index eb9a5c2..71c3b20 100644
--- a/ui/src/frontend/logs_panel.ts
+++ b/ui/src/frontend/logs_panel.ts
@@ -14,7 +14,6 @@
import m from 'mithril';
-import {assertExists} from '../base/logging';
import {Actions} from '../common/actions';
import {HighPrecisionTimeSpan} from '../common/high_precision_time';
import {
@@ -30,21 +29,20 @@
import {LOG_PRIORITIES, LogsFilters} from './logs_filters';
import {Panel} from './panel';
import {asTPTimestamp} from './sql_types';
+import {DetailsShell} from './widgets/details_shell';
import {Timestamp} from './widgets/timestamp';
+import {VirtualScrollContainer} from './widgets/virtual_scroll_container';
const ROW_H = 20;
export class LogPanel extends Panel<{}> {
- private scrollContainer?: HTMLElement;
private bounds?: LogBounds;
private entries?: LogEntries;
private visibleRowOffset = 0;
private visibleRowCount = 0;
- recomputeVisibleRowsAndUpdate() {
- const scrollContainer = assertExists(this.scrollContainer);
-
+ recomputeVisibleRowsAndUpdate(scrollContainer: HTMLElement) {
const prevOffset = this.visibleRowOffset;
const prevCount = this.visibleRowCount;
this.visibleRowOffset = Math.floor(scrollContainer.scrollTop / ROW_H);
@@ -59,15 +57,11 @@
}
}
- oncreate({dom}: m.CVnodeDOM) {
- this.scrollContainer = assertExists(dom.parentElement as HTMLElement);
- this.scrollContainer.addEventListener(
- 'scroll', this.onScroll.bind(this), {passive: true});
+ oncreate(_: m.CVnodeDOM) {
// TODO(stevegolton): Type assersions are a source of bugs.
// Let's try to find another way of doing this.
this.bounds = globals.trackDataStore.get(LogBoundsKey) as LogBounds;
this.entries = globals.trackDataStore.get(LogEntriesKey) as LogEntries;
- this.recomputeVisibleRowsAndUpdate();
}
onbeforeupdate(_: m.CVnodeDOM) {
@@ -75,14 +69,12 @@
// Let's try to find another way of doing this.
this.bounds = globals.trackDataStore.get(LogBoundsKey) as LogBounds;
this.entries = globals.trackDataStore.get(LogEntriesKey) as LogEntries;
- this.recomputeVisibleRowsAndUpdate();
}
- onScroll() {
- if (this.scrollContainer === undefined) return;
- this.recomputeVisibleRowsAndUpdate();
+ onScroll = (scrollContainer: HTMLElement) => {
+ this.recomputeVisibleRowsAndUpdate(scrollContainer);
globals.rafScheduler.scheduleFullRedraw();
- }
+ };
onRowOver(ts: TPTime) {
globals.dispatch(Actions.setHoverCursorTimestamp({ts}));
@@ -167,18 +159,22 @@
}
}
+ // TODO(stevegolton): Add a 'loading' state to DetailsShell, which shows a
+ // scrolling scrolly bar at the bottom of the banner & map isStale to it
return m(
- '.log-panel',
- m('header',
- {
- 'class': isStale ? 'stale' : '',
- },
- [
- m('.log-rows-label',
- `Logs rows [${offset}, ${offset + count}] / ${total}`),
- m(LogsFilters),
- ]),
- m('.rows', {style: {height: `${total * ROW_H}px`}}, rows));
+ DetailsShell,
+ {
+ title: 'Android Logs',
+ description: `[${offset}, ${offset + count}] / ${total}`,
+ buttons: m(LogsFilters),
+ },
+ m(
+ VirtualScrollContainer,
+ {onScroll: this.onScroll},
+ m('.log-panel',
+ m('.rows', {style: {height: `${total * ROW_H}px`}}, rows)),
+ ),
+ );
}
renderCanvas() {}
diff --git a/ui/src/frontend/query_result_tab.ts b/ui/src/frontend/query_result_tab.ts
index e0eb644..7f370f6 100644
--- a/ui/src/frontend/query_result_tab.ts
+++ b/ui/src/frontend/query_result_tab.ts
@@ -105,6 +105,7 @@
return m(QueryTable, {
query: this.config.query,
resp: this.queryResponse,
+ fillParent: true,
onClose: () => closeTab(this.uuid),
contextButtons: [
this.sqlViewName === undefined ?
diff --git a/ui/src/frontend/query_table.ts b/ui/src/frontend/query_table.ts
index be864fc..ad75cb6 100644
--- a/ui/src/frontend/query_table.ts
+++ b/ui/src/frontend/query_table.ts
@@ -14,8 +14,8 @@
import m from 'mithril';
-import {BigintMath} from '../base/bigint_math';
+import {BigintMath} from '../base/bigint_math';
import {Actions} from '../common/actions';
import {QueryResponse} from '../common/queries';
import {Row} from '../common/query_result';
@@ -28,6 +28,9 @@
import {Router} from './router';
import {reveal} from './scroll_helper';
import {Button} from './widgets/button';
+import {Callout} from './widgets/callout';
+import {DetailsShell} from './widgets/details_shell';
+import {exists} from './widgets/utils';
interface QueryTableRowAttrs {
row: Row;
@@ -183,7 +186,8 @@
if (resp.error) {
return m('.query-error', `SQL error: ${resp.error}`);
} else {
- return m('table.query-table', m('thead', tableHeader), m('tbody', rows));
+ return m(
+ 'table.pf-query-table', m('thead', tableHeader), m('tbody', rows));
}
}
}
@@ -193,59 +197,81 @@
onClose: () => void;
resp?: QueryResponse;
contextButtons?: m.Child[];
+ fillParent: boolean;
}
export class QueryTable extends Panel<QueryTableAttrs> {
- view(vnode: m.CVnode<QueryTableAttrs>) {
- const resp = vnode.attrs.resp;
+ view({attrs}: m.CVnode<QueryTableAttrs>) {
+ const {
+ resp,
+ query,
+ onClose,
+ contextButtons = [],
+ fillParent,
+ } = attrs;
- const header: m.Child[] = [
- m('span',
- resp ? `Query result - ${Math.round(resp.durationMs)} ms` :
- `Query - running`),
- m('span.code.text-select', vnode.attrs.query),
- m('span.spacer'),
- ...(vnode.attrs.contextButtons ?? []),
+ return m(
+ DetailsShell,
+ {
+ title: this.renderTitle(resp),
+ description: query,
+ buttons: this.renderButtons(query, onClose, contextButtons, resp),
+ fillParent,
+ },
+ resp && this.renderTableContent(resp),
+ );
+ }
+
+ renderTitle(resp?: QueryResponse) {
+ if (exists(resp)) {
+ return `Query result - ${Math.round(resp.durationMs)} ms`;
+ } else {
+ return 'Query - running';
+ }
+ }
+
+ renderButtons(
+ query: string, onClose: () => void, contextButtons: m.Child[],
+ resp?: QueryResponse) {
+ return [
+ contextButtons,
m(Button, {
label: 'Copy query',
minimal: true,
onclick: () => {
- copyToClipboard(vnode.attrs.query);
+ copyToClipboard(query);
},
}),
+ (resp && resp.error === undefined) && m(Button, {
+ label: 'Copy result (.tsv)',
+ minimal: true,
+ onclick: () => {
+ queryResponseToClipboard(resp);
+ },
+ }),
+ m(Button, {
+ minimal: true,
+ label: 'Close',
+ onclick: onClose,
+ }),
];
- if (resp) {
- if (resp.error === undefined) {
- header.push(m(Button, {
- label: 'Copy result (.tsv)',
- minimal: true,
- onclick: () => {
- queryResponseToClipboard(resp);
- },
- }));
- }
- }
- header.push(m(Button, {
- label: 'Close',
- minimal: true,
- onclick: () => vnode.attrs.onClose(),
- }));
+ }
- const headers = [m('header.overview', ...header)];
-
- if (resp === undefined) {
- return headers;
- }
-
- if (resp.statementWithOutputCount > 1) {
- headers.push(
- m('header.overview',
- `${resp.statementWithOutputCount} out of ${resp.statementCount} ` +
- `statements returned a result. Only the results for the last ` +
- `statement are displayed in the table below.`));
- }
-
- return [...headers, m(QueryTableContent, {resp})];
+ renderTableContent(resp: QueryResponse) {
+ return m(
+ '.pf-query-panel',
+ resp.statementWithOutputCount > 1 &&
+ m('.pf-query-warning',
+ m(
+ Callout,
+ {icon: 'warning'},
+ `${resp.statementWithOutputCount} out of ${
+ resp.statementCount} `,
+ 'statements returned a result. ',
+ 'Only the results for the last statement are displayed.',
+ )),
+ m(QueryTableContent, {resp}),
+ );
}
renderCanvas() {}
diff --git a/ui/src/frontend/widgets/callout.ts b/ui/src/frontend/widgets/callout.ts
new file mode 100644
index 0000000..cd583af
--- /dev/null
+++ b/ui/src/frontend/widgets/callout.ts
@@ -0,0 +1,36 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+
+import {Icon} from './icon';
+
+interface CalloutAttrs {
+ icon?: string;
+ // Remaining attributes forwarded to the underlying HTML <button>.
+ [htmlAttrs: string]: any;
+}
+
+export class Callout implements m.ClassComponent<CalloutAttrs> {
+ view({attrs, children}: m.CVnode<CalloutAttrs>) {
+ const {icon, ...htmlAttrs} = attrs;
+
+ return m(
+ '.pf-callout',
+ {...htmlAttrs},
+ icon && m(Icon, {className: 'pf-left-icon', icon}),
+ children,
+ );
+ }
+}
diff --git a/ui/src/frontend/widgets/details_shell.ts b/ui/src/frontend/widgets/details_shell.ts
index 7fe684b..8b6aa41 100644
--- a/ui/src/frontend/widgets/details_shell.ts
+++ b/ui/src/frontend/widgets/details_shell.ts
@@ -19,10 +19,8 @@
title: m.Children;
description?: m.Children;
buttons?: m.Children;
- // If true, this container will fill the parent, and content scrolling is
- // expected to be handled internally.
- // Defaults to false.
- matchParent?: boolean;
+ // Stretch/shrink the content to fill the parent vertically.
+ fillParent?: boolean;
}
// A shell for details panels to be more visually consistent.
@@ -33,12 +31,12 @@
title,
description,
buttons,
- matchParent,
+ fillParent = true,
} = attrs;
return m(
'section.pf-details-shell',
- {class: classNames(matchParent && 'pf-match-parent')},
+ {class: classNames(fillParent && 'pf-fill-parent')},
m(
'header.pf-header-bar',
m('h1.pf-header-title', title),
diff --git a/ui/src/frontend/widgets/virtual_scroll_container.ts b/ui/src/frontend/widgets/virtual_scroll_container.ts
new file mode 100644
index 0000000..2e210ff
--- /dev/null
+++ b/ui/src/frontend/widgets/virtual_scroll_container.ts
@@ -0,0 +1,51 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+
+import {findRef, toHTMLElement} from './utils';
+
+interface VirtualScrollContainerAttrs {
+ // Called when the scrolling element is created, updates, or scrolls.
+ onScroll?: (dom: HTMLElement) => void;
+}
+
+export class VirtualScrollContainer implements
+ m.ClassComponent<VirtualScrollContainerAttrs> {
+ private readonly REF = 'virtual-scroll-container';
+ view({attrs, children}: m.Vnode<VirtualScrollContainerAttrs>) {
+ const {
+ onScroll = () => {},
+ } = attrs;
+
+ return m(
+ '.pf-virtual-scroll-container',
+ {
+ ref: this.REF,
+ onscroll: (e: Event) => onScroll(e.target as HTMLElement),
+ },
+ children);
+ }
+
+ oncreate({dom, attrs}: m.VnodeDOM<VirtualScrollContainerAttrs, this>) {
+ const {
+ onScroll = () => {},
+ } = attrs;
+
+ const element = findRef(dom, this.REF);
+ if (element) {
+ onScroll(toHTMLElement(element));
+ }
+ }
+}
diff --git a/ui/src/frontend/widgets_page.ts b/ui/src/frontend/widgets_page.ts
index e3fc2e3..ea0e632 100644
--- a/ui/src/frontend/widgets_page.ts
+++ b/ui/src/frontend/widgets_page.ts
@@ -23,6 +23,7 @@
import {Icons} from './semantic_icons';
import {TableShowcase} from './tables/table_showcase';
import {Button} from './widgets/button';
+import {Callout} from './widgets/callout';
import {Checkbox} from './widgets/checkbox';
import {EmptyState} from './widgets/empty_state';
import {Form, FormButtonBar, FormLabel} from './widgets/form';
@@ -649,6 +650,20 @@
}),
),
}),
+ m('h2', 'Callout'),
+ m(
+ WidgetShowcase, {
+ renderWidget: () => m(
+ Callout,
+ {
+ icon: 'info',
+ },
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' +
+ 'Nulla rhoncus tempor neque, sed malesuada eros dapibus vel. ' +
+ 'Aliquam in ligula vitae tortor porttitor laoreet iaculis ' +
+ 'finibus est.',
+ ),
+ }),
);
},
});