Skip to content
Snippets Groups Projects
Verified Commit 463356b9 authored by Daniel Göbel's avatar Daniel Göbel
Browse files

Add parser to render basic HTML form for parameter schema

#38
parent 5e86d977
No related branches found
No related tags found
1 merge request!40Resolve "Implement json schema parser"
Showing
with 581 additions and 14 deletions
<script setup lang="ts">
import type { BucketIn } from "@/client/s3proxy";
import { reactive, onMounted, computed, ref } from "vue";
import { reactive, onMounted, ref } from "vue";
import BootstrapModal from "@/components/modals/BootstrapModal.vue";
import { useRouter } from "vue-router";
import { Modal } from "bootstrap";
......@@ -30,17 +30,13 @@ onMounted(() => {
createBucketModal = new Modal("#" + props.modalID);
});
const formValid = computed<boolean>(
() => bucketCreateForm.value?.checkValidity() ?? false
);
function createBucket() {
formState.validated = true;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
formState.bucketNameTaken = false;
bucket.description = bucket.description.trim();
bucket.name = bucket.name.trim();
if (formValid.value) {
if (bucketCreateForm.value?.checkValidity()) {
formState.loading = true;
bucketRepository.createBucket(
bucket,
......
......@@ -75,10 +75,6 @@ const permissionUserReadonly = computed<boolean>(() => {
return formState.readonly || editPermission.value;
});
const formValid = computed<boolean>(
() => permissionForm.value?.checkValidity() ?? false
);
// Watchers
// -----------------------------------------------------------------------------
watch(
......@@ -189,7 +185,7 @@ function findSubFolders(
*/
function formSubmit() {
formState.error = false;
if (formValid.value) {
if (permissionForm.value?.checkValidity()) {
const tempPermission: BucketPermissionIn = permission;
if (permission.from_timestamp != null) {
tempPermission.from_timestamp =
......
<script setup lang="ts">
import { computed, ref, reactive, watch, onMounted } from "vue";
import ParameterGroupForm from "@/components/parameter-schema/form-mode/ParameterGroupForm.vue";
import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
import Ajv from "ajv";
import type { ValidateFunction } from "ajv";
const launchForm = ref<HTMLFormElement | null>(null);
const schemaCompiler = new Ajv({
strict: false,
});
let validateSchema: ValidateFunction;
const props = defineProps({
schema: {
type: Object,
required: true,
},
});
type ParameterGroup = {
group: string;
title: string;
icon?: string;
};
const formState = reactive<{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
formInput: Record<string, any>;
validated: boolean;
}>({
formInput: {},
validated: false,
});
watch(
() => props.schema,
(newValue) => {
updateSchema(newValue);
}
);
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unused-vars */
function updateSchema(schema: Record<string, any>) {
validateSchema = schemaCompiler.compile(props.schema);
const b = Object.keys(schema["definitions"]).map((groupName) => [
groupName,
Object.fromEntries(
Object.entries(schema["definitions"][groupName]["properties"])
// @ts-ignore
.filter(([_, parameter]) => !parameter["hidden"])
.map(([parameterName, parameter]) => [
parameterName,
// @ts-ignore
parameter["default"],
])
),
]);
formState.formInput = Object.fromEntries(b);
}
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unused-vars */
onMounted(() => {
updateSchema(props.schema);
});
const parameterGroups = computed<Record<string, never>>(
() => props.schema["definitions"]
);
const navParameterGroups = computed<ParameterGroup[]>(() =>
Object.keys(parameterGroups.value)
.map((group) => {
return {
group: group,
title: parameterGroups.value[group]["title"],
icon: parameterGroups.value[group]["fa_icon"],
};
})
.filter(
// filter all groups that have only hidden parameters
(group) =>
Object.keys(parameterGroups.value[group.group]["properties"]).filter(
(key) =>
!parameterGroups.value[group.group]["properties"][key]["hidden"]
).length > 0
)
);
function startWorkflow() {
formState.validated = true;
if (launchForm.value?.checkValidity()) {
const realInput = Object.values(formState.formInput).reduce((acc, val) => {
return { ...acc, ...val };
});
const schemaValid = validateSchema(realInput);
console.log(realInput);
if (!schemaValid) console.log(validateSchema.errors);
} else {
console.log("invalid");
}
}
</script>
<template>
<div class="row mb-5 align-items-start">
<form
class="col-9"
id="launchWorkflowForm"
ref="launchForm"
:class="{ 'was-validated': formState.validated }"
>
<div v-for="(group, groupName) in parameterGroups" :key="groupName">
<parameter-group-form
:modelValue="formState.formInput[groupName]"
@update:model-value="
(newValue) => (formState.formInput[groupName] = newValue)
"
v-if="formState.formInput[groupName]"
:parameter-group-name="groupName"
:parameter-group="group"
/>
</div>
</form>
<div
class="col-3 sticky-top bg-dark rounded-1 px-0"
style="top: 70px !important; max-height: calc(100vh - 150px)"
>
<div class="d-grid gap-2 col-6">
<button
type="submit"
form="launchWorkflowForm"
@click.prevent="startWorkflow"
class="btn btn-success"
>
Launch
</button>
</div>
<nav class="h-100">
<nav class="nav">
<ul class="ps-0">
<li
class="nav-link"
v-for="group in navParameterGroups"
:key="group.group"
>
<a :href="'#' + group.group"
><font-awesome-icon
:icon="group.icon"
v-if="group.icon"
class="me-2"
/>{{ group.title }}</a
>
</li>
</ul>
</nav>
</nav>
</div>
</div>
</template>
<style scoped></style>
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps({
parameter: {
type: Object,
required: true,
validator(value: Record<string, never>) {
return "number" === value["type"];
},
},
required: Boolean,
parameterName: {
type: String,
required: true,
},
modelValue: {
type: Boolean,
},
helpId: {
type: String,
},
});
const randomIDSuffix = Math.random().toString(16).substr(2, 8);
const helpTextPresent = computed<boolean>(() => props.parameter["help_text"]);
const defaultValue = computed<boolean>(
() => props.parameter["default"] ?? false
);
const emit = defineEmits<{
(e: "update:modelValue", value: boolean): void;
}>();
</script>
<template>
<div
class="flex-fill mb-0 text-bg-light fs-6 ps-4 d-flex align-items-center justify-content-start"
:class="{ 'rounded-end': !helpTextPresent }"
>
<div class="form-check form-check-inline">
<label class="form-check-label" :for="'trueOption' + randomIDSuffix"
>True</label
>
<input
class="form-check-input"
type="radio"
:name="'inlineRadioOptions' + randomIDSuffix"
:id="'trueOption' + randomIDSuffix"
:value="true"
:checked="defaultValue"
@input="emit('update:modelValue', true)"
/>
</div>
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="radio"
:name="'inlineRadioOptions' + randomIDSuffix"
:id="'falseOption' + randomIDSuffix"
:value="false"
@input="emit('update:modelValue', false)"
:checked="!defaultValue"
/>
<label class="form-check-label" :for="'falseOption' + randomIDSuffix"
>False</label
>
</div>
</div>
</template>
<style scoped></style>
<script setup lang="ts">
import { computed, ref } from "vue";
const props = defineProps({
parameter: {
type: Object,
required: true,
validator(value: Record<string, never>) {
return "string" === value["type"] && value["enum"];
},
},
required: Boolean,
parameterName: {
type: String,
required: true,
},
modelValue: {
type: String,
},
helpId: {
type: String,
},
});
const defaultValue = computed<string>(() => props.parameter["default"]);
const possibleValues = computed<string[]>(() => props.parameter["enum"]);
const enumSelection = ref<HTMLSelectElement | undefined>(undefined);
const emit = defineEmits<{
(e: "update:modelValue", value: string | undefined): void;
}>();
function updateValue() {
emit("update:modelValue", enumSelection.value?.value);
}
</script>
<template>
<select
ref="enumSelection"
:value="props.modelValue"
@input="updateValue"
class="form-select"
:required="required"
:aria-describedby="props.helpId"
>
<option
v-for="val in possibleValues"
:key="val"
:selected="defaultValue === val"
>
{{ val }}
</option>
</select>
</template>
<style scoped></style>
<script setup lang="ts">
import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
import { computed, watch } from "vue";
import ParameterNumberInput from "@/components/parameter-schema/form-mode/ParameterNumberInput.vue";
import MarkdownRenderer from "@/components/MarkdownRenderer.vue";
import ParameterBooleanInput from "@/components/parameter-schema/form-mode/ParameterBooleanInput.vue";
import ParameterEnumInput from "@/components/parameter-schema/form-mode/ParameterEnumInput.vue";
import ParameterStringInput from "@/components/parameter-schema/form-mode/ParameterStringInput.vue";
const props = defineProps({
parameterGroup: {
type: Object,
required: true,
validator(value: Record<string, never>) {
return "object" === value["type"];
},
},
parameterGroupName: {
type: String,
required: true,
},
modelValue: {
type: Object,
required: true,
},
});
const title = computed<string>(() => props.parameterGroup["title"]);
const icon = computed<string>(() => props.parameterGroup["fa_icon"]);
const description = computed<string>(() => props.parameterGroup["description"]);
const groupHidden = computed<boolean>(() =>
Object.keys(parameters.value).reduce(
(acc: boolean, val: string) => acc && parameters.value[val]["hidden"],
true
)
);
const parameters = computed<Record<string, never>>(
() => props.parameterGroup["properties"]
);
const modelStuff = computed(() => props.modelValue);
const emit = defineEmits<{
(
e: "update:modelValue",
value: Record<string, number | string | boolean | undefined>
): void;
}>();
watch(
modelStuff,
(newVal) => {
//console.log("Group", props.parameterGroupName, newVal);
emit("update:modelValue", newVal);
},
{
deep: true,
}
);
</script>
<template>
<div class="card bg-dark mb-3" v-if="!groupHidden">
<h2 class="card-header" :id="props.parameterGroupName">
<font-awesome-icon :icon="icon" class="me-2" v-if="icon" />
{{ title }}
</h2>
<div class="card-body">
<h5 class="card-title" v-if="description">{{ description }}</h5>
<template
v-for="(parameter, parameterName) in parameters"
:key="parameterName"
>
<template v-if="!parameter['hidden']">
<div class="input-group">
<span class="input-group-text" :id="parameterName + '-help'">
<font-awesome-icon
class="me-2 text-dark"
:icon="parameter['fa_icon']"
v-if="parameter['fa_icon']"
/>
<code>--{{ parameterName }}</code>
</span>
<parameter-number-input
v-if="
parameter['type'] === 'number' ||
parameter['type'] === 'integer'
"
:parameter-name="parameterName"
:parameter="parameter"
:help-id="parameterName + '-help'"
:required="parameterGroup['required']?.includes(parameterName)"
:model-value="modelStuff[parameterName]"
@update:model-value="
(newValue) => (modelStuff[parameterName] = newValue)
"
/>
<parameter-boolean-input
v-else-if="parameter['type'] === 'boolean'"
:parameter-name="parameterName"
:parameter="parameter"
:help-id="parameterName + '-help'"
:model-value="modelStuff[parameterName]"
@update:model-value="
(newValue) => (modelStuff[parameterName] = newValue)
"
/>
<template v-else-if="parameter['type'] === 'string'">
<parameter-enum-input
v-if="parameter['enum']"
:parameter-name="parameterName"
:parameter="parameter"
:model-value="modelStuff[parameterName]"
:required="parameterGroup['required']?.includes(parameterName)"
@update:model-value="
(newValue) => (modelStuff[parameterName] = newValue)
"
/>
<parameter-string-input
v-else
:parameter-name="parameterName"
:parameter="parameter"
:model-value="modelStuff[parameterName]"
:required="parameterGroup['required']?.includes(parameterName)"
@update:model-value="
(newValue) => (modelStuff[parameterName] = newValue)
"
/>
</template>
<span
class="input-group-text cursor-pointer px-2"
v-if="parameter['help_text']"
data-bs-toggle="collapse"
:data-bs-target="'#helpCollapse' + parameterName"
aria-expanded="false"
aria-controls="collapseExample"
>
<font-awesome-icon
class="cursor-pointer"
icon="fa-solid fa-circle-question"
/>
</span>
</div>
<label v-if="parameter['description']"
><markdown-renderer :markdown="parameter['description']"
/></label>
<div
class="collapse p-2 pb-0 bg-dark mx-2 mt-1 flex-shrink-1"
:id="'helpCollapse' + parameterName"
v-if="parameter['help_text']"
>
<markdown-renderer
class="helpTextCode"
:markdown="parameter['help_text']"
/>
</div>
</template>
</template>
</div>
</div>
</template>
<style scoped>
div.card-body {
filter: brightness(0.9);
}
span.cursor-pointer:hover {
color: var(--bs-info);
background: var(--bs-light);
}
</style>
<script setup lang="ts">
import { ref } from "vue";
const props = defineProps({
parameter: {
type: Object,
required: true,
validator(value: Record<string, never>) {
return "number" === value["type"] || "integer" === value["type"];
},
},
required: Boolean,
parameterName: {
type: String,
required: true,
},
modelValue: {
type: Number,
},
helpId: {
type: String,
},
});
const emit = defineEmits<{
(e: "update:modelValue", value: number | undefined): void;
}>();
const numberInput = ref<HTMLInputElement | undefined>(undefined);
function updateValue() {
emit("update:modelValue", Number(numberInput.value?.value));
}
</script>
<template>
<input
class="form-control"
type="number"
ref="numberInput"
:max="props.parameter['maximum']"
:min="props.parameter['minimum']"
:value="props.modelValue"
:required="props.required"
:aria-describedby="props.helpId"
@input="updateValue"
/>
</template>
<style scoped></style>
<script setup lang="ts">
import { computed, watch, ref } from "vue";
const props = defineProps({
parameter: {
type: Object,
required: true,
validator(value: Record<string, never>) {
return "string" === value["type"] && value["enum"];
},
},
required: Boolean,
parameterName: {
type: String,
required: true,
},
modelValue: {
type: String,
},
helpId: {
type: String,
},
});
const defaultValue = computed<string>(() => props.parameter["default"]);
watch(defaultValue, (newVal, oldVal) => {
if (newVal != oldVal && newVal != undefined) {
emit("update:modelValue", newVal);
}
});
const pattern = computed<string>(() => props.parameter["pattern"]);
const emit = defineEmits<{
(e: "update:modelValue", value: string | undefined): void;
}>();
const stringInput = ref<HTMLInputElement | undefined>(undefined);
function updateValue() {
emit(
"update:modelValue",
stringInput.value?.value ? stringInput.value?.value : undefined
);
}
</script>
<template>
<input
ref="stringInput"
class="form-control"
type="text"
:value="props.modelValue"
:required="props.required"
:aria-describedby="props.helpId"
:pattern="pattern"
@input="updateValue"
/>
</template>
<style scoped></style>
......@@ -50,7 +50,6 @@ onMounted(() => {
v-if="latestVersion?.icon_url != null"
:src="latestVersion.icon_url"
class="img-fluid float-end icon"
alt="Workflow icon"
/>
</div>
<p class="card-text" :class="{ 'text-truncate': truncateDescription }">
......
......@@ -4,7 +4,7 @@ import { WorkflowVersionService } from "@/client/workflow";
import type { WorkflowVersionFull } from "@/client/workflow";
import axios from "axios";
import MarkdownRenderer from "@/components/MarkdownRenderer.vue";
import ParameterSchemaDescriptionComponent from "@/components/parameter-schema/ParameterSchemaDescriptionComponent.vue";
import ParameterSchemaFormComponent from "@/components/parameter-schema/ParameterSchemaFormComponent.vue";
const props = defineProps<{
versionId: string;
......@@ -132,7 +132,7 @@ onMounted(() => {
<p v-if="props.activeTab === 'description'">
<markdown-renderer :markdown="versionState.descriptionMarkdown" />
</p>
<parameter-schema-description-component
<parameter-schema-form-component
v-else-if="props.activeTab === 'parameters'"
:schema="versionState.parameterSchema"
/>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment