fix(admin-ui): show failure reason for batch jobs (#3526)
* fix: display error messages for batch jobs * feat: add changesets * feat: tooltip size, load more jobs --------- Co-authored-by: fPolic <frane@medusajs.com> Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
5
.changeset/thirty-shrimps-fly.md
Normal file
5
.changeset/thirty-shrimps-fly.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/admin-ui": patch
|
||||
---
|
||||
|
||||
fix(admin-ui): display error messages for batch jobs
|
||||
@@ -0,0 +1,3 @@
|
||||
Product Variant ID,SKU,Price EUR,Price NA [USD]
|
||||
,MEDUSA-SWEAT-SMALL,15,13.5
|
||||
variant_1234,,15,13.5
|
||||
|
@@ -0,0 +1,6 @@
|
||||
Product Id;Product Handle;Product Title;Product Subtitle;Product Description;Product Status;Product Thumbnail;Product Weight;Product Length;Product Width;Product Height;Product HS Code;Product Origin Country;Product MID Code;Product Material;Product Collection Title;Product Collection Handle;Product Type;Product Tags;Product Discountable;Product External Id;Product Profile Name;Product Profile Type;Variant Id;Variant Title;Variant SKU;Variant Barcode;Variant Inventory Quantity;Variant Allow Backorder;Variant Manage Inventory;Variant Weight;Variant Length;Variant Width;Variant Height;Variant HS Code;Variant Origin Country;Variant MID Code;Variant Material;Price EUR;Price USD;Option 1 Name;Option 1 Value;Image 1 Url;Image 2 Url
|
||||
;coffee-mug-v2;Medusa Coffee Mug;;Every programmer's best friend.;published;https://medusa-public-images.s3.eu-west-1.amazonaws.com/coffee-mug.png;400;;;;;;;;;;;;true;;;;;One Size;;;100;false;true;;;;;;;;;1000;1200;Size;One Size;https://medusa-public-images.s3.eu-west-1.amazonaws.com/coffee-mug.png;
|
||||
;sweatpants-v2;Medusa Sweatpants;;Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.;published;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;400;;;;;;;;;;;;true;;;;;S;;;100;false;true;;;;;;;;;2950;3350;Size;S;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png
|
||||
;sweatpants-v2;Medusa Sweatpants;;Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.;published;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;400;;;;;;;;;;;;true;;;;;M;;;100;false;true;;;;;;;;;2950;3350;Size;M;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png
|
||||
;sweatpants-v2;Medusa Sweatpants;;Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.;published;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;400;;;;;;;;;;;;true;;;;;L;;;100;false;true;;;;;;;;;2950;3350;Size;L;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png
|
||||
;sweatpants-v2;Medusa Sweatpants;;Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.;published;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;400;;;;;;;;;;;;true;;;;;XL;;;100;false;true;;;;;;;;;2950;3350;Size;XL;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png;https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png
|
||||
|
@@ -10,6 +10,7 @@ export type TooltipProps = RadixTooltip.TooltipContentProps &
|
||||
content: React.ReactNode
|
||||
side?: "bottom" | "left" | "top" | "right"
|
||||
onClick?: React.ButtonHTMLAttributes<HTMLButtonElement>["onClick"]
|
||||
maxWidth?: number
|
||||
}
|
||||
|
||||
const Tooltip = ({
|
||||
@@ -19,6 +20,7 @@ const Tooltip = ({
|
||||
defaultOpen,
|
||||
onOpenChange,
|
||||
delayDuration,
|
||||
maxWidth = 220,
|
||||
className,
|
||||
side,
|
||||
onClick,
|
||||
@@ -43,10 +45,10 @@ const Tooltip = ({
|
||||
"inter-small-semibold text-grey-50",
|
||||
"bg-grey-0 shadow-dropdown rounded-rounded py-2 px-3",
|
||||
"border-grey-20 border border-solid",
|
||||
"max-w-[220px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
style={{ ...props.style, maxWidth }}
|
||||
>
|
||||
{content}
|
||||
</RadixTooltip.Content>
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
import { ReactNode } from "react"
|
||||
import clsx from "clsx"
|
||||
|
||||
import Tooltip from "../../atoms/tooltip"
|
||||
|
||||
type Props = {
|
||||
fileName: string
|
||||
fileSize?: string
|
||||
errorMessage?: string
|
||||
hasError?: boolean
|
||||
icon?: ReactNode
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const BatchJobFileCard = ({ fileName, fileSize, icon, onClick }: Props) => {
|
||||
const BatchJobFileCard = ({
|
||||
fileName,
|
||||
fileSize,
|
||||
icon,
|
||||
onClick,
|
||||
hasError,
|
||||
errorMessage,
|
||||
}: Props) => {
|
||||
const preparedOnClick = onClick ?? (() => void 0)
|
||||
|
||||
return (
|
||||
@@ -27,9 +39,26 @@ const BatchJobFileCard = ({ fileName, fileSize, icon, onClick }: Props) => {
|
||||
{fileName}
|
||||
</div>
|
||||
|
||||
{!!fileSize && (
|
||||
<div className="text-grey-40 inter-small-regular">{fileSize}</div>
|
||||
)}
|
||||
<Tooltip
|
||||
side="top"
|
||||
open={hasError ? undefined : false}
|
||||
maxWidth={320}
|
||||
content={
|
||||
hasError && errorMessage ? (
|
||||
<span className="font-normal text-rose-500">{errorMessage}</span>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{!!fileSize && (
|
||||
<div
|
||||
className={clsx("text-grey-40 inter-small-regular", {
|
||||
"text-rose-500": hasError,
|
||||
})}
|
||||
>
|
||||
{fileSize}
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ import MedusaIcon from "../../fundamentals/icons/medusa-icon"
|
||||
import { ActivityCard } from "../../molecules/activity-card"
|
||||
import BatchJobFileCard from "../../molecules/batch-job-file-card"
|
||||
import { batchJobDescriptionBuilder, BatchJobOperation } from "./utils"
|
||||
import CrossIcon from "../../fundamentals/icons/cross-icon"
|
||||
|
||||
/**
|
||||
* Retrieve a batch job and refresh the data depending on the last batch job status
|
||||
@@ -97,6 +98,8 @@ const BatchJobActivityCard = (props: { batchJob: BatchJob }) => {
|
||||
batchJob.status !== "failed" &&
|
||||
batchJob.status !== "canceled"
|
||||
|
||||
const hasError = batchJob.status === "failed"
|
||||
|
||||
const canDownload =
|
||||
batchJob.status === "completed" && batchJob.result?.file_key
|
||||
|
||||
@@ -157,7 +160,11 @@ const BatchJobActivityCard = (props: { batchJob: BatchJob }) => {
|
||||
|
||||
const icon =
|
||||
batchJob.status !== "completed" && batchJob.status !== "canceled" ? (
|
||||
<Spinner size={"medium"} variant={"secondary"} />
|
||||
batchJob.status === "failed" ? (
|
||||
<CrossIcon size={18} />
|
||||
) : (
|
||||
<Spinner size={"medium"} variant={"secondary"} />
|
||||
)
|
||||
) : (
|
||||
<FileIcon
|
||||
className={clsx({
|
||||
@@ -175,7 +182,7 @@ const BatchJobActivityCard = (props: { batchJob: BatchJob }) => {
|
||||
preprocessing: `Preparing ${operation.toLowerCase()}...`,
|
||||
processing: `Processing ${operation.toLowerCase()}...`,
|
||||
completed: `Successful ${operation.toLowerCase()}`,
|
||||
failed: `Failed batch ${operation.toLowerCase()} job`,
|
||||
failed: `Job failed`,
|
||||
canceled: `Canceled batch ${operation.toLowerCase()} job`,
|
||||
}[batchJob.status]
|
||||
|
||||
@@ -185,6 +192,8 @@ const BatchJobActivityCard = (props: { batchJob: BatchJob }) => {
|
||||
fileName={fileName}
|
||||
icon={icon}
|
||||
fileSize={fileSize}
|
||||
hasError={hasError}
|
||||
errorMessage={batchJob?.result?.errors?.join(" \n")}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ReactNode, useState } from "react"
|
||||
import clsx from "clsx"
|
||||
|
||||
import Button from "../../fundamentals/button"
|
||||
import CheckCircleIcon from "../../fundamentals/icons/check-circle-icon"
|
||||
@@ -8,12 +9,13 @@ import FileIcon from "../../fundamentals/icons/file-icon"
|
||||
import Modal from "../../molecules/modal"
|
||||
import TrashIcon from "../../fundamentals/icons/trash-icon"
|
||||
import WarningCircleIcon from "../../fundamentals/icons/warning-circle"
|
||||
import XCircleIcon from "../../fundamentals/icons/x-circle-icon"
|
||||
import clsx from "clsx"
|
||||
import Tooltip from "../../atoms/tooltip"
|
||||
|
||||
type FileSummaryProps = {
|
||||
name: string
|
||||
size: number
|
||||
hasError?: boolean
|
||||
errorMessage?: string
|
||||
action: ReactNode
|
||||
progress?: number
|
||||
status?: string
|
||||
@@ -23,7 +25,7 @@ type FileSummaryProps = {
|
||||
* Render an upload file summary (& upload progress).
|
||||
*/
|
||||
function FileSummary(props: FileSummaryProps) {
|
||||
const { action, name, progress, size, status } = props
|
||||
const { action, name, progress, size, status, hasError, errorMessage } = props
|
||||
|
||||
const formattedSize =
|
||||
size / 1024 < 10
|
||||
@@ -32,32 +34,46 @@ function FileSummary(props: FileSummaryProps) {
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
style={{ width: `${progress}%` }}
|
||||
className="bg-grey-5 transition-width absolute h-full duration-150 ease-in-out"
|
||||
/>
|
||||
<div className="border-1 relative mt-6 flex items-center rounded-xl border">
|
||||
<div className="m-4">
|
||||
<FileIcon size={30} fill={progress ? "#9CA3AF" : "#2DD4BF"} />
|
||||
</div>
|
||||
|
||||
<div className="my-6 flex-1">
|
||||
<div className="text-small text-grey-90 leading-5">{name}</div>
|
||||
<div className="text-xsmall text-grey-50 leading-4">
|
||||
{status || formattedSize}
|
||||
<Tooltip
|
||||
side="top"
|
||||
maxWidth={320}
|
||||
open={hasError ? undefined : false}
|
||||
content={
|
||||
hasError && errorMessage ? (
|
||||
<span className="font-normal text-rose-500">{errorMessage}</span>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={{ width: `${progress}%` }}
|
||||
className="bg-grey-5 transition-width absolute h-full duration-150 ease-in-out"
|
||||
/>
|
||||
<div className="border-1 relative mt-6 flex items-center rounded-xl border">
|
||||
<div className="m-4">
|
||||
<FileIcon size={30} fill={progress ? "#9CA3AF" : "#2DD4BF"} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="m-6">{action}</div>
|
||||
</div>
|
||||
<div className="my-6 flex-1">
|
||||
<div className="text-small text-grey-90 leading-5">{name}</div>
|
||||
<div
|
||||
className={clsx("text-xsmall text-grey-50 leading-4", {
|
||||
"text-rose-500": hasError,
|
||||
})}
|
||||
>
|
||||
{status || formattedSize}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="m-6">{action}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type UploadSummaryProps = {
|
||||
creations: number
|
||||
updates: number
|
||||
rejections?: number
|
||||
creations?: number
|
||||
updates?: number
|
||||
type: string
|
||||
}
|
||||
|
||||
@@ -65,25 +81,18 @@ type UploadSummaryProps = {
|
||||
* Render a batch update request summary.
|
||||
*/
|
||||
function UploadSummary(props: UploadSummaryProps) {
|
||||
const { creations, updates, rejections, type } = props
|
||||
const { creations, updates, type } = props
|
||||
return (
|
||||
<div className="flex gap-6">
|
||||
<div className="text-small text-grey-90 flex items-center">
|
||||
<CheckCircleIcon color="#9CA3AF" className="mr-2" />
|
||||
<span className="font-semibold"> {creations} </span> new {type}
|
||||
<span className="font-semibold"> {creations || 0} </span> new{" "}
|
||||
{type}
|
||||
</div>
|
||||
<div className="text-small text-grey-90 flex items-center">
|
||||
<WarningCircleIcon fill="#9CA3AF" className="mr-2" />
|
||||
<span className="font-semibold">{updates || 0} </span> updates
|
||||
</div>
|
||||
{updates && (
|
||||
<div className="text-small text-grey-90 flex items-center">
|
||||
<WarningCircleIcon fill="#9CA3AF" className="mr-2" />
|
||||
<span className="font-semibold">{updates} </span> updates
|
||||
</div>
|
||||
)}
|
||||
{rejections && (
|
||||
<div className="text-small text-grey-90 flex items-center">
|
||||
<XCircleIcon color="#9CA3AF" className="mr-2" />
|
||||
<span className="font-semibold">{rejections} </span> rejections
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -155,6 +164,8 @@ function DropArea(props: DropAreaProps) {
|
||||
type UploadModalProps = {
|
||||
type: string
|
||||
status?: string
|
||||
hasError?: boolean
|
||||
errorMessage?: string
|
||||
fileTitle: string
|
||||
description1Text: string
|
||||
description2Title: string
|
||||
@@ -184,8 +195,9 @@ function UploadModal(props: UploadModalProps) {
|
||||
onSubmit,
|
||||
onFileRemove,
|
||||
templateLink,
|
||||
progress,
|
||||
summary,
|
||||
hasError,
|
||||
errorMessage,
|
||||
status,
|
||||
type,
|
||||
} = props
|
||||
@@ -237,6 +249,8 @@ function UploadModal(props: UploadModalProps) {
|
||||
size={size!}
|
||||
name={name!}
|
||||
status={status}
|
||||
hasError={hasError}
|
||||
errorMessage={errorMessage}
|
||||
// progress={progress}
|
||||
// TODO: change this to actual progress once this we can track upload
|
||||
progress={100}
|
||||
@@ -284,7 +298,7 @@ function UploadModal(props: UploadModalProps) {
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
disabled={!canImport}
|
||||
disabled={!canImport || hasError}
|
||||
variant="primary"
|
||||
className="text-small"
|
||||
onClick={onSubmit}
|
||||
|
||||
@@ -125,7 +125,7 @@ function ImportProducts(props: ImportProductsProps) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const res = batchJob.result?.stat_descriptors[0].message.match(/\d+/g)
|
||||
const res = batchJob.result?.stat_descriptors?.[0].message.match(/\d+/g)
|
||||
|
||||
if (!res) {
|
||||
return undefined
|
||||
@@ -182,6 +182,7 @@ function ImportProducts(props: ImportProductsProps) {
|
||||
type="products"
|
||||
status={status}
|
||||
progress={progress}
|
||||
hasError={hasError}
|
||||
canImport={isPreprocessed}
|
||||
onSubmit={onSubmit}
|
||||
onClose={onClose}
|
||||
@@ -190,6 +191,7 @@ function ImportProducts(props: ImportProductsProps) {
|
||||
processUpload={processUpload}
|
||||
fileTitle={"products list"}
|
||||
templateLink="/temp/product-import-template.csv"
|
||||
errorMessage={batchJob?.result?.errors?.join(" \n")}
|
||||
description2Title="Unsure about how to arrange your list?"
|
||||
description2Text="Download the template below to ensure you are following the correct format."
|
||||
description1Text="Through imports you can add or update products. To update existing products/variants you must set an existing id in the Product/Variant id columns. If the value is unset a new record will be created. You will be asked for confirmation before we import products."
|
||||
|
||||
@@ -56,8 +56,8 @@ export const PollingProvider = ({ children }: PropsWithChildren) => {
|
||||
refetch,
|
||||
} = useAdminBatchJobs(
|
||||
{
|
||||
limit: 100,
|
||||
created_at: { gte: oneMonthAgo },
|
||||
failed_at: null,
|
||||
} as AdminGetBatchParams,
|
||||
{
|
||||
refetchOnWindowFocus: true,
|
||||
|
||||
Reference in New Issue
Block a user