| // 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 {copyToClipboard} from '../../base/clipboard'; |
| import {isString} from '../../base/object_utils'; |
| import {Icons} from '../../base/semantic_icons'; |
| import {sqliteString} from '../../base/string_utils'; |
| import {Duration, Time} from '../../base/time'; |
| import {Row, SqlValue} from '../../trace_processor/query_result'; |
| import {Anchor} from '../../widgets/anchor'; |
| import {renderError} from '../../widgets/error'; |
| import {MenuItem, PopupMenu2} from '../../widgets/menu'; |
| import {SliceRef} from '../sql/slice'; |
| import {asSliceSqlId} from '../sql_types'; |
| import {sqlValueToString} from '../sql_utils'; |
| import {DurationWidget} from '../widgets/duration'; |
| import {Timestamp} 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 (isString(value)) { |
| 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 display(column: Column, row: Row): m.Children { |
| const value = row[column.alias]; |
| 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 (isString(value)) { |
| 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 renderStandardColumn( |
| column: Column, |
| row: Row, |
| state: SqlTableState, |
| ): m.Children { |
| const displayValue = display(column, row); |
| const contextMenuItems: m.Child[] = getContextMenuItems(column, row, state); |
| return m( |
| PopupMenu2, |
| { |
| trigger: m(Anchor, displayValue), |
| }, |
| ...contextMenuItems, |
| ); |
| } |
| |
| function renderTimestampColumn( |
| column: Column, |
| row: Row, |
| state: SqlTableState, |
| ): m.Children { |
| const value = row[column.alias]; |
| if (typeof value !== 'bigint') { |
| return renderStandardColumn(column, row, state); |
| } |
| |
| return m(Timestamp, { |
| ts: Time.fromRaw(value), |
| extraMenuItems: getContextMenuItems(column, row, state), |
| }); |
| } |
| |
| function renderDurationColumn( |
| column: Column, |
| row: Row, |
| state: SqlTableState, |
| ): m.Children { |
| const value = row[column.alias]; |
| if (typeof value !== 'bigint') { |
| return renderStandardColumn(column, row, state); |
| } |
| |
| return m(DurationWidget, { |
| dur: Duration.fromRaw(value), |
| extraMenuItems: getContextMenuItems(column, row, state), |
| }); |
| } |
| |
| 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) => |
| renderError(`${type} column ${name} not found`); |
| const wrongTypeError = (type: string, name: string, value: SqlValue) => |
| renderError( |
| `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: Time.fromRaw(ts), |
| dur: dur, |
| sqlTrackId: Number(trackId), |
| switchToCurrentSelectionTab: false, |
| }); |
| } |
| |
| export function renderCell( |
| column: Column, |
| row: Row, |
| state: SqlTableState, |
| ): m.Children { |
| if (column.display) { |
| switch (column.display?.type) { |
| case 'slice_id': |
| return renderSliceIdColumn( |
| {alias: column.alias, display: column.display}, |
| row, |
| ); |
| case 'timestamp': |
| return renderTimestampColumn(column, row, state); |
| case 'duration': |
| case 'thread_duration': |
| return renderDurationColumn(column, row, state); |
| } |
| } |
| return renderStandardColumn(column, row, state); |
| } |