Skip to content

Commit 291da6f

Browse files
ideadaptErikBjare
andauthored
feat(ux): improve timeline usability (#610)
* require fewer clicks for time interval changes. apply bootstrap styles. * notify user, that timeline was not updated / re-rendered on purpose (because there are no matching events to render). addresses issues reported in #395 * update lastUpdate timer display every 500ms * use for loop to render duration buttons. replace sr-only by d-none. * extra margin for small screens. * re-add sync button next to last update label. always show it, and label it "reload" instead of former "update". * Apply suggestions from code review * tests: fixed e2e test for new timeline inputs * fix: minor style improvements to new timeline --------- Co-authored-by: Erik Bjäreholt <[email protected]> Co-authored-by: Erik Bjäreholt <[email protected]>
1 parent 85c007e commit 291da6f

File tree

4 files changed

+78
-56
lines changed

4 files changed

+78
-56
lines changed

src/components/InputTimeInterval.vue

Lines changed: 67 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,58 @@
11
<template lang="pug">
22
div
33
div
4-
b-alert(v-if="mode == 'range' && invalidDaterange", variant="warning", show)
4+
b-alert(v-if="invalidDaterange", variant="warning", show)
55
| The selected date range is invalid. The second date must be greater or equal to the first date.
6-
b-alert(v-if="mode == 'range' && daterangeTooLong", variant="warning", show)
6+
b-alert(v-if="daterangeTooLong", variant="warning", show)
77
| The selected date range is too long. The maximum is {{ maxDuration/(24*60*60) }} days.
88

9-
div.d-flex.justify-content-between.align-items-end
9+
div.d-flex.justify-content-between
1010
table
1111
tr
12-
th.pr-2
13-
label(for="mode") Interval mode:
14-
td
15-
select(id="mode", v-model="mode")
16-
option(value='last_duration') Last duration
17-
option(value='range') Date range
18-
tr(v-if="mode == 'last_duration'")
19-
th.pr-2
20-
label(for="duration") Show last:
21-
td
22-
select(id="duration", v-model="duration", @change="valueChanged")
23-
option(:value="15*60") 15min
24-
option(:value="30*60") 30min
25-
option(:value="60*60") 1h
26-
option(:value="2*60*60") 2h
27-
option(:value="4*60*60") 4h
28-
option(:value="6*60*60") 6h
29-
option(:value="12*60*60") 12h
30-
option(:value="24*60*60") 24h
31-
tr(v-if="mode == 'range'")
32-
th.pr-2 Range:
12+
td.pr-2
13+
label.col-form-label.col-form-label-sm Show last
14+
td(colspan=2)
15+
.btn-group(role="group")
16+
template(v-for="(dur, idx) in durations")
17+
input(
18+
type="radio"
19+
:id="'dur' + idx"
20+
:value="dur.seconds"
21+
v-model="duration"
22+
@change="applyLastDuration"
23+
).d-none
24+
label(:for="'dur' + idx" v-html="dur.label").btn.btn-light.btn-sm
25+
26+
tr
27+
td.pr-2
28+
label.col-form-label.col-form-label-sm Show from
3329
td
34-
input(type="date", v-model="start")
35-
input(type="date", v-model="end")
36-
button(
37-
class="btn btn-outline-dark btn-sm",
30+
input.form-control.form-control-sm.d-inline-block.p-1(type="date", v-model="start", style="height: auto; width: auto;")
31+
label.col-form-label.col-form-label-sm.px-2 to
32+
input.form-control.form-control-sm.d-inline.p-1(type="date", v-model="end", style="height: auto; width: auto")
33+
td.text-right
34+
button.ml-2.btn.btn-outline-dark.btn-sm(
3835
type="button",
39-
:disabled="mode == 'range' && (invalidDaterange || emptyDaterange || daterangeTooLong)",
40-
@click="valueChanged"
41-
) Update
36+
:disabled="invalidDaterange || emptyDaterange || daterangeTooLong",
37+
@click="applyRange"
38+
) Apply
4239

43-
div(style="text-align:right" v-if="showUpdate && mode=='last_duration'")
44-
b-button.px-2(@click="update()", variant="outline-dark", size="sm")
40+
div.text-muted.d-none.d-md-block(style="text-align:right" v-if="showUpdate")
41+
b-button.mt-2.px-2(@click="refresh()", variant="outline-dark", size="sm", style="opacity: 0.7")
4542
icon(name="sync")
4643
span.d-none.d-md-inline
47-
| Update
48-
div.mt-1.small.text-muted(v-if="lastUpdate")
44+
| Refresh
45+
div.mt-2.small(v-if="lastUpdate")
4946
| Last update: #[time(:datetime="lastUpdate.format()") {{lastUpdate | friendlytime}}]
5047
</template>
5148

52-
<style scoped lang="scss"></style>
49+
<style scoped lang="scss">
50+
.btn-group {
51+
input[type='radio']:checked + label {
52+
background-color: #aaa;
53+
}
54+
}
55+
</style>
5356

5457
<script lang="ts">
5558
import moment from 'moment';
@@ -77,6 +80,18 @@ export default {
7780
start: null,
7881
end: null,
7982
lastUpdate: null,
83+
durations: [
84+
{ seconds: 0.25 * 60 * 60, label: '&frac14;h' },
85+
{ seconds: 0.5 * 60 * 60, label: '&frac12;h' },
86+
{ seconds: 60 * 60, label: '1h' },
87+
{ seconds: 2 * 60 * 60, label: '2h' },
88+
{ seconds: 3 * 60 * 60, label: '3h' },
89+
{ seconds: 4 * 60 * 60, label: '4h' },
90+
{ seconds: 6 * 60 * 60, label: '6h' },
91+
{ seconds: 12 * 60 * 60, label: '12h' },
92+
{ seconds: 24 * 60 * 60, label: '24h' },
93+
{ seconds: 48 * 60 * 60, label: '48h' },
94+
],
8095
};
8196
},
8297
computed: {
@@ -103,13 +118,13 @@ export default {
103118
this.duration = this.defaultDuration;
104119
this.valueChanged();
105120
106-
// We want our lastUpdated text to update every ~3s
121+
// We want our lastUpdated text to update every ~500ms
107122
// We can do this by setting it to null and then the previous value.
108123
this.lastUpdateTimer = setInterval(() => {
109124
const _lastUpdate = this.lastUpdate;
110125
this.lastUpdate = null;
111126
this.lastUpdate = _lastUpdate;
112-
}, 1000);
127+
}, 500);
113128
},
114129
beforeDestroy() {
115130
clearInterval(this.lastUpdateTimer);
@@ -124,12 +139,20 @@ export default {
124139
this.$emit('input', this.value);
125140
}
126141
},
127-
update() {
128-
if (this.mode == 'last_duration') {
129-
this.mode = ''; // remove cache on v-model, see explanation: https://github.com/ActivityWatch/aw-webui/pull/344/files#r892982094
130-
this.mode = 'last_duration';
131-
this.valueChanged();
132-
}
142+
refresh() {
143+
const tmpMode = this.mode;
144+
this.mode = '';
145+
this.mode = tmpMode;
146+
this.valueChanged();
147+
},
148+
applyRange() {
149+
this.mode = 'range';
150+
this.duration = 0;
151+
this.valueChanged();
152+
},
153+
applyLastDuration() {
154+
this.mode = 'last_duration';
155+
this.valueChanged();
133156
},
134157
},
135158
};

src/views/Timeline.vue

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@
22
div
33
h2 Timeline
44

5-
input-timeinterval(v-model="daterange", :defaultDuration="timeintervalDefaultDuration", :maxDuration="maxDuration").mb-2
5+
input-timeinterval(v-model="daterange", :defaultDuration="timeintervalDefaultDuration", :maxDuration="maxDuration").mb-3
66

77
// blocks
8-
div.d-inline-block.border.rounded.p-2.mr-2
9-
| Events shown: {{ num_events }}
108
details.d-inline-block.bg-light.small.border.rounded.mr-2.px-2
119
summary.p-2
1210
b Filters
@@ -26,7 +24,11 @@ div
2624
select(v-model="filter_client")
2725
option(:value='null') All
2826
option(v-for="client in clients", :value="client") {{ client }}
29-
div(style="float: right; color: #999").d-inline-block.pt-3
27+
div.d-inline-block.border.rounded.p-2.mr-2(v-if="num_events !== 0")
28+
| Events shown: {{ num_events }}
29+
b-alert.d-inline-block.p-2.mb-0.mt-2(v-if="num_events === 0", variant="warning", show)
30+
| No events match selected criteria. Timeline is not updated.
31+
div.float-right.small.text-muted.pt-3
3032
| Drag to pan and scroll to zoom
3133

3234
div(v-if="buckets !== null")
@@ -63,6 +65,7 @@ export default {
6365
const settingsStore = useSettingsStore();
6466
return Number(settingsStore.durationDefault);
6567
},
68+
// This does not match the chartData which is rendered in the timeline, as chartData excludes short events.
6669
num_events() {
6770
return _.sumBy(this.buckets, 'events.length');
6871
},

src/visualizations/VisTimeline.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
div
33
div#visualization
44

5-
div.small.my-2(v-if="bucketsFromEither.length != 1")
5+
div.small.text-muted.my-2(v-if="bucketsFromEither.length != 1")
66
i Buckets with no events in the queried range will be hidden.
77

88
div(v-if="editingEvent")

test/e2e/screenshot.test.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,6 @@ test.clientScripts({
121121

122122
fixture(`Timeline view`).page(`${baseURL}/#/timeline`).requestHooks(HTTPLogger);
123123

124-
const durationSelect = Selector('select#duration');
125-
const durationOption = durationSelect.find('option');
126-
127124
test.clientScripts({
128125
content: logJsErrorCode,
129126
})('Screenshot the timeline view', async t => {
@@ -134,10 +131,9 @@ test.clientScripts({
134131
});
135132
await waitForLoading(t);
136133
await t
137-
.click(durationSelect)
138-
.click(durationOption.withText('12h'))
139-
.expect(durationSelect.value)
140-
.eql('43200');
134+
.click(Selector('label').withText('12h'))
135+
.expect(Selector('input[value="43200"]').checked)
136+
.eql(true);
141137

142138
await t.takeScreenshot({
143139
path: 'timeline.png',

0 commit comments

Comments
 (0)