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    const state = globals.frontendLocalState;
111    return m(
112        `.omnibox${commandMode ? '.command-mode' : ''}`,
113        m('input', {
114          placeholder: PLACEHOLDER[mode],
115          oninput: (e: InputEvent) => {
116            const value = (e.target as HTMLInputElement).value;
117            globals.frontendLocalState.setOmnibox(
118                value, commandMode ? 'COMMAND' : 'SEARCH');
119            if (mode === SEARCH) {
120              globals.frontendLocalState.setSearchIndex(-1);
121              displayStepThrough = value.length >= 4;
122              globals.rafScheduler.scheduleFullRedraw();
123            }
124          },
125          value: globals.frontendLocalState.omnibox,
126        }),
127        displayStepThrough ?
128            m(
129                '.stepthrough',
130                m('.current',
131                  `${
132                      globals.currentSearchResults.totalResults === 0 ?
133                          '0 / 0' :
134                          `${state.searchIndex + 1} / ${
135                              globals.currentSearchResults.totalResults}`}`),
136                m('button',
137                  {
138                    disabled: state.searchIndex <= 0,
139                    onclick: () => {
140                      executeSearch(true /* reverse direction */);
141                    }
142                  },
143                  m('i.material-icons.left', 'keyboard_arrow_left')),
144                m('button',
145                  {
146                    disabled: state.searchIndex ===
147                        globals.currentSearchResults.totalResults - 1,
148                    onclick: () => {
149                      executeSearch();
150                    }
151                  },
152                  m('i.material-icons.right', 'keyboard_arrow_right')),
153                ) :
154            '');
155  }
156}
157
158class Progress implements m.ClassComponent {
159  private loading: () => void;
160  private progressBar?: HTMLElement;
161
162  constructor() {
163    this.loading = () => this.loadingAnimation();
164  }
165
166  oncreate(vnodeDom: m.CVnodeDOM) {
167    this.progressBar = vnodeDom.dom as HTMLElement;
168    globals.rafScheduler.addRedrawCallback(this.loading);
169  }
170
171  onremove() {
172    globals.rafScheduler.removeRedrawCallback(this.loading);
173  }
174
175  view() {
176    return m('.progress');
177  }
178
179  loadingAnimation() {
180    if (this.progressBar === undefined) return;
181    const engine: EngineConfig = globals.state.engines['0'];
182    if ((engine !== undefined && !engine.ready) ||
183        globals.numQueuedQueries > 0 || taskTracker.hasPendingTasks()) {
184      this.progressBar.classList.add('progress-anim');
185    } else {
186      this.progressBar.classList.remove('progress-anim');
187    }
188  }
189}
190
191
192class NewVersionNotification implements m.ClassComponent {
193  view() {
194    return m(
195        '.new-version-toast',
196        `Updated to ${version.VERSION} and ready for offline use!`,
197        m('button.notification-btn.preferred',
198          {
199            onclick: () => {
200              globals.frontendLocalState.newVersionAvailable = false;
201              globals.rafScheduler.scheduleFullRedraw();
202            }
203          },
204          'Dismiss'),
205    );
206  }
207}
208
209
210class HelpPanningNotification implements m.ClassComponent {
211  view() {
212    const dismissed = localStorage.getItem(DISMISSED_PANNING_HINT_KEY);
213    if (dismissed === 'true' || !globals.frontendLocalState.showPanningHint) {
214      return;
215    }
216    return m(
217        '.helpful-hint',
218        m('.hint-text',
219          'Are you trying to pan? Use the WASD keys or hold shift to click ' +
220              'and drag. Press \'?\' for more help.'),
221        m('button.hint-dismiss-button',
222          {
223            onclick: () => {
224              globals.frontendLocalState.showPanningHint = false;
225              localStorage.setItem(DISMISSED_PANNING_HINT_KEY, 'true');
226              globals.rafScheduler.scheduleFullRedraw();
227            }
228          },
229          'Dismiss'),
230    );
231  }
232}
233
234class TraceErrorIcon implements m.ClassComponent {
235  view() {
236    const errors = globals.traceErrors;
237    if (!errors && !globals.metricError || mode === COMMAND) return;
238    const message = errors ? `${errors} import or data loss errors detected.` :
239                             `Metric error detected.`;
240    return m(
241        'a.error',
242        {href: '#!/info'},
243        m('i.material-icons',
244          {
245            title: message + ` Click for more info.`,
246          },
247          'announcement'));
248  }
249}
250
251export class Topbar implements m.ClassComponent {
252  view() {
253    return m(
254        '.topbar',
255        {
256          class: globals.frontendLocalState.sidebarVisible ? '' : 'hide-sidebar'
257        },
258        globals.frontendLocalState.newVersionAvailable ?
259            m(NewVersionNotification) :
260            m(Omnibox),
261        m(Progress),
262        m(HelpPanningNotification),
263        m(TraceErrorIcon));
264  }
265}
266