1// Copyright (C) 2022 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 {Actions} from '../common/actions'; 18import {globals} from './globals'; 19 20export const LOG_PRIORITIES = 21 ['-', '-', 'Verbose', 'Debug', 'Info', 'Warn', 'Error', 'Fatal']; 22const IGNORED_STATES = 2; 23 24interface LogPriorityWidgetAttrs { 25 options: string[]; 26 selectedIndex: number; 27 onSelect: (id: number) => void; 28} 29 30interface LogTagChipAttrs { 31 name: string; 32 removeTag: (name: string) => void; 33} 34 35interface LogTagsWidgetAttrs { 36 tags: string[]; 37} 38 39interface FilterByTextWidgetAttrs { 40 hideNonMatching: boolean; 41} 42 43class LogPriorityWidget implements m.ClassComponent<LogPriorityWidgetAttrs> { 44 view(vnode: m.Vnode<LogPriorityWidgetAttrs>) { 45 const attrs = vnode.attrs; 46 const optionComponents = []; 47 for (let i = IGNORED_STATES; i < attrs.options.length; i++) { 48 const selected = i === attrs.selectedIndex; 49 optionComponents.push( 50 m('option', {value: i, selected}, attrs.options[i])); 51 } 52 return m( 53 'select', 54 { 55 onchange: (e: InputEvent) => { 56 const selectionValue = (e.target as HTMLSelectElement).value; 57 attrs.onSelect(Number(selectionValue)); 58 }, 59 }, 60 optionComponents, 61 ); 62 } 63} 64 65class LogTagChip implements m.ClassComponent<LogTagChipAttrs> { 66 view({attrs}: m.CVnode<LogTagChipAttrs>) { 67 return m( 68 '.chip', 69 m('.chip-text', attrs.name), 70 m('button.chip-button', 71 { 72 onclick: () => { 73 attrs.removeTag(attrs.name); 74 }, 75 }, 76 '×')); 77 } 78} 79 80class LogTagsWidget implements m.ClassComponent<LogTagsWidgetAttrs> { 81 removeTag(tag: string) { 82 globals.dispatch(Actions.removeLogTag({tag})); 83 } 84 85 view(vnode: m.Vnode<LogTagsWidgetAttrs>) { 86 const tags = vnode.attrs.tags; 87 return m( 88 '.tag-container', 89 m('.chips', tags.map((tag) => m(LogTagChip, { 90 name: tag, 91 removeTag: this.removeTag.bind(this), 92 }))), 93 m(`input.chip-input[placeholder='Add new tag']`, { 94 onkeydown: (e: KeyboardEvent) => { 95 // This is to avoid zooming on 'w'(and other unexpected effects 96 // of key presses in this input field). 97 e.stopPropagation(); 98 const htmlElement = e.target as HTMLInputElement; 99 100 // When the user clicks 'Backspace' we delete the previous tag. 101 if (e.key === 'Backspace' && tags.length > 0 && 102 htmlElement.value === '') { 103 globals.dispatch( 104 Actions.removeLogTag({tag: tags[tags.length - 1]})); 105 return; 106 } 107 108 if (e.key !== 'Enter') { 109 return; 110 } 111 if (htmlElement.value === '') { 112 return; 113 } 114 globals.dispatch( 115 Actions.addLogTag({tag: htmlElement.value.trim()})); 116 htmlElement.value = ''; 117 }, 118 })); 119 } 120} 121 122class LogTextWidget implements m.ClassComponent { 123 view() { 124 return m( 125 '.tag-container', m(`input.chip-input[placeholder='Search log text']`, { 126 onkeydown: (e: KeyboardEvent) => { 127 // This is to avoid zooming on 'w'(and other unexpected effects 128 // of key presses in this input field). 129 e.stopPropagation(); 130 }, 131 132 onkeyup: (e: KeyboardEvent) => { 133 // We want to use the value of the input field after it has been 134 // updated with the latest key (onkeyup). 135 const htmlElement = e.target as HTMLInputElement; 136 globals.dispatch( 137 Actions.updateLogFilterText({textEntry: htmlElement.value})); 138 }, 139 })); 140 } 141} 142 143class FilterByTextWidget implements m.ClassComponent<FilterByTextWidgetAttrs> { 144 view({attrs}: m.Vnode<FilterByTextWidgetAttrs>) { 145 const icon = attrs.hideNonMatching ? 'unfold_less' : 'unfold_more'; 146 const tooltip = attrs.hideNonMatching ? 'Expand all and view highlighted' : 147 'Collapse all'; 148 return m( 149 '.filter-widget', 150 m('.tooltip', tooltip), 151 m('i.material-icons', 152 { 153 onclick: () => { 154 globals.dispatch(Actions.toggleCollapseByTextEntry({})); 155 }, 156 }, 157 icon)); 158 } 159} 160 161export class LogsFilters implements m.ClassComponent { 162 view(_: m.CVnode<{}>) { 163 return m( 164 '.log-filters', 165 m('.log-label', 'Log Level'), 166 m(LogPriorityWidget, { 167 options: LOG_PRIORITIES, 168 selectedIndex: globals.state.logFilteringCriteria.minimumLevel, 169 onSelect: (minimumLevel) => { 170 globals.dispatch(Actions.setMinimumLogLevel({minimumLevel})); 171 }, 172 }), 173 m(LogTagsWidget, {tags: globals.state.logFilteringCriteria.tags}), 174 m(LogTextWidget), 175 m(FilterByTextWidget, { 176 hideNonMatching: globals.state.logFilteringCriteria.hideNonMatching, 177 })); 178 } 179} 180