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