Repository navigation
fix(bucket): show event count and a not-found state for unknown buckets #1022
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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"> | ||
|
|
@@ -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; | ||
| }, | ||
| bucket_with_events() { | ||
| console.log(this.bucket); | ||
| return { | ||
| ...this.bucket, | ||
| events: this.events, | ||
|
|
@@ -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(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Comment on lines
+82
to
+84
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On a cold visit to an unknown bucket, 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) { | ||
|
|
@@ -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); | ||
|
|
||
| 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); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.