Skip to content

Commit 04f1c2a

Browse files
authored
Merge pull request #22640 from davelopez/add_quota_store
Replace QuotaUsageProvider with Pinia store + enhancements
2 parents 5c32614 + f75577f commit 04f1c2a

16 files changed

Lines changed: 427 additions & 127 deletions

client/src/api/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,13 @@ export interface AnonymousUser extends AnonymousUserModel {
275275
/** Represents any user, including anonymous users or session-less (null) users.**/
276276
export type AnyUser = RegisteredUser | AnonymousUser | null;
277277

278+
export function toAnyUser(user: UserModel): AnyUser {
279+
if ("email" in user) {
280+
return { ...user, isAnonymous: false } as RegisteredUser;
281+
}
282+
return { ...user, isAnonymous: true } as AnonymousUser;
283+
}
284+
278285
export function isRegisteredUser(user: AnyUser | UserModel): user is RegisteredUser {
279286
return user !== null && "email" in user;
280287
}

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)