diff --git a/client/src/components/Page/PageDropdown.test.ts b/client/src/components/Page/PageDropdown.test.ts index 222f892ed48d..33e0b548d313 100644 --- a/client/src/components/Page/PageDropdown.test.ts +++ b/client/src/components/Page/PageDropdown.test.ts @@ -52,7 +52,13 @@ describe("PageDropdown.vue", () => { pinia: pinia, }); const userStore = useUserStore(); - userStore.currentUser = { email: "my@email", id: "1", tags_used: [], isAnonymous: false }; + userStore.currentUser = { + email: "my@email", + id: "1", + tags_used: [], + isAnonymous: false, + total_disk_usage: 1048576, + }; }); it("should show page title", async () => { @@ -113,7 +119,13 @@ describe("PageDropdown.vue", () => { }, }); const userStore = useUserStore(); - userStore.currentUser = { email: "my@email", id: "1", tags_used: [], isAnonymous: false }; + userStore.currentUser = { + email: "my@email", + id: "1", + tags_used: [], + isAnonymous: false, + total_disk_usage: 1048576, + }; wrapper.find(".page-dropdown").trigger("click"); await wrapper.vm.$nextTick(); wrapper.find(".dropdown-item-delete").trigger("click"); diff --git a/client/src/components/User/DiskUsage/DiskUsageSummary.test.ts b/client/src/components/User/DiskUsage/DiskUsageSummary.test.ts index c2fe85c41ed0..f6f657de91d1 100644 --- a/client/src/components/User/DiskUsage/DiskUsageSummary.test.ts +++ b/client/src/components/User/DiskUsage/DiskUsageSummary.test.ts @@ -3,46 +3,66 @@ import flushPromises from "flush-promises"; import { createPinia } from "pinia"; import { getLocalVue } from "tests/jest/helpers"; -import MockCurrentUser from "@/components/providers/MockCurrentUser"; -import MockProvider from "@/components/providers/MockProvider"; import { mockFetcher } from "@/schema/__mocks__"; +import { getCurrentUser } from "@/stores/users/queries"; import { useUserStore } from "@/stores/userStore"; +import { UserQuotaUsageData } from "./Quota/model"; + import DiskUsageSummary from "./DiskUsageSummary.vue"; jest.mock("@/schema"); +jest.mock("@/stores/users/queries"); const localVue = getLocalVue(); -const quotaUsageSummaryComponentId = "quota-usage-summary"; -const basicDiskUsageSummaryId = "basic-disk-usage-summary"; +const quotaUsageClassSelector = ".quota-usage"; +const basicDiskUsageSummaryId = "#basic-disk-usage-summary"; -const fakeUser = { - total_disk_usage: 1054068, +const fakeUserWithQuota = { + id: "fakeUser", + email: "fakeUserEmail", + tags_used: [], + isAnonymous: false, + total_disk_usage: 1048576, + quota_bytes: 104857600, + quota_percent: 1, + quota_source_label: "Default", }; -const CurrentUserMock = MockCurrentUser(fakeUser); -const QuotaUsageProviderMock = MockProvider({ - result: [], -}); -const QuotaUsageSummaryMock = { template: `
` }; + +// TODO: Replace this with a mockFetcher when #16608 is merged +const mockGetCurrentUser = getCurrentUser as jest.Mock; +mockGetCurrentUser.mockImplementation(() => Promise.resolve(fakeUserWithQuota)); + +const fakeQuotaUsages: UserQuotaUsageData[] = [ + { + quota_source_label: "Default", + quota_bytes: 104857600, + total_disk_usage: 1048576, + }, +]; + +const fakeTaskId = "fakeTaskId"; async function mountDiskUsageSummaryWrapper(enableQuotas: boolean) { mockFetcher .path("/api/configuration") .method("get") .mock({ data: { enable_quotas: enableQuotas } }); + mockFetcher.path("/api/users/{user_id}/usage").method("get").mock({ data: fakeQuotaUsages }); + mockFetcher + .path("/api/users/current/recalculate_disk_usage") + .method("put") + .mock({ status: 200, data: { id: fakeTaskId } }); + mockFetcher.path("/api/tasks/{task_id}/state").method("get").mock({ data: "SUCCESS" }); + const pinia = createPinia(); const wrapper = mount(DiskUsageSummary, { - stubs: { - CurrentUser: CurrentUserMock, - QuotaUsageProvider: QuotaUsageProviderMock, - QuotaUsageSummary: QuotaUsageSummaryMock, - }, localVue, pinia, }); const userStore = useUserStore(); - userStore.currentUser = { id: "fakeUser", email: "fakeUserEmail", tags_used: [], isAnonymous: false }; + userStore.currentUser = fakeUserWithQuota; await flushPromises(); return wrapper; } @@ -51,14 +71,48 @@ describe("DiskUsageSummary.vue", () => { it("should display basic disk usage summary if quotas are NOT enabled", async () => { const enableQuotasInConfig = false; const wrapper = await mountDiskUsageSummaryWrapper(enableQuotasInConfig); - expect(wrapper.find(`#${basicDiskUsageSummaryId}`).exists()).toBe(true); - expect(wrapper.find(`#${quotaUsageSummaryComponentId}`).exists()).toBe(false); + expect(wrapper.find(basicDiskUsageSummaryId).exists()).toBe(true); + const quotaUsages = wrapper.findAll(quotaUsageClassSelector); + expect(quotaUsages.length).toBe(0); }); it("should display quota usage summary if quotas are enabled", async () => { const enableQuotasInConfig = true; const wrapper = await mountDiskUsageSummaryWrapper(enableQuotasInConfig); - expect(wrapper.find(`#${basicDiskUsageSummaryId}`).exists()).toBe(false); - expect(wrapper.find(`#${quotaUsageSummaryComponentId}`).exists()).toBe(true); + expect(wrapper.find(basicDiskUsageSummaryId).exists()).toBe(false); + const quotaUsages = wrapper.findAll(quotaUsageClassSelector); + expect(quotaUsages.length).toBe(1); + }); + + it("should display the correct quota usage", async () => { + const enableQuotasInConfig = true; + const wrapper = await mountDiskUsageSummaryWrapper(enableQuotasInConfig); + const quotaUsage = wrapper.find(quotaUsageClassSelector); + expect(quotaUsage.text()).toContain("1 MB"); + }); + + it("should refresh the quota usage when the user clicks the refresh button", async () => { + const enableQuotasInConfig = true; + const wrapper = await mountDiskUsageSummaryWrapper(enableQuotasInConfig); + const quotaUsage = wrapper.find(quotaUsageClassSelector); + expect(quotaUsage.text()).toContain("1 MB"); + const updatedFakeQuotaUsages: UserQuotaUsageData[] = [ + { + quota_source_label: "Default", + quota_bytes: 104857600, + total_disk_usage: 2097152, + }, + ]; + mockFetcher.path("/api/users/{user_id}/usage").method("get").mock({ data: updatedFakeQuotaUsages }); + await wrapper.find("#refresh-disk-usage").trigger("click"); + await flushPromises(); + const refreshingAlert = wrapper.find(".refreshing-alert"); + expect(refreshingAlert.exists()).toBe(true); + // Make sure the refresh has finished before checking the quota usage + await flushPromises(); + await flushPromises(); + // The refreshing alert should disappear and the quota usage should be updated + expect(refreshingAlert.exists()).toBe(false); + expect(quotaUsage.text()).toContain("2 MB"); }); }); diff --git a/client/src/components/User/DiskUsage/DiskUsageSummary.vue b/client/src/components/User/DiskUsage/DiskUsageSummary.vue index bb1baea3ee65..2aca40bb11e8 100644 --- a/client/src/components/User/DiskUsage/DiskUsageSummary.vue +++ b/client/src/components/User/DiskUsage/DiskUsageSummary.vue @@ -1,7 +1,98 @@ + - - diff --git a/client/src/components/User/DiskUsage/Quota/model/QuotaUsage.test.ts b/client/src/components/User/DiskUsage/Quota/model/QuotaUsage.test.ts index 134aaa510b06..006440faf1a2 100644 --- a/client/src/components/User/DiskUsage/Quota/model/QuotaUsage.test.ts +++ b/client/src/components/User/DiskUsage/Quota/model/QuotaUsage.test.ts @@ -1,20 +1,20 @@ -import { DEFAULT_QUOTA_SOURCE_LABEL, QuotaUsage, type QuotaUsageResponse } from "./QuotaUsage"; +import { DEFAULT_QUOTA_SOURCE_LABEL, QuotaUsage, type UserQuotaUsageData } from "./QuotaUsage"; -const RAW_DEFAULT_QUOTA_USAGE: QuotaUsageResponse = { +const RAW_DEFAULT_QUOTA_USAGE: UserQuotaUsageData = { quota_source_label: undefined, quota_bytes: 68468436, total_disk_usage: 4546654, quota_percent: 20, }; -const RAW_LIMITED_SOURCE_QUOTA_USAGE: QuotaUsageResponse = { +const RAW_LIMITED_SOURCE_QUOTA_USAGE: UserQuotaUsageData = { quota_source_label: "The source", quota_bytes: 68468436, total_disk_usage: 4546654, quota_percent: 20, }; -const RAW_UNLIMITED_SOURCE_QUOTA_USAGE: QuotaUsageResponse = { +const RAW_UNLIMITED_SOURCE_QUOTA_USAGE: UserQuotaUsageData = { quota_source_label: "The unlimited source", quota_bytes: undefined, total_disk_usage: 4546654, diff --git a/client/src/components/User/DiskUsage/Quota/model/QuotaUsage.ts b/client/src/components/User/DiskUsage/Quota/model/QuotaUsage.ts index 28ce5dfb1b84..9f0c2577bf11 100644 --- a/client/src/components/User/DiskUsage/Quota/model/QuotaUsage.ts +++ b/client/src/components/User/DiskUsage/Quota/model/QuotaUsage.ts @@ -1,21 +1,17 @@ +import type { components } from "@/schema"; import { bytesToString } from "@/utils/utils"; export const DEFAULT_QUOTA_SOURCE_LABEL = "Default"; -export interface QuotaUsageResponse { - quota_source_label?: string; - quota_bytes?: number; - total_disk_usage: number; - quota_percent?: number; -} +export type UserQuotaUsageData = components["schemas"]["UserQuotaUsage"]; /** * Contains information about quota usage for a particular ObjectStore. */ export class QuotaUsage { - private _data: QuotaUsageResponse; + private _data: UserQuotaUsageData; - constructor(data: QuotaUsageResponse) { + constructor(data: UserQuotaUsageData) { this._data = data; } diff --git a/client/src/components/User/DiskUsage/Quota/model/index.js b/client/src/components/User/DiskUsage/Quota/model/index.js index 43c68fd60b0b..c5818f3a191e 100644 --- a/client/src/components/User/DiskUsage/Quota/model/index.js +++ b/client/src/components/User/DiskUsage/Quota/model/index.js @@ -1 +1 @@ -export { QuotaUsage } from "./QuotaUsage"; +export { QuotaUsage, UserQuotaUsageData } from "./QuotaUsage"; diff --git a/client/src/stores/userStore.ts b/client/src/stores/userStore.ts index 5cb89f483c41..13093688433c 100644 --- a/client/src/stores/userStore.ts +++ b/client/src/stores/userStore.ts @@ -2,6 +2,7 @@ import { defineStore } from "pinia"; import { computed, ref } from "vue"; import { useUserLocalStorage } from "@/composables/userLocalStorage"; +import { components } from "@/schema"; import { useHistoryStore } from "@/stores/historyStore"; import { addFavoriteToolQuery, @@ -10,7 +11,9 @@ import { setCurrentThemeQuery, } from "@/stores/users/queries"; -interface User { +type QuotaUsageResponse = components["schemas"]["UserQuotaUsage"]; + +interface User extends QuotaUsageResponse { id: string; email: string; tags_used: string[]; @@ -54,20 +57,22 @@ export const useUserStore = defineStore("userStore", () => { currentUser.value = user; } - function loadUser() { + function loadUser(includeHistories = true) { if (!loadPromise) { loadPromise = getCurrentUser() .then(async (user) => { - const historyStore = useHistoryStore(); currentUser.value = { ...user, isAnonymous: !user.email }; currentPreferences.value = user?.preferences ?? null; // TODO: This is a hack to get around the fact that the API returns a string if (currentPreferences.value?.favorites) { currentPreferences.value.favorites = JSON.parse(user?.preferences?.favorites ?? { tools: [] }); } - await historyStore.loadCurrentHistory(); - // load first few histories for user to start pagination - await historyStore.loadHistories(); + if (includeHistories) { + const historyStore = useHistoryStore(); + await historyStore.loadCurrentHistory(); + // load first few histories for user to start pagination + await historyStore.loadHistories(); + } }) .catch((e) => { console.error("Failed to load user", e);