blob: fc2eb7f90fb78a126b7c93903b6b323448aa6324 [file] [log] [blame]
// 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() {}
}