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 }, 81 }, 82 m(Icon, {icon: Icons.Star, filled: vnode.attrs.entry.starred}), 83 ), 84 m( 85 'button', 86 { 87 onclick: () => vnode.attrs.setQuery(query), 88 }, 89 m(Icon, {icon: 'edit'}), 90 ), 91 m( 92 'button', 93 { 94 onclick: () => vnode.attrs.runQuery(query), 95 }, 96 m(Icon, {icon: 'play_arrow'}), 97 ), 98 m( 99 'button', 100 { 101 onclick: () => { 102 queryHistoryStorage.remove(vnode.attrs.index); 103 }, 104 }, 105 m(Icon, {icon: 'delete'}), 106 ), 107 ), 108 m( 109 'pre', 110 { 111 onclick: () => vnode.attrs.setQuery(query), 112 ondblclick: () => vnode.attrs.runQuery(query), 113 }, 114 query, 115 ), 116 ); 117 } 118} 119 120class HistoryStorage { 121 data: QueryHistory; 122 maxItems = 50; 123 124 constructor() { 125 this.data = this.load(); 126 } 127 128 saveQuery(query: string) { 129 const items = this.data; 130 let firstUnstarred = -1; 131 let countUnstarred = 0; 132 for (let i = 0; i < items.length; i++) { 133 if (!items[i].starred) { 134 countUnstarred++; 135 if (firstUnstarred === -1) { 136 firstUnstarred = i; 137 } 138 } 139 140 if (items[i].query === query) { 141 // Query is already in the history, no need to save 142 return; 143 } 144 } 145 146 if (countUnstarred >= this.maxItems) { 147 assertTrue(firstUnstarred !== -1); 148 items.splice(firstUnstarred, 1); 149 } 150 151 items.push({query, starred: false}); 152 this.save(); 153 } 154 155 setStarred(index: number, starred: boolean) { 156 assertTrue(index >= 0 && index < this.data.length); 157 this.data[index].starred = starred; 158 this.save(); 159 } 160 161 remove(index: number) { 162 assertTrue(index >= 0 && index < this.data.length); 163 this.data.splice(index, 1); 164 this.save(); 165 } 166 167 private load(): QueryHistory { 168 const value = window.localStorage.getItem(QUERY_HISTORY_KEY); 169 if (value === null) { 170 return []; 171 } 172 const res = QUERY_HISTORY_SCHEMA.safeParse(JSON.parse(value)); 173 return res.success ? res.data : []; 174 } 175 176 private save() { 177 window.localStorage.setItem(QUERY_HISTORY_KEY, JSON.stringify(this.data)); 178 } 179} 180 181const QUERY_HISTORY_ENTRY_SCHEMA = z.object({ 182 query: z.string(), 183 starred: z.boolean().default(false), 184}); 185 186type QueryHistoryEntry = z.infer<typeof QUERY_HISTORY_ENTRY_SCHEMA>; 187 188const QUERY_HISTORY_SCHEMA = z.array(QUERY_HISTORY_ENTRY_SCHEMA); 189 190type QueryHistory = z.infer<typeof QUERY_HISTORY_SCHEMA>; 191 192export const queryHistoryStorage = new HistoryStorage(); 193