• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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