Compare commits
39 Commits
fix/remove
...
feat/visua
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bde7f89aad | ||
|
|
0332a8add5 | ||
|
|
9067b37141 | ||
|
|
9742de34b4 | ||
|
|
5e04db9c08 | ||
|
|
15a5791f33 | ||
|
|
4044ffbb61 | ||
|
|
85da7f235d | ||
|
|
a2cd308b26 | ||
|
|
2a8fa30789 | ||
|
|
68fed44e42 | ||
|
|
b6681a9c02 | ||
|
|
bbdf4b1278 | ||
|
|
78b94b1a42 | ||
|
|
417e3d2437 | ||
|
|
20ade34240 | ||
|
|
3e789278ea | ||
|
|
4364ab5a6a | ||
|
|
9cc55dc5e1 | ||
|
|
6302476c30 | ||
|
|
9217d94d46 | ||
|
|
363f315ac6 | ||
|
|
56807534d2 | ||
|
|
9a1f8e1674 | ||
|
|
a417842d9a | ||
|
|
6300f608e1 | ||
|
|
8fc036d32b | ||
|
|
aa944e3f32 | ||
|
|
8026b6e4ea | ||
|
|
fe28238297 | ||
|
|
25cfceb933 | ||
|
|
0eadb5c2c5 | ||
|
|
c28ca5e606 | ||
|
|
dbb2db9703 | ||
|
|
9b9fea698e | ||
|
|
33ddfecef2 | ||
|
|
0149a55e3d | ||
|
|
73c703649e | ||
|
|
8689306b9a |
7
packages/plugin-visual-jobs/.gitignore
vendored
Normal file
7
packages/plugin-visual-jobs/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
.env
|
||||
dist
|
||||
demo/uploads
|
||||
build
|
||||
.DS_Store
|
||||
package-lock.json
|
||||
12
packages/plugin-visual-jobs/.prettierignore
Normal file
12
packages/plugin-visual-jobs/.prettierignore
Normal file
@@ -0,0 +1,12 @@
|
||||
.tmp
|
||||
**/.git
|
||||
**/.hg
|
||||
**/.pnp.*
|
||||
**/.svn
|
||||
**/.yarn/**
|
||||
**/build
|
||||
**/dist/**
|
||||
**/node_modules
|
||||
**/temp
|
||||
**/docs/**
|
||||
tsconfig.json
|
||||
24
packages/plugin-visual-jobs/.swcrc
Normal file
24
packages/plugin-visual-jobs/.swcrc
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"sourceMaps": true,
|
||||
"jsc": {
|
||||
"target": "esnext",
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": true,
|
||||
"dts": true
|
||||
},
|
||||
"transform": {
|
||||
"react": {
|
||||
"runtime": "automatic",
|
||||
"pragmaFrag": "React.Fragment",
|
||||
"throwIfNamespace": true,
|
||||
"development": false,
|
||||
"useBuiltins": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"module": {
|
||||
"type": "es6"
|
||||
}
|
||||
}
|
||||
1
packages/plugin-visual-jobs/README.md
Normal file
1
packages/plugin-visual-jobs/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# Payload Visual Jobs Plugin
|
||||
18
packages/plugin-visual-jobs/eslint.config.js
Normal file
18
packages/plugin-visual-jobs/eslint.config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
|
||||
/** @type {Config[]} */
|
||||
export const index = [
|
||||
...rootEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
...rootParserOptions,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
22
packages/plugin-visual-jobs/license.md
Normal file
22
packages/plugin-visual-jobs/license.md
Normal file
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2024 Payload CMS, Inc. <info@payloadcms.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
99
packages/plugin-visual-jobs/package.json
Normal file
99
packages/plugin-visual-jobs/package.json
Normal file
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-visual-jobs",
|
||||
"version": "3.2.2",
|
||||
"description": "Visual Jobs plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
"cms",
|
||||
"plugin",
|
||||
"typescript",
|
||||
"react"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/payloadcms/payload.git",
|
||||
"directory": "packages/plugin-visual-jobs"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "Payload <dev@payloadcms.com> (https://payloadcms.com)",
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Payload",
|
||||
"email": "info@payloadcms.com",
|
||||
"url": "https://payloadcms.com"
|
||||
}
|
||||
],
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./client": {
|
||||
"import": "./src/exports/client.ts",
|
||||
"types": "./src/exports/client.ts",
|
||||
"default": "./src/exports/client.ts"
|
||||
},
|
||||
"./rsc": {
|
||||
"import": "./src/exports/rsc.ts",
|
||||
"types": "./src/exports/rsc.ts",
|
||||
"default": "./src/exports/rsc.ts"
|
||||
}
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
||||
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/translations": "workspace:*",
|
||||
"@payloadcms/ui": "workspace:*",
|
||||
"@xyflow/react": "^12.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@payloadcms/next": "workspace:*",
|
||||
"@types/react": "19.0.1",
|
||||
"@types/react-dom": "19.0.1",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"payload": "workspace:*",
|
||||
"react": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020",
|
||||
"react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020"
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./client": {
|
||||
"import": "./dist/exports/client.js",
|
||||
"types": "./dist/exports/client.d.ts",
|
||||
"default": "./dist/exports/client.js"
|
||||
},
|
||||
"./rsc": {
|
||||
"import": "./dist/exports/rsc.js",
|
||||
"types": "./dist/exports/rsc.d.ts",
|
||||
"default": "./dist/exports/rsc.js"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"registry": "https://registry.npmjs.org/",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"homepage:": "https://payloadcms.com"
|
||||
}
|
||||
1
packages/plugin-visual-jobs/src/exports/client.ts
Normal file
1
packages/plugin-visual-jobs/src/exports/client.ts
Normal file
@@ -0,0 +1 @@
|
||||
'use client'
|
||||
1
packages/plugin-visual-jobs/src/exports/rsc.ts
Normal file
1
packages/plugin-visual-jobs/src/exports/rsc.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { JobsView } from '../ui/JobsView/index.js'
|
||||
42
packages/plugin-visual-jobs/src/index.ts
Normal file
42
packages/plugin-visual-jobs/src/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { Config } from 'payload'
|
||||
|
||||
import type { VisualJobsPluginConfig } from './types.js'
|
||||
|
||||
export const visualJobsPlugin =
|
||||
(pluginConfig: VisualJobsPluginConfig) =>
|
||||
(config: Config): Config => {
|
||||
return {
|
||||
...config,
|
||||
jobs: {
|
||||
...config.jobs,
|
||||
|
||||
jobsCollectionOverrides: ({ defaultJobsCollection }) => {
|
||||
return {
|
||||
...defaultJobsCollection,
|
||||
|
||||
admin: {
|
||||
...(defaultJobsCollection.admin || {}),
|
||||
components: {
|
||||
...(defaultJobsCollection.admin?.components || {}),
|
||||
views: {
|
||||
...(defaultJobsCollection.admin?.components?.views || {}),
|
||||
edit: {
|
||||
jobs: {
|
||||
Component: '@payloadcms/plugin-visual-jobs/rsc#JobsView',
|
||||
path: '/visualize',
|
||||
tab: {
|
||||
href: '/visualize',
|
||||
label: 'Visualize',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
hidden: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
tasks: [...(config.jobs?.tasks || [])],
|
||||
},
|
||||
}
|
||||
}
|
||||
1
packages/plugin-visual-jobs/src/types.ts
Normal file
1
packages/plugin-visual-jobs/src/types.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type VisualJobsPluginConfig = {}
|
||||
@@ -0,0 +1,44 @@
|
||||
'use client'
|
||||
import { TabComponent, TabsProvider } from '@payloadcms/ui'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
export type PanelTab = {
|
||||
Content: React.ReactNode
|
||||
name: string
|
||||
}
|
||||
const baseClass = 'tabs-field'
|
||||
|
||||
type Props = {
|
||||
tabs: PanelTab[]
|
||||
}
|
||||
export const Tabs: React.FC<Props> = (props) => {
|
||||
const { tabs } = props
|
||||
const [activeTabIndex, setActiveTabIndex] = useState<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
setActiveTabIndex(0)
|
||||
}, [tabs])
|
||||
|
||||
return (
|
||||
<TabsProvider>
|
||||
<div className={`${baseClass}__tabs-wrap`}>
|
||||
<div className={`${baseClass}__tabs`}>
|
||||
{tabs.map((tab, tabIndex) => {
|
||||
return (
|
||||
<TabComponent
|
||||
isActive={activeTabIndex === tabIndex}
|
||||
key={tabIndex}
|
||||
parentPath={''}
|
||||
setIsActive={() => {
|
||||
setActiveTabIndex(tabIndex)
|
||||
}}
|
||||
tab={{ name: tab.name, fields: [] }}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__content-wrap`}>{tabs[activeTabIndex]?.Content}</div>
|
||||
</TabsProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
@import '~@payloadcms/ui/scss';
|
||||
|
||||
.nodePanel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: base(1);
|
||||
border: 1px solid var(--theme-elevation-100);
|
||||
background: var(--theme-input-bg);
|
||||
width: 400px;
|
||||
|
||||
.tabs-field__tabs::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.tabs-field__content-wrap {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
max-height: 800px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.task-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
.label {
|
||||
min-width: 100px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
|
||||
&.state {
|
||||
text-transform: capitalize;
|
||||
font-weight: 600;
|
||||
|
||||
&.succeeded {
|
||||
color: #2ecc71;
|
||||
}
|
||||
|
||||
&.failed {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
&.running {
|
||||
color: #3498db;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-container {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
background: var(--theme-error-300);
|
||||
color: var(--theme-elevation-1000);
|
||||
border: 1px solid var(--theme-error-100);
|
||||
border-radius: 6px;
|
||||
max-width: 400px;
|
||||
|
||||
.error-type {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stack-trace {
|
||||
margin-top: 8px;
|
||||
|
||||
.stack-header {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stack-content {
|
||||
background: var(--theme-error-200);
|
||||
border: 1px solid var(--theme-error-400);
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.stack-line {
|
||||
padding: 2px 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
'use client'
|
||||
|
||||
import type { JobLog } from 'payload'
|
||||
|
||||
import { CodeEditorLazy } from '@payloadcms/ui'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
import { Panel, useNodes } from '@xyflow/react'
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
import { type PanelTab, Tabs } from './Tabs.js'
|
||||
|
||||
const ErrorDisplay = ({ error }: { error: any }) => {
|
||||
const formatStack = (stack: string) => {
|
||||
return stack.split('\n').map((line, index) => (
|
||||
<div className="stack-line" key={index}>
|
||||
{line.trim()}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="error-container">
|
||||
<div className="detail-row">
|
||||
<span className="label">Error Type:</span>
|
||||
<span className="value error-type">{error.name}</span>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<span className="label">Message:</span>
|
||||
<span className="value error-message">{error.message}</span>
|
||||
</div>
|
||||
{error.stack && (
|
||||
<div className="stack-trace">
|
||||
<div className="stack-header">Stack Trace:</div>
|
||||
<div className="stack-content">{formatStack(error.stack)}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const NodePanel = () => {
|
||||
const nodes = useNodes()
|
||||
|
||||
const tabs = useMemo(() => {
|
||||
const selectedNodes = nodes.filter((node) => node.selected)
|
||||
if (selectedNodes.length !== 1) {
|
||||
return null
|
||||
}
|
||||
const taskLog = selectedNodes[0].data as JobLog
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleString()
|
||||
}
|
||||
|
||||
const baseTabs: PanelTab[] = [
|
||||
{
|
||||
name: 'Metadata',
|
||||
Content: (
|
||||
<div className="task-details">
|
||||
<div className="detail-row">
|
||||
<span className="label">Task ID:</span>
|
||||
<span className="value">{taskLog.taskID}</span>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<span className="label">Task Slug:</span>
|
||||
<span className="value">{taskLog.taskSlug}</span>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<span className="label">State:</span>
|
||||
<span className={`value state ${taskLog.state}`}>{taskLog.state}</span>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<span className="label">Executed At:</span>
|
||||
<span className="value">{formatDate(taskLog.executedAt)}</span>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<span className="label">Completed At:</span>
|
||||
<span className="value">{formatDate(taskLog.completedAt)}</span>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<span className="label">ID:</span>
|
||||
<span className="value">{taskLog.id}</span>
|
||||
</div>
|
||||
{taskLog.error ? <ErrorDisplay error={taskLog.error} /> : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
if (taskLog.input && Object.keys(taskLog.input).length > 0) {
|
||||
baseTabs.push({
|
||||
name: 'Input',
|
||||
Content: (
|
||||
<CodeEditorLazy
|
||||
key="input"
|
||||
language="json"
|
||||
minHeight={168}
|
||||
readOnly={true}
|
||||
value={JSON.stringify(taskLog.input, null, 2)}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
if (taskLog.output && Object.keys(taskLog.output).length > 0) {
|
||||
baseTabs.push({
|
||||
name: 'Output',
|
||||
Content: (
|
||||
<CodeEditorLazy
|
||||
key="output"
|
||||
language="json"
|
||||
minHeight={168}
|
||||
readOnly={true}
|
||||
value={JSON.stringify(taskLog.output, null, 2)}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return baseTabs
|
||||
}, [nodes])
|
||||
|
||||
if (!tabs) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel className="nodePanel" position="top-right">
|
||||
<Tabs tabs={tabs} />
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
153
packages/plugin-visual-jobs/src/ui/JobsView/index.client.tsx
Normal file
153
packages/plugin-visual-jobs/src/ui/JobsView/index.client.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
'use client'
|
||||
import type { ClientCollectionConfig } from 'payload'
|
||||
|
||||
import {
|
||||
SelectInput,
|
||||
SetDocumentStepNav,
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
useTheme,
|
||||
} from '@payloadcms/ui'
|
||||
import {
|
||||
applyEdgeChanges,
|
||||
applyNodeChanges,
|
||||
Background,
|
||||
Controls,
|
||||
type Edge,
|
||||
type Node,
|
||||
ReactFlow,
|
||||
} from '@xyflow/react'
|
||||
import '@xyflow/react/dist/style.css'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import type { RetrySequences } from './utilities/logsToRetrySequences.js'
|
||||
|
||||
import { NodePanel } from './components/NodePanel/index.js'
|
||||
import { TaskNode } from './nodes/taskNode/index.js'
|
||||
|
||||
const nodeTypes = {
|
||||
taskNode: TaskNode,
|
||||
}
|
||||
|
||||
export const JobsViewClient: React.FC<{ retrySequences: RetrySequences }> = (props) => {
|
||||
const { retrySequences } = props
|
||||
const { theme } = useTheme()
|
||||
const { getEntityConfig } = useConfig()
|
||||
const { id } = useDocumentInfo()
|
||||
const collectionConfig = getEntityConfig({
|
||||
collectionSlug: 'payload-jobs',
|
||||
}) as ClientCollectionConfig
|
||||
|
||||
const [run, setRun] = React.useState<string>(String(retrySequences.length - 1))
|
||||
|
||||
const log = retrySequences[Number(run)]
|
||||
|
||||
const [nodes, setNodes] = useState<Node[]>([])
|
||||
const [edges, setEdges] = useState<Edge[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
setNodes(
|
||||
log.map((entry, index) => {
|
||||
return {
|
||||
id: entry.id,
|
||||
type: 'taskNode',
|
||||
data: entry,
|
||||
position: { x: 0, y: index * 100 },
|
||||
} as Node
|
||||
}),
|
||||
)
|
||||
|
||||
setEdges(
|
||||
log
|
||||
.map((entry, index) => {
|
||||
if (index === 0) {
|
||||
return
|
||||
}
|
||||
return {
|
||||
id: `e${log[index - 1].id}-${entry.id}`,
|
||||
markerEnd: {
|
||||
type: 'arrowclosed',
|
||||
color: theme === 'light' ? 'black' : 'white',
|
||||
height: 25,
|
||||
strokeWidth: 2,
|
||||
width: 25,
|
||||
},
|
||||
// Set completion time between entry.completedAt and log[index - 1].completedAt
|
||||
label: `${String(
|
||||
new Date(entry.completedAt).getTime() -
|
||||
new Date(log[index - 1].completedAt).getTime(),
|
||||
)} ms`,
|
||||
source: log[index - 1].id,
|
||||
target: entry.id,
|
||||
} as Edge
|
||||
})
|
||||
.filter(Boolean),
|
||||
)
|
||||
}, [log, retrySequences, theme])
|
||||
|
||||
const onNodesChange = useCallback(
|
||||
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
||||
[],
|
||||
)
|
||||
const onEdgesChange = useCallback(
|
||||
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
|
||||
[],
|
||||
)
|
||||
return (
|
||||
<div>
|
||||
<SetDocumentStepNav
|
||||
collectionSlug={'payload-jobs'}
|
||||
globalLabel={undefined}
|
||||
globalSlug={undefined}
|
||||
id={id}
|
||||
pluralLabel={collectionConfig ? collectionConfig?.labels?.plural : undefined}
|
||||
useAsTitle={collectionConfig ? collectionConfig?.admin?.useAsTitle : undefined}
|
||||
view="Visualize"
|
||||
/>
|
||||
<SelectInput
|
||||
label={'Job Run'}
|
||||
name="run"
|
||||
onChange={(option) => {
|
||||
if (Array.isArray(option)) {
|
||||
return
|
||||
}
|
||||
const value = option.value
|
||||
|
||||
setRun(value as string)
|
||||
}}
|
||||
options={retrySequences
|
||||
.map((sequence, index) => {
|
||||
if (index === retrySequences.length - 1) {
|
||||
return {
|
||||
label: 'Latest',
|
||||
value: String(index),
|
||||
}
|
||||
}
|
||||
return {
|
||||
label: `Run ${index + 1}`,
|
||||
value: String(index),
|
||||
}
|
||||
})
|
||||
.reverse()}
|
||||
path="run"
|
||||
value={run}
|
||||
/>
|
||||
<div style={{ height: '1000px', width: '100%' }}>
|
||||
<br />
|
||||
<ReactFlow
|
||||
colorMode={theme}
|
||||
edges={edges}
|
||||
nodes={nodes}
|
||||
nodesConnectable={false}
|
||||
nodeTypes={nodeTypes}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodesChange={onNodesChange}
|
||||
>
|
||||
<Background />
|
||||
<Controls />
|
||||
<NodePanel />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
28
packages/plugin-visual-jobs/src/ui/JobsView/index.tsx
Normal file
28
packages/plugin-visual-jobs/src/ui/JobsView/index.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { ServerSideEditViewProps } from 'payload'
|
||||
|
||||
import { Gutter } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
import { JobsViewClient } from './index.client.js'
|
||||
import { logsToRetrySequences } from './utilities/logsToRetrySequences.js'
|
||||
|
||||
export const JobsView: React.FC<ServerSideEditViewProps> = (props) => {
|
||||
const { doc } = props
|
||||
|
||||
const retrySequences = logsToRetrySequences(doc.log)
|
||||
|
||||
if (!retrySequences?.length) {
|
||||
return (
|
||||
<Gutter>
|
||||
<p>No logs to display</p>
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Gutter>
|
||||
<br />
|
||||
<JobsViewClient retrySequences={retrySequences} />
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
@import '~@payloadcms/ui/scss';
|
||||
|
||||
.taskNode {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1em;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--theme-elevation-100);
|
||||
background: var(--theme-input-bg);
|
||||
|
||||
&__label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__taskID {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
&__id {
|
||||
color: var(--theme-elevation-600);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
.selected .taskNode {
|
||||
outline: 1px solid var(--theme-elevation-800);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
'use client'
|
||||
import type { JobLog } from 'payload'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
import { ErrorIcon, SuccessIcon } from '@payloadcms/ui'
|
||||
import { Handle, Position } from '@xyflow/react'
|
||||
import React, { memo } from 'react'
|
||||
|
||||
export const TaskNode: React.FC<{
|
||||
data: JobLog
|
||||
isConnectable: boolean
|
||||
}> = memo(({ data, isConnectable }) => {
|
||||
return (
|
||||
<>
|
||||
<Handle
|
||||
isConnectable={isConnectable}
|
||||
onConnect={(params) => console.log('handle onConnect', params)}
|
||||
position={Position.Top}
|
||||
type="target"
|
||||
/>
|
||||
<div
|
||||
className={[
|
||||
'taskNode',
|
||||
data.state === 'succeeded' ? 'taskNode--succeeded' : 'taskNode--failed',
|
||||
].join(' ')}
|
||||
>
|
||||
{data.state === 'succeeded' ? <SuccessIcon /> : <ErrorIcon />}
|
||||
<div className="taskNode__label">
|
||||
<strong className="taskNode__taskID">{data.taskID}</strong>
|
||||
<span className="taskNode__id">{data.id}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Handle id="a" isConnectable={isConnectable} position={Position.Bottom} type="source" />
|
||||
</>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { JobLog } from 'payload'
|
||||
|
||||
export type RetrySequences = JobLog[][]
|
||||
|
||||
export function logsToRetrySequences(logs: JobLog[]): RetrySequences {
|
||||
const sequences: RetrySequences = []
|
||||
|
||||
// Group logs by taskID
|
||||
const groupedByTaskID = new Map<string, JobLog[]>()
|
||||
|
||||
logs.forEach((log) => {
|
||||
if (!groupedByTaskID.has(log.taskID)) {
|
||||
groupedByTaskID.set(log.taskID, [])
|
||||
}
|
||||
groupedByTaskID.get(log.taskID).push(log)
|
||||
})
|
||||
|
||||
let curSequence: JobLog[] = []
|
||||
let i = 0
|
||||
for (const log of logs) {
|
||||
i++
|
||||
if (curSequence.length === 0 && sequences.length > 0) {
|
||||
const lastSequence = sequences[sequences.length - 1]
|
||||
let i = 0
|
||||
for (const entry of lastSequence) {
|
||||
i++
|
||||
if (i === lastSequence.length) {
|
||||
break
|
||||
}
|
||||
curSequence.push(entry)
|
||||
}
|
||||
}
|
||||
|
||||
curSequence.push(log)
|
||||
if (log.state === 'failed') {
|
||||
sequences.push([...curSequence])
|
||||
curSequence = []
|
||||
}
|
||||
|
||||
if (i === logs.length) {
|
||||
sequences.push([...curSequence])
|
||||
}
|
||||
}
|
||||
|
||||
return sequences
|
||||
}
|
||||
25
packages/plugin-visual-jobs/tsconfig.json
Normal file
25
packages/plugin-visual-jobs/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true, // Make sure typescript knows that this module depends on their references
|
||||
"noEmit": false /* Do not emit outputs. */,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
|
||||
"rootDir": "./src", /* Specify the root folder within your source files. */
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"exclude": [
|
||||
"dist",
|
||||
"build",
|
||||
"tests",
|
||||
"test",
|
||||
"node_modules",
|
||||
"eslint.config.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.spec.tsx"
|
||||
],
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"],
|
||||
"references": [{ "path": "../payload" }, { "path": "../ui" }, { "path": "../next" }]
|
||||
}
|
||||
217
pnpm-lock.yaml
generated
217
pnpm-lock.yaml
generated
@@ -1182,6 +1182,40 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../payload
|
||||
|
||||
packages/plugin-visual-jobs:
|
||||
dependencies:
|
||||
'@payloadcms/translations':
|
||||
specifier: workspace:*
|
||||
version: link:../translations
|
||||
'@payloadcms/ui':
|
||||
specifier: workspace:*
|
||||
version: link:../ui
|
||||
'@xyflow/react':
|
||||
specifier: ^12.3.6
|
||||
version: 12.3.6(@types/react@19.0.1)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
react:
|
||||
specifier: 19.0.0
|
||||
version: 19.0.0
|
||||
react-dom:
|
||||
specifier: 19.0.0
|
||||
version: 19.0.0(react@19.0.0)
|
||||
devDependencies:
|
||||
'@payloadcms/eslint-config':
|
||||
specifier: workspace:*
|
||||
version: link:../eslint-config
|
||||
'@payloadcms/next':
|
||||
specifier: workspace:*
|
||||
version: link:../next
|
||||
'@types/react':
|
||||
specifier: 19.0.1
|
||||
version: 19.0.1
|
||||
'@types/react-dom':
|
||||
specifier: 19.0.1
|
||||
version: 19.0.1
|
||||
payload:
|
||||
specifier: workspace:*
|
||||
version: link:../payload
|
||||
|
||||
packages/richtext-lexical:
|
||||
dependencies:
|
||||
'@faceless-ui/modal':
|
||||
@@ -1679,6 +1713,9 @@ importers:
|
||||
'@payloadcms/plugin-stripe':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/plugin-stripe
|
||||
'@payloadcms/plugin-visual-jobs':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/plugin-visual-jobs
|
||||
'@payloadcms/richtext-lexical':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/richtext-lexical
|
||||
@@ -5003,6 +5040,24 @@ packages:
|
||||
'@types/connect@3.4.36':
|
||||
resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==}
|
||||
|
||||
'@types/d3-color@3.1.3':
|
||||
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
|
||||
|
||||
'@types/d3-drag@3.0.7':
|
||||
resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
|
||||
|
||||
'@types/d3-interpolate@3.0.4':
|
||||
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
|
||||
|
||||
'@types/d3-selection@3.0.11':
|
||||
resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
|
||||
|
||||
'@types/d3-transition@3.0.9':
|
||||
resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
|
||||
|
||||
'@types/d3-zoom@3.0.8':
|
||||
resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||
|
||||
@@ -5413,6 +5468,15 @@ packages:
|
||||
'@xtuc/long@4.2.2':
|
||||
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
|
||||
|
||||
'@xyflow/react@12.3.6':
|
||||
resolution: {integrity: sha512-9GS+cz8hDZahpvTrVCmySAEgKUL8oN4b2q1DluHrKtkqhAMWfH2s7kblhbM4Y4Y4SUnH2lt4drXKZ/4/Lot/2Q==}
|
||||
peerDependencies:
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0
|
||||
|
||||
'@xyflow/system@0.0.47':
|
||||
resolution: {integrity: sha512-aUXJPIvsCFxGX70ccRG8LPsR+A8ExYXfh/noYNpqn8udKerrLdSHxMG2VsvUrQ1PGex10fOpbJwFU4A+I/Xv8w==}
|
||||
|
||||
abab@2.0.6:
|
||||
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
|
||||
deprecated: Use your platform's native atob() and btoa() methods instead
|
||||
@@ -5878,6 +5942,9 @@ packages:
|
||||
cjs-module-lexer@1.4.1:
|
||||
resolution: {integrity: sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==}
|
||||
|
||||
classcat@5.0.5:
|
||||
resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==}
|
||||
|
||||
classnames@2.5.1:
|
||||
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
|
||||
|
||||
@@ -6063,6 +6130,44 @@ packages:
|
||||
csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
d3-color@3.1.0:
|
||||
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-dispatch@3.0.1:
|
||||
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-drag@3.0.0:
|
||||
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-ease@3.0.1:
|
||||
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-interpolate@3.0.1:
|
||||
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-selection@3.0.0:
|
||||
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-timer@3.0.1:
|
||||
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-transition@3.0.1:
|
||||
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
|
||||
engines: {node: '>=12'}
|
||||
peerDependencies:
|
||||
d3-selection: 2 - 3
|
||||
|
||||
d3-zoom@3.0.0:
|
||||
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
damerau-levenshtein@1.0.8:
|
||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||
|
||||
@@ -10027,6 +10132,11 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
use-sync-external-store@1.2.2:
|
||||
resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==}
|
||||
peerDependencies:
|
||||
react: 19.0.0
|
||||
|
||||
utf-8-validate@6.0.5:
|
||||
resolution: {integrity: sha512-EYZR+OpIXp9Y1eG1iueg8KRsY8TuT8VNgnanZ0uA3STqhHQTLwbl+WX76/9X5OY12yQubymBpaBSmMPkSTQcKA==}
|
||||
engines: {node: '>=6.14.2'}
|
||||
@@ -10282,6 +10392,21 @@ packages:
|
||||
zod@3.23.8:
|
||||
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
|
||||
|
||||
zustand@4.5.5:
|
||||
resolution: {integrity: sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q==}
|
||||
engines: {node: '>=12.7.0'}
|
||||
peerDependencies:
|
||||
'@types/react': '>=16.8'
|
||||
immer: '>=9.0.6'
|
||||
react: 19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
immer:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
|
||||
zwitch@2.0.4:
|
||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||
|
||||
@@ -14383,6 +14508,27 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 22.5.4
|
||||
|
||||
'@types/d3-color@3.1.3': {}
|
||||
|
||||
'@types/d3-drag@3.0.7':
|
||||
dependencies:
|
||||
'@types/d3-selection': 3.0.11
|
||||
|
||||
'@types/d3-interpolate@3.0.4':
|
||||
dependencies:
|
||||
'@types/d3-color': 3.1.3
|
||||
|
||||
'@types/d3-selection@3.0.11': {}
|
||||
|
||||
'@types/d3-transition@3.0.9':
|
||||
dependencies:
|
||||
'@types/d3-selection': 3.0.11
|
||||
|
||||
'@types/d3-zoom@3.0.8':
|
||||
dependencies:
|
||||
'@types/d3-interpolate': 3.0.4
|
||||
'@types/d3-selection': 3.0.11
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
dependencies:
|
||||
'@types/ms': 0.7.34
|
||||
@@ -14961,6 +15107,27 @@ snapshots:
|
||||
|
||||
'@xtuc/long@4.2.2': {}
|
||||
|
||||
'@xyflow/react@12.3.6(@types/react@19.0.1)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
'@xyflow/system': 0.0.47
|
||||
classcat: 5.0.5
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
zustand: 4.5.5(@types/react@19.0.1)(immer@9.0.21)(react@19.0.0)
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
- immer
|
||||
|
||||
'@xyflow/system@0.0.47':
|
||||
dependencies:
|
||||
'@types/d3-drag': 3.0.7
|
||||
'@types/d3-selection': 3.0.11
|
||||
'@types/d3-transition': 3.0.9
|
||||
'@types/d3-zoom': 3.0.8
|
||||
d3-drag: 3.0.0
|
||||
d3-selection: 3.0.0
|
||||
d3-zoom: 3.0.0
|
||||
|
||||
abab@2.0.6: {}
|
||||
|
||||
abbrev@2.0.0: {}
|
||||
@@ -15492,6 +15659,8 @@ snapshots:
|
||||
|
||||
cjs-module-lexer@1.4.1: {}
|
||||
|
||||
classcat@5.0.5: {}
|
||||
|
||||
classnames@2.5.1: {}
|
||||
|
||||
clean-stack@2.2.0: {}
|
||||
@@ -15673,6 +15842,42 @@ snapshots:
|
||||
|
||||
csstype@3.1.3: {}
|
||||
|
||||
d3-color@3.1.0: {}
|
||||
|
||||
d3-dispatch@3.0.1: {}
|
||||
|
||||
d3-drag@3.0.0:
|
||||
dependencies:
|
||||
d3-dispatch: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
|
||||
d3-ease@3.0.1: {}
|
||||
|
||||
d3-interpolate@3.0.1:
|
||||
dependencies:
|
||||
d3-color: 3.1.0
|
||||
|
||||
d3-selection@3.0.0: {}
|
||||
|
||||
d3-timer@3.0.1: {}
|
||||
|
||||
d3-transition@3.0.1(d3-selection@3.0.0):
|
||||
dependencies:
|
||||
d3-color: 3.1.0
|
||||
d3-dispatch: 3.0.1
|
||||
d3-ease: 3.0.1
|
||||
d3-interpolate: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
d3-timer: 3.0.1
|
||||
|
||||
d3-zoom@3.0.0:
|
||||
dependencies:
|
||||
d3-dispatch: 3.0.1
|
||||
d3-drag: 3.0.0
|
||||
d3-interpolate: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
d3-transition: 3.0.1(d3-selection@3.0.0)
|
||||
|
||||
damerau-levenshtein@1.0.8: {}
|
||||
|
||||
data-uri-to-buffer@4.0.1: {}
|
||||
@@ -20216,6 +20421,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.1
|
||||
|
||||
use-sync-external-store@1.2.2(react@19.0.0):
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
|
||||
utf-8-validate@6.0.5:
|
||||
dependencies:
|
||||
node-gyp-build: 4.8.2
|
||||
@@ -20476,4 +20685,12 @@ snapshots:
|
||||
|
||||
zod@3.23.8: {}
|
||||
|
||||
zustand@4.5.5(@types/react@19.0.1)(immer@9.0.21)(react@19.0.0):
|
||||
dependencies:
|
||||
use-sync-external-store: 1.2.2(react@19.0.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.1
|
||||
immer: 9.0.21
|
||||
react: 19.0.0
|
||||
|
||||
zwitch@2.0.4: {}
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"@payloadcms/plugin-search": "workspace:*",
|
||||
"@payloadcms/plugin-sentry": "workspace:*",
|
||||
"@payloadcms/plugin-seo": "workspace:*",
|
||||
"@payloadcms/plugin-visual-jobs": "workspace:*",
|
||||
"@payloadcms/plugin-stripe": "workspace:*",
|
||||
"@payloadcms/richtext-lexical": "workspace:*",
|
||||
"@payloadcms/richtext-slate": "workspace:*",
|
||||
|
||||
35
test/queues/QueueDemoJobs.tsx
Normal file
35
test/queues/QueueDemoJobs.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Button } from '@payloadcms/ui'
|
||||
import { type CustomComponent, getPayload, type PayloadServerReactComponent } from 'payload'
|
||||
import React from 'react'
|
||||
|
||||
import config from './config.js'
|
||||
|
||||
async function handleClick() {
|
||||
'use server'
|
||||
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
await payload.jobs.queue({
|
||||
input: {},
|
||||
queue: 'default',
|
||||
workflow: 'randomRetries',
|
||||
})
|
||||
|
||||
let hasJobsRemaining = true
|
||||
|
||||
while (hasJobsRemaining) {
|
||||
const response = await payload.jobs.run()
|
||||
|
||||
if (response.noJobsRemaining) {
|
||||
hasJobsRemaining = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const QueueDemoJobs: PayloadServerReactComponent<CustomComponent> = ({ payload }) => {
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={handleClick}>Queue and run demo jobs</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { TaskConfig, WorkflowConfig } from 'payload'
|
||||
|
||||
import { visualJobsPlugin } from '@payloadcms/plugin-visual-jobs'
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
@@ -13,6 +14,7 @@ const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
plugins: [visualJobsPlugin({})],
|
||||
collections: [
|
||||
{
|
||||
slug: 'posts',
|
||||
@@ -76,6 +78,9 @@ export default buildConfigWithDefaults({
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
components: {
|
||||
beforeDashboard: ['./QueueDemoJobs#QueueDemoJobs'],
|
||||
},
|
||||
},
|
||||
jobs: {
|
||||
jobsCollectionOverrides: ({ defaultJobsCollection }) => {
|
||||
@@ -87,6 +92,7 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
}
|
||||
},
|
||||
deleteJobOnComplete: false,
|
||||
tasks: [
|
||||
{
|
||||
retries: 2,
|
||||
@@ -846,6 +852,106 @@ export default buildConfigWithDefaults({
|
||||
})
|
||||
},
|
||||
} as WorkflowConfig<'retriesBackoffTest'>,
|
||||
{
|
||||
slug: 'randomRetries',
|
||||
retries: 200,
|
||||
handler: async ({ job, inlineTask, req }) => {
|
||||
const { customerData } = await inlineTask('Fetch Customer Data', {
|
||||
task: ({ req }) => {
|
||||
// 20% chance to fail
|
||||
if (Math.random() < 0.2) {
|
||||
throw new Error('Failed on purpose')
|
||||
}
|
||||
return {
|
||||
output: {
|
||||
customerData: 'test',
|
||||
},
|
||||
}
|
||||
},
|
||||
retries: {
|
||||
attempts: 40,
|
||||
},
|
||||
})
|
||||
|
||||
await inlineTask('Analyze Segments', {
|
||||
task: ({ req }) => {
|
||||
// 20% chance to fail
|
||||
if (Math.random() < 0.4) {
|
||||
throw new Error('Failed on purpose')
|
||||
}
|
||||
return {
|
||||
output: {
|
||||
segments: ['test1', 'test2'],
|
||||
},
|
||||
}
|
||||
},
|
||||
retries: {
|
||||
attempts: 40,
|
||||
},
|
||||
input: {
|
||||
customerData,
|
||||
},
|
||||
})
|
||||
|
||||
await inlineTask('Generate Copy', {
|
||||
task: ({ req }) => {
|
||||
// 20% chance to fail
|
||||
if (Math.random() < 0.6) {
|
||||
throw new Error('Failed on purpose')
|
||||
}
|
||||
return {
|
||||
output: {},
|
||||
}
|
||||
},
|
||||
retries: {
|
||||
attempts: 40,
|
||||
},
|
||||
})
|
||||
await inlineTask('Create Images', {
|
||||
task: ({ req }) => {
|
||||
// 20% chance to fail
|
||||
if (Math.random() < 0.8) {
|
||||
throw new Error('Failed on purpose')
|
||||
}
|
||||
return {
|
||||
output: {},
|
||||
}
|
||||
},
|
||||
retries: {
|
||||
attempts: 40,
|
||||
},
|
||||
})
|
||||
await inlineTask('Schedule Posts', {
|
||||
task: ({ req }) => {
|
||||
// 20% chance to fail
|
||||
if (Math.random() < 0.8) {
|
||||
throw new Error('Failed on purpose')
|
||||
}
|
||||
return {
|
||||
output: {},
|
||||
}
|
||||
},
|
||||
retries: {
|
||||
attempts: 40,
|
||||
},
|
||||
})
|
||||
|
||||
await inlineTask('Track Metrics', {
|
||||
task: ({ req }) => {
|
||||
// 20% chance to fail
|
||||
if (Math.random() < 0.9) {
|
||||
throw new Error('Failed on purpose')
|
||||
}
|
||||
return {
|
||||
output: {},
|
||||
}
|
||||
},
|
||||
retries: {
|
||||
attempts: 80,
|
||||
},
|
||||
})
|
||||
},
|
||||
} as WorkflowConfig<'randomRetries'>,
|
||||
],
|
||||
},
|
||||
editor: lexicalEditor(),
|
||||
|
||||
@@ -66,6 +66,7 @@ export interface Config {
|
||||
inlineTaskTest: WorkflowInlineTaskTest;
|
||||
externalWorkflow: WorkflowExternalWorkflow;
|
||||
retriesBackoffTest: WorkflowRetriesBackoffTest;
|
||||
randomRetries: WorkflowRandomRetries;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -237,6 +238,7 @@ export interface PayloadJob {
|
||||
| 'inlineTaskTest'
|
||||
| 'externalWorkflow'
|
||||
| 'retriesBackoffTest'
|
||||
| 'randomRetries'
|
||||
)
|
||||
| null;
|
||||
taskSlug?:
|
||||
@@ -629,6 +631,13 @@ export interface WorkflowRetriesBackoffTest {
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "WorkflowRandomRetries".
|
||||
*/
|
||||
export interface WorkflowRandomRetries {
|
||||
input?: unknown;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
|
||||
@@ -28,6 +28,7 @@ export const tgzToPkgNameMap = {
|
||||
'@payloadcms/plugin-search': 'payloadcms-plugin-search-*',
|
||||
'@payloadcms/plugin-sentry': 'payloadcms-plugin-sentry-*',
|
||||
'@payloadcms/plugin-seo': 'payloadcms-plugin-seo-*',
|
||||
'@payloadcms/plugin-visual-jobs': 'payloadcms-plugin-visual-jobs-*',
|
||||
'@payloadcms/plugin-stripe': 'payloadcms-plugin-stripe-*',
|
||||
'@payloadcms/richtext-lexical': 'payloadcms-richtext-lexical-*',
|
||||
'@payloadcms/richtext-slate': 'payloadcms-richtext-slate-*',
|
||||
|
||||
@@ -78,6 +78,12 @@
|
||||
"@payloadcms/plugin-seo/client": [
|
||||
"./packages/plugin-seo/src/exports/client.ts"
|
||||
],
|
||||
"@payloadcms/plugin-visual-jobs/client": [
|
||||
"./packages/plugin-visual-jobs/src/exports/client.ts"
|
||||
],
|
||||
"@payloadcms/plugin-visual-jobs/rsc": [
|
||||
"./packages/plugin-visual-jobs/src/exports/rsc.ts"
|
||||
],
|
||||
"@payloadcms/plugin-sentry/client": [
|
||||
"./packages/plugin-sentry/src/exports/client.ts"
|
||||
],
|
||||
@@ -154,6 +160,9 @@
|
||||
{
|
||||
"path": "./packages/plugin-seo"
|
||||
},
|
||||
{
|
||||
"path": "./packages/plugin-visual-jobs"
|
||||
},
|
||||
{
|
||||
"path": "./packages/plugin-stripe"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user