| // 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 {QueryResponse} from '../common/queries'; |
| import {Row} from '../common/query_result'; |
| import {fromNs} from '../common/time'; |
| |
| import {queryResponseToClipboard} from './clipboard'; |
| import {globals} from './globals'; |
| import {Panel} from './panel'; |
| import {Router} from './router'; |
| import { |
| horizontalScrollAndZoomToRange, |
| verticalScrollToTrack |
| } from './scroll_helper'; |
| |
| interface QueryTableRowAttrs { |
| row: Row; |
| columns: string[]; |
| } |
| |
| class QueryTableRow implements m.ClassComponent<QueryTableRowAttrs> { |
| static columnsContainsSliceLocation(columns: string[]) { |
| const requiredColumns = ['ts', 'dur', 'track_id']; |
| for (const col of requiredColumns) { |
| if (!columns.includes(col)) return false; |
| } |
| return true; |
| } |
| |
| static rowOnClickHandler( |
| event: Event, row: Row, nextTab: 'CurrentSelection'|'QueryResults') { |
| // TODO(dproy): Make click handler work from analyze page. |
| if (Router.parseUrl(window.location.href).page !== '/viewer') return; |
| // If the click bubbles up to the pan and zoom handler that will deselect |
| // the slice. |
| event.stopPropagation(); |
| |
| const sliceStart = fromNs(row.ts as number); |
| // row.dur can be negative. Clamp to 1ns. |
| const sliceDur = fromNs(Math.max(row.dur as number, 1)); |
| const sliceEnd = sliceStart + sliceDur; |
| const trackId = row.track_id as number; |
| const uiTrackId = globals.state.uiTrackIdByTraceTrackId[trackId]; |
| if (uiTrackId === undefined) return; |
| verticalScrollToTrack(uiTrackId, true); |
| horizontalScrollAndZoomToRange(sliceStart, sliceEnd); |
| let sliceId: number|undefined; |
| if (row.type?.toString().includes('slice')) { |
| sliceId = row.id as number | undefined; |
| } else { |
| sliceId = row.slice_id as number | undefined; |
| } |
| if (sliceId !== undefined) { |
| globals.makeSelection( |
| Actions.selectChromeSlice( |
| {id: sliceId, trackId: uiTrackId, table: 'slice'}), |
| nextTab === 'QueryResults' ? globals.state.currentTab : |
| 'current_selection'); |
| } |
| } |
| |
| view(vnode: m.Vnode<QueryTableRowAttrs>) { |
| const cells = []; |
| const {row, columns} = vnode.attrs; |
| for (const col of columns) { |
| cells.push(m('td', row[col])); |
| } |
| const containsSliceLocation = |
| QueryTableRow.columnsContainsSliceLocation(columns); |
| const maybeOnClick = containsSliceLocation ? |
| (e: Event) => QueryTableRow.rowOnClickHandler(e, row, 'QueryResults') : |
| null; |
| const maybeOnDblClick = containsSliceLocation ? |
| (e: Event) => |
| QueryTableRow.rowOnClickHandler(e, row, 'CurrentSelection') : |
| null; |
| return m( |
| 'tr', |
| { |
| onclick: maybeOnClick, |
| // TODO(altimin): Consider improving the logic here (e.g. delay?) to |
| // account for cases when dblclick fires late. |
| ondblclick: maybeOnDblClick, |
| 'clickable': containsSliceLocation |
| }, |
| cells); |
| } |
| } |
| |
| interface QueryTableAttrs { |
| queryId: string; |
| } |
| |
| export class QueryTable extends Panel<QueryTableAttrs> { |
| private previousResponse?: QueryResponse; |
| |
| onbeforeupdate(vnode: m.CVnode<QueryTableAttrs>) { |
| const {queryId} = vnode.attrs; |
| const resp = globals.queryResults.get(queryId) as QueryResponse; |
| const res = resp !== this.previousResponse; |
| return res; |
| } |
| |
| view(vnode: m.CVnode<QueryTableAttrs>) { |
| const {queryId} = vnode.attrs; |
| const resp = globals.queryResults.get(queryId) as QueryResponse; |
| if (resp === undefined) { |
| return m(''); |
| } |
| this.previousResponse = resp; |
| const cols = []; |
| for (const col of resp.columns) { |
| cols.push(m('td', col)); |
| } |
| const header = m('tr', cols); |
| |
| const rows = []; |
| for (let i = 0; i < resp.rows.length; i++) { |
| rows.push(m(QueryTableRow, {row: resp.rows[i], columns: resp.columns})); |
| } |
| |
| const headers = [ |
| m( |
| 'header.overview', |
| `Query result - ${Math.round(resp.durationMs)} ms`, |
| m('span.code', resp.query), |
| resp.error ? null : |
| m('button.query-ctrl', |
| { |
| onclick: () => { |
| queryResponseToClipboard(resp); |
| }, |
| }, |
| 'Copy as .tsv'), |
| m('button.query-ctrl', |
| { |
| onclick: () => { |
| globals.queryResults.delete(queryId); |
| globals.rafScheduler.scheduleFullRedraw(); |
| } |
| }, |
| 'Close'), |
| ), |
| ]; |
| |
| |
| 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 m( |
| 'div', |
| ...headers, |
| // TODO(rsavitski): the x-scrollable works for the |
| // dedicated query page, but is insufficient in the case of |
| // the results being presented within the bottom details |
| // pane in the timeline view. In that case, the |
| // details-panel-container enforces non-scrollability. |
| // Ideally we'd want to make that case scrollable as well. |
| resp.error ? |
| m('.query-error', `SQL error: ${resp.error}`) : |
| m('.query-table-container.x-scrollable', |
| m('table.query-table', m('thead', header), m('tbody', rows)))); |
| } |
| |
| renderCanvas() {} |
| } |