Skip to content

Commit

Permalink
Merge pull request #16670 from davelopez/enhance_disk_usage_summary
Browse files Browse the repository at this point in the history
Enhance disk quota usage summary
  • Loading branch information
dannon authored Sep 11, 2023
2 parents e79ba43 + 8be06f0 commit 7e1971e
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 129 deletions.
16 changes: 14 additions & 2 deletions client/src/components/Page/PageDropdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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");
Expand Down
96 changes: 75 additions & 21 deletions client/src/components/User/DiskUsage/DiskUsageSummary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: `<div id='${quotaUsageSummaryComponentId}'/>` };

// 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;
}
Expand All @@ -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");
});
});
184 changes: 97 additions & 87 deletions client/src/components/User/DiskUsage/DiskUsageSummary.vue
Original file line number Diff line number Diff line change
@@ -1,113 +1,123 @@
<script setup lang="ts">
import { storeToRefs } from "pinia";
import { computed, onMounted, ref, watch } from "vue";
import { useConfig } from "@/composables/config";
import { useTaskMonitor } from "@/composables/taskMonitor";
import { fetcher } from "@/schema";
import { useUserStore } from "@/stores/userStore";
import { errorMessageAsString } from "@/utils/simple-error";
import { bytesToString } from "@/utils/utils";
import { QuotaUsage, UserQuotaUsageData } from "./Quota/model";
import QuotaUsageSummary from "@/components/User/DiskUsage/Quota/QuotaUsageSummary.vue";
const { config, isConfigLoaded } = useConfig(true);
const userStore = useUserStore();
const { currentUser } = storeToRefs(userStore);
const { isRunning: isRecalculateTaskRunning, waitForTask } = useTaskMonitor();
const quotaUsages = ref<QuotaUsage[]>();
const errorMessage = ref<string>();
const isRecalculating = ref<boolean>(false);
const niceTotalDiskUsage = computed(() => {
if (!currentUser.value || currentUser.value.isAnonymous) {
return "Unknown";
}
return bytesToString(currentUser.value.total_disk_usage, true);
});
const isRefreshing = computed(() => {
return isRecalculateTaskRunning.value || isRecalculating.value;
});
watch(
() => isRefreshing.value,
(newValue, oldValue) => {
// Make sure we reload the user and the quota usages when the recalculation is done
if (oldValue && !newValue) {
const includeHistories = false;
userStore.loadUser(includeHistories);
loadQuotaUsages();
}
}
);
async function displayRecalculationForSeconds(seconds: number) {
return new Promise<void>((resolve) => {
isRecalculating.value = true;
setTimeout(() => {
isRecalculating.value = false;
resolve();
}, seconds * 1000);
});
}
const recalculateDiskUsage = fetcher.path("/api/users/current/recalculate_disk_usage").method("put").create();
async function onRefresh() {
try {
const response = await recalculateDiskUsage({});
if (response.status == 200) {
// Wait for the task to complete
waitForTask(response.data.id);
} else if (response.status == 204) {
// We cannot track any task, so just display the
// recalculation message for a reasonable amount of time
await displayRecalculationForSeconds(30);
}
} catch (e) {
errorMessage.value = errorMessageAsString(e);
}
}
const fetchQuotaUsages = fetcher.path("/api/users/{user_id}/usage").method("get").create();
async function loadQuotaUsages() {
try {
const { data } = await fetchQuotaUsages({ user_id: "current" });
quotaUsages.value = data.map((u: UserQuotaUsageData) => new QuotaUsage(u));
} catch (e) {
errorMessage.value = errorMessageAsString(e);
}
}
onMounted(async () => {
await loadQuotaUsages();
});
</script>
<template>
<div>
<b-alert v-if="errorMessage" variant="danger" show>
<h2 class="alert-heading h-sm">{{ errorMessageTitle }}</h2>
<h2 v-localize class="alert-heading h-sm">Failed to access disk usage details.</h2>
{{ errorMessage }}
</b-alert>
<b-container v-if="currentUser">
<b-row v-if="isConfigLoaded && config.enable_quotas" class="justify-content-md-center">
<QuotaUsageSummary v-if="quotaUsages" :quota-usages="quotaUsages" />
</b-row>
<h2 v-else id="basic-disk-usage-summary" class="text-center my-3">
You're using <b>{{ getTotalDiskUsage(currentUser) }}</b> of disk space.
You're using <b>{{ niceTotalDiskUsage }}</b> of disk space.
</h2>
</b-container>
<b-container class="text-center mb-5 w-75">
<button
id="refresh-disk-usage"
title="Recalculate disk usage"
:disabled="isRecalculating"
:disabled="isRefreshing"
variant="outline-secondary"
size="sm"
pill
@click="onRefresh">
<b-spinner v-if="isRecalculating" small />
<b-spinner v-if="isRefreshing" small />
<span v-else>Refresh</span>
</button>
<b-alert
v-if="isRecalculating"
class="mt-2"
variant="info"
dismissible
fade
:show="dismissCountDown"
@dismiss-count-down="countDownChanged">
<b-alert v-if="isRefreshing" class="refreshing-alert mt-2" variant="info" show dismissible fade>
Recalculating disk usage... this may take some time, please check back later.
</b-alert>
</b-container>
</div>
</template>

<script>
import axios from "axios";
import QuotaUsageSummary from "components/User/DiskUsage/Quota/QuotaUsageSummary";
import { getAppRoot } from "onload/loadConfig";
import { mapActions, mapState } from "pinia";
import _l from "utils/localization";
import { rethrowSimple } from "utils/simple-error";
import { bytesToString } from "utils/utils";
import { useConfig } from "@/composables/config";
import { useUserStore } from "@/stores/userStore";
import { QuotaUsage } from "./Quota/model";
export default {
components: {
QuotaUsageSummary,
},
setup() {
const { config, isConfigLoaded } = useConfig(true);
return { config, isConfigLoaded };
},
data() {
return {
errorMessageTitle: _l("Failed to access disk usage details."),
errorMessage: null,
isRecalculating: false,
dismissCountDown: 0,
};
},
computed: {
...mapState(useUserStore, ["currentUser"]),
quotaUsages() {
return [new QuotaUsage(this.currentUser)];
},
},
methods: {
...mapActions(useUserStore, ["loadUser"]),
getTotalDiskUsage(user) {
return bytesToString(user.total_disk_usage, true);
},
onError(errorMessage) {
this.errorMessage = errorMessage;
},
async onRefresh() {
await this.requestDiskUsageRecalculation();
await this.displayRecalculationForSeconds(30);
this.loadUser();
},
async requestDiskUsageRecalculation() {
try {
await axios.put(`${getAppRoot()}api/users/current/recalculate_disk_usage`);
} catch (e) {
rethrowSimple(e);
}
},
async displayRecalculationForSeconds(seconds) {
return new Promise((resolve) => {
this.isRecalculating = true;
this.dismissCountDown = seconds;
setTimeout(() => {
this.isRecalculating = false;
resolve();
}, seconds * 1000);
});
},
countDownChanged(dismissCountDown) {
this.dismissCountDown = dismissCountDown;
},
},
};
</script>
Loading

0 comments on commit 7e1971e

Please sign in to comment.