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:
Frane Polić
2023-03-21 16:13:39 +01:00
committed by GitHub
parent 7f2223b650
commit f831b7db37
9 changed files with 117 additions and 47 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/admin-ui": patch
---
fix(admin-ui): display error messages for batch jobs

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>
)

View File

@@ -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")}
/>
)
}

View File

@@ -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}&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>
{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>
)
}
@@ -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}

View File

@@ -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."

View File

@@ -56,8 +56,8 @@ export const PollingProvider = ({ children }: PropsWithChildren) => {
refetch,
} = useAdminBatchJobs(
{
limit: 100,
created_at: { gte: oneMonthAgo },
failed_at: null,
} as AdminGetBatchParams,
{
refetchOnWindowFocus: true,