Skip to content

Commit

Permalink
Add custom date range package filters
Browse files Browse the repository at this point in the history
Refs #1102

- Convert pre-defined date range selector to a drop-down widget to
  reduce size of list
- Add start and end date pickers to a allow selecting a custom times
  for filtering the package list

Note: Vitest shows a very low coverage for statements in the TimeDrodown
component, even though the unit tests should cover most of the code.
I've tried to fix the coverage report a number of different ways, but I
haven't been able to determine why it's not working correctly.
  • Loading branch information
djjuhasz committed Feb 25, 2025
1 parent 75604d6 commit cd4d715
Show file tree
Hide file tree
Showing 10 changed files with 340 additions and 132 deletions.
39 changes: 39 additions & 0 deletions dashboard/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"dependencies": {
"@pinia/plugin-debounce": "^1.0.1",
"@types/humanize-duration": "^3.27.4",
"@vuepic/vue-datepicker": "^11.0.1",
"@vueuse/core": "^11.0.3",
"buffer": "^6.0.3",
"humanize-duration": "^3.32.1",
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/components/PreservationActionCollapse.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function isComplete(task: EnduroPackagePreservationTask) {
/>
<div
v-for="(task, index) in action.tasks.slice().reverse()"
:key="action.id"
:key="task.id"
class="card"
>
<div class="card-body">
Expand Down
200 changes: 153 additions & 47 deletions dashboard/src/components/TimeDropdown.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
<script setup lang="ts">
import { onMounted, ref, useTemplateRef } from "vue";
import VueDatePicker from "@vuepic/vue-datepicker";
import type { ModelValue } from "@vuepic/vue-datepicker";
import "@vuepic/vue-datepicker/dist/main.css";
import { onMounted, ref, useTemplateRef, watch } from "vue";
const emit = defineEmits<{ change: [field: string, value: string] }>();
import IconCloseLine from "~icons/clarity/close-line";
const emit = defineEmits<{
change: [name: string, start: string, end: string];
}>();
type option = {
value: string;
label: string;
};
const options: option[] = [
{ value: "", label: "Any time" },
{ value: "", label: "Select a time range" },
{ value: "3h", label: "The last 3 hours" },
{ value: "6h", label: "The last 6 hours" },
{ value: "12h", label: "The last 12 hours" },
Expand All @@ -18,56 +25,102 @@ const options: option[] = [
{ value: "7d", label: "The last 7 days" },
];
const defaultLabel = "Started";
const props = defineProps<{
fieldname: string;
name: string;
}>();
const label = ref("Started");
const selected = ref(options[0]);
const dropdown = useTemplateRef("date-filter");
const label = ref(defaultLabel);
const timeFilter = useTemplateRef("time-filter");
const selected = ref("");
const startTime = ref<Date | null>(null);
const endTime = ref<Date | null>(null);
onMounted(() => {
if (dropdown.value) {
dropdown.value.style.display = "none";
if (timeFilter.value) {
timeFilter.value.style.display = "none";
}
});
watch(selected, async (newValue) => {
let sel = options.find((o) => o.value == newValue);
if (!sel || sel.value === "") {
return;
}
label.value = "Started: " + sel.label;
startTime.value = earliestTimeFromOption(sel.value);
endTime.value = null;
toggle();
emit(
"change",
props.name,
formatDate(startTime.value),
formatDate(endTime.value),
);
});
const toggle = () => {
if (dropdown.value) {
if (dropdown.value.style.display == "none") {
dropdown.value.style.display = "block";
} else if (dropdown.value.style.display == "block") {
dropdown.value.style.display = "none";
if (timeFilter.value) {
if (timeFilter.value.style.display == "none") {
timeFilter.value.style.display = "block";
} else if (timeFilter.value.style.display == "block") {
timeFilter.value.style.display = "none";
}
}
};
const handleChange = (opt: option) => {
selected.value = opt;
const handleCustomTimeChange = () => {
label.value = "Started: Custom";
selected.value = "";
if (opt.value === "") {
label.value = "Started";
} else {
label.value = "Started: " + opt.label;
}
emit(
"change",
props.name,
formatDate(startTime.value),
formatDate(endTime.value),
);
};
toggle();
emit("change", props.fieldname, earliestTimeFromOption(opt));
const handleStartChange = (modelData: ModelValue) => {
startTime.value = modelData as Date;
handleCustomTimeChange();
};
const handleEndChange = (modelData: ModelValue) => {
endTime.value = modelData as Date;
handleCustomTimeChange();
};
const reset = () => {
label.value = defaultLabel;
selected.value = "";
startTime.value = null;
endTime.value = null;
emit(
"change",
props.name,
formatDate(startTime.value),
formatDate(endTime.value),
);
};
const earliestTimeFromOption = (opt: option) => {
const formatDate = (date: Date) => {
let t = date.toISOString();
t = t.split(".")[0] + "Z"; // remove milliseconds.
return t;
};
const formatDate = (date: Date | null) => {
if (!date) return "";
let t = date.toISOString();
t = t.split(".")[0] + "Z"; // remove milliseconds.
return t;
};
const earliestTimeFromOption = (value: string) => {
// convert hours and days to milliseconds.
const hour = 60 * 60 * 1000;
const day = 24 * hour;
let start = new Date();
switch (opt.value) {
switch (value) {
case "3h":
start = new Date(Date.now() - 3 * hour);
break;
Expand All @@ -87,30 +140,83 @@ const earliestTimeFromOption = (opt: option) => {
start = new Date(Date.now() - 7 * day);
break;
default:
return "";
return new Date(0);
}
return formatDate(start);
return start;
};
</script>

<template>
<div class="dropdown">
<button
@click="toggle"
class="btn btn-primary dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
<div class="input-group">
<button
:id="'time-filter-' + props.name + '-toggle'"
@click="toggle"
class="btn btn-primary dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
{{ label }}
</button>
<button
:id="'time-filter-' + props.name + '-clear'"
@click="reset"
class="btn btn-secondary"
type="reset"
aria-label="Clear time filter"
:hidden="startTime === null && endTime === null"
>
<IconCloseLine />
</button>
</div>
<div
:id="'time-filter-' + props.name"
class="dropdown-menu p-2"
ref="time-filter"
style="min-width: 250px"
>
{{ label }}
</button>
<ul ref="date-filter" class="dropdown-menu">
<li v-for="item in options" :key="item.value">
<a class="dropdown-item" href="#" @click.prevent="handleChange(item)">{{
item.label
}}</a>
</li>
</ul>
<div :label-for="'time-filter-' + props.name + '-preset'">
Preset range
</div>
<select
:id="'time-filter-' + props.name + '-preset'"
name="preset-times"
class="form-select"
aria-label="Select a time range"
v-model="selected"
>
<option
v-for="item in options"
:key="item.value"
:value="item.value"
:selected="item.value == selected"
:disabled="item.value == ''"
>
{{ item.label }}
</option>
</select>
<hr />
<div>Custom range</div>
<div>
<VueDatePicker
time-picker-inline
:id="'time-filter-' + props.name + '-start'"
v-model="startTime"
data-test="startTime"
placeholder="Start time"
@update:model-value="handleStartChange"
/>
to
<VueDatePicker
time-picker-inline
:id="'time-filter-' + props.name + '-end'"
v-model="endTime"
placeholder="End time"
@update:model-value="handleEndChange"
/>
</div>
</div>
</div>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ describe("PackageDetailsCard.vue", () => {
});

it("renders when the package is in pending status", async () => {
const now = new Date();
const { getByText } = render(PackageDetailsCard, {
global: {
plugins: [
Expand Down
Loading

0 comments on commit cd4d715

Please sign in to comment.