diff --git a/src/components/NavbarTop.vue b/src/components/NavbarTop.vue index 2c08939a0e2769614f48e6c99a829fa8469d8094..1116c0dbc77dcfb0dd6ed6ee8f3b2761a31dfdbb 100644 --- a/src/components/NavbarTop.vue +++ b/src/components/NavbarTop.vue @@ -9,6 +9,7 @@ import { OpenAPI as S3ProxyOpenAPI } from "@/client/s3proxy"; import { OpenAPI as AuthOpenAPI } from "@/client/auth"; import { OpenAPI as WorkflowOpenAPI } from "@/client/workflow"; import CopyToClipboardIcon from "@/components/CopyToClipboardIcon.vue"; +import dayjs from "dayjs"; const store = useAuthStore(); const { cookies } = useCookies(); @@ -200,6 +201,7 @@ watch( static-backdrop modal-i-d="advancedUsageModal" modal-label="Advanced Usage Modal" + v-if="store.authenticated" > <template v-slot:header> <h3>Advanced Usage</h3> @@ -243,12 +245,17 @@ watch( </tbody> </table> <div class="mt-4"> - <label for="clowm-jwt" class="form-label">JWT for Services</label> + <label for="clowm-jwt" class="form-label" + >JWT for Services (expires at + {{ + dayjs.unix(store.decodedToken!.exp).format("DD.MM.YYYY HH:mm") + }})</label + > <div class="input-group"> <input type="text" readonly - class="form-control" + class="form-control text-truncate" id="clowm-jwt" :value="store.token" aria-describedby="clowm-jwt-copy" diff --git a/src/components/workflows/WorkflowWithVersionsCard.vue b/src/components/workflows/WorkflowWithVersionsCard.vue index 0e72ccb8b29d9f807d5b2fc914dc1ca03d005460..6d814c88dc91846178375cc42d836c2d91257363 100644 --- a/src/components/workflows/WorkflowWithVersionsCard.vue +++ b/src/components/workflows/WorkflowWithVersionsCard.vue @@ -119,6 +119,15 @@ onMounted(() => { </div> <div v-else> <table class="table table-sm table-hover"> + <thead> + <tr> + <th scope="col">Version</th> + <th scope="col">Status</th> + <th scope="col">Updated at</th> + <th scope="col" class="text-align-center">Icon</th> + <th scope="col">Link</th> + </tr> + </thead> <tbody> <tr v-for="version in sortedVersions(props.workflow.versions)" @@ -141,7 +150,7 @@ onMounted(() => { <td> {{ dayjs.unix(version.created_at).format("D MMMM YYYY") }} </td> - <td class="w-fit"> + <td class="text-align-center"> <img v-if="version.icon_url != null" :src="version.icon_url" @@ -207,4 +216,7 @@ td > img { .add-icon-hover:hover { color: var(--bs-success) !important; } +.text-align-center { + text-align: center; +} </style> diff --git a/src/components/workflows/modals/ArbitraryWorkflowModal.vue b/src/components/workflows/modals/ArbitraryWorkflowModal.vue index 9abbaf2783fa0d6868361493de778f1114a70f86..df01d892735acabd6975cde1c26700709061ceb2 100644 --- a/src/components/workflows/modals/ArbitraryWorkflowModal.vue +++ b/src/components/workflows/modals/ArbitraryWorkflowModal.vue @@ -100,7 +100,7 @@ function checkRepository() { : undefined, ); repo - .checkFilesExist(requiredRepositoryFiles, true) + .checkFilesExist(requiredRepositoryFiles([]), true) .then(() => { formState.allowUpload = true; }) diff --git a/src/components/workflows/modals/CreateWorkflowModal.vue b/src/components/workflows/modals/CreateWorkflowModal.vue index 3971a16223b875c5fb295d56bcaf601f7b02c631..fb63fbc47601861853088e2d3b3da77447fb32aa 100644 --- a/src/components/workflows/modals/CreateWorkflowModal.vue +++ b/src/components/workflows/modals/CreateWorkflowModal.vue @@ -1,7 +1,11 @@ <script setup lang="ts"> import { computed, onMounted, reactive, ref, watch } from "vue"; import { Modal, Toast, Collapse, Tooltip } from "bootstrap"; -import type { WorkflowIn, WorkflowOut } from "@/client/workflow"; +import type { + WorkflowIn, + WorkflowOut, + WorkflowModeOut, +} from "@/client/workflow"; import BootstrapModal from "@/components/modals/BootstrapModal.vue"; import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; import { ApiError, WorkflowService } from "@/client/workflow"; @@ -30,6 +34,7 @@ let createWorkflowModal: Modal | null = null; let successToast: Toast | null = null; let privateRepositoryCollapse: Collapse | null = null; let tokenHelpCollapse: Collapse | null = null; +let workflowModesCollapse: Collapse | null = null; // HTML Form Elements // ============================================================================= @@ -81,6 +86,21 @@ const repositoryCredentials = reactive<{ privateRepo: false, }); +const workflowModes = reactive<{ + hasModes: boolean; + modes: WorkflowModeOut[]; +}>({ + hasModes: false, + modes: [ + { + mode_id: crypto.randomUUID(), + name: "", + schema_path: "", + entrypoint: "", + }, + ], +}); + // Computed Properties // ============================================================================= const gitIcon = computed<string>(() => @@ -99,6 +119,17 @@ watch( }, ); +watch( + () => workflowModes.hasModes, + (show) => { + if (show) { + workflowModesCollapse?.show(); + } else { + workflowModesCollapse?.hide(); + } + }, +); + // Functions // ============================================================================= function modalClosed() { @@ -130,6 +161,15 @@ function createWorkflow() { ) { workflow.token = repositoryCredentials.token; } + if (workflowModes.hasModes) { + workflow.modes = workflowModes.modes.map((mode) => { + return { + name: mode.name, + schema_path: mode.schema_path, + entrypoint: mode.entrypoint, + }; + }); + } WorkflowService.workflowCreateWorkflow(workflow) .then((w) => { emit("workflow-created", w); @@ -164,18 +204,21 @@ function resetForm() { workflow.git_commit_hash = ""; workflow.initial_version = undefined; workflow.token = undefined; + workflow.modes = []; + workflowModes.modes = [ + { + mode_id: crypto.randomUUID(), + name: "", + schema_path: "", + entrypoint: "", + }, + ]; + workflowModes.hasModes = false; repositoryCredentials.privateRepo = false; repositoryCredentials.token = ""; privateRepositoryCollapse?.hide(); } -/** - * Watcher function for the file upload in the form. - */ -//function iconChanged() { -// workflow.icon = workflowIconInput.value?.files?.[0].slice(); -//} - /** * Check the workflow repository for the necessary files. */ @@ -194,8 +237,11 @@ function checkRepository() { ? repositoryCredentials.token : undefined, ); + const requiredFiles = requiredRepositoryFiles( + workflowModes.hasModes ? workflowModes.modes : [], + ); repo - .checkFilesExist(requiredRepositoryFiles, true) + .checkFilesExist(requiredFiles, true) .then(() => { formState.allowUpload = true; }) @@ -227,6 +273,27 @@ function checkVersionValidity() { } } +function addMode() { + if (workflowModes.modes.length < 11) { + workflowModes.modes.push({ + mode_id: crypto.randomUUID(), + name: "", + schema_path: "", + entrypoint: "", + }); + } +} + +function removeMode(index: number) { + if ( + workflowModes.modes.length > 1 && + index > -1 && + index < (workflowModes.modes.length ?? 0) + ) { + workflowModes.modes.splice(index, 1); + } +} + // Lifecycle Events // ============================================================================= onMounted(() => { @@ -238,6 +305,9 @@ onMounted(() => { tokenHelpCollapse = new Collapse("#tokenHelpCollapse", { toggle: false, }); + workflowModesCollapse = new Collapse("#workflowModesCollapse", { + toggle: false, + }); new Tooltip("#tooltip-version-" + randomIDSuffix); new Tooltip("#tooltip-commit-" + randomIDSuffix); new Tooltip("#tooltip-url-" + randomIDSuffix); @@ -430,7 +500,7 @@ onMounted(() => { aria-controls="#privateRepositoryCollapse" /> <label class="form-check-label" for="privateRepositoryCheckbox"> - Private Git Repository + Enable private Git Repository </label> </div> <div class="collapse" id="privateRepositoryCollapse"> @@ -490,6 +560,101 @@ onMounted(() => { </div> </div> </div> + <div class="mb-3"> + <div class="form-check fs-5"> + <input + class="form-check-input" + type="checkbox" + v-model="workflowModes.hasModes" + id="workflowModesCheckbox" + @change="formState.allowUpload = false" + aria-controls="#workflowModesCollapse" + /> + <label class="form-check-label" for="workflowModesCheckbox"> + Enable Workflow Modes + </label> + <button + v-if="workflowModes.hasModes" + class="btn btn-primary float-end" + @click="addMode" + :disabled="workflow.modes!.length >= 10" + > + Add Mode + </button> + </div> + </div> + <div class="collapse" id="workflowModesCollapse"> + <TransitionGroup name="list" tag="div"> + <div + v-for="(mode, index) in workflowModes.modes" + :key="mode.mode_id" + class="row mb-3" + > + <h6> + <font-awesome-icon + icon="fa-solid fa-minus" + class="text-danger me-1 fs-6 cursor-pointer" + @click="removeMode(index)" + /> + Mode {{ index + 1 }} + </h6> + <div class="col-6"> + <label :for="'modeNameInput-' + index" class="form-label" + >Name</label + > + <div class="input-group"> + <div class="input-group-text"> + <font-awesome-icon icon="fa-solid fa-tag" /> + </div> + <input + type="text" + class="form-control" + :id="'modeNameInput-' + index" + maxlength="128" + v-model="mode.name" + :required="workflowModes.hasModes" + /> + </div> + </div> + <div class="col-6 mb-2"> + <label :for="'modeEntryInput-' + index" class="form-label" + >Entrypoint</label + > + <div class="input-group"> + <div class="input-group-text"> + <font-awesome-icon icon="fa-solid fa-tag" /> + </div> + <input + type="text" + class="form-control" + :id="'modeEntryInput-' + index" + maxlength="128" + v-model="mode.entrypoint" + :required="workflowModes.hasModes" + /> + </div> + </div> + <label :for="'modeSchemaInput-' + index" class="form-label" + >Schema File</label + > + <div class="input-group"> + <div class="input-group-text"> + <font-awesome-icon icon="fa-solid fa-file-code" /> + </div> + <input + type="text" + class="form-control" + :id="'modeSchemaInput-' + index" + maxlength="128" + pattern=".*\.json$" + v-model="mode.schema_path" + @change="formState.allowUpload = false" + :required="workflowModes.hasModes" + /> + </div> + </div> + </TransitionGroup> + </div> </form> </template> <template v-slot:footer> @@ -533,4 +698,26 @@ onMounted(() => { .hover-info:hover { color: var(--bs-info) !important; } + +.list-move, /* apply transition to moving elements */ +.list-enter-active, +.list-leave-active { + transition: all 0.5s ease; +} + +.list-enter-from { + opacity: 0; + transform: translateX(50%); +} + +.list-leave-to { + opacity: 0; + transform: translateX(-50%); +} + +/* ensure leaving items are taken out of layout flow so that moving + animations can be calculated correctly. */ +.list-leave-active { + position: absolute; +} </style> diff --git a/src/components/workflows/modals/UpdateWorkflowModal.vue b/src/components/workflows/modals/UpdateWorkflowModal.vue index 4ce43dc0026daf6c66241584f25372d8601187d1..9664a5581d57766452dbb7a68083d795b36d45e8 100644 --- a/src/components/workflows/modals/UpdateWorkflowModal.vue +++ b/src/components/workflows/modals/UpdateWorkflowModal.vue @@ -149,7 +149,7 @@ function checkRepository() { formState.workflowToken, ); repo - .checkFilesExist(requiredRepositoryFiles, true) + .checkFilesExist(requiredRepositoryFiles([]), true) .then(() => { formState.allowUpload = true; }) diff --git a/src/utils/GitRepository.ts b/src/utils/GitRepository.ts index 68004e6c06e19000f3710ff2db33f8430c200301..4299afa536ef37a08b43227d447a7957353a3255 100644 --- a/src/utils/GitRepository.ts +++ b/src/utils/GitRepository.ts @@ -1,14 +1,22 @@ import axios from "axios"; import type { AxiosInstance, AxiosBasicCredentials } from "axios"; - -export const requiredRepositoryFiles = [ - "main.nf", - "CHANGELOG.md", - "README.md", - "nextflow_schema.json", - "docs/usage.md", - "docs/output.md", -]; +import type { WorkflowModeOut } from "@/client/workflow"; + +export function requiredRepositoryFiles(modes: WorkflowModeOut[]) { + const list = [ + "main.nf", + "CHANGELOG.md", + "README.md", + "docs/usage.md", + "docs/output.md", + ]; + if (modes.length > 0) { + list.push(...modes.map((mode) => mode.schema_path)); + } else { + list.push("nextflow_schema.json"); + } + return list; +} export function determineGitIcon(repo_url?: string): string { let gitProvider = "git-alt"; diff --git a/src/utils/Workflow.ts b/src/utils/Workflow.ts index 9257886a74986eebd6e7bd1da9ed3a3e17bc4fd2..e01237b68f4f3c5baaec652c5c10c5c3a36f6e2f 100644 --- a/src/utils/Workflow.ts +++ b/src/utils/Workflow.ts @@ -1,7 +1,5 @@ import type { WorkflowVersion } from "@/client/workflow"; -export function sortedVersions(versions: WorkflowVersion[]): WorkflowVersion[]; - export function sortedVersions( versions: WorkflowVersion[], desc = true, @@ -15,10 +13,6 @@ export function sortedVersions( return vs; } -export function latestVersion( - versions: WorkflowVersion[], -): WorkflowVersion | undefined; - export function latestVersion( versions: WorkflowVersion[], ): WorkflowVersion | undefined { diff --git a/src/views/workflows/ArbitraryWorkflowView.vue b/src/views/workflows/ArbitraryWorkflowView.vue index cc1c87b0c28732dd69e14989523e1f7c01230f64..bc2559916b743cc0b72590fc29aaba898111a9f3 100644 --- a/src/views/workflows/ArbitraryWorkflowView.vue +++ b/src/views/workflows/ArbitraryWorkflowView.vue @@ -46,7 +46,7 @@ function downloadVersionFiles( workflowState.loading = true; const repo = GitRepository.buildRepository(repository, commit_hash, token); Promise.all( - requiredRepositoryFiles.map((file) => + requiredRepositoryFiles([]).map((file) => repo.downloadFile(file).then((response) => { if (file.includes("README")) { workflowState.descriptionMarkdown = response.data;