• 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 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  zeroIsDefault?: boolean;
163}
164
165export class Slider implements m.ClassComponent<SliderAttrs> {
166  onValueChange(attrs: SliderAttrs, newVal: number) {
167    const traceCfg = produce(globals.state.recordConfig, (draft) => {
168      attrs.set(draft, newVal);
169    });
170    globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
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    let min = attrs.min || 1;
191    if (attrs.zeroIsDefault) {
192      min = Math.min(0, min);
193    }
194    const description = attrs.description;
195    const disabled = attrs.disabled;
196
197    // Find the index of the closest value in the slider.
198    let idx = 0;
199    for (; idx < attrs.values.length && attrs.values[idx] < val; idx++) {
200    }
201
202    let spinnerCfg = {};
203    if (attrs.isTime) {
204      spinnerCfg = {
205        type: 'text',
206        pattern: '(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2}',  // hh:mm:ss
207        value: new Date(val).toISOString().substr(11, 8),
208        oninput: (e: InputEvent) => {
209          this.onTimeValueChange(attrs, (e.target as HTMLInputElement).value);
210        },
211      };
212    } else {
213      const isDefault = attrs.zeroIsDefault && val === 0;
214      spinnerCfg = {
215        type: 'number',
216        value: isDefault ? '' : val,
217        placeholder: isDefault ? '(default)' : '',
218        oninput: (e: InputEvent) => {
219          this.onValueChange(attrs, +(e.target as HTMLInputElement).value);
220        },
221      };
222    }
223    return m(
224        '.slider' + (attrs.cssClass || ''),
225        m('header', attrs.title),
226        description ? m('header.descr', attrs.description) : '',
227        attrs.icon !== undefined ? m('i.material-icons', attrs.icon) : [],
228        m(`input[id="${id}"][type=range][min=0][max=${maxIdx}][value=${idx}]`, {
229          disabled,
230          oninput: (e: InputEvent) => {
231            this.onSliderChange(attrs, +(e.target as HTMLInputElement).value);
232          },
233        }),
234        m(`input.spinner[min=${min}][for=${id}]`, spinnerCfg),
235        m('.unit', attrs.unit));
236  }
237}
238
239// +---------------------------------------------------------------------------+
240// | Dropdown: wrapper around <select>. Supports single an multiple selection. |
241// +---------------------------------------------------------------------------+
242
243export interface DropdownAttrs {
244  title: string;
245  cssClass?: string;
246  options: Map<string, string>;
247  sort?: (a: string, b: string) => number;
248  get: Getter<string[]>;
249  set: Setter<string[]>;
250}
251
252export class Dropdown implements m.ClassComponent<DropdownAttrs> {
253  resetScroll(dom: HTMLSelectElement) {
254    // Chrome seems to override the scroll offset on creationa, b without this,
255    // even though we call it after having marked the options as selected.
256    setTimeout(() => {
257      // Don't reset the scroll position if the element is still focused.
258      if (dom !== document.activeElement) dom.scrollTop = 0;
259    }, 0);
260  }
261
262  onChange(attrs: DropdownAttrs, e: Event) {
263    const dom = e.target as HTMLSelectElement;
264    const selKeys: string[] = [];
265    for (let i = 0; i < dom.selectedOptions.length; i++) {
266      const item = assertExists(dom.selectedOptions.item(i));
267      selKeys.push(item.value);
268    }
269    const traceCfg = produce(globals.state.recordConfig, (draft) => {
270      attrs.set(draft, selKeys);
271    });
272    globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
273  }
274
275  view({attrs}: m.CVnode<DropdownAttrs>) {
276    const options: m.Children = [];
277    const selItems = attrs.get(globals.state.recordConfig);
278    let numSelected = 0;
279    const entries = [...attrs.options.entries()];
280    const f = attrs.sort === undefined ? defaultSort : attrs.sort;
281    entries.sort((a, b) => f(a[1], b[1]));
282    for (const [key, label] of entries) {
283      const opts = {value: key, selected: false};
284      if (selItems.includes(key)) {
285        opts.selected = true;
286        numSelected++;
287      }
288      options.push(m('option', opts, label));
289    }
290    const label = `${attrs.title} ${numSelected ? `(${numSelected})` : ''}`;
291    return m(
292        `select.dropdown${attrs.cssClass || ''}[multiple=multiple]`,
293        {
294          onblur: (e: Event) => this.resetScroll(e.target as HTMLSelectElement),
295          onmouseleave: (e: Event) =>
296              this.resetScroll(e.target as HTMLSelectElement),
297          oninput: (e: Event) => this.onChange(attrs, e),
298          oncreate: (vnode) => this.resetScroll(vnode.dom as HTMLSelectElement),
299        },
300        m('optgroup', {label}, options));
301  }
302}
303
304
305// +---------------------------------------------------------------------------+
306// | Textarea: wrapper around <textarea>.                                      |
307// +---------------------------------------------------------------------------+
308
309export interface TextareaAttrs {
310  placeholder: string;
311  docsLink?: string;
312  cssClass?: string;
313  get: Getter<string>;
314  set: Setter<string>;
315  title?: string;
316}
317
318export class Textarea implements m.ClassComponent<TextareaAttrs> {
319  onChange(attrs: TextareaAttrs, dom: HTMLTextAreaElement) {
320    const traceCfg = produce(globals.state.recordConfig, (draft) => {
321      attrs.set(draft, dom.value);
322    });
323    globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
324  }
325
326  view({attrs}: m.CVnode<TextareaAttrs>) {
327    return m(
328        '.textarea-holder',
329        m('header',
330          attrs.title,
331          attrs.docsLink && [' ', m(DocsChip, {href: attrs.docsLink})]),
332        m(`textarea.extra-input${attrs.cssClass || ''}`, {
333          onchange: (e: Event) =>
334              this.onChange(attrs, e.target as HTMLTextAreaElement),
335          placeholder: attrs.placeholder,
336          value: attrs.get(globals.state.recordConfig),
337        }));
338  }
339}
340
341// +---------------------------------------------------------------------------+
342// | CodeSnippet: command-prompt-like box with code snippets to copy/paste.    |
343// +---------------------------------------------------------------------------+
344
345export interface CodeSnippetAttrs {
346  text: string;
347  hardWhitespace?: boolean;
348}
349
350export class CodeSnippet implements m.ClassComponent<CodeSnippetAttrs> {
351  view({attrs}: m.CVnode<CodeSnippetAttrs>) {
352    return m(
353        '.code-snippet',
354        m('button',
355          {
356            title: 'Copy to clipboard',
357            onclick: () => copyToClipboard(attrs.text),
358          },
359          m('i.material-icons', 'assignment')),
360        m('code', attrs.text),
361    );
362  }
363}
364
365
366interface CategoriesCheckboxListParams {
367  categories: Map<string, string>;
368  title: string;
369  get: Getter<string[]>;
370  set: Setter<string[]>;
371}
372
373export class CategoriesCheckboxList implements
374    m.ClassComponent<CategoriesCheckboxListParams> {
375  updateValue(
376      attrs: CategoriesCheckboxListParams, value: string, enabled: boolean) {
377    const traceCfg = produce(globals.state.recordConfig, (draft) => {
378      const values = attrs.get(draft);
379      const index = values.indexOf(value);
380      if (enabled && index === -1) {
381        values.push(value);
382      }
383      if (!enabled && index !== -1) {
384        values.splice(index, 1);
385      }
386    });
387    globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
388  }
389
390  view({attrs}: m.CVnode<CategoriesCheckboxListParams>) {
391    const enabled = new Set(attrs.get(globals.state.recordConfig));
392    return m(
393        '.categories-list',
394        m('h3',
395          attrs.title,
396          m('button.config-button',
397            {
398              onclick: () => {
399                const config = produce(globals.state.recordConfig, (draft) => {
400                  attrs.set(draft, Array.from(attrs.categories.keys()));
401                });
402                globals.dispatch(Actions.setRecordConfig({config}));
403              },
404            },
405            'All'),
406          m('button.config-button',
407            {
408              onclick: () => {
409                const config = produce(globals.state.recordConfig, (draft) => {
410                  attrs.set(draft, []);
411                });
412                globals.dispatch(Actions.setRecordConfig({config}));
413              },
414            },
415            'None')),
416        m('ul.checkboxes',
417          Array.from(attrs.categories.entries()).map(([key, value]) => {
418            const id = `category-checkbox-${key}`;
419            return m(
420                'label',
421                {'for': id},
422                m('li',
423                  m('input[type=checkbox]', {
424                    id,
425                    checked: enabled.has(key),
426                    onclick: (e: InputEvent) => {
427                      const target = e.target as HTMLInputElement;
428                      this.updateValue(attrs, key, target.checked);
429                    },
430                  }),
431                  value));
432          })));
433  }
434}
435