Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,11 @@ export default function CreateAnalysisForm({
rules={{ required: true }}
/>

<InputError className="-mt-6 mb-1">
<InputError className="mb-0">
{errors.indexId && "A reference must be selected"}
</InputError>

<DialogFooter className="items-center justify-between mt-2.5 [&_button]:ml-auto">
<DialogFooter className="items-center justify-between">
<CreateAnalysisSummary
sampleCount={sampleCount}
indexCount={watch("indexId") ? 1 : 0}
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/analyses/components/Create/IndexSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export default function IndexSelector({
));

return (
<div className="mb-8">
<div>
<CreateAnalysisFieldTitle>Reference</CreateAnalysisFieldTitle>
{indexes.length ? (
<Select value={selected} onValueChange={onChange}>
Expand All @@ -90,7 +90,7 @@ export default function IndexSelector({
<SelectContent>{indexItems}</SelectContent>
</Select>
) : (
<Box>
<Box className="mb-0">
<Empty className="py-12">
<EmptyMedia className="text-gray-400">
<Library size={40} strokeWidth={1.5} />
Expand Down
20 changes: 7 additions & 13 deletions apps/web/src/analyses/components/Create/QuickAnalyze.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,34 @@ import { Dialog, DialogTitle } from "@base/Dialog";
import QueryError from "@base/QueryError";
import { useListHmms } from "@hmm/queries";
import type { SampleMinimal } from "@samples/types";
import { useEffect } from "react";
import HMMAlert from "../HMMAlert";
import CreateAnalysisDialogContent from "./CreateAnalysisDialogContent";
import CreateAnalysisForm from "./CreateAnalysisForm";
import { SelectedSamples } from "./SelectedSamples";
import { getCompatibleWorkflows } from "./workflows";

type QuickAnalyzeProps = {
/** Whether the samples came from the list selection rather than a single sample */
fromSelection: boolean;

open: boolean;
/** A callback function to clear selected samples */
onClear: () => void;
setOpen: (open: boolean) => void;

/** The selected samples */
/** The samples to analyze */
samples: SampleMinimal[];
};

/**
* A form for triggering quick analyses on selected samples
* A form for triggering quick analyses on the passed samples
*/
export default function QuickAnalyze({
fromSelection,
open,
samples,
setOpen,
}: QuickAnalyzeProps) {
const { data: hmms, isPending, isError } = useListHmms(1, 1, "");

// The dialog should close when all selected samples have been analyzed and deselected.
useEffect(() => {
if (open && samples.length === 0) {
setOpen(false);
}
}, [open, samples, setOpen]);

if (isError && !hmms) {
return (
<Dialog open={open} onOpenChange={setOpen}>
Expand All @@ -61,7 +55,7 @@ export default function QuickAnalyze({
<DialogTitle>Quick Analyze</DialogTitle>
<HMMAlert installed={Boolean(hmms.status.task?.complete)} />

<SelectedSamples samples={samples} />
<SelectedSamples fromSelection={fromSelection} samples={samples} />

<CreateAnalysisForm
compatibleWorkflows={compatibleWorkflows}
Expand Down
35 changes: 23 additions & 12 deletions apps/web/src/analyses/components/Create/SelectedSamples.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,45 @@ import CreateAnalysisFieldTitle from "./CreateAnalysisFieldTitle";
type SelectedSamplesProps = {
/** The samples selected for the open quick analysis dialog. */
samples: SampleMinimal[];

/**
* Whether the samples came from the list selection rather than a single
* sample. A selection is titled in the plural, counted even when only one
* sample is in it, and scrolls once it outgrows the dialog.
*/
fromSelection: boolean;
};

/**
* Displays the sample selected for the analyses that will be started by the open
* quick analysis dialog.
*/
export function SelectedSamples({ samples }: SelectedSamplesProps) {
export function SelectedSamples({
fromSelection,
samples,
}: SelectedSamplesProps) {
return (
<>
<div className="mb-6">
<CreateAnalysisFieldTitle>
Selected Samples <Badge>{samples.length}</Badge>
{fromSelection ? (
<>
Selected Samples <Badge>{samples.length}</Badge>
</>
) : (
"Selected Sample"
)}
</CreateAnalysisFieldTitle>
<div
className={cn(
"border",
"border-gray-300",
"mb-2",
"max-h-32",
"overflow-y-scroll",
"rounded-sm",
)}
className={cn("border", "border-gray-300", "rounded-sm", {
"max-h-32 overflow-y-scroll": fromSelection,
})}
>
{samples.map(({ id, name }) => (
<BoxGroupSection key={id} disabled>
{name}
</BoxGroupSection>
))}
</div>
</>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default function SubtractionSelector({
}

return (
<div className="mb-8">
<div className="mb-6">
<MultiSelectComboBox<SubtractionOption>
label="Subtractions"
items={results}
Expand Down
44 changes: 14 additions & 30 deletions apps/web/src/analyses/components/Create/WorkflowSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import BoxGroup from "@base/BoxGroup";
import BoxGroupSection from "@base/BoxGroupSection";
import { RadioGroup, RadioGroupItem } from "@base/RadioGroup";
import CreateAnalysisFieldTitle from "./CreateAnalysisFieldTitle";
import { SelectBox, SelectBoxItem } from "@base/SelectBox";
import type { workflow } from "./workflows";

type WorkflowSelectorProps = {
Expand All @@ -16,41 +13,28 @@ type WorkflowSelectorProps = {
};

/**
* A radio group for choosing which analysis workflow to run.
* A boxed picker for choosing which analysis workflow to run.
*/
export default function WorkflowSelector({
workflows,
selected,
onChange,
}: WorkflowSelectorProps) {
return (
<div>
<CreateAnalysisFieldTitle>Workflow</CreateAnalysisFieldTitle>
<RadioGroup
aria-label="Workflow"
value={selected}
<div className="mb-6">
<SelectBox
className="grid-cols-2"
label="Workflow"
onValueChange={onChange}
value={selected}
>
<BoxGroup>
{workflows.map((workflow) => (
<BoxGroupSection
key={workflow.id}
className="flex items-center gap-3"
>
<RadioGroupItem
id={`workflow-${workflow.id}`}
value={workflow.id}
/>
<label
htmlFor={`workflow-${workflow.id}`}
className="grow cursor-pointer"
>
{workflow.name}
</label>
</BoxGroupSection>
))}
</BoxGroup>
</RadioGroup>
{workflows.map(({ description, id, name }) => (
<SelectBoxItem key={id} value={id}>
<div>{name}</div>
<span>{description}</span>
</SelectBoxItem>
))}
</SelectBox>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { describe, expect, it } from "vitest";
import WorkflowSelector from "../WorkflowSelector";
import { nuvsWorkflow, pathoscopeWorkflow } from "../workflows";

const pathoscopeName = `${pathoscopeWorkflow.name} ${pathoscopeWorkflow.description}`;
const nuvsName = `${nuvsWorkflow.name} ${nuvsWorkflow.description}`;

function Harness() {
const [selected, setSelected] = useState(pathoscopeWorkflow.id);

Expand All @@ -23,24 +26,33 @@ describe("<WorkflowSelector>", () => {
renderWithProviders(<Harness />);

expect(screen.getByRole("radiogroup", { name: "Workflow" })).toBeVisible();
expect(screen.getByRole("radio", { name: "Pathoscope" })).toBeChecked();
expect(screen.getByRole("radio", { name: "NuVs" })).not.toBeChecked();
expect(screen.getByRole("radio", { name: pathoscopeName })).toBeChecked();
expect(screen.getByRole("radio", { name: nuvsName })).not.toBeChecked();
});

it("describes what each workflow finds", () => {
renderWithProviders(<Harness />);

expect(screen.getByText("Find known viruses.")).toBeVisible();
expect(screen.getByText("Find novel viruses.")).toBeVisible();
});

it("selects a workflow when its option is chosen", async () => {
renderWithProviders(<Harness />);

await userEvent.click(screen.getByRole("radio", { name: "NuVs" }));
await userEvent.click(screen.getByRole("radio", { name: nuvsName }));

expect(screen.getByRole("radio", { name: "NuVs" })).toBeChecked();
expect(screen.getByRole("radio", { name: "Pathoscope" })).not.toBeChecked();
expect(screen.getByRole("radio", { name: nuvsName })).toBeChecked();
expect(
screen.getByRole("radio", { name: pathoscopeName }),
).not.toBeChecked();
});

it("selects a workflow when its label is clicked", async () => {
it("selects a workflow when its name is clicked", async () => {
renderWithProviders(<Harness />);

await userEvent.click(screen.getByText("NuVs"));
await userEvent.click(screen.getByText(nuvsWorkflow.name));

expect(screen.getByRole("radio", { name: "NuVs" })).toBeChecked();
expect(screen.getByRole("radio", { name: nuvsName })).toBeChecked();
});
});
3 changes: 3 additions & 0 deletions apps/web/src/analyses/components/Create/workflows.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
export type workflow = {
description: string;
id: string;
name: string;
};

export const pathoscopeWorkflow = {
description: "Find known viruses.",
id: "pathoscope",
name: "Pathoscope",
};

export const nuvsWorkflow = {
description: "Find novel viruses.",
id: "nuvs",
name: "NuVs",
};
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/base/IconButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import Tooltip from "./Tooltip";
import type { IconColor } from "./types";

export type IconButtonProps = {
/** Accessible name for the button. Defaults to ``tip``. */
ariaLabel?: string;
className?: string;
color?: IconColor;
IconComponent: LucideIcon;
Expand All @@ -17,6 +19,7 @@ export type IconButtonProps = {
* A styled clickable icon with tooltip describing its action
*/
export default function IconButton({
ariaLabel,
className,
color = "black",
IconComponent,
Expand Down Expand Up @@ -55,7 +58,7 @@ export default function IconButton({
},
className,
)}
aria-label={tip}
aria-label={ariaLabel ?? tip}
type="button"
onClick={onClick}
>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/base/SelectBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function SelectBox({
const labelId = useId();

return (
<div className="mb-4">
<div>
<InputLabel id={labelId}>{label}</InputLabel>
<ToggleGroup.Root
aria-labelledby={labelId}
Expand Down
38 changes: 20 additions & 18 deletions apps/web/src/references/components/CreateReference.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,26 @@ export function CreateReference({ open, onOpenChange }: CreateReferenceProps) {
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent size="lg">
<DialogTitle>Create Reference</DialogTitle>
<SelectBox
className="grid-cols-2"
label="Method"
onValueChange={(value) => setMode(value as "empty" | "import")}
value={mode}
>
<SelectBoxItem value="empty">
<div>Empty</div>
<span>Start from a blank reference.</span>
</SelectBoxItem>
<SelectBoxItem value="import">
<div>Import</div>
<span>
Create a reference from a file previously exported from another
Virtool reference.
</span>
</SelectBoxItem>
</SelectBox>
<div className="mb-4">
<SelectBox
className="grid-cols-2"
label="Method"
onValueChange={(value) => setMode(value as "empty" | "import")}
value={mode}
>
<SelectBoxItem value="empty">
<div>Empty</div>
<span>Start from a blank reference.</span>
</SelectBoxItem>
<SelectBoxItem value="import">
<div>Import</div>
<span>
Create a reference from a file previously exported from another
Virtool reference.
</span>
</SelectBoxItem>
</SelectBox>
</div>
<CreateReferenceForm mode={mode} onSuccess={handleSuccess} />
</DialogContent>
</Dialog>
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/samples/components/Item/EndIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { ChartArea } from "lucide-react";
import { cn } from "@/app/utils";

type SampleItemEndIconProps = {
/** Accessible name for the quick analyze button */
ariaLabel: string;

/** Progress of the job responsible for creating the sample */
progress: number;

Expand All @@ -25,6 +28,7 @@ type SampleItemEndIconProps = {
* Icon indicating the status of sample
*/
export default function SampleItemEndIcon({
ariaLabel,
onClick,
ready,
progress,
Expand All @@ -40,6 +44,7 @@ export default function SampleItemEndIcon({
return (
<div className={containerClasses}>
<IconButton
ariaLabel={ariaLabel}
className="text-lg"
color="green"
IconComponent={ChartArea}
Expand Down
Loading