Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@
"options": {
"polyfills": ["zone.js", "zone.js/testing"],
"tsConfig": "tsconfig.spec.json",
"codeCoverageExclude": [
"src/app/features/workflows/components/molstar-viewer/molstar-viewer.component.ts"
],
"assets": [
{
"glob": "**/*",
Expand All @@ -117,7 +120,17 @@
"styles": ["src/styles.scss"],
"scripts": [],
"karmaConfig": "karma.conf.js",
"watch": false
"watch": false,
"fileReplacements": [
{
"replace": "node_modules/h264-mp4-encoder/embuild/dist/h264-mp4-encoder.node.js",
"with": "src/stubs/h264-mp4-encoder.stub.js"
}
],
"loader": {
".jpg": "file",
".png": "file"
}
}
},
"lint": {
Expand Down
195 changes: 97 additions & 98 deletions src/app/features/jobs/jobs-list/jobs-list.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<!-- Page Header -->
<div class="space-y-6">
<h1 class="text-4xl font-semibold">My Jobs</h1>
<p class="text-gray-600">View and manage my submitted workflow runs</p>
<p class="text-gray-600">View and manage submitted workflow runs</p>
</div>

@if (!authLoading()) { @if (!canExecuteWorkflows()) {
Expand Down Expand Up @@ -65,97 +65,101 @@ <h1 class="text-4xl font-semibold">My Jobs</h1>
</div>
} @else {
<!-- Search and Filter Bar -->
<div class="pt-4 bg-gray-50 sticky top-10 z-20 -mx-6 px-6">
<div class="bg-white mt-12 mb-6 shadow-sm rounded-lg border p-4">
<div
class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between"
>
<!-- Search Input -->
<div class="w-full md:w-2/3">
<label for="search" class="mb-1 block text-sm font-medium">
Search
</label>
<div
class="flex h-10 w-full text-sm flex-1 items-center justify-between rounded-md border border-gray-300 bg-white pl-4 pr-2 focus-within:ring-2 focus-within:ring-sky-100"
>
<input
type="text"
id="search"
class="flex-1 focus:outline-none"
[ngModel]="searchQuery()"
(ngModelChange)="onSearch($event)"
placeholder="Search by job name, theme, workflow type or tool…"
/>
<button
type="button"
class="flex items-center justify-center rounded-full p-1 transition-all hover:bg-gray-200"
aria-label="Search"
<div
class="sticky top-14 z-20 -mx-6 px-6 before:content-[''] before:absolute before:inset-y-0 before:left-0 before:w-6 before:bg-gray-50 after:content-[''] after:absolute after:inset-y-0 after:right-0 after:w-6 after:bg-gray-50"
>
<div class="bg-gray-50 pt-6 rounded-b-lg">
<div class="bg-white mt-6 mb-6 shadow-sm rounded-lg border p-4">
<div
class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between"
>
<!-- Search Input -->
<div class="w-full md:w-2/3">
<label for="search" class="mb-1 block text-sm font-medium">
Search
</label>
<div
class="flex h-10 w-full text-sm flex-1 items-center justify-between rounded-md border border-gray-300 bg-white pl-4 pr-2 focus-within:ring-2 focus-within:ring-sky-100"
>
<ng-icon name="heroMagnifyingGlass" size="16"></ng-icon>
</button>
<input
type="text"
id="search"
class="flex-1 focus:outline-none"
[ngModel]="searchQuery()"
(ngModelChange)="onSearch($event)"
placeholder="Search by job name, theme, workflow type or tool…"
/>
<button
type="button"
class="flex items-center justify-center rounded-full p-1 transition-all hover:bg-gray-200"
aria-label="Search"
>
<ng-icon name="heroMagnifyingGlass" size="16"></ng-icon>
</button>
</div>
</div>
</div>

<!-- Status Filter + Clear -->
<div class="flex items-end gap-4 w-full md:w-1/3">
<div class="flex-1">
<p class="mb-1 block text-sm font-medium">Filter</p>
<app-dropdown-menu
[isOpen]="showStatusDropdown()"
(isOpenChange)="showStatusDropdown.set($event)"
>
<ng-template #trigger>
<button
type="button"
(click)="toggleStatusDropdown()"
[attr.aria-expanded]="showStatusDropdown()"
aria-haspopup="true"
class="flex h-10 w-full items-center justify-between rounded-md border border-gray-300 bg-white px-4 focus-within:ring-2 focus-within:ring-sky-100 focus-visible:outline-none hover:cursor-pointer"
>
<span class="flex items-center">
<span class="text-sm font-medium mr-2">Status:</span>
@if (selectedStatuses().length > 0) {
<span
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-sky-100 text-sky-800"
>
{{ selectedStatuses().length }}
<!-- Status Filter + Clear -->
<div class="flex items-end gap-4 w-full md:w-1/3">
<div class="flex-1">
<p class="mb-1 block text-sm font-medium">Filter</p>
<app-dropdown-menu
[isOpen]="showStatusDropdown()"
(isOpenChange)="showStatusDropdown.set($event)"
>
<ng-template #trigger>
<button
type="button"
(click)="toggleStatusDropdown()"
[attr.aria-expanded]="showStatusDropdown()"
aria-haspopup="true"
class="flex h-10 w-full items-center justify-between rounded-md border border-gray-300 bg-white px-4 focus-within:ring-2 focus-within:ring-sky-100 focus-visible:outline-none hover:cursor-pointer"
>
<span class="flex items-center">
<span class="text-sm font-medium mr-2">Status:</span>
@if (selectedStatuses().length > 0) {
<span
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-sky-100 text-sky-800"
>
{{ selectedStatuses().length }}
</span>
} @else {
<span class="text-sm text-gray-500">All</span>
}
</span>
} @else {
<span class="text-sm text-gray-500">All</span>
}
</span>
<ng-icon name="heroChevronDown" size="16"></ng-icon>
</button>
</ng-template>
<ng-template #menu>
@for (status of statusOptions; track status) {
<label
class="flex cursor-pointer items-center px-4 py-2 text-sm hover:bg-gray-100"
>
<input
type="checkbox"
[checked]="isStatusSelected(status)"
(change)="toggleStatus(status)"
class="mr-3 h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-500"
/>
<span>{{ status }}</span>
</label>
}
</ng-template>
</app-dropdown-menu>
</div>
<ng-icon name="heroChevronDown" size="16"></ng-icon>
</button>
</ng-template>
<ng-template #menu>
@for (status of statusOptions; track status) {
<label
class="flex cursor-pointer items-center px-4 py-2 text-sm hover:bg-gray-100"
>
<input
type="checkbox"
[checked]="isStatusSelected(status)"
(change)="toggleStatus(status)"
class="mr-3 h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-500"
/>
<span>{{ status }}</span>
</label>
}
</ng-template>
</app-dropdown-menu>
</div>

<!-- Clear Filters Button -->
<button
type="button"
(click)="clearFilters()"
[disabled]="selectedStatuses().length === 0"
[class.opacity-50]="selectedStatuses().length === 0"
[class.cursor-not-allowed]="selectedStatuses().length === 0"
class="h-10 whitespace-nowrap rounded-md border border-gray-300 bg-white px-4 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:hover:bg-white"
>
Clear Filters
</button>
<!-- Clear Filters Button -->
<button
type="button"
(click)="clearFilters()"
[disabled]="selectedStatuses().length === 0"
[class.opacity-50]="selectedStatuses().length === 0"
[class.cursor-not-allowed]="selectedStatuses().length === 0"
class="h-10 whitespace-nowrap rounded-md border border-gray-300 bg-white px-4 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:hover:bg-white"
>
Clear Filters
</button>
</div>
</div>
</div>
</div>
Expand Down Expand Up @@ -334,21 +338,16 @@ <h2 class="text-2xl font-semibold">All jobs</h2>
{{ job.tool || "—" }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
@if (isInProgress(job.status)) {
<span
class="inline-flex gap-2 items-center text-sm font-semibold text-gray-500"
>
<ng-icon name="heroClock" size="16"></ng-icon>
In progress...
</span>
} @else {
<span
[class]="getStatusClass(job.status)"
class="px-2 py-1 inline-flex text-xs font-semibold rounded-full"
class="px-2 py-1 inline-flex justify-center items-center text-xs font-semibold rounded-full"
>
{{ job.status }}
@if (job.status === 'In progress') {
<span
class="h-1.5 w-1.5 rounded-full mr-1.5 bg-blue-600 animate-pulse"
></span>
} {{ job.status }}
</span>
}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ job.submittedAt | date:'dd/MM/yyyy' }}
Expand Down
14 changes: 8 additions & 6 deletions src/app/features/jobs/jobs-list/jobs-list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,18 +421,20 @@ describe("JobsListComponent", () => {
expect(component.getStatusClass("Completed")).toBe(
"bg-green-100 text-green-800"
);
expect(component.getStatusClass("In progress")).toBe("text-gray-700");
expect(component.getStatusClass("Pending")).toBe("bg-sky-100 text-sky-800");
expect(component.getStatusClass("In progress")).toBe(
"bg-blue-100 text-blue-800"
);
expect(component.getStatusClass("In queue")).toBe(
"bg-white text-black border border-black"
"bg-gray-100 text-gray-800"
);
expect(component.getStatusClass("Failed")).toBe("bg-red-600 text-white");
expect(component.getStatusClass("Failed")).toBe("bg-red-100 text-red-800");
expect(component.getStatusClass("Stopped")).toBe(
"bg-amber-100 text-amber-700"
"bg-amber-100 text-amber-800"
);
expect(component.getStatusClass("Unknown")).toBe(
"bg-gray-100 text-gray-900"
"bg-gray-100 text-gray-800"
);
expect(component.isInProgress("In progress")).toBeTrue();
});

it("should toggle individual and all job selections", () => {
Expand Down
16 changes: 7 additions & 9 deletions src/app/features/jobs/jobs-list/jobs-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,23 +371,21 @@ export default class JobsListComponent implements OnInit, OnDestroy {
switch (status) {
case "Completed":
return "bg-green-100 text-green-800";
case "Pending":
return "bg-sky-100 text-sky-800";
case "In progress":
return "text-gray-700";
return "bg-blue-100 text-blue-800";
case "In queue":
return "bg-white text-black border border-black";
return "bg-gray-100 text-gray-800";
case "Failed":
return "bg-red-600 text-white";
return "bg-red-100 text-red-800";
case "Stopped":
return "bg-amber-100 text-amber-700";
return "bg-amber-100 text-amber-800";
default:
return "bg-gray-100 text-gray-900";
return "bg-gray-100 text-gray-800";
}
}

isInProgress(status: string): boolean {
return status === "In progress";
}

/**
* Toggle selection for a single job
*/
Expand Down
17 changes: 5 additions & 12 deletions src/app/features/workflows/bulk-prediction/bulk-prediction.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import { CommonModule } from "@angular/common";
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
Signal,
signal,
} from "@angular/core";
import { Component, computed, inject, Signal, signal } from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import {
AbstractControl,
Expand Down Expand Up @@ -52,7 +45,6 @@ interface ToolChip extends ToolOption {

@Component({
selector: "app-bulk-prediction",
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
ReactiveFormsModule,
Expand Down Expand Up @@ -93,7 +85,8 @@ export default class BulkPredictionComponent extends WorkflowPageBase {
fasta: ["", bulkFastaValidator],
});
private formStatus = toSignal(
this.form.statusChanges.pipe(startWith(this.form.status))
this.form.statusChanges.pipe(startWith(this.form.status)),
{ requireSync: true }
);
private formValue: Signal<{ jobName: string; fasta: string }> = toSignal(
this.form.valueChanges.pipe(
Expand Down Expand Up @@ -181,7 +174,7 @@ export default class BulkPredictionComponent extends WorkflowPageBase {

// Submission
private buildBulkPayload(): { id: string; sequence: string }[] {
const entries = parseMultiFasta(this.form.value.fasta ?? "");
const entries = parseMultiFasta(this.form.getRawValue().fasta);
return entries.map((e) => ({ id: e.header, sequence: e.sequence }));
}

Expand All @@ -190,7 +183,7 @@ export default class BulkPredictionComponent extends WorkflowPageBase {
}

protected performSubmit(): void {
const jobName = this.form.value.jobName ?? "";
const jobName = this.form.getRawValue().jobName;
const sequences = this.buildBulkPayload();

this.workflowSubmission.isSubmitting.set(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
[attr.aria-controls]="menuId"
(click)="toggleMenu()"
class="flex h-10 w-full items-center justify-between rounded-md border bg-white px-4 text-left text-sm text-gray-900 transition-colors focus:outline-none focus:ring-2"
[class.border-red-500]="invalid()"
[class.focus:ring-red-100]="invalid()"
[class.focus:ring-sky-100]="!invalid()"
[class]="
invalid()
? 'border-red-500 focus:ring-red-100'
: 'border-gray-300 focus:ring-sky-100'
"
>
<span class="block min-w-0 truncate">{{ selectedLabel }}</span>
<svg
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ describe("WorkflowPreviewModalComponent", () => {
expect(component).toBeTruthy();
});

it("should default the title to 'Workflow Preview'", () => {
it("should default the title to 'Review & Submit'", () => {
fixture.componentRef.setInput("isOpen", true);
fixture.detectChanges();
const heading = fixture.nativeElement.querySelector(
"#workflow-preview-title"
);
expect(heading?.textContent?.trim()).toBe("Workflow Preview");
expect(heading?.textContent?.trim()).toBe("Review & Submit");
});

it("should label the submit button with the credit cost", () => {
Expand Down
Loading