blob: c27ecc29a509533cfaced014b0eb3712a4cacfde [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 m from 'mithril';
import {Actions} from '../common/actions';
import {QueryResponse} from '../common/queries';
import {ColumnType, Row} from '../common/query_result';
import {fromNs} from '../common/time';
import {Anchor} from './anchor';
import {copyToClipboard, queryResponseToClipboard} from './clipboard';
import {downloadData} from './download_utils';
import {globals} from './globals';
import {Panel} from './panel';
import {Router} from './router';
import {
focusHorizontalRange,
verticalScrollToTrack,
} from './scroll_helper';
import {Button} from './widgets/button';
interface QueryTableRowAttrs {
row: Row;
columns: string[];
}
// Convert column value to number if it's a bigint or a number, otherwise throw
function colToNumber(colValue: ColumnType): number {
if (typeof colValue === 'bigint') {
return Number(colValue);
} else if (typeof colValue === 'number') {
return colValue;
} else {
throw Error('Value is not a number or a bigint');
}
}
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(colToNumber(row.ts));
// row.dur can be negative. Clamp to 1ns.
const sliceDur = fromNs(Math.max(colToNumber(row.dur), 1));
const sliceEnd = sliceStart + sliceDur;
const trackId: number = colToNumber(row.track_id);
const uiTrackId = globals.state.uiTrackIdByTraceTrackId[trackId];
if (uiTrackId === undefined) return;
verticalScrollToTrack(uiTrackId, true);
// TODO(stevegolton) Soon this function will only accept Bigints
focusHorizontalRange(sliceStart, sliceEnd);
let sliceId: number|undefined;
if (row.type?.toString().includes('slice')) {
sliceId = colToNumber(row.id);
} else {
sliceId = colToNumber(row.slice_id);
}
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) {
const value = row[col];
if (value instanceof Uint8Array) {
cells.push(
m('td',
m(Anchor,
{
onclick: () => downloadData(`${col}.blob`, value),
},
`Blob (${value.length} bytes)`)));
} else if (typeof value === 'bigint') {
cells.push(m('td', value.toString()));
} else {
cells.push(m('td', value));
}
}
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 QueryTableContentAttrs {
resp: QueryResponse;
}
class QueryTableContent implements m.ClassComponent<QueryTableContentAttrs> {
private previousResponse?: QueryResponse;
onbeforeupdate(vnode: m.CVnode<QueryTableContentAttrs>) {
return vnode.attrs.resp !== this.previousResponse;
}
view(vnode: m.CVnode<QueryTableContentAttrs>) {
const resp = vnode.attrs.resp;
this.previousResponse = resp;
const cols = [];
for (const col of resp.columns) {
cols.push(m('td', col));
}
const tableHeader = m('tr', cols);
const rows =
resp.rows.map((row) => m(QueryTableRow, {row, columns: resp.columns}));
if (resp.error) {
return m('.query-error', `SQL error: ${resp.error}`);
} else {
return m(
'.query-table-container.x-scrollable',
m('table.query-table', m('thead', tableHeader), m('tbody', rows)));
}
}
}
interface QueryTableAttrs {
query: string;
onClose: () => void;
resp?: QueryResponse;
contextButtons?: m.Child[];
}
export class QueryTable extends Panel<QueryTableAttrs> {
view(vnode: m.CVnode<QueryTableAttrs>) {
const resp = vnode.attrs.resp;
const header: m.Child[] = [
m('span',
resp ? `Query result - ${Math.round(resp.durationMs)} ms` :
`Query - running`),
m('span.code.text-select', vnode.attrs.query),
m('span.spacer'),
...(vnode.attrs.contextButtons ?? []),
m(Button, {
label: 'Copy query',
minimal: true,
onclick: () => {
copyToClipboard(vnode.attrs.query);
},
}),
];
if (resp) {
if (resp.error === undefined) {
header.push(m(Button, {
label: 'Copy result (.tsv)',
minimal: true,
onclick: () => {
queryResponseToClipboard(resp);
},
}));
}
}
header.push(m(Button, {
label: 'Close',
minimal: true,
onclick: () => vnode.attrs.onClose(),
}));
const headers = [m('header.overview', ...header)];
if (resp === undefined) {
return m('div', ...headers);
}
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, m(QueryTableContent, {resp}));
}
renderCanvas() {}
}