Skip to content

Commit

Permalink
Implement quota tracking options per ObjectStore.
Browse files Browse the repository at this point in the history
  • Loading branch information
jmchilton committed Feb 19, 2023
1 parent 8e0a01e commit f7a4992
Show file tree
Hide file tree
Showing 36 changed files with 1,434 additions and 289 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,18 @@ import { getLocalVue } from "tests/jest/helpers";
import flushPromises from "flush-promises";
import MockAdapter from "axios-mock-adapter";
import axios from "axios";
import MarkdownIt from "markdown-it";

const localVue = getLocalVue();

const TEST_STORAGE_API_RESPONSE_WITHOUT_ID = {
object_store_id: null,
private: false,
};
const TEST_STORAGE_API_RESPONSE_WITH_ID = {
object_store_id: "foobar",
private: false,
};
const TEST_STORAGE_API_RESPONSE_WITH_NAME = {
object_store_id: "foobar",
name: "my cool storage",
description: "My cool **markdown**",
private: true,
};
const TEST_DATASET_ID = "1";
const TEST_STORAGE_URL = `/api/datasets/${TEST_DATASET_ID}/storage`;
const TEST_RENDERED_MARKDOWN_AS_HTML = "<p>My cool <strong>markdown</strong>\n";
const TEST_ERROR_MESSAGE = "Opps all errors.";

// works fine without mocking but I guess it is more JS unit-y with the mock?
jest.mock("markdown-it");
MarkdownIt.mockImplementation(() => {
return {
render(markdown) {
return TEST_RENDERED_MARKDOWN_AS_HTML;
},
};
});

describe("Dataset Storage", () => {
describe("DatasetStorage.vue", () => {
let axiosMock;
let wrapper;

Expand All @@ -62,6 +40,7 @@ describe("Dataset Storage", () => {
mount();
await wrapper.vm.$nextTick();
expect(wrapper.findAll("loading-span-stub").length).toBe(1);
expect(wrapper.findAll("describe-object-store-stub").length).toBe(0);
});

it("test error rendering...", async () => {
Expand All @@ -78,46 +57,8 @@ describe("Dataset Storage", () => {
it("test dataset storage with object store without id", async () => {
await mountWithResponse(TEST_STORAGE_API_RESPONSE_WITHOUT_ID);
expect(wrapper.findAll("loading-span-stub").length).toBe(0);
expect(wrapper.vm.descriptionRendered).toBeNull();
const header = wrapper.findAll("h2");
expect(header.length).toBe(1);
expect(header.at(0).text()).toBe("Dataset Storage");
const byIdSpan = wrapper.findAll(".display-os-by-id");
expect(byIdSpan.length).toBe(0);
const byNameSpan = wrapper.findAll(".display-os-by-name");
expect(byNameSpan.length).toBe(0);
const byDefaultSpan = wrapper.findAll(".display-os-default");
expect(byDefaultSpan.length).toBe(1);
});

it("test dataset storage with object store id", async () => {
await mountWithResponse(TEST_STORAGE_API_RESPONSE_WITH_ID);
expect(wrapper.findAll("loading-span-stub").length).toBe(0);
expect(wrapper.vm.storageInfo.object_store_id).toBe("foobar");
expect(wrapper.vm.descriptionRendered).toBeNull();
const header = wrapper.findAll("h2");
expect(header.length).toBe(1);
expect(header.at(0).text()).toBe("Dataset Storage");
const byIdSpan = wrapper.findAll(".display-os-by-id");
expect(byIdSpan.length).toBe(1);
const byNameSpan = wrapper.findAll(".display-os-by-name");
expect(byNameSpan.length).toBe(0);
expect(wrapper.find("object-store-restriction-span-stub").props("isPrivate")).toBeFalsy();
});

it("test dataset storage with object store name", async () => {
await mountWithResponse(TEST_STORAGE_API_RESPONSE_WITH_NAME);
expect(wrapper.findAll("loading-span-stub").length).toBe(0);
expect(wrapper.vm.storageInfo.object_store_id).toBe("foobar");
expect(wrapper.vm.descriptionRendered).toBe(TEST_RENDERED_MARKDOWN_AS_HTML);
const header = wrapper.findAll("h2");
expect(header.length).toBe(1);
expect(header.at(0).text()).toBe("Dataset Storage");
const byIdSpan = wrapper.findAll(".display-os-by-id");
expect(byIdSpan.length).toBe(0);
const byNameSpan = wrapper.findAll(".display-os-by-name");
expect(byNameSpan.length).toBe(1);
expect(wrapper.find("object-store-restriction-span-stub").props("isPrivate")).toBeTruthy();
expect(wrapper.findAll("describe-object-store-stub").length).toBe(1);
expect(wrapper.vm.storageInfo.private).toEqual(false);
});

afterEach(() => {
Expand Down
33 changes: 4 additions & 29 deletions client/src/components/Dataset/DatasetStorage/DatasetStorage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,22 @@
</p>
</div>
<div v-else>
<p>
This dataset is stored in
<span class="display-os-by-name" v-if="storageInfo.name">
a Galaxy <object-store-restriction-span :is-private="isPrivate" /> object store named
<b>{{ storageInfo.name }}</b>
</span>
<span class="display-os-by-id" v-else-if="storageInfo.object_store_id">
a Galaxy <object-store-restriction-span :is-private="isPrivate" /> object store with id
<b>{{ storageInfo.object_store_id }}</b>
</span>
<span class="display-os-default" v-else>
the default configured Galaxy <object-store-restriction-span :is-private="isPrivate" /> object store </span
>.
</p>
<div v-html="descriptionRendered"></div>
<describe-object-store what="This dataset is stored in" :storage-info="storageInfo" />
</div>
</div>
</template>

<script>
import axios from "axios";
import { getAppRoot } from "onload/loadConfig";
import LoadingSpan from "components/LoadingSpan";
import MarkdownIt from "markdown-it";
import { errorMessageAsString } from "utils/simple-error";
import ObjectStoreRestrictionSpan from "./ObjectStoreRestrictionSpan";
import DescribeObjectStore from "components/ObjectStore/DescribeObjectStore";
import LoadingSpan from "components/LoadingSpan";
export default {
components: {
DescribeObjectStore,
LoadingSpan,
ObjectStoreRestrictionSpan,
},
props: {
datasetId: {
Expand All @@ -64,7 +49,6 @@ export default {
data() {
return {
storageInfo: null,
descriptionRendered: null,
errorMessage: null,
};
},
Expand All @@ -86,9 +70,6 @@ export default {
}
return rootSources[0].source_uri;
},
isPrivate() {
return this.storageInfo.private;
},
},
created() {
const datasetId = this.datasetId;
Expand All @@ -103,13 +84,7 @@ export default {
methods: {
handleResponse(response) {
const storageInfo = response.data;
const description = storageInfo.description;
this.storageInfo = storageInfo;
if (description) {
this.descriptionRendered = MarkdownIt({ html: true }).render(storageInfo.description);
} else {
this.descriptionRendered = null;
}
},
},
};
Expand Down
80 changes: 80 additions & 0 deletions client/src/components/ObjectStore/DescribeObjectStore.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { shallowMount } from "@vue/test-utils";
import DescribeObjectStore from "./DescribeObjectStore";
import { getLocalVue } from "tests/jest/helpers";
import MarkdownIt from "markdown-it";

const localVue = getLocalVue();

const TEST_STORAGE_API_RESPONSE_WITHOUT_ID = {
object_store_id: null,
private: false,
};
const TEST_RENDERED_MARKDOWN_AS_HTML = "<p>My cool <strong>markdown</strong>\n";

const TEST_STORAGE_API_RESPONSE_WITH_ID = {
object_store_id: "foobar",
private: false,
};
const TEST_STORAGE_API_RESPONSE_WITH_NAME = {
object_store_id: "foobar",
name: "my cool storage",
description: "My cool **markdown**",
private: true,
};

// works fine without mocking but I guess it is more JS unit-y with the mock?
jest.mock("markdown-it");
MarkdownIt.mockImplementation(() => {
return {
render(markdown) {
return TEST_RENDERED_MARKDOWN_AS_HTML;
},
};
});

describe("DescribeObjectStore.vue", () => {
let wrapper;

async function mountWithResponse(response) {
wrapper = shallowMount(DescribeObjectStore, {
propsData: { storageInfo: response, what: "where i am throwing my test dataset" },
localVue,
});
}

it("test dataset storage with object store without id", async () => {
await mountWithResponse(TEST_STORAGE_API_RESPONSE_WITHOUT_ID);
expect(wrapper.findAll("loading-span-stub").length).toBe(0);
expect(wrapper.vm.descriptionRendered).toBeNull();
const byIdSpan = wrapper.findAll(".display-os-by-id");
expect(byIdSpan.length).toBe(0);
const byNameSpan = wrapper.findAll(".display-os-by-name");
expect(byNameSpan.length).toBe(0);
const byDefaultSpan = wrapper.findAll(".display-os-default");
expect(byDefaultSpan.length).toBe(1);
});

it("test dataset storage with object store id", async () => {
await mountWithResponse(TEST_STORAGE_API_RESPONSE_WITH_ID);
expect(wrapper.findAll("loading-span-stub").length).toBe(0);
expect(wrapper.vm.storageInfo.object_store_id).toBe("foobar");
expect(wrapper.vm.descriptionRendered).toBeNull();
const byIdSpan = wrapper.findAll(".display-os-by-id");
expect(byIdSpan.length).toBe(1);
const byNameSpan = wrapper.findAll(".display-os-by-name");
expect(byNameSpan.length).toBe(0);
expect(wrapper.find("object-store-restriction-span-stub").props("isPrivate")).toBeFalsy();
});

it("test dataset storage with object store name", async () => {
await mountWithResponse(TEST_STORAGE_API_RESPONSE_WITH_NAME);
expect(wrapper.findAll("loading-span-stub").length).toBe(0);
expect(wrapper.vm.storageInfo.object_store_id).toBe("foobar");
expect(wrapper.vm.descriptionRendered).toBe(TEST_RENDERED_MARKDOWN_AS_HTML);
const byIdSpan = wrapper.findAll(".display-os-by-id");
expect(byIdSpan.length).toBe(0);
const byNameSpan = wrapper.findAll(".display-os-by-name");
expect(byNameSpan.length).toBe(1);
expect(wrapper.find("object-store-restriction-span-stub").props("isPrivate")).toBeTruthy();
});
});
70 changes: 70 additions & 0 deletions client/src/components/ObjectStore/DescribeObjectStore.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<template>
<div>
<div>
<span v-localize>{{ what }}</span>
<span v-if="storageInfo.name" class="display-os-by-name">
a Galaxy <object-store-restriction-span :is-private="isPrivate" /> object store named
<b>{{ storageInfo.name }}</b>
</span>
<span v-else-if="storageInfo.object_store_id" class="display-os-by-id">
a Galaxy <object-store-restriction-span :is-private="isPrivate" /> object store with id
<b>{{ storageInfo.object_store_id }}</b>
</span>
<span v-else class="display-os-default">
the default configured Galaxy <object-store-restriction-span :is-private="isPrivate" /> object store </span
>.
</div>
<QuotaSourceUsageProvider
v-if="storageInfo.quota && storageInfo.quota.enabled"
v-slot="{ result: quotaUsage, loading: isLoadingUsage }"
:quota-source-label="quotaSourceLabel">
<b-spinner v-if="isLoadingUsage" />
<QuotaUsageBar v-else-if="quotaUsage" :quota-usage="quotaUsage" :embedded="true" />
</QuotaSourceUsageProvider>
<div v-else>Galaxy has no quota configured for this object store.</div>
<div v-html="descriptionRendered"></div>
</div>
</template>

<script>
import MarkdownIt from "markdown-it";
import ObjectStoreRestrictionSpan from "./ObjectStoreRestrictionSpan";
import QuotaUsageBar from "components/User/DiskUsage/Quota/QuotaUsageBar";
import { QuotaSourceUsageProvider } from "components/User/DiskUsage/Quota/QuotaUsageProvider";
export default {
components: {
ObjectStoreRestrictionSpan,
QuotaSourceUsageProvider,
QuotaUsageBar,
},
props: {
storageInfo: {
type: Object,
required: true,
},
what: {
type: String,
required: true,
},
},
computed: {
quotaSourceLabel() {
return this.storageInfo.quota?.source;
},
descriptionRendered() {
const description = this.storageInfo.description;
let descriptionRendered;
if (description) {
descriptionRendered = MarkdownIt({ html: true }).render(description);
} else {
descriptionRendered = null;
}
return descriptionRendered;
},
isPrivate() {
return this.storageInfo.private;
},
},
};
</script>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<span class="stored-how" v-b-tooltip.hover :title="title">{{ text }}</span>
<span v-b-tooltip.hover class="stored-how" :title="title">{{ text }}</span>
</template>

<script>
Expand Down
29 changes: 22 additions & 7 deletions client/src/components/User/DiskUsage/Quota/QuotaUsageBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import { DEFAULT_QUOTA_SOURCE_LABEL, QuotaUsage } from "./model/QuotaUsage";
interface QuotaUsageBarProps {
quotaUsage: QuotaUsage;
embedded: boolean;
}
const props = defineProps<QuotaUsageBarProps>();
const props = withDefaults(defineProps<QuotaUsageBarProps>(), {
embedded: false,
});
const storageSourceText = ref(localize("storage source"));
const percentOfDiskQuotaUsedText = ref(localize("% of disk quota used"));
Expand All @@ -33,27 +36,39 @@ const progressVariant = computed(() => {
return "danger";
});
const sourceTag = computed(() => {
return props.embedded ? "div" : "h2";
});
const usageTag = computed(() => {
return props.embedded ? "div" : "h3";
});
defineExpose({
isDefaultQuota,
quotaHasLimit,
});
</script>

<template>
<div class="quota-usage-bar w-75 mx-auto my-5">
<h2 v-if="!isDefaultQuota" class="quota-storage-source">
<div class="quota-usage-bar mx-auto" :class="{ 'w-75': !embedded, 'my-5': !embedded, 'my-1': embedded }">
<component :is="sourceTag" v-if="!isDefaultQuota" class="quota-storage-source">
<span class="storage-source-label">
<b>{{ quotaUsage.sourceLabel }}</b>
</span>
{{ storageSourceText }}
</h2>
<h3>
</component>
<component :is="usageTag">
<b>{{ quotaUsage.niceTotalDiskUsage }}</b>
<span v-if="quotaHasLimit"> of {{ quotaUsage.niceQuota }}</span> used
</h3>
</component>
<span v-if="quotaHasLimit" class="quota-percent-text">
{{ quotaUsage.quotaPercent }}{{ percentOfDiskQuotaUsedText }}
</span>
<b-progress :value="quotaUsage.quotaPercent" :variant="progressVariant" max="100" />
<b-progress
v-if="quotaHasLimit || !embedded"
:value="quotaUsage.quotaPercent"
:variant="progressVariant"
max="100" />
</div>
</template>
Loading

0 comments on commit f7a4992

Please sign in to comment.