| // 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'}))); |
| } |
| } |