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