Skip to content
Merged
26 changes: 24 additions & 2 deletions client/src/api/jobs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { components } from "@/api/schema";
import { rethrowSimple } from "@/utils/simple-error";

import { GalaxyApi } from "./client";

export type JobDestinationParams = components["schemas"]["JobDestinationParams"];
export type ShowFullJobResponse = components["schemas"]["ShowFullJobResponse"];
Expand All @@ -15,8 +18,8 @@ export type JobMessage =
| components["schemas"]["RegexJobMessage"]
| components["schemas"]["MaxDiscoveredFilesJobMessage"];

export const NON_TERMINAL_STATES = ["new", "queued", "running", "waiting", "paused", "resubmitted", "stop"];
export const ERROR_STATES = ["error", "deleted", "deleting"];
export const NON_TERMINAL_STATES = ["new", "queued", "running", "waiting", "paused", "resubmitted", "upload"];
export const ERROR_STATES = ["error", "deleted", "deleting", "failed"];
export const TERMINAL_STATES = ["ok", "skipped", "stop", "stopping"].concat(ERROR_STATES);

interface JobDef {
Expand All @@ -40,3 +43,22 @@ export interface ResponseVal {
jobResponse: JobResponse;
toolName: string;
}

/**
* Delete/Stop a job.
* @param jobId The ID of the job to delete.
* @param message An optional message to be set on the job and output dataset(s) to explain the reason for stopping.
* @returns A promise that resolves to a boolean indicating whether the job was successfully deleted or job was already in a terminal state.
*/
export async function deleteJob(jobId: string, message?: string): Promise<boolean> {
const { data, error } = await GalaxyApi().DELETE("/api/jobs/{job_id}", {
params: { path: { job_id: jobId } },
data: { message },
});

if (error) {
rethrowSimple(error);
}

return data;
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
<script setup lang="ts">
import { faDownload, faInfoCircle, faRedo, faTable } from "@fortawesome/free-solid-svg-icons";
import { faDownload, faInfoCircle, faTable } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { computed } from "vue";
import { useRouter } from "vue-router/composables";
import { useRoute } from "vue-router/composables";

import type { HDCASummary } from "@/api";
import { getAppRoot } from "@/onload/loadConfig";

const router = useRouter();
import GButton from "@/components/BaseComponents/GButton.vue";
import GButtonGroup from "@/components/BaseComponents/GButtonGroup.vue";
import RerunJobButton from "@/components/JobInformation/RerunJobButton.vue";

const route = useRoute();

const props = defineProps<{
dsc: HDCASummary; // typescript recognizes HDCADetailed IS_A HDCASummary
}>();

const downloadUrl = computed(() => `${getAppRoot()}api/dataset_collections/${props.dsc.id}/download`);
const rerunUrl = computed(() =>
props.dsc.job_source_type == "Job" ? `/root?job_id=${props.dsc.job_source_id}` : null,
);
const showCollectionDetailsUrl = computed(() =>
props.dsc.job_source_type == "Job" ? `/jobs/${props.dsc.job_source_id}/view` : null,
);
Expand All @@ -26,62 +27,60 @@ const hasSampleSheet = computed(() => {
return props.dsc.collection_type && props.dsc.collection_type.startsWith("sample_sheet");
});

const sheetUrl = computed(() => {
return `${getAppRoot()}collection/${props.dsc.id}/sheet`;
});

function onDownload() {
window.location.href = downloadUrl.value;
}
const sheetUrl = computed(() => `/collection/${props.dsc.id}/sheet`);
</script>
<template>
<section>
<nav class="content-operations d-flex justify-content-between bg-secondary">
<b-button-group>
<b-button
<GButtonGroup class="collection-operations-btn-group">
<GButton
title="Download Collection"
:disabled="disableDownload"
class="rounded-0 text-decoration-none"
size="sm"
variant="link"
:href="downloadUrl"
@click="onDownload">
<FontAwesomeIcon class="mr-1" :icon="faDownload" />
size="small"
color="blue"
transparent
:href="downloadUrl">
<FontAwesomeIcon fixed-width :icon="faDownload" />
<span>Download</span>
</b-button>
<b-button
</GButton>
<GButton
v-if="showCollectionDetailsUrl"
class="collection-job-details-btn px-1"
class="collection-job-details-btn"
title="Show Details"
size="sm"
variant="link"
:href="showCollectionDetailsUrl"
@click.prevent.stop="router.push(showCollectionDetailsUrl)">
<FontAwesomeIcon class="mr-1" :icon="faInfoCircle" />
size="small"
color="blue"
transparent
:pressed="route.fullPath === showCollectionDetailsUrl"
:to="showCollectionDetailsUrl">
<FontAwesomeIcon fixed-width :icon="faInfoCircle" />
<span>Show Details</span>
</b-button>
<b-button
v-if="rerunUrl"
title="Rerun job"
class="rounded-0 text-decoration-none"
size="sm"
variant="link"
:href="rerunUrl"
@click.prevent.stop="router.push(rerunUrl)">
<FontAwesomeIcon class="mr-1" :icon="faRedo" />
<span>Run Job Again</span>
</b-button>
<b-button
</GButton>
<RerunJobButton
v-if="props.dsc.job_source_type === 'Job' && props.dsc.job_source_id"
:job-id="props.dsc.job_source_id" />
<GButton
v-if="hasSampleSheet && sheetUrl"
class="rounded-0 text-decoration-none"
size="sm"
variant="link"
:href="sheetUrl"
@click.prevent.stop="router.push(sheetUrl)">
<FontAwesomeIcon class="mr-1" :icon="faTable" />
title="View Sample Sheet"
size="small"
color="blue"
transparent
:pressed="route.fullPath === sheetUrl"
:to="sheetUrl">
<FontAwesomeIcon fixed-width :icon="faTable" />
<span>View Sheet</span>
</b-button>
</b-button-group>
</GButton>
</GButtonGroup>
</nav>
</section>
</template>

<style scoped lang="scss">
.collection-operations-btn-group {
display: flex;
flex-wrap: wrap;
:deep(.g-button) {
border-radius: 0;
white-space: nowrap;
}
}
</style>
2 changes: 2 additions & 0 deletions client/src/components/JobInformation/JobInformation.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import "@tests/vitest/mockHelpPopovers";

import { createTestingPinia } from "@pinia/testing";
import { getLocalVue } from "@tests/vitest/helpers";
import { mount } from "@vue/test-utils";
import flushPromises from "flush-promises";
Expand Down Expand Up @@ -61,6 +62,7 @@ describe("JobInformation/JobInformation.vue", () => {
wrapper = mount(JobInformation, {
propsData,
localVue,
pinia: createTestingPinia({ createSpy: vi.fn }),
});
await flushPromises();
jobInfoTable = wrapper.find("#job-information");
Expand Down
27 changes: 25 additions & 2 deletions client/src/components/JobInformation/JobInformation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import { getJobDuration } from "./utilities";

import Heading from "../Common/Heading.vue";
import DecodedId from "../DecodedId.vue";
import JobState from "../JobStates/JobState.vue";
import CodeRow from "./CodeRow.vue";
import RerunJobButton from "./RerunJobButton.vue";
import CopyToClipboard from "@/components/CopyToClipboard.vue";
import HelpText from "@/components/Help/HelpText.vue";
import UtcDate from "@/components/UtcDate.vue";
Expand Down Expand Up @@ -47,6 +49,13 @@ const jobIsTerminal = computed(() => (job.value?.state ? jobStateIsTerminal(job.
const jobIsRunning = computed(() => (job.value?.state ? jobStateIsRunning(job.value.state) : false));
const routeToInvocation = computed(() => `/workflows/invocations/${fetchedInvocationId.value}`);

/** Whether the job can be rerun; actually decided based on if the tool `is_workflow_compatible`,
* but that would require an extra fetch. Just going by known non-rerunnable tool ids for now.
*/
const jobIsRerunnable = computed(
() => !!job.value?.tool_id && !job.value.tool_id.startsWith("upload") && job.value.tool_id !== "__DATA_FETCH__",
);

// Curious as to why we're trying to access tool_version and traceback like this, when they don't exist on
// `ShowFullJobResponse`? Possibly historical reasons or maybe the `JobProvider` can return different types (doesn't seem like it)?
const toolVersion = computed(() =>
Expand Down Expand Up @@ -144,7 +153,15 @@ watch(
:stderr_position="stderr_position"
:stderr_length="stderr_length"
@update:result="updateConsoleOutputs" />
<Heading id="job-information-heading" h1 separator inline size="md"> Job Information </Heading>
<div class="d-flex justify-content-between flex-gapx-1">
<Heading id="job-information-heading" class="flex-grow-1" h1 separator inline size="md">
Job Information
<JobState v-if="job" class="job-information-state-badge" :job="job" />
</Heading>
<div v-if="job && jobIsRerunnable">
<RerunJobButton :job-id="props.jobId" outline />
</div>
</div>
<table id="job-information" class="tabletip info_data_table">
<tbody>
<tr v-if="job && job.tool_id">
Expand Down Expand Up @@ -261,9 +278,15 @@ watch(
</table>
</div>
</template>
<style scoped>
<style scoped lang="scss">
@import "@/style/scss/theme/blue.scss";

.tooltipJobInfo {
text-decoration-line: underline;
text-decoration-style: dashed;
}

.job-information-state-badge {
font-size: $h5-font-size;
}
</style>
31 changes: 31 additions & 0 deletions client/src/components/JobInformation/RerunJobButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script setup lang="ts">
import { faRedo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { computed } from "vue";
import { useRoute } from "vue-router/composables";

import GButton from "../BaseComponents/GButton.vue";

const route = useRoute();

const props = defineProps<{
jobId: string;
outline?: boolean;
}>();

const rerunUrl = computed(() => `/?job_id=${props.jobId}`);
</script>

<template>
<GButton
title="Rerun job"
size="small"
color="blue"
:outline="props.outline"
:transparent="!props.outline"
:pressed="route.fullPath === rerunUrl"
:to="rerunUrl">
<FontAwesomeIcon fixed-width :icon="faRedo" />
<span>Run Job Again</span>
</GButton>
</template>
Loading
Loading