1// Copyright (C) 2018 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import * as m from 'mithril'; 16 17import {Actions} from '../common/actions'; 18import {EngineConfig} from '../common/state'; 19import * as version from '../gen/perfetto_version'; 20 21import {globals} from './globals'; 22import {executeSearch} from './search_handler'; 23import {taskTracker} from './task_tracker'; 24 25const SEARCH = Symbol('search'); 26const COMMAND = Symbol('command'); 27type Mode = typeof SEARCH|typeof COMMAND; 28 29const PLACEHOLDER = { 30 [SEARCH]: 'Search', 31 [COMMAND]: 'e.g. select * from sched left join thread using(utid) limit 10' 32}; 33 34export const DISMISSED_PANNING_HINT_KEY = 'dismissedPanningHint'; 35 36let mode: Mode = SEARCH; 37let displayStepThrough = false; 38 39function onKeyDown(e: Event) { 40 const event = (e as KeyboardEvent); 41 const key = event.key; 42 if (key !== 'Enter') { 43 e.stopPropagation(); 44 } 45 const txt = (e.target as HTMLInputElement); 46 47 if (mode === SEARCH && txt.value === '' && key === ':') { 48 e.preventDefault(); 49 mode = COMMAND; 50 globals.rafScheduler.scheduleFullRedraw(); 51 return; 52 } 53 54 if (mode === COMMAND && txt.value === '' && key === 'Backspace') { 55 mode = SEARCH; 56 globals.rafScheduler.scheduleFullRedraw(); 57 return; 58 } 59 60 if (mode === SEARCH && key === 'Enter') { 61 txt.blur(); 62 } 63} 64 65function onKeyUp(e: Event) { 66 e.stopPropagation(); 67 const event = (e as KeyboardEvent); 68 const key = event.key; 69 const txt = e.target as HTMLInputElement; 70 71 if (key === 'Escape') { 72 globals.dispatch(Actions.deleteQuery({queryId: 'command'})); 73 mode = SEARCH; 74 txt.value = ''; 75 txt.blur(); 76 globals.rafScheduler.scheduleFullRedraw(); 77 return; 78 } 79 if (mode === COMMAND && key === 'Enter') { 80 globals.dispatch(Actions.executeQuery( 81 {engineId: '0', queryId: 'command', query: txt.value})); 82 } 83} 84 85class Omnibox implements m.ClassComponent { 86 oncreate(vnode: m.VnodeDOM) { 87 const txt = vnode.dom.querySelector('input') as HTMLInputElement; 88 txt.addEventListener('keydown', onKeyDown); 89 txt.addEventListener('keyup', onKeyUp); 90 } 91 92 view() { 93 const msgTTL = globals.state.status.timestamp + 1 - Date.now() / 1e3; 94 let enginesAreBusy = false; 95 for (const engine of Object.values(globals.state.engines)) { 96 enginesAreBusy = enginesAreBusy || !engine.ready; 97 } 98 99 if (msgTTL > 0 || enginesAreBusy) { 100 setTimeout( 101 () => globals.rafScheduler.scheduleFullRedraw(), msgTTL * 1000); 102 return m( 103 `.omnibox.message-mode`, 104 m(`input[placeholder=${globals.state.status.msg}][readonly]`, { 105 value: '', 106 })); 107 } 108 109 const commandMode = mode === COMMAND; 110 return m( 111 `.omnibox${commandMode ? '.command-mode' : ''}`, 112 m('input', { 113 placeholder: PLACEHOLDER[mode], 114 oninput: (e: InputEvent) => { 115 const value = (e.target as HTMLInputElement).value; 116 globals.frontendLocalState.setOmnibox( 117 value, commandMode ? 'COMMAND' : 'SEARCH'); 118 if (mode === SEARCH) { 119 displayStepThrough = value.length >= 4; 120 globals.dispatch(Actions.setSearchIndex({index: -1})); 121 } 122 }, 123 value: globals.frontendLocalState.omnibox, 124 }), 125 displayStepThrough ? 126 m( 127 '.stepthrough', 128 m('.current', 129 `${ 130 globals.currentSearchResults.totalResults === 0 ? 131 '0 / 0' : 132 `${globals.state.searchIndex + 1} / ${ 133 globals.currentSearchResults.totalResults}`}`), 134 m('button', 135 { 136 disabled: globals.state.searchIndex <= 0, 137 onclick: () => { 138 executeSearch(true /* reverse direction */); 139 } 140 }, 141 m('i.material-icons.left', 'keyboard_arrow_left')), 142 m('button', 143 { 144 disabled: globals.state.searchIndex === 145 globals.currentSearchResults.totalResults - 1, 146 onclick: () => { 147 executeSearch(); 148 } 149 }, 150 m('i.material-icons.right', 'keyboard_arrow_right')), 151 ) : 152 ''); 153 } 154} 155 156class Progress implements m.ClassComponent { 157 private loading: () => void; 158 private progressBar?: HTMLElement; 159 160 constructor() { 161 this.loading = () => this.loadingAnimation(); 162 } 163 164 oncreate(vnodeDom: m.CVnodeDOM) { 165 this.progressBar = vnodeDom.dom as HTMLElement; 166 globals.rafScheduler.addRedrawCallback(this.loading); 167 } 168 169 onremove() { 170 globals.rafScheduler.removeRedrawCallback(this.loading); 171 } 172 173 view() { 174 return m('.progress'); 175 } 176 177 loadingAnimation() { 178 if (this.progressBar === undefined) return; 179 const engine: EngineConfig = globals.state.engines['0']; 180 if ((engine !== undefined && !engine.ready) || 181 globals.numQueuedQueries > 0 || taskTracker.hasPendingTasks()) { 182 this.progressBar.classList.add('progress-anim'); 183 } else { 184 this.progressBar.classList.remove('progress-anim'); 185 } 186 } 187} 188 189 190class NewVersionNotification implements m.ClassComponent { 191 view() { 192 return m( 193 '.new-version-toast', 194 `Updated to ${version.VERSION} and ready for offline use!`, 195 m('button.notification-btn.preferred', 196 { 197 onclick: () => { 198 globals.frontendLocalState.newVersionAvailable = false; 199 globals.rafScheduler.scheduleFullRedraw(); 200 } 201 }, 202 'Dismiss'), 203 ); 204 } 205} 206 207 208class HelpPanningNotification implements m.ClassComponent { 209 view() { 210 const dismissed = localStorage.getItem(DISMISSED_PANNING_HINT_KEY); 211 if (dismissed === 'true' || !globals.frontendLocalState.showPanningHint) { 212 return; 213 } 214 return m( 215 '.helpful-hint', 216 m('.hint-text', 217 'Are you trying to pan? Use the WASD keys or hold shift to click ' + 218 'and drag. Press \'?\' for more help.'), 219 m('button.hint-dismiss-button', 220 { 221 onclick: () => { 222 globals.frontendLocalState.showPanningHint = false; 223 localStorage.setItem(DISMISSED_PANNING_HINT_KEY, 'true'); 224 globals.rafScheduler.scheduleFullRedraw(); 225 } 226 }, 227 'Dismiss'), 228 ); 229 } 230} 231 232class TraceErrorIcon implements m.ClassComponent { 233 view() { 234 const errors = globals.traceErrors; 235 if (!errors && !globals.metricError || mode === COMMAND) return; 236 const message = errors ? `${errors} import or data loss errors detected.` : 237 `Metric error detected.`; 238 return m( 239 'a.error', 240 {href: '#!/info'}, 241 m('i.material-icons', 242 { 243 title: message + ` Click for more info.`, 244 }, 245 'announcement')); 246 } 247} 248 249export class Topbar implements m.ClassComponent { 250 view() { 251 return m( 252 '.topbar', 253 {class: globals.state.sidebarVisible ? '' : 'hide-sidebar'}, 254 globals.frontendLocalState.newVersionAvailable ? 255 m(NewVersionNotification) : 256 m(Omnibox), 257 m(Progress), 258 m(HelpPanningNotification), 259 m(TraceErrorIcon)); 260 } 261} 262