Skip to content

Commit

Permalink
Added file size and download confirmation for kernel files
Browse files Browse the repository at this point in the history
  • Loading branch information
ivntsng committed Nov 20, 2024
1 parent 3656533 commit a2e6fda
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 8 deletions.
6 changes: 6 additions & 0 deletions frontend/src/components/listing/ListingArtifactRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 Down Expand Up @@ -45,6 +46,11 @@ const ListingArtifactRenderer = ({ artifact }: Props) => {
<div className="text-center">
<div className="font-medium">{artifact.name}</div>
<div className="text-xs text-gray-500 mt-1">Kernel Image File</div>
{artifact.size && (
<div className="text-xs text-gray-500">
{formatFileSize(artifact.size)}
</div>
)}
<div className="text-sm">
{new Date(artifact.timestamp * 1000).toLocaleString()}
</div>
Expand Down
24 changes: 17 additions & 7 deletions frontend/src/components/listing/ListingImageGallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ 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 @@ -125,8 +128,12 @@ const ListingImageItem = ({
artifact.name.toLowerCase().endsWith(".img") ||
artifact.artifact_type === "kernel";

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

const handleDownload = () => {
if (!artifact.urls.large) {
addErrorAlert("Artifact URL not available.");
return;
Expand Down Expand Up @@ -168,14 +175,10 @@ const ListingImageItem = ({
</Tooltip>
)}
{isKernelImage && (
<Tooltip
content="Download Kernel Image"
position="left"
size="sm"
>
<Tooltip content="Download Kernel" position="left" size="sm">
<Button
variant="default"
onClick={handleDownload}
onClick={initiateDownload}
className="text-gray-12 bg-gray-2 hover:bg-gray-12 hover:text-gray-2"
>
<FaFileDownload />
Expand Down Expand Up @@ -205,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
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;
11 changes: 10 additions & 1 deletion frontend/src/gen/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1807,6 +1807,13 @@ export interface components {
order: components["schemas"]["Order"];
product: components["schemas"]["ProductInfo"] | null;
};
/** PresignedUrlResponse */
PresignedUrlResponse: {
/** Upload Url */
upload_url: string;
/** Artifact Id */
artifact_id: string;
};
/** ProcessPreorderResponse */
ProcessPreorderResponse: {
/** Status */
Expand Down Expand Up @@ -1958,6 +1965,8 @@ export interface components {
* @default false
*/
can_edit: boolean;
/** Size */
size?: number | null;
};
/** SingleRobotResponse */
SingleRobotResponse: {
Expand Down Expand Up @@ -2719,7 +2728,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": Record<string, never>;
"application/json": components["schemas"]["PresignedUrlResponse"];
};
};
/** @description Validation Error */
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/lib/utils/formatters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const formatFileSize = (bytes: number): string => {
if (!bytes) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
};
16 changes: 16 additions & 0 deletions store/app/routers/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import Annotated, Literal, Self

from boto3.dynamodb.conditions import Key
from botocore.exceptions import ClientError
from fastapi import APIRouter, Depends, HTTPException, UploadFile, status
from fastapi.responses import RedirectResponse
from pydantic.main import BaseModel
Expand Down Expand Up @@ -135,6 +136,7 @@ class SingleArtifactResponse(BaseModel):
urls: ArtifactUrls
is_main: bool = False
can_edit: bool = False
size: int | None = None

@classmethod
async def from_artifact(
Expand Down Expand Up @@ -187,6 +189,19 @@ async def get_listing(listing: Listing | None) -> Listing:
get_listing(listing),
)

s3_filename = get_artifact_name(artifact=artifact)
size = None
if crud is not None:
try:
s3_object = await crud.s3.meta.client.head_object(
Bucket=settings.s3.bucket, Key=f"{settings.s3.prefix}{s3_filename}"
)
size = s3_object.get("ContentLength")
except ClientError as e:
logger.error(f"Failed to get S3 object size: {e}")
except Exception as e:
logger.error(f"Unexpected error getting file size: {e}")

return cls(
artifact_id=artifact.id,
listing_id=artifact.listing_id,
Expand All @@ -199,6 +214,7 @@ async def get_listing(listing: Listing | None) -> Listing:
urls=get_artifact_url_response(artifact=artifact),
is_main=artifact.is_main,
can_edit=can_edit,
size=size,
)


Expand Down

0 comments on commit a2e6fda

Please sign in to comment.