feat(binary): add engine selection for binary analysis

This commit is contained in:
Rodrigo Verdiani 2026-06-09 20:25:14 -03:00
parent decb831ba1
commit cd208b51fc
11 changed files with 400 additions and 38 deletions

View File

@ -4,6 +4,12 @@
* OpenAPI definition * OpenAPI definition
* OpenAPI spec version: v0 * OpenAPI spec version: v0
*/ */
export interface EngineResponse {
name: string;
label: string;
available: boolean;
}
export interface WorkspaceRequest { export interface WorkspaceRequest {
/** @minLength 1 */ /** @minLength 1 */
name: string; name: string;

View File

@ -0,0 +1,120 @@
/**
* Generated by orval v8.15.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
import { useQuery } from "@tanstack/react-query";
import type {
DataTag,
DefinedInitialDataOptions,
DefinedUseQueryResult,
QueryClient,
QueryFunction,
QueryKey,
UndefinedInitialDataOptions,
UseQueryOptions,
UseQueryResult,
} from "@tanstack/react-query";
import type { EngineResponse } from "../api.schemas";
import { customInstance } from "../../api";
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/**
* Retrieve all registered static analysis engines with their availability status.
* @summary List available analysis engines
*/
export const listEngines = (
options?: SecondParameter<typeof customInstance>,
signal?: AbortSignal
) => {
return customInstance<EngineResponse[]>({ url: `/engines`, method: "GET", signal }, options);
};
export const getListEnginesQueryKey = () => {
return [`/engines`] as const;
};
export const getListEnginesQueryOptions = <
TData = Awaited<ReturnType<typeof listEngines>>,
TError = unknown,
>(options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listEngines>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
}) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListEnginesQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof listEngines>>> = ({ signal }) =>
listEngines(requestOptions, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listEngines>>,
TError,
TData
> & { queryKey: DataTag<QueryKey, TData, TError> };
};
export type ListEnginesQueryResult = NonNullable<Awaited<ReturnType<typeof listEngines>>>;
export type ListEnginesQueryError = unknown;
export function useListEngines<TData = Awaited<ReturnType<typeof listEngines>>, TError = unknown>(
options: {
query: Partial<UseQueryOptions<Awaited<ReturnType<typeof listEngines>>, TError, TData>> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof listEngines>>,
TError,
Awaited<ReturnType<typeof listEngines>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useListEngines<TData = Awaited<ReturnType<typeof listEngines>>, TError = unknown>(
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listEngines>>, TError, TData>> &
Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof listEngines>>,
TError,
Awaited<ReturnType<typeof listEngines>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useListEngines<TData = Awaited<ReturnType<typeof listEngines>>, TError = unknown>(
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listEngines>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
/**
* @summary List available analysis engines
*/
export function useListEngines<TData = Awaited<ReturnType<typeof listEngines>>, TError = unknown>(
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listEngines>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getListEnginesQueryOptions(options);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
}

View File

@ -0,0 +1,57 @@
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Loader2 } from "lucide-react";
import type { EngineResponse } from "@/api/generated/api.schemas";
import { useMemo } from "react";
interface EngineSelectProps {
engines: EngineResponse[];
value?: string;
onChange: (engine: string) => void;
placeholder?: string;
isLoading?: boolean;
disabled?: boolean;
}
export const EngineSelect = ({
engines,
value,
onChange,
placeholder = "Select engine",
isLoading = false,
disabled = false,
}: EngineSelectProps) => {
const available = useMemo(() => engines.filter((e) => e.available), [engines]);
return (
<Select value={value} onValueChange={onChange} disabled={disabled || isLoading}>
<SelectTrigger className="w-full">
{isLoading ? (
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-muted-foreground">Loading engines...</span>
</div>
) : (
<SelectValue placeholder={placeholder} />
)}
</SelectTrigger>
<SelectContent>
{available.length === 0 && !isLoading && (
<div className="text-muted-foreground px-2 py-4 text-center text-sm">
No engines available
</div>
)}
{available.map((engine) => (
<SelectItem key={engine.name} value={engine.name}>
{engine.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
};

View File

@ -109,7 +109,7 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item <SelectPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground relative flex w-full cursor-pointer items-center rounded-sm py-1.5 pr-2 pl-3 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:cursor-default data-[disabled]:opacity-50 data-[state=checked]:pl-8",
className className
)} )}
{...props} {...props}

View File

@ -0,0 +1,96 @@
import { useState } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { EngineSelect } from "@/components/EngineSelect";
import { AlertTriangle } from "lucide-react";
import type { EngineResponse } from "@/api/generated/api.schemas";
interface AnalyzeDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
isReanalyze: boolean;
engines: EngineResponse[];
enginesLoading: boolean;
onConfirm: (engine: string) => void;
}
export const AnalyzeDialog = ({
open,
onOpenChange,
isReanalyze,
engines,
enginesLoading,
onConfirm,
}: AnalyzeDialogProps) => {
const [selectedEngine, setSelectedEngine] = useState<string>();
const handleConfirm = () => {
if (!selectedEngine) return;
onConfirm(selectedEngine);
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
{isReanalyze && (
<div className="flex items-center gap-3">
<div className="bg-destructive/10 flex h-10 w-10 items-center justify-center rounded-full">
<AlertTriangle className="text-destructive h-5 w-5" />
</div>
<div>
<DialogTitle>Re-analyze binary</DialogTitle>
<DialogDescription className="mt-1">
This will replace all current analysis data. Function names, comments, and
decompiled code will be lost.
</DialogDescription>
</div>
</div>
)}
{!isReanalyze && (
<div>
<DialogTitle>Analyze binary</DialogTitle>
<DialogDescription className="mt-1">
Select the analysis engine to use.
</DialogDescription>
</div>
)}
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">Analysis Engine</label>
<EngineSelect
engines={engines}
value={selectedEngine}
onChange={setSelectedEngine}
isLoading={enginesLoading}
placeholder="Select an engine"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
variant={isReanalyze ? "destructive" : "default"}
onClick={handleConfirm}
disabled={!selectedEngine}
>
{isReanalyze ? "Re-analyze" : "Analyze"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@ -6,6 +6,7 @@ import { useGetProject } from "@/api/generated/project/project.ts";
import { useGetWorkspace } from "@/api/generated/workspace/workspace.ts"; import { useGetWorkspace } from "@/api/generated/workspace/workspace.ts";
import { useListAnalyses, useListSegments } from "@/api/generated/analysis/analysis.ts"; import { useListAnalyses, useListSegments } from "@/api/generated/analysis/analysis.ts";
import { useGetFunction } from "@/api/generated/function/function.ts"; import { useGetFunction } from "@/api/generated/function/function.ts";
import { useListEngines } from "@/api/generated/engine/engine.ts";
import { customInstance } from "@/api/api.ts"; import { customInstance } from "@/api/api.ts";
import type { import type {
FunctionSummaryResponse, FunctionSummaryResponse,
@ -42,7 +43,7 @@ export function useBinaryPage() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [selectedFunctionId, setSelectedFunctionId] = useState<string>(); const [selectedFunctionId, setSelectedFunctionId] = useState<string>();
const [confirmReanalyzeOpen, setConfirmReanalyzeOpen] = useState(false); const [analyzeDialogOpen, setAnalyzeDialogOpen] = useState(false);
const [selectedSegmentId, setSelectedSegmentId] = useState<string>(); const [selectedSegmentId, setSelectedSegmentId] = useState<string>();
const [selectedFlags, setSelectedFlags] = useState<Set<string>>(new Set()); const [selectedFlags, setSelectedFlags] = useState<Set<string>>(new Set());
@ -96,6 +97,9 @@ export function useBinaryPage() {
{ query: { enabled: !!selectedFunctionId } } { query: { enabled: !!selectedFunctionId } }
); );
// Fetch available engines
const { data: engines, isLoading: enginesLoading } = useListEngines();
// Analyze / re-analyze mutation // Analyze / re-analyze mutation
const analyzeMutation = useAnalyzeBinary({ const analyzeMutation = useAnalyzeBinary({
mutation: { mutation: {
@ -107,17 +111,13 @@ export function useBinaryPage() {
}, },
}); });
const handleAnalyze = () => { const handleOpenAnalyzeDialog = () => {
analyzeMutation.mutate({ binaryId, params: { engine: "IDA5" } }); setAnalyzeDialogOpen(true);
}; };
const handleReanalyzeRequest = () => { const handleAnalyzeConfirm = (engine: string) => {
setConfirmReanalyzeOpen(true); setAnalyzeDialogOpen(false);
}; analyzeMutation.mutate({ binaryId, params: { engine } });
const handleReanalyzeConfirm = () => {
setConfirmReanalyzeOpen(false);
handleAnalyze();
}; };
// Flatten all pages into a single array // Flatten all pages into a single array
@ -217,10 +217,11 @@ export function useBinaryPage() {
selectedFunction, selectedFunction,
selectedFunctionLoading, selectedFunctionLoading,
analyzeMutation, analyzeMutation,
confirmReanalyzeOpen, analyzeDialogOpen,
setConfirmReanalyzeOpen, setAnalyzeDialogOpen,
handleAnalyze, handleOpenAnalyzeDialog,
handleReanalyzeRequest, handleAnalyzeConfirm,
handleReanalyzeConfirm, engines: engines ?? [],
enginesLoading,
}; };
} }

View File

@ -13,6 +13,8 @@ interface FileUploadProps {
accept?: string; accept?: string;
maxSize?: number; maxSize?: number;
className?: string; className?: string;
autoUpload?: boolean;
onFileSelect?: (file: File) => void;
} }
export const FileUpload = ({ export const FileUpload = ({
@ -23,6 +25,8 @@ export const FileUpload = ({
accept = ".exe,.com,.dll,.bin", accept = ".exe,.com,.dll,.bin",
maxSize = 50 * 1024 * 1024, maxSize = 50 * 1024 * 1024,
className, className,
autoUpload = true,
onFileSelect,
}: FileUploadProps) => { }: FileUploadProps) => {
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
@ -49,9 +53,13 @@ export const FileUpload = ({
setValidationError(null); setValidationError(null);
setFile(file); setFile(file);
onUpload(file); if (autoUpload) {
onUpload(file);
} else {
onFileSelect?.(file);
}
}, },
[onUpload, maxSize] [onUpload, maxSize, autoUpload, onFileSelect]
); );
const handleDrop = useCallback( const handleDrop = useCallback(

View File

@ -1,3 +1,4 @@
import { useState } from "react";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -8,14 +9,19 @@ import {
} from "@/components/ui/dialog.tsx"; } from "@/components/ui/dialog.tsx";
import { Button } from "@/components/ui/button.tsx"; import { Button } from "@/components/ui/button.tsx";
import { FileUpload } from "@/features/project/FileUpload.tsx"; import { FileUpload } from "@/features/project/FileUpload.tsx";
import { EngineSelect } from "@/components/EngineSelect";
import { Upload } from "lucide-react";
import type { EngineResponse } from "@/api/generated/api.schemas";
interface UploadBinaryDialogProps { interface UploadBinaryDialogProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
onUpload: (file: File) => void; onUpload: (file: File, engine?: string) => void;
isUploading: boolean; isUploading: boolean;
uploadError: string | null; uploadError: string | null;
onReset: () => void; onReset: () => void;
engines: EngineResponse[];
enginesLoading: boolean;
} }
export const UploadBinaryDialog = ({ export const UploadBinaryDialog = ({
@ -25,7 +31,31 @@ export const UploadBinaryDialog = ({
isUploading, isUploading,
uploadError, uploadError,
onReset, onReset,
engines,
enginesLoading,
}: UploadBinaryDialogProps) => { }: UploadBinaryDialogProps) => {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [selectedEngine, setSelectedEngine] = useState<string>();
const handleFileSelect = (file: File) => {
setSelectedFile(file);
};
const handleEngineChange = (engine: string) => {
setSelectedEngine(engine || undefined);
};
const handleUpload = () => {
if (!selectedFile) return;
onUpload(selectedFile, selectedEngine);
};
const handleReset = () => {
setSelectedFile(null);
setSelectedEngine(undefined);
onReset();
};
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg"> <DialogContent className="sm:max-w-lg">
@ -45,19 +75,52 @@ export const UploadBinaryDialog = ({
<span className="text-sm font-medium">Select Binary File</span> <span className="text-sm font-medium">Select Binary File</span>
</div> </div>
<FileUpload <FileUpload
onUpload={onUpload} onUpload={() => {}}
onFileSelect={handleFileSelect}
isUploading={isUploading} isUploading={isUploading}
error={uploadError} error={uploadError}
onReset={onReset} onReset={handleReset}
accept=".exe,.com,.dll,.bin,.elf" accept=".exe,.com,.dll,.bin,.elf"
maxSize={100 * 1024 * 1024} maxSize={100 * 1024 * 1024}
autoUpload={false}
/> />
</div> </div>
{selectedFile && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="bg-muted text-muted-foreground flex h-6 w-6 items-center justify-center rounded-full text-xs font-medium">
2
</div>
<span className="text-sm font-medium">Analysis Engine</span>
<span className="text-muted-foreground text-xs">(optional)</span>
</div>
<EngineSelect
engines={engines}
value={selectedEngine}
onChange={handleEngineChange}
isLoading={enginesLoading}
placeholder="Select engine (optional)"
/>
</div>
)}
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isUploading}> <Button variant="outline" onClick={() => onOpenChange(false)} disabled={isUploading}>
Cancel Cancel
</Button> </Button>
{selectedFile && (
<Button onClick={handleUpload} disabled={isUploading}>
{isUploading ? (
<>Uploading...</>
) : (
<>
<Upload className="mr-2 h-4 w-4" />
Upload
</>
)}
</Button>
)}
</DialogFooter> </DialogFooter>
</div> </div>
</DialogContent> </DialogContent>

View File

@ -4,6 +4,7 @@ import { useGetProject } from "@/api/generated/project/project.ts";
import { useGetWorkspace } from "@/api/generated/workspace/workspace.ts"; import { useGetWorkspace } from "@/api/generated/workspace/workspace.ts";
import { useDeleteBinary, useGetBinaries, useUploadBinary } from "@/api/generated/binary/binary.ts"; import { useDeleteBinary, useGetBinaries, useUploadBinary } from "@/api/generated/binary/binary.ts";
import { downloadBinary } from "@/api/generated/binary/binary.ts"; import { downloadBinary } from "@/api/generated/binary/binary.ts";
import { useListEngines } from "@/api/generated/engine/engine.ts";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import type { BinaryResponse } from "@/api/generated/api.schemas.ts"; import type { BinaryResponse } from "@/api/generated/api.schemas.ts";
@ -28,6 +29,8 @@ export function useProjectPage(projectId: string) {
{ query: { enabled: !!projectId } } { query: { enabled: !!projectId } }
); );
const { data: engines, isLoading: enginesLoading } = useListEngines();
const { mutate: mutateDeleteBinary } = useDeleteBinary({ const { mutate: mutateDeleteBinary } = useDeleteBinary({
mutation: { mutation: {
onSuccess: () => { onSuccess: () => {
@ -69,8 +72,8 @@ export function useProjectPage(projectId: string) {
setBinaryToDelete(null); setBinaryToDelete(null);
}; };
const handleUploadBinary = (file: File) => { const handleUploadBinary = (file: File, engine?: string) => {
uploadMutation.mutate({ projectId, data: { file } }); uploadMutation.mutate({ projectId, data: { file }, params: engine ? { engine } : undefined });
}; };
const handleDownloadBinary = async (binary: BinaryResponse) => { const handleDownloadBinary = async (binary: BinaryResponse) => {
@ -104,5 +107,7 @@ export function useProjectPage(projectId: string) {
uploadError, uploadError,
handleUploadBinary, handleUploadBinary,
handleDownloadBinary, handleDownloadBinary,
engines: engines ?? [],
enginesLoading,
}; };
} }

View File

@ -1,7 +1,6 @@
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/EmptyState"; import { EmptyState } from "@/components/EmptyState";
import { ConfirmDialog } from "@/components/ConfirmDialog";
import { Breadcrumbs } from "@/components/Breadcrumbs.tsx"; import { Breadcrumbs } from "@/components/Breadcrumbs.tsx";
import { BinaryHeader } from "@/features/binary/BinaryHeader.tsx"; import { BinaryHeader } from "@/features/binary/BinaryHeader.tsx";
import { FunctionTree } from "@/features/binary/FunctionTree"; import { FunctionTree } from "@/features/binary/FunctionTree";
@ -14,6 +13,7 @@ import {
import { useBinaryPage } from "@/features/binary/hooks/useBinaryPage"; import { useBinaryPage } from "@/features/binary/hooks/useBinaryPage";
import { CodeViewer } from "@/features/binary/CodeViewer"; import { CodeViewer } from "@/features/binary/CodeViewer";
import { AIChatPanel } from "@/features/binary/AIChatPanel"; import { AIChatPanel } from "@/features/binary/AIChatPanel";
import { AnalyzeDialog } from "@/features/binary/AnalyzeDialog";
const Binary = () => { const Binary = () => {
const { const {
@ -39,11 +39,12 @@ const Binary = () => {
selectedFunction, selectedFunction,
selectedFunctionLoading, selectedFunctionLoading,
analyzeMutation, analyzeMutation,
confirmReanalyzeOpen, analyzeDialogOpen,
setConfirmReanalyzeOpen, setAnalyzeDialogOpen,
handleAnalyze, handleOpenAnalyzeDialog,
handleReanalyzeRequest, handleAnalyzeConfirm,
handleReanalyzeConfirm, engines,
enginesLoading,
} = useBinaryPage(); } = useBinaryPage();
const navigateToEntryPoint = () => { const navigateToEntryPoint = () => {
@ -91,7 +92,7 @@ const Binary = () => {
binary={binary} binary={binary}
hasAnalysis={binary.staticAnalysisDone} hasAnalysis={binary.staticAnalysisDone}
isAnalyzing={analyzeMutation.isPending} isAnalyzing={analyzeMutation.isPending}
onAnalyze={binary.staticAnalysisDone ? handleReanalyzeRequest : handleAnalyze} onAnalyze={handleOpenAnalyzeDialog}
entryPoint={analysis?.entryPoint} entryPoint={analysis?.entryPoint}
onNavigateToEntryPoint={navigateToEntryPoint} onNavigateToEntryPoint={navigateToEntryPoint}
/> />
@ -165,14 +166,14 @@ const Binary = () => {
</ResizablePanelGroup> </ResizablePanelGroup>
</div> </div>
<ConfirmDialog <AnalyzeDialog
open={confirmReanalyzeOpen} key={analyzeDialogOpen ? "analyze-open" : "analyze-closed"}
onOpenChange={setConfirmReanalyzeOpen} open={analyzeDialogOpen}
title="Re-analyze binary" onOpenChange={setAnalyzeDialogOpen}
description="This will replace all current analysis data. Function names, comments, and decompiled code will be lost. Are you sure?" isReanalyze={!!binary.staticAnalysisDone}
confirmLabel="Re-analyze" engines={engines}
variant="destructive" enginesLoading={enginesLoading}
onConfirm={handleReanalyzeConfirm} onConfirm={handleAnalyzeConfirm}
/> />
</div> </div>
); );

View File

@ -32,6 +32,8 @@ const Project = () => {
uploadError, uploadError,
handleUploadBinary, handleUploadBinary,
handleDownloadBinary, handleDownloadBinary,
engines,
enginesLoading,
} = useProjectPage(projectId!); } = useProjectPage(projectId!);
if (!project || !workspace) { if (!project || !workspace) {
@ -111,12 +113,15 @@ const Project = () => {
)} )}
<UploadBinaryDialog <UploadBinaryDialog
key={uploadDialogOpen ? "upload-open" : "upload-closed"}
open={uploadDialogOpen} open={uploadDialogOpen}
onOpenChange={handleCloseUploadDialog} onOpenChange={handleCloseUploadDialog}
onUpload={handleUploadBinary} onUpload={handleUploadBinary}
isUploading={uploadMutation.isPending} isUploading={uploadMutation.isPending}
uploadError={uploadError} uploadError={uploadError}
onReset={uploadMutation.reset} onReset={uploadMutation.reset}
engines={engines}
enginesLoading={enginesLoading}
/> />
<ConfirmDialog <ConfirmDialog