chore(dashboard): Remove v1 code and medusa-react (#7420)
This commit is contained in:
committed by
GitHub
parent
9cbe0085d0
commit
e01472aae6
@@ -0,0 +1,26 @@
|
||||
import { TransactionState, TransactionStepState } from "./types"
|
||||
|
||||
export const STEP_IN_PROGRESS_STATES = [
|
||||
TransactionStepState.COMPENSATING,
|
||||
TransactionStepState.INVOKING,
|
||||
]
|
||||
export const STEP_OK_STATES = [TransactionStepState.DONE]
|
||||
export const STEP_ERROR_STATES = [
|
||||
TransactionStepState.FAILED,
|
||||
TransactionStepState.REVERTED,
|
||||
TransactionStepState.TIMEOUT,
|
||||
TransactionStepState.DORMANT,
|
||||
TransactionStepState.SKIPPED,
|
||||
]
|
||||
export const STEP_INACTIVE_STATES = [TransactionStepState.NOT_STARTED]
|
||||
|
||||
export const TRANSACTION_OK_STATES = [TransactionState.DONE]
|
||||
export const TRANSACTION_ERROR_STATES = [
|
||||
TransactionState.FAILED,
|
||||
TransactionState.REVERTED,
|
||||
]
|
||||
export const TRANSACTION_IN_PROGRESS_STATES = [
|
||||
TransactionState.INVOKING,
|
||||
TransactionState.WAITING_TO_COMPENSATE,
|
||||
TransactionState.COMPENSATING,
|
||||
]
|
||||
@@ -0,0 +1,95 @@
|
||||
export type WorkflowExecutionStep = {
|
||||
id: string
|
||||
invoke: {
|
||||
state: TransactionStepState
|
||||
status: TransactionStepStatus
|
||||
}
|
||||
definition: {
|
||||
async?: boolean
|
||||
compensateAsync?: boolean
|
||||
noCompensation?: boolean
|
||||
continueOnPermanentFailure?: boolean
|
||||
maxRetries?: number
|
||||
noWait?: boolean
|
||||
retryInterval?: number
|
||||
retryIntervalAwaiting?: number
|
||||
saveResponse?: boolean
|
||||
timeout?: number
|
||||
}
|
||||
compensate: {
|
||||
state: TransactionStepState
|
||||
status: TransactionStepStatus
|
||||
}
|
||||
depth: number
|
||||
startedAt: number
|
||||
}
|
||||
|
||||
export type StepInvoke = {
|
||||
output: {
|
||||
output: unknown
|
||||
compensateInput: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type StepError = {
|
||||
error: Record<string, unknown>
|
||||
action: string
|
||||
handlerType: string
|
||||
}
|
||||
|
||||
export type WorkflowExecutionContext = {
|
||||
data: {
|
||||
invoke: Record<string, StepInvoke>
|
||||
payload?: unknown
|
||||
}
|
||||
compensate: Record<string, unknown>
|
||||
errors: StepError[]
|
||||
}
|
||||
|
||||
type WorflowExecutionExecution = {
|
||||
steps: Record<string, WorkflowExecutionStep>
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-implements WorkflowExecutionDTO as it is currently only exported from `@medusajs/workflows-sdk`.
|
||||
* Also adds type definitions for fields that have vague types, such as `execution` and `context`.
|
||||
*/
|
||||
export interface WorkflowExecutionDTO {
|
||||
id: string
|
||||
workflow_id: string
|
||||
transaction_id: string
|
||||
execution: WorflowExecutionExecution | null
|
||||
context: WorkflowExecutionContext | null
|
||||
state: any
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
deleted_at: Date
|
||||
}
|
||||
|
||||
export enum TransactionStepStatus {
|
||||
IDLE = "idle",
|
||||
OK = "ok",
|
||||
WAITING = "waiting_response",
|
||||
TEMPORARY_FAILURE = "temp_failure",
|
||||
PERMANENT_FAILURE = "permanent_failure",
|
||||
}
|
||||
export enum TransactionState {
|
||||
NOT_STARTED = "not_started",
|
||||
INVOKING = "invoking",
|
||||
WAITING_TO_COMPENSATE = "waiting_to_compensate",
|
||||
COMPENSATING = "compensating",
|
||||
DONE = "done",
|
||||
REVERTED = "reverted",
|
||||
FAILED = "failed",
|
||||
}
|
||||
export enum TransactionStepState {
|
||||
NOT_STARTED = "not_started",
|
||||
INVOKING = "invoking",
|
||||
COMPENSATING = "compensating",
|
||||
DONE = "done",
|
||||
REVERTED = "reverted",
|
||||
FAILED = "failed",
|
||||
DORMANT = "dormant",
|
||||
SKIPPED = "skipped",
|
||||
TIMEOUT = "timeout",
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { AdminGetWorkflowExecutionsParams } from "@medusajs/medusa"
|
||||
import { TFunction } from "i18next"
|
||||
import {
|
||||
STEP_ERROR_STATES,
|
||||
STEP_INACTIVE_STATES,
|
||||
STEP_IN_PROGRESS_STATES,
|
||||
TRANSACTION_ERROR_STATES,
|
||||
TRANSACTION_IN_PROGRESS_STATES,
|
||||
} from "./constants"
|
||||
import { TransactionState, TransactionStepState } from "./types"
|
||||
|
||||
export const adminExecutionKey = {
|
||||
detail: (id: string) => ["workflow_executions", "detail", id],
|
||||
list: (query?: AdminGetWorkflowExecutionsParams) => [
|
||||
"workflow_executions",
|
||||
"list",
|
||||
{ query },
|
||||
],
|
||||
}
|
||||
|
||||
export const getTransactionStateColor = (
|
||||
state: TransactionState
|
||||
): "green" | "orange" | "red" => {
|
||||
let statusColor: "green" | "red" | "orange" = "green"
|
||||
|
||||
if (TRANSACTION_ERROR_STATES.includes(state)) {
|
||||
statusColor = "red"
|
||||
}
|
||||
|
||||
if (TRANSACTION_IN_PROGRESS_STATES.includes(state)) {
|
||||
statusColor = "orange"
|
||||
}
|
||||
|
||||
return statusColor
|
||||
}
|
||||
|
||||
export const getTransactionState = (
|
||||
t: TFunction<"translation", any>,
|
||||
state: TransactionState
|
||||
) => {
|
||||
switch (state) {
|
||||
case TransactionState.DONE:
|
||||
return t("workflowExecutions.state.done")
|
||||
case TransactionState.FAILED:
|
||||
return t("workflowExecutions.state.failed")
|
||||
case TransactionState.REVERTED:
|
||||
return t("workflowExecutions.state.reverted")
|
||||
case TransactionState.INVOKING:
|
||||
return t("workflowExecutions.state.invoking")
|
||||
case TransactionState.WAITING_TO_COMPENSATE:
|
||||
return t("workflowExecutions.transaction.state.waitingToCompensate")
|
||||
case TransactionState.COMPENSATING:
|
||||
return t("workflowExecutions.state.compensating")
|
||||
case TransactionState.NOT_STARTED:
|
||||
return t("workflowExecutions.state.notStarted")
|
||||
}
|
||||
}
|
||||
|
||||
export const getStepStateColor = (state: TransactionStepState) => {
|
||||
let statusColor: "green" | "red" | "orange" | "grey" = "green"
|
||||
|
||||
if (STEP_ERROR_STATES.includes(state)) {
|
||||
statusColor = "red"
|
||||
}
|
||||
|
||||
if (STEP_INACTIVE_STATES.includes(state)) {
|
||||
statusColor = "grey"
|
||||
}
|
||||
|
||||
if (STEP_IN_PROGRESS_STATES.includes(state)) {
|
||||
statusColor = "orange"
|
||||
}
|
||||
|
||||
return statusColor
|
||||
}
|
||||
|
||||
export const getStepState = (
|
||||
t: TFunction<"translation", any>,
|
||||
state: TransactionStepState
|
||||
) => {
|
||||
switch (state) {
|
||||
case TransactionStepState.DONE:
|
||||
return t("workflowExecutions.state.done")
|
||||
case TransactionStepState.FAILED:
|
||||
return t("workflowExecutions.state.failed")
|
||||
case TransactionStepState.REVERTED:
|
||||
return t("workflowExecutions.state.reverted")
|
||||
case TransactionStepState.INVOKING:
|
||||
return t("workflowExecutions.state.invoking")
|
||||
case TransactionStepState.COMPENSATING:
|
||||
return t("workflowExecutions.state.compensating")
|
||||
case TransactionStepState.NOT_STARTED:
|
||||
return t("workflowExecutions.state.notStarted")
|
||||
case TransactionStepState.SKIPPED:
|
||||
return t("workflowExecutions.step.state.skipped")
|
||||
case TransactionStepState.DORMANT:
|
||||
return t("workflowExecutions.step.state.dormant")
|
||||
case TransactionStepState.TIMEOUT:
|
||||
return t("workflowExecutions.step.state.timeout")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./workflow-execution-general-section"
|
||||
@@ -0,0 +1,118 @@
|
||||
import {
|
||||
Badge,
|
||||
Container,
|
||||
Copy,
|
||||
Heading,
|
||||
StatusBadge,
|
||||
Text,
|
||||
clx,
|
||||
} from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import {
|
||||
TransactionStepState,
|
||||
WorkflowExecutionDTO,
|
||||
WorkflowExecutionStep,
|
||||
} from "../../../types"
|
||||
import { getTransactionState, getTransactionStateColor } from "../../../utils"
|
||||
|
||||
type WorkflowExecutionGeneralSectionProps = {
|
||||
execution: WorkflowExecutionDTO
|
||||
}
|
||||
|
||||
export const WorkflowExecutionGeneralSection = ({
|
||||
execution,
|
||||
}: WorkflowExecutionGeneralSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const cleanId = execution.id.replace("wf_exec_", "")
|
||||
const translatedState = getTransactionState(t, execution.state)
|
||||
const stateColor = getTransactionStateColor(execution.state)
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<div className="flex items-center gap-x-0.5">
|
||||
<Heading>{cleanId}</Heading>
|
||||
<Copy content={cleanId} className="text-ui-fg-muted" />
|
||||
</div>
|
||||
<StatusBadge color={stateColor}>{translatedState}</StatusBadge>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("workflowExecutions.workflowIdLabel")}
|
||||
</Text>
|
||||
<Badge size="2xsmall" className="w-fit">
|
||||
{execution.workflow_id}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("workflowExecutions.transactionIdLabel")}
|
||||
</Text>
|
||||
<Badge size="2xsmall" className="w-fit">
|
||||
{execution.transaction_id}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("workflowExecutions.progressLabel")}
|
||||
</Text>
|
||||
<Progress steps={execution.execution?.steps} />
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const ROOT_PREFIX = "_root"
|
||||
|
||||
const Progress = ({
|
||||
steps,
|
||||
}: {
|
||||
steps?: Record<string, WorkflowExecutionStep> | null
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!steps) {
|
||||
return (
|
||||
<Text size="small" leading="compact" className="text-ui-fg-subtle">
|
||||
{t("workflowExecutions.stepsCompletedLabel", {
|
||||
completed: 0,
|
||||
total: 0,
|
||||
})}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
const actionableSteps = Object.values(steps).filter(
|
||||
(step) => step.id !== ROOT_PREFIX
|
||||
)
|
||||
|
||||
const completedSteps = actionableSteps.filter(
|
||||
(step) => step.invoke.state === TransactionStepState.DONE
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex w-fit items-center gap-x-2">
|
||||
<div className="flex items-center gap-x-[3px]">
|
||||
{actionableSteps.map((step) => (
|
||||
<div
|
||||
key={step.id}
|
||||
className={clx(
|
||||
"bg-ui-bg-switch-off shadow-details-switch-background h-3 w-1.5 rounded-full",
|
||||
{
|
||||
"bg-ui-fg-muted":
|
||||
step.invoke.state === TransactionStepState.DONE,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Text size="small" leading="compact" className="text-ui-fg-subtle">
|
||||
{t("workflowExecutions.stepsCompletedLabel", {
|
||||
completed: completedSteps.length,
|
||||
count: actionableSteps.length,
|
||||
})}
|
||||
</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./workflow-execution-history-section"
|
||||
@@ -0,0 +1,324 @@
|
||||
import { Spinner, TriangleDownMini } from "@medusajs/icons"
|
||||
import {
|
||||
CodeBlock,
|
||||
Container,
|
||||
Heading,
|
||||
IconButton,
|
||||
Text,
|
||||
clx,
|
||||
} from "@medusajs/ui"
|
||||
import * as Collapsible from "@radix-ui/react-collapsible"
|
||||
import { format } from "date-fns"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useLocation } from "react-router-dom"
|
||||
import {
|
||||
STEP_ERROR_STATES,
|
||||
STEP_INACTIVE_STATES,
|
||||
STEP_IN_PROGRESS_STATES,
|
||||
STEP_OK_STATES,
|
||||
} from "../../../constants"
|
||||
import {
|
||||
StepError,
|
||||
StepInvoke,
|
||||
TransactionStepState,
|
||||
TransactionStepStatus,
|
||||
WorkflowExecutionDTO,
|
||||
WorkflowExecutionStep,
|
||||
} from "../../../types"
|
||||
|
||||
type WorkflowExecutionHistorySectionProps = {
|
||||
execution: WorkflowExecutionDTO
|
||||
}
|
||||
|
||||
export const WorkflowExecutionHistorySection = ({
|
||||
execution,
|
||||
}: WorkflowExecutionHistorySectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const map = Object.values(execution.execution?.steps || {})
|
||||
const steps = map.filter((step) => step.id !== "_root")
|
||||
|
||||
// check if any of the steps have a .invoke.state of "permanent_failure" and if that is the case then return its id
|
||||
const unreachableStepId = steps.find(
|
||||
(step) => step.invoke.status === TransactionStepStatus.PERMANENT_FAILURE
|
||||
)?.id
|
||||
|
||||
// return an array of step ids of all steps that come after the unreachable step if there is one
|
||||
const unreachableSteps = unreachableStepId
|
||||
? steps
|
||||
.filter(
|
||||
(step) =>
|
||||
step.id !== unreachableStepId && step.id.includes(unreachableStepId)
|
||||
)
|
||||
.map((step) => step.id)
|
||||
: []
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">
|
||||
{t("workflowExecutions.history.sectionTitle")}
|
||||
</Heading>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-0.5 px-6 py-4">
|
||||
{steps.map((step, index) => {
|
||||
const stepId = step.id.split(".").pop()
|
||||
|
||||
if (!stepId) {
|
||||
return null
|
||||
}
|
||||
|
||||
const context = execution.context?.data.invoke[stepId]
|
||||
const error = execution.context?.errors.find(
|
||||
(e) => e.action === stepId
|
||||
)
|
||||
|
||||
return (
|
||||
<Event
|
||||
key={step.id}
|
||||
step={step}
|
||||
stepInvokeContext={context}
|
||||
stepError={error}
|
||||
isLast={index === steps.length - 1}
|
||||
isUnreachable={unreachableSteps.includes(step.id)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Event = ({
|
||||
step,
|
||||
stepInvokeContext,
|
||||
stepError,
|
||||
isLast,
|
||||
isUnreachable,
|
||||
}: {
|
||||
step: WorkflowExecutionStep
|
||||
stepInvokeContext: StepInvoke | undefined
|
||||
stepError?: StepError | undefined
|
||||
isLast: boolean
|
||||
isUnreachable?: boolean
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const { hash } = useLocation()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const stepId = step.id.split(".").pop()!
|
||||
|
||||
useEffect(() => {
|
||||
if (hash === `#${stepId}`) {
|
||||
setOpen(true)
|
||||
}
|
||||
}, [hash, stepId])
|
||||
|
||||
const identifier = step.id.split(".").pop()
|
||||
|
||||
stepInvokeContext
|
||||
|
||||
return (
|
||||
<div
|
||||
className="grid grid-cols-[20px_1fr] items-start gap-x-2 px-2"
|
||||
id={stepId}
|
||||
>
|
||||
<div className="grid h-full grid-rows-[20px_1fr] items-center justify-center gap-y-0.5">
|
||||
<div className="flex size-5 items-center justify-center">
|
||||
<div className="bg-ui-bg-base shadow-borders-base flex size-2.5 items-center justify-center rounded-full">
|
||||
<div
|
||||
className={clx("size-1.5 rounded-full", {
|
||||
"bg-ui-tag-green-icon": STEP_OK_STATES.includes(
|
||||
step.invoke.state
|
||||
),
|
||||
"bg-ui-tag-orange-icon": STEP_IN_PROGRESS_STATES.includes(
|
||||
step.invoke.state
|
||||
),
|
||||
"bg-ui-tag-red-icon": STEP_ERROR_STATES.includes(
|
||||
step.invoke.state
|
||||
),
|
||||
"bg-ui-tag-neutral-icon": STEP_INACTIVE_STATES.includes(
|
||||
step.invoke.state
|
||||
),
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full flex-col items-center">
|
||||
<div
|
||||
aria-hidden
|
||||
role="presentation"
|
||||
className={clx({
|
||||
"bg-ui-border-base h-full min-h-[14px] w-px": !isLast,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
||||
<Collapsible.Trigger asChild>
|
||||
<div className="group flex cursor-pointer items-start justify-between outline-none">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{identifier}
|
||||
</Text>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<StepState
|
||||
state={step.invoke.state}
|
||||
startedAt={step.startedAt}
|
||||
isUnreachable={isUnreachable}
|
||||
/>
|
||||
<IconButton size="2xsmall" variant="transparent">
|
||||
<TriangleDownMini className="text-ui-fg-muted transition-transform group-data-[state=open]:rotate-180" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content ref={ref}>
|
||||
<div className="flex flex-col gap-y-2 pb-4 pt-2">
|
||||
<div className="text-ui-fg-subtle flex flex-col gap-y-2">
|
||||
<Text size="small" leading="compact">
|
||||
{t("workflowExecutions.history.definitionLabel")}
|
||||
</Text>
|
||||
<CodeBlock
|
||||
snippets={[
|
||||
{
|
||||
code: JSON.stringify(step.definition, null, 2),
|
||||
label: t("workflowExecutions.history.definitionLabel"),
|
||||
language: "json",
|
||||
hideLineNumbers: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CodeBlock.Body />
|
||||
</CodeBlock>
|
||||
</div>
|
||||
{stepInvokeContext && (
|
||||
<div className="text-ui-fg-subtle flex flex-col gap-y-2">
|
||||
<Text size="small" leading="compact">
|
||||
{t("workflowExecutions.history.outputLabel")}
|
||||
</Text>
|
||||
<CodeBlock
|
||||
snippets={[
|
||||
{
|
||||
code: JSON.stringify(
|
||||
stepInvokeContext.output.output,
|
||||
null,
|
||||
2
|
||||
),
|
||||
label: t("workflowExecutions.history.outputLabel"),
|
||||
language: "json",
|
||||
hideLineNumbers: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CodeBlock.Body />
|
||||
</CodeBlock>
|
||||
</div>
|
||||
)}
|
||||
{!!stepInvokeContext?.output.compensateInput &&
|
||||
step.compensate.state === TransactionStepState.REVERTED && (
|
||||
<div className="text-ui-fg-subtle flex flex-col gap-y-2">
|
||||
<Text size="small" leading="compact">
|
||||
{t("workflowExecutions.history.compensateInputLabel")}
|
||||
</Text>
|
||||
<CodeBlock
|
||||
snippets={[
|
||||
{
|
||||
code: JSON.stringify(
|
||||
stepInvokeContext.output.compensateInput,
|
||||
null,
|
||||
2
|
||||
),
|
||||
label: t(
|
||||
"workflowExecutions.history.compensateInputLabel"
|
||||
),
|
||||
language: "json",
|
||||
hideLineNumbers: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CodeBlock.Body />
|
||||
</CodeBlock>
|
||||
</div>
|
||||
)}
|
||||
{stepError && (
|
||||
<div className="text-ui-fg-subtle flex flex-col gap-y-2">
|
||||
<Text size="small" leading="compact">
|
||||
{t("workflowExecutions.history.errorLabel")}
|
||||
</Text>
|
||||
<CodeBlock
|
||||
snippets={[
|
||||
{
|
||||
code: JSON.stringify(
|
||||
{
|
||||
error: stepError.error,
|
||||
handlerType: stepError.handlerType,
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
label: t("workflowExecutions.history.errorLabel"),
|
||||
language: "json",
|
||||
hideLineNumbers: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CodeBlock.Body />
|
||||
</CodeBlock>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const StepState = ({
|
||||
state,
|
||||
startedAt,
|
||||
isUnreachable,
|
||||
}: {
|
||||
state: TransactionStepState
|
||||
startedAt?: number | null
|
||||
isUnreachable?: boolean
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const isFailed = state === TransactionStepState.FAILED
|
||||
const isRunning = state === TransactionStepState.INVOKING
|
||||
|
||||
if (isUnreachable) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
return (
|
||||
<div className="flex items-center gap-x-1">
|
||||
<Text size="small" leading="compact" className="text-ui-fg-subtle">
|
||||
{t("workflowExecutions.history.runningState")}
|
||||
</Text>
|
||||
<Spinner className="text-ui-fg-interactive animate-spin" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isFailed) {
|
||||
return (
|
||||
<Text size="small" leading="compact" className="text-ui-fg-subtle">
|
||||
{t("workflowExecutions.history.failedState")}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
if (startedAt) {
|
||||
return (
|
||||
<Text size="small" leading="compact" className="text-ui-fg-muted">
|
||||
{format(startedAt, "dd MMM yyyy HH:mm:ss")}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./workflow-execution-payload-section"
|
||||
@@ -0,0 +1,24 @@
|
||||
import { JsonViewSection } from "../../../../../components/common/json-view-section"
|
||||
import { WorkflowExecutionDTO } from "../../../types"
|
||||
|
||||
type WorkflowExecutionPayloadSectionProps = {
|
||||
execution: WorkflowExecutionDTO
|
||||
}
|
||||
|
||||
export const WorkflowExecutionPayloadSection = ({
|
||||
execution,
|
||||
}: WorkflowExecutionPayloadSectionProps) => {
|
||||
let payload = execution.context?.data?.payload
|
||||
|
||||
if (!payload) {
|
||||
return null
|
||||
}
|
||||
|
||||
// payloads may be a primitive, so we need to wrap them in an object
|
||||
// to ensure the JsonViewSection component can render them.
|
||||
if (typeof payload !== "object") {
|
||||
payload = { input: payload }
|
||||
}
|
||||
|
||||
return <JsonViewSection title="Payload" data={payload as object} />
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./workflow-execution-timeline-section"
|
||||
@@ -0,0 +1,435 @@
|
||||
import { ArrowPathMini, MinusMini, PlusMini } from "@medusajs/icons"
|
||||
import { Container, DropdownMenu, Heading, Text, clx } from "@medusajs/ui"
|
||||
import {
|
||||
motion,
|
||||
useAnimationControls,
|
||||
useDragControls,
|
||||
useMotionValue,
|
||||
} from "framer-motion"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
import {
|
||||
STEP_ERROR_STATES,
|
||||
STEP_INACTIVE_STATES,
|
||||
STEP_IN_PROGRESS_STATES,
|
||||
STEP_OK_STATES,
|
||||
} from "../../../constants"
|
||||
import { WorkflowExecutionDTO, WorkflowExecutionStep } from "../../../types"
|
||||
|
||||
type WorkflowExecutionTimelineSectionProps = {
|
||||
execution: WorkflowExecutionDTO
|
||||
}
|
||||
|
||||
export const WorkflowExecutionTimelineSection = ({
|
||||
execution,
|
||||
}: WorkflowExecutionTimelineSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Container className="overflow-hidden px-0 pb-8 pt-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">{t("general.timeline")}</Heading>
|
||||
</div>
|
||||
<div className="w-full overflow-hidden border-y">
|
||||
<Canvas execution={execution} />
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const createNodeClusters = (steps: Record<string, WorkflowExecutionStep>) => {
|
||||
const actionableSteps = Object.values(steps).filter(
|
||||
(step) => step.id !== "_root"
|
||||
)
|
||||
|
||||
const clusters: Record<number, WorkflowExecutionStep[]> = {}
|
||||
|
||||
actionableSteps.forEach((step) => {
|
||||
if (!clusters[step.depth]) {
|
||||
clusters[step.depth] = []
|
||||
}
|
||||
|
||||
clusters[step.depth].push(step)
|
||||
})
|
||||
|
||||
return clusters
|
||||
}
|
||||
|
||||
const getNextCluster = (
|
||||
clusters: Record<number, WorkflowExecutionStep[]>,
|
||||
depth: number
|
||||
) => {
|
||||
const nextDepth = depth + 1
|
||||
return clusters[nextDepth]
|
||||
}
|
||||
|
||||
type ZoomScale = 0.5 | 0.75 | 1
|
||||
|
||||
const defaultState = {
|
||||
x: -860,
|
||||
y: -1020,
|
||||
scale: 1,
|
||||
}
|
||||
|
||||
const MAX_ZOOM = 1.5
|
||||
const MIN_ZOOM = 0.5
|
||||
const ZOOM_STEP = 0.25
|
||||
|
||||
const Canvas = ({ execution }: { execution: WorkflowExecutionDTO }) => {
|
||||
const [zoom, setZoom] = useState<number>(1)
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
|
||||
const scale = useMotionValue(defaultState.scale)
|
||||
const x = useMotionValue(defaultState.x)
|
||||
const y = useMotionValue(defaultState.y)
|
||||
|
||||
const controls = useAnimationControls()
|
||||
|
||||
const dragControls = useDragControls()
|
||||
const dragConstraints = useRef<HTMLDivElement>(null)
|
||||
|
||||
const canZoomIn = zoom < MAX_ZOOM
|
||||
const canZoomOut = zoom > MIN_ZOOM
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = scale.on("change", (latest) => {
|
||||
setZoom(latest as ZoomScale)
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubscribe()
|
||||
}
|
||||
}, [scale])
|
||||
|
||||
const clusters = createNodeClusters(execution.execution?.steps || {})
|
||||
|
||||
function scaleXandY(
|
||||
prevScale: number,
|
||||
newScale: number,
|
||||
x: number,
|
||||
y: number
|
||||
) {
|
||||
const scaleRatio = newScale / prevScale
|
||||
return {
|
||||
x: x * scaleRatio,
|
||||
y: y * scaleRatio,
|
||||
}
|
||||
}
|
||||
|
||||
const changeZoom = (newScale: number) => {
|
||||
const { x: newX, y: newY } = scaleXandY(zoom, newScale, x.get(), y.get())
|
||||
|
||||
setZoom(newScale)
|
||||
controls.set({ scale: newScale, x: newX, y: newY })
|
||||
}
|
||||
|
||||
const zoomIn = () => {
|
||||
const curr = scale.get()
|
||||
|
||||
if (curr < 1.5) {
|
||||
const newScale = curr + ZOOM_STEP
|
||||
changeZoom(newScale)
|
||||
}
|
||||
}
|
||||
|
||||
const zoomOut = () => {
|
||||
const curr = scale.get()
|
||||
|
||||
if (curr > 0.5) {
|
||||
const newScale = curr - ZOOM_STEP
|
||||
changeZoom(newScale)
|
||||
}
|
||||
}
|
||||
|
||||
const resetCanvas = () => {
|
||||
controls.start(defaultState)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-[400px] w-full">
|
||||
<div ref={dragConstraints} className="relative size-full">
|
||||
<div className="relative size-full overflow-hidden object-contain">
|
||||
<div>
|
||||
<motion.div
|
||||
onMouseDown={() => setIsDragging(true)}
|
||||
onMouseUp={() => setIsDragging(false)}
|
||||
drag
|
||||
dragConstraints={dragConstraints}
|
||||
dragElastic={0}
|
||||
dragMomentum={false}
|
||||
dragControls={dragControls}
|
||||
initial={false}
|
||||
animate={controls}
|
||||
transition={{ duration: 0.25 }}
|
||||
style={{
|
||||
x,
|
||||
y,
|
||||
scale,
|
||||
}}
|
||||
className={clx(
|
||||
"bg-ui-bg-subtle relative size-[500rem] origin-top-left items-start justify-start overflow-hidden",
|
||||
"bg-[radial-gradient(var(--border-base)_1.5px,transparent_0)] bg-[length:20px_20px] bg-repeat",
|
||||
{
|
||||
"cursor-grab": !isDragging,
|
||||
"cursor-grabbing": isDragging,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<main className="size-full">
|
||||
<div className="absolute left-[1100px] top-[1100px] flex select-none items-start">
|
||||
{Object.entries(clusters).map(([depth, cluster]) => {
|
||||
const next = getNextCluster(clusters, Number(depth))
|
||||
|
||||
return (
|
||||
<div key={depth} className="flex items-start">
|
||||
<div className="flex flex-col justify-center gap-y-2">
|
||||
{cluster.map((step) => (
|
||||
<Node key={step.id} step={step} />
|
||||
))}
|
||||
</div>
|
||||
<Line next={next} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</main>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-ui-bg-base shadow-borders-base text-ui-fg-subtle absolute bottom-4 left-6 flex h-7 items-center overflow-hidden rounded-md">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={zoomIn}
|
||||
type="button"
|
||||
disabled={!canZoomIn}
|
||||
aria-label="Zoom in"
|
||||
className="disabled:text-ui-fg-disabled transition-fg hover:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed focus-visible:bg-ui-bg-base-pressed border-r p-1 outline-none"
|
||||
>
|
||||
<PlusMini />
|
||||
</button>
|
||||
<div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger className="disabled:text-ui-fg-disabled transition-fg hover:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed focus-visible:bg-ui-bg-base-pressed flex w-[50px] items-center justify-center border-r p-1 outline-none">
|
||||
<Text
|
||||
as="span"
|
||||
size="xsmall"
|
||||
leading="compact"
|
||||
className="select-none tabular-nums"
|
||||
>
|
||||
{Math.round(zoom * 100)}%
|
||||
</Text>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
{[50, 75, 100, 125, 150].map((value) => (
|
||||
<DropdownMenu.Item
|
||||
key={value}
|
||||
onClick={() => changeZoom(value / 100)}
|
||||
>
|
||||
{value}%
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<button
|
||||
onClick={zoomOut}
|
||||
type="button"
|
||||
disabled={!canZoomOut}
|
||||
aria-label="Zoom out"
|
||||
className="disabled:text-ui-fg-disabled transition-fg hover:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed focus-visible:bg-ui-bg-base-pressed border-r p-1 outline-none"
|
||||
>
|
||||
<MinusMini />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={resetCanvas}
|
||||
type="button"
|
||||
aria-label="Reset canvas"
|
||||
className="disabled:text-ui-fg-disabled transition-fg hover:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed focus-visible:bg-ui-bg-base-pressed p-1 outline-none"
|
||||
>
|
||||
<ArrowPathMini />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const HorizontalArrow = () => {
|
||||
return (
|
||||
<svg
|
||||
width="42"
|
||||
height="12"
|
||||
viewBox="0 0 42 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M41.5303 6.53033C41.8232 6.23744 41.8232 5.76256 41.5303 5.46967L36.7574 0.696699C36.4645 0.403806 35.9896 0.403806 35.6967 0.696699C35.4038 0.989593 35.4038 1.46447 35.6967 1.75736L39.9393 6L35.6967 10.2426C35.4038 10.5355 35.4038 11.0104 35.6967 11.3033C35.9896 11.5962 36.4645 11.5962 36.7574 11.3033L41.5303 6.53033ZM0.999996 5.25C0.585785 5.25 0.249996 5.58579 0.249996 6C0.249996 6.41421 0.585785 6.75 0.999996 6.75V5.25ZM41 5.25L0.999996 5.25V6.75L41 6.75V5.25Z"
|
||||
fill="var(--border-strong)"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const MiddleArrow = () => {
|
||||
return (
|
||||
<svg
|
||||
width="22"
|
||||
height="38"
|
||||
viewBox="0 0 22 38"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="-mt-[6px]"
|
||||
>
|
||||
<path
|
||||
d="M0.999878 32H0.249878V32.75H0.999878V32ZM21.5284 32.5303C21.8213 32.2374 21.8213 31.7626 21.5284 31.4697L16.7554 26.6967C16.4625 26.4038 15.9876 26.4038 15.6947 26.6967C15.4019 26.9896 15.4019 27.4645 15.6947 27.7574L19.9374 32L15.6947 36.2426C15.4019 36.5355 15.4019 37.0104 15.6947 37.3033C15.9876 37.5962 16.4625 37.5962 16.7554 37.3033L21.5284 32.5303ZM0.249878 0L0.249878 32H1.74988L1.74988 0H0.249878ZM0.999878 32.75L20.998 32.75V31.25L0.999878 31.25V32.75Z"
|
||||
fill="var(--border-strong)"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const EndArrow = () => {
|
||||
return (
|
||||
<svg
|
||||
width="22"
|
||||
height="38"
|
||||
viewBox="0 0 22 38"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="-mt-[6px]"
|
||||
>
|
||||
<path
|
||||
d="M21.5284 32.5303C21.8213 32.2374 21.8213 31.7626 21.5284 31.4697L16.7554 26.6967C16.4625 26.4038 15.9876 26.4038 15.6947 26.6967C15.4019 26.9896 15.4019 27.4645 15.6947 27.7574L19.9374 32L15.6947 36.2426C15.4019 36.5355 15.4019 37.0104 15.6947 37.3033C15.9876 37.5962 16.4625 37.5962 16.7554 37.3033L21.5284 32.5303ZM0.249878 0L0.249878 28H1.74988L1.74988 0H0.249878ZM4.99988 32.75L20.998 32.75V31.25L4.99988 31.25V32.75ZM0.249878 28C0.249878 30.6234 2.37653 32.75 4.99988 32.75V31.25C3.20495 31.25 1.74988 29.7949 1.74988 28H0.249878Z"
|
||||
fill="var(--border-strong)"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const Arrow = ({ depth }: { depth: number }) => {
|
||||
if (depth === 1) {
|
||||
return <HorizontalArrow />
|
||||
}
|
||||
|
||||
if (depth === 2) {
|
||||
return (
|
||||
<div className="flex flex-col items-end">
|
||||
<HorizontalArrow />
|
||||
<EndArrow />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const inbetween = Array.from({ length: depth - 2 }).map((_, index) => (
|
||||
<MiddleArrow key={index} />
|
||||
))
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-end">
|
||||
<HorizontalArrow />
|
||||
{inbetween}
|
||||
<EndArrow />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Line = ({ next }: { next?: WorkflowExecutionStep[] }) => {
|
||||
if (!next) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="-ml-[5px] -mr-[7px] w-[60px] pr-[7px]">
|
||||
<div className="flex min-h-[24px] w-full items-start">
|
||||
<div className="flex h-6 w-2.5 items-center justify-center">
|
||||
<div className="bg-ui-button-neutral shadow-borders-base size-2.5 shrink-0 rounded-full" />
|
||||
</div>
|
||||
<div className="pt-1.5">
|
||||
<Arrow depth={next.length} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Node = ({ step }: { step: WorkflowExecutionStep }) => {
|
||||
if (step.id === "_root") {
|
||||
return null
|
||||
}
|
||||
|
||||
const stepId = step.id.split(".").pop()
|
||||
|
||||
/**
|
||||
* We can't rely on the built-in hash scrolling because the collapsible,
|
||||
* so we instead need to manually scroll to the step when the hash changes
|
||||
*/
|
||||
const handleScrollTo = () => {
|
||||
if (!stepId) {
|
||||
return
|
||||
}
|
||||
|
||||
const historyItem = document.getElementById(stepId)
|
||||
|
||||
if (!historyItem) {
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to the step if it's the one we're looking for but
|
||||
* we need to wait for the collapsible to open before scrolling
|
||||
*/
|
||||
setTimeout(() => {
|
||||
historyItem.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "end",
|
||||
})
|
||||
}, 100)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={`#${stepId}`}
|
||||
onClick={handleScrollTo}
|
||||
className="focus-visible:shadow-borders-focus transition-fg rounded-md outline-none"
|
||||
>
|
||||
<div
|
||||
className="bg-ui-bg-base shadow-borders-base flex min-w-[120px] items-center gap-x-0.5 rounded-md p-0.5"
|
||||
data-step-id={step.id}
|
||||
>
|
||||
<div className="flex size-5 items-center justify-center">
|
||||
<div
|
||||
className={clx(
|
||||
"size-2 rounded-sm shadow-[inset_0_0_0_1px_rgba(0,0,0,0.12)]",
|
||||
{
|
||||
"bg-ui-tag-green-icon": STEP_OK_STATES.includes(
|
||||
step.invoke.state
|
||||
),
|
||||
"bg-ui-tag-orange-icon": STEP_IN_PROGRESS_STATES.includes(
|
||||
step.invoke.state
|
||||
),
|
||||
"bg-ui-tag-red-icon": STEP_ERROR_STATES.includes(
|
||||
step.invoke.state
|
||||
),
|
||||
"bg-ui-tag-neutral-icon": STEP_INACTIVE_STATES.includes(
|
||||
step.invoke.state
|
||||
),
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Text
|
||||
size="xsmall"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
className="select-none"
|
||||
>
|
||||
{stepId}
|
||||
</Text>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ExecutionDetail as Component } from "./workflow-detail"
|
||||
@@ -0,0 +1,21 @@
|
||||
import { LoaderFunctionArgs } from "react-router-dom"
|
||||
|
||||
import { workflowExecutionsQueryKeys } from "../../../hooks/api/workflow-executions"
|
||||
import { client } from "../../../lib/client"
|
||||
import { queryClient } from "../../../lib/query-client"
|
||||
import { WorkflowExecutionRes } from "../../../types/api-responses"
|
||||
|
||||
const executionDetailQuery = (id: string) => ({
|
||||
queryKey: workflowExecutionsQueryKeys.detail(id),
|
||||
queryFn: async () => client.workflowExecutions.retrieve(id),
|
||||
})
|
||||
|
||||
export const executionLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||
const id = params.id
|
||||
const query = executionDetailQuery(id!)
|
||||
|
||||
return (
|
||||
queryClient.getQueryData<WorkflowExecutionRes>(query.queryKey) ??
|
||||
(await queryClient.fetchQuery(query))
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useParams } from "react-router-dom"
|
||||
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { useWorkflowExecution } from "../../../hooks/api/workflow-executions"
|
||||
import { WorkflowExecutionGeneralSection } from "./components/workflow-execution-general-section"
|
||||
import { WorkflowExecutionHistorySection } from "./components/workflow-execution-history-section"
|
||||
import { WorkflowExecutionPayloadSection } from "./components/workflow-execution-payload-section"
|
||||
import { WorkflowExecutionTimelineSection } from "./components/workflow-execution-timeline-section"
|
||||
|
||||
export const ExecutionDetail = () => {
|
||||
const { id } = useParams()
|
||||
|
||||
const { workflow_execution, isLoading, isError, error } =
|
||||
useWorkflowExecution(id!)
|
||||
|
||||
if (isLoading || !workflow_execution) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<WorkflowExecutionGeneralSection execution={workflow_execution} />
|
||||
<WorkflowExecutionTimelineSection execution={workflow_execution} />
|
||||
<WorkflowExecutionPayloadSection execution={workflow_execution} />
|
||||
<WorkflowExecutionHistorySection execution={workflow_execution} />
|
||||
<JsonViewSection data={workflow_execution} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./workflow-execution-list-table"
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Badge } from "@medusajs/ui"
|
||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { StatusCell } from "../../../../../components/table/table-cells/common/status-cell"
|
||||
import {
|
||||
TransactionStepState,
|
||||
WorkflowExecutionDTO,
|
||||
WorkflowExecutionStep,
|
||||
} from "../../../types"
|
||||
import { getTransactionState, getTransactionStateColor } from "../../../utils"
|
||||
|
||||
const columnHelper = createColumnHelper<WorkflowExecutionDTO>()
|
||||
|
||||
export const useWorkflowExecutionTableColumns = (): ColumnDef<
|
||||
WorkflowExecutionDTO,
|
||||
any
|
||||
>[] => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("transaction_id", {
|
||||
header: t("workflowExecutions.transactionIdLabel"),
|
||||
cell: ({ getValue }) => <Badge size="2xsmall">{getValue()}</Badge>,
|
||||
}),
|
||||
columnHelper.accessor("state", {
|
||||
header: t("fields.state"),
|
||||
cell: ({ getValue }) => {
|
||||
const state = getValue()
|
||||
|
||||
const color = getTransactionStateColor(state)
|
||||
const translatedState = getTransactionState(t, state)
|
||||
|
||||
return (
|
||||
<StatusCell color={color}>
|
||||
<span className="capitalize">{translatedState}</span>
|
||||
</StatusCell>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("execution", {
|
||||
header: t("workflowExecutions.progressLabel"),
|
||||
cell: ({ getValue }) => {
|
||||
const steps = getValue()?.steps as
|
||||
| Record<string, WorkflowExecutionStep>
|
||||
| undefined
|
||||
|
||||
if (!steps) {
|
||||
return "0 of 0 steps"
|
||||
}
|
||||
|
||||
const actionableSteps = Object.values(steps).filter(
|
||||
(step) => step.id !== ROOT_PREFIX
|
||||
)
|
||||
|
||||
const completedSteps = actionableSteps.filter(
|
||||
(step) => step.invoke.state === TransactionStepState.DONE
|
||||
)
|
||||
|
||||
return t("workflowExecutions.stepsCompletedLabel", {
|
||||
completed: completedSteps.length,
|
||||
count: actionableSteps.length,
|
||||
})
|
||||
},
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
|
||||
const ROOT_PREFIX = "_root"
|
||||
@@ -0,0 +1,25 @@
|
||||
import { AdminGetWorkflowExecutionsParams } from "@medusajs/medusa"
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
export const useWorkflowExecutionTableQuery = ({
|
||||
pageSize = 20,
|
||||
prefix,
|
||||
}: {
|
||||
pageSize?: number
|
||||
prefix?: string
|
||||
}) => {
|
||||
const raw = useQueryParams(["q", "offset"], prefix)
|
||||
|
||||
const { offset, ...rest } = raw
|
||||
|
||||
const searchParams: AdminGetWorkflowExecutionsParams = {
|
||||
limit: pageSize,
|
||||
offset: offset ? parseInt(offset) : 0,
|
||||
...rest,
|
||||
}
|
||||
|
||||
return {
|
||||
searchParams,
|
||||
raw,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
import { keepPreviousData } from "@tanstack/react-query"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { useWorkflowExecutions } from "../../../../../hooks/api/workflow-executions"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { WorkflowExecutionDTO } from "../../../types"
|
||||
import { useWorkflowExecutionTableColumns } from "./use-workflow-execution-table-columns"
|
||||
import { useWorkflowExecutionTableQuery } from "./use-workflow-execution-table-query"
|
||||
|
||||
/**
|
||||
* Type isn't exported from the package
|
||||
*/
|
||||
type WorkflowExecutionsRes = {
|
||||
workflow_executions: WorkflowExecutionDTO[]
|
||||
count: number
|
||||
offset: number
|
||||
limit: number
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export const WorkflowExecutionListTable = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { searchParams, raw } = useWorkflowExecutionTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
const { workflow_executions, count, isLoading, isError, error } =
|
||||
useWorkflowExecutions(
|
||||
{
|
||||
...searchParams,
|
||||
fields: "execution,state",
|
||||
},
|
||||
{
|
||||
placeholderData: keepPreviousData,
|
||||
}
|
||||
)
|
||||
|
||||
const columns = useWorkflowExecutionTableColumns()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: workflow_executions || [],
|
||||
columns,
|
||||
count: count,
|
||||
pageSize: PAGE_SIZE,
|
||||
enablePagination: true,
|
||||
getRowId: (row) => row.id,
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading>{t("workflowExecutions.domain")}</Heading>
|
||||
</div>
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
count={count}
|
||||
isLoading={isLoading}
|
||||
pageSize={PAGE_SIZE}
|
||||
navigateTo={(row) => `${row.id}`}
|
||||
search
|
||||
pagination
|
||||
queryObject={raw}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { WorkflowExcecutionList as Component } from "./workflow-execution-list"
|
||||
@@ -0,0 +1,9 @@
|
||||
import { WorkflowExecutionListTable } from "./components/workflow-execution-list-table"
|
||||
|
||||
export const WorkflowExcecutionList = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<WorkflowExecutionListTable />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user