Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

List report.html files on job result page #33

Merged
merged 14 commits into from
May 31, 2023
Merged
Show file tree
Hide file tree
Changes from 12 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 app/components/Haddock3/Form.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { prepareCatalog } from "@i-vresse/wb-core/dist/catalog";
import { useEffect } from "react";
import { WorkflowSubmitButton } from "./SubmitButton";
import { useLoaderData } from "@remix-run/react";
import type { loader } from "~/routes/applications/builder";
import type { loader } from "~/routes/builder";
import { WorkflowDownloadButton } from "./DownloadButton";
import { FormActions } from "./FormActions";

Expand Down
4 changes: 4 additions & 0 deletions app/components/Haddock3/Form.css
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,7 @@ div.checkbox {
padding: 0.25rem;
overflow: unset;
}

code .table{
@apply contents
}
29 changes: 29 additions & 0 deletions app/components/ListReportFiles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useMemo } from "react";
import type { DirectoryItem } from "~/bartender-client";

export function ListReportFiles({ files, prefix }: { files: DirectoryItem, prefix: string }) {
const htmlFiles: [string, DirectoryItem[]][] = useMemo(() => {
if (!files.children) {
return [];
}
const analyisRoot = files.children.find((i) => i.name === "analysis");
if (!analyisRoot|| !analyisRoot.children) {
return [];
}
return analyisRoot.children.map((module) => {
if (!module.children) {
return [module.name, []]
}
const htmls = module.children.filter((file) => file.name.endsWith("report.html"))
.map((file) => file);
return [module.name, htmls];
});
}, [files]);
return (
<ul className="list-disc list-inside">
{htmlFiles.map(([module, htmls]) => {
return <li key={module}><a target="_blank" rel="noreferrer" href={`${prefix}${htmls[0].path}`}>{module}</a></li>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does htmls only ever contain 1 item?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right.
Each caprieval analysis module has multiple html files, but only one report.html.
We only want to show report.html for now, later each plot in report.html could link to the single plot html file.

Changed code to use map<module,report path>

})}
</ul>
);
}
4 changes: 2 additions & 2 deletions app/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ export const Navbar = () => {
<div className="navbar-start flex">
<ul className="menu menu-horizontal px-1">
<li>
<NavLink to="/applications/builder">Build</NavLink>
<NavLink to="/builder">Build</NavLink>
</li>
<li>
<NavLink to="/applications/upload">Upload</NavLink>
<NavLink to="/upload">Upload</NavLink>
</li>
<li>
<NavLink to="/jobs">Manage</NavLink>
Expand Down
2 changes: 1 addition & 1 deletion app/models/application.server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { assert, describe, test } from "vitest";

import {
rewriteConfigInArchive,
WORKFLOW_CONFIG_FILENAME,
} from "./applicaton.server";
import { WORKFLOW_CONFIG_FILENAME } from "./constants";

const HY3_PDB = `\
ATOM 1 SHA SHA S 1 30.913 40.332 2.133 1.00 36.12 S
Expand Down
31 changes: 10 additions & 21 deletions app/models/applicaton.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,23 @@ import { stringify, parse } from "@ltd/j-toml";
import { ApplicationApi } from "~/bartender-client/apis/ApplicationApi";
import type { JobModelDTO } from "~/bartender-client/models/JobModelDTO";
import { buildConfig } from "./config.server";

export const WORKFLOW_CONFIG_FILENAME = "workflow.cfg";
const JOB_OUTPUT_DIR = "output";
import { BARTENDER_APPLICATION_NAME, JOB_OUTPUT_DIR, WORKFLOW_CONFIG_FILENAME } from './constants';

function buildApplicationApi(accessToken: string = "") {
return new ApplicationApi(buildConfig(accessToken));
}

export async function applicationNames() {
const api = buildApplicationApi();
return await api.listApplications();
}

export async function applicationByName(name: string) {
const api = buildApplicationApi();
return await api.getApplication({
application: name,
});
}

export async function submitJob(
application: string,
upload: File,
accessToken: string
) {
const api = buildApplicationApi(accessToken);
const rewritten_upload = await rewriteConfigInArchive(upload);
const rewritten_upload = new File([await rewriteConfigInArchive(upload)], upload.name, {
type: upload.type,
lastModified: upload.lastModified,
});
const response = await api.uploadJobRaw({
application,
application: BARTENDER_APPLICATION_NAME,
upload: rewritten_upload,
});
const job: JobModelDTO = await response.raw.json();
Expand All @@ -52,8 +40,9 @@ export async function submitJob(
* @param config_body Body of workflow config file to rewrite
* @returns The rewritten config file
*/
function rewriteConfig(config_body: string) {
const table = parse(config_body, { bigint: false });
async function rewriteConfig(config_body: string) {
const { dedupWorkflow } = await import('@i-vresse/wb-core/dist/toml.js');
const table = parse(dedupWorkflow(config_body), { bigint: false });
table.run_dir = JOB_OUTPUT_DIR;
table.mode = "local";
table.postprocess = true;
Expand Down Expand Up @@ -87,7 +76,7 @@ export async function rewriteConfigInArchive(upload: Blob) {

// TODO validate config using catalog and ajv

const new_config = rewriteConfig(config_body);
const new_config = await rewriteConfig(config_body);

zip.file(WORKFLOW_CONFIG_FILENAME, new_config);
return await zip.generateAsync({type: "blob"});
Expand Down
4 changes: 4 additions & 0 deletions app/models/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

export const BARTENDER_APPLICATION_NAME = "haddock3";
export const WORKFLOW_CONFIG_FILENAME = "workflow.cfg";
export const JOB_OUTPUT_DIR = "output";
11 changes: 11 additions & 0 deletions app/models/job.server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { JobApi } from "~/bartender-client/apis/JobApi";
import { buildConfig } from "./config.server";
import { JOB_OUTPUT_DIR } from "./constants";

function buildJobApi(accessToken: string = '') {
return new JobApi(buildConfig(accessToken));
Expand Down Expand Up @@ -44,3 +45,13 @@ export async function getJobfile(jobid: number, path: string, accessToken: strin
return response.raw;
}

export async function listOutputFiles(jobid: number, accessToken: string) {
const api = buildJobApi(accessToken)
const items = await api.retrieveJobDirectoriesFromPath({
jobid,
path: JOB_OUTPUT_DIR,
maxDepth: 3
})
// Filter on html files
return items
}
16 changes: 8 additions & 8 deletions app/routes/applications/builder.tsx → app/routes/builder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import { type ActionArgs, type LoaderArgs, redirect } from "@remix-run/node";
import { getCatalog } from "~/catalogs/index.server";
import { Haddock3WorkflowBuilder } from "~/components/Haddock3/Form.client";
import { haddock3Styles } from "~/components/Haddock3/styles";
import { getAccessToken } from "~/token.server";
import { submitJob } from "~/models/applicaton.server";
import { getLevel, isSubmitAllowed } from "~/models/user.server";
import { checkAuthenticated, getLevel, isSubmitAllowed } from "~/models/user.server";
import { getSession } from "~/session.server";

export const loader = async ({ request }: LoaderArgs) => {
Expand All @@ -22,19 +21,20 @@ export const loader = async ({ request }: LoaderArgs) => {
};

export const action = async ({ request }: ActionArgs) => {
const name = "haddock3";
const formData = await request.formData();
const upload = formData.get("upload");

if (typeof upload === "string" || upload === null) {
throw new Error("Bad upload");
}
const access_token = await getAccessToken(request);
if (access_token === undefined) {
throw new Error("Unauthenticated");
const session = await getSession(request)
const accessToken = session.data.bartenderToken
checkAuthenticated(accessToken);
const level = await getLevel(session.data.roles)
if (!isSubmitAllowed(level)) {
throw new Error("Forbidden");
}

const job = await submitJob(name, upload, access_token);
const job = await submitJob(upload, accessToken!);
const job_url = `/jobs/${job.id}`;
return redirect(job_url);
};
Expand Down
9 changes: 4 additions & 5 deletions app/routes/help.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
export default function About() {
export default function Help() {
return (
<p className="prose">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Iusto amet iure temporibus vero dignissimos, dolorem adipisci ipsam suscipit nemo enim a magni ad ipsum. Sint iure itaque assumenda expedita debitis.
</p>

<main>
<a href="https://www.bonvinlab.org/haddock3/">Haddock3 command line documentation</a>
</main>
)
}
4 changes: 2 additions & 2 deletions app/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import Card from "~/components/Card";

const cards = [
{
"target": "/applications/builder",
"target": "/builder",
"image": "https://static.thenounproject.com/png/1781890-200.png",
"title": "Build",
"description": "Use the workflow builder to create and submit a job.",
},
{
"target": "/applications/upload",
"target": "/upload",
"image": "https://www.filemail.com/images/marketing/anonymously-upload-files.svg",
"title": "Upload",
"description": "Upload a workflow and submit as job.",
Expand Down
117 changes: 78 additions & 39 deletions app/routes/jobs/$id.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,93 @@
import { json, type LoaderArgs } from "@remix-run/node";
import { Link, useLoaderData } from "@remix-run/react";
import { useLoaderData } from "@remix-run/react";
import { getAccessToken } from "~/token.server";
import { applicationByName } from "~/models/applicaton.server";
import { getJobById } from "~/models/job.server";
import { WORKFLOW_CONFIG_FILENAME } from "~/models/constants";
import { listOutputFiles, getJobById } from "~/models/job.server";
import { CompletedJobs } from "~/utils";
import { checkAuthenticated } from "~/models/user.server";
import type { DirectoryItem } from "~/bartender-client";
import { ListReportFiles } from "~/components/ListReportFiles";

export const loader = async ({ params, request }: LoaderArgs) => {
const job_id = params.id || "";
const access_token = await getAccessToken(request);
if (access_token === undefined) {
throw new Error("Unauthenticated");
const jobId = parseInt(params.id || "");
const accessToken = await getAccessToken(request);
checkAuthenticated(accessToken);
const job = await getJobById(jobId, accessToken!);
// TODO check if job belongs to user
let outputFiles: DirectoryItem | undefined = undefined;
if (CompletedJobs.has(job.state)) {
outputFiles = await listOutputFiles(jobId, accessToken!);
}
const job = await getJobById(parseInt(job_id), access_token);
const app = await applicationByName(job.application);
return json({ job, app });
return json({ job, outputFiles });
};

export default function JobPage() {
const { job, app } = useLoaderData<typeof loader>();
const { job, outputFiles } = useLoaderData<typeof loader>();
return (
<main>
<p>
Application:
<Link to={`/applications/${job.application}`}>{job.application}</Link>
</p>
<p>State: {job.state}</p>
<p>createdOn: {job.createdOn}</p>
<p>updatedOn: {job.updatedOn}</p>
<p>Name: {job.name}</p>
<main className="flex gap-16">
<div>
<p>ID: {job.id}</p>
<p>Name: {job.name}</p>
<p>
State: <b>{job.state}</b>
</p>
<p>Created on: {job.createdOn}</p>
<p>Updated on: {job.updatedOn}</p>
</div>
{CompletedJobs.has(job.state) && (
<>
<p>
<a target="_blank" rel="noreferrer" href={`/jobs/${job.id}/stdout`}>
Stdout
</a>
</p>
<p>
<a target="_blank" rel="noreferrer" href={`/jobs/${job.id}/stderr`}>
Stderr
</a>
</p>
<p>
<a
target="_blank"
rel="noreferrer"
href={`/jobs/${job.id}/files/${app.config}`}
>
{app.config}
</a>
</p>
{/* TODO allow to read input files when job is not completed */}
<div>
<h2 className="text-xl">Input</h2>
<ul className="list-disc list-inside">
<li>
<a
target="_blank"
rel="noreferrer"
href={`/jobs/${job.id}/files/${WORKFLOW_CONFIG_FILENAME}`}
>
{WORKFLOW_CONFIG_FILENAME}
</a>
</li>
{/* TODO list files mentioned in workflow config */}
</ul>
</div>
<div>
<h2 className="text-xl">Output</h2>
<ListReportFiles
files={outputFiles!}
prefix={`/jobs/${job.id}/files/`}
/>
<ul className="list-disc list-inside">
<li>
<a
target="_blank"
rel="noreferrer"
href={`/jobs/${job.id}/stdout`}
>
Stdout
</a>
</li>
<li>
<a
target="_blank"
rel="noreferrer"
href={`/jobs/${job.id}/stderr`}
>
Stderr
</a>
</li>
<li>
<a
target="_blank"
rel="noreferrer"
href={`/jobs/${job.id}/files/output/log`}
>
Haddock3 log
</a>
</li>
</ul>
</div>
</>
)}
</main>
Expand Down
18 changes: 15 additions & 3 deletions app/routes/jobs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ export default function JobPage() {
const { jobs } = useLoaderData<typeof loader>();
return (
<main>
<table>
<table className="table w-full">
<thead>
<tr>
<th>ID</th>
<th>Status</th>
<th>Name</th>
<th>Created on</th>
<th>Updated on</th>
</tr>
</thead>
<tbody>
Expand All @@ -30,8 +32,18 @@ export default function JobPage() {
<td>
<Link to={`/jobs/${job.id}`}>{job.id}</Link>
</td>
<td>{job.state}</td>
<td>{job.name}</td>
<td>
<Link to={`/jobs/${job.id}`}>{job.state}</Link>
</td>
<td>
<Link to={`/jobs/${job.id}`}>{job.name}</Link>
</td>
<td>
<Link to={`/jobs/${job.id}`}>{job.createdOn}</Link>
</td>
<td>
<Link to={`/jobs/${job.id}`}>{job.updatedOn}</Link>
</td>
</tr>
))}
</tbody>
Expand Down
Loading