Skip to content

Commit 9060c9e

Browse files
committed
feat: added basic search feature
1 parent 737b3d0 commit 9060c9e

3 files changed

Lines changed: 160 additions & 2 deletions

File tree

src/queries.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,14 @@ function escape_doublequote(s: string) {
1313
return s.replace(/"/g, '\\"');
1414
}
1515

16+
interface Rule {
17+
type: string;
18+
regex?: string;
19+
}
20+
1621
interface BaseQueryParams {
1722
include_audible?: boolean;
18-
classes: Record<string, any>;
23+
classes: [string[], Rule][];
1924
filter_classes: string[][];
2025
}
2126

@@ -45,7 +50,7 @@ function isAndroidParams(object: any): object is AndroidQueryParams {
4550
// - Categorization (if classes specified)
4651
// - Filters by category (if filter_classes set)
4752
// Puts it's results in `events` and `not_afk` (if not_afk available for platform).
48-
function canonicalEvents(params: DesktopQueryParams | AndroidQueryParams): string {
53+
export function canonicalEvents(params: DesktopQueryParams | AndroidQueryParams): string {
4954
// Needs escaping for regex patterns like '\w' to work (JSON.stringify adds extra unecessary escaping)
5055
const classes_str = JSON.stringify(params.classes).replace(/\\\\/g, '\\');
5156
const cat_filter_str = JSON.stringify(params.filter_classes);

src/route.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const QueryExplorer = () => import('./views/QueryExplorer.vue');
1313
const Timeline = () => import('./views/Timeline.vue');
1414
const Settings = () => import('./views/settings/Settings.vue');
1515
const Stopwatch = () => import('./views/Stopwatch.vue');
16+
const Search = () => import('./views/Search.vue');
1617
const Dev = () => import('./views/Dev.vue');
1718

1819
Vue.use(VueRouter);
@@ -46,6 +47,7 @@ const router = new VueRouter({
4647
{ path: '/query', component: QueryExplorer },
4748
{ path: '/settings', component: Settings },
4849
{ path: '/stopwatch', component: Stopwatch },
50+
{ path: '/search', component: Search },
4951
{ path: '/dev', component: Dev },
5052
],
5153
});

src/views/Search.vue

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
<template lang="pug">
2+
3+
div
4+
h3 Search
5+
6+
div.alert.alert-danger(v-if="error")
7+
| {{error}}
8+
9+
b-input-group(size="lg")
10+
b-input(v-model="pattern" placeholder="Regex pattern to search for")
11+
b-input-group-append
12+
b-button(type="button", @click="search()" variant="success")
13+
icon(name="search")
14+
| Search
15+
16+
div.d-flex.mt-1
17+
span.mr-auto.small(style="color: #666") Hostname: {{hostname}}
18+
b-button(size="sm", variant="outline-dark" style="border: 0" @click="show_options = !show_options")
19+
span(v-if="!show_options")
20+
| #[icon(name="angle-double-down")] Show options
21+
span(v-else)
22+
| #[icon(name="angle-double-up")] Hide options
23+
24+
div(v-if="show_options")
25+
h4 Options
26+
div Hostname
27+
select(v-model="hostname")
28+
option(v-for="hostname in Object.keys($store.getters['buckets/bucketsByHostname'])")
29+
| {{hostname}}
30+
div Start: {{start.format()}}
31+
div End: {{stop.format()}}
32+
//div
33+
label Use regex
34+
input(type="checkbox" v-model="use_regex")
35+
div
36+
label Filter AFK
37+
input(type="checkbox" v-model="filter_afk")
38+
//div.form-row
39+
div.form-group.col-md-6
40+
| Start
41+
input.form-control(type="date", :max="today", v-model="startdate")
42+
div.form-group.col-md-6
43+
| End
44+
input.form-control(type="date", :max="tomorrow", v-model="enddate")
45+
46+
div.form-inline
47+
48+
div(v-if="status == 'searching'")
49+
div #[icon(name="spinner" pulse)] Searching...
50+
51+
div(v-if="events != null")
52+
hr
53+
54+
div.form-group
55+
select.form-control(v-model="vis_method")
56+
option(value="eventlist") Event List
57+
option(value="timeline") Timeline
58+
option(value="summary") Summary
59+
option(value="raw") Raw JSON
60+
61+
div(v-if="vis_method == 'timeline'")
62+
aw-timeline(type="simple", :event_type="event_type", :events="events")
63+
div(v-if="vis_method == 'eventlist'")
64+
aw-eventlist(:events="events")
65+
div(v-if="vis_method == 'summary'")
66+
input.form-control(type="text" v-model.lazy.trim="summaryKey" placeholder="data key" style="margin-bottom: 1em;")
67+
aw-summary(:fields="events", :colorfunc="colorfunc", :namefunc="namefunc")
68+
div(v-if="vis_method == 'raw'")
69+
pre {{ events }}
70+
71+
div
72+
| Didn't find what you were looking for?
73+
br
74+
| Add a week to the search: #[b-button(size="sm" variant="outline-dark" @click="start = start.subtract(1, 'week'); search()") +1 week]
75+
76+
</template>
77+
78+
<style scoped lang="scss"></style>
79+
80+
<script>
81+
import _ from 'lodash';
82+
import moment from 'moment';
83+
import { canonicalEvents } from '~/queries';
84+
85+
import 'vue-awesome/icons/search';
86+
import 'vue-awesome/icons/spinner';
87+
import 'vue-awesome/icons/angle-double-down';
88+
import 'vue-awesome/icons/angle-double-up';
89+
90+
export default {
91+
name: 'Search',
92+
data() {
93+
return {
94+
pattern: '',
95+
vis_method: 'eventlist',
96+
event_type: 'currentwindow',
97+
events: null,
98+
error: '',
99+
status: null,
100+
hostname: '',
101+
102+
// Options
103+
show_options: false,
104+
use_regex: true,
105+
filter_afk: true,
106+
start: moment().subtract(1, 'day'),
107+
stop: moment().add(1, 'day'),
108+
109+
/* Summary props */
110+
summaryKey: 'title',
111+
colorfunc: null,
112+
namefunc: null,
113+
};
114+
},
115+
mounted: async function () {
116+
this.colorfunc = this.summaryKeyFunc;
117+
this.namefunc = this.summaryKeyFunc;
118+
await this.$store.dispatch('buckets/ensureBuckets');
119+
this.hostname = Object.keys(this.$store.getters['buckets/bucketsByHostname'])[0];
120+
},
121+
methods: {
122+
search: async function () {
123+
let query = canonicalEvents({
124+
bid_window: 'aw-watcher-window_' + this.hostname,
125+
bid_afk: 'aw-watcher-afk_' + this.hostname,
126+
filter_afk: this.filter_afk,
127+
classes: [[['searched'], { type: 'regex', regex: this.pattern }]],
128+
filter_classes: [['searched']],
129+
});
130+
query += '; RETURN = events;';
131+
132+
const query_array = query.split(';').map(s => s.trim() + ';');
133+
const timeperiods = [this.start.format() + '/' + this.stop.format()];
134+
try {
135+
this.status = 'searching';
136+
const data = await this.$aw.query(timeperiods, query_array);
137+
this.events = _.orderBy(data[0], ['timestamp'], ['desc']);
138+
this.error = '';
139+
} catch (e) {
140+
console.error(e);
141+
this.error = e.response.data.message;
142+
} finally {
143+
this.status = null;
144+
}
145+
},
146+
summaryKeyFunc: function (e) {
147+
return e.data[this.summaryKey];
148+
},
149+
},
150+
};
151+
</script>

0 commit comments

Comments
 (0)