Skip to content

Commit

Permalink
feat: updated design of 'Raw data' (Buckets) view (#448)
Browse files Browse the repository at this point in the history
  • Loading branch information
ErikBjare committed May 29, 2023
1 parent 9b387d2 commit 5484d68
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 50 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
node-version: [16]
python-version: ['3.9']
aw-server: ["aw-server", "aw-server-rust"]
aw-version: ["v0.12.1"]
aw-version: ["v0.12.3b3"]
include:
- node-version: '16'
python-version: '3.9'
Expand Down
62 changes: 59 additions & 3 deletions src/stores/buckets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import _ from 'lodash';
import { IBucket } from '~/util/interfaces';
import { defineStore } from 'pinia';
import { getClient } from '~/util/awclient';
import { useServerStore } from '~/stores/server';

function select_buckets(
buckets: IBucket[],
Expand All @@ -29,12 +30,12 @@ export const useBucketsStore = defineStore('buckets', {
getters: {
hosts(this: State): string[] {
// TODO: Include consideration of device_id UUID
return _.uniq(_.map(this.buckets, bucket => bucket.hostname));
return _.uniq(_.map(this.buckets, bucket => bucket.hostname || bucket.data.hostname));
},
// Uses device_id instead of hostname
devices(this: State): string[] {
// TODO: Include consideration of device_id UUID
return _.uniq(_.map(this.buckets, bucket => bucket.device_id));
return _.uniq(_.map(this.buckets, bucket => bucket.device_id || bucket.data.device_id));
},

available(): (hostname: string) => {
Expand Down Expand Up @@ -117,6 +118,52 @@ export const useBucketsStore = defineStore('buckets', {
bucketsByHostname(this: State): Record<string, IBucket[]> {
return _.groupBy(this.buckets, 'hostname');
},

// Group all buckets by their device.
// Returns a dict with buckets by device/host (hostname or device_id)
//
// First element will be the current hostname/device, if present.
// Others sorted by last_updated.
bucketsByDevice: function () {
let devices = _.mapValues(
_.groupBy(this.buckets, b => b.hostname || b.device_id),
d => {
const hostnames = _.uniq(_.map(d, b => b.hostname || b.data.hostname));
const device_ids = _.uniq(_.map(d, b => b.data.device_id || b.hostname));
return {
buckets: d,
device_id: device_ids[0],
device_ids,
hostname: hostnames[0],
hostnames,
first_seen: _.min(_.map(d, b => b.first_seen)),
last_updated: _.max(_.map(d, b => b.last_updated)),
};
}
);

// Sort by last_updated
const sortObjectByUpdated = _.flow([
_.toPairs,
pairs => _.orderBy(pairs, pair => pair[1].last_updated, ['desc']),
_.fromPairs,
]);
devices = sortObjectByUpdated(devices);

// find self-device and put first
const serverStore = useServerStore();
const hostname = serverStore.info && serverStore.info.hostname;
const currentDevice = Object.prototype.hasOwnProperty.call(devices, hostname)
? devices[hostname]
: null;
if (currentDevice) {
// remove self from list
delete devices[hostname];
// add self-device back to the top;
devices = { [hostname]: currentDevice, ...devices };
}
return devices;
},
},

actions: {
Expand Down Expand Up @@ -171,7 +218,16 @@ export const useBucketsStore = defineStore('buckets', {

// mutations
update_buckets(this: State, buckets: IBucket[]): void {
this.buckets = buckets;
this.buckets = _.orderBy(buckets, [b => b.id], ['asc']).map(b => {
// Some harmonization as aw-server-rust and aw-server-python APIs diverge slightly
if (!b.last_updated && b.metadata && b.metadata.end) {
b.last_updated = b.metadata.end;
}
if (!b.first_seen && b.metadata && b.metadata.start) {
b.first_seen = b.metadata.start;
}
return b;
});
},
},
});
3 changes: 3 additions & 0 deletions src/util/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ export interface IBucket {
device_id: string;
type: string;
data: Record<string, any>;
metadata?: { start: Date; end: Date };
last_updated?: Date;
first_seen?: Date;
}
152 changes: 107 additions & 45 deletions src/views/Buckets.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,73 @@ div
b-alert(show)
| Are you looking to collect more data? Check out #[a(href="https://activitywatch.readthedocs.io/en/latest/watchers.html") the docs] for more watchers.

b-table(hover, small, :items="buckets", :fields="fields", responsive="md", sort-by="last_updated", :sort-desc="true")
template(v-slot:cell(id)="data")
small
| {{ data.item.id }}
template(v-slot:cell(hostname)="data")
small
| {{ data.item.hostname }}
template(v-slot:cell(last_updated)="data")
// aw-server-python
small(v-if="data.item.last_updated")
| {{ data.item.last_updated | friendlytime }}
// aw-server-rust
small(v-if="data.item.metadata && data.item.metadata.end")
| {{ data.item.metadata.end | friendlytime }}
template(v-slot:cell(actions)="data")
b-button-toolbar.float-right
b-button-group(size="sm", class="mx-1")
b-button(variant="primary", :to="'/buckets/' + data.item.id")
icon(name="folder-open").d-none.d-md-inline-block
| Open
b-dropdown(variant="outline-secondary", size="sm", text="More")
// FIXME: These also exist as almost-copies in the Bucket view, can maybe be shared/reused instead.
b-dropdown-item(
:href="$aw.baseURL + '/api/0/buckets/' + data.item.id + '/export'",
:download="'aw-bucket-export-' + data.item.id + '.json'",
title="Export bucket to JSON",
variant="secondary")
icon(name="download")
| Export bucket as JSON
b-dropdown-item(
@click="export_csv(data.item.id)",
title="Export events to CSV",
variant="secondary")
icon(name="download")
| Export events as CSV
b-dropdown-divider
b-dropdown-item-button(@click="openDeleteBucketModal(data.item.id)",
title="Delete this bucket permanently",
variant="danger")
| #[icon(name="trash")] Delete bucket
// By device
b-card.mb-3(v-for="device in bucketsStore.bucketsByDevice", :key="device.hostname || device.device_id")
div.mb-3
div.d-flex
div
icon(v-if="device.hostname === 'unknown'" name="question")
// TODO: detect device type somewhere else (should unify with store logic)
icon(v-else, name="desktop")
| &nbsp;
div
b {{ device.hostname }}
span.small.ml-2(v-if="serverStore.info.hostname == device.hostname")
| (the current device)
div.small
div(v-if="device.hostname !== device.device_id", style="color: #666")
| ID: {{ device.id }}
div
| Last updated:&nbsp;
time(:style="{'color': isRecent(device.last_updated) ? 'green' : 'inherit'}",
:datetime="device.last_updated",
:title="device.last_updated")
| {{ device.last_updated | friendlytime }}
div
| First seen:&nbsp;
time(:datetime="device.first_seen",
:title="device.first_seen")
| {{ device.first_seen | friendlytime }}

b-row
b-col
b-table.mb-0(small, hover, :items="device.buckets", :fields="fields", responsive="md")
template(v-slot:cell(last_updated)="data")
small(v-if="data.item.last_updated", :style="{'color': isRecent(data.item.last_updated) ? 'green' : 'inherit'}")
| {{ data.item.last_updated | friendlytime }}
template(v-slot:cell(actions)="data")
b-button-toolbar.float-right
b-button-group(size="sm", class="mx-1")
b-button(variant="primary", :to="'/buckets/' + data.item.id")
icon(name="folder-open").d-none.d-md-inline-block
| Open
b-dropdown(variant="outline-secondary", size="sm", text="More")
// FIXME: These also exist as almost-copies in the Bucket view, can maybe be shared/reused instead.
b-dropdown-item(
:href="$aw.baseURL + '/api/0/buckets/' + data.item.id + '/export'",
:download="'aw-bucket-export-' + data.item.id + '.json'",
title="Export bucket to JSON",
variant="secondary")
icon(name="download")
| Export bucket as JSON
b-dropdown-item(
@click="export_csv(data.item.id)",
title="Export events to CSV",
variant="secondary")
icon(name="download")
| Export events as CSV
b-dropdown-divider
b-dropdown-item-button(@click="openDeleteBucketModal(data.item.id)",
title="Delete this bucket permanently",
variant="danger")
| #[icon(name="trash")] Delete bucket

// Checks
hr.mt-1(v-if="runChecks(device).length > 0")
div.small.text-muted(v-for="msg in runChecks(device)", style="color: #333")
icon(name="exclamation-triangle")
| &nbsp;
| {{ msg }}

b-modal(id="delete-modal", title="Danger!", centered, hide-footer)
| Are you sure you want to delete bucket "{{delete_bucket_selected}}"?
Expand Down Expand Up @@ -122,9 +149,16 @@ div
import 'vue-awesome/icons/trash';
import 'vue-awesome/icons/download';
import 'vue-awesome/icons/folder-open';
import 'vue-awesome/icons/desktop';
import 'vue-awesome/icons/mobile';
import 'vue-awesome/icons/question';
import 'vue-awesome/icons/exclamation-triangle';
import _ from 'lodash';
import Papa from 'papaparse';
import moment from 'moment';
import { useServerStore } from '~/stores/server';
import { useBucketsStore } from '~/stores/buckets';
export default {
Expand All @@ -135,7 +169,9 @@ export default {
},
data() {
return {
moment,
bucketsStore: useBucketsStore(),
serverStore: useServerStore(),
import_file: null,
import_error: null,
Expand All @@ -148,11 +184,6 @@ export default {
],
};
},
computed: {
buckets: function () {
return _.orderBy(this.bucketsStore.buckets, [b => b.id], ['asc']);
},
},
watch: {
import_file: async function (_new_value, _old_value) {
if (this.import_file != null) {
Expand All @@ -177,6 +208,37 @@ export default {
await this.bucketsStore.ensureLoaded();
},
methods: {
isRecent: function (date) {
return moment().diff(date) / 1000 < 120;
},
runChecks: function (device) {
const checks = [
{
msg: () => {
return `Device known by several hostnames: ${device.hostnames}`;
},
failed: () => device.hostnames.length > 1,
},
{
msg: () => {
return `Device known by several IDs: ${device.device_ids}`;
},
failed: () => device.device_ids.length > 1,
},
{
msg: () => {
return `Device is a special device, unattributed to a hostname, or not assigned a device ID.`;
},
failed: () => _.isEqual(device.hostnames, ['unknown']),
},
//{
// msg: () => 'just a test',
// failed: () => true,
//},
];
const failedChecks = _.filter(checks, c => c.failed());
return _.map(failedChecks, c => c.msg());
},
openDeleteBucketModal: function (bucketId: string) {
this.delete_bucket_selected = bucketId;
this.$root.$emit('bv::show::modal', 'delete-modal');
Expand Down
5 changes: 4 additions & 1 deletion static/dark.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ body {
background-color: #0f131a !important;
}

hr {
border-color: #282c32;
}

.aw-container {
background-color: #1a1d24 !important;
border-color: #282c32 !important;
Expand Down Expand Up @@ -99,7 +103,6 @@ body {

[class*=table-responsive-] > .table {
color: #e9ebf0 !important;
background-color: #343a40 !important;
}

[class*=table-responsive-] > .table * {
Expand Down

0 comments on commit 5484d68

Please sign in to comment.