mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-10-16 15:45:04 +08:00
add jobDetails composable that will replace job and job console providers
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { computed, ref, toRef, watch } from "vue";
|
||||
|
||||
import { GalaxyApi } from "@/api";
|
||||
import { type JobConsoleOutput, NON_TERMINAL_STATES, type ShowFullJobResponse } from "@/api/jobs";
|
||||
import { JobConsoleOutputProvider, JobDetailsProvider } from "@/components/providers/JobProvider";
|
||||
import { useJobConsoleOutput, useJobDetails } from "@/composables/jobDetails";
|
||||
import { errorMessageAsString, rethrowSimple } from "@/utils/simple-error";
|
||||
import { stateIsTerminal } from "@/utils/utils";
|
||||
|
||||
import type { JobMessage } from "../../api/jobs";
|
||||
|
||||
@@ -34,27 +34,17 @@ const props = withDefaults(
|
||||
{ invocationId: undefined },
|
||||
);
|
||||
|
||||
const job = ref<ShowFullJobResponse | null>(null);
|
||||
const fetchedInvocationId = ref<string | null | undefined>(props.invocationId);
|
||||
const invocationLookupError = ref<string | null>(null);
|
||||
|
||||
const stdout_length = ref(50000);
|
||||
const stdout_text = ref("");
|
||||
const stderr_length = ref(50000);
|
||||
const stderr_text = ref("");
|
||||
const { job } = useJobDetails(toRef(props, "jobId"));
|
||||
|
||||
const stdout_position = computed(() => stdout_text.value.length);
|
||||
const stderr_position = computed(() => stderr_text.value.length);
|
||||
const jobIsRunning = computed(() => job.value?.state === "running");
|
||||
|
||||
function jobStateIsTerminal(jobState: string) {
|
||||
return jobState && !NON_TERMINAL_STATES.includes(jobState);
|
||||
}
|
||||
// Console output is only polled while the job is actively running
|
||||
const consoleOutputJobId = computed(() => (jobIsRunning.value ? props.jobId : undefined));
|
||||
const { stdout: stdout_text, stderr: stderr_text } = useJobConsoleOutput(consoleOutputJobId);
|
||||
|
||||
function jobStateIsRunning(jobState: string) {
|
||||
return jobState == "running";
|
||||
}
|
||||
|
||||
const jobIsRunning = computed(() => (job.value?.state ? jobStateIsRunning(job.value.state) : false));
|
||||
const routeToInvocation = computed(() => `/workflows/invocations/${fetchedInvocationId.value}`);
|
||||
|
||||
// Curious as to why we're trying to access tool_version and traceback like this, when they don't exist on
|
||||
@@ -86,29 +76,14 @@ const metadataDetail = ref<Record<string, string>>({
|
||||
error_level: `NO_ERROR = 0</br>LOG = 1</br>QC = 1.1</br>WARNING = 2</br>FATAL = 3</br>FATAL_OOM = 4</br>MAX = 4`,
|
||||
});
|
||||
|
||||
function updateJob(newJob: ShowFullJobResponse) {
|
||||
job.value = newJob;
|
||||
if (jobStateIsTerminal(newJob?.state)) {
|
||||
if (newJob.tool_stdout) {
|
||||
stdout_text.value = newJob.tool_stdout;
|
||||
}
|
||||
if (newJob.tool_stderr) {
|
||||
stderr_text.value = newJob.tool_stderr;
|
||||
}
|
||||
// Once the job reaches a terminal state, prefer its own tool_stdout/tool_stderr as the final,
|
||||
// complete output
|
||||
watch(job, (newJob) => {
|
||||
if (newJob && stateIsTerminal({ state: newJob.state })) {
|
||||
stdout_text.value = newJob.tool_stdout ?? "";
|
||||
stderr_text.value = newJob.tool_stderr ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
function updateConsoleOutputs(output: JobConsoleOutput) {
|
||||
// Keep stdout in memory and only fetch new text via JobProvider
|
||||
if (output) {
|
||||
if (output.stdout != null) {
|
||||
stdout_text.value += output.stdout;
|
||||
}
|
||||
if (output.stderr != null) {
|
||||
stderr_text.value += output.stderr;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function filterMetadata(jobMessages: JobMessage[]): Partial<JobMessage>[] {
|
||||
return jobMessages.map((item) => {
|
||||
@@ -168,17 +143,6 @@ watch(
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<JobDetailsProvider auto-refresh :job-id="props.jobId" @update:result="updateJob" />
|
||||
<JobConsoleOutputProvider
|
||||
v-if="jobIsRunning"
|
||||
auto-refresh
|
||||
:job-id="props.jobId"
|
||||
:stdout_position="stdout_position"
|
||||
:stdout_length="stdout_length"
|
||||
:stderr_position="stderr_position"
|
||||
:stderr_length="stderr_length"
|
||||
@update:result="updateConsoleOutputs" />
|
||||
|
||||
<template v-if="job && props.includeTitle">
|
||||
<JobHeader :job="job" />
|
||||
<hr />
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { computed, onUnmounted, type Ref, ref, watch } from "vue";
|
||||
|
||||
import { GalaxyApi } from "@/api";
|
||||
import type { JobConsoleOutput } from "@/api/jobs";
|
||||
import { useJobStore } from "@/stores/jobStore";
|
||||
import { rethrowSimple } from "@/utils/simple-error";
|
||||
import { stateIsTerminal } from "@/utils/utils";
|
||||
|
||||
const DEFAULT_POLL_INTERVAL_MS = 3000;
|
||||
|
||||
/** Reactively reads (and keeps polling) a job's full details via `jobStore`. */
|
||||
export function useJobDetails(jobId: Ref<string | undefined>, options: { autoRefresh?: boolean } = {}) {
|
||||
const { autoRefresh = true } = options;
|
||||
const jobStore = useJobStore();
|
||||
|
||||
const job = computed(() => jobStore.getJob(jobId.value ?? "") ?? null);
|
||||
const error = computed(() => (jobId.value ? jobStore.getJobLoadError(jobId.value) : null));
|
||||
const loading = computed(() => (jobId.value ? jobStore.isLoadingJob(jobId.value) : false));
|
||||
|
||||
watch(
|
||||
jobId,
|
||||
(id) => {
|
||||
if (autoRefresh && id) {
|
||||
jobStore.pollJobUntilTerminal({ id });
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
return { job, error, loading };
|
||||
}
|
||||
|
||||
/** Fetches and auto-polls a job's console output (stdout/stderr)*/
|
||||
export function useJobConsoleOutput(
|
||||
jobId: Ref<string | undefined>,
|
||||
options: { autoRefresh?: boolean; pollInterval?: number; chunkLength?: number } = {},
|
||||
) {
|
||||
const { autoRefresh = true, pollInterval = DEFAULT_POLL_INTERVAL_MS, chunkLength = 50000 } = options;
|
||||
|
||||
const stdout = ref("");
|
||||
const stderr = ref("");
|
||||
const state = ref<string | null | undefined>(undefined);
|
||||
const error = ref<unknown>(null);
|
||||
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
let currentJobId: string | undefined;
|
||||
|
||||
function clearPoll() {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchConsoleOutput(id: string) {
|
||||
const { data, error: fetchError } = await GalaxyApi().GET("/api/jobs/{job_id}/console_output", {
|
||||
params: {
|
||||
path: { job_id: id },
|
||||
query: {
|
||||
stdout_position: stdout.value.length,
|
||||
stdout_length: chunkLength,
|
||||
stderr_position: stderr.value.length,
|
||||
stderr_length: chunkLength,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (fetchError) {
|
||||
rethrowSimple(fetchError);
|
||||
}
|
||||
return data as JobConsoleOutput;
|
||||
}
|
||||
|
||||
async function poll(id: string) {
|
||||
try {
|
||||
const result = await fetchConsoleOutput(id);
|
||||
if (id !== currentJobId) {
|
||||
return;
|
||||
}
|
||||
if (result.stdout != null) {
|
||||
stdout.value += result.stdout;
|
||||
}
|
||||
if (result.stderr != null) {
|
||||
stderr.value += result.stderr;
|
||||
}
|
||||
state.value = result.state;
|
||||
error.value = null;
|
||||
} catch (e) {
|
||||
if (id !== currentJobId) {
|
||||
return;
|
||||
}
|
||||
error.value = e;
|
||||
}
|
||||
|
||||
if (id !== currentJobId) {
|
||||
return;
|
||||
}
|
||||
if (autoRefresh && !stateIsTerminal({ state: state.value })) {
|
||||
timeoutId = setTimeout(() => poll(id), pollInterval);
|
||||
}
|
||||
}
|
||||
|
||||
function restart(id: string | undefined) {
|
||||
clearPoll();
|
||||
currentJobId = id;
|
||||
stdout.value = "";
|
||||
stderr.value = "";
|
||||
state.value = undefined;
|
||||
error.value = null;
|
||||
if (id) {
|
||||
poll(id);
|
||||
}
|
||||
}
|
||||
|
||||
watch(jobId, (id) => restart(id), { immediate: true });
|
||||
|
||||
onUnmounted(clearPoll);
|
||||
|
||||
return { stdout, stderr, error };
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
*
|
||||
* Usage: mount(Component, { global: getLocalVue() })
|
||||
*/
|
||||
import flushPromises from "flush-promises";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { expect, vi } from "vitest";
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
@@ -289,3 +290,13 @@ export function mockUnprivilegedToolsRequest(server, http) {
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances Vitest's fake timers by `ms`, then flushes promises.
|
||||
* Requires `vi.useFakeTimers()` to have been called in the test file.
|
||||
* @example await advanceTimersAndFlush(1000);
|
||||
*/
|
||||
export async function advanceTimersAndFlush(ms) {
|
||||
vi.advanceTimersByTime(ms);
|
||||
await flushPromises();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user