Skip to content

Commit e282128

Browse files
committed
Replace QuotaUsageProvider with Pinia store
Removes the old quota source usage provider component and introduces a Pinia store to manage quota usage state and fetching logic across the application. Adopts the store pattern throughout all relevant components, replacing provider logic and updating all quota usage retrieval and reactivity to use the store. Centralizes state, simplifies usage, and makes quota usage handling more maintainable and testable. Updates tests and supporting files to mock or use the store as appropriate.
1 parent 1af99bd commit e282128

10 files changed

Lines changed: 322 additions & 111 deletions

File tree

client/src/components/Common/FilterMenuDropdown.vue

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
44
import { BButton, BDropdown, BDropdownItem, BInputGroup, BInputGroupAppend } from "bootstrap-vue";
55
import { computed, onMounted, ref, watch } from "vue";
66
7-
import { fetchCurrentUserQuotaUsages, type QuotaUsage } from "@/api/users";
7+
import type { QuotaUsage } from "@/api/users";
8+
import { useQuotaUsageStore } from "@/stores/quotaUsageStore";
89
import type { FilterType, ValidFilter } from "@/utils/filtering";
9-
import { errorMessageAsString } from "@/utils/simple-error";
1010
import { capitalizeFirstLetter } from "@/utils/strings";
1111
1212
import GModal from "../BaseComponents/GModal.vue";
@@ -77,25 +77,22 @@ function onHelp(_: string, value: string) {
7777
}
7878
7979
// Quota Source refs and operations
80-
const quotaUsages = ref<QuotaUsage[]>([]);
81-
const errorMessage = ref<string>();
80+
const quotaUsageStore = useQuotaUsageStore();
81+
const quotaUsages = computed<QuotaUsage[]>(() => quotaUsageStore.quotaUsages ?? []);
82+
8283
async function loadQuotaUsages() {
83-
try {
84-
quotaUsages.value = await fetchCurrentUserQuotaUsages();
85-
86-
// if the propValue is a string, find the corresponding QuotaUsage object and update the localValue
87-
if (propValue.value && typeof propValue.value === "string") {
88-
localValue.value = quotaUsages.value.find(
89-
(quotaUsage) => props.filter.handler.converter!(quotaUsage) === propValue.value,
90-
);
91-
}
92-
} catch (e) {
93-
errorMessage.value = errorMessageAsString(e);
84+
await quotaUsageStore.loadQuotaUsages();
85+
86+
// if the propValue is a string, find the corresponding QuotaUsage object and update the localValue
87+
if (propValue.value && typeof propValue.value === "string") {
88+
localValue.value = quotaUsages.value.find(
89+
(quotaUsage) => props.filter.handler.converter!(quotaUsage) === propValue.value,
90+
);
9491
}
9592
}
9693
9794
const hasMultipleQuotaSources = computed<boolean>(() => {
98-
return !!(quotaUsages.value && quotaUsages.value.length > 1);
95+
return quotaUsages.value.length > 1;
9996
});
10097
10198
onMounted(async () => {

client/src/components/ConfigTemplates/SourceOptionCard.vue

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<script setup lang="ts">
22
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
33
import type { IconDefinition } from "font-awesome-6";
4-
import { computed } from "vue";
4+
import { computed, watch } from "vue";
55
66
import type { UserConcreteObjectStoreModel } from "@/api";
77
import type { FileSourceTemplateSummary } from "@/api/fileSources";
88
import type { ObjectStoreTemplateSummary } from "@/api/objectStores.templates";
99
import type { CardAction } from "@/components/Common/GCard.types";
10-
import { QuotaSourceUsageProvider } from "@/components/User/DiskUsage/Quota/QuotaUsageProvider.js";
10+
import { useQuotaUsageStore } from "@/stores/quotaUsageStore";
1111
1212
import GButton from "@/components/BaseComponents/GButton.vue";
1313
import GCard from "@/components/Common/GCard.vue";
@@ -69,11 +69,32 @@ const buttonTooltip = computed(() => {
6969
});
7070
const quotaSourceLabel = computed(() => {
7171
if ("quota" in props.sourceOption && props.sourceOption.quota.enabled) {
72-
return props.sourceOption.quota.source;
72+
return props.sourceOption.quota.source ?? null;
7373
}
7474
75-
return "";
75+
return null;
7676
});
77+
const quotaSourceKey = computed(() => quotaSourceLabel.value ?? "__null__");
78+
79+
const quotaUsageStore = useQuotaUsageStore();
80+
81+
const isLoadingUsage = computed(() => Boolean(quotaUsageStore.loadingBySource[quotaSourceKey.value]));
82+
const quotaUsage = computed(() => quotaUsageStore.getQuotaUsageBySourceLabel(quotaSourceLabel.value) ?? null);
83+
84+
watch(
85+
() => {
86+
const hasQuota = "quota" in props.sourceOption && props.sourceOption.quota.enabled;
87+
return [hasQuota, quotaSourceLabel.value] as const;
88+
},
89+
([hasQuota, sourceLabel]) => {
90+
if (!hasQuota) {
91+
return;
92+
}
93+
94+
void quotaUsageStore.loadQuotaUsageForSource(sourceLabel, true);
95+
},
96+
{ immediate: true },
97+
);
7798
7899
const primaryActions = computed<CardAction[]>(() => [
79100
{
@@ -118,14 +139,10 @@ const primaryActions = computed<CardAction[]>(() => [
118139
</template>
119140

120141
<template v-if="'quota' in props.sourceOption && props.sourceOption.quota.enabled" v-slot:tags>
121-
<QuotaSourceUsageProvider
122-
ref="quotaUsageProvider"
123-
v-slot="{ result: quotaUsage, loading: isLoadingUsage }"
124-
class="w-100"
125-
:quota-source-label="quotaSourceLabel">
142+
<div class="w-100">
126143
<LoadingSpan v-if="isLoadingUsage" message="Loading usage" />
127144
<QuotaUsageBar v-else-if="quotaUsage" :quota-usage="quotaUsage" :embedded="true" />
128-
</QuotaSourceUsageProvider>
145+
</div>
129146
</template>
130147
</GCard>
131148
</template>

client/src/components/ObjectStore/DescribeObjectStore.test.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import { createTestingPinia } from "@pinia/testing";
12
import { getLocalVue } from "@tests/vitest/helpers";
23
import { shallowMount } from "@vue/test-utils";
3-
import { describe, expect, it } from "vitest";
4+
import { PiniaVuePlugin, setActivePinia } from "pinia";
5+
import { beforeEach, describe, expect, it, vi } from "vitest";
46

57
import DescribeObjectStore from "./DescribeObjectStore.vue";
68

79
const localVue = getLocalVue();
10+
localVue.use(PiniaVuePlugin);
811

912
const DESCRIPTION = "My cool **markdown**";
1013

@@ -31,11 +34,18 @@ const TEST_STORAGE_API_RESPONSE_WITH_NAME = {
3134

3235
describe("DescribeObjectStore.vue", () => {
3336
let wrapper;
37+
let pinia;
38+
39+
beforeEach(() => {
40+
pinia = createTestingPinia({ createSpy: vi.fn });
41+
setActivePinia(pinia);
42+
});
3443

3544
async function mountWithResponse(response) {
3645
wrapper = shallowMount(DescribeObjectStore, {
3746
propsData: { storageInfo: response, what: "where i am throwing my test dataset" },
3847
localVue,
48+
pinia,
3949
});
4050
}
4151

client/src/components/ObjectStore/DescribeObjectStore.vue

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup lang="ts">
2-
import { computed, ref, watch } from "vue";
2+
import { computed, watch } from "vue";
33
4-
import { QuotaSourceUsageProvider } from "@/components/User/DiskUsage/Quota/QuotaUsageProvider.js";
4+
import { useQuotaUsageStore } from "@/stores/quotaUsageStore";
55
66
import type { AnyStorageDescription } from "./types";
77
@@ -22,14 +22,22 @@ const isPrivate = computed(() => props.storageInfo.private);
2222
const badges = computed(() => props.storageInfo.badges);
2323
const userDefined = computed(() => props.storageInfo.object_store_id?.startsWith("user_objects://"));
2424
25-
const quotaUsageProvider = ref(null);
25+
const quotaUsageStore = useQuotaUsageStore();
26+
const quotaSourceKey = computed(() => quotaSourceLabel.value ?? "__null__");
27+
const isLoadingUsage = computed(() => Boolean(quotaUsageStore.loadingBySource[quotaSourceKey.value]));
28+
const quotaUsage = computed(() => quotaUsageStore.getQuotaUsageBySourceLabel(quotaSourceLabel.value) ?? null);
2629
27-
watch(props, async () => {
28-
if (quotaUsageProvider.value) {
29-
// @ts-ignore
30-
quotaUsageProvider.value.update({ quotaSourceLabel: quotaSourceLabel.value });
31-
}
32-
});
30+
watch(
31+
() => [props.storageInfo.quota?.enabled, quotaSourceLabel.value] as const,
32+
([enabled, sourceLabel]) => {
33+
if (!enabled) {
34+
return;
35+
}
36+
37+
void quotaUsageStore.loadQuotaUsageForSource(sourceLabel, true);
38+
},
39+
{ immediate: true },
40+
);
3341
3442
defineExpose({
3543
isPrivate,
@@ -59,14 +67,10 @@ export default {
5967
>.
6068
</div>
6169
<ObjectStoreBadges :badges="badges"> </ObjectStoreBadges>
62-
<QuotaSourceUsageProvider
63-
v-if="storageInfo.quota && storageInfo.quota.enabled"
64-
ref="quotaUsageProvider"
65-
v-slot="{ result: quotaUsage, loading: isLoadingUsage }"
66-
:quota-source-label="quotaSourceLabel">
70+
<div v-if="storageInfo.quota && storageInfo.quota.enabled">
6771
<b-spinner v-if="isLoadingUsage" />
6872
<QuotaUsageBar v-else-if="quotaUsage" :quota-usage="quotaUsage" :embedded="true" />
69-
</QuotaSourceUsageProvider>
73+
</div>
7074
<div v-else>Galaxy has no quota configured for this storage.</div>
7175
<ConfigurationMarkdown
7276
v-if="storageInfo.description"

client/src/components/ObjectStore/ShowSelectedObjectStore.test.js

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { createTestingPinia } from "@pinia/testing";
12
import { getLocalVue } from "@tests/vitest/helpers";
23
import { mount } from "@vue/test-utils";
34
import flushPromises from "flush-promises";
4-
import { describe, expect, it } from "vitest";
5+
import { describe, expect, it, vi } from "vitest";
56

67
import { useServerMock } from "@/api/client/__mocks__";
78

@@ -44,19 +45,23 @@ const USER_OBJECT_STORE_DATA = {
4445
variables: {},
4546
};
4647

47-
describe("ShowSelectedObjectStore", () => {
48-
let wrapper;
48+
function mountWithPreferredStoreId(preferredObjectStoreId) {
49+
const wrapper = mount(ShowSelectedObjectStore, {
50+
propsData: { preferredObjectStoreId, forWhat: "Data goes into..." },
51+
localVue,
52+
pinia: createTestingPinia({ createSpy: vi.fn }),
53+
});
54+
return wrapper;
55+
}
4956

57+
describe("ShowSelectedObjectStore", () => {
5058
it("should show a loading message and then a DescribeObjectStore component", async () => {
5159
server.use(
5260
http.get("/api/object_stores/{object_store_id}", ({ response }) => {
5361
return response(200).json(OBJECT_STORE_DATA);
5462
}),
5563
);
56-
wrapper = mount(ShowSelectedObjectStore, {
57-
propsData: { preferredObjectStoreId: TEST_OBJECT_ID, forWhat: "Data goes into..." },
58-
localVue,
59-
});
64+
const wrapper = mountWithPreferredStoreId(TEST_OBJECT_ID);
6065
let loadingEl = wrapper.findComponent(LoadingSpan);
6166
expect(loadingEl.exists()).toBeTruthy();
6267
expect(loadingEl.find(".loading-message").text()).toContain("Loading Galaxy storage details");
@@ -73,10 +78,7 @@ describe("ShowSelectedObjectStore", () => {
7378
}),
7479
);
7580

76-
wrapper = mount(ShowSelectedObjectStore, {
77-
propsData: { preferredObjectStoreId: TEST_USER_OBJECT_STORE_ID, forWhat: "Data goes into..." },
78-
localVue,
79-
});
81+
const wrapper = mountWithPreferredStoreId(TEST_USER_OBJECT_STORE_ID);
8082
let loadingEl = wrapper.findComponent(LoadingSpan);
8183
expect(loadingEl.exists()).toBeTruthy();
8284
expect(loadingEl.find(".loading-message").text()).toContain("Loading Galaxy storage details");

client/src/components/User/DiskUsage/DiskUsageSummary.vue

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { storeToRefs } from "pinia";
55
import { computed, onMounted, ref, watch } from "vue";
66
77
import { type AsyncTaskResultSummary, GalaxyApi } from "@/api";
8-
import { fetchCurrentUserQuotaUsages, type QuotaUsage } from "@/api/users";
98
import { useConfig } from "@/composables/config";
109
import { useTaskMonitor } from "@/composables/taskMonitor";
10+
import { useQuotaUsageStore } from "@/stores/quotaUsageStore";
1111
import { useUserStore } from "@/stores/userStore";
1212
import { errorMessageAsString } from "@/utils/simple-error";
1313
import { bytesToString } from "@/utils/utils";
@@ -17,9 +17,10 @@ import QuotaUsageSummary from "@/components/User/DiskUsage/Quota/QuotaUsageSumma
1717
const { config, isConfigLoaded } = useConfig(true);
1818
const userStore = useUserStore();
1919
const { currentUser } = storeToRefs(userStore);
20+
const quotaUsageStore = useQuotaUsageStore();
2021
const { isRunning: isRecalculateTaskRunning, waitForTask } = useTaskMonitor();
2122
22-
const quotaUsages = ref<QuotaUsage[]>();
23+
const quotaUsages = computed(() => quotaUsageStore?.quotaUsages);
2324
const errorMessage = ref<string>();
2425
const isRecalculating = ref<boolean>(false);
2526
@@ -39,9 +40,8 @@ watch(
3940
(newValue, oldValue) => {
4041
// Make sure we reload the user and the quota usages when the recalculation is done
4142
if (oldValue && !newValue) {
42-
const includeHistories = false;
43-
userStore.loadUser(includeHistories);
44-
loadQuotaUsages();
43+
userStore.refreshUser();
44+
quotaUsageStore.applyRecalculationCompletedRefresh();
4545
}
4646
},
4747
);
@@ -76,17 +76,12 @@ async function onRefresh() {
7676
}
7777
}
7878
79-
async function loadQuotaUsages() {
79+
onMounted(async () => {
8080
try {
81-
const currentUserQuotaUsages = await fetchCurrentUserQuotaUsages();
82-
quotaUsages.value = currentUserQuotaUsages;
81+
await quotaUsageStore.loadQuotaUsages();
8382
} catch (error) {
8483
errorMessage.value = errorMessageAsString(error);
8584
}
86-
}
87-
88-
onMounted(async () => {
89-
await loadQuotaUsages();
9085
});
9186
</script>
9287
<template>

client/src/components/User/DiskUsage/Management/StorageManager.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import { ref } from "vue";
33
44
import { useConfig } from "@/composables/config";
5+
import { useQuotaUsageStore } from "@/stores/quotaUsageStore";
6+
import { useUserStore } from "@/stores/userStore";
57
import localize from "@/utils/localization";
68
import { wait } from "@/utils/utils";
79
@@ -25,6 +27,8 @@ const breadcrumbItems = [
2527
2628
const { config } = useConfig();
2729
const { cleanupCategories } = useCleanupCategories();
30+
const quotaUsageStore = useQuotaUsageStore();
31+
const userStore = useUserStore();
2832
2933
const currentOperation = ref<CleanupOperation>();
3034
const currentTotalItems = ref(0);
@@ -49,6 +53,8 @@ async function onConfirmCleanupSelected(selectedItems: CleanableItem[]) {
4953
cleanupResult.value = await currentOperation.value.cleanupItems(selectedItems);
5054
if (cleanupResult.value.hasUpdatedResults) {
5155
refreshOperationId.value = currentOperation.value.id.toString();
56+
quotaUsageStore.requestRefreshDebounced();
57+
userStore.refreshUser();
5258
}
5359
}
5460
}

0 commit comments

Comments
 (0)