Skip to content
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

✨ OSIDB-3562: Flaw history filtering #477

Merged
merged 4 commits into from
Dec 3, 2024
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
### Added
* Add component column to trackers table (`OSIDB-3721`)
* Warning when filing trackers on CVEs with low severity (`OSIDB-3429`)
* Date range filter on history (`OSIDB-3562`)

### Fixed
* Fix trackers status filter not working (`OSIDB-3720`)
Expand Down
139 changes: 106 additions & 33 deletions src/components/FlawHistory/FlawHistory.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { computed, ref } from 'vue';

import { usePagination } from '@/composables/usePagination';

import EditableDate from '@/widgets/EditableDate/EditableDate.vue';
import { capitalize, formatDateWithTimezone } from '@/utils/helpers';
import { flawFieldNamesMapping } from '@/constants/flawFields';
import type { ZodFlawHistoryItemType } from '@/types/zodFlaw';
Expand All @@ -12,11 +13,47 @@ const props = defineProps<{
history: null | undefined | ZodFlawHistoryItemType[];
}>();

const startDate = ref<null | string | undefined>(null);
const endDate = ref<null | string | undefined>(null);

const emptyFilters = computed(() => {
return !startDate.value && !endDate.value;
});

const validDateRange = computed(() => {
const start = startDate.value ? new Date(startDate.value) : null;
const end = endDate.value ? new Date(endDate.value) : null;

return (
start instanceof Date && !Number.isNaN(start.getTime())
&& end instanceof Date && !Number.isNaN(end.getTime())
&& end >= start
);
});

const filteredHistoryItems = computed(() => {
if (!validDateRange.value) {
return props.history;
}

const start = new Date(startDate.value!);
const end = new Date(endDate.value!);
end.setDate(end.getDate() + 1);

return props.history?.filter((item) => {
if (!item.pgh_created_at) return false;

const itemDate = new Date(item.pgh_created_at);

return itemDate.getTime() >= start.getTime() && itemDate.getTime() <= end.getTime();
C-Valen marked this conversation as resolved.
Show resolved Hide resolved
});
});

const historyExpanded = ref(true);

const itemsPerPage = 20;
const totalPages = computed(() =>
Math.ceil((props.history?.length || 0) / itemsPerPage),
Math.ceil((filteredHistoryItems.value?.length || 0) / itemsPerPage),
);

const {
Expand All @@ -28,12 +65,17 @@ const {
const paginatedHistoryItems = computed(() => {
const start = (currentPage.value - 1) * itemsPerPage;
const end = start + itemsPerPage;
return props.history?.slice(start, end);
return filteredHistoryItems.value?.slice(start, end);
});

function isDateField(field: string) {
return field.includes('_dt');
}

function clearFilters() {
startDate.value = null;
endDate.value = null;
}
</script>

<template>
Expand All @@ -51,37 +93,68 @@ function isDateField(field: string) {
<template v-if="!history?.length">
<span>There are no tracked changes for this flaw.</span>
</template>
<div v-else class="mt-2">
<label class="mx-2 form-label w-100">
<template v-for="historyEntry in paginatedHistoryItems" :key="historyEntry.pgh_slug">
<div v-if="historyEntry.pgh_diff" class="alert alert-info mb-1 p-2">
<span>
{{ formatDateWithTimezone(historyEntry.pgh_created_at || '') }}
- {{ historyEntry.pgh_context?.user || 'System' }}
</span>
<ul class="mb-2">
<li v-for="(diffEntry, diffKey) in historyEntry.pgh_diff" :key="diffKey">
<div class="ms-3 pb-0">
<span>{{ capitalize(historyEntry.pgh_label) }}</span>
<span class="fw-bold">{{ ' ' + (flawFieldNamesMapping[diffKey] || diffKey) }}</span>
<span>{{ ': ' +
(isDateField(diffKey) && diffEntry[0]
? formatDateWithTimezone(diffEntry[0])
: (diffEntry[0]?.toString() || '')
) + ' '
}}</span>
<i class="bi bi-arrow-right" />
{{ (isDateField(diffKey) && diffEntry[1]
? formatDateWithTimezone(diffEntry[1])
: (diffEntry[1]?.toString() || '')
) }}
</div>
</li>
</ul>
</div>
</template>
</label>
</div>
<template v-else>
<div class="d-flex mb-2">
<button
tabindex="-1"
type="button"
:disabled="emptyFilters"
class="input-group-text ms-2"
@click="clearFilters()"
@mousedown="event => event.preventDefault()"
>
<i class="bi bi-eraser"></i>
</button>
<EditableDate
v-model="startDate as string"
class="ms-2"
style="width: 225px;"
placeholder="[Start date]"
/>
<EditableDate
v-model="endDate as string"
class="ms-2"
style="width: 225px;"
placeholder="[End date]"
/>
</div>
<template v-if="!filteredHistoryItems?.length">
<span>There are no results for current filter.</span>
</template>
<div v-else class="mt-2">
<div class="mt-2">
<label class="mx-2 form-label w-100">
<template v-for="historyEntry in paginatedHistoryItems" :key="historyEntry.pgh_slug">
<div v-if="historyEntry.pgh_diff" class="alert alert-info mb-1 p-2">
<span>
{{ formatDateWithTimezone(historyEntry.pgh_created_at || '') }}
- {{ historyEntry.pgh_context?.user || 'System' }}
</span>
<ul class="mb-2">
<li v-for="(diffEntry, diffKey) in historyEntry.pgh_diff" :key="diffKey">
<div class="ms-3 pb-0">
<span>{{ capitalize(historyEntry.pgh_label) }}</span>
<span class="fw-bold">{{ ' ' + (flawFieldNamesMapping[diffKey] || diffKey) }}</span>
<span>{{ ': ' +
(isDateField(diffKey) && diffEntry[0]
? formatDateWithTimezone(diffEntry[0])
: (diffEntry[0]?.toString() || '')
) + ' '
}}</span>
<i class="bi bi-arrow-right" />
{{ (isDateField(diffKey) && diffEntry[1]
? formatDateWithTimezone(diffEntry[1])
: (diffEntry[1]?.toString() || '')
) }}
</div>
</li>
</ul>
</div>
</template>
</label>
</div>
</div>
</template>
</LabelCollapsible>
<div v-if="history?.length" class="pagination-controls gap-1 my-2">
<button
Expand Down
86 changes: 86 additions & 0 deletions src/components/__tests__/FlawHistory.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { computed, ref, type Directive } from 'vue';

import { mount } from '@vue/test-utils';
import { IMaskDirective } from 'vue-imask';

import FlawHistory from '@/components/FlawHistory/FlawHistory.vue';

Expand All @@ -22,6 +25,11 @@ describe('flawHistory', () => {
props: {
history: flaw.history,
},
global: {
stubs: {
EditableDate: true,
},
},
});
const historyItems = subject.findAll('.alert-info');
expect(historyItems.length).toBe(0);
Expand All @@ -34,6 +42,11 @@ describe('flawHistory', () => {
props: {
history: flaw.history,
},
global: {
stubs: {
EditableDate: true,
},
},
});
const historyItems = subject.findAll('.alert-info');
expect(historyItems.length).toBe(1);
Expand All @@ -47,8 +60,81 @@ describe('flawHistory', () => {
props: {
history: flaw.history,
},
global: {
stubs: {
EditableDate: true,
},
},
});
const historyListItem = subject.find('li div');
expect(historyListItem?.text()).includes('Update Owner:');
Copy link
Collaborator

Choose a reason for hiding this comment

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

Adding tests for the date range filtering would be awesome

Copy link
Member Author

Choose a reason for hiding this comment

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

Will do that, and also as discussed adding a test for EditableDate making sure that it renders and the property placeholder works as expected.

});

it('filters history items based on date range', async () => {
const mockHistory = [{
pgh_created_at: '2024-10-04T15:00:00.000000Z',
pgh_slug: 'osidb.FlawAudit:123456',
pgh_label: 'update',
pgh_context: { url: 'osidb/api/v1/flaws/12d3820a-a43b-417c-a22e-46e47c232a63', user: 1 },
pgh_diff: { owner: ['', '[email protected]'] },
}, {
pgh_created_at: '2024-10-14T15:08:46.760289Z',
pgh_slug: 'osidb.FlawAudit:123456',
pgh_label: 'update',
pgh_context: { url: 'osidb/api/v1/flaws/12d3820a-a43b-417c-a22e-46e47c232a63', user: 1 },
pgh_diff: { owner: ['', '[email protected]'] },
}];

const props = {
history: mockHistory,
};

const startDate = ref('');
const endDate = ref('');
const validDateRange = computed(() => !!startDate.value && !!endDate.value);

const filteredHistoryItems = computed(() => {
if (!validDateRange.value) {
return props.history;
}

const start = new Date(startDate.value!);
const end = new Date(endDate.value!);
end.setDate(end.getDate() + 1);

return props.history?.filter((item) => {
if (!item.pgh_created_at) return false;

const itemDate = new Date(item.pgh_created_at);

return itemDate.getTime() >= start.getTime() && itemDate.getTime() <= end.getTime();
});
});

const subject = mount(FlawHistory, {
props,
global: {
provide: {
startDate,
endDate,
validDateRange,
},
directives: {
imask: IMaskDirective as Directive,
},
},
});

expect(filteredHistoryItems.value).toEqual(mockHistory);

startDate.value = '2024-10-03';
endDate.value = '2024-10-04';
await subject.vm.$nextTick();
expect(filteredHistoryItems.value.length).toEqual(1);

startDate.value = '2024-02-01';
endDate.value = '2024-02-28';
await subject.vm.$nextTick();
expect(filteredHistoryItems.value).toEqual([]);
});
});
24 changes: 15 additions & 9 deletions src/components/__tests__/__snapshots__/FlawHistory.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@ exports[`flawHistory > is shown if history present on flaw 1`] = `
</label></button>
<div data-v-4715e8e2="" class="ps-3 border-start">
<div data-v-4715e8e2="" class="">
<div data-v-f4fcc8d4="" class="mt-2"><label data-v-f4fcc8d4="" class="mx-2 form-label w-100">
<div data-v-f4fcc8d4="" class="alert alert-info mb-1 p-2"><span data-v-f4fcc8d4="">2024-10-04 15:06 UTC - 1</span>
<ul data-v-f4fcc8d4="" class="mb-2">
<li data-v-f4fcc8d4="">
<div data-v-f4fcc8d4="" class="ms-3 pb-0"><span data-v-f4fcc8d4="">Update</span><span data-v-f4fcc8d4="" class="fw-bold"> Owner</span><span data-v-f4fcc8d4="">: </span><i data-v-f4fcc8d4="" class="bi bi-arrow-right"></i> [email protected]</div>
</li>
</ul>
</div>
</label></div>
<div data-v-f4fcc8d4="" class="d-flex mb-2"><button data-v-f4fcc8d4="" tabindex="-1" type="button" disabled="" class="input-group-text ms-2"><i data-v-f4fcc8d4="" class="bi bi-eraser"></i></button>
<editable-date-stub data-v-f4fcc8d4="" placeholder="[Start date]" editing="false" includestime="false" readonly="false" class="ms-2" style="width: 225px;"></editable-date-stub>
<editable-date-stub data-v-f4fcc8d4="" placeholder="[End date]" editing="false" includestime="false" readonly="false" class="ms-2" style="width: 225px;"></editable-date-stub>
</div>
<div data-v-f4fcc8d4="" class="mt-2">
<div data-v-f4fcc8d4="" class="mt-2"><label data-v-f4fcc8d4="" class="mx-2 form-label w-100">
<div data-v-f4fcc8d4="" class="alert alert-info mb-1 p-2"><span data-v-f4fcc8d4="">2024-10-04 15:06 UTC - 1</span>
<ul data-v-f4fcc8d4="" class="mb-2">
<li data-v-f4fcc8d4="">
<div data-v-f4fcc8d4="" class="ms-3 pb-0"><span data-v-f4fcc8d4="">Update</span><span data-v-f4fcc8d4="" class="fw-bold"> Owner</span><span data-v-f4fcc8d4="">: </span><i data-v-f4fcc8d4="" class="bi bi-arrow-right"></i> [email protected]</div>
</li>
</ul>
</div>
</label></div>
</div>
</div>
</div>
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/widgets/EditableDate/EditableDate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const props = defineProps<{
error?: null | string;
includesTime?: boolean;
modelValue: string | undefined;
placeholder?: string;
readOnly?: boolean;
}>();

Expand Down Expand Up @@ -179,7 +180,7 @@ function isValidDate(d: Date | string): boolean {

function osimFormatDate(date?: null | string): string {
if (date == null) {
return '[No date selected]';
return props.placeholder ?? '[No date selected]';
}

const formattedDate = formatDate(date, props.includesTime);
Expand Down
Loading