diff --git a/src/assets/main.css b/src/assets/main.css index 8237a72ed8cd830c4dfca825a3cc69eb7ed07564..30edd8d3f5626056cc2b2d22a7310ba56a3b0457 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -3,6 +3,10 @@ body { background: #181818; } +.fs-0 { + font-size: 3.5rem; +} + .top-toast { top: 4rem; } diff --git a/src/components/parameter-schema/ParameterSchemaFormComponent.vue b/src/components/parameter-schema/ParameterSchemaFormComponent.vue index b80fd1eb59bcedf639a898f81065ad497ae78be6..29ebd83f608caf4cf899159fd0380ce9a6301494 100644 --- a/src/components/parameter-schema/ParameterSchemaFormComponent.vue +++ b/src/components/parameter-schema/ParameterSchemaFormComponent.vue @@ -14,7 +14,6 @@ import { Toast } from "bootstrap"; const props = defineProps({ schema: { type: Object, - required: true, }, workflowVersionId: { type: String, @@ -66,7 +65,7 @@ const formState = reactive<{ // Computed Properties // ============================================================================= const parameterGroups = computed<Record<string, never>>( - () => props.schema["definitions"] + () => props.schema?.["definitions"] ); // Create a list with the names of all parameter groups @@ -94,7 +93,9 @@ const navParameterGroups = computed<ParameterGroup[]>(() => watch( () => props.schema, (newValue) => { - updateSchema(newValue); + if (newValue) { + updateSchema(newValue); + } } ); @@ -102,7 +103,7 @@ watch( // ============================================================================= /* 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); + validateSchema = schemaCompiler.compile(schema); const b = Object.keys(schema["definitions"]).map((groupName) => [ groupName, Object.fromEntries( @@ -122,6 +123,7 @@ function updateSchema(schema: Record<string, any>) { function startWorkflow() { formState.validated = true; + formState.errorType = undefined; if (launchForm.value?.checkValidity()) { const realInput = Object.values(formState.formInput).reduce((acc, val) => { return { ...acc, ...val }; @@ -132,7 +134,6 @@ function startWorkflow() { console.error(validateSchema.errors); errorToast?.show(); } else { - formState.errorType = undefined; formState.loading = true; WorkflowExecutionService.workflowExecutionStartWorkflow({ workflow_version_id: props.workflowVersionId, @@ -154,13 +155,16 @@ function startWorkflow() { formState.loading = false; }); } + } else { + formState.errorType = "form"; + errorToast?.show(); } } // Lifecycle Events // ============================================================================= onMounted(() => { - updateSchema(props.schema); + if (props.schema) updateSchema(props.schema); errorToast = new Toast("#workflowExecutionErrorToast"); }); </script> @@ -175,17 +179,22 @@ onMounted(() => { data-bs-autohide="true" id="workflowExecutionErrorToast" > - <div class="d-flex p-2"> - <div v-if="formState.errorType === 'limit'" class="toast-body"> - You have too many active workflow executions to start a new one - </div> - <div v-else> - There was an error with starting the workflow execution. Look in the - console for more information + <div class="d-flex p-2 justify-content-between align-items-center"> + <div class="toast-body"> + <template v-if="formState.errorType === 'limit'"> + You have too many active workflow executions to start a new one. + </template> + <template v-else-if="formState.errorType === 'form'"> + Some inputs are not valid. + </template> + <template v-else> + There was an error with starting the workflow execution. Look in the + console for more information. + </template> </div> <button type="button" - class="btn-close btn-close-white m-auto" + class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close" ></button> @@ -194,10 +203,13 @@ onMounted(() => { </div> <div class="row mb-5 align-items-start"> <form + v-if="props.schema" class="col-9" id="launchWorkflowForm" ref="launchForm" :class="{ 'was-validated': formState.validated }" + @submit.prevent="startWorkflow" + novalidate > <div class="card bg-dark mb-3"> <h2 class="card-header" id="pipelineGeneralOptions"> @@ -218,7 +230,7 @@ onMounted(() => { </span> <textarea class="form-control" - rows="2" + rows="1" v-model="formState.pipelineNotes" /> </div> @@ -260,6 +272,27 @@ onMounted(() => { /> </template> </form> + <!-- Loading card --> + <div v-else class="col-9"> + <div class="card bg-dark mb-3"> + <h2 class="card-header placeholder-glow"> + <span class="placeholder col-6"></span> + </h2> + <div class="card-body"> + <h5 class="card-title placeholder-glow"> + <span class="placeholder col-5"> </span> + </h5> + <template v-for="n in 4" :key="n"> + <div class="placeholder-glow fs-5"> + <span class="placeholder w-100"> </span> + </div> + <div class="mb-3 placeholder-glow"> + <span class="placeholder col-3"> </span> + </div> + </template> + </div> + </div> + </div> <div class="col-3 sticky-top bg-dark rounded-1 px-0" style="top: 70px !important; max-height: calc(100vh - 150px)" @@ -268,9 +301,8 @@ onMounted(() => { <button type="submit" form="launchWorkflowForm" - @click.prevent="startWorkflow" class="btn btn-success w-50 mx-2" - :disabled="formState.loading" + :disabled="formState.loading || !props.schema" > <font-awesome-icon icon="fa-solid fa-rocket" class="me-2" /> Launch @@ -286,7 +318,7 @@ onMounted(() => { </router-link> </div> <nav class="h-100"> - <nav class="nav"> + <nav v-if="props.schema" class="nav"> <ul class="ps-0"> <li class="nav-link"> <a href="#pipelineGeneralOptions" @@ -309,6 +341,14 @@ onMounted(() => { </li> </ul> </nav> + <!-- Loading nav links --> + <div v-else class="placeholder-glow ps-3 pt-3"> + <span + v-for="n in 5" + :key="n" + class="placeholder col-8 mt-2 mb-3" + ></span> + </div> </nav> </div> </div> diff --git a/src/router/index.ts b/src/router/index.ts index 3460872612edae5a193e4d2e6a3767e637c5ce47..c52fe14c0f359ba4979f022292602f2ead1aab12 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -46,7 +46,7 @@ const router = createRouter({ meta: { requiresReviewerRole: true }, }, { - path: "workflows/:workflowId/", + path: "workflows/:workflowId", name: "workflow", component: () => import("../views/workflows/WorkflowView.vue"), props: true, @@ -62,6 +62,13 @@ const router = createRouter({ activeTab: route.query.tab ?? "description", }), }, + { + path: "version/:versionId/start", + name: "workflow-start", + component: () => + import("../views/workflows/StartWorkflowView.vue"), + props: true, + }, ], }, ], diff --git a/src/views/workflows/StartWorkflowView.vue b/src/views/workflows/StartWorkflowView.vue new file mode 100644 index 0000000000000000000000000000000000000000..b539b33e5d26394231004855e6b80b61a6e54a28 --- /dev/null +++ b/src/views/workflows/StartWorkflowView.vue @@ -0,0 +1,55 @@ +<script setup lang="ts"> +import ParameterSchemaFormComponent from "@/components/parameter-schema/ParameterSchemaFormComponent.vue"; +import type { WorkflowVersionFull } from "@/client/workflow"; +import { WorkflowVersionService } from "@/client/workflow"; +import axios from "axios"; +import { onMounted, ref, reactive } from "vue"; +import type { JSONSchemaType } from "ajv"; + +const props = defineProps<{ + versionId: string; + workflowId: string; +}>(); + +const parameterSchema = ref(undefined); + +const versionState = reactive<{ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parameterSchema?: JSONSchemaType<any>; + workflowVersion?: WorkflowVersionFull; +}>({ + parameterSchema: undefined, + workflowVersion: undefined, +}); + +function downloadVersion() { + WorkflowVersionService.workflowVersionGetWorkflowVersion( + props.versionId, + props.workflowId + ) + .then((version) => { + versionState.workflowVersion = version; + return version; + }) + .then(downloadVersionFiles); +} + +function downloadVersionFiles(version: WorkflowVersionFull) { + axios.get(version.parameter_schema_url).then((response) => { + parameterSchema.value = response.data; + }); +} + +onMounted(() => { + downloadVersion(); +}); +</script> + +<template> + <parameter-schema-form-component + :workflow-version-id="versionId" + :schema="parameterSchema" + /> +</template> + +<style scoped></style> diff --git a/src/views/workflows/WorkflowView.vue b/src/views/workflows/WorkflowView.vue index 9a5b54fc2778d4531c8f134cfc42095633b0e40d..efff5466d9dd065f3d1e049bad9927dade635bbb 100644 --- a/src/views/workflows/WorkflowView.vue +++ b/src/views/workflows/WorkflowView.vue @@ -57,7 +57,11 @@ watch( watch( () => workflowState.activeVersionId, (newVersionId, oldVersionId) => { - if (newVersionId !== oldVersionId) { + if ( + newVersionId && + newVersionId !== oldVersionId && + route.name !== "workflow-start" + ) { router.push({ name: "workflow-version", params: { versionId: newVersionId }, @@ -101,7 +105,7 @@ function updateWorkflow(workflowId: string) { WorkflowService.workflowGetWorkflow(workflowId) .then((workflow) => { workflowState.workflow = workflow; - if (!workflowState.initialOpen || route.params.versionId == null) { + if (!workflowState.initialOpen || !route.params.versionId) { workflowState.activeVersionId = workflow.versions[workflow.versions.length - 1].git_commit_hash; } else { @@ -145,7 +149,10 @@ onMounted(() => { </div> <div v-else-if="workflowState.workflow != null"> <div class="d-flex justify-content-between align-items-center"> - <span class="fs-0 w-fit">{{ workflowState.workflow.name }}</span> + <div class="fs-0 w-fit text-light"> + {{ workflowState.workflow.name }} + <span v-if="activeVersionString">@{{ activeVersionString }}</span> + </div> <a :href="workflowState.workflow.repository_url" target="_blank"> <img v-if="activeVersionIcon != null" @@ -155,70 +162,80 @@ onMounted(() => { /></a> </div> <p class="fs-4 mb-5 mt-3">{{ workflowState.workflow.short_description }}</p> - <div - v-if="!versionLaunchable" - class="alert alert-warning w-fit mx-auto" - role="alert" - > - This version can not be used. - <router-link - v-if="latestVersion != null" - class="alert-link" - :to="{ - name: 'workflow-version', - params: { - versionId: latestVersion.git_commit_hash, - }, - query: { tab: route.query.tab }, - }" - >Try the latest version {{ latestVersion?.version }}.</router-link - > - </div> - <div class="row align-items-center"> - <a - role="button" - class="btn btn-success btn-lg w-fit mx-auto" - :class="{ disabled: !versionLaunchable }" - href="#" - > - <font-awesome-icon icon="fa-solid fa-rocket" class="me-2" /> - <span class="align-middle">Launch {{ activeVersionString }}</span> - </a> + <template v-if="route.name !== 'workflow-start'"> <div - v-if="latestVersion != null" - class="input-group w-fit position-absolute end-0" + v-if="!versionLaunchable" + class="alert alert-warning w-fit mx-auto" + role="alert" > - <span class="input-group-text px-2" id="workflow-version-wrapping" - ><font-awesome-icon icon="fa-solid fa-tags" class="text-secondary" - /></span> - <select - class="form-select form-select-sm" - aria-label="Workflow version selection" - aria-describedby="workflow-version-wrapping" - v-model="workflowState.activeVersionId" + This version can not be used. + <router-link + v-if="latestVersion != null" + class="alert-link" + :to="{ + name: 'workflow-version', + params: { + versionId: latestVersion.git_commit_hash, + }, + query: { tab: route.query.tab }, + }" + >Try the latest version {{ latestVersion?.version }}.</router-link + > + </div> + <div class="row align-items-center"> + <router-link + role="button" + class="btn btn-success btn-lg w-fit mx-auto" + :class="{ disabled: !versionLaunchable }" + :to="{ + name: 'workflow-start', + params: { + versionId: props.versionId, + workflowId: props.workflowId, + }, + }" + > + <font-awesome-icon icon="fa-solid fa-rocket" class="me-2" /> + <span class="align-middle">Launch {{ activeVersionString }}</span> + </router-link> + <div + v-if="latestVersion != null" + class="input-group w-fit position-absolute end-0" > - <option - v-for="version in sortedVersions(workflowState.workflow?.versions)" - :key="version.git_commit_hash" - :value="version.git_commit_hash" + <span class="input-group-text px-2" id="workflow-version-wrapping" + ><font-awesome-icon icon="fa-solid fa-tags" class="text-secondary" + /></span> + <select + class="form-select form-select-sm" + aria-label="Workflow version selection" + aria-describedby="workflow-version-wrapping" + v-model="workflowState.activeVersionId" > - {{ version.version }} - </option> - </select> + <option + v-for="version in sortedVersions( + workflowState.workflow?.versions + )" + :key="version.git_commit_hash" + :value="version.git_commit_hash" + > + {{ version.version }} + </option> + </select> + </div> </div> - </div> - <div class="row w-100 mb-4 mt-2 mx-0"> - <a - :href="workflowState.workflow.repository_url" - target="_blank" - class="text-secondary text-decoration-none mx-auto w-fit p-0" - > - <font-awesome-icon :icon="gitIcon" class="me-1" /> - <span class="align-middle"> - {{ workflowState.workflow.repository_url }}</span - ></a - > - </div> + <div class="row w-100 mb-4 mt-2 mx-0"> + <a + :href="workflowState.workflow.repository_url" + target="_blank" + class="text-secondary text-decoration-none mx-auto w-fit p-0" + > + <font-awesome-icon :icon="gitIcon" class="me-1" /> + <span class="align-middle"> + {{ workflowState.workflow.repository_url }}</span + ></a + > + </div> + </template> </div> <router-view v-if="workflowState.loading || workflowState.workflow != null" /> <div v-else class="text-center fs-1 mt-5"> @@ -235,10 +252,6 @@ onMounted(() => { </template> <style scoped> -.fs-0 { - font-size: 4em; -} - .icon:hover { opacity: 0.8; }