<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>