Skip to content

Commit

Permalink
Kernel image upload cli (#630)
Browse files Browse the repository at this point in the history
* Support for uploading kernel images via CLI

* Frontend failure & model changes

* Uploading kernel images via CLI

* Kernel-Images upload instructions

* Kernel image handling in listing view

* Ensure kernel images are downloaded with the correct format

* Added file size and download confirmation for kernel files

* Improve UI for kernel image

* Refactor S3 operations and additional changes

* Removed f-string from logger

* Added checksum for uploading kernel image
  • Loading branch information
ivntsng authored Nov 21, 2024
1 parent 7903b49 commit 655e4a9
Show file tree
Hide file tree
Showing 9 changed files with 382 additions and 32 deletions.
25 changes: 23 additions & 2 deletions frontend/src/components/listing/ListingArtifactRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { FaFileArchive } from "react-icons/fa";
import { FaFile, FaFileArchive } from "react-icons/fa";
import { Link } from "react-router-dom";

import placeholder from "@/components/listing/pics/placeholder.jpg";
import { Artifact } from "@/components/listing/types";
import ROUTES from "@/lib/types/routes";
import { formatFileSize } from "@/lib/utils/formatters";

interface Props {
artifact: Artifact;
Expand All @@ -30,7 +31,27 @@ const ListingArtifactRenderer = ({ artifact }: Props) => {
</Link>
<div className="text-center">
<div className="font-medium">{artifact.name}</div>
<div className="text-sm">
<div className="text-xs">
{new Date(artifact.timestamp * 1000).toLocaleString()}
</div>
</div>
</div>
);
case "kernel":
return (
<div className="w-full h-full flex flex-col items-center justify-center gap-1 p-2">
<div className="p-2 sm:p-4">
<FaFile className="w-12 h-12 sm:w-16 sm:h-16" />
</div>
<div className="text-center w-full px-2">
<div className="font-medium text-sm sm:text-base truncate">
{artifact.name}
</div>
<div className="text-xs text-gray-500">Kernel Image File</div>
{artifact.size && (
<div className="text-xs">{formatFileSize(artifact.size)}</div>
)}
<div className="text-xs">
{new Date(artifact.timestamp * 1000).toLocaleString()}
</div>
</div>
Expand Down
46 changes: 45 additions & 1 deletion frontend/src/components/listing/ListingImageGallery.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useState } from "react";
import { FaStar, FaTimes } from "react-icons/fa";
import { FaFileDownload, FaStar, FaTimes } from "react-icons/fa";

import { Artifact } from "@/components/listing/types";
import DeleteConfirmationModal from "@/components/modals/DeleteConfirmationModal";
import DownloadConfirmationModal from "@/components/modals/DownloadConfirmationModal";
import { useAlertQueue } from "@/hooks/useAlertQueue";
import { useAuthentication } from "@/hooks/useAuth";
import { formatFileSize } from "@/lib/utils/formatters";

import { Tooltip } from "../ui/ToolTip";
import { Button } from "../ui/button";
Expand Down Expand Up @@ -41,6 +43,7 @@ const ListingImageItem = ({
const [isDeleting, setIsDeleting] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showDownloadModal, setShowDownloadModal] = useState(false);

const auth = useAuthentication();
const { addErrorAlert } = useAlertQueue();
Expand Down Expand Up @@ -121,6 +124,29 @@ const ListingImageItem = ({
}
};

const isKernelImage =
artifact.name.toLowerCase().endsWith(".img") ||
artifact.artifact_type === "kernel";

const initiateDownload = (e: React.MouseEvent) => {
e.stopPropagation();
setShowDownloadModal(true);
};

const handleDownload = () => {
if (!artifact.urls.large) {
addErrorAlert("Artifact URL not available.");
return;
}

const link = document.createElement("a");
link.href = artifact.urls.large;
link.download = artifact.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};

return (
<>
<div
Expand Down Expand Up @@ -148,6 +174,17 @@ const ListingImageItem = ({
</Button>
</Tooltip>
)}
{isKernelImage && (
<Tooltip content="Download Kernel" position="left" size="sm">
<Button
variant="default"
onClick={initiateDownload}
className="text-gray-12 bg-gray-2 hover:bg-gray-12 hover:text-gray-2"
>
<FaFileDownload />
</Button>
</Tooltip>
)}
<Tooltip content="Delete Image" position="left" size="sm">
<Button
variant={isDeleting ? "ghost" : "destructive"}
Expand All @@ -171,6 +208,13 @@ const ListingImageItem = ({
description="Are you sure you want to delete this image? This action cannot be undone."
buttonText="Delete Image"
/>
<DownloadConfirmationModal
isOpen={showDownloadModal}
onClose={() => setShowDownloadModal(false)}
onDownload={handleDownload}
fileName={artifact.name}
fileSize={artifact.size ? formatFileSize(artifact.size) : "Unknown"}
/>
</>
);
};
Expand Down
75 changes: 64 additions & 11 deletions frontend/src/components/listing/ListingOnshape.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ const ListingOnshape = (props: Props) => {
const [permUrl, setPermUrl] = useState<string | null>(onshapeUrl);
const [updateOnshape, setUpdateOnshape] = useState(false);
const [showInstructions, setShowInstructions] = useState(false);
const [showKernelInstructions, setShowKernelInstructions] = useState(false);

const handleCopy = async () => {
try {
Expand Down Expand Up @@ -369,6 +370,10 @@ const ListingOnshape = (props: Props) => {
setShowInstructions(!showInstructions);
};

const toggleKernelInstructions = () => {
setShowKernelInstructions(!showKernelInstructions);
};

const renderUrdfInstructions = (listingId: string) => (
<div className="mt-6 p-8 bg-gray-12 rounded-xl shadow-lg w-full border border-gray-200">
<h4 className="text-2xl font-semibold mb-6 text-gray-1">
Expand All @@ -393,19 +398,56 @@ const ListingOnshape = (props: Props) => {
</div>
);

const renderKernelInstructions = (listingId: string) => (
<div className="mt-6 p-8 bg-gray-12 rounded-xl shadow-lg w-full border border-gray-200">
<h4 className="text-2xl font-semibold mb-6 text-gray-1">
Kernel Image Upload Instructions
</h4>
<ol className="list-decimal list-outside ml-6 space-y-6 text-base text-gray-1">
<li className="leading-relaxed">
Install the K-Scale CLI:
<CopyableCode
code="pip install kscale"
className="bg-gray-12 text-green-600 p-4 rounded-lg mt-3 font-mono text-sm border border-gray-200"
/>
</li>
<li className="leading-relaxed">
Upload your kernel image:
<CopyableCode
code={`kscale kernel-images upload ${listingId} /path/to/your/kernel/kernel_image.img`}
className="bg-gray-12 text-green-600 p-2 sm:p-4 rounded-lg mt-3 font-mono text-xs sm:text-sm border border-gray-200 break-all whitespace-pre-wrap"
/>
</li>
</ol>
</div>
);

const renderContent = () => {
if (isEditing) {
return (
<div className="flex flex-col items-start w-full">
<UrlInput url={url} setUrl={setUrl} handleSave={handleSave} />
{edit && (
<UpdateButtons
isEditing={true}
onEdit={() => {}}
onSave={handleSave}
onToggleInstructions={toggleInstructions}
showInstructions={showInstructions}
/>
<div className="flex flex-wrap gap-2 pt-2">
<IconButton
icon={FaCheck}
label="Save"
onClick={handleSave}
disabled={false}
/>
<HelpButton
showInstructions={showInstructions}
onToggle={toggleInstructions}
/>
<Button onClick={toggleKernelInstructions} variant="default">
<FaInfoCircle />
<span className="ml-2 text-sm sm:text-base">
{showKernelInstructions
? "Hide Kernel Instructions"
: "View Kernel Image Upload Instructions"}
</span>
</Button>
</div>
)}
</div>
);
Expand All @@ -418,10 +460,20 @@ const ListingOnshape = (props: Props) => {
Add Onshape URL
</Button>
{edit && (
<HelpButton
showInstructions={showInstructions}
onToggle={toggleInstructions}
/>
<>
<HelpButton
showInstructions={showInstructions}
onToggle={toggleInstructions}
/>
<Button onClick={toggleKernelInstructions} variant="default">
<FaInfoCircle />
<span className="ml-2 text-sm sm:text-base">
{showKernelInstructions
? "Hide Kernel Instructions"
: "View Kernel Image Upload Instructions"}
</span>
</Button>
</>
)}
</div>
);
Expand Down Expand Up @@ -473,6 +525,7 @@ const ListingOnshape = (props: Props) => {
<div className="flex flex-col w-full">
{renderContent()}
{showInstructions && renderUrdfInstructions(listingId)}
{showKernelInstructions && renderKernelInstructions(listingId)}
</div>
)}
</CardTitle>
Expand Down
55 changes: 55 additions & 0 deletions frontend/src/components/modals/DownloadConfirmationModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Modal from "@/components/ui/Modal";
import { Button } from "@/components/ui/button";

interface DownloadConfirmationModalProps {
isOpen: boolean;
onClose: () => void;
onDownload: () => void;
fileName: string;
fileSize: string;
}

const DownloadConfirmationModal = ({
isOpen,
onClose,
onDownload,
fileName,
fileSize,
}: DownloadConfirmationModalProps) => {
return (
<Modal isOpen={isOpen} onClose={onClose}>
<div className="p-8 bg-gray-12 rounded-lg shadow-lg">
<h2 className="text-2xl font-bold mb-4 text-gray-2">
Download Kernel Image
</h2>
<div className="mb-6 space-y-2 text-gray-7">
<p>Are you sure you want to download this kernel image?</p>
<p>
<span className="text-gray-2 font-bold mb-4">File: </span>
{fileName}
</p>
<p>
<span className="text-gray-2 front-bold mb-4">Size: </span>
{fileSize}
</p>
</div>
<div className="flex justify-end space-x-4">
<Button onClick={onClose} variant="outline">
Cancel
</Button>
<Button
onClick={() => {
onDownload();
onClose();
}}
variant="default"
>
Download
</Button>
</div>
</div>
</Modal>
);
};

export default DownloadConfirmationModal;
Loading

0 comments on commit 655e4a9

Please sign in to comment.