Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 49 additions & 32 deletions src/views/Bucket.vue
Original file line number Diff line number Diff line change
@@ -1,36 +1,41 @@
<template lang="pug">
div
h3 {{ id }}
table
tr
th Type:
td {{ bucket.type }}
tr
th Client:
td {{ bucket.client }}
tr
th Hostname:
td {{ bucket.hostname }}
tr
th Created:
td {{ bucket.created | iso8601 }}
tr(v-if="bucket.metadata")
th First/last event:
td
| {{ bucket.metadata.start}} /
| {{ bucket.metadata.end }}
tr
th Eventcount:
td {{ eventcount }}
tr(v-if="bucket.data && Object.keys(bucket.data).length > 0")
th Data:
td {{ bucket.data }}
b-alert(v-if="notFound", show, variant="warning")
| No bucket named "{{ id }}".
|
router-link(to="/buckets") See all buckets
div(v-else-if="bucket")
table
tr
th Type:
td {{ bucket.type }}
tr
th Client:
td {{ bucket.client }}
tr
th Hostname:
td {{ bucket.hostname }}
tr
th Created:
td {{ bucket.created | iso8601 }}
tr(v-if="bucket.metadata")
th First/last event:
td
| {{ bucket.metadata.start}} /
| {{ bucket.metadata.end }}
tr
th Eventcount:
td {{ eventcount }}
tr
th Data:
td {{ bucket.data }}

input-timeinterval(v-model="daterange", :maxDuration="maxDuration")
input-timeinterval(v-model="daterange", :maxDuration="maxDuration")

vis-timeline(:buckets="[bucket_with_events]", :showRowLabels="false")
vis-timeline(:buckets="[bucket_with_events]", :showRowLabels="false")

aw-eventlist(:bucket_id="id", @save="updateEvent", :events="events" editable=true)
aw-eventlist(:bucket_id="id", @save="updateEvent", :events="events" editable=true)
</template>

<script lang="ts">
Expand All @@ -48,16 +53,19 @@ export default {

events: [],
eventcount: '?',
loaded: false,
daterange: null,
maxDuration: 31 * 24 * 60 * 60,
};
},
computed: {
bucket() {
return this.bucketsStore.getBucket(this.id) || { id: this.id };
return this.bucketsStore.getBucket(this.id);
},
notFound() {
return this.loaded && !this.bucket;
Comment thread
TimeToBuildBob marked this conversation as resolved.
},
bucket_with_events() {
console.log(this.bucket);
return {
...this.bucket,
events: this.events,
Expand All @@ -71,7 +79,14 @@ export default {
},
mounted: async function () {
await this.bucketsStore.ensureLoaded();
await this.getEventCount(this.id);
if (!this.bucket) {
// The cached list may predate this bucket, so refresh before calling it missing.
await this.bucketsStore.loadBuckets();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Failed refresh leaves blank detail

If the bucket-list refresh fails, this await stops the mount hook before loaded becomes true. The page then shows only the bucket ID, with neither a not-found state nor an explanation that loading failed, and it does not retry.

Comment on lines +82 to +84

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unknown bucket fetches twice

On a cold visit to an unknown bucket, ensureLoaded() already fetches the empty bucket list. This branch fetches it again before showing “No bucket named …”, adding an unnecessary request and delay.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

}
this.loaded = true;
if (this.bucket) {
await this.getEventCount(this.id);
}
},
methods: {
getEvents: async function (bucket_id) {
Expand All @@ -83,8 +98,10 @@ export default {
this.events = bucket.events;
},
getEventCount: async function (bucket_id) {
// aw-client already unwraps the response body
this.eventcount = await getClient().countEvents(bucket_id);
const count = await getClient().countEvents(bucket_id);
// aw-client already unwraps the response body, except that 0.3.x returns the
// raw response when the body is falsy (a count of 0).
this.eventcount = typeof count === 'number' ? count : count.data;
},
updateEvent: function (event) {
const i = this.events.findIndex(e => e.id == event.id);
Expand Down
112 changes: 112 additions & 0 deletions test/unit/Bucket.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import Bucket from '~/views/Bucket.vue';

const mockCountEvents = jest.fn();
const mockGetBucketWithEvents = jest.fn();
const mockLoadBuckets = jest.fn();
const knownBucket = {
id: 'aw-watcher-window_host',
type: 'currentwindow',
hostname: 'host',
client: 'test',
};
// Reactive like the real Pinia store, so the view's `bucket` computed sees a refresh.
const mockState = Vue.observable({ buckets: [] });

jest.mock('~/util/awclient', () => ({
getClient: () => ({ countEvents: mockCountEvents }),
}));

jest.mock('~/stores/buckets', () => ({
useBucketsStore: () => ({
ensureLoaded: jest.fn().mockResolvedValue(undefined),
loadBuckets: mockLoadBuckets,
getBucket: id => mockState.buckets.find(b => b.id === id),
getBucketWithEvents: mockGetBucketWithEvents,
}),
}));

// Like the real input-timeinterval, emit an initial date range on mount.
const InputTimeIntervalStub = {
template: '<div class="timeinterval" />',
mounted() {
const t = iso => ({ format: () => iso });
this.$emit('input', [t('2026-10-01T00:00:00Z'), t('2026-10-02T00:00:00Z')]);
},
};

function mountBucket(id) {
return shallowMount(Bucket, {
propsData: { id },
stubs: {
'b-alert': { template: '<div class="alert"><slot /></div>' },
'router-link': { template: '<a><slot /></a>' },
'input-timeinterval': InputTimeIntervalStub,
'vis-timeline': true,
'aw-eventlist': true,
},
filters: { iso8601: v => v },
});
}

async function flush() {
for (let i = 0; i < 3; i++) await new Promise(resolve => setTimeout(resolve, 0));
}

describe('Bucket view', () => {
beforeEach(() => {
mockState.buckets = [knownBucket];
mockCountEvents.mockReset();
mockLoadBuckets.mockReset().mockResolvedValue(undefined);
mockGetBucketWithEvents.mockReset().mockResolvedValue({ ...knownBucket, events: [] });
});

test('shows the event count returned by countEvents', async () => {
mockCountEvents.mockResolvedValue(49828);
const wrapper = mountBucket('aw-watcher-window_host');
await flush();

expect(mockCountEvents).toHaveBeenCalledWith('aw-watcher-window_host');
expect(wrapper.vm.eventcount).toBe(49828);
expect(wrapper.text()).toContain('49828');
expect(wrapper.find('.alert').exists()).toBe(false);
expect(mockGetBucketWithEvents).toHaveBeenCalledWith(
expect.objectContaining({ id: 'aw-watcher-window_host' })
);
});

test('shows a count of 0 when aw-client returns the raw response', async () => {
mockCountEvents.mockResolvedValue({ data: 0, status: 200 });
const wrapper = mountBucket('aw-watcher-window_host');
await flush();

expect(wrapper.vm.eventcount).toBe(0);
});

test('refreshes a stale bucket list before calling a bucket missing', async () => {
const newBucket = { ...knownBucket, id: 'aw-watcher-new_host' };
mockLoadBuckets.mockImplementation(async () => {
mockState.buckets = [knownBucket, newBucket];
});
mockCountEvents.mockResolvedValue(3);
const wrapper = mountBucket('aw-watcher-new_host');
await flush();

expect(mockLoadBuckets).toHaveBeenCalled();
expect(wrapper.find('.alert').exists()).toBe(false);
expect(mockCountEvents).toHaveBeenCalledWith('aw-watcher-new_host');
expect(wrapper.vm.eventcount).toBe(3);
});

test('renders a not-found state for an unknown bucket', async () => {
const wrapper = mountBucket('does-not-exist');
await flush();

expect(mockLoadBuckets).toHaveBeenCalled();
expect(mockCountEvents).not.toHaveBeenCalled();
expect(mockGetBucketWithEvents).not.toHaveBeenCalled();
expect(wrapper.find('.alert').text()).toContain('No bucket named "does-not-exist"');
expect(wrapper.find('table').exists()).toBe(false);
});
});
Loading