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 Jul 26, 2022
1 parent fbb9bbf commit 77be402
Show file tree
Hide file tree
Showing 36 changed files with 1,430 additions and 286 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,18 @@ import { getLocalVue } from "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("h3");
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("h3");
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("h3");
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
83 changes: 83 additions & 0 deletions client/src/components/ObjectStore/DescribeObjectStore.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { shallowMount } from "@vue/test-utils";
import DescribeObjectStore from "./DescribeObjectStore";
import { getLocalVue } from "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_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 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
>.
</div>
<QuotaSourceUsageProvider
:quotaSourceLabel="quotaSourceLabel"
v-if="storageInfo.quota && storageInfo.quota.enabled"
v-slot="{ result: quotaUsage, loading: isLoadingUsage }">
<b-spinner v-if="isLoadingUsage" />
<QuotaUsageBar v-else-if="quotaUsage" :quota-usage="quotaUsage" :embedded="true" />
</QuotaSourceUsageProvider>
<div v-else>Galaxy has no quota configured fo 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>
29 changes: 23 additions & 6 deletions client/src/components/User/DiskUsage/Quota/QuotaUsageBar.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
<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>

Expand All @@ -27,6 +31,12 @@ export default {
type: Object,
required: true,
},
// If this is embedded in DatasetStorage or more intricate components like
// that - shrink everything and avoid h2/h3 (component already has those).
embedded: {
type: Boolean,
default: false,
},
},
data() {
return {
Expand Down Expand Up @@ -55,6 +65,13 @@ export default {
}
return "danger";
},
/** @returns {String} */
sourceTag() {
return this.embedded ? "div" : "h2";
},
usageTag() {
return this.embedded ? "div" : "h3";
},
},
};
</script>
Loading

0 comments on commit 77be402

Please sign in to comment.