Skip to content
Snippets Groups Projects
ParameterModal.vue 11.71 KiB
<script setup lang="ts">
import BootstrapModal from "@/components/modals/BootstrapModal.vue";
import { computed, onMounted, reactive, watch } from "vue";
import { useWorkflowExecutionStore } from "@/stores/workflowExecutions";
import type { RouteLocationRaw, RouteParamsRaw } from "vue-router";
import { Modal } from "bootstrap";
import { useRouter } from "vue-router";
import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
import type { WorkflowExecutionOut, WorkflowVersion } from "@/client/workflow";
import { useNameStore } from "@/stores/names";
import { useWorkflowStore } from "@/stores/workflows";
import { createDownloadUrl } from "@/utils/DownloadJson";
import type { WorkflowParameters } from "@/types/WorkflowParameters";

const nameRepository = useNameStore();
const executionRepository = useWorkflowExecutionStore();
const workflowRepository = useWorkflowStore();
const router = useRouter();

let parameterModal: Modal | null = null;

const props = defineProps<{
  modalID: string;
  executionId?: string;
}>();

const parameterState = reactive<{
  loading: boolean;
  error: boolean;
}>({
  loading: false,
  error: false,
});

const execution = computed<WorkflowExecutionOut | undefined>(() => {
  if (props.executionId == undefined) {
    return undefined;
  }
  return executionRepository.executionMapping[props.executionId] ?? undefined;
});

const workflowVersion = computed<WorkflowVersion | undefined>(
  () =>
    workflowRepository.versionMapping[
      execution.value?.workflow_version_id ?? ""
    ],
);

watch(execution, (newVal, oldVal) => {
  if (
    newVal != undefined &&
    newVal.execution_id != oldVal?.execution_id &&
    newVal.workflow_id != undefined &&
    workflowRepository.workflowMapping[newVal.workflow_id] == undefined
  ) {
    workflowRepository.fetchWorkflow(newVal.workflow_id);
  }
});

const logs_s3_path = computed<string | undefined>(
  () => execution.value?.logs_s3_path ?? undefined,
);
const debug_s3_path = computed<string | undefined>(
  () => execution.value?.debug_s3_path ?? undefined,
);
const provenance_s3_path = computed<string | undefined>(
  () => execution.value?.provenance_s3_path ?? undefined,
);
const notes = computed<string | undefined>(
  () => execution.value?.notes ?? undefined,
);

const parameters = computed<WorkflowParameters | undefined>(() => {
  if (props.executionId == undefined) {
    return undefined;
  }
  return executionRepository.parameters[props.executionId] ?? undefined;
});

const parameterDownloadUrl = computed<string | undefined>(() => {
  if (parameters.value == undefined) {
    return undefined;
  }
  return createDownloadUrl(
    JSON.stringify(parameters.value, null, 2),
    "application/json",
  );
});

const workflowName = computed<string>(() => {
  if (props.executionId) {
    const execution = executionRepository.executionMapping[props.executionId];
    if (
      execution?.workflow_id != undefined &&
      execution?.workflow_version_id != undefined
    ) {
      return (
        nameRepository.getName(execution.workflow_id) +
        "@" +
        nameRepository.getName(execution.workflow_version_id)
      );
    }
  }
  return "";
});

function fetchWorkflowExecutionParameters(executionId?: string) {
  parameterState.error = false;
  if (executionId != undefined) {
    parameterState.loading = true;
    executionRepository
      .fetchExecutionParameters(executionId)
      .catch(() => {
        parameterState.error = true;
      })
      .finally(() => {
        parameterState.loading = false;
      });
  }
}

function handleLinkClick(route: RouteLocationRaw) {
  parameterModal?.hide();
  router.push(route);
}

function saveParameters() {
  if (parameters.value != undefined) {
    executionRepository.pushTemporaryParameters(parameters.value, {
      logs_s3_path: logs_s3_path?.value?.split("/")?.slice(0, -1)?.join("/"),
      debug_s3_path: debug_s3_path?.value?.split("/")?.slice(0, -1)?.join("/"),
      provenance_s3_path: provenance_s3_path?.value
        ?.split("/")
        ?.slice(0, -1)
        ?.join("/"),
      notes: notes.value,
    });
  }
}

function getS3LinkParameters(s3String: string): RouteParamsRaw {
  const pathComponents = s3String.slice(5).split("/");
  const s3File =
    pathComponents.length > 1 &&
    pathComponents[pathComponents.length - 1].includes(".");
  return {
    bucketName: pathComponents[0],
    subFolders: pathComponents.slice(1, s3File ? -1 : undefined),
  };
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isBucketLink(value: any): boolean {
  if (typeof value === "string") {
    return value.startsWith("s3://");
  }
  return false;
}

watch(
  () => props.executionId,
  (newId, oldId) => {
    if (newId != oldId) {
      fetchWorkflowExecutionParameters(newId);
    }
  },
);

onMounted(() => {
  fetchWorkflowExecutionParameters(props.executionId);
  parameterModal = Modal.getOrCreateInstance("#" + props.modalID);
});
</script>

<template>
  <bootstrap-modal
    :modalId="modalID"
    :static-backdrop="false"
    modal-label="Workflow Execution Parameters Modal"
    size-modifier="lg"
  >
    <template v-slot:header
      >Workflow Execution Parameters
      <b>
        {{ workflowName }}
      </b>
    </template>
    <template v-slot:body>
      <div v-if="parameterState.error" class="text-center fs-4 mt-5">
        <font-awesome-icon
          icon="fa-solid fa-magnifying-glass"
          class="mb-3 fs-0"
          style="color: var(--bs-secondary)"
        />
        <p>
          Workflow Execution <i>{{ props.executionId }}</i> not found
        </p>
      </div>
      <table v-else-if="props.executionId" class="table table-hover">
        <caption class="placeholder-glow">
          <span v-if="parameterState.loading" class="placeholder col-1"></span>
          <template v-else>
            {{
              Object.keys(executionRepository.parameters[props.executionId])
                .length
            }}
          </template>
          Parameters
        </caption>
        <tbody>
          <template v-if="parameterState.loading">
            <tr v-for="n in 6" :key="n">
              <th scope="row" style="width: 20%" class="placeholder-glow">
                <div class="placeholder col-12"></div>
              </th>
              <td class="placeholder-glow">
                <div class="placeholder col-8"></div>
              </td>
            </tr>
          </template>
          <template v-else>
            <tr
              v-if="logs_s3_path"
              :class="{
                'border-bottom-thick': !debug_s3_path && !provenance_s3_path,
              }"
            >
              <th scope="row" style="width: 10%" class="text-end">
                <b>logs</b>
              </th>
              <td>
                <router-link
                  :to="{
                    name: 'bucket',
                    params: getS3LinkParameters(logs_s3_path),
                  }"
                  @click.prevent="
                    handleLinkClick({
                      name: 'bucket',
                      params: getS3LinkParameters(logs_s3_path),
                    })
                  "
                  >{{ logs_s3_path }}
                </router-link>
              </td>
            </tr>
            <tr
              v-if="provenance_s3_path"
              :class="{
                'border-bottom-thick': !debug_s3_path,
              }"
            >
              <th scope="row" style="width: 10%" class="text-end">
                <b>provenance</b>
              </th>
              <td>
                <router-link
                  :to="{
                    name: 'bucket',
                    params: getS3LinkParameters(provenance_s3_path),
                  }"
                  @click.prevent="
                    handleLinkClick({
                      name: 'bucket',
                      params: getS3LinkParameters(provenance_s3_path),
                    })
                  "
                  >{{ provenance_s3_path }}
                </router-link>
              </td>
            </tr>
            <tr v-if="debug_s3_path" class="border-bottom-thick">
              <th scope="row" style="width: 10%" class="text-end">
                <b>debug</b>
              </th>
              <td>
                <router-link
                  :to="{
                    name: 'bucket',
                    params: getS3LinkParameters(debug_s3_path),
                  }"
                  @click.prevent="
                    handleLinkClick({
                      name: 'bucket',
                      params: getS3LinkParameters(debug_s3_path),
                    })
                  "
                  >{{ debug_s3_path }}
                </router-link>
              </td>
            </tr>
            <tr v-for="(value, name) in parameters" :key="name">
              <th scope="row" style="width: 10%" class="text-end">
                <b>{{ name }}</b>
              </th>
              <td>
                <router-link
                  v-if="typeof value === 'string' && isBucketLink(value)"
                  :to="{
                    name: 'bucket',
                    params: getS3LinkParameters(value),
                  }"
                  @click.prevent="
                    handleLinkClick({
                      name: 'bucket',
                      params: getS3LinkParameters(value),
                    })
                  "
                  >{{ value }}
                </router-link>
                <a
                  v-else-if="
                    typeof value === 'string' && value.startsWith('http')
                  "
                  :href="value"
                  target="_blank"
                  >{{ value }}
                  <font-awesome-icon
                    icon="fa-solid fa-arrow-up-right-from-square"
                    class="ms-1"
                  />
                </a>
                <template v-else>{{ value }}</template>
              </td>
            </tr>
          </template>
        </tbody>
      </table>
    </template>
    <template v-slot:footer>
      <router-link
        v-if="workflowVersion"
        class="btn btn-primary"
        :class="{
          disabled: parameterState.loading || parameterState.error,
        }"
        @click.prevent="
          saveParameters();
          handleLinkClick({
            name: 'workflow-start',
            params: {
              versionId: workflowVersion.workflow_version_id,
              workflowId: workflowVersion.workflow_id,
            },
            query: {
              workflowModeId: execution?.mode_id,
            },
          });
        "
        :to="{
          name: 'workflow-start',
          params: {
            versionId: workflowVersion.workflow_version_id,
            workflowId: workflowVersion.workflow_id,
          },
          query: {
            workflowModeId: execution?.mode_id,
          },
        }"
      >
        Rerun Execution
        <font-awesome-icon icon="fa-solid fa-arrow-rotate-right" class="me2" />
      </router-link>
      <a
        class="btn btn-primary"
        role="button"
        :class="{
          disabled: parameterState.loading || parameterState.error,
        }"
        :href="parameterDownloadUrl"
        :download="
          'parameters-' + props.executionId?.replace(/-/g, '') + '.json'
        "
        :tabindex="parameterState.loading || parameterState.error ? -1 : 0"
        :aria-disabled="parameterState.loading || parameterState.error"
      >
        Download Parameters
        <font-awesome-icon icon="fa-solid fa-download" class="ms-1" />
      </a>
      <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
        Close
      </button>
    </template>
  </bootstrap-modal>
</template>

<style scoped>
.border-bottom-thick {
  border-bottom: 3px var(--bs-border-style) var(--bs-border-color) !important;
}
</style>