diff --git a/src/assets/main.css b/src/assets/main.css index 06d798ea4ea6f5af9cf9dce8a26eaf8bbdc7ba90..7024e78b25fc6d1c60c77ff425523d2a21fca9e4 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -6,3 +6,11 @@ body { .top-toast { top: 4rem; } + +.w-fit { + width: fit-content; +} + +.cursor-pointer { + cursor: pointer; +} diff --git a/src/components/NavbarTop.vue b/src/components/NavbarTop.vue index 949d38e3c3e7ea54d9e86467eefe61fa934be7e6..d8fbb2f5c5818102c073affce8c418c42ddbae96 100644 --- a/src/components/NavbarTop.vue +++ b/src/components/NavbarTop.vue @@ -142,7 +142,7 @@ watch( > <li><hr class="dropdown-divider" /></li> <li> - <a class="dropdown-item pseudo-link" @click="logout">Sign out</a> + <a class="dropdown-item cursor-pointer" @click="logout">Sign out</a> </li> </ul> </div> @@ -150,8 +150,4 @@ watch( </header> </template> -<style scoped> -.pseudo-link { - cursor: pointer; -} -</style> +<style scoped></style> diff --git a/src/components/object-storage/modals/PermissionModal.vue b/src/components/object-storage/modals/PermissionModal.vue index 0c61ba93d20d4531bca60a83cbf6a6a886d66a55..30048c8b331711f61bc284cc358b2dcbe2430b89 100644 --- a/src/components/object-storage/modals/PermissionModal.vue +++ b/src/components/object-storage/modals/PermissionModal.vue @@ -336,7 +336,7 @@ onMounted(() => { <bootstrap-icon v-if="props.deletable" icon="trash-fill" - class="me-2" + class="me-2 cursor-pointer" :class="{ 'delete-icon': !formState.loading }" data-bs-toggle="modal" :data-bs-target="'#delete-permission-modal' + randomIDSuffix" @@ -344,7 +344,7 @@ onMounted(() => { <bootstrap-icon v-if="formState.readonly && props.editable" icon="pencil-fill" - class="pseudo-link" + class="pseudo-link cursor-pointer" @click="formState.readonly = false" /> </template> @@ -547,7 +547,6 @@ onMounted(() => { <style scoped> .pseudo-link { - cursor: pointer; color: var(--bs-secondary); } .pseudo-link:hover { @@ -556,7 +555,6 @@ onMounted(() => { .delete-icon { color: var(--bs-secondary); - cursor: pointer; } .delete-icon:hover { color: var(--bs-danger); diff --git a/src/components/workflows/WorkflowCard.vue b/src/components/workflows/WorkflowCard.vue index 09826b6bad1425767fc2ee1e267b7811c78e4e67..42e31f131eb0547f04c642b08bfac8f229778c5d 100644 --- a/src/components/workflows/WorkflowCard.vue +++ b/src/components/workflows/WorkflowCard.vue @@ -30,9 +30,7 @@ onMounted(() => { </script> <template> - <div - class="card-hover border border-secondary card text-bg-dark m-3 align-self-center" - > + <div class="card-hover border border-secondary card text-bg-dark m-2"> <div class="card-body"> <h3 class="card-title"> <div v-if="props.loading" class="placeholder-glow"> @@ -48,7 +46,7 @@ onMounted(() => { v-else @click="truncateDescription = false" :class="{ - 'pointer-cursor': truncateDescription, + 'cursor-pointer': truncateDescription, }" >{{ props.workflow.short_description }}</span > @@ -91,14 +89,10 @@ onMounted(() => { <style scoped> .card-hover { transition: transform 0.3s ease-out; - width: 47%; + width: 48%; } .card-hover:hover { transform: translate(0, -5px); } - -.pointer-cursor { - cursor: pointer; -} </style> diff --git a/src/router/index.ts b/src/router/index.ts index 41c8d18437bf16f0fe9162147ce8cb94392057af..39acd6ecddd838a0a4346340f3dba5d803fe3db6 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -18,8 +18,7 @@ const router = createRouter({ { path: ":bucketName/:subFolders*", name: "bucket", - component: () => - import("../components/object-storage/BucketView.vue"), + component: () => import("../views/object-storage/BucketView.vue"), props: true, }, ], diff --git a/src/components/object-storage/BucketView.vue b/src/views/object-storage/BucketView.vue similarity index 100% rename from src/components/object-storage/BucketView.vue rename to src/views/object-storage/BucketView.vue diff --git a/src/components/object-storage/S3KeyView.vue b/src/views/object-storage/S3KeyView.vue similarity index 100% rename from src/components/object-storage/S3KeyView.vue rename to src/views/object-storage/S3KeyView.vue diff --git a/src/views/object-storage/S3KeysView.vue b/src/views/object-storage/S3KeysView.vue index b0d1c10b5b403cd252a21dedebd893a75ff037f6..42766a2e6c7c74142d5cfd5776a41aee760233e6 100644 --- a/src/views/object-storage/S3KeysView.vue +++ b/src/views/object-storage/S3KeysView.vue @@ -1,5 +1,5 @@ <script setup lang="ts"> -import S3KeyView from "@/components/object-storage/S3KeyView.vue"; +import S3KeyView from "@/views/object-storage/S3KeyView.vue"; import BootstrapIcon from "@/components/BootstrapIcon.vue"; import { reactive, onMounted, computed } from "vue"; import type { ComputedRef } from "vue"; diff --git a/src/views/workflows/ListWorkflowsView.vue b/src/views/workflows/ListWorkflowsView.vue index 8ea76021dd2424a80a1e8aa6fafd51ae90aacf89..211b92019f525f93620bb5137f834fb579b2cf3b 100644 --- a/src/views/workflows/ListWorkflowsView.vue +++ b/src/views/workflows/ListWorkflowsView.vue @@ -4,23 +4,53 @@ import type { ComputedRef } from "vue"; import { useWorkflowStore } from "@/stores/workflows"; import type { WorkflowOut } from "@/client/workflow"; import WorkflowCard from "@/components/workflows/WorkflowCard.vue"; +import dayjs from "dayjs"; +import BootstrapIcon from "@/components/BootstrapIcon.vue"; const workflowRepository = useWorkflowStore(); const workflowsState = reactive({ loading: true, filterString: "", + sortByAttribute: "name", + sortDesc: true, } as { loading: boolean; filterString: string; + sortByAttribute: string; + sortDesc: boolean; }); -const filteredWorkflows: ComputedRef<WorkflowOut[]> = computed(() => { +const bla: Record<string, (a: WorkflowOut, b: WorkflowOut) => boolean> = { + name: (a: WorkflowOut, b: WorkflowOut) => + workflowsState.sortDesc ? a.name > b.name : a.name < b.name, + release: (a: WorkflowOut, b: WorkflowOut) => { + const a_date = dayjs(a.versions[a.versions.length - 1].created_at); + const b_date = dayjs(b.versions[b.versions.length - 1].created_at); + return workflowsState.sortDesc + ? a_date.isBefore(b_date) + : a_date.isAfter(b_date); + }, +}; + +function filterWorkflowByString(workflow: WorkflowOut): boolean { return workflowsState.filterString.length > 0 - ? workflowRepository.workflows.filter((workflow) => - workflow.name.includes(workflowsState.filterString) - ) - : workflowRepository.workflows; + ? workflow.name.includes(workflowsState.filterString) + : true; +} + +function filterWorkflowWithoutVersion(workflow: WorkflowOut): boolean { + return workflow.versions.length > 0; +} + +const processedWorkflows: ComputedRef<WorkflowOut[]> = computed(() => { + return workflowRepository.workflows + .filter( + (workflow) => + filterWorkflowByString(workflow) && + filterWorkflowWithoutVersion(workflow) + ) + .sort((a, b) => (bla[workflowsState.sortByAttribute](a, b) ? 1 : -1)); }); onMounted(() => { @@ -33,17 +63,116 @@ onMounted(() => { </script> <template> - <div v-if="!workflowsState.loading" class="d-flex flex-wrap"> - <workflow-card - v-for="workflow in filteredWorkflows" - :key="workflow.workflow_id" - :workflow="workflow" - :loading="false" + <div class="row m-2 border-bottom border-light mb-4"> + <div class="col-12"></div> + <h1 class="mb-2 text-light">Workflows</h1> + </div> + <div class="d-flex m-2 mb-3 align-items-center justify-content-between"> + <div class="col-5 me-auto"> + <div class="input-group"> + <span class="input-group-text" id="workflows-search-wrapping" + ><bootstrap-icon icon="search" + /></span> + <input + type="text" + class="form-control" + placeholder="Filter Workflows" + aria-label="Filter Workflows" + aria-describedby="workflows-search-wrapping" + :disabled="workflowsState.loading" + v-model.trim="workflowsState.filterString" + maxlength="20" + /> + </div> + </div> + <span class="fs-5 me-3">Sort By</span> + <div + class="btn-group btn-group-sm w-fit" + role="group" + aria-label="Basic radio toggle button group" + > + <input + type="radio" + class="btn-check" + name="btnradio" + id="sortName" + autocomplete="off" + checked + v-model="workflowsState.sortByAttribute" + value="name" + /> + <label class="btn btn-outline-secondary" for="sortName" + >Alphabetical</label + > + + <input + type="radio" + class="btn-check" + name="btnradio" + id="sortLatestRelease" + autocomplete="off" + v-model="workflowsState.sortByAttribute" + value="release" + /> + <label class="btn btn-outline-secondary" for="sortLatestRelease" + >Latest Release</label + > + </div> + <bootstrap-icon + :icon="workflowsState.sortDesc ? 'sort-down' : 'sort-up'" + @click="workflowsState.sortDesc = !workflowsState.sortDesc" + class="fs-4 ms-3 cursor-pointer" /> </div> - <div v-else class="d-flex flex-wrap"> + <div v-if="!workflowsState.loading"> + <div + v-if="workflowRepository.workflows.length === 0" + class="text-center fs-2 mt-5" + > + <bootstrap-icon + icon="x-lg" + class="mb-5" + width="75" + height="75" + style="color: var(--bs-secondary)" + /> + <br /> + There are no workflows in the system. Please come again later. + </div> + <div + v-else-if="processedWorkflows.length === 0" + class="text-center fs-2 mt-5" + > + <bootstrap-icon + icon="search" + class="mb-5" + width="75" + height="75" + style="color: var(--bs-secondary)" + /> + <br /> + Could not find any Workflows containing<br />'{{ + workflowsState.filterString + }}' + </div> + <div + v-else + class="d-flex flex-wrap align-items-center justify-content-between" + > + <workflow-card + v-for="workflow in processedWorkflows" + :key="workflow.workflow_id" + :workflow="workflow" + :loading="false" + /> + </div> + </div> + <div + v-else + class="d-flex flex-wrap align-items-center justify-content-between" + > <workflow-card - v-for="workflow in 6" + v-for="workflow in 4" :key="workflow" :workflow="{ name: '',