• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2020 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 {channelChanged, getNextChannel, setChannel} from '../common/channels';
18import {featureFlags, Flag, OverrideState} from '../core/feature_flags';
19import {raf} from '../core/raf_scheduler';
20
21import {createPage} from './pages';
22import {Router} from './router';
23
24const RELEASE_PROCESS_URL =
25  'https://perfetto.dev/docs/visualization/perfetto-ui-release-process';
26
27interface FlagOption {
28  id: string;
29  name: string;
30}
31
32interface SelectWidgetAttrs {
33  id: string;
34  label: string;
35  description: m.Children;
36  options: FlagOption[];
37  selected: string;
38  onSelect: (id: string) => void;
39}
40
41class SelectWidget implements m.ClassComponent<SelectWidgetAttrs> {
42  view(vnode: m.Vnode<SelectWidgetAttrs>) {
43    const route = Router.parseUrl(window.location.href);
44    const attrs = vnode.attrs;
45    const cssClass = route.subpage === `/${attrs.id}` ? '.focused' : '';
46    return m(
47      '.flag-widget' + cssClass,
48      {id: attrs.id},
49      m('label', attrs.label),
50      m(
51        'select',
52        {
53          onchange: (e: InputEvent) => {
54            const value = (e.target as HTMLSelectElement).value;
55            attrs.onSelect(value);
56            raf.scheduleFullRedraw();
57          },
58        },
59        attrs.options.map((o) => {
60          const selected = o.id === attrs.selected;
61          return m('option', {value: o.id, selected}, o.name);
62        }),
63      ),
64      m('.description', attrs.description),
65    );
66  }
67}
68
69interface FlagWidgetAttrs {
70  flag: Flag;
71}
72
73class FlagWidget implements m.ClassComponent<FlagWidgetAttrs> {
74  view(vnode: m.Vnode<FlagWidgetAttrs>) {
75    const flag = vnode.attrs.flag;
76    const defaultState = flag.defaultValue ? 'Enabled' : 'Disabled';
77    return m(SelectWidget, {
78      label: flag.name,
79      id: flag.id,
80      description: flag.description,
81      options: [
82        {id: OverrideState.DEFAULT, name: `Default (${defaultState})`},
83        {id: OverrideState.TRUE, name: 'Enabled'},
84        {id: OverrideState.FALSE, name: 'Disabled'},
85      ],
86      selected: flag.overriddenState(),
87      onSelect: (value: string) => {
88        switch (value) {
89          case OverrideState.TRUE:
90            flag.set(true);
91            break;
92          case OverrideState.FALSE:
93            flag.set(false);
94            break;
95          default:
96          case OverrideState.DEFAULT:
97            flag.reset();
98            break;
99        }
100      },
101    });
102  }
103}
104
105export const FlagsPage = createPage({
106  view() {
107    const needsReload = channelChanged();
108    return m(
109      '.flags-page',
110      m(
111        '.flags-content',
112        m('h1', 'Feature flags'),
113        needsReload && [
114          m('h2', 'Please reload for your changes to take effect'),
115        ],
116        m(SelectWidget, {
117          label: 'Release channel',
118          id: 'releaseChannel',
119          description: [
120            'Which release channel of the UI to use. See ',
121            m(
122              'a',
123              {
124                href: RELEASE_PROCESS_URL,
125              },
126              'Release Process',
127            ),
128            ' for more information.',
129          ],
130          options: [
131            {id: 'stable', name: 'Stable (default)'},
132            {id: 'canary', name: 'Canary'},
133            {id: 'autopush', name: 'Autopush'},
134          ],
135          selected: getNextChannel(),
136          onSelect: (id) => setChannel(id),
137        }),
138        m(
139          'button',
140          {
141            onclick: () => {
142              featureFlags.resetAll();
143              raf.scheduleFullRedraw();
144            },
145          },
146          'Reset all below',
147        ),
148
149        featureFlags.allFlags().map((flag) => m(FlagWidget, {flag})),
150      ),
151    );
152  },
153
154  oncreate(vnode: m.VnodeDOM) {
155    const route = Router.parseUrl(window.location.href);
156    const flagId = /[/](\w+)/.exec(route.subpage)?.slice(1, 2)[0];
157    if (flagId) {
158      const flag = vnode.dom.querySelector(`#${flagId}`);
159      if (flag) {
160        flag.scrollIntoView({block: 'center'});
161      }
162    }
163  },
164});
165