From 2d00625729e7dab02149751327239992dea3a8e1 Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Sat, 9 Mar 2024 19:46:10 +0100 Subject: [PATCH] feat(dashboard): Workflow executions (#6564) **What** - v1 of executions domain **TODO in follow up PR** I think it might be a good idea for Carlos or Adrien to go over the design with Ludvig, and make sure we are displaying what is relevant to developers, and how things should be displayed. So this PR is just the initial implementation, and does not handle displaying things like compensation steps (not part of the current design), step input (not supported by the API but part of the design), etc. Closes CORE-1751, CORE-1755 --- .changeset/plenty-bikes-drop.md | 6 + packages/admin-next/dashboard/package.json | 2 +- .../public/locales/en-US/translation.json | 51 ++- .../json-view-section/json-view-section.tsx | 85 ++-- .../settings-layout/settings-layout.tsx | 73 +++- packages/admin-next/dashboard/src/index.css | 14 + .../router-provider/router-provider.tsx | 17 + .../src/routes/executions/constants.ts | 26 ++ .../execution-general-section.tsx | 118 ++++++ .../execution-general-section/index.ts | 1 + .../execution-history-section.tsx | 320 ++++++++++++++ .../execution-history-section/index.ts | 1 + .../execution-payload-section.tsx | 24 ++ .../execution-payload-section/index.ts | 1 + .../execution-timeline-section.tsx | 399 ++++++++++++++++++ .../execution-timeline-section/index.ts | 1 + .../execution-detail/execution-detail.tsx | 35 ++ .../executions/execution-detail/index.ts | 1 + .../executions/execution-detail/loader.ts | 21 + .../executions-list-table.tsx | 78 ++++ .../components/executions-list-table/index.ts | 1 + .../use-execution-table-columns.tsx | 72 ++++ .../use-execution-table-query.tsx | 25 ++ .../execution-list/execution-list.tsx | 9 + .../routes/executions/execution-list/index.ts | 1 + .../dashboard/src/routes/executions/types.ts | 95 +++++ .../dashboard/src/routes/executions/utils.ts | 101 +++++ packages/design-system/ui/package.json | 1 + .../src/components/code-block/code-block.tsx | 14 +- .../admin/workflows-executions/index.ts | 1 + yarn.lock | 13 +- 31 files changed, 1545 insertions(+), 62 deletions(-) create mode 100644 .changeset/plenty-bikes-drop.md create mode 100644 packages/admin-next/dashboard/src/routes/executions/constants.ts create mode 100644 packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-general-section/execution-general-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-general-section/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-history-section/execution-history-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-history-section/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-payload-section/execution-payload-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-payload-section/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-timeline-section/execution-timeline-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-timeline-section/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/executions/execution-detail/execution-detail.tsx create mode 100644 packages/admin-next/dashboard/src/routes/executions/execution-detail/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/executions/execution-detail/loader.ts create mode 100644 packages/admin-next/dashboard/src/routes/executions/execution-list/components/executions-list-table/executions-list-table.tsx create mode 100644 packages/admin-next/dashboard/src/routes/executions/execution-list/components/executions-list-table/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/executions/execution-list/components/executions-list-table/use-execution-table-columns.tsx create mode 100644 packages/admin-next/dashboard/src/routes/executions/execution-list/components/executions-list-table/use-execution-table-query.tsx create mode 100644 packages/admin-next/dashboard/src/routes/executions/execution-list/execution-list.tsx create mode 100644 packages/admin-next/dashboard/src/routes/executions/execution-list/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/executions/types.ts create mode 100644 packages/admin-next/dashboard/src/routes/executions/utils.ts diff --git a/.changeset/plenty-bikes-drop.md b/.changeset/plenty-bikes-drop.md new file mode 100644 index 0000000000..eb058e7079 --- /dev/null +++ b/.changeset/plenty-bikes-drop.md @@ -0,0 +1,6 @@ +--- +"@medusajs/ui": patch +"@medusajs/medusa": patch +--- + +fix(medusa,ui) Export param types for workflow endpoints. Add support for JSON to CodeBlock component. diff --git a/packages/admin-next/dashboard/package.json b/packages/admin-next/dashboard/package.json index d82741aea5..7337b56e67 100644 --- a/packages/admin-next/dashboard/package.json +++ b/packages/admin-next/dashboard/package.json @@ -25,7 +25,7 @@ "@tanstack/react-query": "4.22.0", "@tanstack/react-table": "8.10.7", "@tanstack/react-virtual": "^3.0.4", - "@uiw/react-json-view": "2.0.0-alpha.10", + "@uiw/react-json-view": "^2.0.0-alpha.17", "cmdk": "^0.2.0", "date-fns": "^3.2.0", "framer-motion": "^11.0.3", diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json index f1d4210ee7..70a5adfe85 100644 --- a/packages/admin-next/dashboard/public/locales/en-US/translation.json +++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json @@ -17,9 +17,6 @@ "is": "is", "select": "Select", "selected": "Selected", - "extensions": "Extensions", - "settings": "Settings", - "general": "General", "details": "Details", "enabled": "Enabled", "disabled": "Disabled", @@ -41,7 +38,14 @@ "noRecordsMessage": "There are no records to show", "unsavedChangesTitle": "Are you sure you want to leave this page?", "unsavedChangesDescription": "You have unsaved changes that will be lost if you leave this page.", - "includesTaxTooltip": "Enter the total amount including tax. The net amount excluding tax will be automatically calculated and saved." + "includesTaxTooltip": "Enter the total amount including tax. The net amount excluding tax will be automatically calculated and saved.", + "timeline": "Timeline" + }, + "nav": { + "general": "General", + "developer": "Developer", + "extensions": "Extensions", + "settings": "Settings" }, "actions": { "create": "Create", @@ -497,6 +501,45 @@ "tokenExpiresIn": "Token expires in <0>{{time}} 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", + "transactionIdLabel": "Transaction ID", + "workflowIdLabel": "Workflow ID", + "progressLabel": "Progress", + "stepsCompletedLabel_one": "{{completed}} of {{count}} step", + "stepsCompletedLabel_other": "{{completed}} of {{count}} steps", + "history": { + "sectionTitle": "History", + "runningState": "Running...", + "awaitingState": "Awaiting", + "failedState": "Failed", + "definitionLabel": "Definition", + "outputLabel": "Output", + "compensateInputLabel": "Compensate input", + "revertedLabel": "Reverted", + "errorLabel": "Error" + }, + "state": { + "done": "Done", + "failed": "Failed", + "reverted": "Reverted", + "invoking": "Invoking", + "compensating": "Compensating", + "notStarted": "Not started" + }, + "transaction": { + "state": { + "waitingToCompensate": "Waiting to compensate" + } + }, + "step": { + "state": { + "skipped": "Skipped", + "dormant": "Dormant", + "timeout": "Timeout" + } + } + }, "errors": { "serverError": "Server error - Try again later.", "invalidCredentials": "Wrong email or password" diff --git a/packages/admin-next/dashboard/src/components/common/json-view-section/json-view-section.tsx b/packages/admin-next/dashboard/src/components/common/json-view-section/json-view-section.tsx index 18b5ffaa1c..02c2f6988d 100644 --- a/packages/admin-next/dashboard/src/components/common/json-view-section/json-view-section.tsx +++ b/packages/admin-next/dashboard/src/components/common/json-view-section/json-view-section.tsx @@ -1,9 +1,4 @@ -import { - ArrowsPointingOut, - CheckCircleMiniSolid, - SquareTwoStackMini, - XMarkMini, -} from "@medusajs/icons" +import { ArrowsPointingOut, XMarkMini } from "@medusajs/icons" import { Badge, Container, @@ -14,20 +9,27 @@ import { } from "@medusajs/ui" import Primitive from "@uiw/react-json-view" import { CSSProperties, Suspense } from "react" +import { useTranslation } from "react-i18next" -type JsonViewProps = { +type JsonViewSectionProps = { data: object root?: string + title?: string } // TODO: Fix the positioning of the copy btn -export const JsonViewSection = ({ data, root }: JsonViewProps) => { +export const JsonViewSection = ({ + data, + root, + title = "JSON", +}: JsonViewSectionProps) => { + const { t } = useTranslation() const numberOfKeys = Object.keys(data).length return (
- JSON + {title} {numberOfKeys} keys
@@ -40,10 +42,10 @@ export const JsonViewSection = ({ data, root }: JsonViewProps) => { - -
+ +
- JSON + {title} {numberOfKeys} keys
@@ -68,52 +70,53 @@ export const JsonViewSection = ({ data, root }: JsonViewProps) => { style={ { "--w-rjv-font-family": "Roboto Mono, monospace", - "--w-rjv-line-color": "#2E3035", - "--w-rjv-curlybraces-color": "#ADB1B8", - "--w-rjv-key-string": "#A78BFA", - "--w-rjv-info-color": "#FBBF24", - "--w-rjv-type-string-color": "#34D399", - "--w-rjv-quotes-string-color": "#34D399", - "--w-rjv-type-boolean-color": "#FBBF24", - "--w-rjv-type-int-color": "#60A5FA", - "--w-rjv-type-float-color": "#60A5FA", - "--w-rjv-type-bigint-color": "#60A5FA", - "--w-rjv-key-number": "#60A5FA", + "--w-rjv-line-color": "var(--code-border)", + "--w-rjv-curlybraces-color": "rgb(255,255,255)", + "--w-rjv-key-string": "rgb(247,208,25)", + "--w-rjv-info-color": "var(--code-fg-muted)", + "--w-rjv-type-string-color": "rgb(73,209,110)", + "--w-rjv-quotes-string-color": "rgb(73,209,110)", + "--w-rjv-type-boolean-color": "rgb(187,77,96)", + "--w-rjv-type-int-color": "rgb(247,208,25)", + "--w-rjv-type-float-color": "rgb(247,208,25)", + "--w-rjv-type-bigint-color": "rgb(247,208,25)", + "--w-rjv-key-number": "rgb(247,208,25)", + "--w-rjv-arrow-color": "rgb(255,255,255)", + "--w-rjv-copied-color": "var(--code-fg-subtle)", + "--w-rjv-copied-success-color": "var(--code-fg-base)", + "--w-rjv-colon-color": "rgb(255,255,255)", } as CSSProperties } collapsed={1} > - { - if (copied) { - return ( - - ) - } - return ( - - ) - }} - /> - " "} /> + } /> ( null )} /> + ( + undefined + )} + /> { return ( - {Object.keys(value as object).length} items + {t("general.items", { + count: Object.keys(value as object).length, + })} ) }} /> + {/* + + */} + + : + diff --git a/packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx b/packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx index 25f181756c..9702645749 100644 --- a/packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx +++ b/packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx @@ -1,5 +1,6 @@ -import { ArrowUturnLeft } from "@medusajs/icons" +import { ArrowUturnLeft, MinusMini } from "@medusajs/icons" import { IconButton, Text } from "@medusajs/ui" +import * as Collapsible from "@radix-ui/react-collapsible" import { useEffect, useMemo, useState } from "react" import { useTranslation } from "react-i18next" import { Link, useLocation } from "react-router-dom" @@ -48,10 +49,24 @@ const useSettingRoutes = (): NavItemProps[] => { label: t("salesChannels.domain"), to: "/settings/sales-channels", }, + ], + [t] + ) +} + +const useDeveloperRoutes = (): NavItemProps[] => { + const { t } = useTranslation() + + return useMemo( + () => [ { label: t("apiKeyManagement.domain"), to: "/settings/api-key-management", }, + { + label: t("executions.domain"), + to: "/settings/executions", + }, ], [t] ) @@ -59,6 +74,7 @@ const useSettingRoutes = (): NavItemProps[] => { const SettingsSidebar = () => { const routes = useSettingRoutes() + const developerRoutes = useDeveloperRoutes() const { t } = useTranslation() const location = useLocation() @@ -80,19 +96,60 @@ const SettingsSidebar = () => { - {t("general.settings")} + {t("nav.settings")}
-
- +
+ +
+
+ + {t("nav.general")} + + + + + + +
+
+ +
+ +
+
+
+ +
+
+ + {t("nav.developer")} + + + + + + +
+
+ +
+ +
+
+
) diff --git a/packages/admin-next/dashboard/src/index.css b/packages/admin-next/dashboard/src/index.css index b8cac0738d..5171757173 100644 --- a/packages/admin-next/dashboard/src/index.css +++ b/packages/admin-next/dashboard/src/index.css @@ -31,3 +31,17 @@ @apply bg-ui-bg-subtle text-ui-fg-base; } } + +@layer components { + .worfklow-grid { + background-image: radial-gradient(black 1px, transparent 0); + background-size: 40px 40px; + background: repeat; + } +} + +.worfklow-grid { + background-image: radial-gradient(black 1px, transparent 0); + background-size: 40px 40px; + background: repeat; +} diff --git a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx index 7172a1354d..694aaf19b9 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx @@ -688,6 +688,23 @@ const router = createBrowserRouter([ }, ], }, + { + path: "executions", + element: , + handle: { + crumb: () => "Executions", + }, + children: [ + { + path: "", + lazy: () => import("../../routes/executions/execution-list"), + }, + { + path: ":id", + lazy: () => import("../../routes/executions/execution-detail"), + }, + ], + }, ...settingsExtensions, ], }, diff --git a/packages/admin-next/dashboard/src/routes/executions/constants.ts b/packages/admin-next/dashboard/src/routes/executions/constants.ts new file mode 100644 index 0000000000..c02f3cf0ef --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/executions/constants.ts @@ -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, +] diff --git a/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-general-section/execution-general-section.tsx b/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-general-section/execution-general-section.tsx new file mode 100644 index 0000000000..17eb6f77fe --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-general-section/execution-general-section.tsx @@ -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 ExecutionGeneralSectionProps = { + execution: WorkflowExecutionDTO +} + +export const ExecutionGeneralSection = ({ + execution, +}: ExecutionGeneralSectionProps) => { + const { t } = useTranslation() + + const cleanId = execution.id.replace("wf_exec_", "") + const translatedState = getTransactionState(t, execution.state) + const stateColor = getTransactionStateColor(execution.state) + + return ( + +
+
+ {cleanId} + +
+ {translatedState} +
+
+ + {t("executions.workflowIdLabel")} + + + {execution.workflow_id} + +
+
+ + {t("executions.transactionIdLabel")} + + + {execution.transaction_id} + +
+
+ + {t("executions.progressLabel")} + + +
+
+ ) +} + +const ROOT_PREFIX = "_root" + +const Progress = ({ + steps, +}: { + steps?: Record | null +}) => { + const { t } = useTranslation() + + if (!steps) { + return ( + + {t("executions.stepsCompletedLabel", { + completed: 0, + total: 0, + })} + + ) + } + + const actionableSteps = Object.values(steps).filter( + (step) => step.id !== ROOT_PREFIX + ) + + const completedSteps = actionableSteps.filter( + (step) => step.invoke.state === TransactionStepState.DONE + ) + + return ( +
+
+ {actionableSteps.map((step) => ( +
+ ))} +
+ + {t("executions.stepsCompletedLabel", { + completed: completedSteps.length, + count: actionableSteps.length, + })} + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-general-section/index.ts b/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-general-section/index.ts new file mode 100644 index 0000000000..c8ae4714fe --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-general-section/index.ts @@ -0,0 +1 @@ +export * from "./execution-general-section" diff --git a/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-history-section/execution-history-section.tsx b/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-history-section/execution-history-section.tsx new file mode 100644 index 0000000000..71f7b60911 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-history-section/execution-history-section.tsx @@ -0,0 +1,320 @@ +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 ExecutionHistorySectionProps = { + execution: WorkflowExecutionDTO +} + +export const ExecutionHistorySection = ({ + execution, +}: ExecutionHistorySectionProps) => { + 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 ( + +
+ {t("executions.history.sectionTitle")} +
+
+ {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 ( + + ) + })} +
+
+ ) +} + +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(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 ( +
+
+
+
+
+
+
+
+
+
+
+ + +
+ + {identifier} + +
+ + + + +
+
+
+ +
+
+ + {t("executions.history.definitionLabel")} + + + + +
+ {stepInvokeContext && ( +
+ + {t("executions.history.outputLabel")} + + + + +
+ )} + {!!stepInvokeContext?.output.compensateInput && + step.compensate.state === TransactionStepState.REVERTED && ( +
+ + {t("executions.history.compensateInputLabel")} + + + + +
+ )} + {stepError && ( +
+ + {t("executions.history.errorLabel")} + + + + +
+ )} +
+
+
+
+ ) +} + +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 ( +
+ + {t("executions.history.runningState")} + + +
+ ) + } + + if (isFailed) { + return ( + + {t("executions.history.failedState")} + + ) + } + + if (startedAt) { + return ( + + {format(startedAt, "dd MMM yyyy HH:mm:ss")} + + ) + } +} diff --git a/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-history-section/index.ts b/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-history-section/index.ts new file mode 100644 index 0000000000..bf1afb788f --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-history-section/index.ts @@ -0,0 +1 @@ +export * from "./execution-history-section" diff --git a/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-payload-section/execution-payload-section.tsx b/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-payload-section/execution-payload-section.tsx new file mode 100644 index 0000000000..ae1860e476 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-payload-section/execution-payload-section.tsx @@ -0,0 +1,24 @@ +import { JsonViewSection } from "../../../../../components/common/json-view-section" +import { WorkflowExecutionDTO } from "../../../types" + +type ExecutionPayloadSectionProps = { + execution: WorkflowExecutionDTO +} + +export const ExecutionPayloadSection = ({ + execution, +}: ExecutionPayloadSectionProps) => { + 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 + if (typeof payload !== "object") { + payload = { input: payload } + } + + return +} diff --git a/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-payload-section/index.ts b/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-payload-section/index.ts new file mode 100644 index 0000000000..d3b4a12f25 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-payload-section/index.ts @@ -0,0 +1 @@ +export * from "./execution-payload-section" diff --git a/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-timeline-section/execution-timeline-section.tsx b/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-timeline-section/execution-timeline-section.tsx new file mode 100644 index 0000000000..740a7aed13 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-timeline-section/execution-timeline-section.tsx @@ -0,0 +1,399 @@ +import { ArrowPathMini, MinusMini, PlusMini } from "@medusajs/icons" +import { Container, 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 ExecutionTimelineSectionProps = { + execution: WorkflowExecutionDTO +} + +export const ExecutionTimelineSection = ({ + execution, +}: ExecutionTimelineSectionProps) => { + const { t } = useTranslation() + + return ( + +
+ {t("general.timeline")} +
+
+ +
+
+ ) +} + +const createNodeClusters = (steps: Record) => { + const actionableSteps = Object.values(steps).filter( + (step) => step.id !== "_root" + ) + + const clusters: Record = {} + + actionableSteps.forEach((step) => { + if (!clusters[step.depth]) { + clusters[step.depth] = [] + } + + clusters[step.depth].push(step) + }) + + return clusters +} + +const getNextCluster = ( + clusters: Record, + 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(1) + + const scale = useMotionValue(defaultState.scale) + const x = useMotionValue(defaultState.x) + const y = useMotionValue(defaultState.y) + + const controls = useAnimationControls() + + const dragControls = useDragControls() + const dragConstraints = useRef(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 ( +
+
+
+
+ +
+
+ {Object.entries(clusters).map(([depth, cluster]) => { + const next = getNextCluster(clusters, Number(depth)) + + return ( +
+
+ {cluster.map((step) => ( + + ))} +
+ +
+ ) + })} +
+
+
+
+
+
+ + + +
+
+
+ ) +} + +const HorizontalArrow = () => { + return ( + + + + ) +} + +const MiddleArrow = () => { + return ( + + + + ) +} + +const EndArrow = () => { + return ( + + + + ) +} + +const Arrow = ({ depth }: { depth: number }) => { + if (depth === 1) { + return + } + + if (depth === 2) { + return ( +
+ + +
+ ) + } + + const inbetween = Array.from({ length: depth - 2 }).map((_, index) => ( + + )) + + return ( +
+ + {inbetween} + +
+ ) +} + +const Line = ({ next }: { next?: WorkflowExecutionStep[] }) => { + if (!next) { + return null + } + + return ( +
+
+
+
+
+
+ +
+
+
+ ) +} + +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 ( + +
+
+
+
+ + {stepId} + +
+ + ) +} diff --git a/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-timeline-section/index.ts b/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-timeline-section/index.ts new file mode 100644 index 0000000000..4acff81887 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/executions/execution-detail/components/execution-timeline-section/index.ts @@ -0,0 +1 @@ +export * from "./execution-timeline-section" diff --git a/packages/admin-next/dashboard/src/routes/executions/execution-detail/execution-detail.tsx b/packages/admin-next/dashboard/src/routes/executions/execution-detail/execution-detail.tsx new file mode 100644 index 0000000000..cabf1996a8 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/executions/execution-detail/execution-detail.tsx @@ -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 { 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
Loading...
+ } + + if (isError) { + throw error + } + + return ( +
+ + + + + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/executions/execution-detail/index.ts b/packages/admin-next/dashboard/src/routes/executions/execution-detail/index.ts new file mode 100644 index 0000000000..d939b77df5 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/executions/execution-detail/index.ts @@ -0,0 +1 @@ +export { ExecutionDetail as Component } from "./execution-detail" diff --git a/packages/admin-next/dashboard/src/routes/executions/execution-detail/loader.ts b/packages/admin-next/dashboard/src/routes/executions/execution-detail/loader.ts new file mode 100644 index 0000000000..b1037283fb --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/executions/execution-detail/loader.ts @@ -0,0 +1,21 @@ +import { AdminProductsRes } from "@medusajs/medusa" +import { Response } from "@medusajs/medusa-js" +import { adminProductKeys } from "medusa-react" +import { LoaderFunctionArgs } from "react-router-dom" + +import { medusa, queryClient } from "../../../lib/medusa" + +const executionDetailQuery = (id: string) => ({ + queryKey: adminProductKeys.detail(id), + queryFn: async () => medusa.admin.custom.get(`/workflows-executions/${id}`), +}) + +export const executionLoader = async ({ params }: LoaderFunctionArgs) => { + const id = params.id + const query = executionDetailQuery(id!) + + return ( + queryClient.getQueryData>(query.queryKey) ?? + (await queryClient.fetchQuery(query)) + ) +} diff --git a/packages/admin-next/dashboard/src/routes/executions/execution-list/components/executions-list-table/executions-list-table.tsx b/packages/admin-next/dashboard/src/routes/executions/execution-list/components/executions-list-table/executions-list-table.tsx new file mode 100644 index 0000000000..d921635dc6 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/executions/execution-list/components/executions-list-table/executions-list-table.tsx @@ -0,0 +1,78 @@ +import { AdminGetWorkflowExecutionsParams } from "@medusajs/medusa" +import { Container, Heading } from "@medusajs/ui" +import { useAdminCustomQuery } from "medusa-react" +import { useTranslation } from "react-i18next" +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" + +/** + * Type isn't exported from the package + */ +type WorkflowExecutionsRes = { + workflow_executions: WorkflowExecutionDTO[] + count: number + offset: number + limit: number +} + +const PAGE_SIZE = 20 + +export const ExecutionsListTable = () => { + const { t } = useTranslation() + + const { searchParams, raw } = useExecutionTableQuery({ + pageSize: PAGE_SIZE, + }) + const { data, isLoading, isError, error } = useAdminCustomQuery< + AdminGetWorkflowExecutionsParams, + WorkflowExecutionsRes + >( + "/workflows-executions", + adminExecutionKey.list(searchParams), + { + ...searchParams, + fields: "execution,state", + }, + { + keepPreviousData: true, + } + ) + + const columns = useExecutionTableColumns() + + const { table } = useDataTable({ + data: data?.workflow_executions || [], + columns, + count: data?.count, + pageSize: PAGE_SIZE, + enablePagination: true, + getRowId: (row) => row.id, + }) + + if (isError) { + throw error + } + + return ( + +
+ {t("executions.domain")} +
+ `${row.id}`} + search + pagination + queryObject={raw} + /> +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/executions/execution-list/components/executions-list-table/index.ts b/packages/admin-next/dashboard/src/routes/executions/execution-list/components/executions-list-table/index.ts new file mode 100644 index 0000000000..d23ad6ee55 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/executions/execution-list/components/executions-list-table/index.ts @@ -0,0 +1 @@ +export * from "./executions-list-table" diff --git a/packages/admin-next/dashboard/src/routes/executions/execution-list/components/executions-list-table/use-execution-table-columns.tsx b/packages/admin-next/dashboard/src/routes/executions/execution-list/components/executions-list-table/use-execution-table-columns.tsx new file mode 100644 index 0000000000..9852a2fbe0 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/executions/execution-list/components/executions-list-table/use-execution-table-columns.tsx @@ -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() + +export const useExecutionTableColumns = (): ColumnDef< + WorkflowExecutionDTO, + any +>[] => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.accessor("transaction_id", { + header: t("executions.transactionIdLabel"), + cell: ({ getValue }) => {getValue()}, + }), + columnHelper.accessor("state", { + header: t("fields.state"), + cell: ({ getValue }) => { + const state = getValue() + + const color = getTransactionStateColor(state) + const translatedState = getTransactionState(t, state) + + return ( + + {translatedState} + + ) + }, + }), + columnHelper.accessor("execution", { + header: t("executions.progressLabel"), + cell: ({ getValue }) => { + const steps = getValue()?.steps as + | Record + | 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("executions.stepsCompletedLabel", { + completed: completedSteps.length, + count: actionableSteps.length, + }) + }, + }), + ], + [t] + ) +} + +const ROOT_PREFIX = "_root" diff --git a/packages/admin-next/dashboard/src/routes/executions/execution-list/components/executions-list-table/use-execution-table-query.tsx b/packages/admin-next/dashboard/src/routes/executions/execution-list/components/executions-list-table/use-execution-table-query.tsx new file mode 100644 index 0000000000..5057e82f83 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/executions/execution-list/components/executions-list-table/use-execution-table-query.tsx @@ -0,0 +1,25 @@ +import { AdminGetWorkflowExecutionsParams } from "@medusajs/medusa" +import { useQueryParams } from "../../../../../hooks/use-query-params" + +export const useExecutionTableQuery = ({ + 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, + } +} diff --git a/packages/admin-next/dashboard/src/routes/executions/execution-list/execution-list.tsx b/packages/admin-next/dashboard/src/routes/executions/execution-list/execution-list.tsx new file mode 100644 index 0000000000..4fd985c946 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/executions/execution-list/execution-list.tsx @@ -0,0 +1,9 @@ +import { ExecutionsListTable } from "./components/executions-list-table" + +export const ExcecutionList = () => { + return ( +
+ +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/executions/execution-list/index.ts b/packages/admin-next/dashboard/src/routes/executions/execution-list/index.ts new file mode 100644 index 0000000000..2cfda278ad --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/executions/execution-list/index.ts @@ -0,0 +1 @@ +export { ExcecutionList as Component } from "./execution-list" diff --git a/packages/admin-next/dashboard/src/routes/executions/types.ts b/packages/admin-next/dashboard/src/routes/executions/types.ts new file mode 100644 index 0000000000..eacd3d964f --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/executions/types.ts @@ -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 + action: string + handlerType: string +} + +export type WorkflowExecutionContext = { + data: { + invoke: Record + payload?: unknown + } + compensate: Record + errors: StepError[] +} + +type WorflowExecutionExecution = { + steps: Record +} + +/** + * 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", +} diff --git a/packages/admin-next/dashboard/src/routes/executions/utils.ts b/packages/admin-next/dashboard/src/routes/executions/utils.ts new file mode 100644 index 0000000000..c0da09a8ee --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/executions/utils.ts @@ -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("executions.state.done") + case TransactionState.FAILED: + return t("executions.state.failed") + case TransactionState.REVERTED: + return t("executions.state.reverted") + case TransactionState.INVOKING: + return t("executions.state.invoking") + case TransactionState.WAITING_TO_COMPENSATE: + return t("executions.transaction.state.waitingToCompensate") + case TransactionState.COMPENSATING: + return t("executions.state.compensating") + case TransactionState.NOT_STARTED: + return t("executions.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("executions.state.done") + case TransactionStepState.FAILED: + return t("executions.state.failed") + case TransactionStepState.REVERTED: + return t("executions.state.reverted") + case TransactionStepState.INVOKING: + return t("executions.state.invoking") + case TransactionStepState.COMPENSATING: + return t("executions.state.compensating") + case TransactionStepState.NOT_STARTED: + return t("executions.state.notStarted") + case TransactionStepState.SKIPPED: + return t("executions.step.state.skipped") + case TransactionStepState.DORMANT: + return t("executions.step.state.dormant") + case TransactionStepState.TIMEOUT: + return t("executions.step.state.timeout") + } +} diff --git a/packages/design-system/ui/package.json b/packages/design-system/ui/package.json index 97ca652454..f6d066f94c 100644 --- a/packages/design-system/ui/package.json +++ b/packages/design-system/ui/package.json @@ -105,6 +105,7 @@ "cva": "1.0.0-beta.1", "date-fns": "^2.30.0", "prism-react-renderer": "^2.0.6", + "prismjs": "^1.29.0", "react-currency-input-field": "^3.6.11", "react-day-picker": "^8.8.0", "tailwind-merge": "^2.2.1" diff --git a/packages/design-system/ui/src/components/code-block/code-block.tsx b/packages/design-system/ui/src/components/code-block/code-block.tsx index c8723eeccd..8013742a8d 100644 --- a/packages/design-system/ui/src/components/code-block/code-block.tsx +++ b/packages/design-system/ui/src/components/code-block/code-block.tsx @@ -1,6 +1,10 @@ "use client" -import { Highlight, themes } from "prism-react-renderer" +import { Highlight, Prism, themes } from "prism-react-renderer" import * as React from "react" +;(typeof global !== "undefined" ? global : window).Prism = Prism + +// @ts-ignore +import("prismjs/components/prism-json") import { Copy } from "@/components/copy" import { clx } from "@/utils/clx" @@ -215,6 +219,12 @@ const Body = ({ color: "rgb(247,208,25)", }, }, + { + types: ["property"], + style: { + color: "rgb(247,208,25)", + }, + }, { types: ["maybe-class-name"], style: { @@ -230,7 +240,7 @@ const Body = ({ { types: ["comment"], style: { - color: "rgb(52,211,153)", + color: "var(--code-fg-subtle)", }, }, ], diff --git a/packages/medusa/src/api/routes/admin/workflows-executions/index.ts b/packages/medusa/src/api/routes/admin/workflows-executions/index.ts index 092568bf04..c539ae5431 100644 --- a/packages/medusa/src/api/routes/admin/workflows-executions/index.ts +++ b/packages/medusa/src/api/routes/admin/workflows-executions/index.ts @@ -81,3 +81,4 @@ export default (app) => { } export * from "./query-config" +export * from "./validators" diff --git a/yarn.lock b/yarn.lock index 3eda1edccb..e1a76c8a27 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8156,7 +8156,7 @@ __metadata: "@types/node": ^20.11.15 "@types/react": 18.2.43 "@types/react-dom": 18.2.17 - "@uiw/react-json-view": 2.0.0-alpha.10 + "@uiw/react-json-view": ^2.0.0-alpha.17 "@vitejs/plugin-react": 4.2.1 autoprefixer: ^10.4.17 cmdk: ^0.2.0 @@ -9047,6 +9047,7 @@ __metadata: jsdom: ^22.1.0 postcss: ^8.4.33 prism-react-renderer: ^2.0.6 + prismjs: ^1.29.0 prop-types: ^15.8.1 react: ^18.2.0 react-currency-input-field: ^3.6.11 @@ -18958,14 +18959,14 @@ __metadata: languageName: node linkType: hard -"@uiw/react-json-view@npm:2.0.0-alpha.10": - version: 2.0.0-alpha.10 - resolution: "@uiw/react-json-view@npm:2.0.0-alpha.10" +"@uiw/react-json-view@npm:^2.0.0-alpha.17": + version: 2.0.0-alpha.17 + resolution: "@uiw/react-json-view@npm:2.0.0-alpha.17" peerDependencies: "@babel/runtime": ">=7.10.0" react: ">=18.0.0" react-dom: ">=18.0.0" - checksum: d1278e92320251b1b61ecacd5c701989bd732ce4155b14a00b674d151c5b0da1b4a043b8696d23675006bf345559e56492960e33a9c0f40bc3c18eed031a136d + checksum: 6c8f10af40db9ae60da4b799fc391a544d2e6eec071d882d64050869b057e26fa6104c00de0406693a341c03a8017cb37c14136d803c9ff60e86c6e0d0187cbc languageName: node linkType: hard @@ -43008,7 +43009,7 @@ __metadata: languageName: node linkType: hard -"prismjs@npm:^1.27.0": +"prismjs@npm:^1.27.0, prismjs@npm:^1.29.0": version: 1.29.0 resolution: "prismjs@npm:1.29.0" checksum: d906c4c4d01b446db549b4f57f72d5d7e6ccaca04ecc670fb85cea4d4b1acc1283e945a9cbc3d81819084a699b382f970e02f9d1378e14af9808d366d9ed7ec6