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