Unverified Commit e44d3f09 authored by David López's avatar David López Committed by GitHub
Browse files

Merge pull request #20332 from davelopez/explore_short_term_storage_expiration_indicator

Add short term storage expiration indicator to history items
parents f26ba3e6 ef93b67c
Loading
Loading
Loading
Loading
+51 −0
Original line number Diff line number Diff line
@@ -7580,6 +7580,8 @@ export interface components {
            device?: string | null;
            /** Name */
            name?: string | null;
            /** Object Expires After Days */
            object_expires_after_days?: number | null;
            /** Object Store Id */
            object_store_id?: string | null;
            /** Private */
@@ -12002,6 +12004,11 @@ export interface components {
             * @description The name of the item.
             */
            name?: string | null;
            /**
             * Object Store ID
             * @description The ID of the object store that this dataset is stored in.
             */
            object_store_id?: string | null;
            /**
             * Peek
             * @description A few lines of contents from the start of the file.
@@ -12260,6 +12267,11 @@ export interface components {
             * @description The name of the item.
             */
            name: string | null;
            /**
             * Object Store ID
             * @description The ID of the object store that this dataset is stored in.
             */
            object_store_id?: string | null;
            /**
             * Peek
             * @description A few lines of contents from the start of the file.
@@ -12527,6 +12539,11 @@ export interface components {
             * @description The name of the item.
             */
            name: string | null;
            /**
             * Object Store ID
             * @description The ID of the object store that this dataset is stored in.
             */
            object_store_id?: string | null;
            /**
             * Purged
             * @description Whether this dataset has been removed from disk.
@@ -12685,6 +12702,11 @@ export interface components {
             * @description Optional message with further information in case the population of the dataset collection failed.
             */
            populated_state_message?: string | null;
            /**
             * Store Times Summary
             * @description A list of objects containing the object store ID and the oldest creation time of the datasets stored in that object store for this collection.This is used to determine the age of the datasets in the collection when the object store is short-lived.
             */
            store_times_summary?: components["schemas"]["OldestCreateTimeByObjectStoreId"][] | null;
            tags?: components["schemas"]["TagCollection"] | null;
            /**
             * Type
@@ -12839,6 +12861,11 @@ export interface components {
             * @description Optional message with further information in case the population of the dataset collection failed.
             */
            populated_state_message?: string | null;
            /**
             * Store Times Summary
             * @description A list of objects containing the object store ID and the oldest creation time of the datasets stored in that object store for this collection.This is used to determine the age of the datasets in the collection when the object store is short-lived.
             */
            store_times_summary?: components["schemas"]["OldestCreateTimeByObjectStoreId"][] | null;
            tags: components["schemas"]["TagCollection"];
            /**
             * Type
@@ -12974,6 +13001,11 @@ export interface components {
             * @description Optional message with further information in case the population of the dataset collection failed.
             */
            populated_state_message?: string | null;
            /**
             * Store Times Summary
             * @description A list of objects containing the object store ID and the oldest creation time of the datasets stored in that object store for this collection.This is used to determine the age of the datasets in the collection when the object store is short-lived.
             */
            store_times_summary?: components["schemas"]["OldestCreateTimeByObjectStoreId"][] | null;
            tags: components["schemas"]["TagCollection"];
            /**
             * Type
@@ -17392,6 +17424,23 @@ export interface components {
             */
            version: number;
        };
        /**
         * OldestCreateTimeByObjectStoreId
         * @description Represents the oldest creation time of a set of datasets stored in a specific object store.
         */
        OldestCreateTimeByObjectStoreId: {
            /**
             * Object Store ID
             * @description The ID of the object store.
             */
            object_store_id: string;
            /**
             * Oldest Create Time
             * Format: date-time
             * @description The oldest creation time of a set of datasets stored in this object store.
             */
            oldest_create_time: string;
        };
        /** OutputReferenceByLabel */
        OutputReferenceByLabel: {
            /**
@@ -21529,6 +21578,8 @@ export interface components {
            hidden: boolean;
            /** Name */
            name?: string | null;
            /** Object Expires After Days */
            object_expires_after_days?: number | null;
            /** Object Store Id */
            object_store_id?: string | null;
            /** Private */
+161 −0
Original line number Diff line number Diff line
<script setup lang="ts">
import { faHourglass } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BBadge } from "bootstrap-vue";
import { parseISO } from "date-fns";
import { storeToRefs } from "pinia";
import { computed } from "vue";

import type { HDASummary, HDCASummary } from "@/api";
import { isHDA } from "@/api";
import { useObjectStoreStore } from "@/stores/objectStoreStore";

interface ExpirableObjectStoreTime {
    objectStoreId: string;
    objectExpiresAfterDays: number;
    objectStoreName: string;
    oldestCreateTime: Date;
}

const props = defineProps<{
    item: HDASummary | HDCASummary;
}>();

const store = useObjectStoreStore();
const { selectableObjectStores } = storeToRefs(store);

const defaultName = "Unnamed Object Store";

const expirableObjectStoreTime = computed<ExpirableObjectStoreTime | undefined>(() => {
    const item = props.item;
    if (isHDA(item)) {
        // Single object store case: check if it has an expiration policy
        const expirableObjectStore = selectableObjectStores.value?.find(
            (objectStore) =>
                objectStore.object_store_id === item.object_store_id &&
                (objectStore.object_expires_after_days ?? 0) > 0,
        );
        if (!expirableObjectStore) {
            return undefined;
        }
        return {
            objectStoreId: expirableObjectStore.object_store_id ?? "default",
            objectExpiresAfterDays: expirableObjectStore.object_expires_after_days ?? 0,
            objectStoreName: expirableObjectStore.name ?? defaultName,
            oldestCreateTime: parseISO(`${item.create_time}Z`),
        };
    } else if (item.store_times_summary !== undefined) {
        // Multiple object stores case: find the one with the shortest expiration date
        const expirableStoreTimes: ExpirableObjectStoreTime[] = (item.store_times_summary ?? [])
            .map((storeTime) => {
                const objectStore = selectableObjectStores.value?.find(
                    (os) => os.object_store_id === storeTime.object_store_id,
                );
                if (!objectStore || (objectStore.object_expires_after_days ?? 0) <= 0) {
                    return null;
                }
                return {
                    objectStoreId: storeTime.object_store_id,
                    objectExpiresAfterDays: objectStore.object_expires_after_days ?? 0,
                    objectStoreName: objectStore.name ?? defaultName,
                    oldestCreateTime: parseISO(`${storeTime.oldest_create_time}Z`),
                };
            })
            .filter((storeTime): storeTime is ExpirableObjectStoreTime => storeTime !== null);
        if (expirableStoreTimes.length === 0) {
            return undefined;
        }
        // Find the store with the shortest expiration time according to the oldest creation time and expiration days
        expirableStoreTimes.sort((a, b) => {
            const aExpiration = new Date(a.oldestCreateTime);
            aExpiration.setDate(aExpiration.getDate() + a.objectExpiresAfterDays);
            const bExpiration = new Date(b.oldestCreateTime);
            bExpiration.setDate(bExpiration.getDate() + b.objectExpiresAfterDays);
            return aExpiration.getTime() - bExpiration.getTime();
        });
        return expirableStoreTimes[0];
    }
    return undefined;
});

const objectStoreName = computed(() => {
    return expirableObjectStoreTime.value?.objectStoreName ?? defaultName;
});

const expirationDate = computed(() => {
    const target = expirableObjectStoreTime.value;
    if (!target) {
        return null;
    }
    // Calculate the expiration date based on the creation date and the expiration days of the object store
    const expirationDate = new Date(target.oldestCreateTime);
    expirationDate.setDate(expirationDate.getDate() + target.objectExpiresAfterDays);
    return expirationDate;
});

const timeToExpire = computed<number | null>(() => {
    if (!expirationDate.value) {
        return null;
    }
    // Calculate the difference in days between the expiration date and the current date
    return expirationDate.value.getTime() - new Date().getTime();
});

const canExpire = computed(() => timeToExpire.value !== null);

const daysToExpire = computed(() => {
    if (timeToExpire.value === null) {
        return null;
    }
    return timeToExpire.value < 0 ? 0 : Math.floor(timeToExpire.value / (1000 * 60 * 60 * 24));
});

const hasExpired = computed(() => {
    return expirationDate.value ? expirationDate.value < new Date() : false;
});

const expirationMessage = computed(() => {
    if (daysToExpire.value === null) {
        return undefined;
    }
    if (hasExpired.value) {
        return "Expired";
    }
    if (daysToExpire.value !== null && daysToExpire.value <= 1) {
        return `Expires soon!`;
    }
    return `Expires in ${daysToExpire.value} days`;
});

const expirationTooltip = computed(() => {
    const itemType = isHDA(props.item) ? "dataset" : "dataset collection (or any of its datasets)";
    if (!expirationDate.value) {
        return `This ${itemType} does not have an expiration date.`;
    }
    if (hasExpired.value) {
        return `This ${itemType} was stored in ${
            objectStoreName.value
        } and has expired on ${expirationDate.value.toDateString()}.`;
    }
    return `This ${itemType} is stored in ${
        objectStoreName.value
    } and expires on ${expirationDate.value.toDateString()}.`;
});

const variant = computed(() => {
    if (hasExpired.value) {
        return "danger";
    }
    if (daysToExpire.value && daysToExpire.value <= 5) {
        return "warning";
    }
    return "secondary";
});
</script>
<template>
    <span v-if="canExpire" class="expiration-indicator">
        <BBadge v-b-tooltip.noninteractive.hover.left :variant="variant" :title="expirationTooltip">
            <FontAwesomeIcon :icon="faHourglass" /> {{ expirationMessage }}
        </BBadge>
    </span>
</template>
+3 −0
Original line number Diff line number Diff line
@@ -6,6 +6,7 @@ import VueRouter from "vue-router";

import { HttpResponse, useServerMock } from "@/api/client/__mocks__";
import { updateContentFields } from "@/components/History/model/queries";
import { setupSelectableMock } from "@/components/ObjectStore/mockServices";

import ContentItem from "./ContentItem.vue";

@@ -23,6 +24,8 @@ jest.mock("vue-router/composables", () => ({
    useRouter: jest.fn(() => ({})),
}));

setupSelectableMock();

// mock queries
updateContentFields.mockImplementation(async () => {});

+2 −0
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import { getContentItemState, type State, STATES } from "./model/states";
import type { RouterPushOptions } from "./router-push-options";

import CollectionDescription from "./Collection/CollectionDescription.vue";
import ContentExpirationIndicator from "./ContentExpirationIndicator.vue";
import ContentOptions from "./ContentOptions.vue";
import DatasetDetails from "./Dataset/DatasetDetails.vue";
import StatelessTags from "@/components/TagsMultiselect/StatelessTags.vue";
@@ -419,6 +420,7 @@ function unexpandedClick(event: Event) {
                </span>
            </div>
        </div>
        <ContentExpirationIndicator :item="item" class="ml-auto align-self-start btn-group p-1" />
        <!-- eslint-disable-next-line vuejs-accessibility/click-events-have-key-events, vuejs-accessibility/no-static-element-interactions -->
        <span @click.stop="unexpandedClick">
            <CollectionDescription v-if="!isDataset" class="px-2 pb-2 cursor-pointer" :hdca="item" />
+4 −0
Original line number Diff line number Diff line
@@ -3,10 +3,14 @@ import { mount } from "@vue/test-utils";
import { getLocalVue, suppressLucideVue2Deprecation } from "tests/jest/helpers";
import VueRouter from "vue-router";

import { setupSelectableMock } from "@/components/ObjectStore/mockServices";

import GenericElement from "./GenericElement";

jest.mock("components/History/model/queries");

setupSelectableMock();

const localVue = getLocalVue();
localVue.use(VueRouter);
const router = new VueRouter();
Loading