|  | // 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 {isEmptyData} from '../common/aggregation_data'; | 
|  | import {LogExists, LogExistsKey} from '../common/logs'; | 
|  |  | 
|  | import {AggregationPanel} from './aggregation_panel'; | 
|  | import {ChromeSliceDetailsPanel} from './chrome_slice_panel'; | 
|  | import {CounterDetailsPanel} from './counter_panel'; | 
|  | import {CpuProfileDetailsPanel} from './cpu_profile_panel'; | 
|  | import {DEFAULT_DETAILS_CONTENT_HEIGHT} from './css_constants'; | 
|  | import {DragGestureHandler} from './drag_gesture_handler'; | 
|  | import {FlamegraphDetailsPanel} from './flamegraph_panel'; | 
|  | import { | 
|  | FlowEventsAreaSelectedPanel, | 
|  | FlowEventsPanel, | 
|  | } from './flow_events_panel'; | 
|  | import {globals} from './globals'; | 
|  | import {LogPanel} from './logs_panel'; | 
|  | import {NotesEditorPanel} from './notes_panel'; | 
|  | import {AnyAttrsVnode, PanelContainer} from './panel_container'; | 
|  | import {PivotTableRedux} from './pivot_table_redux'; | 
|  | import {QueryTable} from './query_table'; | 
|  | import {SliceDetailsPanel} from './slice_details_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; | 
|  |  | 
|  | function getDetailsHeight() { | 
|  | // This needs to be a function instead of a const to ensure the CSS constants | 
|  | // have been initialized by the time we perform this calculation; | 
|  | return DEFAULT_DETAILS_CONTENT_HEIGHT + DRAG_HANDLE_HEIGHT_PX; | 
|  | } | 
|  |  | 
|  | function getFullScreenHeight() { | 
|  | const panelContainer = | 
|  | document.querySelector('.pan-and-zoom-content') as HTMLElement; | 
|  | if (panelContainer !== null) { | 
|  | return panelContainer.clientHeight; | 
|  | } else { | 
|  | return getDetailsHeight(); | 
|  | } | 
|  | } | 
|  |  | 
|  | function hasLogs(): boolean { | 
|  | const data = globals.trackDataStore.get(LogExistsKey) as LogExists; | 
|  | return data && data.exists; | 
|  | } | 
|  |  | 
|  | interface Tab { | 
|  | key: string; | 
|  | name: string; | 
|  | } | 
|  |  | 
|  | interface DragHandleAttrs { | 
|  | height: number; | 
|  | resize: (height: number) => void; | 
|  | tabs: Tab[]; | 
|  | currentTabKey?: string; | 
|  | } | 
|  |  | 
|  | 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 = getDetailsHeight(); | 
|  |  | 
|  | 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 renderTab = (tab: Tab) => { | 
|  | if (attrs.currentTabKey === 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 = getDetailsHeight(); | 
|  | } | 
|  | 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))); | 
|  | } | 
|  | } | 
|  |  | 
|  | // For queries that are supposed to be displayed in the bottom bar, return a | 
|  | // name for a tab. Otherwise, return null. | 
|  | function userVisibleQueryName(id: string): string|null { | 
|  | if (id === 'command') { | 
|  | return 'Omnibox Query'; | 
|  | } | 
|  | if (id === 'analyze-page-query') { | 
|  | return 'Standalone Query'; | 
|  | } | 
|  | if (id.startsWith('command_')) { | 
|  | return 'Pinned Query'; | 
|  | } | 
|  | if (id.startsWith('pivot_table_details_')) { | 
|  | return 'Pivot Table Details'; | 
|  | } | 
|  | if (id.startsWith('slices_with_arg_value_')) { | 
|  | return `Arg: ${id.substr('slices_with_arg_value_'.length)}`; | 
|  | } | 
|  | if (id === 'chrome_scroll_jank_long_tasks') { | 
|  | return 'Scroll Jank: long tasks'; | 
|  | } | 
|  | return null; | 
|  | } | 
|  |  | 
|  | export class DetailsPanel implements m.ClassComponent { | 
|  | private detailsHeight = getDetailsHeight(); | 
|  |  | 
|  | 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, | 
|  | }), | 
|  | }); | 
|  | } | 
|  | if (globals.flamegraphDetails.isInAreaSelection) { | 
|  | detailsPanels.push({ | 
|  | key: 'flamegraph_selection', | 
|  | name: 'Flamegraph Selection', | 
|  | vnode: m(FlamegraphDetailsPanel, {key: 'flamegraph'}), | 
|  | }); | 
|  | } | 
|  | 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(FlamegraphDetailsPanel, {key: 'flamegraph'}), | 
|  | }); | 
|  | 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'}), | 
|  | }); | 
|  | } | 
|  |  | 
|  | const queryResults = []; | 
|  | for (const queryId of globals.queryResults.keys()) { | 
|  | const readableName = userVisibleQueryName(queryId); | 
|  | if (readableName !== null) { | 
|  | queryResults.push({queryId, name: readableName}); | 
|  | } | 
|  | } | 
|  |  | 
|  | for (const {queryId, name} of queryResults) { | 
|  | const count = | 
|  | (globals.queryResults.get(queryId) as QueryResponse).rows.length; | 
|  | detailsPanels.push({ | 
|  | key: `query_result_${queryId}`, | 
|  | name: `${name} (${count})`, | 
|  | vnode: m(QueryTable, {key: `query_${queryId}`, queryId}), | 
|  | }); | 
|  | } | 
|  |  | 
|  |  | 
|  | if (globals.state.nonSerializableState.pivotTableRedux.selectionArea !== | 
|  | undefined) { | 
|  | detailsPanels.push({ | 
|  | key: 'pivot_table_redux', | 
|  | name: 'Pivot Table', | 
|  | vnode: m(PivotTableRedux, { | 
|  | key: 'pivot_table_redux', | 
|  | selectionArea: | 
|  | globals.state.nonSerializableState.pivotTableRedux.selectionArea, | 
|  | }), | 
|  | }); | 
|  | } | 
|  |  | 
|  | 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 (!isEmptyData(value)) { | 
|  | 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, {key: 'flow_events_area'}), | 
|  | }); | 
|  | } | 
|  |  | 
|  | let currentTabDetails = | 
|  | detailsPanels.find((tab) => tab.key === globals.state.currentTab); | 
|  | if (currentTabDetails === undefined && detailsPanels.length > 0) { | 
|  | currentTabDetails = detailsPanels[0]; | 
|  | } | 
|  |  | 
|  | const panel = currentTabDetails?.vnode; | 
|  | const panels = panel ? [panel] : []; | 
|  |  | 
|  | return m( | 
|  | '.details-content', | 
|  | { | 
|  | style: { | 
|  | height: `${this.detailsHeight}px`, | 
|  | display: detailsPanels.length > 0 ? 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}; | 
|  | }), | 
|  | currentTabKey: currentTabDetails?.key, | 
|  | }), | 
|  | m('.details-panel-container.x-scrollable', | 
|  | m(PanelContainer, {doesScroll: true, panels, kind: 'DETAILS'}))); | 
|  | } | 
|  | } |