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:
authored and GitHub committed 2023-03-21 16:13:39 +01:00
1 parent 7f2223b650
commit f831b7db37
9 files changed
+117 -47

No files matched your search

+5
View 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
1 Product Variant ID SKU Price EUR Price NA [USD]
2 MEDUSA-SWEAT-SMALL 15 13.5
3 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
1 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
2 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
3 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
4 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
5 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
6 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 content: React.ReactNode
side?: "bottom" | "left" | "top" | "right" side?: "bottom" | "left" | "top" | "right"
onClick?: React.ButtonHTMLAttributes<HTMLButtonElement>["onClick"] onClick?: React.ButtonHTMLAttributes<HTMLButtonElement>["onClick"]
maxWidth?: number
} }
const Tooltip = ({ const Tooltip = ({
@@ -19,6 +20,7 @@ const Tooltip = ({
defaultOpen, defaultOpen,
onOpenChange, onOpenChange,
delayDuration, delayDuration,
maxWidth = 220,
className, className,
side, side,
onClick, onClick,
@@ -43,10 +45,10 @@ const Tooltip = ({
"inter-small-semibold text-grey-50", "inter-small-semibold text-grey-50",
"bg-grey-0 shadow-dropdown rounded-rounded py-2 px-3", "bg-grey-0 shadow-dropdown rounded-rounded py-2 px-3",
"border-grey-20 border border-solid", "border-grey-20 border border-solid",
"max-w-[220px]",
className className
)} )}
{...props} {...props}
style={{ ...props.style, maxWidth }}
> >
{content} {content}
</RadixTooltip.Content> </RadixTooltip.Content>
@@ -1,13 +1,25 @@
import { ReactNode } from "react" import { ReactNode } from "react"
import clsx from "clsx"
import Tooltip from "../../atoms/tooltip"
type Props = { type Props = {
fileName: string fileName: string
fileSize?: string fileSize?: string
errorMessage?: string
hasError?: boolean
icon?: ReactNode icon?: ReactNode
onClick?: () => void onClick?: () => void
} }
const BatchJobFileCard = ({ fileName, fileSize, icon, onClick }: Props) => { const BatchJobFileCard = ({
fileName,
fileSize,
icon,
onClick,
hasError,
errorMessage,
}: Props) => {
const preparedOnClick = onClick ?? (() => void 0) const preparedOnClick = onClick ?? (() => void 0)
return ( return (
@@ -27,9 +39,26 @@ const BatchJobFileCard = ({ fileName, fileSize, icon, onClick }: Props) => {
{fileName} {fileName}
</div> </div>
{!!fileSize && ( <Tooltip
<div className="text-grey-40 inter-small-regular">{fileSize}</div> 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>
</div> </div>
) )
@@ -19,6 +19,7 @@ import MedusaIcon from "../../fundamentals/icons/medusa-icon"
import { ActivityCard } from "../../molecules/activity-card" import { ActivityCard } from "../../molecules/activity-card"
import BatchJobFileCard from "../../molecules/batch-job-file-card" import BatchJobFileCard from "../../molecules/batch-job-file-card"
import { batchJobDescriptionBuilder, BatchJobOperation } from "./utils" 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 * 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 !== "failed" &&
batchJob.status !== "canceled" batchJob.status !== "canceled"
const hasError = batchJob.status === "failed"
const canDownload = const canDownload =
batchJob.status === "completed" && batchJob.result?.file_key batchJob.status === "completed" && batchJob.result?.file_key
@@ -157,7 +160,11 @@ const BatchJobActivityCard = (props: { batchJob: BatchJob }) => {
const icon = const icon =
batchJob.status !== "completed" && batchJob.status !== "canceled" ? ( batchJob.status !== "completed" && batchJob.status !== "canceled" ? (
<Spinner size={"medium"} variant={"secondary"} /> batchJob.status === "failed" ? (
<CrossIcon size={18} />
) : (
<Spinner size={"medium"} variant={"secondary"} />
)
) : ( ) : (
<FileIcon <FileIcon
className={clsx({ className={clsx({
@@ -175,7 +182,7 @@ const BatchJobActivityCard = (props: { batchJob: BatchJob }) => {
preprocessing: `Preparing ${operation.toLowerCase()}...`, preprocessing: `Preparing ${operation.toLowerCase()}...`,
processing: `Processing ${operation.toLowerCase()}...`, processing: `Processing ${operation.toLowerCase()}...`,
completed: `Successful ${operation.toLowerCase()}`, completed: `Successful ${operation.toLowerCase()}`,
failed: `Failed batch ${operation.toLowerCase()} job`, failed: `Job failed`,
canceled: `Canceled batch ${operation.toLowerCase()} job`, canceled: `Canceled batch ${operation.toLowerCase()} job`,
}[batchJob.status] }[batchJob.status]
@@ -185,6 +192,8 @@ const BatchJobActivityCard = (props: { batchJob: BatchJob }) => {
fileName={fileName} fileName={fileName}
icon={icon} icon={icon}
fileSize={fileSize} fileSize={fileSize}
hasError={hasError}
errorMessage={batchJob?.result?.errors?.join(" \n")}
/> />
) )
} }
@@ -1,4 +1,5 @@
import { ReactNode, useState } from "react" import { ReactNode, useState } from "react"
import clsx from "clsx"
import Button from "../../fundamentals/button" import Button from "../../fundamentals/button"
import CheckCircleIcon from "../../fundamentals/icons/check-circle-icon" 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 Modal from "../../molecules/modal"
import TrashIcon from "../../fundamentals/icons/trash-icon" import TrashIcon from "../../fundamentals/icons/trash-icon"
import WarningCircleIcon from "../../fundamentals/icons/warning-circle" import WarningCircleIcon from "../../fundamentals/icons/warning-circle"
import XCircleIcon from "../../fundamentals/icons/x-circle-icon" import Tooltip from "../../atoms/tooltip"
import clsx from "clsx"
type FileSummaryProps = { type FileSummaryProps = {
name: string name: string
size: number size: number
hasError?: boolean
errorMessage?: string
action: ReactNode action: ReactNode
progress?: number progress?: number
status?: string status?: string
@@ -23,7 +25,7 @@ type FileSummaryProps = {
* Render an upload file summary (& upload progress). * Render an upload file summary (& upload progress).
*/ */
function FileSummary(props: FileSummaryProps) { function FileSummary(props: FileSummaryProps) {
const { action, name, progress, size, status } = props const { action, name, progress, size, status, hasError, errorMessage } = props
const formattedSize = const formattedSize =
size / 1024 < 10 size / 1024 < 10
@@ -32,32 +34,46 @@ function FileSummary(props: FileSummaryProps) {
return ( return (
<div className="relative"> <div className="relative">
<div <Tooltip
style={{ width: `${progress}%` }} side="top"
className="bg-grey-5 transition-width absolute h-full duration-150 ease-in-out" maxWidth={320}
/> open={hasError ? undefined : false}
<div className="border-1 relative mt-6 flex items-center rounded-xl border"> content={
<div className="m-4"> hasError && errorMessage ? (
<FileIcon size={30} fill={progress ? "#9CA3AF" : "#2DD4BF"} /> <span className="font-normal text-rose-500">{errorMessage}</span>
</div> ) : null
}
<div className="my-6 flex-1"> >
<div className="text-small text-grey-90 leading-5">{name}</div> <div
<div className="text-xsmall text-grey-50 leading-4"> style={{ width: `${progress}%` }}
{status || formattedSize} 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>
<div className="m-6">{action}</div> <div className="my-6 flex-1">
</div> <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> </div>
) )
} }
type UploadSummaryProps = { type UploadSummaryProps = {
creations: number creations?: number
updates: number updates?: number
rejections?: number
type: string type: string
} }
@@ -65,25 +81,18 @@ type UploadSummaryProps = {
* Render a batch update request summary. * Render a batch update request summary.
*/ */
function UploadSummary(props: UploadSummaryProps) { function UploadSummary(props: UploadSummaryProps) {
const { creations, updates, rejections, type } = props const { creations, updates, type } = props
return ( return (
<div className="flex gap-6"> <div className="flex gap-6">
<div className="text-small text-grey-90 flex items-center"> <div className="text-small text-grey-90 flex items-center">
<CheckCircleIcon color="#9CA3AF" className="mr-2" /> <CheckCircleIcon color="#9CA3AF" className="mr-2" />
<span className="font-semibold"> {creations}&nbsp;</span> new {type} <span className="font-semibold"> {creations || 0}&nbsp;</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}&nbsp;</span> updates
</div> </div>
{updates && (
<div className="text-small text-grey-90 flex items-center">
<WarningCircleIcon fill="#9CA3AF" className="mr-2" />
<span className="font-semibold">{updates}&nbsp;</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}&nbsp;</span> rejections
</div>
)}
</div> </div>
) )
} }
@@ -155,6 +164,8 @@ function DropArea(props: DropAreaProps) {
type UploadModalProps = { type UploadModalProps = {
type: string type: string
status?: string status?: string
hasError?: boolean
errorMessage?: string
fileTitle: string fileTitle: string
description1Text: string description1Text: string
description2Title: string description2Title: string
@@ -184,8 +195,9 @@ function UploadModal(props: UploadModalProps) {
onSubmit, onSubmit,
onFileRemove, onFileRemove,
templateLink, templateLink,
progress,
summary, summary,
hasError,
errorMessage,
status, status,
type, type,
} = props } = props
@@ -237,6 +249,8 @@ function UploadModal(props: UploadModalProps) {
size={size!} size={size!}
name={name!} name={name!}
status={status} status={status}
hasError={hasError}
errorMessage={errorMessage}
// progress={progress} // progress={progress}
// TODO: change this to actual progress once this we can track upload // TODO: change this to actual progress once this we can track upload
progress={100} progress={100}
@@ -284,7 +298,7 @@ function UploadModal(props: UploadModalProps) {
<Button <Button
size="small" size="small"
disabled={!canImport} disabled={!canImport || hasError}
variant="primary" variant="primary"
className="text-small" className="text-small"
onClick={onSubmit} onClick={onSubmit}
@@ -125,7 +125,7 @@ function ImportProducts(props: ImportProductsProps) {
return undefined 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) { if (!res) {
return undefined return undefined
@@ -182,6 +182,7 @@ function ImportProducts(props: ImportProductsProps) {
type="products" type="products"
status={status} status={status}
progress={progress} progress={progress}
hasError={hasError}
canImport={isPreprocessed} canImport={isPreprocessed}
onSubmit={onSubmit} onSubmit={onSubmit}
onClose={onClose} onClose={onClose}
@@ -190,6 +191,7 @@ function ImportProducts(props: ImportProductsProps) {
processUpload={processUpload} processUpload={processUpload}
fileTitle={"products list"} fileTitle={"products list"}
templateLink="/temp/product-import-template.csv" templateLink="/temp/product-import-template.csv"
errorMessage={batchJob?.result?.errors?.join(" \n")}
description2Title="Unsure about how to arrange your list?" description2Title="Unsure about how to arrange your list?"
description2Text="Download the template below to ensure you are following the correct format." 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." 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, refetch,
} = useAdminBatchJobs( } = useAdminBatchJobs(
{ {
limit: 100,
created_at: { gte: oneMonthAgo }, created_at: { gte: oneMonthAgo },
failed_at: null,
} as AdminGetBatchParams, } as AdminGetBatchParams,
{ {
refetchOnWindowFocus: true, refetchOnWindowFocus: true,