diff --git a/src/App.vue b/src/App.vue index d6b5c6850bf365e03fdd5aae0bdc93b4d28ef4ec..405ef8993d04e67552ede81366ffc9f9dbdadab3 100644 --- a/src/App.vue +++ b/src/App.vue @@ -34,7 +34,7 @@ onBeforeMount(() => { <template> <NavbarTop /> - <div class="container"> + <div class="container mt-4"> <router-view></router-view> </div> </template> diff --git a/src/components/BootstrapIcon.vue b/src/components/BootstrapIcon.vue index 2722a662489ea097d2d2e5883f3a6c38bd4aec4a..7c57717af6ea57ba8608201fefd0bb6ebc5531f2 100644 --- a/src/components/BootstrapIcon.vue +++ b/src/components/BootstrapIcon.vue @@ -14,9 +14,9 @@ import iconPath from "bootstrap-icons/bootstrap-icons.svg"; const props = defineProps({ icon: { type: String, required: true }, - width: { type: Number, default: 24, required: false }, - height: { type: Number, default: 24, required: false }, - fill: { type: String, default: "black", required: false }, + width: { type: String, default: "1em", required: false }, + height: { type: String, default: "1em", required: false }, + fill: { type: String, default: "currentColor", required: false }, }); </script> diff --git a/src/components/NavbarTop.vue b/src/components/NavbarTop.vue index 81a6b2e3406253bbd54e7697caf514c460877a6d..949d38e3c3e7ea54d9e86467eefe61fa934be7e6 100644 --- a/src/components/NavbarTop.vue +++ b/src/components/NavbarTop.vue @@ -21,6 +21,9 @@ const activeRoute = ref(""); const objectStorageActive: ComputedRef<boolean> = computed( () => activeRoute.value == "buckets" || activeRoute.value == "s3_keys" ); +const workflowActive: ComputedRef<boolean> = computed( + () => activeRoute.value == "workflows" +); watch( () => route.name, @@ -28,10 +31,8 @@ watch( if (typeof to === "string") { if (to.startsWith("bucket")) { activeRoute.value = "buckets"; - } else if (to.startsWith("s3_keys")) { - activeRoute.value = "s3_keys"; } else { - activeRoute.value = ""; + activeRoute.value = to; } } else { activeRoute.value = ""; @@ -41,7 +42,9 @@ watch( </script> <template> - <header class="navbar navbar-expand-lg navbar-dark bd-navbar sticky-top"> + <header + class="navbar navbar-expand-lg navbar-dark bd-navbar bg-dark sticky-top border-bottom border-secondary" + > <nav class="container-xxl bd-gutter flex-wrap flex-lg-nowrap text-light"> <button class="navbar-toggler" @@ -85,18 +88,12 @@ watch( </a> <ul class="dropdown-menu dropdown-menu-dark"> <li> - <router-link - class="dropdown-item" - :to="{ name: 'buckets' }" - :class="{ active: activeRoute === 'buckets' }" + <router-link class="dropdown-item" :to="{ name: 'buckets' }" >Buckets</router-link > </li> <li> - <router-link - class="dropdown-item" - :to="{ name: 's3_keys' }" - :class="{ active: activeRoute === 's3_keys' }" + <router-link class="dropdown-item" :to="{ name: 's3_keys' }" >S3 Keys</router-link > </li> @@ -107,6 +104,7 @@ watch( <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" + :class="{ active: workflowActive }" href="#" role="button" data-bs-toggle="dropdown" @@ -116,7 +114,9 @@ watch( </a> <ul class="dropdown-menu dropdown-menu-dark"> <li> - <a class="dropdown-item" href="#">Workflows</a> + <router-link class="dropdown-item" :to="{ name: 'workflows' }" + >Workflows</router-link + > </li> <li> <a class="dropdown-item" href="#">Executions</a> @@ -134,7 +134,7 @@ watch( aria-expanded="false" > <strong class="me-2">{{ store.user.display_name }}</strong> - <bootstrap-icon icon="person-circle" fill="white" /> + <bootstrap-icon icon="person-circle" class="fs-4" /> </a> <ul class="dropdown-menu dropdown-menu-dark text-small shadow" diff --git a/src/components/modals/SearchUserModal.vue b/src/components/modals/SearchUserModal.vue index e17b35e14d8c9636c4675a8757c8ce66479e844c..658f095d05a274f202abd96deddf8952fa2f4504 100644 --- a/src/components/modals/SearchUserModal.vue +++ b/src/components/modals/SearchUserModal.vue @@ -82,7 +82,7 @@ function searchUser(name: string) { <template v-slot:body> <div class="input-group mt-2 mb-4"> <span class="input-group-text" id="objects-search-wrapping" - ><bootstrap-icon icon="search" :width="16" :height="16" + ><bootstrap-icon icon="search" /></span> <input class="form-control" @@ -100,10 +100,9 @@ function searchUser(name: string) { <bootstrap-icon icon="x-lg" class="mb-2" - :width="56" - :height="56" + width="56" + height="56" style="color: var(--bs-danger)" - fill="currentColor" /><br /> <span class="text-danger" >There seems to be an error<br />Try again later</span @@ -126,10 +125,9 @@ function searchUser(name: string) { <bootstrap-icon icon="search" class="mb-2" - :width="56" - :height="56" + width="56" + height="56" style="color: var(--bs-secondary)" - fill="currentColor" /><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 1412a89896a7bcc6eecccc74902062585ff48a93..bec5afd837f91d85c869443d293b6c8474a12ee1 100644 --- a/src/components/object-storage/BucketListItem.vue +++ b/src/components/object-storage/BucketListItem.vue @@ -92,9 +92,6 @@ onMounted(() => { v-if="props.active && permission == null && props.deletable" icon="trash-fill" class="delete-icon me-2" - :width="16" - :height="16" - fill="currentColor" @click="emit('delete-bucket', bucket.name)" /> <bootstrap-icon @@ -103,9 +100,6 @@ onMounted(() => { :data-bs-target="'#view-bucket-details-modal' + randomIDSuffix" v-if="props.active" icon="info-circle-fill" - :width="16" - :height="16" - fill="currentColor" /> </div> </router-link> diff --git a/src/components/object-storage/BucketView.vue b/src/components/object-storage/BucketView.vue index 86e21c8a16c19abc7081ef8e44b42a98d1c4d62d..7bd9e5e42fd2396d4c1144cb9aad2f726b5ebbc9 100644 --- a/src/components/object-storage/BucketView.vue +++ b/src/components/object-storage/BucketView.vue @@ -580,7 +580,7 @@ watch( <div class="col-8"> <div class="input-group mt-2"> <span class="input-group-text" id="objects-search-wrapping" - ><bootstrap-icon icon="search" :width="16" :height="16" + ><bootstrap-icon icon="search" /></span> <input type="text" @@ -603,7 +603,7 @@ watch( data-bs-title="Upload Object" data-bs-target="#upload-object-modal" > - <bootstrap-icon icon="upload" :width="16" :height="16" fill="white" /> + <bootstrap-icon icon="upload" fill="white" /> <span class="visually-hidden">Upload Object</span> </button> <upload-object-modal @@ -623,7 +623,7 @@ watch( data-bs-title="Create Folder" data-bs-target="#create-folder-modal" > - <bootstrap-icon icon="plus-lg" :width="16" :height="16" fill="white" /> + <bootstrap-icon icon="plus-lg" /> Folder <span class="visually-hidden">Add Folder</span> </button> @@ -645,12 +645,7 @@ watch( data-bs-title="Create Bucket Permission" data-bs-target="#create-permission-modal" > - <bootstrap-icon - icon="person-plus-fill" - :width="16" - :height="16" - fill="white" - /> + <bootstrap-icon icon="person-plus-fill" /> <span class="visually-hidden">Add Bucket Permission</span> </button> <permission-modal @@ -677,12 +672,7 @@ watch( data-bs-toggle="modal" data-bs-target="#permission-list-modal" > - <bootstrap-icon - icon="person-lines-fill" - :width="16" - :height="16" - fill="white" - /> + <bootstrap-icon icon="person-lines-fill" /> <span class="visually-hidden">View Bucket Permissions</span> </button> <permission-list-modal @@ -704,10 +694,9 @@ watch( <bootstrap-icon icon="search" class="mb-3" - :width="64" - :height="64" + width="64" + height="64" style="color: var(--bs-secondary)" - fill="currentColor" /> <p> Bucket <i>{{ props.bucketName }}</i> not found @@ -721,10 +710,9 @@ watch( <bootstrap-icon icon="folder-x" class="mb-3" - :width="64" - :height="64" + width="64" + height="64" style="color: var(--bs-secondary)" - fill="currentColor" /> <p>You don't have permission for this bucket</p> </div> @@ -869,13 +857,7 @@ watch( data-bs-target="#delete-object-modal" :disabled="!writableBucket" > - <bootstrap-icon - icon="trash-fill" - class="text-danger" - :width="13" - :height="13" - fill="currentColor" - /> + <bootstrap-icon icon="trash-fill" /> <span class="ms-1">Delete</span> </button> </li> @@ -885,7 +867,7 @@ watch( <div v-else> <button type="button" - class="btn btn-danger btn-sm align-middle" + class="btn btn-danger btn-sm align-baseline" :disabled="!writableBucket" data-bs-toggle="modal" data-bs-target="#delete-object-modal" @@ -895,13 +877,7 @@ watch( ) " > - <bootstrap-icon - icon="trash-fill" - class="text-danger me-2" - :width="12" - :height="12" - fill="white" - /> + <bootstrap-icon icon="trash-fill" class="me-2" /> <span>Delete</span> </button> </div> diff --git a/src/components/object-storage/S3KeyView.vue b/src/components/object-storage/S3KeyView.vue index 1f29e7ed3c85e875b82fb621c45e251f382e56ba..79853162da1bbd0ed5e799f2bf37e179ee83d3ed 100644 --- a/src/components/object-storage/S3KeyView.vue +++ b/src/components/object-storage/S3KeyView.vue @@ -60,9 +60,7 @@ function deleteKeyTrigger() { @click="visibleSecret = !visibleSecret" > <bootstrap-icon - :width="18" - :height="18" - fill="white" + class="fs-5" :icon="visibleSecret ? 'eye' : 'eye-slash'" /> </button> diff --git a/src/components/object-storage/modals/PermissionModal.vue b/src/components/object-storage/modals/PermissionModal.vue index ddc289951059108f1f8066d5e98e56c2aa26adcb..0c61ba93d20d4531bca60a83cbf6a6a886d66a55 100644 --- a/src/components/object-storage/modals/PermissionModal.vue +++ b/src/components/object-storage/modals/PermissionModal.vue @@ -336,9 +336,6 @@ onMounted(() => { <bootstrap-icon v-if="props.deletable" icon="trash-fill" - :height="15" - :width="15" - fill="currentColor" class="me-2" :class="{ 'delete-icon': !formState.loading }" data-bs-toggle="modal" @@ -347,9 +344,6 @@ onMounted(() => { <bootstrap-icon v-if="formState.readonly && props.editable" icon="pencil-fill" - :height="15" - :width="15" - fill="currentColor" class="pseudo-link" @click="formState.readonly = false" /> @@ -401,12 +395,7 @@ onMounted(() => { data-bs-toggle="modal" :data-bs-target="'#search-user-modal' + randomIDSuffix" > - <bootstrap-icon - icon="search" - :height="13" - :width="13" - fill="white" - /> + <bootstrap-icon icon="search" /> </button> </div> </div> @@ -510,12 +499,7 @@ onMounted(() => { @click="permission.file_prefix = undefined" :hidden="permission.file_prefix == undefined" > - <bootstrap-icon - icon="x-lg" - :height="14" - :width="14" - fill="currentColor" - /> + <bootstrap-icon icon="x-lg" /> </button> </div> </div> diff --git a/src/components/workflows/WorkflowCard.vue b/src/components/workflows/WorkflowCard.vue new file mode 100644 index 0000000000000000000000000000000000000000..09826b6bad1425767fc2ee1e267b7811c78e4e67 --- /dev/null +++ b/src/components/workflows/WorkflowCard.vue @@ -0,0 +1,104 @@ +<script setup lang="ts"> +import type { WorkflowOut, WorkflowVersionReduced } from "@/client/workflow"; +import BootstrapIcon from "@/components/BootstrapIcon.vue"; +import dayjs from "dayjs"; +import { onMounted, ref, computed } from "vue"; +import type { Ref, ComputedRef } from "vue"; +import { Tooltip } from "bootstrap"; +import { useWorkflowStore } from "@/stores/workflows"; + +const props = defineProps<{ + workflow: WorkflowOut; + loading: boolean; +}>(); +const workflowRepository = useWorkflowStore(); + +const randomIDSuffix: string = Math.random().toString(16).substr(2, 8); +const truncateDescription: Ref<boolean> = ref(true); +const latestVersion: ComputedRef<WorkflowVersionReduced | undefined> = computed( + () => + props.loading + ? undefined + : workflowRepository.latestVersion(props.workflow.workflow_id) +); + +onMounted(() => { + if (!props.loading) { + new Tooltip("#creationDate-" + randomIDSuffix); + } +}); +</script> + +<template> + <div + class="card-hover border border-secondary card text-bg-dark m-3 align-self-center" + > + <div class="card-body"> + <h3 class="card-title"> + <div v-if="props.loading" class="placeholder-glow"> + <span class="placeholder col-6"></span> + </div> + <a v-else href="#">{{ props.workflow.name }}</a> + </h3> + <p class="card-text" :class="{ 'text-truncate': truncateDescription }"> + <span v-if="props.loading" class="placeholder-glow" + ><span class="placeholder col-12"></span + ></span> + <span + v-else + @click="truncateDescription = false" + :class="{ + 'pointer-cursor': truncateDescription, + }" + >{{ props.workflow.short_description }}</span + > + </p> + <div class="d-flex justify-content-between mb-0"> + <div v-if="props.loading" class="placeholder-glow w-50"> + <span class="placeholder placeholder-lg w-50 bg-success"></span> + </div> + <a + v-else + :href=" + workflow.repository_url + '/tree/' + latestVersion?.git_commit_hash + " + target="_blank" + class="btn btn-outline-success" + role="button" + > + <bootstrap-icon class="fs-5" icon="tag-fill" /> + {{ latestVersion?.version }} + </a> + <div v-if="props.loading" class="placeholder-glow w-25"> + <span class="placeholder w-100 bg-secondary"></span> + </div> + <span + v-else + :id="'creationDate-' + randomIDSuffix" + data-bs-toggle="tooltip" + class="align-self-end text-secondary" + :data-bs-title=" + dayjs(workflow.versions[0].created_at).format('DD.MM.YYYY HH:mm:ss') + " + > + {{ dayjs(latestVersion?.created_at).fromNow() }} + </span> + </div> + </div> + </div> +</template> + +<style scoped> +.card-hover { + transition: transform 0.3s ease-out; + width: 47%; +} + +.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 9dbda65734c162aff7ae181d09b704a5ba1bbd3d..41c8d18437bf16f0fe9162147ce8cb94392057af 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -29,6 +29,11 @@ const router = createRouter({ name: "s3_keys", component: () => import("../views/object-storage/S3KeysView.vue"), }, + { + path: "workflows", + name: "workflows", + component: () => import("../views/workflows/ListWorkflowsView.vue"), + }, ], }, { diff --git a/src/stores/workflows.ts b/src/stores/workflows.ts new file mode 100644 index 0000000000000000000000000000000000000000..a15e6c14efe1e0b81d4996d5876eba142fe42c15 --- /dev/null +++ b/src/stores/workflows.ts @@ -0,0 +1,47 @@ +import { defineStore } from "pinia"; +import { WorkflowService } from "@/client/workflow"; +import type { WorkflowVersionReduced } from "@/client/workflow"; +import type { WorkflowOut } from "@/client/workflow"; + +export const useWorkflowStore = defineStore({ + id: "workflows", + state: () => + ({ + workflows: [], + } as { + workflows: WorkflowOut[]; + }), + getters: { + latestVersion(): ( + workflowId: string + ) => WorkflowVersionReduced | undefined { + return (workflowId) => { + const workflow = this.workflows.find( + (w) => workflowId == w.workflow_id + ); + return workflow?.versions[ + Math.max(workflow?.versions?.length - 1 ?? 0, 0) + ]; + }; + }, + }, + actions: { + fetchWorkflows( + onFulfilled: + | ((workflows: WorkflowOut[]) => void) + | null + | undefined = null, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onRejected: ((reason: any) => void) | null | undefined = null, + onFinally: (() => void) | null | undefined = null + ) { + WorkflowService.workflowListWorkflows() + .then((workflows) => { + this.workflows = workflows; + onFulfilled?.(workflows); + }) + .catch(onRejected) + .finally(onFinally); + }, + }, +}); diff --git a/src/views/object-storage/BucketsView.vue b/src/views/object-storage/BucketsView.vue index 877761c706508cf06b32dee924f84252820e29b0..15fc3872513acb8065021a38bef24fa5abab7de4 100644 --- a/src/views/object-storage/BucketsView.vue +++ b/src/views/object-storage/BucketsView.vue @@ -71,7 +71,7 @@ onMounted(() => { modalID="create-bucket-modal" v-if="!authStore.foreignUser" /> - <div class="row m-2 border-bottom border-light mt-4"> + <div class="row m-2 border-bottom border-light"> <div class="col-12"></div> <h1 class="mb-2 text-light">Buckets</h1> </div> @@ -83,7 +83,7 @@ onMounted(() => { class="btn btn-light" @click.stop.prevent="fetchBuckets" > - <bootstrap-icon icon="arrow-clockwise" /> + <bootstrap-icon icon="arrow-clockwise" class="fs-5" /> <span class="visually-hidden">Refresh Buckets</span> </button> <button @@ -93,13 +93,13 @@ onMounted(() => { data-bs-toggle="modal" data-bs-target="#create-bucket-modal" > - <bootstrap-icon icon="plus-lg" /> + <bootstrap-icon icon="plus-lg" class="fs-5" /> <span class="visually-hidden">Create Bucket</span> </button> </div> <div class="input-group flex-nowrap mt-2"> <span class="input-group-text" id="buckets-search-wrapping" - ><bootstrap-icon icon="search" :width="16" :height="16" + ><bootstrap-icon icon="search" /></span> <input type="text" @@ -131,10 +131,9 @@ onMounted(() => { <bootstrap-icon icon="search" class="mb-2" - :width="56" - :height="56" + width="56" + height="56" style="color: var(--bs-secondary)" - fill="currentColor" /> <br /> Could not find any Buckets @@ -145,8 +144,7 @@ onMounted(() => { v-for="n in 5" :key="n" :active="false" - :loading="true" - :permission="undefined" + loading :deletable="false" :bucket="{ name: '', @@ -169,10 +167,9 @@ onMounted(() => { <bootstrap-icon icon="hand-index-thumb-fill" class="mb-5" - :width="64" - :height="64" + width="64" + height="64" style="color: var(--bs-secondary)" - fill="currentColor" /> <p>Click on a bucket to browse its content</p> </div> diff --git a/src/views/object-storage/S3KeysView.vue b/src/views/object-storage/S3KeysView.vue index d234f3294416817478138ce48fa58c15da6c3094..b0d1c10b5b403cd252a21dedebd893a75ff037f6 100644 --- a/src/views/object-storage/S3KeysView.vue +++ b/src/views/object-storage/S3KeysView.vue @@ -118,11 +118,11 @@ onMounted(() => { class="btn btn-light" @click="refreshKeys(authStore.user?.uid ?? 'impossible')" > - <bootstrap-icon icon="arrow-clockwise" /> + <bootstrap-icon icon="arrow-clockwise" class="fs-5" /> <span class="visually-hidden">Refresh S3 Keys</span> </button> <button type="button" class="btn btn-light" @click="createKey"> - <bootstrap-icon icon="plus-lg" /> + <bootstrap-icon icon="plus-lg" class="fs-5" /> <span class="visually-hidden">Create S3 Key</span> </button> </div> diff --git a/src/views/workflows/ListWorkflowsView.vue b/src/views/workflows/ListWorkflowsView.vue new file mode 100644 index 0000000000000000000000000000000000000000..8ea76021dd2424a80a1e8aa6fafd51ae90aacf89 --- /dev/null +++ b/src/views/workflows/ListWorkflowsView.vue @@ -0,0 +1,60 @@ +<script setup lang="ts"> +import { computed, onMounted, reactive } from "vue"; +import type { ComputedRef } from "vue"; +import { useWorkflowStore } from "@/stores/workflows"; +import type { WorkflowOut } from "@/client/workflow"; +import WorkflowCard from "@/components/workflows/WorkflowCard.vue"; + +const workflowRepository = useWorkflowStore(); + +const workflowsState = reactive({ + loading: true, + filterString: "", +} as { + loading: boolean; + filterString: string; +}); + +const filteredWorkflows: ComputedRef<WorkflowOut[]> = computed(() => { + return workflowsState.filterString.length > 0 + ? workflowRepository.workflows.filter((workflow) => + workflow.name.includes(workflowsState.filterString) + ) + : workflowRepository.workflows; +}); + +onMounted(() => { + workflowRepository.fetchWorkflows( + null, + null, + () => (workflowsState.loading = false) + ); +}); +</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> + <div v-else class="d-flex flex-wrap"> + <workflow-card + v-for="workflow in 6" + :key="workflow" + :workflow="{ + name: '', + short_description: '', + repository_url: '', + workflow_id: '', + versions: [], + }" + loading + /> + </div> +</template> + +<style scoped></style>