Skip to content

Commit

Permalink
add a tab view for jobs under a step in invocation view
Browse files Browse the repository at this point in the history
  • Loading branch information
ahmedhamidawan committed Aug 1, 2024
1 parent 3f9c0bb commit feb4bf9
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 12 deletions.
1 change: 1 addition & 0 deletions client/src/api/jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const fetchJobDestinationParams = fetcher.path("/api/jobs/{job_id}/destin
export const jobsFetcher = fetcher.path("/api/jobs").method("get").create();

export type ShowFullJobResponse = components["schemas"]["ShowFullJobResponse"];
export type JobBaseModel = components["schemas"]["JobBaseModel"];
export type JobDetails = components["schemas"]["ShowFullJobResponse"] | components["schemas"]["EncodedJobDetails"];
export const fetchJobDetails = fetcher.path("/api/jobs/{job_id}").method("get").create();

Expand Down
105 changes: 105 additions & 0 deletions client/src/components/WorkflowInvocationState/JobStepTabs.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faClock } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BAlert, BTab, BTabs } from "bootstrap-vue";
import Vue, { computed, ref, watch } from "vue";
import { fetchJobDetails, type JobBaseModel, type JobDetails } from "@/api/jobs";
import { getHeaderClass, iconClasses } from "@/composables/useInvocationGraph";
import { rethrowSimple } from "@/utils/simple-error";
import JobInformation from "../JobInformation/JobInformation.vue";
import JobParameters from "../JobParameters/JobParameters.vue";
import LoadingSpan from "../LoadingSpan.vue";
import UtcDate from "../UtcDate.vue";
library.add(faClock);
interface Props {
jobs: JobBaseModel[];
}
const props = withDefaults(defineProps<Props>(), {
jobs: () => [],
});
const loading = ref(true);
const initialLoading = ref(true);
const jobsDetails = ref<{ [key: string]: JobDetails }>({});
watch(
() => props.jobs,
async (propJobs: JobBaseModel[]) => {
loading.value = true;
for (const job of propJobs) {
try {
const { data } = await fetchJobDetails({ job_id: job.id, full: true });
Vue.set(jobsDetails.value, job.id, data);
} catch (e) {
rethrowSimple(e);
}
}
if (initialLoading.value) {
initialLoading.value = false;
}
loading.value = false;
},
{ immediate: true }
);
const firstJob = computed(() => Object.values(jobsDetails.value)[0]);
const jobCount = computed(() => Object.keys(jobsDetails.value).length);
function getIcon(job: JobDetails) {
return iconClasses[job.state];
}
function getTabClass(job: JobDetails) {
return {
...getHeaderClass(job.state),
"d-flex": true,
"text-center": true,
};
}
</script>

<template>
<BAlert v-if="initialLoading" variant="info" show>
<LoadingSpan message="Loading Jobs" />
</BAlert>
<BAlert v-else-if="!jobsDetails || !jobCount" variant="info" show> No jobs found for this step. </BAlert>
<div v-else-if="jobCount === 1 && firstJob">
<JobInformation :job_id="firstJob.id" />
<p></p>
<JobParameters :job-id="firstJob.id" :include-title="false" />
</div>
<BTabs v-else vertical pills card nav-class="p-0">
<BTab v-for="job in jobsDetails" :key="job.id" :title-item-class="getTabClass(job)" title-link-class="w-100">
<template v-slot:title>
{{ job.state }}
<FontAwesomeIcon
v-if="getIcon(job)"
:class="getIcon(job)?.class"
:icon="getIcon(job)?.icon"
:spin="getIcon(job)?.spin" />
</template>
<div>
<div class="d-flex justify-content-between">
<i>
<FontAwesomeIcon :icon="faClock" class="mr-1" />run
<UtcDate :date="job.create_time" mode="elapsed" />
</i>
<i>
<FontAwesomeIcon :icon="faClock" class="mr-1" />updated
<UtcDate :date="job.update_time" mode="elapsed" />
</i>
</div>
<hr class="w-100" />
<JobInformation :job_id="job.id" />
<p></p>
<JobParameters :job-id="job.id" :include-title="false" />
</div>
</BTab>
</BTabs>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,18 @@
class="invocation-step-job-details"
:open="inGraphView">
<summary>
<b>Jobs <i>(Click on any job to view its details)</i></b>
<b>{{ jobStepHeading(stepDetails) }}</b>
</summary>
<JobStep
v-if="stepDetails.jobs?.length"
:key="inGraphView"
:jobs="stepDetails.jobs"
:invocation-graph="inGraphView"
:showing-job-id="showingJobId"
@row-clicked="showJob" />
<span v-if="stepDetails.jobs?.length">
<JobStep
v-if="!inGraphView"
:key="inGraphView"
:jobs="stepDetails.jobs"
:invocation-graph="inGraphView"
:showing-job-id="showingJobId"
@row-clicked="showJob" />
<JobStepTabs v-else class="mt-1" :jobs="stepDetails.jobs" />
</span>
<b-alert v-else v-localize variant="info" show>This step has no jobs</b-alert>
</details>
<ParameterStep
Expand Down Expand Up @@ -119,12 +122,14 @@ import JobStep from "./JobStep";
import ParameterStep from "./ParameterStep";
import WorkflowStepTitle from "./WorkflowStepTitle";
import JobStepTabs from "./JobStepTabs.vue";
import WorkflowInvocationStepHeader from "./WorkflowInvocationStepHeader.vue";
export default {
components: {
LoadingSpan,
JobStep,
JobStepTabs,
ParameterStep,
InvocationStepProvider,
GenericHistoryItem,
Expand Down Expand Up @@ -197,6 +202,19 @@ export default {
(param) => param.workflow_step_id === stepDetails.workflow_step_id
);
},
jobStepHeading(stepDetails) {
if (stepDetails.jobs?.length > 1) {
return "Jobs (Click on any job to view its details)";
} else if (stepDetails.jobs?.length === 1) {
if (this.inGraphView) {
return "Job";
} else {
return "Job (Click on the job to view its details)";
}
} else {
return "No jobs";
}
},
showJob(id) {
this.$emit("show-job", id);
},
Expand Down
12 changes: 8 additions & 4 deletions client/src/composables/useInvocationGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,7 @@ export function useInvocationGraph(
}

/** Setting the header class for the graph step */
graphStepFromWfStep.headerClass = {
"node-header-invocation": true,
[`header-${graphStepFromWfStep.state}`]: !!graphStepFromWfStep.state,
};
graphStepFromWfStep.headerClass = getHeaderClass(graphStepFromWfStep.state as string);
// TODO: maybe a different one for inputs? Currently they have no state either.

/** Setting the header icon for the graph step */
Expand Down Expand Up @@ -279,3 +276,10 @@ export function useInvocationGraph(
loadInvocationGraph,
};
}

export function getHeaderClass(state: string) {
return {
"node-header-invocation": true,
[`header-${state}`]: !!state,
};
}

0 comments on commit feb4bf9

Please sign in to comment.