• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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