Skip to content

Commit e44d3f0

Browse files
authored
Merge pull request #20332 from davelopez/explore_short_term_storage_expiration_indicator
Add short term storage expiration indicator to history items
2 parents f26ba3e + ef93b67 commit e44d3f0

File tree

18 files changed

+508
-9
lines changed

18 files changed

+508
-9
lines changed

client/src/api/schema/schema.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7580,6 +7580,8 @@ export interface components {
75807580
device?: string | null;
75817581
/** Name */
75827582
name?: string | null;
7583+
/** Object Expires After Days */
7584+
object_expires_after_days?: number | null;
75837585
/** Object Store Id */
75847586
object_store_id?: string | null;
75857587
/** Private */
@@ -12002,6 +12004,11 @@ export interface components {
1200212004
* @description The name of the item.
1200312005
*/
1200412006
name?: string | null;
12007+
/**
12008+
* Object Store ID
12009+
* @description The ID of the object store that this dataset is stored in.
12010+
*/
12011+
object_store_id?: string | null;
1200512012
/**
1200612013
* Peek
1200712014
* @description A few lines of contents from the start of the file.
@@ -12260,6 +12267,11 @@ export interface components {
1226012267
* @description The name of the item.
1226112268
*/
1226212269
name: string | null;
12270+
/**
12271+
* Object Store ID
12272+
* @description The ID of the object store that this dataset is stored in.
12273+
*/
12274+
object_store_id?: string | null;
1226312275
/**
1226412276
* Peek
1226512277
* @description A few lines of contents from the start of the file.
@@ -12527,6 +12539,11 @@ export interface components {
1252712539
* @description The name of the item.
1252812540
*/
1252912541
name: string | null;
12542+
/**
12543+
* Object Store ID
12544+
* @description The ID of the object store that this dataset is stored in.
12545+
*/
12546+
object_store_id?: string | null;
1253012547
/**
1253112548
* Purged
1253212549
* @description Whether this dataset has been removed from disk.
@@ -12685,6 +12702,11 @@ export interface components {
1268512702
* @description Optional message with further information in case the population of the dataset collection failed.
1268612703
*/
1268712704
populated_state_message?: string | null;
12705+
/**
12706+
* Store Times Summary
12707+
* @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.
12708+
*/
12709+
store_times_summary?: components["schemas"]["OldestCreateTimeByObjectStoreId"][] | null;
1268812710
tags?: components["schemas"]["TagCollection"] | null;
1268912711
/**
1269012712
* Type
@@ -12839,6 +12861,11 @@ export interface components {
1283912861
* @description Optional message with further information in case the population of the dataset collection failed.
1284012862
*/
1284112863
populated_state_message?: string | null;
12864+
/**
12865+
* Store Times Summary
12866+
* @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.
12867+
*/
12868+
store_times_summary?: components["schemas"]["OldestCreateTimeByObjectStoreId"][] | null;
1284212869
tags: components["schemas"]["TagCollection"];
1284312870
/**
1284412871
* Type
@@ -12974,6 +13001,11 @@ export interface components {
1297413001
* @description Optional message with further information in case the population of the dataset collection failed.
1297513002
*/
1297613003
populated_state_message?: string | null;
13004+
/**
13005+
* Store Times Summary
13006+
* @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.
13007+
*/
13008+
store_times_summary?: components["schemas"]["OldestCreateTimeByObjectStoreId"][] | null;
1297713009
tags: components["schemas"]["TagCollection"];
1297813010
/**
1297913011
* Type
@@ -17392,6 +17424,23 @@ export interface components {
1739217424
*/
1739317425
version: number;
1739417426
};
17427+
/**
17428+
* OldestCreateTimeByObjectStoreId
17429+
* @description Represents the oldest creation time of a set of datasets stored in a specific object store.
17430+
*/
17431+
OldestCreateTimeByObjectStoreId: {
17432+
/**
17433+
* Object Store ID
17434+
* @description The ID of the object store.
17435+
*/
17436+
object_store_id: string;
17437+
/**
17438+
* Oldest Create Time
17439+
* Format: date-time
17440+
* @description The oldest creation time of a set of datasets stored in this object store.
17441+
*/
17442+
oldest_create_time: string;
17443+
};
1739517444
/** OutputReferenceByLabel */
1739617445
OutputReferenceByLabel: {
1739717446
/**
@@ -21529,6 +21578,8 @@ export interface components {
2152921578
hidden: boolean;
2153021579
/** Name */
2153121580
name?: string | null;
21581+
/** Object Expires After Days */
21582+
object_expires_after_days?: number | null;
2153221583
/** Object Store Id */
2153321584
object_store_id?: string | null;
2153421585
/** Private */
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<script setup lang="ts">
2+
import { faHourglass } from "@fortawesome/free-solid-svg-icons";
3+
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
4+
import { BBadge } from "bootstrap-vue";
5+
import { parseISO } from "date-fns";
6+
import { storeToRefs } from "pinia";
7+
import { computed } from "vue";
8+
9+
import type { HDASummary, HDCASummary } from "@/api";
10+
import { isHDA } from "@/api";
11+
import { useObjectStoreStore } from "@/stores/objectStoreStore";
12+
13+
interface ExpirableObjectStoreTime {
14+
objectStoreId: string;
15+
objectExpiresAfterDays: number;
16+
objectStoreName: string;
17+
oldestCreateTime: Date;
18+
}
19+
20+
const props = defineProps<{
21+
item: HDASummary | HDCASummary;
22+
}>();
23+
24+
const store = useObjectStoreStore();
25+
const { selectableObjectStores } = storeToRefs(store);
26+
27+
const defaultName = "Unnamed Object Store";
28+
29+
const expirableObjectStoreTime = computed<ExpirableObjectStoreTime | undefined>(() => {
30+
const item = props.item;
31+
if (isHDA(item)) {
32+
// Single object store case: check if it has an expiration policy
33+
const expirableObjectStore = selectableObjectStores.value?.find(
34+
(objectStore) =>
35+
objectStore.object_store_id === item.object_store_id &&
36+
(objectStore.object_expires_after_days ?? 0) > 0,
37+
);
38+
if (!expirableObjectStore) {
39+
return undefined;
40+
}
41+
return {
42+
objectStoreId: expirableObjectStore.object_store_id ?? "default",
43+
objectExpiresAfterDays: expirableObjectStore.object_expires_after_days ?? 0,
44+
objectStoreName: expirableObjectStore.name ?? defaultName,
45+
oldestCreateTime: parseISO(`${item.create_time}Z`),
46+
};
47+
} else if (item.store_times_summary !== undefined) {
48+
// Multiple object stores case: find the one with the shortest expiration date
49+
const expirableStoreTimes: ExpirableObjectStoreTime[] = (item.store_times_summary ?? [])
50+
.map((storeTime) => {
51+
const objectStore = selectableObjectStores.value?.find(
52+
(os) => os.object_store_id === storeTime.object_store_id,
53+
);
54+
if (!objectStore || (objectStore.object_expires_after_days ?? 0) <= 0) {
55+
return null;
56+
}
57+
return {
58+
objectStoreId: storeTime.object_store_id,
59+
objectExpiresAfterDays: objectStore.object_expires_after_days ?? 0,
60+
objectStoreName: objectStore.name ?? defaultName,
61+
oldestCreateTime: parseISO(`${storeTime.oldest_create_time}Z`),
62+
};
63+
})
64+
.filter((storeTime): storeTime is ExpirableObjectStoreTime => storeTime !== null);
65+
if (expirableStoreTimes.length === 0) {
66+
return undefined;
67+
}
68+
// Find the store with the shortest expiration time according to the oldest creation time and expiration days
69+
expirableStoreTimes.sort((a, b) => {
70+
const aExpiration = new Date(a.oldestCreateTime);
71+
aExpiration.setDate(aExpiration.getDate() + a.objectExpiresAfterDays);
72+
const bExpiration = new Date(b.oldestCreateTime);
73+
bExpiration.setDate(bExpiration.getDate() + b.objectExpiresAfterDays);
74+
return aExpiration.getTime() - bExpiration.getTime();
75+
});
76+
return expirableStoreTimes[0];
77+
}
78+
return undefined;
79+
});
80+
81+
const objectStoreName = computed(() => {
82+
return expirableObjectStoreTime.value?.objectStoreName ?? defaultName;
83+
});
84+
85+
const expirationDate = computed(() => {
86+
const target = expirableObjectStoreTime.value;
87+
if (!target) {
88+
return null;
89+
}
90+
// Calculate the expiration date based on the creation date and the expiration days of the object store
91+
const expirationDate = new Date(target.oldestCreateTime);
92+
expirationDate.setDate(expirationDate.getDate() + target.objectExpiresAfterDays);
93+
return expirationDate;
94+
});
95+
96+
const timeToExpire = computed<number | null>(() => {
97+
if (!expirationDate.value) {
98+
return null;
99+
}
100+
// Calculate the difference in days between the expiration date and the current date
101+
return expirationDate.value.getTime() - new Date().getTime();
102+
});
103+
104+
const canExpire = computed(() => timeToExpire.value !== null);
105+
106+
const daysToExpire = computed(() => {
107+
if (timeToExpire.value === null) {
108+
return null;
109+
}
110+
return timeToExpire.value < 0 ? 0 : Math.floor(timeToExpire.value / (1000 * 60 * 60 * 24));
111+
});
112+
113+
const hasExpired = computed(() => {
114+
return expirationDate.value ? expirationDate.value < new Date() : false;
115+
});
116+
117+
const expirationMessage = computed(() => {
118+
if (daysToExpire.value === null) {
119+
return undefined;
120+
}
121+
if (hasExpired.value) {
122+
return "Expired";
123+
}
124+
if (daysToExpire.value !== null && daysToExpire.value <= 1) {
125+
return `Expires soon!`;
126+
}
127+
return `Expires in ${daysToExpire.value} days`;
128+
});
129+
130+
const expirationTooltip = computed(() => {
131+
const itemType = isHDA(props.item) ? "dataset" : "dataset collection (or any of its datasets)";
132+
if (!expirationDate.value) {
133+
return `This ${itemType} does not have an expiration date.`;
134+
}
135+
if (hasExpired.value) {
136+
return `This ${itemType} was stored in ${
137+
objectStoreName.value
138+
} and has expired on ${expirationDate.value.toDateString()}.`;
139+
}
140+
return `This ${itemType} is stored in ${
141+
objectStoreName.value
142+
} and expires on ${expirationDate.value.toDateString()}.`;
143+
});
144+
145+
const variant = computed(() => {
146+
if (hasExpired.value) {
147+
return "danger";
148+
}
149+
if (daysToExpire.value && daysToExpire.value <= 5) {
150+
return "warning";
151+
}
152+
return "secondary";
153+
});
154+
</script>
155+
<template>
156+
<span v-if="canExpire" class="expiration-indicator">
157+
<BBadge v-b-tooltip.noninteractive.hover.left :variant="variant" :title="expirationTooltip">
158+
<FontAwesomeIcon :icon="faHourglass" /> {{ expirationMessage }}
159+
</BBadge>
160+
</span>
161+
</template>

client/src/components/History/Content/ContentItem.test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import VueRouter from "vue-router";
66

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

1011
import ContentItem from "./ContentItem.vue";
1112

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

27+
setupSelectableMock();
28+
2629
// mock queries
2730
updateContentFields.mockImplementation(async () => {});
2831

client/src/components/History/Content/ContentItem.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { getContentItemState, type State, STATES } from "./model/states";
2323
import type { RouterPushOptions } from "./router-push-options";
2424
2525
import CollectionDescription from "./Collection/CollectionDescription.vue";
26+
import ContentExpirationIndicator from "./ContentExpirationIndicator.vue";
2627
import ContentOptions from "./ContentOptions.vue";
2728
import DatasetDetails from "./Dataset/DatasetDetails.vue";
2829
import StatelessTags from "@/components/TagsMultiselect/StatelessTags.vue";
@@ -419,6 +420,7 @@ function unexpandedClick(event: Event) {
419420
</span>
420421
</div>
421422
</div>
423+
<ContentExpirationIndicator :item="item" class="ml-auto align-self-start btn-group p-1" />
422424
<!-- eslint-disable-next-line vuejs-accessibility/click-events-have-key-events, vuejs-accessibility/no-static-element-interactions -->
423425
<span @click.stop="unexpandedClick">
424426
<CollectionDescription v-if="!isDataset" class="px-2 pb-2 cursor-pointer" :hdca="item" />

client/src/components/History/Content/GenericElement.test.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ import { mount } from "@vue/test-utils";
33
import { getLocalVue, suppressLucideVue2Deprecation } from "tests/jest/helpers";
44
import VueRouter from "vue-router";
55

6+
import { setupSelectableMock } from "@/components/ObjectStore/mockServices";
7+
68
import GenericElement from "./GenericElement";
79

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

12+
setupSelectableMock();
13+
1014
const localVue = getLocalVue();
1115
localVue.use(VueRouter);
1216
const router = new VueRouter();

client/src/components/History/CurrentCollection/CollectionDetails.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import type { HDCASummary } from "@/api";
33
44
import CollectionDescription from "@/components/History/Content/Collection/CollectionDescription.vue";
5+
import ContentExpirationIndicator from "@/components/History/Content/ContentExpirationIndicator.vue";
56
import DetailsLayout from "@/components/History/Layout/DetailsLayout.vue";
67
78
interface Props {
@@ -22,6 +23,7 @@ defineProps<Props>();
2223
@save="$emit('update:dsc', $event)">
2324
<template v-slot:description>
2425
<CollectionDescription :hdca="dsc" />
26+
<ContentExpirationIndicator :item="dsc" />
2527
</template>
2628
</DetailsLayout>
2729
</template>

client/src/components/History/CurrentHistory/SelectPreferredStore.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ function reset() {
145145

146146
<template>
147147
<BModal
148+
id="modal-select-history-storage-location"
148149
:visible="props.showModal"
149150
centered
150151
scrollable
@@ -154,6 +155,7 @@ function reset() {
154155
title-tag="h3"
155156
ok-title="Change Storage Location"
156157
cancel-variant="outline-primary"
158+
dialog-class="modal-select-history-storage-location"
157159
:ok-disabled="currentSelectedStoreId === props.preferredObjectStoreId"
158160
:no-close-on-backdrop="currentSelectedStoreId !== props.preferredObjectStoreId"
159161
:no-close-on-esc="currentSelectedStoreId !== props.preferredObjectStoreId"

client/src/components/History/HistoryView.test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { setupMockConfig } from "tests/jest/mockConfig";
77
import VueRouter from "vue-router";
88

99
import { useServerMock } from "@/api/client/__mocks__";
10+
import { setupSelectableMock } from "@/components/ObjectStore/mockServices";
1011
import { useHistoryStore } from "@/stores/historyStore";
1112
import { getHistoryByIdFromServer, setCurrentHistoryOnServer } from "@/stores/services/history.services";
1213
import { useUserStore } from "@/stores/userStore";
@@ -19,6 +20,8 @@ localVue.use(VueRouter);
1920

2021
jest.mock("stores/services/history.services");
2122

23+
setupSelectableMock();
24+
2225
const { server, http } = useServerMock();
2326

2427
function create_history(historyId, userId, purged = false, archived = false) {

client/src/stores/collectionElementsStore.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ function mockCollection(id: string, numElements = 10): HDCASummary {
145145
type_id: "dataset_collection",
146146
url: "",
147147
type: "collection",
148+
store_times_summary: null,
148149
};
149150
}
150151

0 commit comments

Comments
 (0)