Skip to content
Snippets Groups Projects

Resolve "Add UI for viewing and managing Resources"

Merged Daniel Göbel requested to merge feature/88-add-ui-for-viewing-and-managing-resources into main
6 files
+ 774
740
Compare changes
  • Side-by-side
  • Inline
Files
6
+ 337
0
<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>
Loading