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

Add view to accept and reject resource synchronization requests

parent bdc5f729
No related branches found
No related tags found
1 merge request!97Resolve "Adapt UI to new Resoruce creation flow"
Pipeline #47746 passed
......@@ -37,7 +37,7 @@ build:
publish-main-docker-container-job:
stage: deploy
image:
name: gcr.io/kaniko-project/executor:v1.20.0-debug
name: gcr.io/kaniko-project/executor:v1.21.0-debug
entrypoint: [""]
only:
refs:
......@@ -54,7 +54,7 @@ publish-main-docker-container-job:
publish-docker-container-job:
stage: deploy
image:
name: gcr.io/kaniko-project/executor:v1.20.0-debug
name: gcr.io/kaniko-project/executor:v1.21.0-debug
entrypoint: [""]
only:
- tags
......
......@@ -54,7 +54,6 @@ onBeforeMount(() => {
window._paq.push(["setCustomUrl", to.path]);
window._paq.push(["setDocumentTitle", to.name]);
if (store.currentUID.length > 0) {
console.log(store.currentUID);
window._paq.push(["setUserId", store.currentUID]);
}
window._paq.push(["trackPageView"]);
......
......@@ -80,7 +80,7 @@ export class ResourceService {
* @returns UserSynchronizationRequestOut Successful Response
* @throws ApiError
*/
public static resourceListReviewableResources(): CancelablePromise<Array<UserSynchronizationRequestOut>> {
public static resourceListSyncRequests(): CancelablePromise<Array<UserSynchronizationRequestOut>> {
return __request(OpenAPI, {
method: 'GET',
url: '/resources/sync_requests',
......
......@@ -216,8 +216,6 @@ watch(
>Users
</router-link>
</li>
<li><a class="dropdown-item disabled" href="#">Bucket</a></li>
<li><a class="dropdown-item disabled" href="#">Workflow</a></li>
<li>
<router-link
class="dropdown-item"
......@@ -225,6 +223,13 @@ watch(
>Resources
</router-link>
</li>
<li>
<router-link
class="dropdown-item"
:to="{ name: 'admin-sync-requests' }"
>Resources Synchronization
</router-link>
</li>
</ul>
</li>
</ul>
......
......@@ -33,7 +33,11 @@ function sendSaveEvent() {
</script>
<template>
<bootstrap-modal :modal-id="props.modalId" :modal-label="props.modalLabel">
<bootstrap-modal
:modal-id="props.modalId"
:modal-label="props.modalLabel"
size-modifier="lg"
>
<template #header>
<slot name="header" />
</template>
......
......@@ -19,4 +19,13 @@ export const adminRoutes: RouteRecordRaw[] = [
title: "Manage Users",
},
},
{
path: "admin/sync-requests",
name: "admin-sync-requests",
component: () => import("../views/admin/AdminSyncRequestsView.vue"),
meta: {
requiresAdminRole: true,
title: "Sync Requests",
},
},
];
......@@ -5,6 +5,9 @@ import { workflowRoutes } from "@/router/workflowRoutes";
import { s3Routes } from "@/router/s3Routes";
import { resourceRoutes } from "@/router/resourceRoutes";
import { adminRoutes } from "@/router/adminRoutes";
import ImprintView from "@/views/ImprintView.vue";
import PrivacyPolicyView from "@/views/PrivacyPolicyView.vue";
import TermsOfUsageView from "@/views/TermsOfUsageView.vue";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
......@@ -44,7 +47,7 @@ const router = createRouter({
meta: {
title: "Privacy Policy",
},
component: import("../views/PrivacyPolicyView.vue"),
component: PrivacyPolicyView,
},
{
path: "/terms",
......@@ -52,7 +55,7 @@ const router = createRouter({
meta: {
title: "Terms of Usage",
},
component: import("../views/TermsOfUsageView.vue"),
component: TermsOfUsageView,
},
{
path: "/imprint",
......@@ -60,7 +63,7 @@ const router = createRouter({
meta: {
title: "Imprint",
},
component: import("../views/ImprintView.vue"),
component: ImprintView,
},
{
path: "/:pathMatch(.*)",
......
......@@ -6,6 +6,7 @@ import type {
ResourceVersionOut,
UserRequestAnswer,
UserSynchronizationRequestIn,
UserSynchronizationRequestOut,
} from "@/client/resource";
import {
ResourceService,
......@@ -22,10 +23,14 @@ export const useResourceStore = defineStore({
resourceMapping: {},
ownResourceMapping: {},
reviewableResourceMapping: {},
syncRequestMapping: {},
__syncRequestsFetched: false,
}) as {
resourceMapping: Record<string, ResourceOut>;
ownResourceMapping: Record<string, ResourceOut>;
reviewableResourceMapping: Record<string, ResourceOut>;
syncRequestMapping: Record<string, UserSynchronizationRequestOut>;
__syncRequestsFetched: boolean;
},
getters: {
resources(): ResourceOut[] {
......@@ -37,6 +42,9 @@ export const useResourceStore = defineStore({
reviewableResources(): ResourceOut[] {
return Object.values(this.reviewableResourceMapping);
},
syncRequests(): UserSynchronizationRequestOut[] {
return Object.values(this.syncRequestMapping);
},
},
actions: {
fetchResource(
......@@ -58,6 +66,24 @@ export const useResourceStore = defineStore({
return resource;
});
},
fetchSyncRequests(
onFinally?: () => void,
): Promise<UserSynchronizationRequestOut[]> {
if (this.__syncRequestsFetched) {
onFinally?.();
}
return ResourceService.resourceListSyncRequests()
.then((requests) => {
this.__syncRequestsFetched = true;
const newMapping: Record<string, UserSynchronizationRequestOut> = {};
for (const request of requests) {
newMapping[request.resource_version_id] = request;
}
this.syncRequestMapping = newMapping;
return requests;
})
.finally(onFinally);
},
fetchResources(
maintainerId?: string,
versionStatus?: Status[],
......@@ -268,6 +294,19 @@ export const useResourceStore = defineStore({
requestAnswer,
).then(this._updateReviewableResourceVersion);
},
syncResource(
resourceVersion: ResourceVersionOut,
requestAnswer: UserRequestAnswer,
): Promise<ResourceVersionOut> {
return ResourceVersionService.resourceVersionResourceVersionSync(
resourceVersion.resource_id,
resourceVersion.resource_version_id,
requestAnswer,
).then((version) => {
delete this.syncRequestMapping[version.resource_version_id];
return version;
});
},
_updateReviewableResourceVersion(
version: ResourceVersionOut,
): ResourceVersionOut {
......
<script setup lang="ts"></script>
<template>
<h2>Impressum</h2>
<p>TBD</p>
<div>
<h2>Impressum</h2>
<p>TBD</p>
</div>
</template>
<style scoped></style>
<script setup lang="ts"></script>
<template>
<h2>Privacy Policy</h2>
<p>TBD</p>
<div>
<h2>Privacy Policy</h2>
<p>TBD</p>
</div>
</template>
<style scoped></style>
<script setup lang="ts"></script>
<template>
<h2>Terms of Usage</h2>
<p>TBD</p>
<div>
<h2>Terms of Usage</h2>
<p>TBD</p>
</div>
</template>
<style scoped></style>
......@@ -110,17 +110,7 @@ function deleteInS3(resourceVersion: ResourceVersionOut) {
function syncToCluster(resourceVersion: ResourceVersionOut) {
resourceState.loading = true;
resourceRepository
.reviewResource(resourceVersion, { deny: false })
.then(replaceResourceVersion)
.finally(() => {
resourceState.loading = false;
});
}
function denyResource(resourceVersion: ResourceVersionOut) {
resourceState.loading = true;
resourceRepository
.reviewResource(resourceVersion, { deny: false, reason: "Whatever" })
.syncResource(resourceVersion, { deny: false })
.then(replaceResourceVersion)
.finally(() => {
resourceState.loading = false;
......@@ -313,11 +303,26 @@ function resetForm() {
Set to Latest
</button>
</li>
<li v-if="version.status === Status.WAIT_FOR_REVIEW">
<router-link
class="dropdown-item"
:to="{ name: 'resource-review' }"
>
Review Resource
</router-link>
</li>
<li v-if="version.status === Status.SYNC_REQUESTED">
<router-link
class="dropdown-item"
:to="{ name: 'admin-sync-requests' }"
>
Review sync request
</router-link>
</li>
<li
v-if="
version.status === Status.APPROVED ||
version.status === Status.SYNC_ERROR ||
version.status === Status.SYNC_REQUESTED
version.status === Status.SYNC_ERROR
"
>
<button
......@@ -331,19 +336,6 @@ function resetForm() {
<span class="ms-1">Sync to Cluster</span>
</button>
</li>
<li v-if="version.status === Status.SYNC_REQUESTED">
<button
class="dropdown-item"
type="button"
@click="denyResource(version)"
>
<font-awesome-icon
icon="fa-solid fa-xmark"
class="text-danger"
/>
<span class="ms-1">Deny Synchronization</span>
</button>
</li>
<li
v-if="
version.status === Status.SYNCHRONIZED ||
......
<script setup lang="ts">
import BootstrapToast from "@/components/BootstrapToast.vue";
import ReasonModal from "@/components/modals/ReasonModal.vue";
import ResourceVersionInfoModal from "@/components/resources/ResourceVersionInfoModal.vue";
import { onMounted, reactive } from "vue";
import {
type ResourceOut,
type ResourceVersionOut,
Status,
type UserSynchronizationRequestOut,
} from "@/client/resource";
import { useResourceStore } from "@/stores/resources";
import { useNameStore } from "@/stores/names";
import { useAuthStore } from "@/stores/users";
import { Modal, Toast } from "bootstrap";
import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
const resourceState = reactive<{
sendingRequest: boolean;
loading: boolean;
resources: Record<string, ResourceOut>;
inspectResource?: ResourceOut;
rejectResource?: ResourceVersionOut;
inspectVersionIndex: number;
}>({
sendingRequest: false,
loading: true,
resources: {},
inspectResource: undefined,
rejectResource: undefined,
inspectVersionIndex: 0,
});
const resourceRepository = useResourceStore();
const nameRepository = useNameStore();
const userRepository = useAuthStore();
let rejectReasonModal: Modal | null = null;
let successToast: Toast | null = null;
let rejectToast: Toast | null = null;
let refreshTimeout: NodeJS.Timeout | undefined = undefined;
function rejectSyncRequest(
reason: string,
resourceVersion?: ResourceVersionOut,
) {
if (resourceVersion != undefined) {
resourceState.sendingRequest = true;
resourceRepository
.syncResource(resourceVersion, { deny: true, reason })
.then(() => {
rejectReasonModal?.hide();
rejectToast?.show();
})
.finally(() => {
resourceState.sendingRequest = false;
});
}
}
function syncVersion(resourceId: string, resourceVersionId: string) {
resourceState.sendingRequest = true;
resourceRepository
.syncResource(
{
resource_version_id: resourceVersionId,
resource_id: resourceId,
release: "",
created_at: 0,
s3_path: "",
cluster_path: "",
status: Status.SYNC_REQUESTED,
},
{ deny: false },
)
.then(() => {
successToast?.show();
delete resourceState.resources[resourceId];
})
.finally(() => {
resourceState.sendingRequest = false;
});
}
function fetchResources(
syncRequests: UserSynchronizationRequestOut[],
): Promise<ResourceOut[]> {
return Promise.all(
syncRequests.map((request) =>
resourceRepository.fetchResource(request.resource_id, [
Status.SYNC_REQUESTED,
]),
),
);
}
function fetchUserNames(
syncRequests: UserSynchronizationRequestOut[],
): UserSynchronizationRequestOut[] {
userRepository.fetchUsernames(
syncRequests.map((request) => request.requester_id),
);
return syncRequests;
}
function fetchRequests() {
resourceState.loading = true;
resourceRepository
.fetchSyncRequests(() => {
resourceState.loading = false;
})
.then(fetchUserNames)
.then(fetchResources)
.then((resources) => {
const newMapping: Record<string, ResourceOut> = {};
for (const resource of resources) {
newMapping[resource.resource_id] = resource;
}
resourceState.resources = newMapping;
});
}
function clickRefreshRequests() {
clearTimeout(refreshTimeout);
refreshTimeout = setTimeout(() => {
fetchRequests();
}, 500);
}
onMounted(() => {
fetchRequests();
rejectReasonModal = new Modal("#sync-request-reject-modal");
successToast = new Toast("#accept-sync-request-toast");
rejectToast = new Toast("#reject-sync-request-toast");
});
</script>
<template>
<bootstrap-toast toast-id="accept-sync-request-toast" color-class="success">
Syncing resource to the cluster
</bootstrap-toast>
<bootstrap-toast toast-id="reject-sync-request-toast" color-class="danger">
Rejected resource synchronization request
</bootstrap-toast>
<resource-version-info-modal
modal-id="sync-request-resource-version-info-modal"
:resource-version-index="resourceState.inspectVersionIndex"
:resource="resourceState.inspectResource"
/>
<reason-modal
modal-id="sync-request-reject-modal"
modal-label="Resource Synchronization Request Reject Modal"
:loading="resourceState.sendingRequest"
@save="(reason) => rejectSyncRequest(reason, resourceState.rejectResource)"
>
<template #header>
Reject Resource Synchronization Request
<b>{{ resourceState.rejectResource?.release }}</b>
</template>
</reason-modal>
<div
class="row border-bottom mb-4 justify-content-between align-items-center"
>
<h2 class="w-fit">Review resource synchronization requests</h2>
<span
class="w-fit"
tabindex="0"
data-bs-title="Refresh Reviewable Resources"
data-bs-toggle="tooltip"
id="refreshReviewableResourcesButton"
>
<button
type="button"
class="btn btn-primary btn-light me-2 shadow-sm border w-fit"
:disabled="resourceState.loading || resourceState.sendingRequest"
@click="clickRefreshRequests"
>
<font-awesome-icon icon="fa-solid fa-arrow-rotate-right" />
<span class="visually-hidden">Refresh Reviewable Resources</span>
</button>
</span>
</div>
<div v-if="resourceState.loading" class="text-center mt-5">
<div class="spinner-border" style="width: 3rem; height: 3rem" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div
v-else-if="resourceRepository.syncRequests.length === 0"
class="text-center mt-5 fs-4"
>
There are currently no resource synchronization requests
</div>
<div v-else class="d-flex flex-column">
<div
class="border p-2 pb-0 rounded mb-2 d-flex"
v-for="request in resourceRepository.syncRequests"
:key="request.resource_version_id"
>
<div class="flex-grow-1">
<h6>
{{ nameRepository.getName(request.resource_id) }}@{{
nameRepository.getName(request.resource_version_id)
}}
</h6>
<div>
<b>Requester</b>: {{ nameRepository.getName(request.requester_id) }}
</div>
<div><b>Reason</b>:</div>
<p>{{ request.reason }}</p>
</div>
<div class="d-flex flex-column justify-content-evenly align-items-center">
<button
type="button"
class="btn btn-secondary btn-sm"
data-bs-toggle="modal"
data-bs-target="#sync-request-resource-version-info-modal"
@click="
resourceState.inspectResource =
resourceState.resources[request.resource_id];
resourceState.inspectVersionIndex = resourceState.resources[
request.resource_id
].versions.findIndex(
(version) =>
version.resource_version_id === request.resource_version_id,
);
"
>
Inspect Resource
</button>
<div class="btn-group">
<button
type="button"
class="btn btn-success btn-sm"
:disabled="resourceState.sendingRequest"
@click="
syncVersion(request.resource_id, request.resource_version_id)
"
>
Accept
</button>
<button
type="button"
class="btn btn-danger btn-sm"
@click="
resourceState.rejectResource = resourceState.resources[
request.resource_id
].versions.find(
(version) =>
version.resource_version_id === request.resource_version_id,
)
"
data-bs-toggle="modal"
data-bs-target="#sync-request-reject-modal"
:disabled="resourceState.sendingRequest"
>
Reject
</button>
</div>
</div>
</div>
</div>
</template>
<style scoped></style>
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