• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2019 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 {Draft, produce} from 'immer';
16import * as m from 'mithril';
17
18import {assertExists} from '../base/logging';
19import {Actions} from '../common/actions';
20import {RecordConfig} from '../controller/record_config_types';
21
22import {copyToClipboard} from './clipboard';
23import {globals} from './globals';
24
25declare type Setter<T> = (draft: Draft<RecordConfig>, val: T) => void;
26declare type Getter<T> = (cfg: RecordConfig) => T;
27
28function defaultSort(a: string, b: string) {
29  return a.localeCompare(b);
30}
31
32// +---------------------------------------------------------------------------+
33// | Docs link with 'i' in circle icon.                                        |
34// +---------------------------------------------------------------------------+
35
36interface DocsChipAttrs {
37  href: string;
38}
39
40class DocsChip implements m.ClassComponent<DocsChipAttrs> {
41  view({attrs}: m.CVnode<DocsChipAttrs>) {
42    return m(
43        'a.inline-chip',
44        {href: attrs.href, title: 'Open docs in new tab', target: '_blank'},
45        m('i.material-icons', 'info'),
46        ' Docs');
47  }
48}
49
50// +---------------------------------------------------------------------------+
51// | Probe: the rectangular box on the right-hand-side with a toggle box.      |
52// +---------------------------------------------------------------------------+
53
54export interface ProbeAttrs {
55  title: string;
56  img: string|null;
57  compact?: boolean;
58  descr: m.Children;
59  isEnabled: Getter<boolean>;
60  setEnabled: Setter<boolean>;
61}
62
63export class Probe implements m.ClassComponent<ProbeAttrs> {
64  view({attrs, children}: m.CVnode<ProbeAttrs>) {
65    const onToggle = (enabled: boolean) => {
66      const traceCfg = produce(globals.state.recordConfig, draft => {
67        attrs.setEnabled(draft, enabled);
68      });
69      globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
70    };
71
72    const enabled = attrs.isEnabled(globals.state.recordConfig);
73
74    return m(
75        `.probe${attrs.compact ? '.compact' : ''}${enabled ? '.enabled' : ''}`,
76        attrs.img && m('img', {
77          src: `${globals.root}assets/${attrs.img}`,
78          onclick: () => onToggle(!enabled),
79        }),
80        m('label',
81          m(`input[type=checkbox]`, {
82            checked: enabled,
83            oninput: (e: InputEvent) => {
84              onToggle((e.target as HTMLInputElement).checked);
85            },
86          }),
87          m('span', attrs.title)),
88        attrs.compact ?
89            '' :
90            m('div', m('div', attrs.descr), m('.probe-config', children)));
91  }
92}
93
94export function CompactProbe(args: {
95  title: string,
96  isEnabled: Getter<boolean>,
97  setEnabled: Setter<boolean>
98}) {
99  return m(Probe, {
100    title: args.title,
101    img: null,
102    compact: true,
103    descr: '',
104    isEnabled: args.isEnabled,
105    setEnabled: args.setEnabled
106  } as ProbeAttrs);
107}
108
109// +-------------------------------------------------------------+
110// | Toggle: an on/off switch.
111// +-------------------------------------------------------------+
112
113export interface ToggleAttrs {
114  title: string;
115  descr: string;
116  cssClass?: string;
117  isEnabled: Getter<boolean>;
118  setEnabled: Setter<boolean>;
119}
120
121export class Toggle implements m.ClassComponent<ToggleAttrs> {
122  view({attrs}: m.CVnode<ToggleAttrs>) {
123    const onToggle = (enabled: boolean) => {
124      const traceCfg = produce(globals.state.recordConfig, draft => {
125        attrs.setEnabled(draft, enabled);
126      });
127      globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
128    };
129
130    const enabled = attrs.isEnabled(globals.state.recordConfig);
131
132    return m(
133        `.toggle${enabled ? '.enabled' : ''}${attrs.cssClass || ''}`,
134        m('label',
135          m(`input[type=checkbox]`, {
136            checked: enabled,
137            oninput: (e: InputEvent) => {
138              onToggle((e.target as HTMLInputElement).checked);
139            },
140          }),
141          m('span', attrs.title)),
142        m('.descr', attrs.descr));
143  }
144}
145
146// +---------------------------------------------------------------------------+
147// | Slider: draggable horizontal slider with numeric spinner.                 |
148// +---------------------------------------------------------------------------+
149
150export interface SliderAttrs {
151  title: string;
152  icon?: string;
153  cssClass?: string;
154  isTime?: boolean;
155  unit: string;
156  values: number[];
157  get: Getter<number>;
158  set: Setter<number>;
159  min?: number;
160  description?: string;
161  disabled?: boolean;
162}
163
164export class Slider implements m.ClassComponent<SliderAttrs> {
165  onValueChange(attrs: SliderAttrs, newVal: number) {
166    const traceCfg = produce(globals.state.recordConfig, draft => {
167      attrs.set(draft, newVal);
168    });
169    globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
170  }
171
172
173  onTimeValueChange(attrs: SliderAttrs, hms: string) {
174    try {
175      const date = new Date(`1970-01-01T${hms}.000Z`);
176      if (isNaN(date.getTime())) return;
177      this.onValueChange(attrs, date.getTime());
178    } catch {
179    }
180  }
181
182  onSliderChange(attrs: SliderAttrs, newIdx: number) {
183    this.onValueChange(attrs, attrs.values[newIdx]);
184  }
185
186  view({attrs}: m.CVnode<SliderAttrs>) {
187    const id = attrs.title.replace(/[^a-z0-9]/gmi, '_').toLowerCase();
188    const maxIdx = attrs.values.length - 1;
189    const val = attrs.get(globals.state.recordConfig);
190    const min = attrs.min;
191    const description = attrs.description;
192    const disabled = attrs.disabled;
193
194    // Find the index of the closest value in the slider.
195    let idx = 0;
196    for (; idx < attrs.values.length && attrs.values[idx] < val; idx++) {
197    }
198
199    let spinnerCfg = {};
200    if (attrs.isTime) {
201      spinnerCfg = {
202        type: 'text',
203        pattern: '(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2}',  // hh:mm:ss
204        value: new Date(val).toISOString().substr(11, 8),
205        oninput: (e: InputEvent) => {
206          this.onTimeValueChange(attrs, (e.target as HTMLInputElement).value);
207        },
208      };
209    } else {
210      spinnerCfg = {
211        type: 'number',
212        value: val,
213        oninput: (e: InputEvent) => {
214          this.onTimeValueChange(attrs, (e.target as HTMLInputElement).value);
215        },
216      };
217    }
218    return m(
219        '.slider' + (attrs.cssClass || ''),
220        m('header', attrs.title),
221        description ? m('header.descr', attrs.description) : '',
222        attrs.icon !== undefined ? m('i.material-icons', attrs.icon) : [],
223        m(`input[id="${id}"][type=range][min=0][max=${maxIdx}][value=${idx}]
224        ${disabled ? '[disabled]' : ''}`,
225          {
226            oninput: (e: InputEvent) => {
227              this.onSliderChange(attrs, +(e.target as HTMLInputElement).value);
228            },
229          }),
230        m(`input.spinner[min=${min !== undefined ? min : 1}][for=${id}]`,
231          spinnerCfg),
232        m('.unit', attrs.unit));
233  }
234}
235
236// +---------------------------------------------------------------------------+
237// | Dropdown: wrapper around <select>. Supports single an multiple selection. |
238// +---------------------------------------------------------------------------+
239
240export interface DropdownAttrs {
241  title: string;
242  cssClass?: string;
243  options: Map<string, string>;
244  sort?: (a: string, b: string) => number;
245  get: Getter<string[]>;
246  set: Setter<string[]>;
247}
248
249export class Dropdown implements m.ClassComponent<DropdownAttrs> {
250  resetScroll(dom: HTMLSelectElement) {
251    // Chrome seems to override the scroll offset on creationa, b without this,
252    // even though we call it after having marked the options as selected.
253    setTimeout(() => {
254      // Don't reset the scroll position if the element is still focused.
255      if (dom !== document.activeElement) dom.scrollTop = 0;
256    }, 0);
257  }
258
259  onChange(attrs: DropdownAttrs, e: Event) {
260    const dom = e.target as HTMLSelectElement;
261    const selKeys: string[] = [];
262    for (let i = 0; i < dom.selectedOptions.length; i++) {
263      const item = assertExists(dom.selectedOptions.item(i));
264      selKeys.push(item.value);
265    }
266    const traceCfg = produce(globals.state.recordConfig, draft => {
267      attrs.set(draft, selKeys);
268    });
269    globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
270  }
271
272  view({attrs}: m.CVnode<DropdownAttrs>) {
273    const options: m.Children = [];
274    const selItems = attrs.get(globals.state.recordConfig);
275    let numSelected = 0;
276    const entries = [...attrs.options.entries()];
277    const f = attrs.sort === undefined ? defaultSort : attrs.sort;
278    entries.sort((a, b) => f(a[1], b[1]));
279    for (const [key, label] of entries) {
280      const opts = {value: key, selected: false};
281      if (selItems.includes(key)) {
282        opts.selected = true;
283        numSelected++;
284      }
285      options.push(m('option', opts, label));
286    }
287    const label = `${attrs.title} ${numSelected ? `(${numSelected})` : ''}`;
288    return m(
289        `select.dropdown${attrs.cssClass || ''}[multiple=multiple]`,
290        {
291          onblur: (e: Event) => this.resetScroll(e.target as HTMLSelectElement),
292          onmouseleave: (e: Event) =>
293              this.resetScroll(e.target as HTMLSelectElement),
294          oninput: (e: Event) => this.onChange(attrs, e),
295          oncreate: (vnode) => this.resetScroll(vnode.dom as HTMLSelectElement),
296        },
297        m('optgroup', {label}, options));
298  }
299}
300
301
302// +---------------------------------------------------------------------------+
303// | Textarea: wrapper around <textarea>.                                      |
304// +---------------------------------------------------------------------------+
305
306export interface TextareaAttrs {
307  placeholder: string;
308  docsLink?: string;
309  cssClass?: string;
310  get: Getter<string>;
311  set: Setter<string>;
312  title?: string;
313}
314
315export class Textarea implements m.ClassComponent<TextareaAttrs> {
316  onChange(attrs: TextareaAttrs, dom: HTMLTextAreaElement) {
317    const traceCfg = produce(globals.state.recordConfig, draft => {
318      attrs.set(draft, dom.value);
319    });
320    globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
321  }
322
323  view({attrs}: m.CVnode<TextareaAttrs>) {
324    return m(
325        '.textarea-holder',
326        m('header',
327          attrs.title,
328          attrs.docsLink && [' ', m(DocsChip, {href: attrs.docsLink})]),
329        m(`textarea.extra-input${attrs.cssClass || ''}`, {
330          onchange: (e: Event) =>
331              this.onChange(attrs, e.target as HTMLTextAreaElement),
332          placeholder: attrs.placeholder,
333          value: attrs.get(globals.state.recordConfig)
334        }));
335  }
336}
337
338// +---------------------------------------------------------------------------+
339// | CodeSnippet: command-prompt-like box with code snippets to copy/paste.    |
340// +---------------------------------------------------------------------------+
341
342export interface CodeSnippetAttrs {
343  text: string;
344  hardWhitespace?: boolean;
345}
346
347export class CodeSnippet implements m.ClassComponent<CodeSnippetAttrs> {
348  view({attrs}: m.CVnode<CodeSnippetAttrs>) {
349    return m(
350        '.code-snippet',
351        m('button',
352          {
353            title: 'Copy to clipboard',
354            onclick: () => copyToClipboard(attrs.text),
355          },
356          m('i.material-icons', 'assignment')),
357        m('code', attrs.text),
358    );
359  }
360}
361
362
363interface CategoriesCheckboxListParams {
364  categories: Map<string, string>;
365  title: string;
366  get: Getter<string[]>;
367  set: Setter<string[]>;
368}
369
370export class CategoriesCheckboxList implements
371    m.ClassComponent<CategoriesCheckboxListParams> {
372  updateValue(
373      attrs: CategoriesCheckboxListParams, value: string, enabled: boolean) {
374    const traceCfg = produce(globals.state.recordConfig, draft => {
375      const values = attrs.get(draft);
376      const index = values.indexOf(value);
377      if (enabled && index === -1) {
378        values.push(value);
379      }
380      if (!enabled && index !== -1) {
381        values.splice(index, 1);
382      }
383    });
384    globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
385  }
386
387  view({attrs}: m.CVnode<CategoriesCheckboxListParams>) {
388    const enabled = new Set(attrs.get(globals.state.recordConfig));
389    return m(
390        '.categories-list',
391        m('h3',
392          attrs.title,
393          m('button.config-button',
394            {
395              onclick: () => {
396                const config = produce(globals.state.recordConfig, draft => {
397                  attrs.set(draft, Array.from(attrs.categories.keys()));
398                });
399                globals.dispatch(Actions.setRecordConfig({config}));
400              }
401            },
402            'All'),
403          m('button.config-button',
404            {
405              onclick: () => {
406                const config = produce(globals.state.recordConfig, draft => {
407                  attrs.set(draft, []);
408                });
409                globals.dispatch(Actions.setRecordConfig({config}));
410              },
411            },
412            'None')),
413        m('ul.checkboxes',
414          Array.from(attrs.categories.entries()).map(([key, value]) => {
415            const id = `category-checkbox-${key}`;
416            return m(
417                'label',
418                {'for': id},
419                m('li',
420                  m('input[type=checkbox]', {
421                    id,
422                    checked: enabled.has(key),
423                    onclick: (e: InputEvent) => {
424                      const target = e.target as HTMLInputElement;
425                      this.updateValue(attrs, key, target.checked);
426                    }
427                  }),
428                  value));
429          })));
430  }
431}
432