1// Copyright (C) 2020 The Android Open Source Project
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
16import * as m from 'mithril';
17
18import {Actions} from '../common/actions';
19import {Row} from '../common/protos';
20import {QueryResponse} from '../common/queries';
21import {fromNs} from '../common/time';
22
23import {copyToClipboard} from './clipboard';
24import {globals} from './globals';
25import {Panel} from './panel';
26import {
27  findUiTrackId,
28  horizontalScrollAndZoomToRange,
29  verticalScrollToTrack
30} from './scroll_helper';
31
32interface QueryTableRowAttrs {
33  row: Row;
34  columns: string[];
35}
36
37class QueryTableRow implements m.ClassComponent<QueryTableRowAttrs> {
38  static columnsContainsSliceLocation(columns: string[]) {
39    const requiredColumns = ['ts', 'dur', 'track_id'];
40    for (const col of requiredColumns) {
41      if (!columns.includes(col)) return false;
42    }
43    return true;
44  }
45
46  static rowOnClickHandler(
47      event: Event, row: Row, nextTab: 'CurrentSelection'|'QueryResults') {
48    // TODO(dproy): Make click handler work from analyze page.
49    if (globals.state.route !== '/viewer') return;
50    // If the click bubbles up to the pan and zoom handler that will deselect
51    // the slice.
52    event.stopPropagation();
53
54    const sliceStart = fromNs(row.ts as number);
55    // row.dur can be negative. Clamp to 1ns.
56    const sliceDur = fromNs(Math.max(row.dur as number, 1));
57    const sliceEnd = sliceStart + sliceDur;
58    const trackId = row.track_id as number;
59    const uiTrackId = findUiTrackId(trackId);
60    if (uiTrackId === null) return;
61    verticalScrollToTrack(uiTrackId, true);
62    horizontalScrollAndZoomToRange(sliceStart, sliceEnd);
63    let sliceId: number|undefined;
64    if (row.type?.toString().includes('slice')) {
65      sliceId = row.id as number | undefined;
66    } else {
67      sliceId = row.slice_id as number | undefined;
68    }
69    if (sliceId !== undefined) {
70      globals.makeSelection(
71          Actions.selectChromeSlice(
72              {id: sliceId, trackId: uiTrackId, table: 'slice'}),
73          nextTab === 'QueryResults' ? globals.frontendLocalState.currentTab :
74                                       'current_selection');
75    }
76  }
77
78  view(vnode: m.Vnode<QueryTableRowAttrs>) {
79    const cells = [];
80    const {row, columns} = vnode.attrs;
81    for (const col of columns) {
82      cells.push(m('td', row[col]));
83    }
84    const containsSliceLocation =
85        QueryTableRow.columnsContainsSliceLocation(columns);
86    const maybeOnClick = containsSliceLocation ?
87        (e: Event) => QueryTableRow.rowOnClickHandler(e, row, 'QueryResults') :
88        null;
89    const maybeOnDblClick = containsSliceLocation ?
90        (e: Event) =>
91            QueryTableRow.rowOnClickHandler(e, row, 'CurrentSelection') :
92        null;
93    return m(
94        'tr',
95        {
96          onclick: maybeOnClick,
97          // TODO(altimin): Consider improving the logic here (e.g. delay?) to
98          // account for cases when dblclick fires late.
99          ondblclick: maybeOnDblClick,
100          'clickable': containsSliceLocation
101        },
102        cells);
103  }
104}
105
106interface QueryTableAttrs {
107  queryId: string;
108}
109
110export class QueryTable extends Panel<QueryTableAttrs> {
111  private previousResponse?: QueryResponse;
112
113  onbeforeupdate(vnode: m.CVnode<QueryTableAttrs>) {
114    const {queryId} = vnode.attrs;
115    const resp = globals.queryResults.get(queryId) as QueryResponse;
116    const res = resp !== this.previousResponse;
117    return res;
118  }
119
120  view(vnode: m.CVnode<QueryTableAttrs>) {
121    const {queryId} = vnode.attrs;
122    const resp = globals.queryResults.get(queryId) as QueryResponse;
123    if (resp === undefined) {
124      return m('');
125    }
126    this.previousResponse = resp;
127    const cols = [];
128    for (const col of resp.columns) {
129      cols.push(m('td', col));
130    }
131    const header = m('tr', cols);
132
133    const rows = [];
134    for (let i = 0; i < resp.rows.length; i++) {
135      rows.push(m(QueryTableRow, {row: resp.rows[i], columns: resp.columns}));
136    }
137
138    return m(
139        'div',
140        m(
141            'header.overview',
142            `Query result - ${Math.round(resp.durationMs)} ms`,
143            m('span.code', resp.query),
144            resp.error ?
145                null :
146                m('button.query-ctrl',
147                  {
148                    onclick: () => {
149                      const lines: string[][] = [];
150                      lines.push(resp.columns);
151                      for (const row of resp.rows) {
152                        const line = [];
153                        for (const col of resp.columns) {
154                          line.push(row[col].toString());
155                        }
156                        lines.push(line);
157                      }
158                      copyToClipboard(
159                          lines.map(line => line.join('\t')).join('\n'));
160                    },
161                  },
162                  'Copy as .tsv'),
163            m('button.query-ctrl',
164              {
165                onclick: () => {
166                  globals.queryResults.delete(queryId);
167                  globals.rafScheduler.scheduleFullRedraw();
168                }
169              },
170              'Close'),
171            ),
172        resp.error ?
173            m('.query-error', `SQL error: ${resp.error}`) :
174            m('.query-table-container',
175              m('table.query-table', m('thead', header), m('tbody', rows))));
176  }
177
178  renderCanvas() {}
179}
180