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 {Icons} from '../base/semantic_icons'; 18 19import { 20 arrayOf, 21 bool, 22 record, 23 runValidator, 24 str, 25 ValidatedType, 26} from '../base/validators'; 27import {assertTrue} from '../base/logging'; 28import {Icon} from '../widgets/icon'; 29import {raf} from '../core/raf_scheduler'; 30 31const QUERY_HISTORY_KEY = 'queryHistory'; 32 33export interface QueryHistoryComponentAttrs { 34 runQuery: (query: string) => void; 35 setQuery: (query: string) => void; 36} 37 38export class QueryHistoryComponent 39 implements m.ClassComponent<QueryHistoryComponentAttrs> 40{ 41 view({attrs}: m.CVnode<QueryHistoryComponentAttrs>): m.Child { 42 const runQuery = attrs.runQuery; 43 const setQuery = attrs.setQuery; 44 const unstarred: HistoryItemComponentAttrs[] = []; 45 const starred: HistoryItemComponentAttrs[] = []; 46 for (let i = queryHistoryStorage.data.length - 1; i >= 0; i--) { 47 const entry = queryHistoryStorage.data[i]; 48 const arr = entry.starred ? starred : unstarred; 49 arr.push({index: i, entry, runQuery, setQuery}); 50 } 51 return m( 52 '.query-history', 53 m( 54 'header.overview', 55 `Query history (${queryHistoryStorage.data.length} queries)`, 56 ), 57 starred.map((attrs) => m(HistoryItemComponent, attrs)), 58 unstarred.map((attrs) => m(HistoryItemComponent, attrs)), 59 ); 60 } 61} 62 63export interface HistoryItemComponentAttrs { 64 index: number; 65 entry: QueryHistoryEntry; 66 runQuery: (query: string) => void; 67 setQuery: (query: string) => void; 68} 69 70export class HistoryItemComponent 71 implements m.ClassComponent<HistoryItemComponentAttrs> 72{ 73 view(vnode: m.Vnode<HistoryItemComponentAttrs>): m.Child { 74 const query = vnode.attrs.entry.query; 75 return m( 76 '.history-item', 77 m( 78 '.history-item-buttons', 79 m( 80 'button', 81 { 82 onclick: () => { 83 queryHistoryStorage.setStarred( 84 vnode.attrs.index, 85 !vnode.attrs.entry.starred, 86 ); 87 raf.scheduleFullRedraw(); 88 }, 89 }, 90 m(Icon, {icon: Icons.Star, filled: vnode.attrs.entry.starred}), 91 ), 92 m( 93 'button', 94 { 95 onclick: () => vnode.attrs.setQuery(query), 96 }, 97 m(Icon, {icon: 'edit'}), 98 ), 99 m( 100 'button', 101 { 102 onclick: () => vnode.attrs.runQuery(query), 103 }, 104 m(Icon, {icon: 'play_arrow'}), 105 ), 106 m( 107 'button', 108 { 109 onclick: () => { 110 queryHistoryStorage.remove(vnode.attrs.index); 111 raf.scheduleFullRedraw(); 112 }, 113 }, 114 m(Icon, {icon: 'delete'}), 115 ), 116 ), 117 m( 118 'pre', 119 { 120 onclick: () => vnode.attrs.setQuery(query), 121 ondblclick: () => vnode.attrs.runQuery(query), 122 }, 123 query, 124 ), 125 ); 126 } 127} 128 129class HistoryStorage { 130 data: QueryHistory; 131 maxItems = 50; 132 133 constructor() { 134 this.data = this.load(); 135 } 136 137 saveQuery(query: string) { 138 const items = this.data; 139 let firstUnstarred = -1; 140 let countUnstarred = 0; 141 for (let i = 0; i < items.length; i++) { 142 if (!items[i].starred) { 143 countUnstarred++; 144 if (firstUnstarred === -1) { 145 firstUnstarred = i; 146 } 147 } 148 149 if (items[i].query === query) { 150 // Query is already in the history, no need to save 151 return; 152 } 153 } 154 155 if (countUnstarred >= this.maxItems) { 156 assertTrue(firstUnstarred !== -1); 157 items.splice(firstUnstarred, 1); 158 } 159 160 items.push({query, starred: false}); 161 this.save(); 162 } 163 164 setStarred(index: number, starred: boolean) { 165 assertTrue(index >= 0 && index < this.data.length); 166 this.data[index].starred = starred; 167 this.save(); 168 } 169 170 remove(index: number) { 171 assertTrue(index >= 0 && index < this.data.length); 172 this.data.splice(index, 1); 173 this.save(); 174 } 175 176 private load(): QueryHistory { 177 const value = window.localStorage.getItem(QUERY_HISTORY_KEY); 178 if (value === null) { 179 return []; 180 } 181 182 return runValidator(queryHistoryValidator, JSON.parse(value)).result; 183 } 184 185 private save() { 186 window.localStorage.setItem(QUERY_HISTORY_KEY, JSON.stringify(this.data)); 187 } 188} 189 190const queryHistoryEntryValidator = record({query: str(), starred: bool()}); 191 192type QueryHistoryEntry = ValidatedType<typeof queryHistoryEntryValidator>; 193 194const queryHistoryValidator = arrayOf(queryHistoryEntryValidator); 195 196type QueryHistory = ValidatedType<typeof queryHistoryValidator>; 197 198export const queryHistoryStorage = new HistoryStorage(); 199