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 m from 'mithril'; 16 17import {Actions} from '../common/actions'; 18import {VERSION} from '../gen/perfetto_version'; 19 20import {globals} from './globals'; 21import {runQueryInNewTab} from './query_result_tab'; 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 if (mode === COMMAND && key === 'Enter') { 65 const openInPinnedTab = event.metaKey || event.ctrlKey; 66 runQueryInNewTab( 67 txt.value, 68 openInPinnedTab ? 'Pinned query' : 'Omnibox query', 69 openInPinnedTab ? undefined : 'omnibox_query', 70 ); 71 } 72} 73 74function onKeyUp(e: Event) { 75 e.stopPropagation(); 76 const event = (e as KeyboardEvent); 77 const key = event.key; 78 const txt = e.target as HTMLInputElement; 79 80 if (key === 'Escape') { 81 mode = SEARCH; 82 txt.value = ''; 83 txt.blur(); 84 globals.rafScheduler.scheduleFullRedraw(); 85 return; 86 } 87} 88 89class Omnibox implements m.ClassComponent { 90 oncreate(vnode: m.VnodeDOM) { 91 const txt = vnode.dom.querySelector('input') as HTMLInputElement; 92 txt.addEventListener('keydown', onKeyDown); 93 txt.addEventListener('keyup', onKeyUp); 94 } 95 96 view() { 97 const msgTTL = globals.state.status.timestamp + 1 - Date.now() / 1e3; 98 const engineIsBusy = 99 globals.state.engine !== undefined && !globals.state.engine.ready; 100 101 if (msgTTL > 0 || engineIsBusy) { 102 setTimeout( 103 () => globals.rafScheduler.scheduleFullRedraw(), msgTTL * 1000); 104 return m( 105 `.omnibox.message-mode`, 106 m(`input[placeholder=${globals.state.status.msg}][readonly]`, { 107 value: '', 108 })); 109 } 110 111 const commandMode = mode === COMMAND; 112 return m( 113 `.omnibox${commandMode ? '.command-mode' : ''}`, 114 m('input', { 115 placeholder: PLACEHOLDER[mode], 116 oninput: (e: InputEvent) => { 117 const value = (e.target as HTMLInputElement).value; 118 globals.dispatch(Actions.setOmnibox({ 119 omnibox: value, 120 mode: commandMode ? 'COMMAND' : 'SEARCH', 121 })); 122 if (mode === SEARCH) { 123 displayStepThrough = value.length >= 4; 124 globals.dispatch(Actions.setSearchIndex({index: -1})); 125 } 126 }, 127 value: globals.state.omniboxState.omnibox, 128 }), 129 displayStepThrough ? 130 m( 131 '.stepthrough', 132 m('.current', 133 `${ 134 globals.currentSearchResults.totalResults === 0 ? 135 '0 / 0' : 136 `${globals.state.searchIndex + 1} / ${ 137 globals.currentSearchResults.totalResults}`}`), 138 m('button', 139 { 140 onclick: () => { 141 executeSearch(true /* reverse direction */); 142 }, 143 }, 144 m('i.material-icons.left', 'keyboard_arrow_left')), 145 m('button', 146 { 147 onclick: () => { 148 executeSearch(); 149 }, 150 }, 151 m('i.material-icons.right', 'keyboard_arrow_right')), 152 ) : 153 ''); 154 } 155} 156 157class Progress implements m.ClassComponent { 158 private loading: () => void; 159 private progressBar?: HTMLElement; 160 161 constructor() { 162 this.loading = () => this.loadingAnimation(); 163 } 164 165 oncreate(vnodeDom: m.CVnodeDOM) { 166 this.progressBar = vnodeDom.dom as HTMLElement; 167 globals.rafScheduler.addRedrawCallback(this.loading); 168 } 169 170 onremove() { 171 globals.rafScheduler.removeRedrawCallback(this.loading); 172 } 173 174 view() { 175 return m('.progress'); 176 } 177 178 loadingAnimation() { 179 if (this.progressBar === undefined) return; 180 const engine = globals.getCurrentEngine(); 181 if ((engine && !engine.ready) || globals.numQueuedQueries > 0 || 182 taskTracker.hasPendingTasks()) { 183 this.progressBar.classList.add('progress-anim'); 184 } else { 185 this.progressBar.classList.remove('progress-anim'); 186 } 187 } 188} 189 190 191class NewVersionNotification implements m.ClassComponent { 192 view() { 193 return m( 194 '.new-version-toast', 195 `Updated to ${VERSION} and ready for offline use!`, 196 m('button.notification-btn.preferred', 197 { 198 onclick: () => { 199 globals.frontendLocalState.newVersionAvailable = false; 200 globals.rafScheduler.scheduleFullRedraw(); 201 }, 202 }, 203 'Dismiss'), 204 ); 205 } 206} 207 208 209class HelpPanningNotification implements m.ClassComponent { 210 view() { 211 const dismissed = localStorage.getItem(DISMISSED_PANNING_HINT_KEY); 212 // Do not show the help notification in embedded mode because local storage 213 // does not persist for iFrames. The host is responsible for communicating 214 // to users that they can press '?' for help. 215 if (globals.embeddedMode || dismissed === 'true' || 216 !globals.frontendLocalState.showPanningHint) { 217 return; 218 } 219 return m( 220 '.helpful-hint', 221 m('.hint-text', 222 'Are you trying to pan? Use the WASD keys or hold shift to click ' + 223 'and drag. Press \'?\' for more help.'), 224 m('button.hint-dismiss-button', 225 { 226 onclick: () => { 227 globals.frontendLocalState.showPanningHint = false; 228 localStorage.setItem(DISMISSED_PANNING_HINT_KEY, 'true'); 229 globals.rafScheduler.scheduleFullRedraw(); 230 }, 231 }, 232 'Dismiss'), 233 ); 234 } 235} 236 237class TraceErrorIcon implements m.ClassComponent { 238 view() { 239 if (globals.embeddedMode) return; 240 241 const errors = globals.traceErrors; 242 if (!errors && !globals.metricError || mode === COMMAND) return; 243 const message = errors ? `${errors} import or data loss errors detected.` : 244 `Metric error detected.`; 245 return m( 246 'a.error', 247 {href: '#!/info'}, 248 m('i.material-icons', 249 { 250 title: message + ` Click for more info.`, 251 }, 252 'announcement')); 253 } 254} 255 256export class Topbar implements m.ClassComponent { 257 view() { 258 return m( 259 '.topbar', 260 {class: globals.state.sidebarVisible ? '' : 'hide-sidebar'}, 261 globals.frontendLocalState.newVersionAvailable ? 262 m(NewVersionNotification) : 263 m(Omnibox), 264 m(Progress), 265 m(HelpPanningNotification), 266 m(TraceErrorIcon)); 267 } 268} 269