Merge "ui: Introduce analyze page with multiline SQL input box"
diff --git a/ui/src/assets/analyze_page.scss b/ui/src/assets/analyze_page.scss
new file mode 100644
index 0000000..bbfed10
--- /dev/null
+++ b/ui/src/assets/analyze_page.scss
@@ -0,0 +1,33 @@
+// Copyright (C) 2020 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.
+
+.analyze-page {
+ overflow-y: auto;
+ overflow-x: hidden;
+ .query-input {
+ width: 100%;
+ background-color: #111;
+ // When the user resizes the text box, the height is explicitly
+ // set as an inline style with overrides this style.
+ min-height: 2em;
+ height: var(--height-before-resize);
+ color: rgb(157, 220, 103);
+ font-size: inherit;
+ font-family: var(--monospace-font);
+ line-height: 1.2em;
+ padding: .5em;
+ overflow: auto;
+ resize: vertical;
+ }
+}
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index 6114874..16fe564 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -231,6 +231,11 @@
color: #333;
}
+.query-table-container {
+ width: 100%;
+ overflow-x: auto;
+}
+
.query-table {
width: 100%;
border-collapse: collapse;
@@ -275,25 +280,6 @@
font-weight: 300;
}
-.page header {
- background-color: hsl(213, 22%, 82%);
- color: hsl(213, 22%, 20%);
- font-family: 'Roboto Condensed', sans-serif;
- font-size: 15px;
- font-weight: 400;
- padding: 4px 10px;
- vertical-align: middle;
- border-color: hsl(213, 22%, 75%);
- border-style: solid;
- border-width: 1px 0;
- .code {
- font-family: var(--monospace-font);
- font-size: 12px;
- margin-left: 10px;
- color: hsl(213, 22%, 40%);
- }
-}
-
.track {
display: grid;
grid-template-columns: auto 1fr;
@@ -454,7 +440,22 @@
header.overview {
display: flex;
align-content: center;
-
+ background-color: hsl(213, 22%, 82%);
+ color: hsl(213, 22%, 20%);
+ font-family: 'Roboto Condensed', sans-serif;
+ font-size: 15px;
+ font-weight: 400;
+ padding: 4px 10px;
+ vertical-align: middle;
+ border-color: hsl(213, 22%, 75%);
+ border-style: solid;
+ border-width: 1px 0;
+ .code {
+ font-family: var(--monospace-font);
+ font-size: 12px;
+ margin-left: 10px;
+ color: hsl(213, 22%, 40%);
+ }
span.code {
user-select: text;
flex-grow: 1;
diff --git a/ui/src/assets/perfetto.scss b/ui/src/assets/perfetto.scss
index 67c0f5e..f08e774 100644
--- a/ui/src/assets/perfetto.scss
+++ b/ui/src/assets/perfetto.scss
@@ -14,6 +14,7 @@
@import 'typefaces';
@import 'common';
+@import 'analyze_page';
@import 'sidebar';
@import 'topbar';
@import 'record';
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index ccb4bf3..0909751 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -611,6 +611,10 @@
state.recordingStatus = args.status;
state.lastRecordingError = undefined;
},
+
+ setAnalyzePageQuery(state: StateDraft, args: {query: string}): void {
+ state.analyzePageQuery = args.query;
+ }
};
// When we are on the frontend side, we don't really want to execute the
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index b61218d..5d06148 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -292,6 +292,7 @@
recordingStatus?: string;
chromeCategories: string[]|undefined;
+ analyzePageQuery?: string;
}
export const defaultTraceTime = {
diff --git a/ui/src/frontend/analyze_page.ts b/ui/src/frontend/analyze_page.ts
new file mode 100644
index 0000000..2585ebc
--- /dev/null
+++ b/ui/src/frontend/analyze_page.ts
@@ -0,0 +1,117 @@
+// Copyright (C) 2020 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 * as m from 'mithril';
+
+import {Actions} from '../common/actions';
+
+import {globals} from './globals';
+import {createPage} from './pages';
+import {QueryTable} from './query_table';
+
+const INPUT_PLACEHOLDER = 'Enter query and press Cmd/Ctrl + Enter';
+const INPUT_MIN_LINES = 2;
+const INPUT_MAX_LINES = 10;
+const INPUT_LINE_HEIGHT_EM = 1.2;
+const TAB_SPACES = 2;
+const QUERY_ID = 'analyze-page-query';
+
+class QueryInput implements m.ClassComponent {
+ // How many lines to display if the user hasn't resized the input box.
+ displayLines = INPUT_MIN_LINES;
+
+ static onKeyDown(e: Event) {
+ const event = e as KeyboardEvent;
+ const target = e.target as HTMLTextAreaElement;
+
+ if (event.code === 'Enter' && (event.metaKey || event.ctrlKey)) {
+ event.preventDefault();
+ const query = target.value;
+ if (!query) return;
+ globals.dispatch(
+ Actions.executeQuery({engineId: '0', queryId: QUERY_ID, query}));
+ }
+
+ if (event.code === 'Tab') {
+ // Handle tabs to insert spaces.
+ event.preventDefault();
+ const whitespace = ' '.repeat(TAB_SPACES);
+ const {selectionStart, selectionEnd} = target;
+ target.value = target.value.substring(0, selectionStart) + whitespace +
+ target.value.substring(selectionEnd);
+ target.selectionEnd = selectionStart + TAB_SPACES;
+ }
+ }
+
+ onInput(textareaValue: string) {
+ const textareaLines = textareaValue.split('\n').length;
+ const clampedNumLines =
+ Math.min(Math.max(textareaLines, INPUT_MIN_LINES), INPUT_MAX_LINES);
+ this.displayLines = clampedNumLines;
+ globals.dispatch(Actions.setAnalyzePageQuery({query: textareaValue}));
+ globals.rafScheduler.scheduleFullRedraw();
+ }
+
+ // This method exists because unfortunatley setting custom properties on an
+ // element's inline style attribue doesn't seem to work in mithril, even
+ // though the docs claim so.
+ setHeightBeforeResize(node: HTMLElement) {
+ // +2em for some extra breathing space to account for padding.
+ const heightEm = this.displayLines * INPUT_LINE_HEIGHT_EM + 2;
+ // We set a height based on the number of lines that we want to display by
+ // default. If the user resizes the textbox using the resize handle in the
+ // bottom-right corner, this height is overridden.
+ node.style.setProperty('--height-before-resize', `${heightEm}em`);
+ // TODO(dproy): The resized height is lost if user navigates away from the
+ // page and comes back.
+ }
+
+ oncreate(vnode: m.VnodeDOM) {
+ // This makes sure query persists if user navigates to other pages and comes
+ // back to analyze page.
+ const existingQuery = globals.state.analyzePageQuery;
+ const textarea = vnode.dom as HTMLTextAreaElement;
+ if (existingQuery) {
+ textarea.value = existingQuery;
+ this.onInput(existingQuery);
+ }
+
+ this.setHeightBeforeResize(textarea);
+ }
+
+ onupdate(vnode: m.VnodeDOM) {
+ this.setHeightBeforeResize(vnode.dom as HTMLElement);
+ }
+
+ view() {
+ return m('textarea.query-input', {
+ placeholder: INPUT_PLACEHOLDER,
+ onkeydown: (e: Event) => QueryInput.onKeyDown(e),
+ oninput: (e: Event) =>
+ this.onInput((e.target as HTMLTextAreaElement).value),
+ });
+ }
+}
+
+
+export const AnalyzePage = createPage({
+ view() {
+ return m(
+ '.analyze-page',
+ m(QueryInput),
+ m(QueryTable, {queryId: QUERY_ID}),
+ );
+ }
+});
diff --git a/ui/src/frontend/header_panel.ts b/ui/src/frontend/header_panel.ts
deleted file mode 100644
index 8b6d4bf..0000000
--- a/ui/src/frontend/header_panel.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (C) 2018 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 * as m from 'mithril';
-import {Panel} from './panel';
-
-interface Attrs {
- title: string;
-}
-
-export class HeaderPanel extends Panel<Attrs> {
- renderCanvas() {}
- view({attrs}: m.CVnode<Attrs>) {
- return m('header', attrs.title);
- }
-}
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 7823f28..c78a143 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -30,6 +30,7 @@
} from '../common/logs';
import {CurrentSearchResults, SearchSummary} from '../common/search_data';
+import {AnalyzePage} from './analyze_page';
import {maybeShowErrorDialog} from './error_dialog';
import {
CounterDetails,
@@ -247,6 +248,7 @@
'/': HomePage,
'/viewer': ViewerPage,
'/record': RecordPage,
+ '/analyze': AnalyzePage,
},
dispatch);
forwardRemoteCalls(frontendChannel.port2, new FrontendApi(router));
diff --git a/ui/src/frontend/query_table.ts b/ui/src/frontend/query_table.ts
index 329fc70..078a70c 100644
--- a/ui/src/frontend/query_table.ts
+++ b/ui/src/frontend/query_table.ts
@@ -160,7 +160,8 @@
),
resp.error ?
m('.query-error', `SQL error: ${resp.error}`) :
- m('table.query-table', m('thead', header), m('tbody', rows)));
+ m('.query-table-container',
+ m('table.query-table', m('thead', header), m('tbody', rows))));
}
renderCanvas() {}
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index 0223406..92bec9e 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -154,6 +154,7 @@
checkDownloadDisabled: true,
},
{t: 'Legacy UI', a: openCurrentTraceWithOldUI, i: 'filter_none'},
+ {t: 'Analyze', a: navigateAnalyze, i: 'control_camera'},
],
},
{
@@ -423,6 +424,11 @@
globals.dispatch(Actions.navigate({route: '/record'}));
}
+function navigateAnalyze(e: Event) {
+ e.preventDefault();
+ globals.dispatch(Actions.navigate({route: '/analyze'}));
+}
+
function navigateViewer(e: Event) {
e.preventDefault();
globals.dispatch(Actions.navigate({route: '/viewer'}));