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'}));