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 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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ jobs:
cache: 'npm'
- run: npm ci
- run: npm run build --if-present
- run: npm test
- run: npm test -- --coverage
- run: npm run typecheck
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ node_modules
/openapitools.json

/sessions
/coverage
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
}
141 changes: 141 additions & 0 deletions app/components/ListReportFiles.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { describe, it, expect, afterEach } from "vitest";
import { render } from "@testing-library/react";
import { cleanup } from "@testing-library/react";
import matchers from "@testing-library/jest-dom/matchers";

import { ListReportFiles } from "./ListReportFiles";
import type { DirectoryItem } from "~/bartender-client";

expect.extend(matchers);

afterEach(() => {
cleanup();
});

describe("ListReportFiles", () => {
it("renders a list of report files", () => {
const files: DirectoryItem = {
name: "root",
path: "",
isDir: true,
isFile: false,
children: [
{
name: "analysis",
path: "analysis",
isDir: true,
isFile: false,
children: [
{
name: "module1",
path: "analysis/module1",
isDir: true,
isFile: false,
children: [
{
name: "report.html",
path: "analysis/module1/report.html",
isDir: false,
isFile: true,
},
],
},
{
name: "module2",
path: "analysis/module2",
isDir: true,
isFile: false,
children: [
{
name: "report.html",
path: "analysis/module2/report.html",
isDir: false,
isFile: true,
},
],
},
],
},
],
};
const prefix = "https://example.com/";
const { getByText } = render(
<ListReportFiles files={files} prefix={prefix} />
);
expect(getByText("module1")).toHaveAttribute(
"href",
"https://example.com/analysis/module1/report.html"
);
expect(getByText("module2")).toHaveAttribute(
"href",
"https://example.com/analysis/module2/report.html"
);
});

it("renders a module with no report.html file", () => {
const files: DirectoryItem = {
name: "root",
path: "",
isDir: true,
isFile: false,
children: [
{
name: "analysis",
path: "analysis",
isDir: true,
isFile: false,
children: [
{
name: "module1",
path: "analysis/module1",
isDir: true,
isFile: false,
children: [],
},
],
},
],
};
const prefix = "https://example.com/";
const { queryByText } = render(
<ListReportFiles files={files} prefix={prefix} />
);
expect(queryByText("module1")).toBeFalsy();
});

it("renders correctly when analysis has no children", () => {
const files: DirectoryItem = {
name: "root",
path: "",
isDir: true,
isFile: false,
children: [
{
name: "analysis",
path: "analysis",
isDir: true,
isFile: false,
},
],
};
const prefix = "https://example.com/";
const { queryByText } = render(
<ListReportFiles files={files} prefix={prefix} />
);
expect(queryByText("analysis")).toBeFalsy();
});

it("renders correctly when root has no children", () => {
const files: DirectoryItem = {
name: "root",
path: "",
isDir: true,
isFile: false,
};
const prefix = "https://example.com/";
const { queryByText } = render(
<ListReportFiles files={files} prefix={prefix} />
);
expect(queryByText("analysis")).toBeFalsy();
});
});
44 changes: 44 additions & 0 deletions app/components/ListReportFiles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useMemo } from "react";
import type { DirectoryItem } from "~/bartender-client";

export function ListReportFiles({
files,
prefix,
}: {
files: DirectoryItem;
prefix: string;
}) {
const reportFiles = useMemo(() => {
const result = new Map<string, string>();
if (!files.children) {
return result;
}
const analyisRoot = files.children.find((i) => i.name === "analysis");
if (!analyisRoot || !analyisRoot.children) {
return result;
}
analyisRoot.children
.filter((module) => module.children !== undefined)
.forEach((module) => {
const html = module.children?.find(
(file) => file.name === "report.html"
);
if (html !== undefined) {
result.set(module.name, html.path);
}
});
return result;
}, [files]);

return (
<ul className="list-disc list-inside">
{Array.from(reportFiles).map(([module, report]) => (
<li key={module}>
<a target="_blank" rel="noreferrer" href={`${prefix}${report}`}>
{module}
</a>
</li>
))}
</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
Loading