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
57 changes: 57 additions & 0 deletions frontend/src/components/shared/modals/confirmation-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Button } from "@/components/ui/button";
import { Dialog } from "@/components/ui/dialog";
import { ButtonVariant, SHOELACE_SIZES } from "@/enums";
import useScreenSize from "@/hooks/use-screen-size";

type ConfirmationModalProps = {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
message: string;
icon: React.ReactNode;
isConfirming?: boolean;
confirmLabel?: string;
cancelLabel?: string;
};

export const ConfirmationModal = ({

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel the new modals are too wide on the desktop. You should reduce the width so it looks close to the design

isOpen,
onClose,
onConfirm,
message,
icon,
isConfirming = false,
confirmLabel = "Confirm",
cancelLabel = "Cancel",
}: ConfirmationModalProps) => {
const { isMobile } = useScreenSize();

if (!isOpen) return null;

return (
<Dialog
isOpened={isOpen}
closeDialog={onClose}
preventClose={isConfirming}
noHeader
size={!isMobile ? SHOELACE_SIZES.SMALL : undefined}
>
<div className="flex flex-col items-center gap-y-4 py-8 px-4">
{icon}
<p className="text-sm text-dark text-center">{message}</p>
<div className="flex gap-x-3 w-full mt-2">
<Button
variant={ButtonVariant.TERTIARY}
onClick={onClose}
disabled={isConfirming}
>
{cancelLabel}
</Button>
<Button onClick={onConfirm} spinner={isConfirming}>
{confirmLabel}
</Button>
</div>
</div>
</Dialog>
);
};
2 changes: 2 additions & 0 deletions frontend/src/components/shared/modals/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { DeleteModal } from "./delete-modal";
export { ConfirmationModal } from "./confirmation-modal";
export { SuccessModal } from "./success-modal";
41 changes: 41 additions & 0 deletions frontend/src/components/shared/modals/success-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Button } from "@/components/ui/button";
import { Dialog } from "@/components/ui/dialog";
import { SHOELACE_SIZES } from "@/enums";
import useScreenSize from "@/hooks/use-screen-size";

type SuccessModalProps = {
isOpen: boolean;
onClose: () => void;
message: string;
icon: React.ReactNode;
closeLabel?: string;
};

export const SuccessModal = ({
isOpen,
onClose,
message,
icon,
closeLabel = "Done",
}: SuccessModalProps) => {
const { isMobile } = useScreenSize();

if (!isOpen) return null;

return (
<Dialog
size={!isMobile ? SHOELACE_SIZES.SMALL : undefined}
isOpened={isOpen}
closeDialog={onClose}
noHeader
>
<div className="flex flex-col items-center gap-y-4 py-6 px-4">
{icon}
<p className="text-body-2 font-semibold text-center">{message}</p>
<Button onClick={onClose} className="mt-2">
{closeLabel}
</Button>
</div>
</Dialog>
);
};
1 change: 1 addition & 0 deletions frontend/src/components/shared/training-status-badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const TrainingStatusBadge = ({ status }: { status: string }) => {
submitted: "blue",
running: "yellow",
pending: "yellow",
published: "green",
};

return (
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/components/ui/icons/warning-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { IconProps, ShoelaceSlotProps } from "@/types";
import React from "react";

export const WarningIcon: React.FC<IconProps & ShoelaceSlotProps> = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
d="M11.9996 1.99805C17.5233 1.99805 22.0011 6.47589 22.0011 11.9996C22.0011 17.5233 17.5233 22.0011 11.9996 22.0011C6.47589 22.0011 1.99805 17.5233 1.99805 11.9996C1.99805 6.47589 6.47589 1.99805 11.9996 1.99805ZM11.9958 10.2486C11.483 10.249 11.0605 10.6353 11.0031 11.1327L10.9964 11.2493L11 16.7509L11.0068 16.8675C11.0649 17.3648 11.4879 17.7506 12.0007 17.7502C12.5135 17.7499 12.936 17.3636 12.9934 16.8662L13 16.7496L12.9964 11.248L12.9896 11.1314C12.9316 10.6341 12.5086 10.2483 11.9958 10.2486ZM12 6.49908C11.3089 6.49908 10.7485 7.0594 10.7485 7.7506C10.7485 8.4418 11.3089 9.00212 12 9.00212C12.6912 9.00212 13.2516 8.4418 13.2516 7.7506C13.2516 7.0594 12.6912 6.49908 12 6.49908Z"
fill="#FFCC00"
/>
</svg>
);
20 changes: 20 additions & 0 deletions frontend/src/components/ui/icons/wavy-check-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { IconProps, ShoelaceSlotProps } from "@/types";
import React from "react";

export const WavyCheckIcon: React.FC<ShoelaceSlotProps & IconProps> = (
props,
) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
d="M9.83585 2.03398C9.94837 2.07226 10.0583 2.11779 10.1649 2.17028L11.4477 2.80183C11.7958 2.97323 12.2038 2.97323 12.5519 2.80183L13.8347 2.17028C15.1972 1.49942 16.8457 2.06018 17.5165 3.42276L17.59 3.58509L17.6528 3.75183L18.1133 5.10543C18.2383 5.4728 18.5268 5.7613 18.8941 5.88627L20.2477 6.34673C21.6856 6.83585 22.4547 8.39799 21.9656 9.83585C21.9273 9.94837 21.8818 10.0583 21.8293 10.1649L21.1977 11.4477C21.0263 11.7958 21.0263 12.2038 21.1977 12.5519L21.8293 13.8347C22.5002 15.1972 21.9394 16.8457 20.5768 17.5165C20.4702 17.569 20.3603 17.6146 20.2477 17.6528L18.8941 18.1133C18.5268 18.2383 18.2383 18.5268 18.1133 18.8941L17.6528 20.2477C17.1637 21.6856 15.6016 22.4547 14.1637 21.9656C14.0512 21.9273 13.9413 21.8818 13.8347 21.8293L12.5519 21.1977C12.2038 21.0263 11.7958 21.0263 11.4477 21.1977L10.1649 21.8293C8.80233 22.5002 7.15389 21.9394 6.48303 20.5768C6.43053 20.4702 6.385 20.3603 6.34673 20.2477L5.88627 18.8941C5.7613 18.5268 5.4728 18.2383 5.10543 18.1133L3.75183 17.6528C2.31396 17.1637 1.54486 15.6016 2.03398 14.1637C2.07226 14.0512 2.11779 13.9413 2.17028 13.8347L2.80183 12.5519C2.97323 12.2038 2.97323 11.7958 2.80183 11.4477L2.17028 10.1649C1.49942 8.80233 2.06018 7.15389 3.42276 6.48303C3.52939 6.43053 3.63931 6.385 3.75183 6.34673L5.10543 5.88627C5.4728 5.7613 5.7613 5.4728 5.88627 5.10543L6.34673 3.75183C6.83585 2.31396 8.39799 1.54486 9.83585 2.03398ZM15.4695 8.96946L10.0502 14.3887L8.07595 12.0197C7.81078 11.7014 7.33786 11.6584 7.01965 11.9236C6.70144 12.1888 6.65845 12.6617 6.92362 12.9799L9.42362 15.9799C9.70596 16.3187 10.2183 16.342 10.5301 16.0301L16.5301 10.0301C16.823 9.73722 16.823 9.26235 16.5301 8.96946C16.2372 8.67657 15.7624 8.67657 15.4695 8.96946Z"
fill="#34C759"
/>
</svg>
);
33 changes: 33 additions & 0 deletions frontend/src/features/user-profile/api/predictions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,36 @@ export const useRetryOfflinePrediction = ({
mutationFn: retryOfflinePrediction,
});
};

export const publishPrediction = ({
predictionId,
published,
}: {
predictionId: number;
published: boolean;
}) => {
return apiClient.patch(
API_ENDPOINTS.PUBLISH_OFFLINE_PREDICTION(predictionId),
{ published },
);
};

type TUsePublishPredictionOptions = {
mutationConfig?: MutationConfig<typeof publishPrediction>;
};
export const usePublishPrediction = ({
mutationConfig,
}: TUsePublishPredictionOptions) => {
const queryClient = useQueryClient();
const { onSuccess, ...restConfig } = mutationConfig || {};
return useMutation({
onSuccess: async (...args) => {
onSuccess?.(...args);
queryClient.invalidateQueries({
queryKey: ["offline-predictions"],
});
},
...restConfig,
mutationFn: publishPrediction,
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { formatDate, formatDuration, formatNumber } from "@/utils";
import { OfflinePredictionActions } from "./offline-predictions-actions";
import { MapIcon } from "@/components/ui/icons";
import { MapSwipeProjectIsActive } from "./mapswipe-project-active";
import { getDisplayStatus } from "@/features/user-profile/utils/get-display-status";

export const OfflinePredictionCard = ({
predictionResult,
Expand Down Expand Up @@ -41,7 +42,13 @@ export const OfflinePredictionCard = ({
}
/>
</div>
<TrainingStatusBadge status={predictionResult.status} />
<TrainingStatusBadge
status={getDisplayStatus(
predictionResult.status,
predictionResult.published,
)}
/>

<div className="flex gap-x-4 mt-2">
<Button
variant={ButtonVariant.TERTIARY}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
useRetryOfflinePrediction,
useTerminateOfflinePrediction,
} from "@/features/user-profile/api/predictions";
import { PublishPredictionFlow } from "@/features/user-profile/components/offline-predictions/publish-prediction-flow";
import { useState } from "react";

export const OfflinePredictionActions = ({
handlePredictionResultModal,
Expand All @@ -31,12 +33,14 @@ export const OfflinePredictionActions = ({
}) => {
const { copyToClipboard } = useCopyToClipboard();
const { dropdownRef } = useDropdownMenu();
const [isPublishFlowOpen, setIsPublishFlowOpen] = useState<boolean>(false);

const handleSettingsInfo = () => {
if (dropdownRef?.current) {
dropdownRef.current.show();
}
};

const { mutate: terminationMutation } = useTerminateOfflinePrediction({
mutationConfig: {
onSuccess: (data) => {
Expand All @@ -60,8 +64,20 @@ export const OfflinePredictionActions = ({
},
predictionId: Number(predictionResult.id),
});
// Extracted isFinished and hasResults for better readability and reusablity
const isFinished = predictionResult.status === ModelTrainingStatus.FINISHED;
const hasResults = (predictionResult?.result?.count ?? 0) > 0;
const canPublishOrRetract = isFinished && hasResults;

return (
<>
<PublishPredictionFlow
predictionId={predictionResult.id}
isPublished={predictionResult.published}
isOpen={isPublishFlowOpen}
onClose={() => setIsPublishFlowOpen(false)}
/>

<OfflinePredictionsSettingsInfo
disableSettingsInfoIcon
predictionConfig={predictionResult.config}
Expand Down Expand Up @@ -115,9 +131,7 @@ export const OfflinePredictionActions = ({
{
name: "Download Results",
value: "Download Results",
disabled:
predictionResult.status !== ModelTrainingStatus.FINISHED ||
predictionResult?.result?.count === 0,
disabled: !isFinished || !hasResults,
subMenuItems: [
{
name: "As Points",
Expand Down Expand Up @@ -154,9 +168,7 @@ export const OfflinePredictionActions = ({
e.stopPropagation();
handlePredictionResultModal(predictionResult);
},
disabled:
predictionResult.status !== ModelTrainingStatus.FINISHED ||
predictionResult?.result?.count === 0,
disabled: !isFinished || !hasResults,
},
{
name: "Copy Results Link",
Expand All @@ -171,9 +183,7 @@ export const OfflinePredictionActions = ({
);
showSuccessToast("Copied results link to clipboard!");
},
disabled:
predictionResult.status !== ModelTrainingStatus.FINISHED ||
predictionResult?.result?.count === 0,
disabled: !isFinished || !hasResults,
},
...(showSettingsInfo
? [
Expand All @@ -198,9 +208,7 @@ export const OfflinePredictionActions = ({
e.stopPropagation();
handleCreateOrViewMapSwipeProject(predictionResult);
},
disabled:
predictionResult.status !== ModelTrainingStatus.FINISHED ||
predictionResult?.result?.count === 0,
disabled: !isFinished || !hasResults,
},
{
name: "View Logs",
Expand All @@ -224,6 +232,22 @@ export const OfflinePredictionActions = ({
showSuccessToast("Copied imagery link to clipboard");
},
},
...(canPublishOrRetract
? [
{
name: predictionResult.published
? "Retract Result"
: "Publish Result",
value: predictionResult.published
? "Retract Result"
: "Publish Result",
onClick: (e: { stopPropagation: () => void }) => {
e.stopPropagation();
setIsPublishFlowOpen(true);
},
},
]
: []),
]}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { OfflinePredictionsSettingsInfo } from "./offline-predictions-settings-i
import { OfflinePredictionActions } from "./offline-predictions-actions";
import { ToolTip } from "@/components/ui/tooltip";
import { MapSwipeProjectIsActive } from "./mapswipe-project-active";

import { getDisplayStatus } from "@/features/user-profile/utils/get-display-status";
type OfflinePredictionsTableProps = {
data: TOfflinePrediction[];
isError: boolean;
Expand Down Expand Up @@ -69,8 +69,15 @@ const columnDefinitions = (
{
header: "Status",
accessorKey: "status",
cell: (row) => <TrainingStatusBadge status={row.getValue() as string} />,
cell: ({ row }) => {
const displayStatus = getDisplayStatus(
row.original.status,
row.original.published,
);
return <TrainingStatusBadge status={displayStatus} />;
},
},

{
header: "Duration",
accessorFn: (row) =>
Expand Down
Loading