blob: 4fdc99b19b8cccaf81d9d417e3c8ae87497d92f9 [file] [log] [blame]
// Copyright (C) 2023 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 {sqliteString} from '../../base/string_utils';
import {Row, SqlValue} from '../../common/query_result';
import {formatDuration, TPTime} from '../../common/time';
import {Anchor} from '../anchor';
import {copyToClipboard} from '../clipboard';
import {Icons} from '../semantic_icons';
import {SliceRef} from '../sql/slice';
import {asSliceSqlId, asTPTimestamp} from '../sql_types';
import {sqlValueToString} from '../sql_utils';
import {Err} from '../widgets/error';
import {MenuItem, PopupMenu2} from '../widgets/menu';
import {renderTimecode} from '../widgets/timestamp';
import {Column} from './column';
import {SqlTableState} from './state';
import {SliceIdDisplayConfig} from './table_description';
// This file is responsible for rendering a value in a given sell based on the
// column type.
function filterOptionMenuItem(
label: string, filter: string, state: SqlTableState): m.Child {
return m(MenuItem, {
label,
onclick: () => {
state.addFilter(filter);
},
});
}
function getStandardFilters(
c: Column, value: SqlValue, state: SqlTableState): m.Child[] {
if (value === null) {
return [
filterOptionMenuItem('is null', `${c.expression} is null`, state),
filterOptionMenuItem('is not null', `${c.expression} is not null`, state),
];
}
if (typeof value === 'string') {
return [
filterOptionMenuItem(
'equals to', `${c.expression} = ${sqliteString(value)}`, state),
filterOptionMenuItem(
'not equals to', `${c.expression} != ${sqliteString(value)}`, state),
];
}
if (typeof value === 'bigint' || typeof value === 'number') {
return [
filterOptionMenuItem('equals to', `${c.expression} = ${value}`, state),
filterOptionMenuItem(
'not equals to', `${c.expression} != ${value}`, state),
filterOptionMenuItem('greater than', `${c.expression} > ${value}`, state),
filterOptionMenuItem(
'greater or equals than', `${c.expression} >= ${value}`, state),
filterOptionMenuItem('less than', `${c.expression} < ${value}`, state),
filterOptionMenuItem(
'less or equals than', `${c.expression} <= ${value}`, state),
];
}
return [];
}
function displayValue(value: SqlValue): m.Child {
if (value === null) {
return m('i', 'NULL');
}
return sqlValueToString(value);
}
function displayTimestamp(value: SqlValue): m.Children {
if (typeof value !== 'bigint') return displayValue(value);
return renderTimecode(asTPTimestamp(value));
}
function displayDuration(value: TPTime): string;
function displayDuration(value: SqlValue): m.Children;
function displayDuration(value: SqlValue): m.Children {
if (typeof value !== 'bigint') return displayValue(value);
return formatDuration(value);
}
function display(column: Column, row: Row): m.Children {
const value = row[column.alias];
// Handle all cases when we have non-trivial formatting.
switch (column.display?.type) {
case 'timestamp':
return displayTimestamp(value);
case 'duration':
case 'thread_duration':
return displayDuration(value);
}
return displayValue(value);
}
function copyMenuItem(label: string, value: string): m.Child {
return m(MenuItem, {
icon: Icons.Copy,
label,
onclick: () => {
copyToClipboard(value);
},
});
}
function getContextMenuItems(
column: Column, row: Row, state: SqlTableState): m.Child[] {
const result: m.Child[] = [];
const value = row[column.alias];
if (column.display?.type === 'timestamp' && typeof value === 'bigint') {
result.push(copyMenuItem('Copy raw timestamp', `${value}`));
// result.push(
// copyMenuItem('Copy formatted timestamp', displayTimestamp(value)));
}
if ((column.display?.type === 'duration' ||
column.display?.type === 'thread_duration') &&
typeof value === 'bigint') {
result.push(copyMenuItem('Copy raw duration', `${value}`));
result.push(
copyMenuItem('Copy formatted duration', displayDuration(value)));
}
if (typeof value === 'string') {
result.push(copyMenuItem('Copy', value));
}
const filters = getStandardFilters(column, value, state);
if (filters.length > 0) {
result.push(
m(MenuItem, {label: 'Add filter', icon: Icons.Filter}, ...filters));
}
return result;
}
function renderSliceIdColumn(
column: {alias: string, display: SliceIdDisplayConfig},
row: Row): m.Children {
const config = column.display;
const id = row[column.alias];
const ts = row[config.ts];
const dur = row[config.dur] === null ? -1n : row[config.dur];
const trackId = row[config.trackId];
const columnNotFoundError = (type: string, name: string) =>
m(Err, `${type} column ${name} not found`);
const wrongTypeError = (type: string, name: string, value: SqlValue) =>
m(Err,
`Wrong type for ${type} column ${name}: bigint expected, ${
typeof value} found`);
if (typeof id !== 'bigint') return sqlValueToString(id);
if (ts === undefined) return columnNotFoundError('Timestamp', config.ts);
if (typeof ts !== 'bigint') return wrongTypeError('timestamp', config.ts, ts);
if (dur === undefined) return columnNotFoundError('Duration', config.dur);
if (typeof dur !== 'bigint') {
return wrongTypeError('duration', config.dur, ts);
}
if (trackId === undefined) return columnNotFoundError('Track id', trackId);
if (typeof trackId !== 'bigint') {
return wrongTypeError('track id', config.trackId, trackId);
}
return m(SliceRef, {
id: asSliceSqlId(Number(id)),
name: `${id}`,
ts: asTPTimestamp(ts as bigint),
dur: dur,
sqlTrackId: Number(trackId),
switchToCurrentSelectionTab: false,
});
}
export function renderCell(
column: Column, row: Row, state: SqlTableState): m.Children {
if (column.display && column.display.type === 'slice_id') {
return renderSliceIdColumn(
{alias: column.alias, display: column.display}, row);
}
const displayValue = display(column, row);
const contextMenuItems: m.Child[] = getContextMenuItems(column, row, state);
return m(
PopupMenu2,
{
trigger: m(Anchor, displayValue),
},
...contextMenuItems,
);
}