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

Add basic list workflow page with cards

#36
/spent 3h 25m 2023-03-03
parent 2a2a0414
No related branches found
No related tags found
2 merge requests!84Remove development branch,!31Resolve "Create List Workflow Page"
Pipeline #26795 passed
Showing
with 265 additions and 102 deletions
......@@ -34,7 +34,7 @@ onBeforeMount(() => {
<template>
<NavbarTop />
<div class="container">
<div class="container mt-4">
<router-view></router-view>
</div>
</template>
......
......@@ -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>
......
......@@ -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"
......
......@@ -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
......
......@@ -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>
......
......@@ -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>
......
......@@ -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>
......
......@@ -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>
......
<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>
......@@ -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"),
},
],
},
{
......
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);
},
},
});
......@@ -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>
......
......@@ -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>
......
<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>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment