xref: /aosp_15_r20/external/perfetto/ui/src/plugins/dev.perfetto.QueryPage/query_history.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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