Browse Source

[7] Added frontend step restarting

Step restarting was added to the frontend.
at-robins 7 months ago
parent
commit
e1a34d3cd5

+ 94 - 5
frontend/src/components/experiment/ExperimentRunDetails.vue

@@ -2,7 +2,7 @@
   <div class="q-pa-md q-gutter-md">
     <q-card>
       <q-card-section>
-        <div class="text-h6">Experiment {{ id }}</div>
+        <div class="text-h6">Experiment: {{ experiment_name }}</div>
       </q-card-section>
       <div class="q-pa-md gutter-md no-wrap row" v-if="!loadingError">
         <div
@@ -77,9 +77,31 @@
         </div>
         <div v-else class="text-h6">{{ selectedStep.name }}</div>
       </q-card-section>
-      <div v-if="selectedStep !== null" class="q-gutter-md q-pa-md col">
+      <q-card-section>
+        <div v-if="selectedStep" class="q-pl-md">
+          <div v-html="selectedStep.description" />
+        </div>
+      </q-card-section>
+      <div v-if="selectedStep" class="q-gutter-md q-pa-md col">
         <q-btn label="Display logs" class="row" />
         <q-btn label="Download output" class="row" />
+
+        <q-btn
+          v-if="canBeStarted(selectedStep)"
+          class="row"
+          :icon="matRestartAlt"
+          label="Restart step"
+          :color="restartingError ? 'negative' : 'positive'"
+          :loading="isRestarting"
+          @click="restartStep(selectedStep)"
+        >
+          <q-tooltip>
+            <div v-if="restartingError">
+              <error-popup :error-response="restartingError" />
+            </div>
+            <div>Restarts the experiment execution step.</div>
+          </q-tooltip>
+        </q-btn>
       </div>
     </q-card>
     <q-dialog v-model="showPollingError" v-if="pollingError">
@@ -89,9 +111,9 @@
 </template>
 
 <script setup lang="ts">
-import { type ErrorResponse } from "@/scripts/types";
+import { type ErrorResponse, type ExperimentDetails } from "@/scripts/types";
 import axios from "axios";
-import { ref, onMounted, type Ref } from "vue";
+import { ref, onMounted, type Ref, computed } from "vue";
 import ErrorPopup from "@/components/ErrorPopup.vue";
 import { onBeforeRouteLeave, useRouter } from "vue-router";
 import {
@@ -105,16 +127,20 @@ import {
   symOutlinedNotStarted,
   symOutlinedStopCircle,
 } from "@quasar/extras/material-symbols-outlined";
+import { matRestartAlt } from "@quasar/extras/material-icons";
 
 // The intervall in which pipeline updates are requested from the server.
 const POLLING_INTERVALL_MILLISECONDS = 10000;
 
+const experiment: Ref<ExperimentDetails | null> = ref(null);
 const pipeline: Ref<PipelineBlueprint | null> = ref(null);
 const sortedSteps: Ref<PipelineStepBlueprint[][]> = ref([]);
 const isLoadingPipelineDetails = ref(false);
+const isRestarting = ref(false);
 const loadingError: Ref<ErrorResponse | null> = ref(null);
 const isPollingPipelineDetails = ref(false);
 const pollingError: Ref<ErrorResponse | null> = ref(null);
+const restartingError: Ref<ErrorResponse | null> = ref(null);
 const selectedStep: Ref<PipelineStepBlueprint | null> = ref(null);
 const showPollingError = ref(false);
 const router = useRouter();
@@ -125,6 +151,10 @@ const props = defineProps({
   id: { type: String, required: true },
 });
 
+const experiment_name = computed(() => {
+  return experiment.value ? experiment.value.name : props.id;
+});
+
 onMounted(() => {
   loadPipelineDetails();
 });
@@ -142,7 +172,11 @@ function loadPipelineDetails() {
   isLoadingPipelineDetails.value = true;
   loadingError.value = null;
   axios
-    .get("/api/experiments/" + props.id + "/run")
+    .get("/api/experiments/" + props.id)
+    .then((response) => {
+      experiment.value = response.data;
+      return axios.get("/api/experiments/" + props.id + "/run");
+    })
     .then((response) => {
       setPipelineDetails(response.data);
       pollingTimer.value = window.setTimeout(
@@ -263,6 +297,61 @@ function selectStep(step: PipelineStepBlueprint) {
     selectedStep.value = step;
   }
 }
+
+/**
+ * Returns ```true``` if the specified step can be (re-)started.
+ *
+ * @param step the step to check
+ */
+function canBeStarted(step: PipelineStepBlueprint | null): boolean {
+  if (!pipeline.value) {
+    return false;
+  }
+  if (!step) {
+    return false;
+  }
+  const satisfied_dependencies = pipeline.value.steps
+    .filter((s) => s.status === PipelineStepStatus.Finished)
+    .map((s) => s.id);
+  const isDependecySatisfied = step.dependencies.every((dependency) =>
+    satisfied_dependencies.includes(dependency)
+  );
+  return (
+    step.status !== PipelineStepStatus.Running &&
+    step.status !== PipelineStepStatus.Waiting &&
+    isDependecySatisfied
+  );
+}
+
+/**
+ * Tries to restart the specified step.
+ * 
+ * @param step the step to restart
+ */
+function restartStep(step: PipelineStepBlueprint | null) {
+  if (step && !isRestarting.value) {
+    isRestarting.value = true;
+    restartingError.value = null;
+    const config = {
+      headers: {
+        "content-type": "application/json",
+      },
+    };
+    axios
+      .post(
+        "/api/experiments/" + props.id + "/rerun",
+        JSON.stringify(step.id),
+        config
+      )
+      .then(() => (step.status = PipelineStepStatus.Waiting))
+      .catch((error) => {
+        restartingError.value = error.response.data;
+      })
+      .finally(() => {
+        isRestarting.value = false;
+      });
+  }
+}
 </script>
 <style scoped lang="scss">
 .chip-unselected {

+ 2 - 2
frontend/src/router/index.ts

@@ -4,7 +4,7 @@ import GlobalDataView from "@/views/GlobalDataView.vue";
 import GlobalDataDetailsView from "@/views/GlobalDataDetailsView.vue";
 import ExperimentView from "@/views/ExperimentView.vue";
 import ExperimentDetailsView from "@/views/ExperimentDetailsView.vue";
-import ExperimentRunDetails from "@/components/experiment/ExperimentRunDetails.vue";
+import ExperimentRunDetailsView from "@/views/ExperimentRunDetailsView.vue";
 
 const router = createRouter({
   history: createWebHistory(import.meta.env.BASE_URL),
@@ -43,7 +43,7 @@ const router = createRouter({
     {
       path: "/ui/experiments/:id/run",
       name: "experiments_run_detail",
-      component: ExperimentRunDetails,
+      component: ExperimentRunDetailsView,
       props: true,
     },
   ],

+ 15 - 0
frontend/src/views/ExperimentRunDetailsView.vue

@@ -0,0 +1,15 @@
+<script setup lang="ts">
+import ExperimentRunDetails from "@/components/experiment/ExperimentRunDetails.vue";
+defineProps({
+  id: {
+    type: String,
+    required: true,
+  },
+});
+</script>
+
+<template>
+  <main>
+    <experiment-run-details :id="id" />
+  </main>
+</template>