diff --git a/src/App.vue b/src/App.vue index 30e6887148b2cffac09f1b9dd14a71a5b549b457..65c7b0dfe57e1c361aa9ea7d36cad82395b59ca6 100644 --- a/src/App.vue +++ b/src/App.vue @@ -63,6 +63,13 @@ onBeforeMount(() => { !(store.rewiewer || store.admin) ) { return { name: "dashboard" }; + } else if ( + to.meta.requiresMaintainerRole && + !(store.resourceMaintainer || store.admin) + ) { + return { name: "dashboard" }; + } else if (to.meta.adminRole && !store.admin) { + return { name: "dashboard" }; } }); nameRepository.loadNameMapping(); diff --git a/src/components/NavbarTop.vue b/src/components/NavbarTop.vue index 20cad27ce06b4af13c5c6cc83fc880ba6005b955..37a3540c874d27e715d88a1b9e3b8e32cb2acbf1 100644 --- a/src/components/NavbarTop.vue +++ b/src/components/NavbarTop.vue @@ -214,7 +214,13 @@ watch( <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> + <li> + <router-link + class="dropdown-item" + :to="{ name: 'admin-resources' }" + >Resources + </router-link> + </li> </ul> </li> </ul> @@ -271,7 +277,7 @@ watch( </nav> </header> <bootstrap-modal - modal-i-d="advancedUsageModal" + modal-id="advancedUsageModal" modal-label="Advanced Usage Modal" v-if="store.authenticated" size-modifier="lg" diff --git a/src/components/admin/ResourceVersionInfoModal.vue b/src/components/admin/ResourceVersionInfoModal.vue new file mode 100644 index 0000000000000000000000000000000000000000..1a774636382d2cf6d2ef52adb925e5f138041593 --- /dev/null +++ b/src/components/admin/ResourceVersionInfoModal.vue @@ -0,0 +1,262 @@ +<script setup lang="ts"> +import BootstrapModal from "@/components/modals/BootstrapModal.vue"; +import type { ResourceVersionOut, ResourceOut } from "@/client/resource"; +import CopyToClipboardIcon from "@/components/CopyToClipboardIcon.vue"; +import dayjs from "dayjs"; +import { computed } from "vue"; +import { useNameStore } from "@/stores/names"; + +const props = defineProps<{ + modalId: string; + resourceVersionIndex: number; + resource?: ResourceOut; +}>(); + +const nameRepository = useNameStore(); + +const resourceVersion = computed<ResourceVersionOut | undefined>( + () => props.resource?.versions[props.resourceVersionIndex], +); +</script> + +<template> + <bootstrap-modal + :modal-id="props.modalId" + modal-label="Resource Version Info Modal" + size-modifier="lg" + > + <template #header + >Resource Version + <span v-if="resourceVersion">{{ resourceVersion.release }}</span> + </template> + <template #body> + <h5>Resource</h5> + <div class="mb-3 row"> + <div class="col-8"> + <label + for="resource-version-info-modal-resource-id" + class="form-label" + >ID</label + > + <div class="input-group"> + <input + type="text" + class="form-control" + id="resource-version-info-modal-resource-id" + readonly + :value="props.resource?.resource_id" + /> + <span class="input-group-text" + ><copy-to-clipboard-icon + :text="props.resource?.resource_id ?? ''" + /></span> + </div> + </div> + <div class="col-4"> + <label + for="resource-version-info-modal-resource-name" + class="form-label" + >Name</label + > + <div class="input-group"> + <input + type="text" + class="form-control" + id="resource-version-info-modal-resource-name" + readonly + :value="props.resource?.name" + /> + </div> + </div> + </div> + <div class="mb-3 row"> + <div class="col-8"> + <label + for="resource-version-info-modal-maintainer-id" + class="form-label" + >Maintainer ID</label + > + <div class="input-group"> + <input + type="text" + class="form-control" + id="resource-version-info-modal-maintainer-id" + readonly + :value="props.resource?.maintainer_id" + /> + <span class="input-group-text" + ><copy-to-clipboard-icon + :text="props.resource?.maintainer_id ?? ''" + /></span> + </div> + </div> + <div class="col-4"> + <label + for="resource-version-info-modal-maintainer-name" + class="form-label" + >Maintainer Name</label + > + <div class="input-group"> + <input + type="text" + class="form-control" + id="resource-version-info-modal-maintainer-name" + readonly + :value="nameRepository.getName(props.resource?.maintainer_id)" + /> + </div> + </div> + </div> + <div class="mb-3"> + <label + for="resource-version-info-modal-resource-description" + class="form-label" + >Description</label + > + <div class="input-group"> + <textarea + class="form-control" + id="resource-version-info-modal-resource-description" + readonly + rows="2" + :value="props.resource?.description" + /> + </div> + </div> + <div class="mb-5"> + <label + for="resource-version-info-modal-resource-source" + class="form-label" + >Source</label + > + <div class="input-group"> + <input + type="text" + class="form-control" + id="resource-version-info-modal-resource-source" + readonly + :value="props.resource?.source" + /> + </div> + </div> + <h5>Resource Version</h5> + <div class="mb-3 row"> + <div class="col-8"> + <label + for="resource-version-info-modal-resource-version-id" + class="form-label" + >Resource Version ID</label + > + <div class="input-group"> + <input + type="text" + class="form-control" + id="resource-version-info-modal-resource-version-id" + readonly + :value="resourceVersion?.resource_version_id" + /> + <span class="input-group-text" + ><copy-to-clipboard-icon + :text="resourceVersion?.resource_version_id ?? ''" + /></span> + </div> + </div> + <div class="col-4"> + <label + for="resource-version-info-modal-resource-version-release" + class="form-label" + >Resource Version Release</label + > + <div class="input-group"> + <input + type="text" + class="form-control" + id="resource-version-info-modal-resource-version-release" + readonly + :value="resourceVersion?.release" + /> + </div> + </div> + </div> + <div class="mb-3 row"> + <div class="col-4"> + <label + for="resource-version-info-modal-resource-version-status" + class="form-label" + >Status</label + > + <div class="input-group"> + <input + type="text" + class="form-control" + id="resource-version-info-modal-resource-version-status" + readonly + :value="resourceVersion?.status" + /> + </div> + </div> + <div class="col-4"> + <label + for="resource-version-info-modal-resource-version-timestamp" + class="form-label" + >Created At</label + > + <div class="input-group"> + <input + type="datetime-local" + class="form-control" + id="resource-version-info-modal-resource-version-timestamp" + readonly + :value=" + dayjs + .unix(resourceVersion?.created_at ?? 0) + .format('YYYY-MM-DDTHH:mm') + " + /> + </div> + </div> + </div> + <div class="mb-3"> + <label + for="resource-version-info-modal-resource-version-s3-path" + class="form-label" + >S3 Path</label + > + <div class="input-group"> + <input + type="text" + class="form-control" + id="resource-version-info-modal-resource-version-s3-path" + readonly + :value="resourceVersion?.s3_path" + /> + <span class="input-group-text" + ><copy-to-clipboard-icon :text="resourceVersion?.s3_path ?? ''" + /></span> + </div> + </div> + <div class="mb-3"> + <label + for="resource-version-info-modal-resource-version-cluster-path" + class="form-label" + >Cluster Path</label + > + <div class="input-group"> + <input + type="text" + class="form-control" + id="resource-version-info-modal-resource-version-cluster-path" + readonly + :value="resourceVersion?.cluster_path" + /> + <span class="input-group-text" + ><copy-to-clipboard-icon + :text="resourceVersion?.cluster_path ?? ''" + /></span> + </div> + </div> + </template> + </bootstrap-modal> +</template> + +<style scoped></style> diff --git a/src/components/modals/BootstrapModal.vue b/src/components/modals/BootstrapModal.vue index 93e2a516247693235ce2f35e8001cd6c250d0a9e..bb4940543ce0dbab9669e28f299d0a443ab74989 100644 --- a/src/components/modals/BootstrapModal.vue +++ b/src/components/modals/BootstrapModal.vue @@ -2,7 +2,7 @@ import { computed } from "vue"; const props = defineProps<{ - modalID: string; + modalId: string; modalLabel: string; staticBackdrop?: boolean; sizeModifier?: string; // https://getbootstrap.com/docs/5.3/components/modal/#optional-sizes, e.g. sm, lg and xl @@ -19,7 +19,7 @@ const modalSizeClass = computed<string>(() => { <template> <div class="modal" - :id="modalID" + :id="modalId" tabindex="-1" :aria-labelledby="modalLabel" aria-hidden="true" diff --git a/src/components/modals/DeleteModal.vue b/src/components/modals/DeleteModal.vue index b51c55f02d5f6c357fc758f0d5311cc453eaa30c..0a7c662f06a0e2e73e43cf3d81b8719d846c54ed 100644 --- a/src/components/modals/DeleteModal.vue +++ b/src/components/modals/DeleteModal.vue @@ -34,7 +34,7 @@ onMounted(() => { <template> <bootstrap-modal - :modalID="props.modalID" + :modalId="props.modalID" :static-backdrop="true" modal-label="Confirm Delete Modal" v-on="{ 'hidden.bs.modal': modalClosed }" diff --git a/src/components/modals/SearchUserModal.vue b/src/components/modals/SearchUserModal.vue index 56c269a501af76531de09aa60dd1553571f0f0f6..414369508f1be90c59a4dab83446ddddb89450b7 100644 --- a/src/components/modals/SearchUserModal.vue +++ b/src/components/modals/SearchUserModal.vue @@ -1,5 +1,5 @@ <script setup lang="ts"> -import { reactive, watch } from "vue"; +import { reactive, ref, watch } from "vue"; import BootstrapModal from "@/components/modals/BootstrapModal.vue"; import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; import { UserService } from "@/client/auth"; @@ -8,13 +8,15 @@ import { useAuthStore } from "@/stores/users"; import { useNameStore } from "@/stores/names"; const props = defineProps<{ - modalID: string; + modalId: string; backModalId?: string; + filterUserSelf?: boolean; }>(); const randomIDSuffix = Math.random().toString(16).substring(2, 8); const store = useAuthStore(); const nameStore = useNameStore(); +const textInputElement = ref<HTMLInputElement | undefined>(undefined); const formState = reactive<{ searchString: string; @@ -55,13 +57,21 @@ function modalClosed() { formState.potentialUsers = []; } +function modalShown() { + textInputElement.value?.focus(); +} + function searchUser(name: string) { formState.error = false; UserService.userListUsers(name) .then((userSuggestions) => { - formState.potentialUsers = userSuggestions.filter( - (user) => store.currentUID != user.uid, - ); + if (props.filterUserSelf) { + formState.potentialUsers = userSuggestions.filter( + (user) => store.currentUID != user.uid, + ); + } else { + formState.potentialUsers = userSuggestions; + } for (const user of userSuggestions) { nameStore.addNameToMapping(user.uid, user.display_name); } @@ -78,10 +88,10 @@ function searchUser(name: string) { <template> <bootstrap-modal - :modalID="props.modalID" + :modalId="props.modalId" :static-backdrop="true" modal-label="Search User Modal" - v-on="{ 'hidden.bs.modal': modalClosed }" + v-on="{ 'hidden.bs.modal': modalClosed, 'shown.bs.modal': modalShown }" > <template v-slot:header>Search User</template> <template v-slot:body> @@ -94,6 +104,7 @@ function searchUser(name: string) { :id="'searchUserInput' + randomIDSuffix" placeholder="Search for a user" v-model.trim="formState.searchString" + ref="textInputElement" /> </div> <div v-if="formState.loading" class="text-center"> @@ -118,8 +129,9 @@ function searchUser(name: string) { :key="user.uid" type="button" class="list-group-item list-group-item-action" - :data-bs-target="'#' + props.backModalId" - data-bs-toggle="modal" + :data-bs-target="props.backModalId ? '#' + props.backModalId : null" + :data-bs-toggle="props.backModalId ? 'modal' : null" + :data-bs-dismiss="props.backModalId ? null : 'modal'" @click="emit('user-found', user)" > {{ user.display_name }} diff --git a/src/components/object-storage/modals/BucketDetailModal.vue b/src/components/object-storage/modals/BucketDetailModal.vue index 09788df9d94e1e290c501bc98cc7e53aadbf1bd5..49eef5918ab7e1e2f26d7a49e876d9a95f386be1 100644 --- a/src/components/object-storage/modals/BucketDetailModal.vue +++ b/src/components/object-storage/modals/BucketDetailModal.vue @@ -12,7 +12,7 @@ const props = defineProps<{ <template> <bootstrap-modal - :modalID="modalID" + :modalId="modalID" :static-backdrop="false" modal-label="Bucket Detail Modal" > diff --git a/src/components/object-storage/modals/CopyObjectModal.vue b/src/components/object-storage/modals/CopyObjectModal.vue index b51e17dad74e5b6eeb21f8094126dc7f33cec902..68f8b3d1a386ca24b0b712836b728e7eeaec85d1 100644 --- a/src/components/object-storage/modals/CopyObjectModal.vue +++ b/src/components/object-storage/modals/CopyObjectModal.vue @@ -95,7 +95,7 @@ onMounted(() => { Try again later </bootstrap-toast> <bootstrap-modal - :modalID="modalID" + :modalId="modalID" :static-backdrop="true" modal-label="Copy Object Modal" v-on="{ 'hidden.bs.modal': modalClosed }" diff --git a/src/components/object-storage/modals/CreateBucketModal.vue b/src/components/object-storage/modals/CreateBucketModal.vue index ae030960e3ab025834d85ba26ef3f56a17d297a4..9de88c11559748cd7fc22d701c42ef294b879af9 100644 --- a/src/components/object-storage/modals/CreateBucketModal.vue +++ b/src/components/object-storage/modals/CreateBucketModal.vue @@ -82,7 +82,7 @@ function modalClosed() { <template> <bootstrap-modal - :modalID="modalID" + :modalId="modalID" :static-backdrop="true" modal-label="Create Bucket Modal" v-on="{ 'hidden.bs.modal': modalClosed }" diff --git a/src/components/object-storage/modals/CreateFolderModal.vue b/src/components/object-storage/modals/CreateFolderModal.vue index 12400a92ddfd07598ff23c6f69779616f207fc1a..7791c9c7a62de44f7b7e290a7be4d0b2fbd9c76d 100644 --- a/src/components/object-storage/modals/CreateFolderModal.vue +++ b/src/components/object-storage/modals/CreateFolderModal.vue @@ -79,7 +79,7 @@ onMounted(() => { Try again later </bootstrap-toast> <bootstrap-modal - :modalID="modalID" + :modalId="modalID" :static-backdrop="true" modal-label="Create Folder Modal" > diff --git a/src/components/object-storage/modals/ObjectDetailModal.vue b/src/components/object-storage/modals/ObjectDetailModal.vue index 08f5280a86aac2400e20909bae8723377ac92916..c1c2eaf0b1f2006db22acb8f893daf221af573c8 100644 --- a/src/components/object-storage/modals/ObjectDetailModal.vue +++ b/src/components/object-storage/modals/ObjectDetailModal.vue @@ -53,7 +53,7 @@ onMounted(() => { <template> <bootstrap-modal - :modalID="modalID" + :modalId="modalID" :static-backdrop="false" modal-label="Object Detail Modal" > diff --git a/src/components/object-storage/modals/PermissionListModal.vue b/src/components/object-storage/modals/PermissionListModal.vue index 1d5c4d020a74e2bf737026c03ad5610cb095b41e..8d74a24c67c91f2a10dfa910c6f522e94c4c1cf6 100644 --- a/src/components/object-storage/modals/PermissionListModal.vue +++ b/src/components/object-storage/modals/PermissionListModal.vue @@ -6,8 +6,13 @@ import { onBeforeMount, watch } from "vue"; import BootstrapModal from "@/components/modals/BootstrapModal.vue"; import PermissionModal from "@/components/object-storage/modals/PermissionModal.vue"; import { useBucketStore } from "@/stores/buckets"; +import { useNameStore } from "@/stores/names"; +import { useAuthStore } from "@/stores/users"; const bucketRepository = useBucketStore(); +const nameRepository = useNameStore(); +const userRepository = useAuthStore(); + // Props // ----------------------------------------------------------------------------- const props = defineProps<{ @@ -46,7 +51,9 @@ watch( // Function // ----------------------------------------------------------------------------- function updateBucketPermissions(bucketName: string) { - bucketRepository.fetchBucketPermissions(bucketName); + bucketRepository.fetchBucketPermissions(bucketName).then((permissions) => { + userRepository.fetchUsernames(permissions.map((p) => p.uid)); + }); } // Lifecycle Hooks @@ -68,7 +75,7 @@ onBeforeMount(() => { :modalID="'permission-list-edit-modal' + randomIDSuffix" /> <bootstrap-modal - :modalID="props.modalID" + :modalId="props.modalID" :static-backdrop="true" modal-label="Permission List Modal" > @@ -99,7 +106,7 @@ onBeforeMount(() => { permission.permission }}</span> <span class="col-9 text-center"> - {{ permission.grantee_display_name }}</span + {{ nameRepository.getName(permission.uid) }}</span > </div> </button> diff --git a/src/components/object-storage/modals/PermissionModal.vue b/src/components/object-storage/modals/PermissionModal.vue index 4ce35b44058b6658ec624818b2f30e85736fdd0a..846aad4522e2f640ef46e01b4f6eec3086a798df 100644 --- a/src/components/object-storage/modals/PermissionModal.vue +++ b/src/components/object-storage/modals/PermissionModal.vue @@ -17,6 +17,7 @@ import { Toast } from "bootstrap"; import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; import { useBucketStore } from "@/stores/buckets"; import BootstrapToast from "@/components/BootstrapToast.vue"; +import { useNameStore } from "@/stores/names"; // Props // ----------------------------------------------------------------------------- @@ -32,6 +33,7 @@ const props = defineProps<{ }>(); const bucketRepository = useBucketStore(); +const nameRepository = useNameStore(); const emit = defineEmits<{ (e: "permission-deleted"): void }>(); // Variables @@ -45,12 +47,10 @@ let successToast: Toast | null = null; // eslint-disable-next-line vue/no-setup-props-destructure const formState = reactive<{ loading: boolean; - grantee_name: string; error: boolean; readonly: boolean; }>({ loading: false, - grantee_name: "", error: false, readonly: props.readonly, }); @@ -133,7 +133,6 @@ function updateLocalPermission() { permission.bucket_name = props.editUserPermission.bucket_name; permission.file_prefix = props.editUserPermission.file_prefix; permission.uid = props.editUserPermission.uid; - formState.grantee_name = props.editUserPermission.grantee_display_name; permission.from_timestamp = props.editUserPermission.from_timestamp; permission.to_timestamp = props.editUserPermission.to_timestamp; permission.permission = props.editUserPermission.permission; @@ -143,7 +142,6 @@ function updateLocalPermission() { permission.from_timestamp = undefined; permission.to_timestamp = undefined; permission.permission = undefined; - formState.grantee_name = ""; } } @@ -234,7 +232,6 @@ function confirmedDeletePermission(bucketName: string, uid: string) { function updateUser(user: User) { permission.uid = user.uid; - formState.grantee_name = user.display_name; } // Lifecycle Hooks @@ -267,9 +264,10 @@ function toTimestampChanged(target?: HTMLInputElement | null) { confirmedDeletePermission(permission.bucket_name, permission.uid) " /> - <SearchUserModal - :modalID="'search-user-modal' + randomIDSuffix" + <search-user-modal + :modal-id="'search-user-modal' + randomIDSuffix" :back-modal-id="modalID" + filter-user-self @user-found="updateUser" /> <bootstrap-toast @@ -283,7 +281,7 @@ function toTimestampChanged(target?: HTMLInputElement | null) { Permission </bootstrap-toast> <bootstrap-modal - :modalID="modalID" + :modalId="modalID" :static-backdrop="true" modal-label="Permission Modal" v-on="{ 'hidden.bs.modal': modalClosed }" @@ -336,32 +334,23 @@ function toTimestampChanged(target?: HTMLInputElement | null) { <label for="permissionGranteeInput" class="col-2 col-form-label"> User<span v-if="!formState.readonly">*</span> </label> - <div - :class="{ - 'col-10': permissionUserReadonly, - 'col-9': !permissionUserReadonly, - }" - > + <div class="col-10"> <input type="text" class="form-control" id="permissionGranteeInput" required placeholder="Search for a user" - v-model.trim="formState.grantee_name" + :value="nameRepository.getName(permission.uid)" readonly + :data-bs-toggle="permissionUserReadonly ? null : 'modal'" + :data-bs-target=" + permissionUserReadonly + ? null + : '#search-user-modal' + randomIDSuffix + " /> </div> - <div v-if="!permissionUserReadonly" class="col-1"> - <button - type="button" - class="btn btn-secondary btn-sm float-end" - data-bs-toggle="modal" - :data-bs-target="'#search-user-modal' + randomIDSuffix" - > - <font-awesome-icon icon="fa-solid fa-magnifying-glass" /> - </button> - </div> </div> <div class="mb-3 row"> <label for="permissionTypeInput" class="col-3 col-form-label"> diff --git a/src/components/object-storage/modals/UploadObjectModal.vue b/src/components/object-storage/modals/UploadObjectModal.vue index a6ec1f38483c07d5132be7674cb7fc9de31705d9..331fbf62a255527b1e1220c4d4b9db089eb554c6 100644 --- a/src/components/object-storage/modals/UploadObjectModal.vue +++ b/src/components/object-storage/modals/UploadObjectModal.vue @@ -113,7 +113,7 @@ onMounted(() => { Try again later </bootstrap-toast> <bootstrap-modal - :modalID="modalID" + :modalId="modalID" :static-backdrop="true" modal-label="Upload Object Modal" > diff --git a/src/components/resources/modals/CreateResourceModal.vue b/src/components/resources/modals/CreateResourceModal.vue index 73dff643642779f6db6ea04235e1571fbc5a711d..93f6cab3fb1e9395ab03d9c945daebfad117ef56 100644 --- a/src/components/resources/modals/CreateResourceModal.vue +++ b/src/components/resources/modals/CreateResourceModal.vue @@ -88,7 +88,7 @@ onMounted(() => { <template> <bootstrap-modal - :modalID="modalID" + :modalId="modalID" static-backdrop modal-label="Create Resource Modal" v-on="{ 'hidden.bs.modal': modalClosed }" diff --git a/src/components/resources/modals/UpdateResourceModal.vue b/src/components/resources/modals/UpdateResourceModal.vue index f40a8d1f4a3268abcb434fdd20a96cb580614b08..f0e73c341af5384ae0b64216b26eb02a86e782c9 100644 --- a/src/components/resources/modals/UpdateResourceModal.vue +++ b/src/components/resources/modals/UpdateResourceModal.vue @@ -60,7 +60,7 @@ onMounted(() => { <template> <bootstrap-modal - :modalID="modalId" + :modalId="modalId" static-backdrop modal-label="Update Resource Modal" v-on="{ 'hidden.bs.modal': modalClosed }" diff --git a/src/components/resources/modals/UploadResourceInfoModal.vue b/src/components/resources/modals/UploadResourceInfoModal.vue index 5d72c70b9be927fa586bfb3cca16eb778bc03771..10463ea778ce2f0f11991e61470337f786da86aa 100644 --- a/src/components/resources/modals/UploadResourceInfoModal.vue +++ b/src/components/resources/modals/UploadResourceInfoModal.vue @@ -134,7 +134,7 @@ onMounted(() => { <template> <bootstrap-modal - :modalID="props.modalId" + :modalId="props.modalId" modal-label="Upload Resource Info Modal" sizeModifier="lg" > diff --git a/src/components/workflows/modals/ArbitraryWorkflowModal.vue b/src/components/workflows/modals/ArbitraryWorkflowModal.vue index 18dedf30b4120162f9e23b1d71bc5e1d0536cc1a..c33d00d11f856eeb47d9f0dba00045f31215723a 100644 --- a/src/components/workflows/modals/ArbitraryWorkflowModal.vue +++ b/src/components/workflows/modals/ArbitraryWorkflowModal.vue @@ -186,7 +186,7 @@ onMounted(() => { <template> <bootstrap-modal - :modalID="modalID" + :modalId="modalID" :static-backdrop="false" modal-label="Create Workflow Modal" v-on="{ 'hidden.bs.modal': modalClosed }" diff --git a/src/components/workflows/modals/CreateWorkflowModal.vue b/src/components/workflows/modals/CreateWorkflowModal.vue index 29c41c3d872c296de6de777bc4181336c41ddd0a..3d7d1bcca8a6ed80cb6532d6c2b3628d52897e04 100644 --- a/src/components/workflows/modals/CreateWorkflowModal.vue +++ b/src/components/workflows/modals/CreateWorkflowModal.vue @@ -324,7 +324,7 @@ onMounted(() => { Successfully created Workflow </bootstrap-toast> <bootstrap-modal - :modalID="modalID" + :modalId="modalID" :static-backdrop="true" modal-label="Create Workflow Modal" v-on="{ 'hidden.bs.modal': modalClosed }" diff --git a/src/components/workflows/modals/ParameterModal.vue b/src/components/workflows/modals/ParameterModal.vue index 00dc11ee4c904b8988840e3480b4ed20b2dc7958..76f0d35f2ae516fd616ec0baf486931dc183fd79 100644 --- a/src/components/workflows/modals/ParameterModal.vue +++ b/src/components/workflows/modals/ParameterModal.vue @@ -139,7 +139,7 @@ onMounted(() => { <template> <bootstrap-modal - :modalID="modalID" + :modalId="modalID" :static-backdrop="false" modal-label="Workflow Execution Parameters Modal" > diff --git a/src/components/workflows/modals/UpdateWorkflowCredentialsModal.vue b/src/components/workflows/modals/UpdateWorkflowCredentialsModal.vue index 130f275792f865f75104b080ecb3b2a37bc6b9b9..ea0a1d22b232ebe9b4fb9e4186dffb73e3e3a95c 100644 --- a/src/components/workflows/modals/UpdateWorkflowCredentialsModal.vue +++ b/src/components/workflows/modals/UpdateWorkflowCredentialsModal.vue @@ -136,7 +136,7 @@ onMounted(() => { Successfully updated credentials </bootstrap-toast> <bootstrap-modal - :modalID="modalID" + :modalId="modalID" :static-backdrop="true" modal-label="Update Workflow Version Icon Modal" v-on="{ 'hidden.bs.modal': modalClosed }" diff --git a/src/components/workflows/modals/UpdateWorkflowModal.vue b/src/components/workflows/modals/UpdateWorkflowModal.vue index 78ca17fc52d89f9421472de599a8b836615ead4c..2961a8d5db685984b352fc55fb49f81b66bc88de 100644 --- a/src/components/workflows/modals/UpdateWorkflowModal.vue +++ b/src/components/workflows/modals/UpdateWorkflowModal.vue @@ -312,7 +312,7 @@ onMounted(() => { Successfully updated Workflow </bootstrap-toast> <bootstrap-modal - :modalID="modalID" + :modalId="modalID" :static-backdrop="true" modal-label="Update Workflow Modal" v-on="{ 'hidden.bs.modal': modalClosed }" diff --git a/src/components/workflows/modals/UpdateWorkflowVersionIconModal.vue b/src/components/workflows/modals/UpdateWorkflowVersionIconModal.vue index 420dfc32dab305f901663891a52bf6f6e6a77864..b820af5335b0021bfbd7538ce8e0c20d664e0d0c 100644 --- a/src/components/workflows/modals/UpdateWorkflowVersionIconModal.vue +++ b/src/components/workflows/modals/UpdateWorkflowVersionIconModal.vue @@ -146,7 +146,7 @@ onMounted(() => { <div v-else>Successfully deleted icon</div> </bootstrap-toast> <bootstrap-modal - :modalID="modalID" + :modalId="modalID" :static-backdrop="true" modal-label="Update Workflow Version IconModal" v-on="{ 'hidden.bs.modal': modalClosed }" diff --git a/src/router/adminRoutes.ts b/src/router/adminRoutes.ts new file mode 100644 index 0000000000000000000000000000000000000000..a7a4384781a425879efc121e17f1c2ea24253195 --- /dev/null +++ b/src/router/adminRoutes.ts @@ -0,0 +1,12 @@ +import type { RouteRecordRaw } from "vue-router"; + +export const adminRoutes: RouteRecordRaw[] = [ + { + path: "admin/resources", + name: "admin-resources", + component: () => import("../views/admin/AdminResourcesView.vue"), + meta: { + requiresAdminRole: true, + }, + }, +]; diff --git a/src/router/index.ts b/src/router/index.ts index 1154704c207ec7c034944a1e8858640413d14b42..21ae88be87f65a9832c6fcdbb7b83d8dc62a6214 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -4,6 +4,7 @@ import LoginView from "../views/LoginView.vue"; import { workflowRoutes } from "@/router/workflowRoutes"; import { s3Routes } from "@/router/s3Routes"; import { resourceRoutes } from "@/router/resourceRoutes"; +import { adminRoutes } from "@/router/adminRoutes"; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -12,7 +13,12 @@ const router = createRouter({ path: "/dashboard", name: "dashboard", component: DashboardView, - children: [...resourceRoutes, ...s3Routes, ...workflowRoutes], + children: [ + ...resourceRoutes, + ...s3Routes, + ...workflowRoutes, + ...adminRoutes, + ], }, { path: "/login", diff --git a/src/router/resourceRoutes.ts b/src/router/resourceRoutes.ts index 6b2ed067d9693568979aa2670847191ba3d3297e..69cd0cffc0282ff9dc6063bc6bd47e551bd50178 100644 --- a/src/router/resourceRoutes.ts +++ b/src/router/resourceRoutes.ts @@ -7,13 +7,19 @@ export const resourceRoutes: RouteRecordRaw[] = [ component: () => import("../views/resources/ListResourcesView.vue"), }, { - path: "developer/resources", + path: "maintainer/resources", name: "resource-maintainer", component: () => import("../views/resources/MyResourcesView.vue"), + meta: { + requiresMaintainerRole: true, + }, }, { path: "reviewer/resources", name: "resource-review", component: () => import("../views/resources/ReviewResourceView.vue"), + meta: { + requiresReviewerRole: true, + }, }, ]; diff --git a/src/stores/names.ts b/src/stores/names.ts index 4dc30f76e0e34b41de204796741a41b5a5277eed..e2b76aa70e58f926db013beb7b5f106950fd6cdd 100644 --- a/src/stores/names.ts +++ b/src/stores/names.ts @@ -9,9 +9,13 @@ export const useNameStore = defineStore({ nameMapping: Record<string, string>; }, getters: { - getName(): (objectID: string) => string | undefined { - return (objectID) => - this.nameMapping[objectID] ?? localStorage.getItem(objectID); + getName(): (objectID?: string) => string | undefined { + return (objectID) => { + if (objectID) { + return this.nameMapping[objectID] ?? localStorage.getItem(objectID); + } + return undefined; + }; }, }, actions: { diff --git a/src/stores/resources.ts b/src/stores/resources.ts index 986b90f7f7f0020be830ac6b0de110f1453c9204..662c5139bfc64336aab6c478bfc568adfd9ce6bb 100644 --- a/src/stores/resources.ts +++ b/src/stores/resources.ts @@ -40,8 +40,25 @@ export const useResourceStore = defineStore({ fetchResources( maintainerId?: string, versionStatus?: Status[], + searchString?: string, ): Promise<ResourceOut[]> { - return ResourceService.resourceListResources(maintainerId, versionStatus); + return ResourceService.resourceListResources( + maintainerId, + versionStatus, + searchString, + ).then((resources) => { + const nameStore = useNameStore(); + for (const resource of resources) { + nameStore.addNameToMapping(resource.resource_id, resource.name); + for (const version of resource.versions) { + nameStore.addNameToMapping( + version.resource_version_id, + version.release, + ); + } + } + return resources; + }); }, fetchReviewableResources(onFinally?: () => void): Promise<ResourceOut[]> { if (Object.keys(this.reviewableResourceMapping).length > 0) { @@ -53,16 +70,8 @@ export const useResourceStore = defineStore({ ]) .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.reviewableResourceMapping = newMapping; return resources; @@ -76,16 +85,8 @@ export const useResourceStore = defineStore({ 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; @@ -125,16 +126,8 @@ export const useResourceStore = defineStore({ 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; diff --git a/src/views/admin/AdminResourcesView.vue b/src/views/admin/AdminResourcesView.vue new file mode 100644 index 0000000000000000000000000000000000000000..380187ca549819e46b7a5cf52e7dbb167515f2bf --- /dev/null +++ b/src/views/admin/AdminResourcesView.vue @@ -0,0 +1,245 @@ +<script setup lang="ts"> +import { useResourceStore } from "@/stores/resources"; +import { reactive } from "vue"; +import { + type ResourceOut, + Status, +} from "@/client/resource"; +import SearchUserModal from "@/components/modals/SearchUserModal.vue"; +import type { User } from "@/client/auth"; +import { useNameStore } from "@/stores/names"; +import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; +import dayjs from "dayjs"; +import ResourceVersionInfoModal from "@/components/admin/ResourceVersionInfoModal.vue"; + +const resourceRepository = useResourceStore(); +const nameRepository = useNameStore(); + +const resourceState = reactive<{ + loading: boolean; + resources: ResourceOut[]; + maintainerId: string; + searchString: string; + resourceStatus: Status[]; + inspectVersionIndex: number; + inspectResource?: ResourceOut; +}>({ + loading: false, + resources: [], + maintainerId: "", + searchString: "", + resourceStatus: [], + inspectVersionIndex: 0, + inspectResource: undefined, +}); + +function updateUser(user: User) { + resourceState.maintainerId = user.uid; +} + +function searchResources() { + resourceState.loading = true; + resourceRepository + .fetchResources( + resourceState.maintainerId ? resourceState.maintainerId : undefined, + resourceState.resourceStatus ? resourceState.resourceStatus : undefined, + resourceState.searchString ? resourceState.searchString : undefined, + ) + .then((resources) => { + resourceState.resources = resources; + }) + .finally(() => { + resourceState.loading = false; + }); +} + +function resetForm() { + resourceState.maintainerId = ""; + resourceState.searchString = ""; + resourceState.resourceStatus = []; + resourceState.resources = []; +} +</script> + +<template> + <search-user-modal + modal-id="admin-resource-search-user-modal" + @user-found="updateUser" + /> + <resource-version-info-modal + modal-id="admin-resource-version-info-modal" + :resource-version-index="resourceState.inspectVersionIndex" + :resource="resourceState.inspectResource" + /> + <div + class="row m-2 border-bottom mb-4 justify-content-between align-items-center" + > + <h2>Manage Resources</h2> + </div> + <form @submit.prevent="searchResources" id="admin-resource-search-form"> + <div class="d-flex justify-content-evenly align-content-center"> + <div class="mx-2"> + <label for="admin-resource-state-select" class="form-label" + >Status of Resource Versions</label + > + <select + v-model="resourceState.resourceStatus" + multiple + class="form-select mb-4 w-fit" + id="admin-resource-state-select" + > + <option v-for="state in Object.values(Status)" :key="state"> + {{ state }} + </option> + </select> + </div> + <div class="flex-fill mx-2"> + <label for="admin-resource-name-search" class="form-label" + >Name of the Resource</label + > + <div class="input-group"> + <input + id="admin-resource-name-search" + type="text" + class="form-control" + maxlength="32" + v-model="resourceState.searchString" + placeholder="Search for resource name" + /> + </div> + </div> + <div class="flex-fill mx-2"> + <label for="admin-resource-user-search" class="form-label" + >Name of the Maintainer</label + > + <div class="input-group"> + <div class="input-group-text"> + <font-awesome-icon icon="fa-solid fa-user" /> + </div> + <input + id="admin-resource-user-search" + type="text" + class="form-control" + readonly + :value="nameRepository.getName(resourceState.maintainerId)" + placeholder="Search for maintainer" + data-bs-toggle="modal" + data-bs-target="#admin-resource-search-user-modal" + /> + </div> + </div> + </div> + <button + type="submit" + class="btn btn-primary w-fit" + :disabled="resourceState.loading" + > + Search + </button> + <button type="button" class="btn-primary btn w-fit ms-4" @click="resetForm"> + Reset + </button> + </form> + <table class="table table-striped" v-if="resourceState.resources"> + <thead> + <tr> + <th scope="col"><b>Resource ID</b></th> + <th scope="col">Name</th> + <th scope="col">Source</th> + <th scope="col">Maintainer</th> + </tr> + </thead> + <tbody v-if="resourceState.resources.length === 0"> + <tr> + <td colspan="5" class="text-center fst-italic fw-light"> + Select a filter and search for Resources + </td> + </tr> + </tbody> + <tbody v-else> + <template + v-for="resource in resourceState.resources" + :key="resource.resource_id" + > + <tr> + <th scope="row">{{ resource.resource_id }}</th> + <td>{{ resource.name }}</td> + <td>{{ resource.source }}</td> + <td>{{ nameRepository.getName(resource.maintainer_id) }}</td> + </tr> + <tr> + <td colspan="5"> + <table class="table mb-0 table-hover"> + <thead> + <tr> + <th scope="col"><b>Version ID</b></th> + <th scope="col">Release</th> + <th scope="col">Created At</th> + <th scope="col">State</th> + <th scope="col" class="text-end">Action</th> + </tr> + </thead> + <tbody> + <tr + v-for="(version, index) in resource.versions" + :key="version.resource_version_id" + > + <th scope="row">{{ version.resource_version_id }}</th> + <th>{{ version.release }}</th> + <th> + {{ + dayjs + .unix(version.created_at) + .format("DD.MM.YYYY HH:mm:ss") + }} + </th> + <th>{{ version.status }}</th> + <th class="text-end"> + <div class="btn-group"> + <button + type="button" + class="btn btn-secondary" + data-bs-toggle="modal" + data-bs-target="#admin-resource-version-info-modal" + @click=" + resourceState.inspectResource = resource; + resourceState.inspectVersionIndex = index; + " + > + Inspect + </button> + <button + type="button" + class="btn btn-secondary dropdown-toggle dropdown-toggle-split" + data-bs-toggle="dropdown" + aria-expanded="false" + > + <span class="visually-hidden">Toggle Dropdown</span> + </button> + <ul class="dropdown-menu"> + <li><a class="dropdown-item" href="#">Action</a></li> + <li> + <a class="dropdown-item" href="#">Another action</a> + </li> + <li> + <a class="dropdown-item" href="#" + >Something else here</a + > + </li> + <li> + <a class="dropdown-item" href="#">Separated link</a> + </li> + </ul> + </div> + </th> + </tr> + </tbody> + </table> + </td> + </tr> + </template> + </tbody> + </table> +</template> + +<style scoped></style>