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 Apr 27, 2023
1 parent 8d83fd4 commit 70dc980
Show file tree
Hide file tree
Showing 67 changed files with 3,390 additions and 134 deletions.
59 changes: 1 addition & 58 deletions client/src/components/Form/FormElement.vue
Original file line number Diff line number Diff line change
Expand Up @@ -283,62 +283,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.25;
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.25;
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;
}
}
}
20 changes: 20 additions & 0 deletions client/src/components/ObjectStore/AdminMarkdown.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script setup lang="ts">
import { computed } from "vue";
import { adminMarkup } from "./adminConfig";
interface Props {
markdown: string;
}
const props = defineProps<Props>();
const adminMarkdownHtml = computed(() => adminMarkup(props.markdown || ""));
</script>
<template>
<!-- Disable v-html warning because this component is only used
for admin defined content - HTML should be trusted in this
context. This component should never be used to render user
generated or defined content.
-->
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="adminMarkdownHtml" />
</template>
123 changes: 123 additions & 0 deletions client/src/components/ObjectStore/Instances/CreateForm.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { mount } from "@vue/test-utils";
import { getLocalVue } from "tests/jest/helpers";
import type { ObjectStoreTemplateSummary } from "@/components/ObjectStore/Templates/types";
import flushPromises from "flush-promises";
import { mockFetcher } from "@/schema/__mocks__";

jest.mock("@/schema");

const FAKE_OBJECT_STORE = "A fake object store";

const localVue = getLocalVue(true);

import CreateForm from "./CreateForm.vue";

const STANDARD_TEMPLATE: ObjectStoreTemplateSummary = {
type: "s3",
name: "moo",
description: undefined,
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: undefined,
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");
});
});
79 changes: 79 additions & 0 deletions client/src/components/ObjectStore/Instances/CreateForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<script lang="ts" setup>
import { computed, ref } from "vue";
import type { ObjectStoreTemplateSummary } from "@/components/ObjectStore/Templates/types";
import type { SecretData, VariableData, UserConcreteObjectStore } from "@/components/ObjectStore/Instances/types";
import InstanceForm from "./InstanceForm.vue";
import { create } from "@/components/ObjectStore/Instances/services";
import { errorMessageAsString } from "@/utils/simple-error";
import {
metadataFormEntryDescription,
metadataFormEntryName,
templateVariableFormEntry,
templateSecretFormEntry,
} from "@/components/ObjectStore/Instances/util";
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>
<instance-form :inputs="inputs" :title="title" :submit-title="submitTitle" @onSubmit="onSubmit" />
</div>
</template>
33 changes: 33 additions & 0 deletions client/src/components/ObjectStore/Instances/CreateInstance.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<script setup lang="ts">
import { computed } from "vue";
import LoadingSpan from "@/components/LoadingSpan.vue";
import CreateForm from "./CreateForm.vue";
import type { UserConcreteObjectStore } from "./types";
interface Props {
templateId: string;
}
import { useInstanceRouting } from "./routing";
import { useObjectStoreTemplatesStore } from "@/stores/objectStoreTemplatesStore";
const objectStoreTemplatesStore = useObjectStoreTemplatesStore();
objectStoreTemplatesStore.fetchTemplates();
const { goToIndex } = useInstanceRouting();
const props = defineProps<Props>();
const template = computed(() => objectStoreTemplatesStore.getLatestTemplate(props.templateId));
async function onCreated(objectStore: UserConcreteObjectStore) {
const message = `Created object store ${objectStore.name}`;
goToIndex({ message });
}
</script>

<template>
<div>
<loading-span v-if="!template" message="Loading object store templates" />
<create-form v-else :template="template" @created="onCreated"></create-form>
</div>
</template>
Loading

0 comments on commit 70dc980

Please sign in to comment.