Skip to content
Draft
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
2 changes: 1 addition & 1 deletion Pulumi.cape-cod-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1311,7 +1311,7 @@ config:
- s3
- name: "cape-frontend"
short_name: "cfe"
image: "ami-07fae65036e62d931"
image: "ami-08143e7b201031ca6"
public_ip: False
instance_type: "t3a.medium"
subnet_types:
Expand Down
23 changes: 20 additions & 3 deletions assets/api/capi/capi-openapi-301.yaml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -915,10 +915,27 @@ paths:
schema:
type: object
description:
A mapping of report name to the report HTML.
Empty when the sample has no reports.
A mapping of report name to an object with
the report HTML and the time it was
generated. Empty when the sample has no
reports.
additionalProperties:
type: string
type: object
properties:
createdAt:
type: string
format: date-time
description:
RFC 3339 / ISO 8601 UTC time the
report was generated, parsable
by the JavaScript Date
constructor.
body:
type: string
description: The report HTML.
required:
- createdAt
- body
"500":
description:
"Server Error - Unable to fetch reports."
Expand Down
39 changes: 32 additions & 7 deletions assets/api/capi/handlers/get_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,26 @@
bucket under `reports/<sample_id>/<report_name>.html` (e.g. the RABiTS report at
`reports/<sample_id>/rabits.html`). This handler lists those objects for a given
sample and returns a mapping of report name (the object file name without the
`.html` suffix) to the report HTML, e.g.:
`.html` suffix) to an object holding the report HTML and the time it was
generated (the object's last-modified time), e.g.:

{"rabits": "<html>...</html>", "bactopia": "<html>...</html>"}
{
"rabits": {"createdAt": "2024-06-01T12:34:56Z", "body": "<html>..."},
"bactopia": {"createdAt": "2024-06-02T09:00:00Z", "body": "<html>..."}
}

so the front end can render every available report (e.g. as an accordion)
without any further per-report requests. A sample with no reports (or an unknown
sample) returns an empty object.
so the front end can render every available report (e.g. as an accordion) and
sort/label them by generation time without any further per-report requests. A
sample with no reports (or an unknown sample) returns an empty object.

`createdAt` is an RFC 3339 / ISO 8601 UTC timestamp, directly parsable by
`new Date(...)` in JavaScript/TypeScript.
"""

import json
import logging
import os
from datetime import timezone

import boto3
from botocore.exceptions import ClientError
Expand All @@ -30,6 +38,10 @@
REPORTS_PREFIX = "reports"
REPORT_SUFFIX = ".html"

# keys of the per-report object returned for each report.
REPORT_CREATED_AT_KEY = "createdAt"
REPORT_BODY_KEY = "body"

# name of the env var (set by the private swimlane api wiring) holding the name
# of the artifacts bucket that reports are stored in.
REPORTS_BUCKET_ENV_VAR = "REPORTS_BUCKET"
Expand All @@ -44,7 +56,9 @@ def get_sample_reports(bucket, sample_id):

Returns:
A mapping of report name (object file name without the `.html` suffix)
to the report HTML string. Empty when the sample has no reports.
to an object of the form `{"createdAt": <iso8601-utc>, "body": <html>}`,
where `createdAt` is the object's last-modified time. Empty when the
sample has no reports.
"""
s3_client = boto3.client("s3")
prefix = f"{REPORTS_PREFIX}/{sample_id}/"
Expand All @@ -65,7 +79,18 @@ def get_sample_reports(bucket, sample_id):
.read()
.decode("utf-8")
)
reports[name[: -len(REPORT_SUFFIX)]] = report_html
# s3 last-modified is a tz-aware utc datetime; emit rfc 3339 with a
# `Z` suffix so it parses directly in javascript/typescript.
created_at = (
obj["LastModified"]
.astimezone(timezone.utc)
.isoformat()
.replace("+00:00", "Z")
)
reports[name[: -len(REPORT_SUFFIX)]] = {
REPORT_CREATED_AT_KEY: created_at,
REPORT_BODY_KEY: report_html,
}

return reports

Expand Down
231 changes: 184 additions & 47 deletions assets/etl/etl_caerbannog_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,58 +292,195 @@ def stoplight_report_rows(stoplight_rows):


# TEMPORARY (TODO(#363)) self-contained HTML report template. The two table
# blocks are duplicated from the bactopia report template
# (assets/report/bactopia-single-sample-analysis/template.html.j2, commit
# 3251ccd); the surrounding <html>/<head>/<style> make it standalone. This
# lives here only for the demo - see render_report_html.
# blocks and the <style> block are duplicated from the bactopia report template
# (assets/report/bactopia-single-sample-analysis/template.html.j2) so the two
# reports render with matching styling; the surrounding <html>/<head> make it
# standalone. This lives here only for the demo - see render_report_html.
REPORT_TEMPLATE = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>RABiTS Sample Results - {{ sample_id }}</title>
<title>RABiTS Sample Results</title>
<style>
body {
margin: 0;
padding: 24px;
max-width: 980px;
margin-inline: auto;
font: 14px/1.6 system-ui, sans-serif;
color: #0f172a;
background: #fff;
}
h1 { font-size: 1.75rem; }
h2 {
font-size: 1.25rem;
margin-top: 2rem;
padding-bottom: .35rem;
border-bottom: 1px solid #e2e8f0;
}
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin-top: 12px;
font-size: .95rem;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 12px;
overflow: hidden;
}
thead th {
text-align: left;
font-weight: 600;
background: #f1f5f9;
border-bottom: 1px solid #e2e8f0;
padding: .6rem .75rem;
}
tbody td {
padding: .55rem .75rem;
border-bottom: 1px solid #f1f5f9;
vertical-align: top;
word-break: break-word;
}
tbody tr:last-child td { border-bottom: 0; }
tbody tr:nth-child(odd) td { background: #f8fafc; }
/* Page & typography */
html,
body {
height: auto;
}

body {
margin: 0;
padding: 24px;
max-width: 980px;
margin-inline: auto;
font: 14px/1.6 system-ui, sans-serif;
color: #0f172a;
/* slate-900-ish */
background: #fff;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

h1,
h2,
h3 {
line-height: 1.25;
margin: 1.25rem 0 .5rem;
}

h1 {
font-size: 1.75rem;
color: oklch(0.3967 0.14 18.05);
}

h2 {
font-size: 1.25rem;
margin-top: 2rem;
padding-bottom: .35rem;
border-bottom: 1px solid #e2e8f0;
color: oklch(0.3967 0.14 18.05);
}

h3 {
font-size: 1.05rem;
margin-top: 1.25rem;
color: #334155;
}

/* slate-700 */
p {
margin: .25rem 0;
}

body>div {
display: grid;
grid-template-columns: 220px 1fr;
gap: 8px 16px;
padding: 12px 16px;
margin-top: 12px;
background: oklch(99.087% 0.00442 33.09);
/* slate-50 */
border: 1px solid #e2e8f0;
/* slate-200 */
border-radius: 12px;
}

@media (max-width: 640px) {
body>div {
grid-template-columns: 1fr;
}
}

/* Label cells are the odd children; values are the even ones */
body>div>div:nth-child(odd) p {
font-weight: 600;
color: #334155;
/* slate-700 */
margin-right: 8px;
}

body>div>div:nth-child(odd) {
text-align: right;
}

@media (max-width: 640px) {
body>div>div:nth-child(odd) {
text-align: left;
}
}

/* Tables */
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin-top: 12px;
font-size: .95rem;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 12px;
overflow: hidden;
}

thead th {
text-align: left;
font-weight: 600;
background: oklch(95.979% 0.00643 16.336);
border-bottom: 1px solid #e2e8f0;
padding: .6rem .75rem;
}

tbody td {
padding: .55rem .75rem;
border-bottom: 1px solid oklch(97.478% 0.0064 16.315);
vertical-align: top;
word-break: break-word;
}

tbody tr:last-child td {
border-bottom: 0;
}

/* Zebra rows for readability */
tbody tr:nth-child(odd) td {
background: oklch(99.087% 0.00442 33.09);
}

/* Make the last 1–2 columns right-aligned if they look numeric */
tbody td:last-child,
tbody td:nth-last-child(2) {
text-align: right;
}

/* Headings near tables */
h2+table,
h3+table {
margin-top: 8px;
}

/* Links (if any) */
a {
color: #2563eb;
text-decoration: underline;
text-underline-offset: 2px;
}

a:hover {
opacity: .9;
}

/* Print friendliness */
@media print {
body {
background: #fff;
color: #000;
font-size: 0.8em;
}

table {
font-size: 0.8em;
}

th, td {
padding: 0px;
}

h1 {
font-size: 1.5em;
}

h2 {
font-size: 1.2em;
}

h3 {
font-size: 1.1em;
}

body>div {
break-inside: avoid;
}
}
</style>
</head>
<body>
Expand Down
Loading