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:
9 files changed
+117
-47
No files matched your search
@@ -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
|
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} </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>
|
</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>
|
</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,
|
||||||
|
|||||||
Reference in new issue
Block a user