feat(dashboard): Setup workflows in V2 routes (#6939)

This commit is contained in:
Kasper Fabricius Kristensen
2024-04-04 21:27:48 +02:00
committed by GitHub
parent 20e8df914e
commit 849010d875
33 changed files with 205 additions and 150 deletions

View File

@@ -860,8 +860,8 @@
"tokenExpiresIn": "Token expires in <0>{{time}}</0> minutes",
"successfulRequest": "We have sent you an email with instructions on how to reset your password. If you don't receive an email, please check your spam folder or try again."
},
"executions": {
"domain": "Executions",
"workflowExecutions": {
"domain": "Workflows",
"transactionIdLabel": "Transaction ID",
"workflowIdLabel": "Workflow ID",
"progressLabel": "Progress",

View File

@@ -68,8 +68,8 @@ const useDeveloperRoutes = (): NavItemProps[] => {
to: "/settings/api-key-management",
},
{
label: t("executions.domain"),
to: "/settings/executions",
label: t("workflowExecutions.domain"),
to: "/settings/workflows",
},
],
[t]

View File

@@ -820,23 +820,6 @@ export const v1Routes: RouteObject[] = [
},
],
},
{
path: "executions",
element: <Outlet />,
handle: {
crumb: () => "Executions",
},
children: [
{
path: "",
lazy: () => import("../../routes/executions/execution-list"),
},
{
path: ":id",
lazy: () => import("../../routes/executions/execution-detail"),
},
],
},
...settingsExtensions,
],
},

View File

@@ -300,6 +300,38 @@ export const v2Routes: RouteObject[] = [
},
],
},
{
path: "workflows",
element: <Outlet />,
handle: {
crumb: () => "Workflows",
},
children: [
{
path: "",
lazy: () =>
import(
"../../v2-routes/workflow-executions/workflow-execution-list"
),
},
{
path: ":id",
lazy: () =>
import(
"../../v2-routes/workflow-executions/workflow-execution-detail"
),
handle: {
crumb: (data: { workflow: any }) => {
if (!data) {
return ""
}
return data.workflow.name
},
},
},
],
},
],
},
],

View File

@@ -1,35 +0,0 @@
import { useAdminCustomQuery } from "medusa-react"
import { useParams } from "react-router-dom"
import { JsonViewSection } from "../../../components/common/json-view-section"
import { adminExecutionKey } from "../utils"
import { ExecutionGeneralSection } from "./components/execution-general-section"
import { ExecutionHistorySection } from "./components/execution-history-section"
import { ExecutionPayloadSection } from "./components/execution-payload-section"
import { ExecutionTimelineSection } from "./components/execution-timeline-section"
export const ExecutionDetail = () => {
const { id } = useParams()
const { data, isLoading, isError, error } = useAdminCustomQuery(
`/workflows-executions/${id}`,
adminExecutionKey.detail(id!)
)
if (isLoading || !data) {
return <div>Loading...</div>
}
if (isError) {
throw error
}
return (
<div className="flex flex-col gap-y-2">
<ExecutionGeneralSection execution={data.workflow_execution} />
<ExecutionPayloadSection execution={data.workflow_execution} />
<ExecutionTimelineSection execution={data.workflow_execution} />
<ExecutionHistorySection execution={data.workflow_execution} />
<JsonViewSection data={data.workflow_execution} />
</div>
)
}

View File

@@ -1 +0,0 @@
export { ExecutionDetail as Component } from "./execution-detail"

View File

@@ -1,9 +0,0 @@
import { ExecutionsListTable } from "./components/executions-list-table"
export const ExcecutionList = () => {
return (
<div className="flex flex-col gap-y-2">
<ExecutionsListTable />
</div>
)
}

View File

@@ -1 +0,0 @@
export { ExcecutionList as Component } from "./execution-list"

View File

@@ -40,19 +40,19 @@ export const getTransactionState = (
) => {
switch (state) {
case TransactionState.DONE:
return t("executions.state.done")
return t("workflowExecutions.state.done")
case TransactionState.FAILED:
return t("executions.state.failed")
return t("workflowExecutions.state.failed")
case TransactionState.REVERTED:
return t("executions.state.reverted")
return t("workflowExecutions.state.reverted")
case TransactionState.INVOKING:
return t("executions.state.invoking")
return t("workflowExecutions.state.invoking")
case TransactionState.WAITING_TO_COMPENSATE:
return t("executions.transaction.state.waitingToCompensate")
return t("workflowExecutions.transaction.state.waitingToCompensate")
case TransactionState.COMPENSATING:
return t("executions.state.compensating")
return t("workflowExecutions.state.compensating")
case TransactionState.NOT_STARTED:
return t("executions.state.notStarted")
return t("workflowExecutions.state.notStarted")
}
}
@@ -80,22 +80,22 @@ export const getStepState = (
) => {
switch (state) {
case TransactionStepState.DONE:
return t("executions.state.done")
return t("workflowExecutions.state.done")
case TransactionStepState.FAILED:
return t("executions.state.failed")
return t("workflowExecutions.state.failed")
case TransactionStepState.REVERTED:
return t("executions.state.reverted")
return t("workflowExecutions.state.reverted")
case TransactionStepState.INVOKING:
return t("executions.state.invoking")
return t("workflowExecutions.state.invoking")
case TransactionStepState.COMPENSATING:
return t("executions.state.compensating")
return t("workflowExecutions.state.compensating")
case TransactionStepState.NOT_STARTED:
return t("executions.state.notStarted")
return t("workflowExecutions.state.notStarted")
case TransactionStepState.SKIPPED:
return t("executions.step.state.skipped")
return t("workflowExecutions.step.state.skipped")
case TransactionStepState.DORMANT:
return t("executions.step.state.dormant")
return t("workflowExecutions.step.state.dormant")
case TransactionStepState.TIMEOUT:
return t("executions.step.state.timeout")
return t("workflowExecutions.step.state.timeout")
}
}

View File

@@ -15,13 +15,13 @@ import {
} from "../../../types"
import { getTransactionState, getTransactionStateColor } from "../../../utils"
type ExecutionGeneralSectionProps = {
type WorkflowExecutionGeneralSectionProps = {
execution: WorkflowExecutionDTO
}
export const ExecutionGeneralSection = ({
export const WorkflowExecutionGeneralSection = ({
execution,
}: ExecutionGeneralSectionProps) => {
}: WorkflowExecutionGeneralSectionProps) => {
const { t } = useTranslation()
const cleanId = execution.id.replace("wf_exec_", "")
@@ -39,7 +39,7 @@ export const ExecutionGeneralSection = ({
</div>
<div className="text-ui-fg-subtle grid grid-cols-2 px-6 py-4">
<Text size="small" leading="compact" weight="plus">
{t("executions.workflowIdLabel")}
{t("workflowExecutions.workflowIdLabel")}
</Text>
<Badge size="2xsmall" className="w-fit">
{execution.workflow_id}
@@ -47,7 +47,7 @@ export const ExecutionGeneralSection = ({
</div>
<div className="text-ui-fg-subtle grid grid-cols-2 px-6 py-4">
<Text size="small" leading="compact" weight="plus">
{t("executions.transactionIdLabel")}
{t("workflowExecutions.transactionIdLabel")}
</Text>
<Badge size="2xsmall" className="w-fit">
{execution.transaction_id}
@@ -55,7 +55,7 @@ export const ExecutionGeneralSection = ({
</div>
<div className="text-ui-fg-subtle grid grid-cols-2 px-6 py-4">
<Text size="small" leading="compact" weight="plus">
{t("executions.progressLabel")}
{t("workflowExecutions.progressLabel")}
</Text>
<Progress steps={execution.execution?.steps} />
</div>
@@ -75,7 +75,7 @@ const Progress = ({
if (!steps) {
return (
<Text size="small" leading="compact" className="text-ui-fg-subtle">
{t("executions.stepsCompletedLabel", {
{t("workflowExecutions.stepsCompletedLabel", {
completed: 0,
total: 0,
})}
@@ -108,7 +108,7 @@ const Progress = ({
))}
</div>
<Text size="small" leading="compact" className="text-ui-fg-subtle">
{t("executions.stepsCompletedLabel", {
{t("workflowExecutions.stepsCompletedLabel", {
completed: completedSteps.length,
count: actionableSteps.length,
})}

View File

@@ -27,13 +27,13 @@ import {
WorkflowExecutionStep,
} from "../../../types"
type ExecutionHistorySectionProps = {
type WorkflowExecutionHistorySectionProps = {
execution: WorkflowExecutionDTO
}
export const ExecutionHistorySection = ({
export const WorkflowExecutionHistorySection = ({
execution,
}: ExecutionHistorySectionProps) => {
}: WorkflowExecutionHistorySectionProps) => {
const { t } = useTranslation()
const map = Object.values(execution.execution?.steps || {})
@@ -57,7 +57,9 @@ export const ExecutionHistorySection = ({
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">{t("executions.history.sectionTitle")}</Heading>
<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) => {
@@ -178,13 +180,13 @@ const Event = ({
<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("executions.history.definitionLabel")}
{t("workflowExecutions.history.definitionLabel")}
</Text>
<CodeBlock
snippets={[
{
code: JSON.stringify(step.definition, null, 2),
label: t("executions.history.definitionLabel"),
label: t("workflowExecutions.history.definitionLabel"),
language: "json",
hideLineNumbers: true,
},
@@ -196,7 +198,7 @@ const Event = ({
{stepInvokeContext && (
<div className="text-ui-fg-subtle flex flex-col gap-y-2">
<Text size="small" leading="compact">
{t("executions.history.outputLabel")}
{t("workflowExecutions.history.outputLabel")}
</Text>
<CodeBlock
snippets={[
@@ -206,7 +208,7 @@ const Event = ({
null,
2
),
label: t("executions.history.outputLabel"),
label: t("workflowExecutions.history.outputLabel"),
language: "json",
hideLineNumbers: true,
},
@@ -220,7 +222,7 @@ const Event = ({
step.compensate.state === TransactionStepState.REVERTED && (
<div className="text-ui-fg-subtle flex flex-col gap-y-2">
<Text size="small" leading="compact">
{t("executions.history.compensateInputLabel")}
{t("workflowExecutions.history.compensateInputLabel")}
</Text>
<CodeBlock
snippets={[
@@ -230,7 +232,9 @@ const Event = ({
null,
2
),
label: t("executions.history.compensateInputLabel"),
label: t(
"workflowExecutions.history.compensateInputLabel"
),
language: "json",
hideLineNumbers: true,
},
@@ -243,7 +247,7 @@ const Event = ({
{stepError && (
<div className="text-ui-fg-subtle flex flex-col gap-y-2">
<Text size="small" leading="compact">
{t("executions.history.errorLabel")}
{t("workflowExecutions.history.errorLabel")}
</Text>
<CodeBlock
snippets={[
@@ -256,7 +260,7 @@ const Event = ({
null,
2
),
label: t("executions.history.errorLabel"),
label: t("workflowExecutions.history.errorLabel"),
language: "json",
hideLineNumbers: true,
},
@@ -295,7 +299,7 @@ const StepState = ({
return (
<div className="flex items-center gap-x-1">
<Text size="small" leading="compact" className="text-ui-fg-subtle">
{t("executions.history.runningState")}
{t("workflowExecutions.history.runningState")}
</Text>
<Spinner className="text-ui-fg-interactive animate-spin" />
</div>
@@ -305,7 +309,7 @@ const StepState = ({
if (isFailed) {
return (
<Text size="small" leading="compact" className="text-ui-fg-subtle">
{t("executions.history.failedState")}
{t("workflowExecutions.history.failedState")}
</Text>
)
}

View File

@@ -1,21 +1,21 @@
import { JsonViewSection } from "../../../../../components/common/json-view-section"
import { WorkflowExecutionDTO } from "../../../types"
type ExecutionPayloadSectionProps = {
type WorkflowExecutionPayloadSectionProps = {
execution: WorkflowExecutionDTO
}
export const ExecutionPayloadSection = ({
export const WorkflowExecutionPayloadSection = ({
execution,
}: ExecutionPayloadSectionProps) => {
}: WorkflowExecutionPayloadSectionProps) => {
let payload = execution.context?.data?.payload
if (!payload) {
return null
}
// payloads may be simple primitives, so we need to wrap them in an object
// to ensure the JsonViewSection component can render them
// 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 }
}

View File

@@ -1,5 +1,5 @@
import { ArrowPathMini, MinusMini, PlusMini } from "@medusajs/icons"
import { Container, Heading, Text, clx } from "@medusajs/ui"
import { Container, DropdownMenu, Heading, Text, clx } from "@medusajs/ui"
import {
motion,
useAnimationControls,
@@ -18,13 +18,13 @@ import {
} from "../../../constants"
import { WorkflowExecutionDTO, WorkflowExecutionStep } from "../../../types"
type ExecutionTimelineSectionProps = {
type WorkflowExecutionTimelineSectionProps = {
execution: WorkflowExecutionDTO
}
export const ExecutionTimelineSection = ({
export const WorkflowExecutionTimelineSection = ({
execution,
}: ExecutionTimelineSectionProps) => {
}: WorkflowExecutionTimelineSectionProps) => {
const { t } = useTranslation()
return (
@@ -79,6 +79,7 @@ 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)
@@ -152,6 +153,8 @@ const Canvas = ({ execution }: { execution: WorkflowExecutionDTO }) => {
<div className="relative size-full overflow-hidden object-contain">
<div>
<motion.div
onMouseDown={() => setIsDragging(true)}
onMouseUp={() => setIsDragging(false)}
drag
dragConstraints={dragConstraints}
dragElastic={0}
@@ -165,7 +168,14 @@ const Canvas = ({ execution }: { execution: WorkflowExecutionDTO }) => {
y,
scale,
}}
className="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"
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">
@@ -188,25 +198,51 @@ const Canvas = ({ execution }: { execution: WorkflowExecutionDTO }) => {
</motion.div>
</div>
</div>
<div className="bg-ui-bg-base shadow-borders-base text-ui-fg-subtle absolute bottom-4 left-6 h-7 overflow-hidden rounded-md">
<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>
<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 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"

View File

@@ -0,0 +1 @@
export { ExecutionDetail as Component } from "./workflow-detail"

View File

@@ -0,0 +1,35 @@
import { useAdminCustomQuery } from "medusa-react"
import { useParams } from "react-router-dom"
import { JsonViewSection } from "../../../components/common/json-view-section"
import { adminExecutionKey } from "../utils"
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 { data, isLoading, isError, error } = useAdminCustomQuery(
`/workflows-executions/${id}`,
adminExecutionKey.detail(id!)
)
if (isLoading || !data) {
return <div>Loading...</div>
}
if (isError) {
throw error
}
return (
<div className="flex flex-col gap-y-2">
<WorkflowExecutionGeneralSection execution={data.workflow_execution} />
<WorkflowExecutionTimelineSection execution={data.workflow_execution} />
<WorkflowExecutionPayloadSection execution={data.workflow_execution} />
<WorkflowExecutionHistorySection execution={data.workflow_execution} />
<JsonViewSection data={data.workflow_execution} />
</div>
)
}

View File

@@ -12,7 +12,7 @@ import { getTransactionState, getTransactionStateColor } from "../../../utils"
const columnHelper = createColumnHelper<WorkflowExecutionDTO>()
export const useExecutionTableColumns = (): ColumnDef<
export const useWorkflowExecutionTableColumns = (): ColumnDef<
WorkflowExecutionDTO,
any
>[] => {
@@ -21,7 +21,7 @@ export const useExecutionTableColumns = (): ColumnDef<
return useMemo(
() => [
columnHelper.accessor("transaction_id", {
header: t("executions.transactionIdLabel"),
header: t("workflowExecutions.transactionIdLabel"),
cell: ({ getValue }) => <Badge size="2xsmall">{getValue()}</Badge>,
}),
columnHelper.accessor("state", {
@@ -40,7 +40,7 @@ export const useExecutionTableColumns = (): ColumnDef<
},
}),
columnHelper.accessor("execution", {
header: t("executions.progressLabel"),
header: t("workflowExecutions.progressLabel"),
cell: ({ getValue }) => {
const steps = getValue()?.steps as
| Record<string, WorkflowExecutionStep>
@@ -58,7 +58,7 @@ export const useExecutionTableColumns = (): ColumnDef<
(step) => step.invoke.state === TransactionStepState.DONE
)
return t("executions.stepsCompletedLabel", {
return t("workflowExecutions.stepsCompletedLabel", {
completed: completedSteps.length,
count: actionableSteps.length,
})

View File

@@ -1,7 +1,7 @@
import { AdminGetWorkflowExecutionsParams } from "@medusajs/medusa"
import { useQueryParams } from "../../../../../hooks/use-query-params"
export const useExecutionTableQuery = ({
export const useWorkflowExecutionTableQuery = ({
pageSize = 20,
prefix,
}: {

View File

@@ -6,8 +6,8 @@ import { DataTable } from "../../../../../components/table/data-table"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { WorkflowExecutionDTO } from "../../../types"
import { adminExecutionKey } from "../../../utils"
import { useExecutionTableColumns } from "./use-execution-table-columns"
import { useExecutionTableQuery } from "./use-execution-table-query"
import { useWorkflowExecutionTableColumns } from "./use-workflow-execution-table-columns"
import { useWorkflowExecutionTableQuery } from "./use-workflow-execution-table-query"
/**
* Type isn't exported from the package
@@ -21,10 +21,10 @@ type WorkflowExecutionsRes = {
const PAGE_SIZE = 20
export const ExecutionsListTable = () => {
export const WorkflowExecutionListTable = () => {
const { t } = useTranslation()
const { searchParams, raw } = useExecutionTableQuery({
const { searchParams, raw } = useWorkflowExecutionTableQuery({
pageSize: PAGE_SIZE,
})
const { data, isLoading, isError, error } = useAdminCustomQuery<
@@ -42,7 +42,7 @@ export const ExecutionsListTable = () => {
}
)
const columns = useExecutionTableColumns()
const columns = useWorkflowExecutionTableColumns()
const { table } = useDataTable({
data: data?.workflow_executions || [],
@@ -60,7 +60,7 @@ export const ExecutionsListTable = () => {
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading>{t("executions.domain")}</Heading>
<Heading>{t("workflowExecutions.domain")}</Heading>
</div>
<DataTable
table={table}

View File

@@ -0,0 +1 @@
export { WorkflowExcecutionList as Component } from "./workflow-execution-list"

View File

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