Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions assets/js/src/core/app/config/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ImageTabManager } from '@Pimcore/modules/asset/editor/types/image/tab-m
import { TextTabManager } from '@Pimcore/modules/asset/editor/types/text/tab-manager/text-tab-manager'
import { UnknownTabManager } from '@Pimcore/modules/asset/editor/types/unknown/tab-manager/unknown-tab-manager'
import { JobComponentRegistry } from '@Pimcore/modules/execution-engine/services/job-component-registry'
import { JobRehydrationRegistry } from '@Pimcore/modules/execution-engine/services/job-rehydration-registry'
import { ExecutionEngine } from '@Pimcore/modules/execution-engine/services/execution-engine'
import { VideoTabManager } from '@Pimcore/modules/asset/editor/types/video/tab-manager/video-tab-manager'
import { ThumbnailService } from '@Pimcore/modules/asset/services/thumbnail-service'
Expand Down Expand Up @@ -711,6 +712,7 @@ container.bind(serviceIds['DynamicTypes/Grid/Transformers/PHPCode']).to(DynamicT

// Execution engine
container.bind(serviceIds['ExecutionEngine/JobComponentRegistry']).to(JobComponentRegistry).inSingletonScope()
container.bind(serviceIds['ExecutionEngine/JobRehydrationRegistry']).to(JobRehydrationRegistry).inSingletonScope()
container.bind(serviceIds.executionEngine).to(ExecutionEngine).inSingletonScope()

// Background processor
Expand Down
1 change: 1 addition & 0 deletions assets/js/src/core/app/config/services/service-ids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ export const serviceIds = {

// Execution engine
'ExecutionEngine/JobComponentRegistry': 'ExecutionEngine/JobComponentRegistry',
'ExecutionEngine/JobRehydrationRegistry': 'ExecutionEngine/JobRehydrationRegistry',

// Execution Engine
executionEngine: 'ExecutionEngine',
Expand Down
13 changes: 13 additions & 0 deletions assets/js/src/core/modules/bulk-import/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import { type MainNavRegistry } from '@Pimcore/modules/app/base-layout/main-nav/
import { useBulkImportContext } from '@Pimcore/modules/bulk-import/components/bulk-import-modal/context/bulk-import-context'
import { UserPermission } from '@sdk/modules/auth'
import { NavPermission } from '@sdk/modules/perspectives'
import { type JobRehydrationRegistry } from '@Pimcore/modules/execution-engine/services/job-rehydration-registry'
import { MessageBusJobHandler } from '@Pimcore/modules/execution-engine/message-handlers/message-bus-job/message-bus-job-handler'
import { t } from 'i18next'

moduleSystem.registerModule({
onInit: () => {
Expand All @@ -41,5 +44,15 @@ moduleSystem.registerModule({
}
}
})

const rehydrationRegistry = container.get<JobRehydrationRegistry>(serviceIds['ExecutionEngine/JobRehydrationRegistry'])

rehydrationRegistry.register(
['studio_ee_job_bulk_import_class_definitions'],
(parent) => new MessageBusJobHandler({
jobRunId: parent.id,
title: t('jobs.bulk-import-job.title')
})
)
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* This source file is available under the terms of the
* Pimcore Open Core License (POCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
* @license Pimcore Open Core License (POCL)
*/

import { container } from '@Pimcore/app/depency-injection'
import { serviceIds } from '@Pimcore/app/config/services/service-ids'
import { type Loader } from '@Pimcore/modules/app/app-loader/services/app-loader-registry'
import { type ExecutionEngine } from '../services/execution-engine'
import { api } from '../execution-engine-api-slice-enhanced'
import { type JobRun } from '../execution-engine-api-slice.gen'
import { store } from '@Pimcore/app/store'

export const rehydrateJobsLoader: Loader = {
name: 'rehydrate-running-jobs',

async onLoad (): Promise<void> {
const { data } = await store.dispatch(
api.endpoints.executionEngineListJobs.initiate(
{ body: { filters: { page: 1, pageSize: 100 } } },
{ forceRefetch: true }
)
)
const activeStates = ['running', 'not_started']

Check warning on line 29 in assets/js/src/core/modules/execution-engine/app-loader/rehydrate-jobs-loader.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

`activeStates` should be a `Set`, and use `activeStates.has()` to check existence or non-existence.

See more on https://sonarcloud.io/project/issues?id=pimcore_studio-ui-bundle&issues=AZ3edIGIDB_MdPLcYnoB&open=AZ3edIGIDB_MdPLcYnoB&pullRequest=3468
const items: JobRun[] = (data?.items ?? []).filter(j => activeStates.includes(j.state))

const executionEngine = container.get<ExecutionEngine>(serviceIds.executionEngine)
executionEngine.rehydrateRunningJobs(items)
}
}
11 changes: 10 additions & 1 deletion assets/js/src/core/modules/execution-engine/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,23 @@
import { container } from '@Pimcore/app/depency-injection'
import { moduleSystem, type AbstractModule } from '@Pimcore/app/module-system/module-system'
import { type JobComponentRegistry } from './services/job-component-registry'
import { type JobRehydrationRegistry } from './services/job-rehydration-registry'
import { serviceIds } from '@Pimcore/app/config/services/service-ids'
import { MessageBusJobNotification as MessageBusJobContainer } from './message-handlers/message-bus-job/message-bus-job-notification'
import { registerAllJobRehydrations } from './job-rehydrations'
import { rehydrateJobsLoader } from './app-loader/rehydrate-jobs-loader'
import { type AppLoaderRegistry } from '@Pimcore/modules/app/app-loader/services/app-loader-registry'

export const executionEngineModule: AbstractModule = {
onInit () {
const jobComponentRegistry = container.get<JobComponentRegistry>(serviceIds['ExecutionEngine/JobComponentRegistry'])

jobComponentRegistry.registerComponent('default-message-bus', MessageBusJobContainer)

const rehydrationRegistry = container.get<JobRehydrationRegistry>(serviceIds['ExecutionEngine/JobRehydrationRegistry'])
registerAllJobRehydrations(rehydrationRegistry)

const appLoaderRegistry = container.get<AppLoaderRegistry>(serviceIds['AppLoader/Registry'])
appLoaderRegistry.registerLoader(rehydrateJobsLoader)
}
}

Expand Down
200 changes: 200 additions & 0 deletions assets/js/src/core/modules/execution-engine/job-rehydrations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/**
* This source file is available under the terms of the
* Pimcore Open Core License (POCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
* @license Pimcore Open Core License (POCL)
*/

import { t } from 'i18next'
import { store } from '@Pimcore/app/store'
import { MessageBusJobHandler } from './message-handlers/message-bus-job/message-bus-job-handler'
import { ChildJobStepTracker } from './message-handlers/message-bus-job/step-tracker/child-job-step-tracker'
import { DefaultStepTracker } from './message-handlers/message-bus-job/step-tracker/default-step-tracker'
import { ProgressFieldCalculator } from './message-handlers/message-bus-job/progress-calculator/progress-field-calculator'
import { StepCompletionCalculator } from './message-handlers/message-bus-job/progress-calculator/step-completion-calculator'
import { type JobRun } from './execution-engine-api-slice.gen'
import { type JobRehydrationRegistry } from './services/job-rehydration-registry'
import { invalidatingTags } from '@Pimcore/app/api/pimcore/tags'
import { api } from '@Pimcore/app/api/pimcore'
import { refreshTreeByElementType } from '@Pimcore/components/element-tree/element-tree-slice'
import { getPrefix } from '@Pimcore/app/api/pimcore/route'
import { type JobButtonCustomizationContext } from './message-handlers/message-bus-job/message-bus-job-notification'

export function registerAllJobRehydrations (registry: JobRehydrationRegistry): void {
// Single-element delete
registry.register(
['studio_ee_job_delete_assets', 'studio_ee_job_delete_data_objects', 'studio_ee_job_delete_documents'],
(parent: JobRun) => new MessageBusJobHandler({
jobRunId: parent.id,
title: t('jobs.delete-job.title'),
progressCalculator: new StepCompletionCalculator()
})
)

// Batch delete
registry.register(
['studio_ee_job_batch_delete_assets', 'studio_ee_job_batch_delete_data_objects'],
(parent: JobRun) => new MessageBusJobHandler({
jobRunId: parent.id,
title: t('jobs.batch-delete-job.title'),
progressCalculator: new StepCompletionCalculator()
})
)

// Clone (may have child job)
registry.register(
['studio_ee_job_clone_assets', 'studio_ee_job_clone_data_objects', 'studio_ee_job_clone_documents'],
(parent: JobRun, child?: JobRun) => {
const isChild = child !== undefined
return new MessageBusJobHandler({
jobRunId: child?.id ?? parent.id,
ancestorJobRunIds: isChild ? [parent.id] : undefined,
title: t('jobs.clone-job.title'),
stepTracker: new ChildJobStepTracker({ startAtStep: isChild ? 2 : 1 }),
progressCalculator: new StepCompletionCalculator()
})
}
)

// Batch edit (patch elements / rewrite references)
registry.register(
['studio_ee_job_patch_elements', 'studio_ee_job_rewrite_element_references'],
(parent: JobRun) => new MessageBusJobHandler({
jobRunId: parent.id,
title: t('jobs.batch-edit-job.title')
})
)

// ZIP upload (two-step: extract → create assets)
registry.register(
['studio_ee_job_upload_zip_file'],
(parent: JobRun, child?: JobRun) => {
const isChild = child !== undefined
return new MessageBusJobHandler({
jobRunId: child?.id ?? parent.id,
ancestorJobRunIds: isChild ? [parent.id] : undefined,
title: isChild ? t('jobs.zip-upload-job.step2.title') : t('jobs.zip-upload-job.step1.title'),
stepTracker: new ChildJobStepTracker({ totalSteps: 2, startAtStep: isChild ? 2 : 1 }),
progressCalculator: new ProgressFieldCalculator()
})
}
)

// Download: selected-row CSV/XLSX (no child job)
registry.register(
['studio_ee_job_create_csv', 'studio_ee_job_create_xlsx'],
(parent: JobRun) => {
const downloadUrl = parent.jobName === 'studio_ee_job_create_csv'
? `${getPrefix()}/export/download/csv/{jobRunId}`
: `${getPrefix()}/export/download/xlsx/{jobRunId}`

return new MessageBusJobHandler({
jobRunId: parent.id,
title: t('jobs.download-job.title'),
stepTracker: new DefaultStepTracker(),
progressCalculator: new ProgressFieldCalculator(),
onCustomizeButtons: buildDownloadButton(downloadUrl)
})
}
)

// Download: ZIP (no child job)
registry.register(
['studio_ee_job_create_download_zip'],
(parent: JobRun) => new MessageBusJobHandler({
jobRunId: parent.id,
title: t('jobs.download-job.title'),
stepTracker: new DefaultStepTracker(),
progressCalculator: new ProgressFieldCalculator(),
onCustomizeButtons: buildDownloadButton(`${getPrefix()}/assets/download/zip/{jobRunId}`)
})
)

// Folder export: collect step transitions to a CSV/XLSX child
registry.register(
['studio_ee_job_collect_folder_export_elements'],
(parent: JobRun, child?: JobRun) => {
const isChild = child !== undefined
const childJobName = child?.jobName ?? ''
let downloadUrl: string | undefined
if (childJobName === 'studio_ee_job_create_csv') {
downloadUrl = `${getPrefix()}/export/download/csv/{jobRunId}`
} else if (childJobName === 'studio_ee_job_create_xlsx') {
downloadUrl = `${getPrefix()}/export/download/xlsx/{jobRunId}`
}

return new MessageBusJobHandler({
jobRunId: child?.id ?? parent.id,
ancestorJobRunIds: isChild ? [parent.id] : undefined,
title: t('jobs.download-job.title'),
stepTracker: new ChildJobStepTracker({ totalSteps: 2, startAtStep: isChild ? 2 : 1 }),
progressCalculator: new ProgressFieldCalculator(),
...(downloadUrl !== undefined && { onCustomizeButtons: buildDownloadButton(downloadUrl) })
})
}
)

// Recycle bin restore
registry.register(
['studio_ee_job_recycle_bin_restore'],
(parent: JobRun) => new MessageBusJobHandler({
jobRunId: parent.id,
title: t('jobs.recycle-bin-restore-job.title'),
onJobCompletion: async (data) => {
if (data.isFinished) {
store.dispatch(refreshTreeByElementType({ elementTypes: ['asset', 'data-object', 'document'] }))
store.dispatch(api.util.invalidateTags(invalidatingTags.RECYCLING_BIN()))
}
}
})
)

// Recycle bin delete
registry.register(
['studio_ee_job_recycle_bin_delete'],
(parent: JobRun) => new MessageBusJobHandler({
jobRunId: parent.id,
title: t('jobs.recycle-bin-delete-job.title'),
onJobCompletion: async (data) => {
if (data.isFinished) {
store.dispatch(api.util.invalidateTags(invalidatingTags.RECYCLING_BIN()))
}
}
})
)

// Tag assign/replace
registry.register(
['studio_ee_job_batch_tag_assign', 'studio_ee_job_batch_tag_replace'],
(parent: JobRun) => new MessageBusJobHandler({
jobRunId: parent.id,
title: t('jobs.tag-assign-job.title')
})
)

// Search and replace assignments
registry.register(
['studio_ee_job_element_usage_replace'],
(parent: JobRun) => new MessageBusJobHandler({
jobRunId: parent.id,
title: t('jobs.search-replace-assignments-job.title')
})
)
}

function buildDownloadButton (downloadUrl: string): (context: JobButtonCustomizationContext) => void {
return (context: JobButtonCustomizationContext) => {
context.addSuccessButton({
label: t('jobs.job.button-download'),
handler: () => {
const a = document.createElement('a')
a.href = downloadUrl.replace('{jobRunId}', context.jobRunId.toString())
a.download = ''
a.click()
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface MessageBusJob extends AbstractJob {
onCustomizeButtons?: (context: JobButtonCustomizationContext) => void
messages?: string[]
jobRunId: number
ancestorJobRunIds?: number[]
}

export interface JobCompletionData {
Expand All @@ -35,6 +36,7 @@ export interface JobCompletionData {

export interface MessageBusJobHandlerOptions {
jobRunId: number
ancestorJobRunIds?: number[]
title: string | ((job: MessageBusJob) => string)
stepDescriptions?: Record<number, string>
stepTracker?: StepTracker
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type { MessageBusJob, JobCompletionData, MessageBusJobHandlerOptions } fr

export class MessageBusJobHandler extends AbstractMessageHandler {
private jobRunId: number
private ancestorJobRunIds: number[] | undefined
private job: MessageBusJob | null = null
private readonly onJobCompletion?: (data: JobCompletionData) => void | Promise<void>
private readonly onRetry?: () => void | Promise<void>
Expand All @@ -53,6 +54,7 @@ export class MessageBusJobHandler extends AbstractMessageHandler {
constructor (options: MessageBusJobHandlerOptions) {
super()
this.jobRunId = options.jobRunId
this.ancestorJobRunIds = options.ancestorJobRunIds
this.title = options.title
this.stepDescriptions = options.stepDescriptions
this.stepTracker = options.stepTracker ?? new DefaultStepTracker()
Expand Down Expand Up @@ -136,7 +138,8 @@ export class MessageBusJobHandler extends AbstractMessageHandler {
stepDescriptionKey: this.stepDescriptions?.[currentStep],
onRetry: this.onRetry,
onCustomizeButtons: this.onCustomizeButtons,
jobRunId: this.jobRunId
jobRunId: this.jobRunId,
ancestorJobRunIds: this.ancestorJobRunIds
}

job.title = this.getTitle(job)
Expand Down Expand Up @@ -164,6 +167,7 @@ export class MessageBusJobHandler extends AbstractMessageHandler {

private transitionToChildJob (newJobRunId: number): void {
const oldJobRunId = this.jobRunId
this.ancestorJobRunIds = [...(this.ancestorJobRunIds ?? []), oldJobRunId]

this.jobRunId = newJobRunId

Expand Down Expand Up @@ -191,7 +195,8 @@ export class MessageBusJobHandler extends AbstractMessageHandler {
totalSteps: newState.totalSteps,
stepDescriptionKey: this.stepDescriptions?.[newState.currentStep]
}),
jobRunId: newJobRunId
jobRunId: newJobRunId,
ancestorJobRunIds: this.ancestorJobRunIds
})
}

Expand Down
Loading
Loading