Skip to content
Snippets Groups Projects
Verified Commit 81aa8cd6 authored by Daniel Göbel's avatar Daniel Göbel
Browse files

Add function to request a resource synchronization

parent 7edac797
No related branches found
No related tags found
1 merge request!97Resolve "Adapt UI to new Resoruce creation flow"
......@@ -2,10 +2,13 @@
import BootstrapModal from "@/components/modals/BootstrapModal.vue";
import { reactive, ref } from "vue";
type reasonfor = "request" | "rejection";
const props = defineProps<{
modalId: string;
modalLabel: string;
loading: boolean;
purpose: reasonfor;
}>();
const formState = reactive<{
......@@ -48,7 +51,7 @@ function sendSaveEvent() {
:class="{ 'was-validated': formState.validated }"
>
<label :for="'reason-modal-input-' + randomIDSuffix" class="form-label"
>Reason for rejection</label
>Reason for {{ props.purpose }}</label
>
<textarea
class="form-control"
......@@ -56,7 +59,7 @@ function sendSaveEvent() {
rows="3"
minlength="16"
maxlength="512"
placeholder="State your reason for rejection"
:placeholder="'State your reason for the ' + props.purpose"
v-model="formState.reason"
></textarea>
</form>
......
......@@ -437,7 +437,7 @@ onMounted(() => {
:checked="props.viewMode === 'simple'"
@click="
router.replace({
query: { viewMode: 'simple' },
query: { ...route.query, viewMode: 'simple' },
hash: route.hash,
})
"
......@@ -454,7 +454,7 @@ onMounted(() => {
:checked="props.viewMode === 'advanced'"
@click="
router.replace({
query: { viewMode: 'advanced' },
query: { ...route.query, viewMode: 'advanced' },
hash: route.hash,
})
"
......@@ -471,7 +471,7 @@ onMounted(() => {
:checked="props.viewMode === 'expert'"
@click="
router.replace({
query: { viewMode: 'expert' },
query: { ...route.query, viewMode: 'expert' },
hash: route.hash,
})
"
......
......@@ -26,7 +26,7 @@ const props = defineProps<{
let refreshTimeout: NodeJS.Timeout | undefined = undefined;
const stateToUIMapping: Record<Status, string> = {
APPROVED: "",
APPROVED: "Resource approved",
WAIT_FOR_REVIEW: "Wait for review",
CLUSTER_DELETE_ERROR: "Error deleting resource on cluster",
CLUSTER_DELETING: "Resource deletion on cluster in progress",
......@@ -39,13 +39,14 @@ const stateToUIMapping: Record<Status, string> = {
S3_DELETED: "Tarball deleted in S3",
SYNCHRONIZED: "Resource available",
SYNCHRONIZING: "Synchronizing to cluster in progress",
SYNC_REQUESTED: "Wait for download on cluster",
SYNC_REQUESTED: "Synchronization to cluster requested",
LATEST: "Resource available (latest)",
};
const emit = defineEmits<{
(e: "click-info", resourceVersion: ResourceVersionOut): void;
(e: "click-update", resource: ResourceOut): void;
(e: "click-request-sync", resourceVersion: ResourceVersionOut): void;
}>();
const resourceVersionS3Ready = ref<Record<string, boolean>>({});
......@@ -82,6 +83,10 @@ function requestReview(resourceVersion: ResourceVersionOut) {
onMounted(() => {
if (!props.loading) {
new Tooltip("#resource-name-" + props.resource.resource_id);
if (props.resource.private) {
new Tooltip("#resource-private-icon-" + props.resource.resource_id);
}
for (const r of props.resource.versions) {
if (r.status == Status.RESOURCE_REQUESTED) {
checkS3Resource(r);
......@@ -110,8 +115,21 @@ onMounted(() => {
<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 v-else class="d-inline-flex align-items-center text-truncate">
<span
:id="'resource-name-' + props.resource.resource_id"
data-bs-toggle="tooltip"
:data-bs-title="props.resource.name"
>{{ props.resource.name }}</span
>
<font-awesome-icon
v-if="props.resource.private"
:id="'resource-private-icon-' + props.resource.resource_id"
icon="fa-solid fa-lock"
class="fs-5 ms-2 tooltip-private-repository"
data-bs-toggle="tooltip"
data-bs-title="Private resource"
/>
</div>
<button
v-if="props.extended"
......@@ -180,13 +198,27 @@ onMounted(() => {
}"
:data-bs-parent="'#accordion-' + props.resource.resource_id"
>
<div class="accordion-body">
<div class="accordion-body d-flex flex-column">
<div>
Registered at:
{{
dayjs.unix(resourceVersion.created_at).format("DD MMM YYYY")
}}
</div>
<div
v-if="resourceVersion.status == Status.APPROVED"
class="d-grid gap-2"
>
<button
type="button"
class="btn btn-primary"
@click="emit('click-request-sync', resourceVersion)"
data-bs-toggle="modal"
data-bs-target="#request-synchronization-modal"
>
Request synchronization
</button>
</div>
<div
v-if="
props.extended &&
......@@ -231,7 +263,6 @@ onMounted(() => {
resourceVersion.status === Status.SYNCHRONIZED ||
resourceVersion.status === Status.LATEST
"
class="my-1"
>
<label
:for="
......@@ -241,7 +272,7 @@ onMounted(() => {
class="form-label"
>Nextflow Access Path:</label
>
<div class="input-group fs-4 mb-3">
<div class="input-group fs-4">
<div
class="input-group-text hover-info"
:id="
......@@ -273,9 +304,8 @@ onMounted(() => {
<div
v-if="
props.extended &&
resourceVersion.status !== Status.S3_DELETED
resourceVersion.status === Status.RESOURCE_REQUESTED
"
class="my-1"
>
<label
:for="
......@@ -339,4 +369,8 @@ onMounted(() => {
transform: translate(0, -5px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
.accordion-body > div:not(:last-child) {
margin-bottom: 0.5rem !important;
}
</style>
......@@ -114,16 +114,16 @@ onMounted(() => {
<div v-if="props.loading" class="placeholder-glow w-100">
<span class="placeholder col-6"></span>
</div>
<div v-else class="text-truncate">
<div v-else class="text-truncate d-inline-flex align-items-center">
<span>{{ props.workflow.name }}</span>
<font-awesome-icon
v-if="props.workflow.private"
icon="fa-solid fa-lock"
class="fs-5 me-2 tooltip-private-repository"
class="fs-5 ms-2 tooltip-private-repository"
:id="'tooltip-' + randomIDSuffix"
data-bs-toggle="tooltip"
data-bs-title="Private Git Repository"
data-bs-title="Private Git repository"
/>
<span>{{ props.workflow.name }}</span>
</div>
<div class="btn-group">
<button
......
......@@ -202,32 +202,37 @@ export const useResourceStore = defineStore({
resourceVersion.resource_id,
resourceVersion.resource_version_id,
request,
).then((changedResourceVersion) => {
if (
this.ownResourceMapping[changedResourceVersion.resource_id] ==
undefined
) {
this.fetchResource(resourceVersion.resource_id);
)
.then((changedResourceVersion) => {
const versionIndex = this.resourceMapping[
changedResourceVersion.resource_id
]?.versions?.findIndex(
(version) =>
version.resource_version_id ==
changedResourceVersion.resource_version_id,
);
if (versionIndex != undefined && versionIndex > -1) {
this.resourceMapping[changedResourceVersion.resource_id].versions[
versionIndex
] = changedResourceVersion;
}
return changedResourceVersion;
}
const versionIndex = this.resourceMapping[
changedResourceVersion.resource_id
].versions.findIndex(
(version) =>
version.resource_version_id ==
changedResourceVersion.resource_version_id,
);
if (versionIndex > -1) {
this.resourceMapping[changedResourceVersion.resource_id].versions[
versionIndex
] = changedResourceVersion;
} else {
this.resourceMapping[
})
.then((changedResourceVersion) => {
const versionIndex = this.ownResourceMapping[
changedResourceVersion.resource_id
].versions.push(changedResourceVersion);
}
return changedResourceVersion;
});
]?.versions?.findIndex(
(version) =>
version.resource_version_id ==
changedResourceVersion.resource_version_id,
);
if (versionIndex != undefined && versionIndex > -1) {
this.ownResourceMapping[
changedResourceVersion.resource_id
].versions[versionIndex] = changedResourceVersion;
}
return changedResourceVersion;
});
},
requestReview(
resourceVersion: ResourceVersionOut,
......
......@@ -152,6 +152,7 @@ onMounted(() => {
modal-id="sync-request-reject-modal"
modal-label="Resource Synchronization Request Reject Modal"
:loading="resourceState.sendingRequest"
purpose="rejection"
@save="(reason) => rejectSyncRequest(reason, resourceState.rejectResource)"
>
<template #header>
......@@ -194,7 +195,7 @@ onMounted(() => {
</div>
<div v-else class="d-flex flex-column">
<div
class="border p-2 pb-0 rounded mb-2 d-flex"
class="border p-2 pb-0 rounded mb-2 d-flex hover-card"
v-for="request in resourceRepository.syncRequests"
:key="request.resource_version_id"
>
......@@ -263,4 +264,8 @@ onMounted(() => {
</div>
</template>
<style scoped></style>
<style scoped>
.hover-card:hover {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
</style>
......@@ -5,27 +5,34 @@ import ResourceCard from "@/components/resources/ResourceCard.vue";
import CardTransitionGroup from "@/components/transitions/CardTransitionGroup.vue";
import { useAuthStore } from "@/stores/users";
import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
import type { ResourceOut } from "@/client/resource";
import type { ResourceOut, ResourceVersionOut } from "@/client/resource";
import ReasonModal from "@/components/modals/ReasonModal.vue";
import { Modal, Toast } from "bootstrap";
import BootstrapToast from "@/components/BootstrapToast.vue";
const resourceRepository = useResourceStore();
const userRepository = useAuthStore();
let requestReasonModal: Modal | null = null;
let syncRequestSuccessToast: Toast | null = null;
const resourceState = reactive<{
loading: boolean;
filterString: string;
sortDesc: boolean;
showPrivate: boolean;
syncResourceVersion?: ResourceVersionOut;
}>({
loading: true,
filterString: "",
sortDesc: true,
showPrivate: false,
syncResourceVersion: undefined,
});
const publicResources = computed<ResourceOut[]>(() =>
resourceRepository.resources.filter((resource) => !resource.private),
);
const sortedResourcesByName = computed<ResourceOut[]>(() => {
return [...publicResources.value].sort((a, b) => (a.name < b.name ? 1 : -1));
return [...resourceRepository.resources].sort((a, b) =>
a.name < b.name ? 1 : -1,
);
});
const sortedResources = computed<ResourceOut[]>(() => {
......@@ -37,7 +44,7 @@ const sortedResources = computed<ResourceOut[]>(() => {
const filteredSortedResources = computed<ResourceOut[]>(() => {
return sortedResources.value
.filter((resource) => !resource.private)
.filter((resource) => !resource.private || resourceState.showPrivate)
.filter((resource) => {
return resourceState.filterString.length > 0
? resource.name.includes(resourceState.filterString)
......@@ -45,7 +52,25 @@ const filteredSortedResources = computed<ResourceOut[]>(() => {
});
});
function requestResourceSync(
reason: string,
resourceVersion?: ResourceVersionOut,
) {
if (resourceVersion != undefined) {
resourceRepository
.requestSynchronization(resourceVersion, {
reason: reason,
})
.then(() => {
requestReasonModal?.hide();
syncRequestSuccessToast?.show();
});
}
}
onMounted(() => {
requestReasonModal = new Modal("#request-synchronization-modal");
syncRequestSuccessToast = new Toast("#request-sync-toast");
resourceRepository
.fetchPublicResources(() => {
resourceState.loading = false;
......@@ -57,6 +82,20 @@ onMounted(() => {
</script>
<template>
<bootstrap-toast toast-id="request-sync-toast" color-class="success">
Requested resource synchronization
</bootstrap-toast>
<reason-modal
modal-id="request-synchronization-modal"
modal-label=""
:loading="false"
purpose="request"
@save="
(reason) => requestResourceSync(reason, resourceState.syncResourceVersion)
"
>
<template #header> Request resource synchronization</template>
</reason-modal>
<div class="row border-bottom mb-4">
<h2 class="mb-2">Available Resources</h2>
</div>
......@@ -79,6 +118,17 @@ onMounted(() => {
/>
</div>
</div>
<div class="form-check fs-5 ms-auto">
<label class="form-check-label" for="public-resources-checkbox">
Show only public resources
<input
class="form-check-input"
type="checkbox"
v-model="resourceState.showPrivate"
id="public-resources-checkbox"
/>
</label>
</div>
<font-awesome-icon
:icon="
resourceState.sortDesc
......@@ -86,18 +136,23 @@ onMounted(() => {
: 'fa-solid fa-arrow-up-wide-short'
"
@click="resourceState.sortDesc = !resourceState.sortDesc"
class="fs-5 cursor-pointer ms-auto"
class="fs-5 cursor-pointer ms-2"
/>
</div>
<div v-if="!resourceState.loading">
<div v-if="publicResources.length === 0" class="text-center fs-2 mt-5">
<div
v-if="resourceRepository.resources.length === 0"
class="text-center fs-2 mt-5"
>
<font-awesome-icon
icon="fa-solid fa-x"
class="my-5 fs-0"
style="color: var(--bs-secondary)"
/>
<p>
There are no public resources in the system. Please come again later.
There are no resources
<span v-if="resourceState.showPrivate">public</span> in the system.
Please come again later.
</p>
</div>
<div
......@@ -125,6 +180,9 @@ onMounted(() => {
:resource="resource"
:loading="false"
style="min-width: 47%; max-width: 48%"
@click-request-sync="
(version) => (resourceState.syncResourceVersion = version)
"
/>
</CardTransitionGroup>
</div>
......
......@@ -9,17 +9,24 @@ import { useS3KeyStore } from "@/stores/s3keys";
import type { ResourceVersionOut, ResourceOut } from "@/client/resource";
import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
import UpdateResourceModal from "@/components/resources/modals/UpdateResourceModal.vue";
import ReasonModal from "@/components/modals/ReasonModal.vue";
import BootstrapToast from "@/components/BootstrapToast.vue";
import { Modal, Toast } from "bootstrap";
const resourceRepository = useResourceStore();
const s3KeyRepository = useS3KeyStore();
let requestReasonModal: Modal | null = null;
let syncRequestSuccessToast: Toast | null = null;
const resourceState = reactive<{
loading: boolean;
resourceVersionInfo?: ResourceVersionOut;
updateResource: ResourceOut;
syncResourceVersion?: ResourceVersionOut;
}>({
loading: true,
resourceVersionInfo: undefined,
syncResourceVersion: undefined,
updateResource: {
name: "",
description: "",
......@@ -38,8 +45,30 @@ function setResourceUpdate(resource: ResourceOut) {
resourceState.updateResource = resource;
}
function setResourceSync(version: ResourceVersionOut) {
resourceState.syncResourceVersion = version;
}
function requestResourceSync(
reason: string,
resourceVersion?: ResourceVersionOut,
) {
if (resourceVersion != undefined) {
resourceRepository
.requestSynchronization(resourceVersion, {
reason: reason,
})
.then(() => {
requestReasonModal?.hide();
syncRequestSuccessToast?.show();
});
}
}
onMounted(() => {
let fetchedResources = false;
requestReasonModal = new Modal("#request-synchronization-modal");
syncRequestSuccessToast = new Toast("#request-sync-toast");
s3KeyRepository.fetchS3Keys(() => {
if (!fetchedResources) {
fetchedResources = true;
......@@ -52,6 +81,20 @@ onMounted(() => {
</script>
<template>
<bootstrap-toast toast-id="request-sync-toast" color-class="success">
Requested resource synchronization
</bootstrap-toast>
<reason-modal
modal-id="request-synchronization-modal"
modal-label=""
:loading="false"
purpose="request"
@save="
(reason) => requestResourceSync(reason, resourceState.syncResourceVersion)
"
>
<template #header> Request resource synchronization</template>
</reason-modal>
<create-resource-modal modal-id="createResourceModal" />
<upload-resource-info-modal
modal-id="uploadResourceInfoModal"
......@@ -109,6 +152,7 @@ onMounted(() => {
extended
@click-info="setResourceVersionInfo"
@click-update="setResourceUpdate"
@click-request-sync="setResourceSync"
/>
</CardTransitionGroup>
</div>
......
......@@ -110,6 +110,7 @@ onMounted(() => {
modal-id="review-reject-modal"
modal-label="Resource Review Reject Modal"
:loading="resourceState.sendingRequest"
purpose="rejection"
@save="(reason) => rejectReview(reason, resourceState.rejectResource)"
>
<template #header>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment