diff --git a/backend/src/predicTCR_server/model.py b/backend/src/predicTCR_server/model.py
index 8f7e06c..046a298 100644
--- a/backend/src/predicTCR_server/model.py
+++ b/backend/src/predicTCR_server/model.py
@@ -81,7 +81,8 @@ class Sample(db.Model):
tumor_type: Mapped[str] = mapped_column(String(128), nullable=False)
source: Mapped[str] = mapped_column(String(128), nullable=False)
timestamp: Mapped[int] = mapped_column(Integer, nullable=False)
- timestamp_results: Mapped[int] = mapped_column(Integer, nullable=False)
+ timestamp_job_start: Mapped[int] = mapped_column(Integer, nullable=False)
+ timestamp_job_end: Mapped[int] = mapped_column(Integer, nullable=False)
status: Mapped[Status] = mapped_column(Enum(Status), nullable=False)
has_results_zip: Mapped[bool] = mapped_column(Boolean, nullable=False)
@@ -161,7 +162,7 @@ def request_job() -> int | None:
db.select(Sample).filter(
(Sample.status == Status.RUNNING)
& (
- timestamp_now() - Sample.timestamp_results
+ timestamp_now() - Sample.timestamp_job_start
> job_timeout_minutes * 60
)
)
@@ -186,7 +187,8 @@ def request_job() -> int | None:
return None
else:
logger.info(f" --> sample id {sample.id}")
- sample.timestamp_results = timestamp_now()
+ sample.timestamp_job_start = timestamp_now()
+ sample.timestamp_job_end = 0
sample.status = Status.RUNNING
db.session.commit()
return sample.id
@@ -210,6 +212,7 @@ def process_result(
if job is None:
logger.warning(f" --> Unknown job id {job_id}")
return f"Unknown job id {job_id}", 400
+ sample.timestamp_job_end = timestamp_now()
job.timestamp_end = timestamp_now()
if success:
job.status = Status.COMPLETED
@@ -496,7 +499,8 @@ def add_new_sample(
tumor_type=tumor_type,
source=source,
timestamp=timestamp_now(),
- timestamp_results=0,
+ timestamp_job_start=0,
+ timestamp_job_end=0,
status=Status.QUEUED,
has_results_zip=False,
)
diff --git a/backend/tests/helpers/flask_test_utils.py b/backend/tests/helpers/flask_test_utils.py
index e82bd21..3a2da58 100644
--- a/backend/tests/helpers/flask_test_utils.py
+++ b/backend/tests/helpers/flask_test_utils.py
@@ -54,7 +54,8 @@ def add_test_samples(app, data_path: pathlib.Path):
tumor_type=f"tumor_type{sample_id}",
source=f"source{sample_id}",
timestamp=sample_id,
- timestamp_results=0,
+ timestamp_job_start=0,
+ timestamp_job_end=0,
status=status,
has_results_zip=False,
)
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index cab301a..df8300f 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -44,7 +44,7 @@ const login_title = computed(() => {
-
+
diff --git a/frontend/src/components/SamplesTable.vue b/frontend/src/components/SamplesTable.vue
index 78e9ab6..a438e59 100644
--- a/frontend/src/components/SamplesTable.vue
+++ b/frontend/src/components/SamplesTable.vue
@@ -18,11 +18,12 @@ import {
download_result,
download_admin_result,
logout,
+ download_string_as_file,
} from "@/utils/api-client";
import type { Sample } from "@/utils/types";
import { ref } from "vue";
-defineProps<{
+const props = defineProps<{
samples: Sample[];
admin: boolean;
}>();
@@ -66,6 +67,29 @@ function delete_current_sample() {
console.log(error);
});
}
+
+function timestamp_to_date(timestamp_secs: number): string {
+ const secs_to_ms = 1000;
+ return new Date(timestamp_secs * secs_to_ms).toLocaleDateString("de-DE");
+}
+
+function job_runtime(sample: Sample): string {
+ // returns job runtime as hh::mm::ss
+ const runtime_secs = sample.timestamp_job_end - sample.timestamp_job_start;
+ const secs_to_ms = 1000;
+ if (runtime_secs <= 0) {
+ return "-";
+ }
+ return new Date(runtime_secs * secs_to_ms).toISOString().slice(11, 19);
+}
+
+function download_samples_as_csv() {
+ let csv = "Id,Date,Email,SampleName,TumorType,Source,Status,Runtime\n";
+ for (const sample of props.samples) {
+ csv += `${sample.id},${timestamp_to_date(sample.timestamp)},${sample.email},${sample.name},${sample.tumor_type},${sample.source},${sample.status},${job_runtime(sample)}\n`;
+ }
+ download_string_as_file("samples.csv", csv);
+}
@@ -78,6 +102,7 @@ function delete_current_sample() {
Tumor type
Source
Status
+ Runtime
Inputs
Results
Actions
@@ -88,15 +113,16 @@ function delete_current_sample() {
:key="sample.id"
:class="sample.status === 'failed' ? '!bg-red-200' : '!bg-slate-50'"
>
- {{ sample["id"] }}
+ {{ sample.id }}
{{
- new Date(sample["timestamp"] * 1000).toLocaleDateString("de-DE")
+ timestamp_to_date(sample.timestamp)
}}
- {{ sample["email"] }}
- {{ sample["name"] }}
- {{ sample["tumor_type"] }}
- {{ sample["source"] }}
- {{ sample["status"] }}
+ {{ sample.email }}
+ {{ sample.name }}
+ {{ sample.tumor_type }}
+ {{ sample.source }}
+ {{ sample.status }}
+ {{ job_runtime(sample) }}
+ Download as CSV
diff --git a/frontend/src/components/SettingsTable.vue b/frontend/src/components/SettingsTable.vue
index a0ffee6..f72cc22 100644
--- a/frontend/src/components/SettingsTable.vue
+++ b/frontend/src/components/SettingsTable.vue
@@ -86,7 +86,7 @@ function update_settings() {
:label="`Timeout for runner jobs: ${settings.runner_job_timeout_mins} minutes`"
class="mb-2"
/>
-
+
Save settings
diff --git a/frontend/src/components/UsersTable.vue b/frontend/src/components/UsersTable.vue
index 64432ea..93ef6d6 100644
--- a/frontend/src/components/UsersTable.vue
+++ b/frontend/src/components/UsersTable.vue
@@ -12,7 +12,7 @@ import {
FwbRange,
} from "flowbite-vue";
import type { User } from "@/utils/types";
-import { apiClient, logout } from "@/utils/api-client";
+import { apiClient, download_string_as_file, logout } from "@/utils/api-client";
import { ref, computed } from "vue";
const props = defineProps<{
@@ -63,6 +63,17 @@ function update_user() {
console.log(error);
});
}
+
+function download_users_as_csv() {
+ let csv =
+ "Id,Email,Activated,Enabled,FullResults,Quota,SubmissionIntervalMinutes,Admin\n";
+ for (const user of users.value) {
+ if (!user.is_runner) {
+ csv += `${user.id},${user.email},${user.activated},${user.enabled},${user.full_results},${user.quota},${user.submission_interval_minutes},${user.is_admin}\n`;
+ }
+ }
+ download_string_as_file("users.csv", csv);
+}
@@ -114,6 +125,9 @@ function update_user() {
+ Download as CSV
diff --git a/frontend/src/utils/api-client.ts b/frontend/src/utils/api-client.ts
index 6671685..53d0c49 100644
--- a/frontend/src/utils/api-client.ts
+++ b/frontend/src/utils/api-client.ts
@@ -80,3 +80,12 @@ export function logout() {
user.user = null;
user.token = "";
}
+
+export function download_string_as_file(filename: string, str: string) {
+ const link = document.createElement("a");
+ const file = new Blob([str], { type: "text/plain" });
+ link.href = URL.createObjectURL(file);
+ link.download = filename;
+ link.click();
+ URL.revokeObjectURL(link.href);
+}
diff --git a/frontend/src/utils/types.ts b/frontend/src/utils/types.ts
index ead3216..9b5fef2 100644
--- a/frontend/src/utils/types.ts
+++ b/frontend/src/utils/types.ts
@@ -5,7 +5,8 @@ export type Sample = {
tumor_type: string;
source: number;
timestamp: number;
- timestamp_results: number;
+ timestamp_job_start: number;
+ timestamp_job_end: number;
status: string;
has_results_zip: boolean;
};
diff --git a/frontend/src/views/AboutView.vue b/frontend/src/views/AboutView.vue
index d048c3c..00bebca 100644
--- a/frontend/src/views/AboutView.vue
+++ b/frontend/src/views/AboutView.vue
@@ -23,7 +23,7 @@ function openModalSignup() {
-
+
-
+
{{ message }}
Go to login page.
diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue
index ea09a49..c6a2ddb 100644
--- a/frontend/src/views/LoginView.vue
+++ b/frontend/src/views/LoginView.vue
@@ -8,7 +8,7 @@ const userStore = useUserStore();
-
+
diff --git a/frontend/src/views/ResetPasswordView.vue b/frontend/src/views/ResetPasswordView.vue
index 3762196..f03ec7a 100644
--- a/frontend/src/views/ResetPasswordView.vue
+++ b/frontend/src/views/ResetPasswordView.vue
@@ -62,7 +62,7 @@ function reset_password() {
-
+