• 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 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