From 7675fe354494aa5df929c8458a3f22b3324ad184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20G=C3=B6bel?= <dgoebel@techfak.uni-bielefeld.de> Date: Tue, 16 Jan 2024 17:52:50 +0100 Subject: [PATCH] Refactor object name cache and add basic resource review view #88 --- src/App.vue | 5 +- src/components/NavbarTop.vue | 53 ++++++-- src/components/modals/SearchUserModal.vue | 12 +- .../object-storage/BucketListItem.vue | 4 +- .../modals/UploadObjectModal.vue | 14 +- src/components/resources/ResourceCard.vue | 10 +- .../workflows/modals/ParameterModal.vue | 8 +- src/router/resourceRoutes.ts | 5 + src/stores/names.ts | 41 ++++++ src/stores/resources.ts | 58 ++++++++- src/stores/users.ts | 32 ++--- src/stores/workflows.ts | 14 +- src/views/resources/ListResourcesView.vue | 2 +- src/views/resources/ReviewResourceView.vue | 120 ++++++++++++++++++ .../workflows/ListWorkflowExecutionsView.vue | 6 +- src/views/workflows/ReviewWorkflowsView.vue | 6 +- src/views/workflows/WorkflowView.vue | 2 +- 17 files changed, 315 insertions(+), 77 deletions(-) create mode 100644 src/stores/names.ts create mode 100644 src/views/resources/ReviewResourceView.vue diff --git a/src/App.vue b/src/App.vue index 306e0d6..30e6887 100644 --- a/src/App.vue +++ b/src/App.vue @@ -11,10 +11,12 @@ import { OpenAPI as ResourceOpenAPI } from "@/client/resource"; import { environment } from "@/environment"; import FooterBottom from "@/components/FooterBottom.vue"; import axios from "axios"; +import { useNameStore } from "@/stores/names"; const { cookies } = useCookies(); const store = useAuthStore(); const router = useRouter(); +const nameRepository = useNameStore(); onBeforeMount(() => { S3ProxyOpenAPI.BASE = environment.S3PROXY_API_BASE_URL; @@ -58,11 +60,12 @@ onBeforeMount(() => { return { name: "dashboard" }; } else if ( to.meta.requiresReviewerRole && - !(store.workflowReviewer || store.admin) + !(store.rewiewer || store.admin) ) { return { name: "dashboard" }; } }); + nameRepository.loadNameMapping(); }); </script> diff --git a/src/components/NavbarTop.vue b/src/components/NavbarTop.vue index 57d8e84..20cad27 100644 --- a/src/components/NavbarTop.vue +++ b/src/components/NavbarTop.vue @@ -28,6 +28,8 @@ const objectStorageActive = computed<boolean>( const workflowActive = computed<boolean>( () => activeRoute.value == "workflows", ); +const resourceActive = computed<boolean>(() => activeRoute.value == "resource"); +const adminActive = computed<boolean>(() => activeRoute.value == "admin"); watch( () => route.name, @@ -37,6 +39,10 @@ watch( activeRoute.value = "buckets"; } else if (to.startsWith("workflow")) { activeRoute.value = "workflows"; + } else if (to.startsWith("resource")) { + activeRoute.value = "resource"; + } else if (to.startsWith("admin")) { + activeRoute.value = "admin"; } else { activeRoute.value = to; } @@ -126,11 +132,7 @@ watch( >My Workflow Executions </router-link> </li> - <li - v-if=" - store.workflowDev || store.workflowReviewer || store.admin - " - > + <li v-if="store.workflowDev || store.rewiewer || store.admin"> <hr class="dropdown-divider" /> </li> <li v-if="store.workflowDev || store.admin"> @@ -140,7 +142,7 @@ watch( >My Workflows </router-link> </li> - <li v-if="store.workflowReviewer || store.admin"> + <li v-if="store.rewiewer || store.admin"> <router-link class="dropdown-item" :to="{ name: 'workflows-reviewer' }" @@ -152,7 +154,7 @@ watch( <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" - :class="{ 'text-black': workflowActive }" + :class="{ 'text-black': resourceActive }" id="resourceDropdown" href="#" role="button" @@ -171,7 +173,9 @@ watch( >Available Resources </router-link> </li> - <li v-if="store.resourceMaintainer || store.admin"> + <li + v-if="store.resourceMaintainer || store.rewiewer || store.admin" + > <hr class="dropdown-divider" /> </li> <li v-if="store.resourceMaintainer || store.admin"> @@ -181,6 +185,36 @@ watch( >My Resources </router-link> </li> + <li v-if="store.rewiewer || store.admin"> + <router-link + class="dropdown-item" + :to="{ name: 'resource-review' }" + >Review + </router-link> + </li> + </ul> + </li> + <li v-if="store.admin"> + <a + class="nav-link dropdown-toggle" + :class="{ 'text-black': adminActive }" + id="adminDropdown" + href="#" + role="button" + data-bs-toggle="dropdown" + aria-expanded="false" + data-bs-auto-close="true" + > + Admin + </a> + <ul + class="dropdown-menu shadow m-0" + aria-labelledby="adminDropdown" + > + <li><a class="dropdown-item disabled" href="#">User</a></li> + <li><a class="dropdown-item disabled" href="#">Bucket</a></li> + <li><a class="dropdown-item disabled" href="#">Workflow</a></li> + <li><a class="dropdown-item disabled" href="#">Resource</a></li> </ul> </li> </ul> @@ -193,7 +227,7 @@ watch( data-bs-toggle="dropdown" aria-expanded="false" > - <strong class="me-2">{{ store.user.display_name }}</strong> + <strong class="me-2">{{ store.user?.display_name }}</strong> <font-awesome-icon icon="fa-solid fa-circle-user" class="fs-5" /> </a> <ul @@ -237,7 +271,6 @@ watch( </nav> </header> <bootstrap-modal - static-backdrop modal-i-d="advancedUsageModal" modal-label="Advanced Usage Modal" v-if="store.authenticated" diff --git a/src/components/modals/SearchUserModal.vue b/src/components/modals/SearchUserModal.vue index 85f24c3..56c269a 100644 --- a/src/components/modals/SearchUserModal.vue +++ b/src/components/modals/SearchUserModal.vue @@ -5,6 +5,7 @@ import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; import { UserService } from "@/client/auth"; import type { User } from "@/client/auth"; import { useAuthStore } from "@/stores/users"; +import { useNameStore } from "@/stores/names"; const props = defineProps<{ modalID: string; @@ -13,6 +14,7 @@ const props = defineProps<{ const randomIDSuffix = Math.random().toString(16).substring(2, 8); const store = useAuthStore(); +const nameStore = useNameStore(); const formState = reactive<{ searchString: string; @@ -60,11 +62,9 @@ function searchUser(name: string) { formState.potentialUsers = userSuggestions.filter( (user) => store.currentUID != user.uid, ); - const uidToName: Record<string, string> = {}; for (const user of userSuggestions) { - uidToName[user.uid] = user.display_name; + nameStore.addNameToMapping(user.uid, user.display_name); } - store.addUidToNameMapping(uidToName); }) .catch((err) => { formState.error = true; @@ -106,7 +106,8 @@ function searchUser(name: string) { icon="fa-solid fa-x" class="mb-2" style="color: var(--bs-danger); font-size: 4em" - /><br /> + /> + <br /> <span class="text-danger" >There seems to be an error<br />Try again later</span > @@ -129,7 +130,8 @@ function searchUser(name: string) { icon="fa-solid fa-magnifying-glass" class="mb-2" style="color: var(--bs-secondary); font-size: 4em" - /><br /> + /> + <br /> <span v-if="formState.searchString.length > 2" >Could not find any Users</span > diff --git a/src/components/object-storage/BucketListItem.vue b/src/components/object-storage/BucketListItem.vue index a1db0ae..04f5f44 100644 --- a/src/components/object-storage/BucketListItem.vue +++ b/src/components/object-storage/BucketListItem.vue @@ -12,6 +12,7 @@ import { useBucketStore } from "@/stores/buckets"; import { useRouter } from "vue-router"; import { useAuthStore } from "@/stores/users"; import type { FolderTree } from "@/types/PseudoFolder"; +import { useNameStore } from "@/stores/names"; const props = defineProps<{ active: boolean; @@ -23,6 +24,7 @@ const props = defineProps<{ const randomIDSuffix = Math.random().toString(16).substring(2, 8); const permissionRepository = useBucketStore(); const userRepository = useAuthStore(); +const nameRepository = useNameStore(); const router = useRouter(); const permission = computed<BucketPermissionOut | undefined>( @@ -168,7 +170,7 @@ onMounted(() => { <tr v-if="permission"> <th scope="row" class="fw-bold">Owner:</th> <td class="text-truncate"> - {{ userRepository.userMapping[bucket.owner] }} + {{ nameRepository.getName(bucket.owner) }} </td> </tr> <tr diff --git a/src/components/object-storage/modals/UploadObjectModal.vue b/src/components/object-storage/modals/UploadObjectModal.vue index dfa6f4d..a6ec1f3 100644 --- a/src/components/object-storage/modals/UploadObjectModal.vue +++ b/src/components/object-storage/modals/UploadObjectModal.vue @@ -189,15 +189,17 @@ onMounted(() => { </template> <template v-slot:footer> <div class="w-50 me-auto" v-if="formState.uploading"> - <div class="progress"> + <div + class="progress" + aria-valuemin="0" + aria-valuemax="100" + role="progressbar" + :aria-valuenow="uploadProgress" + aria-label="Upload Progressbar" + > <div class="progress-bar bg-info" - role="progressbar" - aria-label="Basic example" :style="{ width: uploadProgress + '%' }" - :aria-valuenow="uploadProgress" - aria-valuemin="0" - aria-valuemax="100" > {{ uploadProgress }}% </div> diff --git a/src/components/resources/ResourceCard.vue b/src/components/resources/ResourceCard.vue index c4bca17..821f041 100644 --- a/src/components/resources/ResourceCard.vue +++ b/src/components/resources/ResourceCard.vue @@ -6,17 +6,17 @@ import { } from "@/client/resource"; import { computed, onMounted, ref } from "vue"; import dayjs from "dayjs"; -import { useAuthStore } from "@/stores/users"; 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 userRepository = useAuthStore(); const objectRepository = useS3ObjectStore(); const resourceRepository = useResourceStore(); +const nameRepository = useNameStore(); const props = defineProps<{ resource: ResourceOut; @@ -302,14 +302,12 @@ onMounted(() => { Maintainer: <span v-if=" - props.loading || !userRepository.userMapping[resource.maintainer_id] + props.loading || !nameRepository.getName(resource.maintainer_id) " class="placeholder-glow" ><span class="placeholder col-2"></span ></span> - <span v-else>{{ - userRepository.userMapping[resource.maintainer_id] - }}</span> + <span v-else>{{ nameRepository.getName(resource.maintainer_id) }}</span> </div> </div> </div> diff --git a/src/components/workflows/modals/ParameterModal.vue b/src/components/workflows/modals/ParameterModal.vue index 576746b..00dc11e 100644 --- a/src/components/workflows/modals/ParameterModal.vue +++ b/src/components/workflows/modals/ParameterModal.vue @@ -6,10 +6,10 @@ import type { RouteParamsRaw } from "vue-router"; import { Modal } from "bootstrap"; import { useRouter } from "vue-router"; import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; -import { useWorkflowStore } from "@/stores/workflows"; import type { WorkflowExecutionOut } from "@/client/workflow"; +import { useNameStore } from "@/stores/names"; -const workflowRepository = useWorkflowStore(); +const nameRepository = useNameStore(); const executionRepository = useWorkflowExecutionStore(); const router = useRouter(); @@ -71,9 +71,9 @@ const workflowName = computed<string>(() => { execution?.workflow_version_id != undefined ) { return ( - workflowRepository.getName(execution.workflow_id) + + nameRepository.getName(execution.workflow_id) + "@" + - workflowRepository.getName(execution.workflow_version_id) + nameRepository.getName(execution.workflow_version_id) ); } } diff --git a/src/router/resourceRoutes.ts b/src/router/resourceRoutes.ts index 24b7db1..6b2ed06 100644 --- a/src/router/resourceRoutes.ts +++ b/src/router/resourceRoutes.ts @@ -11,4 +11,9 @@ export const resourceRoutes: RouteRecordRaw[] = [ name: "resource-maintainer", component: () => import("../views/resources/MyResourcesView.vue"), }, + { + path: "reviewer/resources", + name: "resource-review", + component: () => import("../views/resources/ReviewResourceView.vue"), + }, ]; diff --git a/src/stores/names.ts b/src/stores/names.ts new file mode 100644 index 0000000..4dc30f7 --- /dev/null +++ b/src/stores/names.ts @@ -0,0 +1,41 @@ +import { defineStore } from "pinia"; + +export const useNameStore = defineStore({ + id: "names", + state: () => + ({ + nameMapping: {}, + }) as { + nameMapping: Record<string, string>; + }, + getters: { + getName(): (objectID: string) => string | undefined { + return (objectID) => + this.nameMapping[objectID] ?? localStorage.getItem(objectID); + }, + }, + actions: { + addNameToMapping(objectId: string, objectName: string) { + this.nameMapping[objectId] = objectName; + localStorage.setItem(objectId, objectName); + }, + deleteNameFromMapping(objectId: string) { + delete this.nameMapping[objectId]; + localStorage.removeItem(objectId); + }, + loadNameMapping() { + if (Object.keys(this.nameMapping).length > 0) { + return; + } + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key != null) { + const value = localStorage.getItem(key); + if (value != null) { + this.nameMapping[key] = value; + } + } + } + }, + }, +}); diff --git a/src/stores/resources.ts b/src/stores/resources.ts index 40aa0cf..6e64873 100644 --- a/src/stores/resources.ts +++ b/src/stores/resources.ts @@ -8,6 +8,7 @@ import type { import { ResourceService, ResourceVersionService } from "@/client/resource"; import { useAuthStore } from "@/stores/users"; import { Status } from "@/client/resource"; +import { useNameStore } from "@/stores/names"; export const useResourceStore = defineStore({ id: "resources", @@ -28,15 +29,33 @@ export const useResourceStore = defineStore({ }, }, actions: { - fetchResources(onFinally?: () => void): Promise<ResourceOut[]> { + __addNameToMapping(key: string, value: string) { + const nameStore = useNameStore(); + nameStore.addNameToMapping(key, value); + }, + fetchResources( + maintainerId?: string, + versionStatus?: Status[], + ): Promise<ResourceOut[]> { + return ResourceService.resourceListResources(maintainerId, versionStatus); + }, + fetchPublicResources(onFinally?: () => void): Promise<ResourceOut[]> { if (Object.keys(this.resourceMapping).length > 0) { onFinally?.(); } - return ResourceService.resourceListResources() + return this.fetchResources() .then((resources) => { const newMapping: Record<string, ResourceOut> = {}; + const nameStore = useNameStore(); for (const resource of resources) { newMapping[resource.resource_id] = resource; + nameStore.addNameToMapping(resource.resource_id, resource.name); + for (const version of resource.versions) { + nameStore.addNameToMapping( + version.resource_version_id, + version.release, + ); + } } this.resourceMapping = newMapping; return resources; @@ -56,6 +75,14 @@ export const useResourceStore = defineStore({ ) .then((resource) => { this.ownResourceMapping[resource.resource_id] = resource; + const nameStore = useNameStore(); + nameStore.addNameToMapping(resource.resource_id, resource.name); + for (const version of resource.versions) { + nameStore.addNameToMapping( + version.resource_version_id, + version.release, + ); + } return resource; }) .finally(onFinally); @@ -65,14 +92,19 @@ export const useResourceStore = defineStore({ if (Object.keys(this.ownResourceMapping).length > 0) { onFinally?.(); } - return ResourceService.resourceListResources( - authStore.currentUID, - Object.values(Status), - ) + return this.fetchResources(authStore.currentUID, Object.values(Status)) .then((resources) => { const newMapping: Record<string, ResourceOut> = {}; + const nameStore = useNameStore(); for (const resource of resources) { newMapping[resource.resource_id] = resource; + nameStore.addNameToMapping(resource.resource_id, resource.name); + for (const version of resource.versions) { + nameStore.addNameToMapping( + version.resource_version_id, + version.release, + ); + } } this.ownResourceMapping = newMapping; return resources; @@ -83,6 +115,16 @@ export const useResourceStore = defineStore({ const createdResource = await ResourceService.resourceCreateResource(resource); this.ownResourceMapping[createdResource.resource_id] = createdResource; + const nameStore = useNameStore(); + nameStore.addNameToMapping( + createdResource.resource_id, + createdResource.name, + ); + nameStore.addNameToMapping( + createdResource.versions[0].resource_version_id, + createdResource.versions[0].release, + ); + return createdResource; }, requestSynchronization( @@ -130,6 +172,10 @@ export const useResourceStore = defineStore({ this.fetchOwnResource(versionOut.resource_id); return versionOut; } + useNameStore().addNameToMapping( + versionOut.resource_version_id, + versionOut.release, + ); this.ownResourceMapping[versionOut.resource_id].versions.push( versionOut, ); diff --git a/src/stores/users.ts b/src/stores/users.ts index 4d7b524..928ed29 100644 --- a/src/stores/users.ts +++ b/src/stores/users.ts @@ -11,6 +11,7 @@ import { useWorkflowStore } from "@/stores/workflows"; import { useS3KeyStore } from "@/stores/s3keys"; import { useS3ObjectStore } from "@/stores/s3objects"; import { clear as dbclear } from "idb-keyval"; +import { useNameStore } from "@/stores/names"; type DecodedToken = { exp: number; @@ -23,7 +24,6 @@ export type RootState = { token: string | null; decodedToken: DecodedToken | null; user: User | null; - userMapping: Record<string, string>; }; function parseJwt(token: string): DecodedToken { @@ -49,7 +49,6 @@ export const useAuthStore = defineStore({ token: null, decodedToken: null, user: null, - userMapping: {}, }) as RootState, getters: { roles(): string[] { @@ -71,7 +70,7 @@ export const useAuthStore = defineStore({ state.user?.roles?.includes(RoleEnum.USER) ?? state.decodedToken?.roles.includes(RoleEnum.USER) ?? false, - workflowReviewer: (state) => + rewiewer: (state) => state.user?.roles?.includes(RoleEnum.REVIEWER) ?? state.decodedToken?.roles.includes(RoleEnum.REVIEWER) ?? false, @@ -110,8 +109,7 @@ export const useAuthStore = defineStore({ }, updateUser(user: User) { this.user = user; - this.userMapping[user.uid] = user.display_name; - localStorage.setItem(user.uid, user.display_name); + useNameStore().addNameToMapping(user.uid, user.display_name); }, logout() { S3ProxyOpenAPI.TOKEN = undefined; @@ -127,15 +125,10 @@ export const useAuthStore = defineStore({ useS3KeyStore().$reset(); useS3ObjectStore().$reset(); }, - async addUidToNameMapping(mapping: Record<string, string>): Promise<void> { - for (const uid of Object.keys(mapping)) { - this.userMapping[uid] = mapping[uid]; - localStorage.setItem(uid, mapping[uid]); - } - }, async fetchUsernames(uids: string[]): Promise<string[]> { + const nameStore = useNameStore(); const filteredIds = uids - .filter((uid) => !this.userMapping[uid]) // filter already present UIDs + .filter((uid) => !nameStore.getName(uid)) // filter already present UIDs .filter( // filter unique UIDs (modeId, index, array) => @@ -143,7 +136,8 @@ export const useAuthStore = defineStore({ ); // If all uids are already in the store, then return them if (filteredIds.length === 0) { - return uids.map((uid) => this.userMapping[uid]); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return uids.map((uid) => nameStore.getName(uid)!); } const missingIds: string[] = []; const storedNames = filteredIds.map((uid) => localStorage.getItem(uid)); @@ -154,23 +148,19 @@ export const useAuthStore = defineStore({ missingIds.push(filteredIds[index]); } else { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.userMapping[filteredIds[index]] = storedNames[index]!; + nameStore.addNameToMapping(filteredIds[index], storedNames[index]!); } } - // If all uids could be resolved from cache, return them - if (missingIds.length === 0) { - return uids.map((uid) => this.userMapping[uid]); - } // fetch missing users from backend const fetchedUsers = await Promise.all( missingIds.map((uid) => UserService.userGetUser(uid)), ); // Put users in store for (const user of fetchedUsers) { - this.userMapping[user.uid] = user.display_name; - localStorage.setItem(user.uid, user.display_name); + nameStore.addNameToMapping(user.uid, user.display_name); } - return uids.map((uid) => this.userMapping[uid]); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return uids.map((uid) => nameStore.getName(uid)!); }, }, }); diff --git a/src/stores/workflows.ts b/src/stores/workflows.ts index 6c7e04b..8b1bd24 100644 --- a/src/stores/workflows.ts +++ b/src/stores/workflows.ts @@ -18,19 +18,17 @@ import { } from "@/client/workflow"; import { useAuthStore } from "@/stores/users"; import { set, get } from "idb-keyval"; +import { useNameStore } from "@/stores/names"; export const useWorkflowStore = defineStore({ id: "workflows", state: () => ({ workflowMapping: {}, - nameMapping: {}, comprehensiveWorkflowMapping: {}, modeMapping: {}, - modeNameMapping: {}, }) as { workflowMapping: Record<string, WorkflowOut>; - nameMapping: Record<string, string>; comprehensiveWorkflowMapping: Record<string, WorkflowOut>; modeMapping: Record<string, WorkflowModeOut>; }, @@ -61,18 +59,13 @@ export const useWorkflowStore = defineStore({ } return mapping; }, - getName(): (objectID: string) => string | undefined { - return (objectID) => - this.nameMapping[objectID] ?? localStorage.getItem(objectID); - }, getArbitraryWorkflow(): (wid: string) => Promise<WorkflowIn | undefined> { return (wid: string) => get(wid); }, }, actions: { __addNameToMapping(key: string, value: string) { - this.nameMapping[key] = value; - localStorage.setItem(key, value); + useNameStore().addNameToMapping(key, value); }, fetchWorkflows(onFinally?: () => void): Promise<WorkflowOut[]> { if (Object.keys(this.workflowMapping).length > 0) { @@ -300,8 +293,7 @@ export const useWorkflowStore = defineStore({ }, deleteWorkflow(workflow_id: string): Promise<void> { return WorkflowService.workflowDeleteWorkflow(workflow_id).then(() => { - delete this.nameMapping[workflow_id]; - localStorage.removeItem(workflow_id); + useNameStore().deleteNameFromMapping(workflow_id); delete this.workflowMapping[workflow_id]; delete this.comprehensiveWorkflowMapping[workflow_id]; }); diff --git a/src/views/resources/ListResourcesView.vue b/src/views/resources/ListResourcesView.vue index db883da..b615344 100644 --- a/src/views/resources/ListResourcesView.vue +++ b/src/views/resources/ListResourcesView.vue @@ -43,7 +43,7 @@ const filteredSortedResources = computed<ResourceOut[]>(() => { onMounted(() => { resourceRepository - .fetchResources(() => { + .fetchPublicResources(() => { resourceState.loading = false; }) .then((resources) => { diff --git a/src/views/resources/ReviewResourceView.vue b/src/views/resources/ReviewResourceView.vue new file mode 100644 index 0000000..cb91f85 --- /dev/null +++ b/src/views/resources/ReviewResourceView.vue @@ -0,0 +1,120 @@ +<script setup lang="ts"> +import { useResourceStore } from "@/stores/resources"; +import { computed, onMounted, reactive } from "vue"; +import { type ResourceOut, Status } from "@/client/resource"; +import CopyToClipboardIcon from "@/components/CopyToClipboardIcon.vue"; + +const resourceRepository = useResourceStore(); + +const resourceState = reactive<{ + reviewableResources: ResourceOut[]; + loading: boolean; +}>({ + reviewableResources: [], + loading: true, +}); + +const countItems = computed<number>(() => + resourceState.reviewableResources.reduce( + (previousValue, currentValue) => + previousValue + currentValue.versions.length, + 0, + ), +); + +onMounted(() => { + resourceRepository + .fetchResources(undefined, [Status.SYNC_REQUESTED, Status.SYNCHRONIZING]) + .then((resources) => { + resourceState.reviewableResources = resources; + }) + .finally(() => { + resourceState.loading = false; + }); +}); +</script> + +<template> + <div class="row m-2 border-bottom mb-4"> + <h2 class="mb-2">Resource Requests</h2> + </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> + <table class="table caption-top table-striped table-hover align-middle"> + <caption> + Display + {{ + countItems + }} + resource versions + </caption> + <thead> + <tr> + <th scope="col">Resource</th> + <th scope="col">Release</th> + <th scope="col">Status</th> + <th scope="col">S3 Path</th> + <th scope="col" class="text-end">Action</th> + </tr> + </thead> + <tbody> + <template + v-for="resource in resourceState.reviewableResources" + :key="resource.resource_id" + > + <tr + v-for="version in resource.versions" + :key="version.resource_version_id" + > + <th>{{ resource.name }}</th> + <th>{{ version.release }}</th> + <th>{{ version.status }}</th> + <th> + <div class="input-group"> + <input + class="form-control form-control-sm" + type="text" + :value="version.s3_path" + aria-label="S3 Access Path" + readonly + /> + <span class="input-group-text" + ><copy-to-clipboard-icon :text="version.s3_path" + /></span> + </div> + </th> + <th class="text-end"> + <div + v-if="version.status === Status.SYNC_REQUESTED" + class="btn-group" + > + <button type="button" class="btn btn-success btn-sm"> + Synchronize + </button> + <button type="button" class="btn btn-danger btn-sm">Deny</button> + </div> + <div + v-else-if="version.status === Status.SYNCHRONIZING" + class="progress" + role="progressbar" + aria-label="Animated striped example" + aria-valuenow="100" + aria-valuemin="0" + aria-valuemax="100" + > + <div + class="progress-bar progress-bar-striped progress-bar-animated" + style="width: 100%" + ></div> + </div> + </th> + </tr> + </template> + </tbody> + </table> +</template> + +<style scoped></style> diff --git a/src/views/workflows/ListWorkflowExecutionsView.vue b/src/views/workflows/ListWorkflowExecutionsView.vue index cbf7b1f..c3d87aa 100644 --- a/src/views/workflows/ListWorkflowExecutionsView.vue +++ b/src/views/workflows/ListWorkflowExecutionsView.vue @@ -9,8 +9,10 @@ import { useWorkflowStore } from "@/stores/workflows"; import { useWorkflowExecutionStore } from "@/stores/workflowExecutions"; import ParameterModal from "@/components/workflows/modals/ParameterModal.vue"; import { ExponentialBackoff } from "@/utils/BackoffStrategy"; +import { useNameStore } from "@/stores/names"; const workflowRepository = useWorkflowStore(); +const nameRepository = useNameStore(); const executionRepository = useWorkflowExecutionStore(); const backoff = new ExponentialBackoff(); @@ -247,8 +249,8 @@ onUnmounted(() => { }, }" > - {{ workflowRepository.getName(execution.workflow_id) }}@{{ - workflowRepository.getName(execution.workflow_version_id) + {{ nameRepository.getName(execution.workflow_id) }}@{{ + nameRepository.getName(execution.workflow_version_id) }} </router-link> </td> diff --git a/src/views/workflows/ReviewWorkflowsView.vue b/src/views/workflows/ReviewWorkflowsView.vue index 63ed009..80baa8d 100644 --- a/src/views/workflows/ReviewWorkflowsView.vue +++ b/src/views/workflows/ReviewWorkflowsView.vue @@ -6,8 +6,10 @@ import { determineGitIcon } from "@/utils/GitRepository"; import { sortedVersions } from "@/utils/Workflow"; import { useWorkflowStore } from "@/stores/workflows"; import { useAuthStore } from "@/stores/users"; +import { useNameStore } from "@/stores/names"; const workflowRepository = useWorkflowStore(); +const nameRepository = useNameStore(); const userRepository = useAuthStore(); const workflowsState = reactive<{ @@ -78,8 +80,8 @@ onMounted(() => { {{ workflow.repository_url }} </a> </td> - <td v-if="userRepository.userMapping[workflow.developer_id]"> - {{ userRepository.userMapping[workflow.developer_id] }} + <td v-if="nameRepository.getName(workflow.developer_id)"> + {{ nameRepository.getName(workflow.developer_id) }} </td> <td v-else class="placeholder-glow"> <div class="placeholder w-75"></div> diff --git a/src/views/workflows/WorkflowView.vue b/src/views/workflows/WorkflowView.vue index 01af1de..08b1395 100644 --- a/src/views/workflows/WorkflowView.vue +++ b/src/views/workflows/WorkflowView.vue @@ -166,7 +166,7 @@ const gitIcon = computed<string>(() => const allowVersionDeprecation = computed<boolean>(() => { if (activeVersion.value?.status === Status.PUBLISHED) { - if (userRepository.workflowReviewer || userRepository.admin) { + if (userRepository.rewiewer || userRepository.admin) { return true; } else if ( userRepository.workflowDev && -- GitLab