diff --git a/package-lock.json b/package-lock.json index 03f8850644a83ad975e37a422fac5b51b4c851d5..b3a54f3c78f2e2fb405ee781cd260f03b86e8d53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@aws-sdk/client-s3": "^3.379.1", "@aws-sdk/lib-storage": "^3.379.1", "@aws-sdk/s3-request-presigner": "^3.379.1", - "@fortawesome/fontawesome-free": "~6.4.0", + "@fortawesome/fontawesome-free": "~6.4.2", "@popperjs/core": "~2.11.8", "ajv": "~8.12.0", "bootstrap": "~5.3.1", @@ -1327,9 +1327,9 @@ } }, "node_modules/@fortawesome/fontawesome-free": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.0.tgz", - "integrity": "sha512-0NyytTlPJwB/BF5LtRV8rrABDbe3TdTXqNB3PdZ+UUUZAEIrdOJdmABqKjt4AXwIoJNaRVVZEXxpNrqvE1GAYQ==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.2.tgz", + "integrity": "sha512-m5cPn3e2+FDCOgi1mz0RexTUvvQibBebOUlUlW0+YrMjDTPkiJ6VTKukA1GRsvRw+12KyJndNjj0O4AgTxm2Pg==", "hasInstallScript": true, "engines": { "node": ">=6" @@ -7523,9 +7523,9 @@ "dev": true }, "@fortawesome/fontawesome-free": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.0.tgz", - "integrity": "sha512-0NyytTlPJwB/BF5LtRV8rrABDbe3TdTXqNB3PdZ+UUUZAEIrdOJdmABqKjt4AXwIoJNaRVVZEXxpNrqvE1GAYQ==" + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.2.tgz", + "integrity": "sha512-m5cPn3e2+FDCOgi1mz0RexTUvvQibBebOUlUlW0+YrMjDTPkiJ6VTKukA1GRsvRw+12KyJndNjj0O4AgTxm2Pg==" }, "@humanwhocodes/config-array": { "version": "0.11.10", diff --git a/package.json b/package.json index 41e4a72dcd15f579ef85f2d0676dd54e63b19bfd..4f1c1caaa7f9b2bc16f6fdee3ee370ae9dcd2c92 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@aws-sdk/client-s3": "^3.379.1", "@aws-sdk/lib-storage": "^3.379.1", "@aws-sdk/s3-request-presigner": "^3.379.1", - "@fortawesome/fontawesome-free": "~6.4.0", + "@fortawesome/fontawesome-free": "~6.4.2", "@popperjs/core": "~2.11.8", "ajv": "~8.12.0", "bootstrap": "~5.3.1", diff --git a/src/components/workflows/WorkflowCard.vue b/src/components/workflows/WorkflowCard.vue index 3aab9fa2fb0f8d741d2bb5232b56d228cfebe56d..61f2beb88232deac2326c7a36efedb60c38f3074 100644 --- a/src/components/workflows/WorkflowCard.vue +++ b/src/components/workflows/WorkflowCard.vue @@ -115,7 +115,7 @@ onMounted(() => { } .icon { - max-height: 30px; - max-width: 30px; + max-height: 32px; + max-width: 32px; } </style> diff --git a/src/components/workflows/WorkflowWithVersionsCard.vue b/src/components/workflows/WorkflowWithVersionsCard.vue index 1646e72ab32cd9f2453adee899a6e7ce8bb89a50..036388ae8ed0cb31d35e461e2c3c78b09814a752 100644 --- a/src/components/workflows/WorkflowWithVersionsCard.vue +++ b/src/components/workflows/WorkflowWithVersionsCard.vue @@ -1,5 +1,5 @@ <script setup lang="ts"> -import type { WorkflowOut } from "@/client/workflow"; +import type { WorkflowOut, WorkflowVersion } from "@/client/workflow"; import { onMounted, ref, watch } from "vue"; import { Status } from "@/client/workflow"; import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; @@ -17,6 +17,8 @@ const randomIDSuffix: string = Math.random().toString(16).substring(2, 8); const emit = defineEmits<{ (e: "workflow-update-click", workflow: WorkflowOut): void; (e: "workflow-delete-click", workflow: WorkflowOut): void; + (e: "workflow-delete-credentials-click", workflow: WorkflowOut): void; + (e: "workflow-update-icon-click", version: WorkflowVersion): void; }>(); watch( @@ -63,6 +65,17 @@ onMounted(() => { <span>{{ props.workflow.name }}</span> </div> <div> + <button + v-if="props.workflow.private" + type="button" + class="btn btn-outline-danger me-2" + @click="emit('workflow-delete-credentials-click', props.workflow)" + :class="{ disabled: props.loading }" + data-bs-toggle="modal" + data-bs-target="#deleteWorkflowCredentialsModal" + > + <font-awesome-icon icon="fa-solid fa-lock-open" /> + </button> <button type="button" class="btn btn-outline-danger me-2" @@ -112,13 +125,6 @@ onMounted(() => { v-for="version in sortedVersions(props.workflow.versions)" :key="version.git_commit_hash" > - <td class="w-fit"> - <img - v-if="version.icon_url != null" - :src="version.icon_url" - alt="Workflow Version Icon" - /> - </td> <th scope="row" class="fw-bold">{{ version.version }}</th> <td :class="{ @@ -136,6 +142,25 @@ onMounted(() => { <td> {{ dayjs.unix(version.created_at).format("D MMMM YYYY") }} </td> + <td class="w-fit"> + <img + v-if="version.icon_url != null" + :src="version.icon_url" + alt="Workflow Version Icon" + @click="emit('workflow-update-icon-click', version)" + class="cursor-pointer" + data-bs-toggle="modal" + data-bs-target="#updateWorkflowVersionIconModal" + /> + <font-awesome-icon + v-else + icon="fa-solid fa-circle-plus" + class="add-icon-hover cursor-pointer" + @click="emit('workflow-update-icon-click', version)" + data-bs-toggle="modal" + data-bs-target="#updateWorkflowVersionIconModal" + /> + </td> <td> <router-link class="w-fit mx-0" @@ -179,4 +204,8 @@ td > img { max-width: 1em; max-height: 1em; } + +.add-icon-hover:hover { + color: var(--bs-success) !important; +} </style> diff --git a/src/components/workflows/modals/UpdateWorkflowVersionIconModal.vue b/src/components/workflows/modals/UpdateWorkflowVersionIconModal.vue new file mode 100644 index 0000000000000000000000000000000000000000..3ffa9c38c14aff36982b02269bc6865aa8227c8a --- /dev/null +++ b/src/components/workflows/modals/UpdateWorkflowVersionIconModal.vue @@ -0,0 +1,211 @@ +<script setup lang="ts"> +import BootstrapModal from "@/components/modals/BootstrapModal.vue"; +import type { WorkflowVersion } from "@/client/workflow"; +import { WorkflowVersionService } from "@/client/workflow"; +import { onMounted, ref, reactive, computed } from "vue"; +import { Modal, Toast } from "bootstrap"; + +// Constants +// ============================================================================= +const randomIDSuffix = Math.random().toString(16).substring(2, 8); + +// Bootstrap Elements +// ============================================================================= +let updateIconModal: Modal | null = null; +let successToast: Toast | null = null; + +// Form Elements +// ============================================================================= +const iconUpdateForm = ref<HTMLFormElement | undefined>(undefined); +const iconElement = ref<HTMLImageElement | undefined>(undefined); +const iconInputElement = ref<HTMLInputElement | undefined>(undefined); + +// Props +// ============================================================================= +const props = defineProps<{ + modalID: string; + version: WorkflowVersion; + workflowName?: string; +}>(); + +// Reactive State +// ============================================================================= +const iconUpdate = reactive<{ + icon: Blob | null; +}>({ + icon: null, +}); + +const formState = reactive<{ + loading: boolean; + validated: boolean; +}>({ + loading: false, + validated: false, +}); + +// Computed Properties +// ============================================================================= +const showIcon = computed<boolean>( + () => props.version.icon_url != undefined || iconUpdate.icon != undefined, +); + +// Events +// ============================================================================= +const emit = defineEmits<{ + (e: "icon-updated", version: WorkflowVersion, icon_url: string): void; + (e: "icon-deleted", version: WorkflowVersion): void; +}>(); + +// Functions +// ============================================================================= +function resetForm() { + modalClosed(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + iconElement.value!.src = props.version.icon_url ?? ""; + if (iconInputElement.value != undefined) { + iconInputElement.value.value = ""; + } +} + +function modalClosed() { + formState.validated = false; +} + +function iconChanged() { + iconUpdate.icon = iconInputElement.value?.files?.[0].slice() ?? null; + if (iconUpdate.icon != null) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + iconElement.value!.src = URL.createObjectURL(iconUpdate.icon.slice()); + } else { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + iconElement.value!.src = props.version.icon_url ?? ""; + } +} + +function updateIcon() { + formState.validated = true; + if (iconUpdateForm.value?.checkValidity() && iconUpdate.icon != null) { + formState.loading = true; + WorkflowVersionService.workflowVersionUploadWorkflowVersionIcon( + props.version.workflow_id, + props.version.git_commit_hash, + { + icon: iconUpdate.icon, + }, + ) + .then((icon_url) => { + emit("icon-updated", props.version, icon_url); + successToast?.show(); + updateIconModal?.hide(); + resetForm(); + }) + .catch((error) => { + console.error(error); + }) + .finally(() => { + formState.loading = false; + }); + } +} + +// Lifecycle Events +// ============================================================================= +onMounted(() => { + updateIconModal = new Modal("#" + props.modalID); + successToast = new Toast("#successToast-" + randomIDSuffix); +}); +</script> + +<template> + <div class="toast-container position-fixed top-toast end-0 p-3"> + <div + role="alert" + aria-live="assertive" + aria-atomic="true" + class="toast text-bg-success align-items-center border-0" + data-bs-autohide="true" + :id="'successToast-' + randomIDSuffix" + > + <div class="d-flex"> + <div class="toast-body">Successfully updated Workflow</div> + <button + type="button" + class="btn-close btn-close-white me-2 m-auto" + data-bs-dismiss="toast" + aria-label="Close" + ></button> + </div> + </div> + </div> + <bootstrap-modal + :modalID="modalID" + :static-backdrop="true" + modal-label="Update Workflow Version IconModal" + v-on="{ 'hidden.bs.modal': modalClosed }" + > + <template v-slot:header> + Update Icon Workflow + <span class="fw-bold" + >{{ props.workflowName }}@{{ props.version.version }}</span + > + </template> + <template v-slot:body> + <form + ref="iconUpdateForm" + id="iconUpdateForm" + :class="{ 'was-validated': formState.validated }" + > + <div class="row"> + <div class="col-10"> + <label for="workflowIconInput" class="form-label">New Icon</label> + <input + type="file" + ref="iconInputElement" + accept="image/*" + class="form-control" + id="workflowIconInput" + @change="iconChanged" + aria-describedby="iconHelp" + required + /> + </div> + <div class="col-2"> + <img + :src="props.version.icon_url ?? undefined" + ref="iconElement" + :hidden="!showIcon" + /> + </div> + </div> + </form> + </template> + <template v-slot:footer> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> + Close + </button> + <button + type="submit" + form="iconUpdateForm" + class="btn btn-primary" + :disabled="formState.loading" + @click.prevent="updateIcon" + > + <span + v-if="formState.loading" + class="spinner-border spinner-border-sm" + role="status" + aria-hidden="true" + ></span> + Save + </button> + </template> + </bootstrap-modal> +</template> + +<style scoped> +img { + max-height: 64px; + max-width: 64px; +} +</style> diff --git a/src/views/workflows/MyWorkflowsView.vue b/src/views/workflows/MyWorkflowsView.vue index 3e8115f8467dc1e8a50248a86c60e8f6e6f89430..3421a847f5a97f332150a26280f878c0da7fdd1a 100644 --- a/src/views/workflows/MyWorkflowsView.vue +++ b/src/views/workflows/MyWorkflowsView.vue @@ -1,13 +1,18 @@ <script setup lang="ts"> import { onMounted, reactive } from "vue"; import type { WorkflowOut, WorkflowVersion } from "@/client/workflow"; -import { Status, WorkflowService } from "@/client/workflow"; +import { + Status, + WorkflowCredentialsService, + WorkflowService, +} from "@/client/workflow"; import { useAuthStore } from "@/stores/auth"; import WorkflowWithVersionsCard from "@/components/workflows/WorkflowWithVersionsCard.vue"; import CreateWorkflowModal from "@/components/workflows/modals/CreateWorkflowModal.vue"; import CardTransitionGroup from "@/components/transitions/CardTransitionGroup.vue"; import UpdateWorkflowModal from "@/components/workflows/modals/UpdateWorkflowModal.vue"; import DeleteModal from "@/components/modals/DeleteModal.vue"; +import UpdateWorkflowVersionIconModal from "@/components/workflows/modals/UpdateWorkflowVersionIconModal.vue"; const userRepository = useAuthStore(); const workflowsState = reactive<{ @@ -15,6 +20,7 @@ const workflowsState = reactive<{ loading: boolean; updateWorkflow: WorkflowOut; potentialWorkflowDelete?: WorkflowOut; + updateIconVersion: WorkflowVersion; }>({ workflows: [], loading: true, @@ -38,12 +44,25 @@ const workflowsState = reactive<{ developer_id: "", private: false, }, + updateIconVersion: { + version: "", + workflow_id: "", + git_commit_hash: "", + modes: null, + icon_url: null, + created_at: 0, + status: Status.CREATED, + }, }); function workflowUpdateClicked(workflow: WorkflowOut) { workflowsState.updateWorkflow = workflow; } +function iconUpdateClicked(version: WorkflowVersion) { + workflowsState.updateIconVersion = version; +} + function workflowUpdated(version: WorkflowVersion) { workflowsState.workflows .find((w) => w.workflow_id == version.workflow_id) @@ -68,6 +87,53 @@ function confirmedWorkflowDelete(workflow_id?: string) { } } +function confirmedWorkflowCredentialsDelete(workflow_id: string) { + WorkflowCredentialsService.workflowCredentialsDeleteWorkflowCredentials( + workflow_id, + ).then(() => { + const index = workflowsState.workflows.findIndex( + (w) => w.workflow_id == workflow_id, + ); + if (index > -1) { + workflowsState.workflows[index].private = false; + } + }); +} + +function confirmedWorkflwoCredentialsUpdate(workflow_id: string) { + const index = workflowsState.workflows.findIndex( + (w) => w.workflow_id == workflow_id, + ); + if (index > -1) { + workflowsState.workflows[index].private = true; + } +} + +function confirmedWorkflowVersionIconUpdate( + version: WorkflowVersion, + icon_url?: string, +) { + console.log("New Url", icon_url); + const wIndex = workflowsState.workflows.findIndex( + (w) => w.workflow_id == version.workflow_id, + ); + if (wIndex > -1) { + const vIndex = workflowsState.workflows[wIndex].versions.findIndex( + (v) => v.git_commit_hash == version.git_commit_hash, + ); + if (vIndex > -1) { + setTimeout(() => { + workflowsState.workflows[wIndex].versions[vIndex].icon_url = + icon_url ?? null; + }, 1000); + } + } +} + +function updateIconClicked(version: WorkflowVersion) { + workflowsState.updateIconVersion = version; +} + onMounted(() => { WorkflowService.workflowListWorkflows( undefined, @@ -102,6 +168,17 @@ onMounted(() => { ) " /> + <update-workflow-version-icon-modal + modal-i-d="updateWorkflowVersionIconModal" + :workflow-name=" + workflowsState.workflows.find( + (w) => w.workflow_id == workflowsState.updateIconVersion.workflow_id, + )?.name + " + :version="workflowsState.updateIconVersion" + @icon-updated="confirmedWorkflowVersionIconUpdate" + @icon-deleted="confirmedWorkflowVersionIconUpdate" + /> <div class="row m-2 border-bottom mb-4 justify-content-between align-items-center pb-2" > @@ -126,6 +203,7 @@ onMounted(() => { :loading="false" @workflow-update-click="workflowUpdateClicked" @workflow-delete-click="workflowDeleteClicked" + @workflow-update-icon-click="iconUpdateClicked" /> </card-transition-group> <div v-else class="text-center mt-5 fs-2"> @@ -152,6 +230,7 @@ onMounted(() => { private: false, }" loading + @workflow-update-icon-click="updateIconClicked" /> </div> </template> diff --git a/src/views/workflows/WorkflowView.vue b/src/views/workflows/WorkflowView.vue index 997285fcfce9b3cd2bdac4becad765df722b71aa..05e3ee2cfe6507f6fc40e63604ab74e8071ba375 100644 --- a/src/views/workflows/WorkflowView.vue +++ b/src/views/workflows/WorkflowView.vue @@ -323,8 +323,8 @@ onMounted(() => { <style scoped> .icon { - max-width: 60px; - max-height: 60px; + max-width: 64px; + max-height: 64px; min-width: 50px; min-height: 50px; }