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