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"
This commit is part of merge request !40. Comments created here will be created in the context of that merge request.
Showing
with 581 additions and 14 deletions
<script setup lang="ts"> <script setup lang="ts">
import type { BucketIn } from "@/client/s3proxy"; 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 BootstrapModal from "@/components/modals/BootstrapModal.vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
...@@ -30,17 +30,13 @@ onMounted(() => { ...@@ -30,17 +30,13 @@ onMounted(() => {
createBucketModal = new Modal("#" + props.modalID); createBucketModal = new Modal("#" + props.modalID);
}); });
const formValid = computed<boolean>(
() => bucketCreateForm.value?.checkValidity() ?? false
);
function createBucket() { function createBucket() {
formState.validated = true; formState.validated = true;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
formState.bucketNameTaken = false; formState.bucketNameTaken = false;
bucket.description = bucket.description.trim(); bucket.description = bucket.description.trim();
bucket.name = bucket.name.trim(); bucket.name = bucket.name.trim();
if (formValid.value) { if (bucketCreateForm.value?.checkValidity()) {
formState.loading = true; formState.loading = true;
bucketRepository.createBucket( bucketRepository.createBucket(
bucket, bucket,
......
...@@ -75,10 +75,6 @@ const permissionUserReadonly = computed<boolean>(() => { ...@@ -75,10 +75,6 @@ const permissionUserReadonly = computed<boolean>(() => {
return formState.readonly || editPermission.value; return formState.readonly || editPermission.value;
}); });
const formValid = computed<boolean>(
() => permissionForm.value?.checkValidity() ?? false
);
// Watchers // Watchers
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
watch( watch(
...@@ -189,7 +185,7 @@ function findSubFolders( ...@@ -189,7 +185,7 @@ function findSubFolders(
*/ */
function formSubmit() { function formSubmit() {
formState.error = false; formState.error = false;
if (formValid.value) { if (permissionForm.value?.checkValidity()) {
const tempPermission: BucketPermissionIn = permission; const tempPermission: BucketPermissionIn = permission;
if (permission.from_timestamp != null) { if (permission.from_timestamp != null) {
tempPermission.from_timestamp = 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(() => { ...@@ -50,7 +50,6 @@ onMounted(() => {
v-if="latestVersion?.icon_url != null" v-if="latestVersion?.icon_url != null"
:src="latestVersion.icon_url" :src="latestVersion.icon_url"
class="img-fluid float-end icon" class="img-fluid float-end icon"
alt="Workflow icon"
/> />
</div> </div>
<p class="card-text" :class="{ 'text-truncate': truncateDescription }"> <p class="card-text" :class="{ 'text-truncate': truncateDescription }">
......
...@@ -4,7 +4,7 @@ import { WorkflowVersionService } from "@/client/workflow"; ...@@ -4,7 +4,7 @@ import { WorkflowVersionService } from "@/client/workflow";
import type { WorkflowVersionFull } from "@/client/workflow"; import type { WorkflowVersionFull } from "@/client/workflow";
import axios from "axios"; import axios from "axios";
import MarkdownRenderer from "@/components/MarkdownRenderer.vue"; 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<{ const props = defineProps<{
versionId: string; versionId: string;
...@@ -132,7 +132,7 @@ onMounted(() => { ...@@ -132,7 +132,7 @@ onMounted(() => {
<p v-if="props.activeTab === 'description'"> <p v-if="props.activeTab === 'description'">
<markdown-renderer :markdown="versionState.descriptionMarkdown" /> <markdown-renderer :markdown="versionState.descriptionMarkdown" />
</p> </p>
<parameter-schema-description-component <parameter-schema-form-component
v-else-if="props.activeTab === 'parameters'" v-else-if="props.activeTab === 'parameters'"
:schema="versionState.parameterSchema" :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