Skip to content
Snippets Groups Projects
ResourceCard.vue 11.49 KiB
<script setup lang="ts">
import {
  type ResourceOut,
  type ResourceVersionOut,
  Status,
} from "@/client/resource";
import { computed, onMounted, ref } from "vue";
import dayjs from "dayjs";
import CopyToClipboardIcon from "@/components/CopyToClipboardIcon.vue";
import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
import { useS3ObjectStore } from "@/stores/s3objects";
import { useResourceStore } from "@/stores/resources";
import { Tooltip } from "bootstrap";
import { useNameStore } from "@/stores/names";

const randomIDSuffix: string = Math.random().toString(16).substring(2, 8);
const objectRepository = useS3ObjectStore();
const resourceRepository = useResourceStore();
const nameRepository = useNameStore();

const props = defineProps<{
  resource: ResourceOut;
  loading: boolean;
  extended?: boolean;
}>();
let refreshTimeout: NodeJS.Timeout | undefined = undefined;

const stateToUIMapping: Record<Status, string> = {
  CLUSTER_DELETED: "Deleted on Cluster",
  DENIED: "Rejected",
  RESOURCE_REQUESTED: "Resource created",
  S3_DELETED: "Deleted in S3",
  SYNCHRONIZED: "Available",
  SYNCHRONIZING: "Synchronizing to Cluster",
  SYNC_REQUESTED: "Wait for Approval",
  LATEST: "Available (Latest)",
};

const emit = defineEmits<{
  (e: "click-info", resourceVersion: ResourceVersionOut): void;
  (e: "click-update", resource: ResourceOut): void;
}>();

const resourceVersionS3Ready = ref<Record<string, boolean>>({});

const resourceVersions = computed<ResourceVersionOut[]>(() =>
  [...props.resource.versions].sort((a, b) =>
    a.created_at < b.created_at ? 1 : -1,
  ),
);

function checkS3Resource(resourceVersion: ResourceVersionOut) {
  const bucket = resourceVersion.s3_path.slice(5).split("/")[0];
  const key = resourceVersion.s3_path.split(bucket)[1].slice(1);
  objectRepository
    .fetchS3ObjectMeta(bucket, key)
    .then(() => {
      resourceVersionS3Ready.value[resourceVersion.resource_version_id] = true;
    })
    .catch(() => {
      resourceVersionS3Ready.value[resourceVersion.resource_version_id] = false;
    });
}

function clickCheckS3Resource(resourceVersion: ResourceVersionOut) {
  clearTimeout(refreshTimeout);
  refreshTimeout = setTimeout(() => {
    checkS3Resource(resourceVersion);
  }, 500);
}

function requestSynchronization(resourceVersion: ResourceVersionOut) {
  resourceRepository.requestSynchronization(resourceVersion);
}

onMounted(() => {
  if (!props.loading) {
    for (const r of props.resource.versions) {
      if (r.status == Status.RESOURCE_REQUESTED) {
        checkS3Resource(r);
      }
    }
    [
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      ...(document
        .querySelector("#resource-card-" + randomIDSuffix)
        ?.querySelectorAll("[data-bs-toggle='tooltip']") ?? []),
    ].map((el) => new Tooltip(el));
  }
});
</script>

<template>
  <div
    :id="'resource-card-' + randomIDSuffix"
    class="card-hover border border-secondary card m-2"
  >
    <div class="card-body">
      <div
        class="card-title fs-3 d-flex justify-content-between align-items-center"
      >
        <div v-if="props.loading" class="placeholder-glow w-100">
          <span class="placeholder col-6"></span>
        </div>
        <div v-else>
          <span>{{ props.resource.name }}</span>
        </div>
        <button
          v-if="props.extended"
          :disabled="props.loading"
          class="btn btn-primary"
          type="button"
          data-bs-toggle="modal"
          data-bs-target="#updateResourceModal"
          @click="emit('click-update', props.resource)"
        >
          Update
        </button>
      </div>
      <p class="card-text">
        <span v-if="props.loading" class="placeholder-glow"
          ><span class="placeholder col-12"></span
        ></span>
        <span v-else>{{ props.resource.description }}</span>
        <br />
        Source:
        <span v-if="props.loading" class="placeholder-glow"
          ><span class="placeholder col-2"></span
        ></span>
        <span v-else>{{ props.resource.source }}</span>
      </p>
      <div v-if="!props.loading">
        <div class="accordion" :id="'accordion-' + props.resource.resource_id">
          <div
            v-for="resourceVersion of resourceVersions"
            :key="resourceVersion.resource_version_id"
            class="accordion-item"
          >
            <h2 class="accordion-header">
              <button
                class="accordion-button"
                :class="{
                  collapsed:
                    resourceVersion.status != Status.LATEST || props.extended,
                }"
                type="button"
                data-bs-toggle="collapse"
                :data-bs-target="
                  '#collapseResourceVersion-' +
                  resourceVersion.resource_version_id
                "
                :aria-expanded="
                  resourceVersion.status == Status.LATEST && !props.extended
                "
                :aria-controls="
                  '#collapseResourceVersion-' +
                  resourceVersion.resource_version_id
                "
              >
                {{ resourceVersion.release }} -
                {{ stateToUIMapping[resourceVersion.status] }}
              </button>
            </h2>
            <div
              :id="
                'collapseResourceVersion-' + resourceVersion.resource_version_id
              "
              class="accordion-collapse collapse"
              :class="{
                show:
                  resourceVersion.status == Status.LATEST && !props.extended,
              }"
              :data-bs-parent="'#accordion-' + props.resource.resource_id"
            >
              <div class="accordion-body">
                <div>
                  Registered at:
                  {{
                    dayjs.unix(resourceVersion.created_at).format("DD MMM YYYY")
                  }}
                </div>
                <div
                  v-if="
                    props.extended &&
                    (resourceVersion.status == Status.RESOURCE_REQUESTED ||
                      resourceVersion.status == Status.CLUSTER_DELETED)
                  "
                >
                  <div class="btn-group" role="group">
                    <button
                      type="button"
                      class="btn btn-primary"
                      data-bs-toggle="modal"
                      data-bs-target="#uploadResourceInfoModal"
                      @click="emit('click-info', resourceVersion)"
                    >
                      <font-awesome-icon icon="fa-solid fa-circle-question" />
                    </button>
                    <button
                      type="button"
                      class="btn btn-primary"
                      :disabled="
                        !resourceVersionS3Ready[
                          resourceVersion.resource_version_id
                        ]
                      "
                      @click="requestSynchronization(resourceVersion)"
                    >
                      Request Synchronization
                    </button>
                    <button
                      v-if="resourceVersion.status == Status.RESOURCE_REQUESTED"
                      type="button"
                      class="btn btn-primary"
                      @click="clickCheckS3Resource(resourceVersion)"
                    >
                      <font-awesome-icon
                        icon="fa-solid fa-arrow-rotate-right"
                      />
                    </button>
                  </div>
                </div>
                <div
                  v-if="
                    resourceVersion.status === Status.SYNCHRONIZED ||
                    resourceVersion.status === Status.LATEST
                  "
                  class="my-1"
                >
                  <label
                    :for="
                      'nextflow-access-path-' +
                      resourceVersion.resource_version_id
                    "
                    class="form-label"
                    >Nextflow Access Path:</label
                  >
                  <div class="input-group fs-4 mb-3">
                    <div
                      class="input-group-text hover-info"
                      :id="
                        'tooltip-cluster-path-' +
                        resourceVersion.resource_version_id
                      "
                      data-bs-toggle="tooltip"
                      data-bs-title="Path on the cluster where a workflow can access the resource"
                    >
                      <font-awesome-icon icon="fa-solid fa-circle-question" />
                    </div>
                    <input
                      :id="
                        'nextflow-access-path-' +
                        resourceVersion.resource_version_id
                      "
                      class="form-control"
                      type="text"
                      :value="resourceVersion.cluster_path"
                      aria-label="Nextflow Access Path"
                      readonly
                    />
                    <span class="input-group-text"
                      ><copy-to-clipboard-icon
                        :text="resourceVersion.cluster_path ?? ''"
                    /></span>
                  </div>
                </div>
                <div
                  v-if="
                    props.extended &&
                    resourceVersion.status !== Status.S3_DELETED
                  "
                  class="my-1"
                >
                  <label
                    :for="
                      's3-access-path-' + resourceVersion.resource_version_id
                    "
                    class="form-label"
                    >S3 Upload Path:</label
                  >
                  <div class="input-group fs-4 mb-3">
                    <div
                      class="input-group-text hover-info"
                      :id="
                        'tooltip-s3-path-' + resourceVersion.resource_version_id
                      "
                      data-bs-toggle="tooltip"
                      data-bs-title="S3 Path where the resource should be uploaded"
                    >
                      <font-awesome-icon icon="fa-solid fa-circle-question" />
                    </div>
                    <input
                      :id="
                        's3-access-path-' + resourceVersion.resource_version_id
                      "
                      class="form-control"
                      type="text"
                      :value="resourceVersion.s3_path"
                      aria-label="S3 Access Path"
                      readonly
                    />
                    <span class="input-group-text"
                      ><copy-to-clipboard-icon :text="resourceVersion.s3_path"
                    /></span>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
      <div class="mt-2">
        Maintainer:
        <span
          v-if="
            props.loading || !nameRepository.getName(resource.maintainer_id)
          "
          class="placeholder-glow"
          ><span class="placeholder col-2"></span
        ></span>
        <span v-else>{{ nameRepository.getName(resource.maintainer_id) }}</span>
      </div>
    </div>
  </div>
</template>

<style scoped>
.card-hover {
  transition: transform 0.3s ease-out;
}

.card-hover:hover {
  transform: translate(0, -5px);
  box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
</style>