blob: 9212720e558d7e51a6d39ec45b61d2d608e74f3c [file] [log] [blame]
// Copyright (C) 2019 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 {QueryResponse} from 'src/common/queries';
import {Actions} from '../common/actions';
import {LogExists, LogExistsKey} from '../common/logs';
import {DEFAULT_PIVOT_TABLE_ID} from '../common/pivot_table_common';
import {AggregationPanel} from './aggregation_panel';
import {ChromeSliceDetailsPanel} from './chrome_slice_panel';
import {CounterDetailsPanel} from './counter_panel';
import {CpuProfileDetailsPanel} from './cpu_profile_panel';
import {DragGestureHandler} from './drag_gesture_handler';
import {
FlowEventsAreaSelectedPanel,
FlowEventsPanel
} from './flow_events_panel';
import {globals} from './globals';
import {HeapProfileDetailsPanel} from './heap_profile_panel';
import {LogPanel} from './logs_panel';
import {showModal} from './modal';
import {NotesEditorPanel} from './notes_panel';
import {AnyAttrsVnode, PanelContainer} from './panel_container';
import {PivotTable} from './pivot_table';
import {ColumnDisplay, ColumnPicker} from './pivot_table_editor';
import {PivotTableHelper} from './pivot_table_helper';
import {QueryTable} from './query_table';
import {SliceDetailsPanel} from './slice_panel';
import {ThreadStatePanel} from './thread_state_panel';
const UP_ICON = 'keyboard_arrow_up';
const DOWN_ICON = 'keyboard_arrow_down';
const DRAG_HANDLE_HEIGHT_PX = 28;
const DEFAULT_DETAILS_HEIGHT_PX = 230 + DRAG_HANDLE_HEIGHT_PX;
function getFullScreenHeight() {
const panelContainer =
document.querySelector('.pan-and-zoom-content') as HTMLElement;
if (panelContainer !== null) {
return panelContainer.clientHeight;
} else {
return DEFAULT_DETAILS_HEIGHT_PX;
}
}
function hasLogs(): boolean {
const data = globals.trackDataStore.get(LogExistsKey) as LogExists;
return data && data.exists;
}
function showPivotTableEditorModal(helper?: PivotTableHelper) {
if (helper !== undefined && helper.editPivotTableModalOpen) {
let content;
if (helper.availableColumns.length === 0 ||
helper.availableAggregations.length === 0) {
content =
m('.pivot-table-editor-container',
helper.availableColumns.length === 0 ?
m('div', 'No columns available.') :
null,
helper.availableAggregations.length === 0 ?
m('div', 'No aggregations available.') :
null);
} else {
const attrs = {helper};
content =
m('.pivot-table-editor-container',
m(ColumnPicker, attrs),
m(ColumnDisplay, attrs));
}
showModal({
title: 'Edit Pivot Table',
content,
buttons: [],
}).finally(() => {
helper.toggleEditPivotTableModal();
globals.rafScheduler.scheduleFullRedraw();
});
}
}
interface Tab {
key: string;
name: string;
}
interface DragHandleAttrs {
height: number;
resize: (height: number) => void;
tabs: Tab[];
}
class DragHandle implements m.ClassComponent<DragHandleAttrs> {
private dragStartHeight = 0;
private height = 0;
private previousHeight = this.height;
private resize: (height: number) => void = () => {};
private isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX;
private isFullscreen = false;
// We can't get real fullscreen height until the pan_and_zoom_handler exists.
private fullscreenHeight = DEFAULT_DETAILS_HEIGHT_PX;
oncreate({dom, attrs}: m.CVnodeDOM<DragHandleAttrs>) {
this.resize = attrs.resize;
this.height = attrs.height;
this.isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX;
this.fullscreenHeight = getFullScreenHeight();
const elem = dom as HTMLElement;
new DragGestureHandler(
elem,
this.onDrag.bind(this),
this.onDragStart.bind(this),
this.onDragEnd.bind(this));
}
onupdate({attrs}: m.CVnodeDOM<DragHandleAttrs>) {
this.resize = attrs.resize;
this.height = attrs.height;
this.isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX;
}
onDrag(_x: number, y: number) {
const newHeight =
Math.floor(this.dragStartHeight + (DRAG_HANDLE_HEIGHT_PX / 2) - y);
this.isClosed = newHeight <= DRAG_HANDLE_HEIGHT_PX;
this.isFullscreen = newHeight >= this.fullscreenHeight;
this.resize(newHeight);
globals.rafScheduler.scheduleFullRedraw();
}
onDragStart(_x: number, _y: number) {
this.dragStartHeight = this.height;
}
onDragEnd() {}
view({attrs}: m.CVnode<DragHandleAttrs>) {
const icon = this.isClosed ? UP_ICON : DOWN_ICON;
const title = this.isClosed ? 'Show panel' : 'Hide panel';
const activeTabExists = globals.state.currentTab &&
attrs.tabs.map(tab => tab.key).includes(globals.state.currentTab);
if (!activeTabExists) {
globals.dispatch(Actions.setCurrentTab({tab: undefined}));
}
const renderTab = (tab: Tab) => {
if (globals.state.currentTab === tab.key ||
globals.state.currentTab === undefined &&
attrs.tabs[0].key === tab.key) {
// Update currentTab in case we didn't have one before.
globals.dispatch(Actions.setCurrentTab({tab: tab.key}));
return m('.tab[active]', tab.name);
}
return m(
'.tab',
{
onclick: () => {
globals.dispatch(Actions.setCurrentTab({tab: tab.key}));
}
},
tab.name);
};
return m(
'.handle',
m('.tabs', attrs.tabs.map(renderTab)),
m('.buttons',
m('i.material-icons',
{
onclick: () => {
this.isClosed = false;
this.isFullscreen = true;
this.resize(this.fullscreenHeight);
globals.rafScheduler.scheduleFullRedraw();
},
title: 'Open fullscreen',
disabled: this.isFullscreen
},
'vertical_align_top'),
m('i.material-icons',
{
onclick: () => {
if (this.height === DRAG_HANDLE_HEIGHT_PX) {
this.isClosed = false;
if (this.previousHeight === 0) {
this.previousHeight = DEFAULT_DETAILS_HEIGHT_PX;
}
this.resize(this.previousHeight);
} else {
this.isFullscreen = false;
this.isClosed = true;
this.previousHeight = this.height;
this.resize(DRAG_HANDLE_HEIGHT_PX);
}
globals.rafScheduler.scheduleFullRedraw();
},
title
},
icon)));
}
}
export class DetailsPanel implements m.ClassComponent {
private detailsHeight = DEFAULT_DETAILS_HEIGHT_PX;
// Used to set details panel to default height on selection.
private showDetailsPanel = true;
view() {
interface DetailsPanel {
key: string;
name: string;
vnode: AnyAttrsVnode;
}
const detailsPanels: DetailsPanel[] = [];
const curSelection = globals.state.currentSelection;
if (curSelection) {
switch (curSelection.kind) {
case 'NOTE':
detailsPanels.push({
key: 'current_selection',
name: 'Current Selection',
vnode: m(NotesEditorPanel, {
key: 'notes',
id: curSelection.id,
})
});
break;
case 'AREA':
if (curSelection.noteId !== undefined) {
detailsPanels.push({
key: 'current_selection',
name: 'Current Selection',
vnode: m(NotesEditorPanel, {
key: 'area_notes',
id: curSelection.noteId,
})
});
}
break;
case 'SLICE':
detailsPanels.push({
key: 'current_selection',
name: 'Current Selection',
vnode: m(SliceDetailsPanel, {
key: 'slice',
})
});
break;
case 'COUNTER':
detailsPanels.push({
key: 'current_selection',
name: 'Current Selection',
vnode: m(CounterDetailsPanel, {
key: 'counter',
})
});
break;
case 'PERF_SAMPLES':
case 'HEAP_PROFILE':
detailsPanels.push({
key: 'current_selection',
name: 'Current Selection',
vnode: m(HeapProfileDetailsPanel, {key: 'heap_profile'})
});
break;
case 'CPU_PROFILE_SAMPLE':
detailsPanels.push({
key: 'current_selection',
name: 'Current Selection',
vnode: m(CpuProfileDetailsPanel, {
key: 'cpu_profile_sample',
})
});
break;
case 'CHROME_SLICE':
detailsPanels.push({
key: 'current_selection',
name: 'Current Selection',
vnode: m(ChromeSliceDetailsPanel, {key: 'chrome_slice'})
});
break;
case 'THREAD_STATE':
detailsPanels.push({
key: 'current_selection',
name: 'Current Selection',
vnode: m(ThreadStatePanel, {key: 'thread_state'})
});
break;
default:
break;
}
}
if (hasLogs()) {
detailsPanels.push({
key: 'android_logs',
name: 'Android Logs',
vnode: m(LogPanel, {key: 'logs_panel'})
});
}
if (globals.queryResults.has('command')) {
const count =
(globals.queryResults.get('command') as QueryResponse).rows.length;
detailsPanels.push({
key: 'query_result',
name: `Query Result (${count})`,
vnode: m(QueryTable, {key: 'query', queryId: 'command'})
});
}
for (const pivotTableId of Object.keys(globals.state.pivotTable)) {
const pivotTable = globals.state.pivotTable[pivotTableId];
const helper = globals.pivotTableHelper.get(pivotTableId);
if (pivotTableId !== DEFAULT_PIVOT_TABLE_ID ||
globals.frontendLocalState.showPivotTable) {
if (helper !== undefined) {
helper.setSelectedPivotsAndAggregations(
pivotTable.selectedPivots, pivotTable.selectedAggregations);
}
detailsPanels.push({
key: pivotTableId,
name: pivotTable.name,
vnode: m(PivotTable, {key: pivotTableId, pivotTableId, helper})
});
}
showPivotTableEditorModal(helper);
}
if (globals.connectedFlows.length > 0) {
detailsPanels.push({
key: 'bound_flows',
name: 'Flow Events',
vnode: m(FlowEventsPanel, {key: 'flow_events'})
});
}
for (const [key, value] of globals.aggregateDataStore.entries()) {
if (value.columns.length > 0 && value.columns[0].data.length > 0) {
detailsPanels.push({
key: value.tabName,
name: value.tabName,
vnode: m(AggregationPanel, {kind: key, key, data: value})
});
}
}
// Add this after all aggregation panels, to make it appear after 'Slices'
if (globals.selectedFlows.length > 0) {
detailsPanels.push({
key: 'selected_flows',
name: 'Flow Events',
vnode: m(FlowEventsAreaSelectedPanel)
});
}
this.showDetailsPanel = detailsPanels.length > 0;
const currentTabDetails =
detailsPanels.filter(tab => tab.key === globals.state.currentTab)[0];
const panel = currentTabDetails ?
currentTabDetails.vnode :
detailsPanels.values().next().value?.vnode;
const panels = panel ? [panel] : [];
return m(
'.details-content',
{
style: {
height: `${this.detailsHeight}px`,
display: this.showDetailsPanel ? null : 'none'
}
},
m(DragHandle, {
resize: (height: number) => {
this.detailsHeight = Math.max(height, DRAG_HANDLE_HEIGHT_PX);
},
height: this.detailsHeight,
tabs: detailsPanels.map(tab => {
return {key: tab.key, name: tab.name};
}),
}),
m('.details-panel-container',
m(PanelContainer, {doesScroll: true, panels, kind: 'DETAILS'})));
}
}