Skip to content

Commit

Permalink
Merge pull request #21 from JulianFP/dev
Browse files Browse the repository at this point in the history
Add ability to abort jobs, add edit mode
  • Loading branch information
JulianFP authored Oct 25, 2024
2 parents 0f14e6a + 2979232 commit b3ccad2
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 18 deletions.
2 changes: 1 addition & 1 deletion src/components/confirmModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ let waitingForPromise = false;
export let open = false;
export let action: () => Promise<BackendResponse>;
export let response: BackendResponse | null;
export let response: BackendResponse | null = null;
async function submitAction(): Promise<void> {
waitingForPromise = true;
Expand Down
204 changes: 187 additions & 17 deletions src/routes/JobList.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import {
Button,
ButtonGroup,
Checkbox,
P,
Pagination,
Expand All @@ -13,6 +14,7 @@ import {
TableHead,
TableHeadCell,
TableSearch,
Tooltip,
} from "flowbite-svelte";
import type { LinkType } from "flowbite-svelte";
import {
Expand All @@ -21,18 +23,23 @@ import {
CaretUpSolid,
ChevronLeftOutline,
ChevronRightOutline,
CloseOutline,
DownloadSolid,
EditSolid,
PlusOutline,
StopSolid,
} from "flowbite-svelte-icons";
import CenterPage from "../components/centerPage.svelte";
import ConfirmModal from "../components/confirmModal.svelte";
import ErrorMsg from "../components/errorMsg.svelte";
import SubmitJobsModal from "../components/submitJobsModal.svelte";
import Waiting from "../components/waiting.svelte";
import {
type BackendResponse,
type JobStatus,
getLoggedIn,
postLoggedIn,
} from "../utils/httpRequests";
import { loginForward } from "../utils/navigation";
import { alerts, loggedIn, routing } from "../utils/stores";
Expand Down Expand Up @@ -62,10 +69,17 @@ let items: itemObj[] = [];
let jobDownloading: Record<number, boolean> = {};
let submitModalOpen = false;
let abortModalOpen = false;
let abortModalJobs: number[] = [];
let updatingJobList = 0; //0: not updating, 1: updating, 2: error while updating
let updatingJobListError = "";
let fetchedJobs = false;
let tableEditMode = false;
let itemsSelected: Record<number, boolean> = {};
let headerCheckboxSelected = false;
let selectedAbortButtonDisabled = true;
let searchTerm = "";
let searchTermEdited = false;
let sortKey: itemKey = "ID"; // default sort key
Expand Down Expand Up @@ -126,10 +140,25 @@ async function getJobInfo(
jobStatus.step = jobStatus.step != null ? jobStatus.step : "notReported";
jobStatus.runner = jobStatus.runner != null ? jobStatus.runner : -1;
//make sure that job status "aborted" stays the same until the backend updates it to "failed"
for (const item of items) {
if (item.ID === job.jobId) {
if (
item.status.step === "aborting" &&
!["failed", "success", "downloaded"].includes(jobStatus.step)
)
jobStatus.step = "aborting";
break;
}
}
//assign progress values. This is dependent on RUNNER_IN_PROGRESS so that if the user sorts after progress if will also respect the current step
if (jobStatus.progress == null) {
switch (jobStatus.step) {
case "failed":
jobStatus.progress = -6;
break;
case "aborting":
jobStatus.progress = -5;
break;
case "notQueued":
Expand All @@ -148,7 +177,7 @@ async function getJobInfo(
case "downloaded":
jobStatus.progress = 100;
break;
default: //if the step wasn't reported
default: //most notably also if step is notReported
jobStatus.progress = -4;
}
} else {
Expand Down Expand Up @@ -224,6 +253,55 @@ async function downloadTranscript(item: itemObj): Promise<void> {
jobDownloading[item.ID] = false;
}
async function abortJobs(jobIdsToAbort: number[]): Promise<BackendResponse> {
//change step of these items to "aborting"
let originalItems: [itemObj, number][] = [];
for (let i = 0; i < items.length; i++) {
if (jobIdsToAbort.includes(items[i].ID)) {
originalItems.push([structuredClone(items[i]), i]);
//set item to internal status "aborting", adjusting sorting and delete runner id from it
items[i].status = {
step: "aborting",
runner: -1,
};
items[i].progress = -5;
}
}
const JobIdsString = jobIdsToAbort.toString();
const abortJobsResponse: BackendResponse = await postLoggedIn("jobs/abort", {
jobIds: JobIdsString,
});
if (!abortJobsResponse.ok) {
alerts.add(
`Could not abort the jobs with the following IDs: ${JobIdsString}: ${abortJobsResponse.msg}`,
"red",
);
//reset state of items
for (const tuple of originalItems) {
items[tuple[1]] = tuple[0];
}
}
//after successfully performing action on item deselect them (only if performed through button in header)
if (jobIdsToAbort === selectedItems) itemsSelected = {};
return abortJobsResponse;
}
function openSubmitModal() {
if (!submitModalOpen && !abortModalOpen) {
submitModalOpen = true;
}
}
function openAbortModal(jobIds: number[]) {
if (!abortModalOpen && jobIds.length > 0 && !submitModalOpen) {
abortModalJobs = jobIds;
abortModalOpen = true;
}
}
function calcPages(): void {
pages = [];
let params = new URLSearchParams($routing.querystring); //copy because it will be modified down below
Expand Down Expand Up @@ -267,6 +345,22 @@ function setHideOld(newVal: boolean): void {
else routing.set({ params: { hideold: "0" } });
}
function updateHeaderCheckbox(item: itemObj | null = null) {
//update headerCheckbox
if (item == null || itemsSelected[item.ID]) {
let allSelected = true;
for (let item of displayItems) {
if (!itemsSelected[item.ID]) {
allSelected = false;
break;
}
}
headerCheckboxSelected = allSelected;
} else {
headerCheckboxSelected = false;
}
}
function paginationClickedHandler(): void {
paginationHandlerUnsubscribe = routing.subscribe((routingObject) => {
const newPage: string | null = routingObject.querystring.get("page");
Expand Down Expand Up @@ -397,7 +491,36 @@ $: {
}
//update displayItems when sortItems or page gets updated
$: displayItems = sortItems.slice((page - 1) * 10, page * 10);
$: {
displayItems = sortItems.slice((page - 1) * 10, page * 10);
updateHeaderCheckbox();
}
$: selectedItems = Object.entries(itemsSelected)
.filter(([_, v]) => v)
.map(([k, _]) => Number.parseInt(k));
$: {
if (selectedItems.length === 0) selectedAbortButtonDisabled = true;
else {
let returnValue = false;
for (const item of items) {
if (
selectedItems.includes(item.ID) &&
![
"notQueued",
"pendingRunner",
"runnerAssigned",
"runnerInProgress",
].includes(item.status.step)
) {
returnValue = true;
break;
}
}
selectedAbortButtonDisabled = returnValue;
}
}
</script>

<CenterPage title="Your transcription jobs">
Expand All @@ -412,14 +535,19 @@ $: displayItems = sortItems.slice((page - 1) * 10, page * 10);
{:else if updatingJobList == 2}
<P class="inline text-red-700 dark:text-red-500" size="sm" weight="medium">Error during update: {updatingJobListError}</P>
{/if}
<Button pill on:click={() => submitModalOpen = true}><PlusOutline class="mr-2"/>New Job</Button>
<Button pill on:click={() => openSubmitModal()}><PlusOutline class="mr-2"/>New Job</Button>
</div>
{#await getJobs()}
<Waiting/>
{:then response}
{#if response.ok}
<TableSearch shadow placeholder="Search by file name" hoverable={true} bind:inputValue={searchTerm}>
<TableHead>
{#if tableEditMode}
<TableHeadCell class="!p-4">
<Checkbox class="hover:cursor-pointer" bind:checked={headerCheckboxSelected} on:change={() => {displayItems.forEach((item) => itemsSelected[item.ID] = headerCheckboxSelected)}}/>
</TableHeadCell>
{/if}
{#each keys as key}
<TableHeadCell class="hover:dark:text-white hover:text-primary-600 hover:cursor-pointer" on:click={() => sortTable(key)}>
<div class="flex">
Expand All @@ -436,7 +564,23 @@ $: displayItems = sortItems.slice((page - 1) * 10, page * 10);
</div>
</TableHeadCell>
{/each}
<TableHeadCell/>
<TableHeadCell padding="py-1">
<ButtonGroup>
{#if tableEditMode}
<Button pill outline class="!p-2" size="xs" color="alternative" on:click={() => openAbortModal(selectedItems)} disabled={selectedAbortButtonDisabled}>
<StopSolid class="inline mr-1" color="red"/> {selectedItems.length}
</Button>
<Tooltip color="red">Abort selected jobs</Tooltip>
<Button pill outline class="!p-2" size="xs" color="alternative" on:click={() => tableEditMode = false}>
<CloseOutline/>
</Button>
{:else}
<Button pill outline class="!p-2" size="xs" color="alternative" on:click={() => {itemsSelected = {}; tableEditMode = true;}}>
<EditSolid/>
</Button>
{/if}
</ButtonGroup>
</TableHeadCell>
</TableHead>
<TableBody>
{#if items.length === 0}
Expand All @@ -449,11 +593,16 @@ $: displayItems = sortItems.slice((page - 1) * 10, page * 10);
</TableBodyRow>
{/if}
{#each displayItems as item, i}
<TableBodyRow on:click={() => toggleRow(i)} class="cursor-pointer">
<TableBodyRow on:click={() => toggleRow(i)}>
{#if tableEditMode}
<TableBodyCell class="!p-4">
<Checkbox class="hover:cursor-pointer" bind:checked={itemsSelected[item.ID]} on:change={() => updateHeaderCheckbox(item)} on:click={(e) => e.stopPropagation()}/>
</TableBodyCell>
{/if}
<TableBodyCell>{item.ID}</TableBodyCell>
<TableBodyCell>{(item.fileName.length <= 30) ? item.fileName : `${item.fileName.slice(0,30)}...`}</TableBodyCell>
<TableBodyCell class="w-full">
{#if item.status.step === "runnerInProgress"}
<TableBodyCell tdClass="pl-6 pr-4 py-4 whitespace-nowrap font-medium" class="w-full">
{#if (["runnerAssigned", "runnerInProgress"].includes(item.status.step))}
<Progressbar precision={2} progress={(item.progress < 0) ? 0 : item.progress} size="h-4" labelInside animate/>
{:else if item.status.step === "success"}
<Progressbar color="green" precision={2} progress={(item.progress < 0) ? 0 : item.progress} size="h-4" labelInside/>
Expand All @@ -462,25 +611,38 @@ $: displayItems = sortItems.slice((page - 1) * 10, page * 10);
<Progressbar color="red" progress={100} size="h-4"/>
{:else if item.status.step === "downloaded"}
<Progressbar color="indigo" precision={2} progress={(item.progress < 0) ? 0 : item.progress} size="h-4" labelInside/>
{:else if item.status.step === "aborting"}
<P class="text-red-700 dark:text-red-500" size="sm">aborting...</P>
<Progressbar color="yellow" progress={100} size="h-4"/>
{:else}
<Progressbar color="gray" precision={2} progress={(item.progress < 0) ? 0 : item.progress} size="h-4" labelInside/>
{/if}
</TableBodyCell>
<TableBodyCell tdClass="pr-6 py-4 whitespace-nowrap font-medium">
<Button size="xs" color="alternative"
disabled={!(item.status.step === "success" || item.status.step === "downloaded") || jobDownloading[item.ID]}
on:click={() => downloadTranscript(item)}>
{#if jobDownloading[item.ID]}
<Spinner size="5"/>
{:else}
<DownloadSolid/>
<TableBodyCell tdClass="pr-4 py-4 whitespace-nowrap font-medium text-center">
<ButtonGroup>
{#if (["notQueued", "pendingRunner", "runnerAssigned", "runnerInProgress"].includes(item.status.step))}
<Button pill outline class="!p-2" size="xs" color="alternative" on:click={(e) => {e.stopPropagation(); openAbortModal([item.ID]);}}>

<StopSolid color="red"/>
</Button>
<Tooltip color="red">Abort this job</Tooltip>
{/if}
</Button>
{#if (["success", "downloaded"].includes(item.status.step))}
<Button pill outline class="!p-2" size="xs" color="alternative" on:click={(e) => {e.stopPropagation(); downloadTranscript(item);}} disabled={jobDownloading[item.ID]}>
{#if jobDownloading[item.ID]}
<Spinner size="5"/>
{:else}
<DownloadSolid/>
{/if}
</Button>
<Tooltip color="green">Download finished transcript</Tooltip>
{/if}
</ButtonGroup>
</TableBodyCell>
</TableBodyRow>
{#if openRow === i}
<TableBodyRow color="custom" class="bg-slate-100 dark:bg-slate-700">
<TableBodyCell colspan="4">
<TableBodyCell colspan={tableEditMode ? "5" : "4"}>
<div class="grid grid-cols-2 gap-x-8 gap-y-2">
<div class="col-span-full">
<P class="inline" weight="extrabold" size="sm">Filename: </P>
Expand Down Expand Up @@ -540,3 +702,11 @@ $: displayItems = sortItems.slice((page - 1) * 10, page * 10);
</CenterPage>

<SubmitJobsModal bind:open={submitModalOpen} on:afterSubmit={(event) => {getJobInfo(event.detail.jobIds);}}/>

<ConfirmModal bind:open={abortModalOpen} action={() => abortJobs(abortModalJobs)}>
{#if abortModalJobs.length === 1}
Job {abortModalJobs[0].toString()} will be aborted and its current transcription progress will be lost.
{:else}
The jobs {abortModalJobs.join(", ")} will be aborted and their current transcription progress will be lost.
{/if}
</ConfirmModal>
1 change: 1 addition & 0 deletions src/utils/httpRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type ErrorType =

export type JobStep =
| "notReported" //this is not an actual step returned by the backend but a value that I internally assign if the backend doesn't return anything
| "aborting" //this also only exists internally and is used to show an animation while the backend aborts a job
| "failed"
| "notQueued"
| "pendingRunner"
Expand Down

0 comments on commit b3ccad2

Please sign in to comment.