// Copyright (C) 2022 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import m from 'mithril'; import {globals} from './globals'; import {STAR} from './icons'; import { arrayOf, bool, record, runValidator, str, ValidatedType, } from '../controller/validators'; import {assertTrue} from '../base/logging'; import {Icon} from './widgets/icon'; import {runAnalyzeQuery} from './analyze_page'; const QUERY_HISTORY_KEY = 'queryHistory'; export class QueryHistoryComponent implements m.ClassComponent { view(): m.Child { const unstarred: HistoryItemComponentAttrs[] = []; const starred: HistoryItemComponentAttrs[] = []; for (let i = queryHistoryStorage.data.length - 1; i >= 0; i--) { const entry = queryHistoryStorage.data[i]; const arr = entry.starred ? starred : unstarred; arr.push({index: i, entry}); } return m( '.query-history', m('header.overview', `Query history (${queryHistoryStorage.data.length} queries)`), starred.map((attrs) => m(HistoryItemComponent, attrs)), unstarred.map((attrs) => m(HistoryItemComponent, attrs))); } } export interface HistoryItemComponentAttrs { index: number; entry: QueryHistoryEntry; } export class HistoryItemComponent implements m.ClassComponent { view(vnode: m.Vnode): m.Child { const query = vnode.attrs.entry.query; return m( '.history-item', m('.history-item-buttons', m( 'button', { onclick: () => { queryHistoryStorage.setStarred( vnode.attrs.index, !vnode.attrs.entry.starred); globals.rafScheduler.scheduleFullRedraw(); }, }, m(Icon, {icon: STAR, filled: vnode.attrs.entry.starred}), ), m('button', { onclick: () => runAnalyzeQuery(query), }, m(Icon, {icon: 'play_arrow'})), m('button', { onclick: () => { queryHistoryStorage.remove(vnode.attrs.index); globals.rafScheduler.scheduleFullRedraw(); }, }, m(Icon, {icon: 'delete'}))), m('pre', query)); } } class HistoryStorage { data: QueryHistory; maxItems = 50; constructor() { this.data = this.load(); } saveQuery(query: string) { const items = this.data; let firstUnstarred = -1; let countUnstarred = 0; for (let i = 0; i < items.length; i++) { if (!items[i].starred) { countUnstarred++; if (firstUnstarred === -1) { firstUnstarred = i; } } if (items[i].query === query) { // Query is already in the history, no need to save return; } } if (countUnstarred >= this.maxItems) { assertTrue(firstUnstarred !== -1); items.splice(firstUnstarred, 1); } items.push({query, starred: false}); this.save(); } setStarred(index: number, starred: boolean) { assertTrue(index >= 0 && index < this.data.length); this.data[index].starred = starred; this.save(); } remove(index: number) { assertTrue(index >= 0 && index < this.data.length); this.data.splice(index, 1); this.save(); } private load(): QueryHistory { const value = window.localStorage.getItem(QUERY_HISTORY_KEY); if (value === null) { return []; } return runValidator(queryHistoryValidator, JSON.parse(value)).result; } private save() { window.localStorage.setItem(QUERY_HISTORY_KEY, JSON.stringify(this.data)); } } const queryHistoryEntryValidator = record({query: str(), starred: bool()}); type QueryHistoryEntry = ValidatedType; const queryHistoryValidator = arrayOf(queryHistoryEntryValidator); type QueryHistory = ValidatedType; export const queryHistoryStorage = new HistoryStorage();