• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2023 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 {classNames} from '../base/classnames';
18import {Hotkey, Platform} from '../base/hotkeys';
19import {isString} from '../base/object_utils';
20import {Icons} from '../base/semantic_icons';
21import {raf} from '../core/raf_scheduler';
22import {Anchor} from '../widgets/anchor';
23import {Button} from '../widgets/button';
24import {Callout} from '../widgets/callout';
25import {Checkbox} from '../widgets/checkbox';
26import {Editor} from '../widgets/editor';
27import {EmptyState} from '../widgets/empty_state';
28import {Form, FormLabel} from '../widgets/form';
29import {HotkeyGlyphs} from '../widgets/hotkey_glyphs';
30import {Icon} from '../widgets/icon';
31import {Menu, MenuDivider, MenuItem, PopupMenu2} from '../widgets/menu';
32import {showModal} from '../widgets/modal';
33import {
34  MultiSelect,
35  MultiSelectDiff,
36  PopupMultiSelect,
37} from '../widgets/multiselect';
38import {Popup, PopupPosition} from '../widgets/popup';
39import {Portal} from '../widgets/portal';
40import {FilterableSelect, Select} from '../widgets/select';
41import {Spinner} from '../widgets/spinner';
42import {Switch} from '../widgets/switch';
43import {TextInput} from '../widgets/text_input';
44import {MultiParagraphText, TextParagraph} from '../widgets/text_paragraph';
45import {LazyTreeNode, Tree, TreeNode} from '../widgets/tree';
46import {VegaView} from '../widgets/vega_view';
47
48import {createPage} from './pages';
49import {PopupMenuButton} from './popup_menu';
50import {TableShowcase} from './tables/table_showcase';
51import {TreeTable, TreeTableAttrs} from './widgets/treetable';
52import {Intent} from '../widgets/common';
53import {
54  VirtualTable,
55  VirtualTableAttrs,
56  VirtualTableRow,
57} from '../widgets/virtual_table';
58
59const DATA_ENGLISH_LETTER_FREQUENCY = {
60  table: [
61    {category: 'a', amount: 8.167},
62    {category: 'b', amount: 1.492},
63    {category: 'c', amount: 2.782},
64    {category: 'd', amount: 4.253},
65    {category: 'e', amount: 12.7},
66    {category: 'f', amount: 2.228},
67    {category: 'g', amount: 2.015},
68    {category: 'h', amount: 6.094},
69    {category: 'i', amount: 6.966},
70    {category: 'j', amount: 0.253},
71    {category: 'k', amount: 1.772},
72    {category: 'l', amount: 4.025},
73    {category: 'm', amount: 2.406},
74    {category: 'n', amount: 6.749},
75    {category: 'o', amount: 7.507},
76    {category: 'p', amount: 1.929},
77    {category: 'q', amount: 0.095},
78    {category: 'r', amount: 5.987},
79    {category: 's', amount: 6.327},
80    {category: 't', amount: 9.056},
81    {category: 'u', amount: 2.758},
82    {category: 'v', amount: 0.978},
83    {category: 'w', amount: 2.36},
84    {category: 'x', amount: 0.25},
85    {category: 'y', amount: 1.974},
86    {category: 'z', amount: 0.074},
87  ],
88};
89
90const DATA_POLISH_LETTER_FREQUENCY = {
91  table: [
92    {category: 'a', amount: 8.965},
93    {category: 'b', amount: 1.482},
94    {category: 'c', amount: 3.988},
95    {category: 'd', amount: 3.293},
96    {category: 'e', amount: 7.921},
97    {category: 'f', amount: 0.312},
98    {category: 'g', amount: 1.377},
99    {category: 'h', amount: 1.072},
100    {category: 'i', amount: 8.286},
101    {category: 'j', amount: 2.343},
102    {category: 'k', amount: 3.411},
103    {category: 'l', amount: 2.136},
104    {category: 'm', amount: 2.911},
105    {category: 'n', amount: 5.6},
106    {category: 'o', amount: 7.59},
107    {category: 'p', amount: 3.101},
108    {category: 'q', amount: 0.003},
109    {category: 'r', amount: 4.571},
110    {category: 's', amount: 4.263},
111    {category: 't', amount: 3.966},
112    {category: 'u', amount: 2.347},
113    {category: 'v', amount: 0.034},
114    {category: 'w', amount: 4.549},
115    {category: 'x', amount: 0.019},
116    {category: 'y', amount: 3.857},
117    {category: 'z', amount: 5.62},
118  ],
119};
120
121const DATA_EMPTY = {};
122
123const SPEC_BAR_CHART = `
124{
125  "$schema": "https://vega.github.io/schema/vega/v5.json",
126  "description": "A basic bar chart example, with value labels shown upon mouse hover.",
127  "width": 400,
128  "height": 200,
129  "padding": 5,
130
131  "data": [
132    {
133      "name": "table"
134    }
135  ],
136
137  "signals": [
138    {
139      "name": "tooltip",
140      "value": {},
141      "on": [
142        {"events": "rect:mouseover", "update": "datum"},
143        {"events": "rect:mouseout",  "update": "{}"}
144      ]
145    }
146  ],
147
148  "scales": [
149    {
150      "name": "xscale",
151      "type": "band",
152      "domain": {"data": "table", "field": "category"},
153      "range": "width",
154      "padding": 0.05,
155      "round": true
156    },
157    {
158      "name": "yscale",
159      "domain": {"data": "table", "field": "amount"},
160      "nice": true,
161      "range": "height"
162    }
163  ],
164
165  "axes": [
166    { "orient": "bottom", "scale": "xscale" },
167    { "orient": "left", "scale": "yscale" }
168  ],
169
170  "marks": [
171    {
172      "type": "rect",
173      "from": {"data":"table"},
174      "encode": {
175        "enter": {
176          "x": {"scale": "xscale", "field": "category"},
177          "width": {"scale": "xscale", "band": 1},
178          "y": {"scale": "yscale", "field": "amount"},
179          "y2": {"scale": "yscale", "value": 0}
180        },
181        "update": {
182          "fill": {"value": "steelblue"}
183        },
184        "hover": {
185          "fill": {"value": "red"}
186        }
187      }
188    },
189    {
190      "type": "text",
191      "encode": {
192        "enter": {
193          "align": {"value": "center"},
194          "baseline": {"value": "bottom"},
195          "fill": {"value": "#333"}
196        },
197        "update": {
198          "x": {"scale": "xscale", "signal": "tooltip.category", "band": 0.5},
199          "y": {"scale": "yscale", "signal": "tooltip.amount", "offset": -2},
200          "text": {"signal": "tooltip.amount"},
201          "fillOpacity": [
202            {"test": "datum === tooltip", "value": 0},
203            {"value": 1}
204          ]
205        }
206      }
207    }
208  ]
209}
210`;
211
212const SPEC_BAR_CHART_LITE = `
213{
214  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
215  "description": "A simple bar chart with embedded data.",
216  "data": {
217    "name": "table"
218  },
219  "mark": "bar",
220  "encoding": {
221    "x": {"field": "category", "type": "nominal", "axis": {"labelAngle": 0}},
222    "y": {"field": "amount", "type": "quantitative"}
223  }
224}
225`;
226
227const SPEC_BROKEN = `{
228  "description": 123
229}
230`;
231
232enum SpecExample {
233  BarChart = 'Barchart',
234  BarChartLite = 'Barchart (Lite)',
235  Broken = 'Broken',
236}
237
238enum DataExample {
239  English = 'English',
240  Polish = 'Polish',
241  Empty = 'Empty',
242}
243
244function getExampleSpec(example: SpecExample): string {
245  switch (example) {
246    case SpecExample.BarChart:
247      return SPEC_BAR_CHART;
248    case SpecExample.BarChartLite:
249      return SPEC_BAR_CHART_LITE;
250    case SpecExample.Broken:
251      return SPEC_BROKEN;
252    default:
253      const exhaustiveCheck: never = example;
254      throw new Error(`Unhandled case: ${exhaustiveCheck}`);
255  }
256}
257
258function getExampleData(example: DataExample) {
259  switch (example) {
260    case DataExample.English:
261      return DATA_ENGLISH_LETTER_FREQUENCY;
262    case DataExample.Polish:
263      return DATA_POLISH_LETTER_FREQUENCY;
264    case DataExample.Empty:
265      return DATA_EMPTY;
266    default:
267      const exhaustiveCheck: never = example;
268      throw new Error(`Unhandled case: ${exhaustiveCheck}`);
269  }
270}
271
272const options: {[key: string]: boolean} = {
273  foobar: false,
274  foo: false,
275  bar: false,
276  baz: false,
277  qux: false,
278  quux: false,
279  corge: false,
280  grault: false,
281  garply: false,
282  waldo: false,
283  fred: false,
284  plugh: false,
285  xyzzy: false,
286  thud: false,
287};
288
289function PortalButton() {
290  let portalOpen = false;
291
292  return {
293    // eslint-disable-next-line @typescript-eslint/no-explicit-any
294    view: function ({attrs}: any) {
295      const {zIndex = true, absolute = true, top = true} = attrs;
296      return [
297        m(Button, {
298          label: 'Toggle Portal',
299          intent: Intent.Primary,
300          onclick: () => {
301            portalOpen = !portalOpen;
302            raf.scheduleFullRedraw();
303          },
304        }),
305        portalOpen &&
306          m(
307            Portal,
308            {
309              style: {
310                position: absolute && 'absolute',
311                top: top && '0',
312                zIndex: zIndex ? '10' : '0',
313                background: 'white',
314              },
315            },
316            m(
317              '',
318              `A very simple portal - a div rendered outside of the normal
319              flow of the page`,
320            ),
321          ),
322      ];
323    },
324  };
325}
326
327function lorem() {
328  const text = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
329      tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
330      veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
331      commodo consequat.Duis aute irure dolor in reprehenderit in voluptate
332      velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
333      cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
334      est laborum.`;
335  return m('', {style: {width: '200px'}}, text);
336}
337
338function ControlledPopup() {
339  let popupOpen = false;
340
341  return {
342    view: function () {
343      return m(
344        Popup,
345        {
346          trigger: m(Button, {label: `${popupOpen ? 'Close' : 'Open'} Popup`}),
347          isOpen: popupOpen,
348          onChange: (shouldOpen: boolean) => (popupOpen = shouldOpen),
349        },
350        m(Button, {
351          label: 'Close Popup',
352          onclick: () => {
353            popupOpen = !popupOpen;
354            raf.scheduleFullRedraw();
355          },
356        }),
357      );
358    },
359  };
360}
361
362type Options = {
363  [key: string]: EnumOption | boolean | string;
364};
365
366class EnumOption {
367  constructor(public initial: string, public options: string[]) {}
368}
369
370interface WidgetTitleAttrs {
371  label: string;
372}
373
374function recursiveTreeNode(): m.Children {
375  return m(LazyTreeNode, {
376    left: 'Recursive',
377    right: '...',
378    fetchData: async () => {
379      // await new Promise((r) => setTimeout(r, 1000));
380      return () => recursiveTreeNode();
381    },
382  });
383}
384
385class WidgetTitle implements m.ClassComponent<WidgetTitleAttrs> {
386  view({attrs}: m.CVnode<WidgetTitleAttrs>) {
387    const {label} = attrs;
388    const id = label.replaceAll(' ', '').toLowerCase();
389    const href = `#!/widgets#${id}`;
390    return m(Anchor, {id, href}, m('h2', label));
391  }
392}
393
394interface WidgetShowcaseAttrs {
395  label: string;
396  description?: string;
397  initialOpts?: Options;
398  // eslint-disable-next-line @typescript-eslint/no-explicit-any
399  renderWidget: (options: any) => any;
400  wide?: boolean;
401}
402
403// A little helper class to render any vnode with a dynamic set of options
404class WidgetShowcase implements m.ClassComponent<WidgetShowcaseAttrs> {
405  // eslint-disable-next-line @typescript-eslint/no-explicit-any
406  private optValues: any = {};
407  private opts?: Options;
408
409  renderOptions(listItems: m.Child[]): m.Child {
410    if (listItems.length === 0) {
411      return null;
412    }
413    return m('.widget-controls', m('h3', 'Options'), m('ul', listItems));
414  }
415
416  oninit({attrs: {initialOpts: opts}}: m.Vnode<WidgetShowcaseAttrs, this>) {
417    this.opts = opts;
418    if (opts) {
419      // Make the initial options values
420      for (const key in opts) {
421        if (Object.prototype.hasOwnProperty.call(opts, key)) {
422          const option = opts[key];
423          if (option instanceof EnumOption) {
424            this.optValues[key] = option.initial;
425          } else if (typeof option === 'boolean') {
426            this.optValues[key] = option;
427          } else if (isString(option)) {
428            this.optValues[key] = option;
429          }
430        }
431      }
432    }
433  }
434
435  view({attrs}: m.CVnode<WidgetShowcaseAttrs>) {
436    const {renderWidget, wide, label, description} = attrs;
437    const listItems = [];
438
439    if (this.opts) {
440      for (const key in this.opts) {
441        if (Object.prototype.hasOwnProperty.call(this.opts, key)) {
442          listItems.push(m('li', this.renderControlForOption(key)));
443        }
444      }
445    }
446
447    return [
448      m(WidgetTitle, {label}),
449      description && m('p', description),
450      m(
451        '.widget-block',
452        m(
453          'div',
454          {
455            class: classNames(
456              'widget-container',
457              wide && 'widget-container-wide',
458            ),
459          },
460          renderWidget(this.optValues),
461        ),
462        this.renderOptions(listItems),
463      ),
464    ];
465  }
466
467  private renderControlForOption(key: string) {
468    if (!this.opts) return null;
469    const value = this.opts[key];
470    if (value instanceof EnumOption) {
471      return this.renderEnumOption(key, value);
472    } else if (typeof value === 'boolean') {
473      return this.renderBooleanOption(key);
474    } else if (isString(value)) {
475      return this.renderStringOption(key);
476    } else {
477      return null;
478    }
479  }
480
481  private renderBooleanOption(key: string) {
482    return m(Checkbox, {
483      checked: this.optValues[key],
484      label: key,
485      onchange: () => {
486        this.optValues[key] = !this.optValues[key];
487        raf.scheduleFullRedraw();
488      },
489    });
490  }
491
492  private renderStringOption(key: string) {
493    return m(TextInput, {
494      placeholder: key,
495      value: this.optValues[key],
496      oninput: (e: Event) => {
497        this.optValues[key] = (e.target as HTMLInputElement).value;
498        raf.scheduleFullRedraw();
499      },
500    });
501  }
502
503  private renderEnumOption(key: string, opt: EnumOption) {
504    const optionElements = opt.options.map((option: string) => {
505      return m('option', {value: option}, option);
506    });
507    return m(
508      Select,
509      {
510        value: this.optValues[key],
511        onchange: (e: Event) => {
512          const el = e.target as HTMLSelectElement;
513          this.optValues[key] = el.value;
514          raf.scheduleFullRedraw();
515        },
516      },
517      optionElements,
518    );
519  }
520}
521
522interface File {
523  name: string;
524  size: string;
525  date: string;
526  children?: File[];
527}
528
529const files: File[] = [
530  {
531    name: 'foo',
532    size: '10MB',
533    date: '2023-04-02',
534  },
535  {
536    name: 'bar',
537    size: '123KB',
538    date: '2023-04-08',
539    children: [
540      {
541        name: 'baz',
542        size: '4KB',
543        date: '2023-05-07',
544      },
545      {
546        name: 'qux',
547        size: '18KB',
548        date: '2023-05-28',
549        children: [
550          {
551            name: 'quux',
552            size: '4KB',
553            date: '2023-05-07',
554          },
555          {
556            name: 'corge',
557            size: '18KB',
558            date: '2023-05-28',
559            children: [
560              {
561                name: 'grault',
562                size: '4KB',
563                date: '2023-05-07',
564              },
565              {
566                name: 'garply',
567                size: '18KB',
568                date: '2023-05-28',
569              },
570              {
571                name: 'waldo',
572                size: '87KB',
573                date: '2023-05-02',
574              },
575            ],
576          },
577        ],
578      },
579    ],
580  },
581  {
582    name: 'fred',
583    size: '8KB',
584    date: '2022-12-27',
585  },
586];
587
588let virtualTableData: {offset: number; rows: VirtualTableRow[]} = {
589  offset: 0,
590  rows: [],
591};
592
593export const WidgetsPage = createPage({
594  view() {
595    return m(
596      '.widgets-page',
597      m('h1', 'Widgets'),
598      m(WidgetShowcase, {
599        label: 'Button',
600        renderWidget: ({label, icon, rightIcon, ...rest}) =>
601          m(Button, {
602            icon: icon ? 'send' : undefined,
603            rightIcon: rightIcon ? 'arrow_forward' : undefined,
604            label: label ? 'Button' : '',
605            ...rest,
606          }),
607        initialOpts: {
608          label: true,
609          icon: true,
610          rightIcon: false,
611          disabled: false,
612          intent: new EnumOption(Intent.None, Object.values(Intent)),
613          active: false,
614          compact: false,
615          loading: false,
616        },
617      }),
618      m(WidgetShowcase, {
619        label: 'Checkbox',
620        renderWidget: (opts) => m(Checkbox, {label: 'Checkbox', ...opts}),
621        initialOpts: {
622          disabled: false,
623        },
624      }),
625      m(WidgetShowcase, {
626        label: 'Switch',
627        // eslint-disable-next-line @typescript-eslint/no-explicit-any
628        renderWidget: ({label, ...rest}: any) =>
629          m(Switch, {label: label ? 'Switch' : undefined, ...rest}),
630        initialOpts: {
631          label: true,
632          disabled: false,
633        },
634      }),
635      m(WidgetShowcase, {
636        label: 'Text Input',
637        renderWidget: ({placeholder, ...rest}) =>
638          m(TextInput, {
639            placeholder: placeholder ? 'Placeholder...' : '',
640            ...rest,
641          }),
642        initialOpts: {
643          placeholder: true,
644          disabled: false,
645        },
646      }),
647      m(WidgetShowcase, {
648        label: 'Select',
649        renderWidget: (opts) =>
650          m(Select, opts, [
651            m('option', {value: 'foo', label: 'Foo'}),
652            m('option', {value: 'bar', label: 'Bar'}),
653            m('option', {value: 'baz', label: 'Baz'}),
654          ]),
655        initialOpts: {
656          disabled: false,
657        },
658      }),
659      m(WidgetShowcase, {
660        label: 'Filterable Select',
661        renderWidget: () =>
662          m(FilterableSelect, {
663            values: ['foo', 'bar', 'baz'],
664            onSelected: () => {},
665          }),
666      }),
667      m(WidgetShowcase, {
668        label: 'Empty State',
669        renderWidget: ({header, content}) =>
670          m(
671            EmptyState,
672            {
673              title: header && 'No search results found...',
674            },
675            content && m(Button, {label: 'Try again'}),
676          ),
677        initialOpts: {
678          header: true,
679          content: true,
680        },
681      }),
682      m(WidgetShowcase, {
683        label: 'Anchor',
684        renderWidget: ({icon}) =>
685          m(
686            Anchor,
687            {
688              icon: icon && 'open_in_new',
689              href: 'https://perfetto.dev/docs/',
690              target: '_blank',
691            },
692            'Docs',
693          ),
694        initialOpts: {
695          icon: true,
696        },
697      }),
698      m(WidgetShowcase, {
699        label: 'Table',
700        renderWidget: () => m(TableShowcase),
701        initialOpts: {},
702        wide: true,
703      }),
704      m(WidgetShowcase, {
705        label: 'Portal',
706        description: `A portal is a div rendered out of normal flow
707          of the hierarchy.`,
708        renderWidget: (opts) => m(PortalButton, opts),
709        initialOpts: {
710          absolute: true,
711          zIndex: true,
712          top: true,
713        },
714      }),
715      m(WidgetShowcase, {
716        label: 'Popup',
717        description: `A popup is a nicely styled portal element whose position is
718        dynamically updated to appear to float alongside a specific element on
719        the page, even as the element is moved and scrolled around.`,
720        renderWidget: (opts) =>
721          m(
722            Popup,
723            {
724              trigger: m(Button, {label: 'Toggle Popup'}),
725              ...opts,
726            },
727            lorem(),
728          ),
729        initialOpts: {
730          position: new EnumOption(
731            PopupPosition.Auto,
732            Object.values(PopupPosition),
733          ),
734          closeOnEscape: true,
735          closeOnOutsideClick: true,
736        },
737      }),
738      m(WidgetShowcase, {
739        label: 'Controlled Popup',
740        description: `The open/close state of a controlled popup is passed in via
741        the 'isOpen' attribute. This means we can get open or close the popup
742        from wherever we like. E.g. from a button inside the popup.
743        Keeping this state external also means we can modify other parts of the
744        page depending on whether the popup is open or not, such as the text
745        on this button.
746        Note, this is the same component as the popup above, but used in
747        controlled mode.`,
748        renderWidget: (opts) => m(ControlledPopup, opts),
749        initialOpts: {},
750      }),
751      m(WidgetShowcase, {
752        label: 'Icon',
753        renderWidget: (opts) => m(Icon, {icon: 'star', ...opts}),
754        initialOpts: {filled: false},
755      }),
756      m(WidgetShowcase, {
757        label: 'MultiSelect panel',
758        renderWidget: ({...rest}) =>
759          m(MultiSelect, {
760            options: Object.entries(options).map(([key, value]) => {
761              return {
762                id: key,
763                name: key,
764                checked: value,
765              };
766            }),
767            onChange: (diffs: MultiSelectDiff[]) => {
768              diffs.forEach(({id, checked}) => {
769                options[id] = checked;
770              });
771              raf.scheduleFullRedraw();
772            },
773            ...rest,
774          }),
775        initialOpts: {
776          repeatCheckedItemsAtTop: false,
777          fixedSize: false,
778        },
779      }),
780      m(WidgetShowcase, {
781        label: 'Popup with MultiSelect',
782        renderWidget: ({icon, ...rest}) =>
783          m(PopupMultiSelect, {
784            options: Object.entries(options).map(([key, value]) => {
785              return {
786                id: key,
787                name: key,
788                checked: value,
789              };
790            }),
791            popupPosition: PopupPosition.Top,
792            label: 'Multi Select',
793            icon: icon ? Icons.LibraryAddCheck : undefined,
794            onChange: (diffs: MultiSelectDiff[]) => {
795              diffs.forEach(({id, checked}) => {
796                options[id] = checked;
797              });
798              raf.scheduleFullRedraw();
799            },
800            ...rest,
801          }),
802        initialOpts: {
803          icon: true,
804          showNumSelected: true,
805          repeatCheckedItemsAtTop: false,
806        },
807      }),
808      m(WidgetShowcase, {
809        label: 'PopupMenu',
810        renderWidget: () => {
811          return m(PopupMenuButton, {
812            icon: 'description',
813            items: [
814              {itemType: 'regular', text: 'New', callback: () => {}},
815              {itemType: 'regular', text: 'Open', callback: () => {}},
816              {itemType: 'regular', text: 'Save', callback: () => {}},
817              {itemType: 'regular', text: 'Delete', callback: () => {}},
818              {
819                itemType: 'group',
820                text: 'Share',
821                itemId: 'foo',
822                children: [
823                  {itemType: 'regular', text: 'Friends', callback: () => {}},
824                  {itemType: 'regular', text: 'Family', callback: () => {}},
825                  {itemType: 'regular', text: 'Everyone', callback: () => {}},
826                ],
827              },
828            ],
829          });
830        },
831      }),
832      m(WidgetShowcase, {
833        label: 'Menu',
834        renderWidget: () =>
835          m(
836            Menu,
837            m(MenuItem, {label: 'New', icon: 'add'}),
838            m(MenuItem, {label: 'Open', icon: 'folder_open'}),
839            m(MenuItem, {label: 'Save', icon: 'save', disabled: true}),
840            m(MenuDivider),
841            m(MenuItem, {label: 'Delete', icon: 'delete'}),
842            m(MenuDivider),
843            m(
844              MenuItem,
845              {label: 'Share', icon: 'share'},
846              m(MenuItem, {label: 'Everyone', icon: 'public'}),
847              m(MenuItem, {label: 'Friends', icon: 'group'}),
848              m(
849                MenuItem,
850                {label: 'Specific people', icon: 'person_add'},
851                m(MenuItem, {label: 'Alice', icon: 'person'}),
852                m(MenuItem, {label: 'Bob', icon: 'person'}),
853              ),
854            ),
855            m(
856              MenuItem,
857              {label: 'More', icon: 'more_horiz'},
858              m(MenuItem, {label: 'Query', icon: 'database'}),
859              m(MenuItem, {label: 'Download', icon: 'download'}),
860              m(MenuItem, {label: 'Clone', icon: 'copy_all'}),
861            ),
862          ),
863      }),
864      m(WidgetShowcase, {
865        label: 'PopupMenu2',
866        renderWidget: (opts) =>
867          m(
868            PopupMenu2,
869            {
870              trigger: m(Button, {
871                label: 'Menu',
872                rightIcon: Icons.ContextMenu,
873              }),
874              ...opts,
875            },
876            m(MenuItem, {label: 'New', icon: 'add'}),
877            m(MenuItem, {label: 'Open', icon: 'folder_open'}),
878            m(MenuItem, {label: 'Save', icon: 'save', disabled: true}),
879            m(MenuDivider),
880            m(MenuItem, {label: 'Delete', icon: 'delete'}),
881            m(MenuDivider),
882            m(
883              MenuItem,
884              {label: 'Share', icon: 'share'},
885              m(MenuItem, {label: 'Everyone', icon: 'public'}),
886              m(MenuItem, {label: 'Friends', icon: 'group'}),
887              m(
888                MenuItem,
889                {label: 'Specific people', icon: 'person_add'},
890                m(MenuItem, {label: 'Alice', icon: 'person'}),
891                m(MenuItem, {label: 'Bob', icon: 'person'}),
892              ),
893            ),
894            m(
895              MenuItem,
896              {label: 'More', icon: 'more_horiz'},
897              m(MenuItem, {label: 'Query', icon: 'database'}),
898              m(MenuItem, {label: 'Download', icon: 'download'}),
899              m(MenuItem, {label: 'Clone', icon: 'copy_all'}),
900            ),
901          ),
902        initialOpts: {
903          popupPosition: new EnumOption(
904            PopupPosition.Bottom,
905            Object.values(PopupPosition),
906          ),
907        },
908      }),
909      m(WidgetShowcase, {
910        label: 'Spinner',
911        description: `Simple spinner, rotates forever.
912            Width and height match the font size.`,
913        renderWidget: ({fontSize, easing}) =>
914          m('', {style: {fontSize}}, m(Spinner, {easing})),
915        initialOpts: {
916          fontSize: new EnumOption('16px', [
917            '12px',
918            '16px',
919            '24px',
920            '32px',
921            '64px',
922            '128px',
923          ]),
924          easing: false,
925        },
926      }),
927      m(WidgetShowcase, {
928        label: 'Tree',
929        description: `Hierarchical tree with left and right values aligned to
930        a grid.`,
931        renderWidget: (opts) =>
932          m(
933            Tree,
934            opts,
935            m(TreeNode, {left: 'Name', right: 'my_event', icon: 'badge'}),
936            m(TreeNode, {left: 'CPU', right: '2', icon: 'memory'}),
937            m(TreeNode, {
938              left: 'Start time',
939              right: '1s 435ms',
940              icon: 'schedule',
941            }),
942            m(TreeNode, {left: 'Duration', right: '86ms', icon: 'timer'}),
943            m(TreeNode, {
944              left: 'SQL',
945              right: m(
946                PopupMenu2,
947                {
948                  popupPosition: PopupPosition.RightStart,
949                  trigger: m(
950                    Anchor,
951                    {
952                      icon: Icons.ContextMenu,
953                    },
954                    'SELECT * FROM raw WHERE id = 123',
955                  ),
956                },
957                m(MenuItem, {
958                  label: 'Copy SQL Query',
959                  icon: 'content_copy',
960                }),
961                m(MenuItem, {
962                  label: 'Execute Query in new tab',
963                  icon: 'open_in_new',
964                }),
965              ),
966            }),
967            m(TreeNode, {
968              icon: 'account_tree',
969              left: 'Process',
970              right: m(Anchor, {icon: 'open_in_new'}, '/bin/foo[789]'),
971            }),
972            m(TreeNode, {
973              left: 'Thread',
974              right: m(Anchor, {icon: 'open_in_new'}, 'my_thread[456]'),
975            }),
976            m(
977              TreeNode,
978              {
979                left: 'Args',
980                summary: 'foo: string, baz: string, quux: string[4]',
981              },
982              m(TreeNode, {left: 'foo', right: 'bar'}),
983              m(TreeNode, {left: 'baz', right: 'qux'}),
984              m(
985                TreeNode,
986                {left: 'quux', summary: 'string[4]'},
987                m(TreeNode, {left: '[0]', right: 'corge'}),
988                m(TreeNode, {left: '[1]', right: 'grault'}),
989                m(TreeNode, {left: '[2]', right: 'garply'}),
990                m(TreeNode, {left: '[3]', right: 'waldo'}),
991              ),
992            ),
993            m(LazyTreeNode, {
994              left: 'Lazy',
995              icon: 'bedtime',
996              fetchData: async () => {
997                await new Promise((r) => setTimeout(r, 1000));
998                return () => m(TreeNode, {left: 'foo'});
999              },
1000            }),
1001            m(LazyTreeNode, {
1002              left: 'Dynamic',
1003              unloadOnCollapse: true,
1004              icon: 'bedtime',
1005              fetchData: async () => {
1006                await new Promise((r) => setTimeout(r, 1000));
1007                return () => m(TreeNode, {left: 'foo'});
1008              },
1009            }),
1010            recursiveTreeNode(),
1011          ),
1012        wide: true,
1013      }),
1014      m(WidgetShowcase, {
1015        label: 'Form',
1016        renderWidget: () => renderForm('form'),
1017      }),
1018      m(WidgetShowcase, {
1019        label: 'Nested Popups',
1020        renderWidget: () =>
1021          m(
1022            Popup,
1023            {
1024              trigger: m(Button, {label: 'Open the popup'}),
1025            },
1026            m(
1027              PopupMenu2,
1028              {
1029                trigger: m(Button, {label: 'Select an option'}),
1030              },
1031              m(MenuItem, {label: 'Option 1'}),
1032              m(MenuItem, {label: 'Option 2'}),
1033            ),
1034            m(Button, {
1035              label: 'Done',
1036              dismissPopup: true,
1037            }),
1038          ),
1039      }),
1040      m(WidgetShowcase, {
1041        label: 'Callout',
1042        renderWidget: () =>
1043          m(
1044            Callout,
1045            {
1046              icon: 'info',
1047            },
1048            'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' +
1049              'Nulla rhoncus tempor neque, sed malesuada eros dapibus vel. ' +
1050              'Aliquam in ligula vitae tortor porttitor laoreet iaculis ' +
1051              'finibus est.',
1052          ),
1053      }),
1054      m(WidgetShowcase, {
1055        label: 'Editor',
1056        renderWidget: () => m(Editor),
1057      }),
1058      m(WidgetShowcase, {
1059        label: 'VegaView',
1060        renderWidget: (opt) =>
1061          m(VegaView, {
1062            spec: getExampleSpec(opt.exampleSpec),
1063            data: getExampleData(opt.exampleData),
1064          }),
1065        initialOpts: {
1066          exampleSpec: new EnumOption(
1067            SpecExample.BarChart,
1068            Object.values(SpecExample),
1069          ),
1070          exampleData: new EnumOption(
1071            DataExample.English,
1072            Object.values(DataExample),
1073          ),
1074        },
1075      }),
1076      m(WidgetShowcase, {
1077        label: 'Form within PopupMenu2',
1078        description: `A form placed inside a popup menu works just fine,
1079              and the cancel/submit buttons also dismiss the popup. A bit more
1080              margin is added around it too, which improves the look and feel.`,
1081        renderWidget: () =>
1082          m(
1083            PopupMenu2,
1084            {
1085              trigger: m(Button, {label: 'Popup!'}),
1086            },
1087            m(
1088              MenuItem,
1089              {
1090                label: 'Open form...',
1091              },
1092              renderForm('popup-form'),
1093            ),
1094          ),
1095      }),
1096      m(WidgetShowcase, {
1097        label: 'Hotkey',
1098        renderWidget: (opts) => {
1099          if (opts.platform === 'auto') {
1100            return m(HotkeyGlyphs, {hotkey: opts.hotkey as Hotkey});
1101          } else {
1102            const platform = opts.platform as Platform;
1103            return m(HotkeyGlyphs, {
1104              hotkey: opts.hotkey as Hotkey,
1105              spoof: platform,
1106            });
1107          }
1108        },
1109        initialOpts: {
1110          hotkey: 'Mod+Shift+P',
1111          platform: new EnumOption('auto', ['auto', 'Mac', 'PC']),
1112        },
1113      }),
1114      m(WidgetShowcase, {
1115        label: 'Text Paragraph',
1116        description: `A basic formatted text paragraph with wrapping. If
1117              it is desirable to preserve the original text format/line breaks,
1118              set the compressSpace attribute to false.`,
1119        renderWidget: (opts) => {
1120          return m(TextParagraph, {
1121            text: `Lorem ipsum dolor sit amet, consectetur adipiscing
1122                         elit. Nulla rhoncus tempor neque, sed malesuada eros
1123                         dapibus vel. Aliquam in ligula vitae tortor porttitor
1124                         laoreet iaculis finibus est.`,
1125            compressSpace: opts.compressSpace,
1126          });
1127        },
1128        initialOpts: {
1129          compressSpace: true,
1130        },
1131      }),
1132      m(WidgetShowcase, {
1133        label: 'Multi Paragraph Text',
1134        description: `A wrapper for multiple paragraph widgets.`,
1135        renderWidget: () => {
1136          return m(
1137            MultiParagraphText,
1138            m(TextParagraph, {
1139              text: `Lorem ipsum dolor sit amet, consectetur adipiscing
1140                         elit. Nulla rhoncus tempor neque, sed malesuada eros
1141                         dapibus vel. Aliquam in ligula vitae tortor porttitor
1142                         laoreet iaculis finibus est.`,
1143              compressSpace: true,
1144            }),
1145            m(TextParagraph, {
1146              text: `Sed ut perspiciatis unde omnis iste natus error sit
1147                         voluptatem accusantium doloremque laudantium, totam rem
1148                         aperiam, eaque ipsa quae ab illo inventore veritatis et
1149                         quasi architecto beatae vitae dicta sunt explicabo.
1150                         Nemo enim ipsam voluptatem quia voluptas sit aspernatur
1151                         aut odit aut fugit, sed quia consequuntur magni dolores
1152                         eos qui ratione voluptatem sequi nesciunt.`,
1153              compressSpace: true,
1154            }),
1155          );
1156        },
1157      }),
1158      m(WidgetShowcase, {
1159        label: 'Modal',
1160        description: `A helper for modal dialog.`,
1161        renderWidget: () => m(ModalShowcase),
1162      }),
1163      m(WidgetShowcase, {
1164        label: 'TreeTable',
1165        description: `Hierarchical tree with multiple columns`,
1166        renderWidget: () => {
1167          const attrs: TreeTableAttrs<File> = {
1168            rows: files,
1169            getChildren: (file) => file.children,
1170            columns: [
1171              {name: 'Name', getData: (file) => file.name},
1172              {name: 'Size', getData: (file) => file.size},
1173              {name: 'Date', getData: (file) => file.date},
1174            ],
1175          };
1176          return m(TreeTable<File>, attrs);
1177        },
1178      }),
1179      m(WidgetShowcase, {
1180        label: 'VirtualTable',
1181        description: `Virtualized table for efficient rendering of large datasets`,
1182        renderWidget: () => {
1183          const attrs: VirtualTableAttrs = {
1184            columns: [
1185              {header: 'x', width: '4em'},
1186              {header: 'x^2', width: '8em'},
1187            ],
1188            rows: virtualTableData.rows,
1189            firstRowOffset: virtualTableData.offset,
1190            rowHeight: 20,
1191            numRows: 500_000,
1192            style: {height: '200px'},
1193            onReload: (rowOffset, rowCount) => {
1194              const rows = [];
1195              for (let i = rowOffset; i < rowOffset + rowCount; i++) {
1196                rows.push({id: i, cells: [i, i ** 2]});
1197              }
1198              virtualTableData = {
1199                offset: rowOffset,
1200                rows,
1201              };
1202              raf.scheduleFullRedraw();
1203            },
1204          };
1205          return m(VirtualTable, attrs);
1206        },
1207      }),
1208    );
1209  },
1210});
1211class ModalShowcase implements m.ClassComponent {
1212  private static counter = 0;
1213
1214  private static log(txt: string) {
1215    const mwlogs = document.getElementById('mwlogs');
1216    if (!mwlogs || !(mwlogs instanceof HTMLTextAreaElement)) return;
1217    const time = new Date().toLocaleTimeString();
1218    mwlogs.value += `[${time}] ${txt}\n`;
1219    mwlogs.scrollTop = mwlogs.scrollHeight;
1220  }
1221
1222  private static showModalDialog(staticContent = false) {
1223    const id = `N=${++ModalShowcase.counter}`;
1224    ModalShowcase.log(`Open ${id}`);
1225    const logOnClose = () => ModalShowcase.log(`Close ${id}`);
1226
1227    let content;
1228    if (staticContent) {
1229      content = m('.modal-pre', 'Content of the modal dialog.\nEnd of content');
1230    } else {
1231      const component = {
1232        oninit: function (vnode: m.Vnode<{}, {progress: number}>) {
1233          vnode.state.progress = ((vnode.state.progress as number) || 0) + 1;
1234        },
1235        view: function (vnode: m.Vnode<{}, {progress: number}>) {
1236          vnode.state.progress = (vnode.state.progress + 1) % 100;
1237          raf.scheduleFullRedraw();
1238          return m(
1239            'div',
1240            m('div', 'You should see an animating progress bar'),
1241            m('progress', {value: vnode.state.progress, max: 100}),
1242          );
1243        },
1244      } as m.Component<{}, {progress: number}>;
1245      content = () => m(component);
1246    }
1247    const closePromise = showModal({
1248      title: `Modal dialog ${id}`,
1249      buttons: [
1250        {text: 'OK', action: () => ModalShowcase.log(`OK ${id}`)},
1251        {text: 'Cancel', action: () => ModalShowcase.log(`Cancel ${id}`)},
1252        {
1253          text: 'Show another now',
1254          action: () => ModalShowcase.showModalDialog(),
1255        },
1256        {
1257          text: 'Show another in 2s',
1258          action: () => setTimeout(() => ModalShowcase.showModalDialog(), 2000),
1259        },
1260      ],
1261      content,
1262    });
1263    closePromise.then(logOnClose);
1264  }
1265
1266  view() {
1267    return m(
1268      'div',
1269      {
1270        style: {
1271          'display': 'flex',
1272          'flex-direction': 'column',
1273          'width': '100%',
1274        },
1275      },
1276      m('textarea', {
1277        id: 'mwlogs',
1278        readonly: 'readonly',
1279        rows: '8',
1280        placeholder: 'Logs will appear here',
1281      }),
1282      m('input[type=button]', {
1283        value: 'Show modal (static)',
1284        onclick: () => ModalShowcase.showModalDialog(true),
1285      }),
1286      m('input[type=button]', {
1287        value: 'Show modal (dynamic)',
1288        onclick: () => ModalShowcase.showModalDialog(false),
1289      }),
1290    );
1291  }
1292} // class ModalShowcase
1293
1294function renderForm(id: string) {
1295  return m(
1296    Form,
1297    {
1298      submitLabel: 'Submit',
1299      submitIcon: 'send',
1300      cancelLabel: 'Cancel',
1301      resetLabel: 'Reset',
1302      onSubmit: () => window.alert('Form submitted!'),
1303    },
1304    m(FormLabel, {for: `${id}-foo`}, 'Foo'),
1305    m(TextInput, {id: `${id}-foo`}),
1306    m(FormLabel, {for: `${id}-bar`}, 'Bar'),
1307    m(Select, {id: `${id}-bar`}, [
1308      m('option', {value: 'foo', label: 'Foo'}),
1309      m('option', {value: 'bar', label: 'Bar'}),
1310      m('option', {value: 'baz', label: 'Baz'}),
1311    ]),
1312  );
1313}
1314