Skip to content

Commit

Permalink
Object Store creation templates.
Browse files Browse the repository at this point in the history
  • Loading branch information
jmchilton committed Feb 21, 2024
1 parent b416779 commit 2f69f57
Show file tree
Hide file tree
Showing 71 changed files with 3,770 additions and 120 deletions.
14 changes: 12 additions & 2 deletions client/src/api/objectStores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,20 @@ export async function getSelectableObjectStores() {
}

const getObjectStore = fetcher.path("/api/object_stores/{object_store_id}").method("get").create();
const getUserObjectStoreInstance = fetcher
.path("/api/object_store_instances/{user_object_store_id}")
.method("get")
.create();

export async function getObjectStoreDetails(id: string) {
const { data } = await getObjectStore({ object_store_id: id });
return data;
if (id.startsWith("user_objects://")) {
const userObjectStoreId = id.substring("user_objects://".length);
const { data } = await getUserObjectStoreInstance({ user_object_store_id: userObjectStoreId });
return data;
} else {
const { data } = await getObjectStore({ object_store_id: id });
return data;
}
}

const updateObjectStoreFetcher = fetcher.path("/api/datasets/{dataset_id}/object_store_id").method("put").create();
Expand Down
59 changes: 1 addition & 58 deletions client/src/components/Form/FormElement.vue
Original file line number Diff line number Diff line change
Expand Up @@ -312,62 +312,5 @@ const isOptional = computed(() => !isRequired.value && attrs.value["optional"] !
</template>
<style lang="scss" scoped>
@import "theme/blue.scss";
@import "~@fortawesome/fontawesome-free/scss/_variables";
.ui-form-element {
margin-top: $margin-v * 0.25;
margin-bottom: $margin-v * 0.5;
overflow: visible;
clear: both;
.ui-form-title {
word-wrap: break-word;
font-weight: bold;
.ui-form-title-message {
font-size: $font-size-base * 0.7;
font-weight: 300;
vertical-align: text-top;
color: $text-light;
cursor: default;
}
.ui-form-title-star {
color: $text-light;
font-weight: 300;
cursor: default;
}
.warning {
color: $brand-danger;
}
}
.ui-form-field {
position: relative;
margin-top: $margin-v * 0.25;
}
&:deep(.ui-form-collapsible-icon),
&:deep(.ui-form-connected-icon) {
border: none;
background: none;
padding: 0;
line-height: 1;
font-size: 1.2em;
&:hover {
color: $brand-info;
}
&:focus {
color: $brand-primary;
}
&:active {
background: none;
}
}
}
@import "./form-elements.scss";
</style>
58 changes: 58 additions & 0 deletions client/src/components/Form/form-elements.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
@import "theme/blue.scss";
@import "~@fortawesome/fontawesome-free/scss/_variables";

.ui-form-element {
margin-top: $margin-v * 0.25;
margin-bottom: $margin-v * 0.5;
overflow: visible;
clear: both;

.ui-form-title {
word-wrap: break-word;
font-weight: bold;

.ui-form-title-message {
font-size: $font-size-base * 0.7;
font-weight: 300;
vertical-align: text-top;
color: $text-light;
cursor: default;
}

.ui-form-title-star {
color: $text-light;
font-weight: 300;
cursor: default;
}

.warning {
color: $brand-danger;
}
}

.ui-form-field {
position: relative;
margin-top: $margin-v * 0.25;
}

&:deep(.ui-form-collapsible-icon),
&:deep(.ui-form-connected-icon) {
border: none;
background: none;
padding: 0;
line-height: 1;
font-size: 1.2em;

&:hover {
color: $brand-info;
}

&:focus {
color: $brand-primary;
}

&:active {
background: none;
}
}
}
6 changes: 5 additions & 1 deletion client/src/components/ObjectStore/DescribeObjectStore.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const props = defineProps<Props>();
const quotaSourceLabel = computed(() => props.storageInfo.quota?.source);
const isPrivate = computed(() => props.storageInfo.private);
const badges = computed(() => props.storageInfo.badges);
const userDefined = computed(() => props.storageInfo.object_store_id?.startsWith("user_objects://"));
const quotaUsageProvider = ref(null);
Expand Down Expand Up @@ -67,6 +68,9 @@ export default {
<QuotaUsageBar v-else-if="quotaUsage" :quota-usage="quotaUsage" :embedded="true" />
</QuotaSourceUsageProvider>
<div v-else>Galaxy has no quota configured for this object store.</div>
<ConfigurationMarkdown v-if="storageInfo.description" :markdown="storageInfo.description" :admin="true" />
<ConfigurationMarkdown
v-if="storageInfo.description"
:markdown="storageInfo.description"
:admin="!userDefined" />
</div>
</template>
124 changes: 124 additions & 0 deletions client/src/components/ObjectStore/Instances/CreateForm.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { mount } from "@vue/test-utils";
import flushPromises from "flush-promises";
import { getLocalVue } from "tests/jest/helpers";

import { mockFetcher } from "@/api/schema/__mocks__";
import type { ObjectStoreTemplateSummary } from "@/components/ObjectStore/Templates/types";

import CreateForm from "./CreateForm.vue";

jest.mock("@/api/schema");

const FAKE_OBJECT_STORE = "A fake object store";

const localVue = getLocalVue(true);

const STANDARD_TEMPLATE: ObjectStoreTemplateSummary = {
type: "s3",
name: "moo",
description: null,
variables: [
{
name: "myvar",
type: "string",
help: "*myvar help*",
},
],
secrets: [
{
name: "mysecret",
help: "**mysecret help**",
},
],
id: "moo",
version: 0,
badges: [],
};

const SIMPLE_TEMPLATE: ObjectStoreTemplateSummary = {
type: "s3",
name: "moo",
description: null,
variables: [
{
name: "myvar",
type: "string",
help: "*myvar help*",
},
],
secrets: [
{
name: "mysecret",
help: "**mysecret help**",
},
],
id: "moo",
version: 0,
badges: [],
};

describe("CreateForm", () => {
it("should render a form with admin markdown converted to HTML in help", async () => {
const wrapper = mount(CreateForm, {
propsData: {
template: STANDARD_TEMPLATE,
},
localVue,
});
await flushPromises();

const varFormEl = wrapper.find("#form-element-myvar");
expect(varFormEl).toBeTruthy();
expect(varFormEl.html()).toContain("<em>myvar help</em>");

const secretFormEl = wrapper.find("#form-element-mysecret");
expect(secretFormEl).toBeTruthy();
expect(secretFormEl.html()).toContain("<strong>mysecret help</strong>");
});

it("should post to create a new object store on submit", async () => {
const wrapper = mount(CreateForm, {
propsData: {
template: SIMPLE_TEMPLATE,
},
localVue,
});
mockFetcher.path("/api/object_store_instances").method("post").mock({ data: FAKE_OBJECT_STORE });
await flushPromises();
const nameForElement = wrapper.find("#form-element-_meta_name");
nameForElement.find("input").setValue("My New Name");
const submitElement = wrapper.find("#submit");
submitElement.trigger("click");
await flushPromises();
const emitted = wrapper.emitted("created") || [];
expect(emitted).toHaveLength(1);
expect(emitted[0][0]).toBe(FAKE_OBJECT_STORE);
});

it("should indicate an error on failure", async () => {
const wrapper = mount(CreateForm, {
propsData: {
template: SIMPLE_TEMPLATE,
},
localVue,
});
mockFetcher
.path("/api/object_store_instances")
.method("post")
.mock(() => {
throw Error("Error creating this");
});
await flushPromises();
const nameForElement = wrapper.find("#form-element-_meta_name");
nameForElement.find("input").setValue("My New Name");
const submitElement = wrapper.find("#submit");
expect(wrapper.find(".object-store-instance-creation-error").exists()).toBe(false);
submitElement.trigger("click");
await flushPromises();
const emitted = wrapper.emitted("created") || [];
expect(emitted).toHaveLength(0);
const errorEl = wrapper.find(".object-store-instance-creation-error");
expect(errorEl.exists()).toBe(true);
expect(errorEl.html()).toContain("Error creating this");
});
});
81 changes: 81 additions & 0 deletions client/src/components/ObjectStore/Instances/CreateForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<script lang="ts" setup>
import { computed, ref } from "vue";
import { create } from "@/components/ObjectStore/Instances/services";
import type { SecretData, UserConcreteObjectStore, VariableData } from "@/components/ObjectStore/Instances/types";
import {
metadataFormEntryDescription,
metadataFormEntryName,
templateSecretFormEntry,
templateVariableFormEntry,
} from "@/components/ObjectStore/Instances/util";
import type { ObjectStoreTemplateSummary } from "@/components/ObjectStore/Templates/types";
import { errorMessageAsString } from "@/utils/simple-error";
import InstanceForm from "./InstanceForm.vue";
interface CreateFormProps {
template: ObjectStoreTemplateSummary;
}
const error = ref<string | null>(null);
const props = defineProps<CreateFormProps>();
const title = "Create a new object store for your data";
const submitTitle = "Submit";
const inputs = computed(() => {
const form = [];
const variables = props.template.variables ?? [];
const secrets = props.template.secrets ?? [];
form.push(metadataFormEntryName());
form.push(metadataFormEntryDescription());
for (const variable of variables) {
form.push(templateVariableFormEntry(variable, undefined));
}
for (const secret of secrets) {
form.push(templateSecretFormEntry(secret));
}
return form;
});
async function onSubmit(formData: any) {
const variables = props.template.variables ?? [];
const secrets = props.template.secrets ?? [];
const variableData: VariableData = {};
const secretData: SecretData = {};
for (const variable of variables) {
variableData[variable.name] = formData[variable.name];
}
for (const secret of secrets) {
secretData[secret.name] = formData[secret.name];
}
const name: string = formData._meta_name;
const description: string = formData._meta_description;
const payload = {
name: name,
description: description,
secrets: secretData,
variables: variableData,
template_id: props.template.id,
template_version: props.template.version ?? 0,
};
try {
const { data: objectStore } = await create(payload);
emit("created", objectStore);
} catch (e) {
error.value = errorMessageAsString(e);
return;
}
}
const emit = defineEmits<{
(e: "created", objectStore: UserConcreteObjectStore): void;
}>();
</script>
<template>
<div>
<b-alert v-if="error" variant="danger" class="object-store-instance-creation-error" show>
{{ error }}
</b-alert>
<InstanceForm :inputs="inputs" :title="title" :submit-title="submitTitle" @onSubmit="onSubmit" />
</div>
</template>
Loading

0 comments on commit 2f69f57

Please sign in to comment.