diff --git a/workspaces/x2a/.changeset/fresh-bobcats-serve.md b/workspaces/x2a/.changeset/fresh-bobcats-serve.md new file mode 100644 index 00000000000..c2c34fb0ee1 --- /dev/null +++ b/workspaces/x2a/.changeset/fresh-bobcats-serve.md @@ -0,0 +1,7 @@ +--- +'@red-hat-developer-hub/backstage-plugin-x2a-backend': patch +'@red-hat-developer-hub/backstage-plugin-x2a-common': patch +'@red-hat-developer-hub/backstage-plugin-x2a': patch +--- + +Add module and project status. diff --git a/workspaces/x2a/package.json b/workspaces/x2a/package.json index bc2487f18ee..2606a78bbf3 100644 --- a/workspaces/x2a/package.json +++ b/workspaces/x2a/package.json @@ -10,13 +10,13 @@ "start": "NODE_OPTIONS=--no-node-snapshot ; backstage-cli repo start", "build:backend": "yarn workspace backend build", "build:all": "backstage-cli repo build --all", - "build:api-reports": "yarn build:api-reports:only --tsc", + "build:api-reports": "yarn tsc && yarn build:api-reports:only", "build:api-reports:only": "backstage-repo-tools api-reports -o ae-wrong-input-file-type,ae-undocumented --validate-release-tags --exclude client/src/schema/openapi/generated", "build:knip-reports": "backstage-repo-tools knip-reports", "build-image": "yarn workspace backend build-image", "openapi-generate": "cd plugins/x2a-backend && yarn openapi-generate", "tsc": "tsc", - "tsc:full": "tsc --skipLibCheck false --incremental false", + "tsc:full": "tsc --skipLibCheck true --incremental false", "clean": "backstage-cli repo clean", "test": "backstage-cli repo test", "test:all": "yarn openapi-generate && yarn prettier:check && yarn lint:all && backstage-cli repo test --coverage", diff --git a/workspaces/x2a/plugins/x2a-backend/src/router/modules.test.ts b/workspaces/x2a/plugins/x2a-backend/src/router/modules.test.ts index 8a855a29f67..72a646da61d 100644 --- a/workspaces/x2a/plugins/x2a-backend/src/router/modules.test.ts +++ b/workspaces/x2a/plugins/x2a-backend/src/router/modules.test.ts @@ -59,6 +59,33 @@ describe('createRouter – modules', () => { LONG_TEST_TIMEOUT, ); + it.each(supportedDatabaseIds)( + 'should return each module with status field from service enrichment - %p', + async databaseId => { + const { client } = await createDatabase(databaseId); + const x2aDatabase = X2ADatabaseService.create({ + logger: mockServices.logger.mock(), + dbClient: client, + }); + const app = await createApp(client); + const project = await createTestProject(x2aDatabase); + await createTestModule(x2aDatabase, project.id, { + name: 'Module A', + sourcePath: '/a', + }); + + const response = await request(app) + .get(`/projects/${project.id}/modules`) + .send(); + + expect(response.status).toBe(200); + expect(response.body).toHaveLength(1); + expect(response.body[0]).toHaveProperty('status'); + expect(response.body[0].status).toBe('pending'); + }, + LONG_TEST_TIMEOUT, + ); + it.each(supportedDatabaseIds)( 'should return 404 when project does not exist - %p', async databaseId => { @@ -357,5 +384,49 @@ describe('createRouter – modules', () => { ); }, ); + + it.each(supportedDatabaseIds)( + 'should accept optional aapCredentials and pass them to kubeService.createJob - %p', + async databaseId => { + const { client } = await createDatabase(databaseId); + const x2aDatabase = X2ADatabaseService.create({ + logger: mockServices.logger.mock(), + dbClient: client, + }); + const project = await createTestProject(x2aDatabase); + const module = await createTestModule(x2aDatabase, project.id); + + const mockCreateJob = jest + .fn() + .mockResolvedValue({ k8sJobName: 'k8s-job' }); + const appWithMock = await createApp(client, undefined, undefined, { + createJob: mockCreateJob, + }); + + const aapCredentials = { + url: 'https://aap.example.com', + orgName: 'Default', + oauthToken: 'oauth-token', + }; + const response = await request(appWithMock) + .post(`/projects/${project.id}/modules/${module.id}/run`) + .send({ + ...runBody, + aapCredentials, + }); + + expect(response.status).toBe(200); + expect(mockCreateJob).toHaveBeenCalledTimes(1); + expect(mockCreateJob).toHaveBeenCalledWith( + expect.objectContaining({ + aapCredentials, + phase: 'analyze', + moduleId: module.id, + moduleName: module.name, + }), + ); + }, + LONG_TEST_TIMEOUT, + ); }); }); diff --git a/workspaces/x2a/plugins/x2a-backend/src/router/modules.ts b/workspaces/x2a/plugins/x2a-backend/src/router/modules.ts index 7466aefe397..9298239bc69 100644 --- a/workspaces/x2a/plugins/x2a-backend/src/router/modules.ts +++ b/workspaces/x2a/plugins/x2a-backend/src/router/modules.ts @@ -18,14 +18,9 @@ import { z } from 'zod'; import express from 'express'; import { randomUUID } from 'node:crypto'; import { InputError, NotFoundError } from '@backstage/errors'; -import { Module } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; import type { RouterDeps } from './types'; -import { - getUserRef, - reconcileJobStatus, - removeSensitiveFromJob, -} from './common'; +import { getUserRef, reconcileJobStatus } from './common'; export function registerModuleRoutes( router: express.Router, @@ -54,50 +49,7 @@ export function registerModuleRoutes( // List modules const modules = await x2aDatabase.listModules({ projectId }); - // TODO: This can be optimized by using a single query to list all jobs for all modules. - const lastAnalyzeJobsOfModules = await Promise.all( - modules.map(module => - x2aDatabase.listJobs({ - projectId, - moduleId: module.id, - phase: 'analyze', - lastJobOnly: true, - }), - ), - ); - const lastMigrateJobsOfModules = await Promise.all( - modules.map(module => - x2aDatabase.listJobs({ - projectId, - moduleId: module.id, - phase: 'migrate', - lastJobOnly: true, - }), - ), - ); - const lastPublishJobsOfModules = await Promise.all( - modules.map(module => - x2aDatabase.listJobs({ - projectId, - moduleId: module.id, - phase: 'publish', - lastJobOnly: true, - }), - ), - ); - - const response: Array = modules.map((module, idxModule) => { - return { - ...module, - analyze: removeSensitiveFromJob(lastAnalyzeJobsOfModules[idxModule][0]), - migrate: removeSensitiveFromJob(lastMigrateJobsOfModules[idxModule][0]), - publish: removeSensitiveFromJob(lastPublishJobsOfModules[idxModule][0]), - - // TODO: calculate module's status from the last job - }; - }); - - res.json(response); + res.json(modules); }); // TODO: This is a TEMPORARY endpoint for testing only. diff --git a/workspaces/x2a/plugins/x2a-backend/src/schema/openapi.yaml b/workspaces/x2a/plugins/x2a-backend/src/schema/openapi.yaml index 3da4b926a2e..845dabb9c52 100644 --- a/workspaces/x2a/plugins/x2a-backend/src/schema/openapi.yaml +++ b/workspaces/x2a/plugins/x2a-backend/src/schema/openapi.yaml @@ -457,7 +457,6 @@ components: targetRepoBranch: description: Branch of the target repository type: string - # TODO: status as optional field createdAt: type: string format: date-time @@ -468,6 +467,9 @@ components: migrationPlan: $ref: '#/components/schemas/Artifact' description: Project migration plan artifact (by init phase) + status: + $ref: '#/components/schemas/ProjectStatus' + description: Project status calculated from the status of its modules required: - id - name @@ -501,7 +503,11 @@ components: $ref: '#/components/schemas/Job' publish: $ref: '#/components/schemas/Job' - # TODO: module status + status: + $ref: '#/components/schemas/ModuleStatus' + errorDetails: + type: string + description: Detailed error information if the module failed to execute required: - id - name @@ -516,6 +522,79 @@ components: - success - error + ModuleStatus: + type: string + description: | + Module status is the status of the last job of its last phase. + If a later retrigger for an earlier phase fails (e.g. when retrigger on analyze + fails but a former migrate already passed), the modules status should not change (is still based on the last phase). + The pending state is used for modules that are scheduled for execution but not yet actually running. If a module + is in pending state for long time, it can refer to an issue with the OCP setup. + enum: + - pending + - running + - success + - error + + ProjectStatusState: + type: string + description: | + Project status state. + It is calculated from the status of its modules. + - created: Project is created but not yet initialized + - initializing: Project's init job is running or scheduling + - initialized: Project's init job finished successfully. Either module list is empty or all modules are in pending state. + - inProgress: At least one module is beyond the pending state. + - completed: All modules are in success state + - failed: At least one module is in error state + enum: + - created + - initializing + - initialized + - inProgress + - completed + - failed + + ModulesStatusSummary: + type: object + properties: + total: + type: integer + description: Total number of modules in the project + finished: + type: integer + description: Number of modules in success state of the publish phase (no more work is needed) + waiting: + type: integer + description: Number of modules in success state of a non-publish phase (means waiting for human interaction) + pending: + type: integer + description: Number of modules in pending state (scheduled for execution but not actually running) + running: + type: integer + description: Number of modules in running state (actually running) + error: + type: integer + description: Number of modules in error state (execution is over but failed) + required: + - total + - finished + - waiting + - pending + - running + - error + + ProjectStatus: + type: object + properties: + state: + $ref: '#/components/schemas/ProjectStatusState' + modulesSummary: + $ref: '#/components/schemas/ModulesStatusSummary' + required: + - state + - modulesSummary + Job: type: object required: diff --git a/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/Module.model.ts b/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/Module.model.ts index 3cf9de2dc50..89e416caf30 100644 --- a/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/Module.model.ts +++ b/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/Module.model.ts @@ -18,6 +18,7 @@ // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** import { Job } from '../models/Job.model'; +import { ModuleStatus } from '../models/ModuleStatus.model'; /** * @public @@ -42,4 +43,9 @@ export interface Module { analyze?: Job; migrate?: Job; publish?: Job; + status?: ModuleStatus; + /** + * Detailed error information if the module failed to execute + */ + errorDetails?: string; } diff --git a/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/ModuleStatus.model.ts b/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/ModuleStatus.model.ts new file mode 100644 index 00000000000..e6da640cea4 --- /dev/null +++ b/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/ModuleStatus.model.ts @@ -0,0 +1,24 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// ****************************************************************** +// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * +// ****************************************************************** + +/** + * @public + */ +export type ModuleStatus = 'pending' | 'running' | 'success' | 'error'; diff --git a/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/ModulesStatusSummary.model.ts b/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/ModulesStatusSummary.model.ts new file mode 100644 index 00000000000..6d16f4643e8 --- /dev/null +++ b/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/ModulesStatusSummary.model.ts @@ -0,0 +1,49 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// ****************************************************************** +// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * +// ****************************************************************** + +/** + * @public + */ +export interface ModulesStatusSummary { + /** + * Total number of modules in the project + */ + total: number; + /** + * Number of modules in success state of the publish phase (no more work is needed) + */ + finished: number; + /** + * Number of modules in success state of a non-publish phase (means waiting for human interaction) + */ + waiting: number; + /** + * Number of modules in pending state (scheduled for execution but not actually running) + */ + pending: number; + /** + * Number of modules in running state (actually running) + */ + running: number; + /** + * Number of modules in error state (execution is over but failed) + */ + error: number; +} diff --git a/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/Project.model.ts b/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/Project.model.ts index c156ff76241..6e070ba3645 100644 --- a/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/Project.model.ts +++ b/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/Project.model.ts @@ -18,6 +18,7 @@ // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** import { Artifact } from '../models/Artifact.model'; +import { ProjectStatus } from '../models/ProjectStatus.model'; /** * @public @@ -64,4 +65,5 @@ export interface Project { */ createdBy: string; migrationPlan?: Artifact; + status?: ProjectStatus; } diff --git a/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/ProjectStatus.model.ts b/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/ProjectStatus.model.ts new file mode 100644 index 00000000000..d7b0d5665dc --- /dev/null +++ b/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/ProjectStatus.model.ts @@ -0,0 +1,29 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// ****************************************************************** +// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * +// ****************************************************************** +import { ModulesStatusSummary } from '../models/ModulesStatusSummary.model'; +import { ProjectStatusState } from '../models/ProjectStatusState.model'; + +/** + * @public + */ +export interface ProjectStatus { + state: ProjectStatusState; + modulesSummary: ModulesStatusSummary; +} diff --git a/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/ProjectStatusState.model.ts b/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/ProjectStatusState.model.ts new file mode 100644 index 00000000000..63638d30e72 --- /dev/null +++ b/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/ProjectStatusState.model.ts @@ -0,0 +1,30 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// ****************************************************************** +// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * +// ****************************************************************** + +/** + * @public + */ +export type ProjectStatusState = + | 'created' + | 'initializing' + | 'initialized' + | 'inProgress' + | 'completed' + | 'failed'; diff --git a/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/index.ts b/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/index.ts index 182be79bb9b..aaade911c2a 100644 --- a/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/index.ts +++ b/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/models/index.ts @@ -24,7 +24,11 @@ export * from '../models/JobStatusEnum.model'; export * from '../models/MigrationPhase.model'; export * from '../models/Module.model'; export * from '../models/ModulePhase.model'; +export * from '../models/ModuleStatus.model'; +export * from '../models/ModulesStatusSummary.model'; export * from '../models/Project.model'; +export * from '../models/ProjectStatus.model'; +export * from '../models/ProjectStatusState.model'; export * from '../models/ProjectsGet200Response.model'; export * from '../models/ProjectsPostRequest.model'; export * from '../models/ProjectsProjectIdCollectArtifactsPost200Response.model'; diff --git a/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/router.ts b/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/router.ts index 40bf2567963..1719935d669 100644 --- a/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/router.ts +++ b/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/router.ts @@ -700,6 +700,10 @@ export const spec = { "migrationPlan": { "$ref": "#/components/schemas/Artifact", "description": "Project migration plan artifact (by init phase)" + }, + "status": { + "$ref": "#/components/schemas/ProjectStatus", + "description": "Project status calculated from the status of its modules" } }, "required": [ @@ -741,6 +745,13 @@ export const spec = { }, "publish": { "$ref": "#/components/schemas/Job" + }, + "status": { + "$ref": "#/components/schemas/ModuleStatus" + }, + "errorDetails": { + "type": "string", + "description": "Detailed error information if the module failed to execute" } }, "required": [ @@ -759,6 +770,80 @@ export const spec = { "error" ] }, + "ModuleStatus": { + "type": "string", + "description": "Module status is the status of the last job of its last phase.\nIf a later retrigger for an earlier phase fails (e.g. when retrigger on analyze\nfails but a former migrate already passed), the modules status should not change (is still based on the last phase).\nThe pending state is used for modules that are scheduled for execution but not yet actually running. If a module\nis in pending state for long time, it can refer to an issue with the OCP setup.\n", + "enum": [ + "pending", + "running", + "success", + "error" + ] + }, + "ProjectStatusState": { + "type": "string", + "description": "Project status state.\nIt is calculated from the status of its modules.\n- created: Project is created but not yet initialized\n- initializing: Project's init job is running or scheduling\n- initialized: Project's init job finished successfully. Either module list is empty or all modules are in pending state.\n- inProgress: At least one module is beyond the pending state.\n- completed: All modules are in success state\n- failed: At least one module is in error state\n", + "enum": [ + "created", + "initializing", + "initialized", + "inProgress", + "completed", + "failed" + ] + }, + "ModulesStatusSummary": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "description": "Total number of modules in the project" + }, + "finished": { + "type": "integer", + "description": "Number of modules in success state of the publish phase (no more work is needed)" + }, + "waiting": { + "type": "integer", + "description": "Number of modules in success state of a non-publish phase (means waiting for human interaction)" + }, + "pending": { + "type": "integer", + "description": "Number of modules in pending state (scheduled for execution but not actually running)" + }, + "running": { + "type": "integer", + "description": "Number of modules in running state (actually running)" + }, + "error": { + "type": "integer", + "description": "Number of modules in error state (execution is over but failed)" + } + }, + "required": [ + "total", + "finished", + "waiting", + "pending", + "running", + "error" + ] + }, + "ProjectStatus": { + "type": "object", + "properties": { + "state": { + "$ref": "#/components/schemas/ProjectStatusState" + }, + "modulesSummary": { + "$ref": "#/components/schemas/ModulesStatusSummary" + } + }, + "required": [ + "state", + "modulesSummary" + ] + }, "Job": { "type": "object", "required": [ diff --git a/workspaces/x2a/plugins/x2a-backend/src/services/X2ADatabaseService/index.ts b/workspaces/x2a/plugins/x2a-backend/src/services/X2ADatabaseService/index.ts index ac45a9869cf..9eaf605444d 100644 --- a/workspaces/x2a/plugins/x2a-backend/src/services/X2ADatabaseService/index.ts +++ b/workspaces/x2a/plugins/x2a-backend/src/services/X2ADatabaseService/index.ts @@ -39,6 +39,8 @@ import { ProjectsGet } from '../../schema/openapi'; import { JobOperations, CreateJobInput } from './jobOperations'; import { ModuleOperations } from './moduleOperations'; import { ProjectOperations } from './projectOperations'; +import { removeSensitiveFromJob } from '../../router/common'; +import { calculateModuleStatus, calculateProjectStatus } from './status'; export class X2ADatabaseService { readonly #logger: LoggerService; @@ -57,16 +59,25 @@ export class X2ADatabaseService { this.#jobOps = new JobOperations(logger, dbClient); } - private async getMigrationPlanForProject( - projectId: string, - ): Promise { - const lastInitJob = await this.#jobOps.listJobs({ + /** + * Enriches a project with migration plan and status (used by listProjects and getProject). + */ + private async enrichProject(project: Project): Promise { + const projectId = project.id; + + const initJob = await this.listJobs({ projectId, phase: 'init', lastJobOnly: true, }); - if (lastInitJob.length === 0) return undefined; - return lastInitJob[0].artifacts?.find( + const lastInitJob = initJob[0]; + + project.status = calculateProjectStatus( + await this.listModules({ projectId }), + lastInitJob, + ); + + project.migrationPlan = lastInitJob?.artifacts?.find( artifact => artifact.type === 'migration_plan', ); } @@ -102,13 +113,7 @@ export class X2ADatabaseService { this.#logger.info( `this.#projectOps.listProjects finished, adding migration plans to projects`, ); - await Promise.all( - result.projects.map(async project => { - project.migrationPlan = await this.getMigrationPlanForProject( - project.id, - ); - }), - ); + await Promise.all(result.projects.map(p => this.enrichProject(p))); return result; } @@ -121,10 +126,11 @@ export class X2ADatabaseService { ): Promise { const project = await this.#projectOps.getProject({ projectId }, options); if (!project) return undefined; - this.#logger.info( - `this.#projectOps.getProject finished, adding migration plan to project`, + + this.#logger.debug( + `this.#projectOps.getProject finished, adding migration plan and status to project`, ); - project.migrationPlan = await this.getMigrationPlanForProject(project.id); + await this.enrichProject(project); return project; } @@ -153,7 +159,58 @@ export class X2ADatabaseService { } async listModules({ projectId }: { projectId: string }): Promise { - return this.#moduleOps.listModules({ projectId }); + const modules = await this.#moduleOps.listModules({ projectId }); + // TODO: This can be optimized by using a single query to list all jobs for all modules. + const lastAnalyzeJobsOfModules = await Promise.all( + modules.map(module => + this.listJobs({ + projectId, + moduleId: module.id, + phase: 'analyze', + lastJobOnly: true, + }), + ), + ); + const lastMigrateJobsOfModules = await Promise.all( + modules.map(module => + this.listJobs({ + projectId, + moduleId: module.id, + phase: 'migrate', + lastJobOnly: true, + }), + ), + ); + const lastPublishJobsOfModules = await Promise.all( + modules.map(module => + this.listJobs({ + projectId, + moduleId: module.id, + phase: 'publish', + lastJobOnly: true, + }), + ), + ); + + const response: Array = modules.map((module, idxModule) => { + const analyze = removeSensitiveFromJob( + lastAnalyzeJobsOfModules[idxModule][0], + ); + const migrate = removeSensitiveFromJob( + lastMigrateJobsOfModules[idxModule][0], + ); + const publish = removeSensitiveFromJob( + lastPublishJobsOfModules[idxModule][0], + ); + const lastJobs = { analyze, migrate, publish }; + return { + ...module, + ...lastJobs, + ...calculateModuleStatus(lastJobs), + }; + }); + + return response; } async deleteModule({ id }: { id: string }): Promise { diff --git a/workspaces/x2a/plugins/x2a-backend/src/services/X2ADatabaseService/modules.test.ts b/workspaces/x2a/plugins/x2a-backend/src/services/X2ADatabaseService/modules.test.ts index 57b186da45a..086e4b5cf41 100644 --- a/workspaces/x2a/plugins/x2a-backend/src/services/X2ADatabaseService/modules.test.ts +++ b/workspaces/x2a/plugins/x2a-backend/src/services/X2ADatabaseService/modules.test.ts @@ -336,6 +336,110 @@ describe('X2ADatabaseService – modules', () => { expect(project1Modules.map(m => m.id)).toContain(module1Project1.id); }, ); + + it.each(supportedDatabaseIds)( + 'returns each module with status and optional analyze, migrate, publish - %p', + async databaseId => { + const { client } = await createDatabase(databaseId); + const service = createService(client); + const credentials = mockCredentials.user(); + const project = await service.createProject( + { + name: 'Test Project', + abbreviation: 'TP', + description: 'D', + ...defaultProjectRepoFields, + }, + { credentials }, + ); + await service.createModule({ + name: 'Module A', + sourcePath: '/a', + projectId: project.id, + }); + + const modules = await service.listModules({ projectId: project.id }); + + expect(modules).toHaveLength(1); + expect(modules[0]).toHaveProperty('status'); + expect(modules[0].status).toBe('pending'); + expect(modules[0].id).toBeDefined(); + expect(modules[0].name).toBe('Module A'); + }, + LONG_TEST_TIMEOUT, + ); + + it.each(supportedDatabaseIds)( + 'enriches each module with last analyze, migrate, publish when jobs exist - %p', + async databaseId => { + const { client } = await createDatabase(databaseId); + const service = createService(client); + const credentials = mockCredentials.user(); + const project = await service.createProject( + { + name: 'Test Project', + abbreviation: 'TP', + description: 'D', + ...defaultProjectRepoFields, + }, + { credentials }, + ); + const mod = await service.createModule({ + name: 'Module With Jobs', + sourcePath: '/with-jobs', + projectId: project.id, + }); + const analyzeJob = await service.createJob({ + projectId: project.id, + moduleId: mod.id, + phase: 'analyze', + status: 'pending', + callbackToken: 'tk', + }); + await service.updateJob({ + id: analyzeJob.id, + status: 'success', + }); + + const modError = await service.createModule({ + name: 'Module With Error', + sourcePath: '/with-error', + projectId: project.id, + }); + const errorJob = await service.createJob({ + projectId: project.id, + moduleId: modError.id, + phase: 'analyze', + status: 'pending', + callbackToken: 'tk2', + }); + await service.updateJob({ + id: errorJob.id, + status: 'error', + errorDetails: 'Analyze failed: timeout', + }); + + const modules = await service.listModules({ projectId: project.id }); + + expect(modules).toHaveLength(2); + const successModule = modules.find(m => m.name === 'Module With Jobs'); + expect(successModule).toBeDefined(); + expect(successModule?.analyze).toBeDefined(); + expect(successModule?.analyze?.id).toBe(analyzeJob.id); + expect(successModule?.analyze?.phase).toBe('analyze'); + expect(successModule?.analyze).not.toHaveProperty('callbackToken'); + expect(successModule?.migrate).toBeUndefined(); + expect(successModule?.publish).toBeUndefined(); + expect(successModule?.status).toBe('success'); + expect(successModule?.errorDetails ?? undefined).toBeUndefined(); + + const errorModule = modules.find(m => m.name === 'Module With Error'); + expect(errorModule).toBeDefined(); + expect(errorModule?.status).toBe('error'); + expect(errorModule?.errorDetails).toBe('Analyze failed: timeout'); + }, + LONG_TEST_TIMEOUT, + ); }); describe('deleteModule', () => { diff --git a/workspaces/x2a/plugins/x2a-backend/src/services/X2ADatabaseService/status.test.ts b/workspaces/x2a/plugins/x2a-backend/src/services/X2ADatabaseService/status.test.ts new file mode 100644 index 00000000000..64b4a2d8cb1 --- /dev/null +++ b/workspaces/x2a/plugins/x2a-backend/src/services/X2ADatabaseService/status.test.ts @@ -0,0 +1,536 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { + Job, + Module, + ModuleStatus, +} from '@red-hat-developer-hub/backstage-plugin-x2a-common'; + +import { calculateModuleStatus, calculateProjectStatus } from './status'; + +function job(status: Job['status'], options?: { errorDetails?: string }): Job { + return { + id: 'job-id', + projectId: 'project-id', + startedAt: new Date(), + phase: 'analyze', + k8sJobName: 'k8s-job', + status, + ...(options?.errorDetails !== undefined && { + errorDetails: options.errorDetails, + }), + }; +} + +describe('calculateModuleStatus', () => { + it('returns pending when no jobs are provided', () => { + expect(calculateModuleStatus({})).toEqual({ + status: 'pending', + errorDetails: undefined, + }); + }); + + it('returns analyze status when only analyze job is provided', () => { + expect(calculateModuleStatus({ analyze: job('pending') }).status).toBe( + 'pending', + ); + expect(calculateModuleStatus({ analyze: job('running') }).status).toBe( + 'running', + ); + expect(calculateModuleStatus({ analyze: job('success') }).status).toBe( + 'success', + ); + expect(calculateModuleStatus({ analyze: job('error') }).status).toBe( + 'error', + ); + }); + + it('returns migrate status when only migrate job is provided', () => { + expect(calculateModuleStatus({ migrate: job('success') }).status).toBe( + 'success', + ); + expect(calculateModuleStatus({ migrate: job('error') }).status).toBe( + 'error', + ); + }); + + it('returns publish status when only publish job is provided', () => { + expect(calculateModuleStatus({ publish: job('success') }).status).toBe( + 'success', + ); + expect(calculateModuleStatus({ publish: job('running') }).status).toBe( + 'running', + ); + }); + + it('prefers migrate over analyze when both are provided', () => { + expect( + calculateModuleStatus({ + analyze: job('success'), + migrate: job('error'), + }).status, + ).toBe('error'); + expect( + calculateModuleStatus({ + analyze: job('error'), + migrate: job('success'), + }).status, + ).toBe('success'); + }); + + it('prefers publish over migrate and analyze when multiple are provided', () => { + expect( + calculateModuleStatus({ + analyze: job('success'), + migrate: job('success'), + publish: job('error'), + }).status, + ).toBe('error'); + expect( + calculateModuleStatus({ + analyze: job('error'), + migrate: job('error'), + publish: job('success'), + }).status, + ).toBe('success'); + }); + + it('uses last phase status so retrigger of earlier phase does not change module status', () => { + // When publish already passed, a later retrigger on analyze that fails + // should not change the module status (still success from publish). + expect( + calculateModuleStatus({ + analyze: job('error'), + migrate: job('success'), + publish: job('success'), + }).status, + ).toBe('success'); + }); + + describe('errorDetails', () => { + it('returns errorDetails from analyze job when only analyze is provided', () => { + const result = calculateModuleStatus({ + analyze: job('error', { errorDetails: 'Analyze failed: timeout' }), + }); + expect(result.status).toBe('error'); + expect(result.errorDetails).toBe('Analyze failed: timeout'); + }); + + it('returns errorDetails from migrate job when migrate is the last phase', () => { + const result = calculateModuleStatus({ + analyze: job('success'), + migrate: job('error', { errorDetails: 'Migration failed' }), + }); + expect(result.status).toBe('error'); + expect(result.errorDetails).toBe('Migration failed'); + }); + + it('returns errorDetails from publish job when publish is the last phase', () => { + const result = calculateModuleStatus({ + analyze: job('success'), + migrate: job('success'), + publish: job('error', { errorDetails: 'Publish failed' }), + }); + expect(result.status).toBe('error'); + expect(result.errorDetails).toBe('Publish failed'); + }); + + it('returns undefined errorDetails when the chosen phase job has none', () => { + expect( + calculateModuleStatus({ analyze: job('error') }).errorDetails, + ).toBeUndefined(); + expect( + calculateModuleStatus({ publish: job('success') }).errorDetails, + ).toBeUndefined(); + }); + + it('prefers errorDetails from last phase when multiple phases have errorDetails', () => { + const result = calculateModuleStatus({ + analyze: job('error', { errorDetails: 'Analyze error' }), + migrate: job('error', { errorDetails: 'Migrate error' }), + publish: job('error', { errorDetails: 'Publish error' }), + }); + expect(result.errorDetails).toBe('Publish error'); + }); + + it('returns undefined errorDetails when no jobs are provided', () => { + expect(calculateModuleStatus({}).errorDetails).toBeUndefined(); + }); + }); +}); + +/** Minimal module for project status tests; only status and publish are used by calculateProjectStatus. */ +function module( + status: ModuleStatus, + options?: { publishStatus?: Job['status'] }, +): Module { + const m: Module = { + id: 'mod-id', + name: 'Module', + sourcePath: '/path', + projectId: 'project-id', + status, + }; + if (options?.publishStatus !== undefined) { + m.publish = job(options.publishStatus); + } + return m; +} + +function initJob(status: Job['status']): Job { + return job(status); +} + +/** + * Project state tests follow OpenAPI ProjectStatusState (openapi.yaml 543–549): + * - created: Project is created but not yet initialized + * - initializing: Project's init job is running or scheduling + * - initialized: Project's init job finished successfully. Either module list is empty or all modules are in pending state. + * - inProgress: At least one module is beyond the pending state. + * - completed: All modules are in success state + * - failed: At least one module is in error state (or init failed / no init with modules) + */ +describe('calculateProjectStatus', () => { + describe('state: created', () => { + it('returns created when no init job and no modules (project created but not yet initialized)', () => { + const result = calculateProjectStatus([], undefined); + expect(result.state).toBe('created'); + expect(result.modulesSummary).toEqual({ + total: 0, + finished: 0, + waiting: 0, + pending: 0, + running: 0, + error: 0, + }); + }); + + it('does not return created when init job exists even with no modules', () => { + expect(calculateProjectStatus([], initJob('pending')).state).toBe( + 'initializing', + ); + expect(calculateProjectStatus([], initJob('success')).state).toBe( + 'initialized', + ); + }); + }); + + describe('state: failed', () => { + it('returns failed when at least one module is in error state', () => { + const result = calculateProjectStatus([ + module('success', { publishStatus: 'success' }), + module('error'), + ]); + expect(result.state).toBe('failed'); + expect(result.modulesSummary.error).toBe(1); + }); + + it('returns failed when only one module and it is in error', () => { + const result = calculateProjectStatus([module('error')]); + expect(result.state).toBe('failed'); + expect(result.modulesSummary.error).toBe(1); + expect(result.modulesSummary.total).toBe(1); + }); + + it('returns failed when all modules are in error', () => { + const result = calculateProjectStatus([module('error'), module('error')]); + expect(result.state).toBe('failed'); + expect(result.modulesSummary.error).toBe(2); + }); + + it('returns failed when no init job is provided but project has modules (init never ran)', () => { + const result = calculateProjectStatus([ + module('pending'), + module('pending'), + ]); + expect(result.state).toBe('failed'); + }); + + it('returns failed when init job completed with error', () => { + const result = calculateProjectStatus( + [module('pending')], + initJob('error'), + ); + expect(result.state).toBe('failed'); + }); + + it('returns failed even when init succeeded if any module is in error (failed takes precedence)', () => { + const result = calculateProjectStatus( + [module('success', { publishStatus: 'success' }), module('error')], + initJob('success'), + ); + expect(result.state).toBe('failed'); + expect(result.modulesSummary.error).toBe(1); + }); + }); + + describe('state: initializing', () => { + it('returns initializing when init job is pending (scheduling)', () => { + const result = calculateProjectStatus( + [module('pending'), module('pending')], + initJob('pending'), + ); + expect(result.state).toBe('initializing'); + }); + + it('returns initializing when init job is running', () => { + const result = calculateProjectStatus( + [module('pending')], + initJob('running'), + ); + expect(result.state).toBe('initializing'); + }); + + it('returns initializing with empty module list when init is pending', () => { + const result = calculateProjectStatus([], initJob('pending')); + expect(result.state).toBe('initializing'); + }); + + it('returns initializing with empty module list when init is running', () => { + const result = calculateProjectStatus([], initJob('running')); + expect(result.state).toBe('initializing'); + }); + }); + + describe('state: initialized', () => { + it('returns initialized when init succeeded and module list is empty', () => { + const result = calculateProjectStatus([], initJob('success')); + expect(result.state).toBe('initialized'); + expect(result.modulesSummary.total).toBe(0); + expect(result.modulesSummary.finished).toBe(0); + }); + + it('returns initialized when init succeeded and all modules are in pending state', () => { + const result = calculateProjectStatus( + [module('pending'), module('pending')], + initJob('success'), + ); + expect(result.state).toBe('initialized'); + expect(result.modulesSummary.pending).toBe(2); + expect(result.modulesSummary.total).toBe(2); + }); + + it('returns initialized when init succeeded and single module is pending', () => { + const result = calculateProjectStatus( + [module('pending')], + initJob('success'), + ); + expect(result.state).toBe('initialized'); + expect(result.modulesSummary.pending).toBe(1); + }); + }); + + describe('state: inProgress', () => { + it('returns inProgress when at least one module is running (beyond pending)', () => { + const result = calculateProjectStatus( + [module('pending'), module('running')], + initJob('success'), + ); + expect(result.state).toBe('inProgress'); + expect(result.modulesSummary.running).toBe(1); + expect(result.modulesSummary.pending).toBe(1); + }); + + it('returns inProgress when at least one module has success but not all finished publish', () => { + const result = calculateProjectStatus( + [ + module('success', { publishStatus: 'success' }), + module('success'), // no publish yet – waiting + ], + initJob('success'), + ); + expect(result.state).toBe('inProgress'); + expect(result.modulesSummary.finished).toBe(1); + expect(result.modulesSummary.waiting).toBe(1); + expect(result.modulesSummary.total).toBe(2); + }); + + it('returns inProgress when one running and rest pending', () => { + const result = calculateProjectStatus( + [module('running'), module('pending'), module('pending')], + initJob('success'), + ); + expect(result.state).toBe('inProgress'); + expect(result.modulesSummary.running).toBe(1); + expect(result.modulesSummary.pending).toBe(2); + }); + + it('returns inProgress when mix of success (waiting), running and pending', () => { + const result = calculateProjectStatus( + [ + module('success'), // waiting for publish + module('running'), + module('pending'), + ], + initJob('success'), + ); + expect(result.state).toBe('inProgress'); + expect(result.modulesSummary.waiting).toBe(1); + expect(result.modulesSummary.running).toBe(1); + expect(result.modulesSummary.pending).toBe(1); + }); + + it('returns inProgress when one module finished publish but others have not', () => { + const result = calculateProjectStatus( + [ + module('success', { publishStatus: 'success' }), + module('pending'), + module('pending'), + ], + initJob('success'), + ); + expect(result.state).toBe('inProgress'); + expect(result.modulesSummary.finished).toBe(1); + expect(result.modulesSummary.pending).toBe(2); + }); + }); + + describe('state: completed', () => { + it('returns completed only when init succeeded and all modules are in success state (finished publish)', () => { + const result = calculateProjectStatus( + [ + module('success', { publishStatus: 'success' }), + module('success', { publishStatus: 'success' }), + ], + initJob('success'), + ); + expect(result.state).toBe('completed'); + expect(result.modulesSummary.finished).toBe(2); + expect(result.modulesSummary.total).toBe(2); + }); + + it('returns completed when init succeeded and single module has finished publish', () => { + const result = calculateProjectStatus( + [module('success', { publishStatus: 'success' })], + initJob('success'), + ); + expect(result.state).toBe('completed'); + expect(result.modulesSummary.finished).toBe(1); + expect(result.modulesSummary.total).toBe(1); + }); + + it('does not return completed when module list is empty (that is initialized)', () => { + const result = calculateProjectStatus([], initJob('success')); + expect(result.state).not.toBe('completed'); + expect(result.state).toBe('initialized'); + }); + }); + + describe('state precedence (per spec)', () => { + it('failed overrides initializing when init is running but a module is error', () => { + // In practice init running implies modules might not have run yet; if they do have error, failed wins. + const result = calculateProjectStatus( + [module('error'), module('pending')], + initJob('running'), + ); + expect(result.state).toBe('failed'); + }); + + it('failed overrides completed when any module is in error', () => { + const result = calculateProjectStatus( + [ + module('success', { publishStatus: 'success' }), + module('success', { publishStatus: 'success' }), + module('error'), + ], + initJob('success'), + ); + expect(result.state).toBe('failed'); + }); + + it('initializing overrides initialized when init is pending even if all modules pending', () => { + const result = calculateProjectStatus( + [module('pending'), module('pending')], + initJob('pending'), + ); + expect(result.state).toBe('initializing'); + }); + }); + + describe('modulesSummary counts', () => { + it('counts total as number of project modules', () => { + const result = calculateProjectStatus( + [module('pending'), module('pending'), module('pending')], + initJob('pending'), + ); + expect(result.modulesSummary.total).toBe(3); + }); + + it('counts finished as modules with success status and publish phase success', () => { + const result = calculateProjectStatus( + [ + module('success', { publishStatus: 'success' }), + module('success'), // no publish + module('success', { publishStatus: 'success' }), + ], + initJob('success'), + ); + expect(result.modulesSummary.finished).toBe(2); + }); + + it('counts waiting as modules with success status but no publish job', () => { + const result = calculateProjectStatus( + [ + module('success'), // success but no publish → waiting + module('success', { publishStatus: 'success' }), // finished + ], + initJob('success'), + ); + expect(result.modulesSummary.waiting).toBe(1); + expect(result.modulesSummary.finished).toBe(1); + }); + + it('counts pending as modules with status pending', () => { + const result = calculateProjectStatus( + [module('pending'), module('pending'), module('success')], + initJob('success'), + ); + expect(result.modulesSummary.pending).toBe(2); + }); + + it('counts running as modules with status running', () => { + const result = calculateProjectStatus( + [module('running'), module('pending')], + initJob('success'), + ); + expect(result.modulesSummary.running).toBe(1); + expect(result.modulesSummary.pending).toBe(1); + }); + + it('counts error as modules with status error', () => { + const result = calculateProjectStatus([ + module('error'), + module('success'), + module('error'), + ]); + expect(result.modulesSummary.error).toBe(2); + expect(result.modulesSummary.total).toBe(3); + }); + + it('returns zero counts for empty module list', () => { + const result = calculateProjectStatus([], initJob('success')); + expect(result.modulesSummary).toEqual({ + total: 0, + finished: 0, + waiting: 0, + pending: 0, + running: 0, + error: 0, + }); + }); + }); +}); diff --git a/workspaces/x2a/plugins/x2a-backend/src/services/X2ADatabaseService/status.ts b/workspaces/x2a/plugins/x2a-backend/src/services/X2ADatabaseService/status.ts new file mode 100644 index 00000000000..b93dd107836 --- /dev/null +++ b/workspaces/x2a/plugins/x2a-backend/src/services/X2ADatabaseService/status.ts @@ -0,0 +1,122 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Job, + Module, + ModuleStatus, + ProjectStatus, + ProjectStatusState, +} from '@red-hat-developer-hub/backstage-plugin-x2a-common'; + +/** + * Module's status is the status of the last job of its last phase. + * + * If a later retrigger for an earlier phase fails (e.g. when retrigger on analyze + * fails but a former migrate already passed), the modules status should not change. + */ +export function calculateModuleStatus({ + analyze, + migrate, + publish, +}: { + analyze?: Job; + migrate?: Job; + publish?: Job; +}): { status: ModuleStatus; errorDetails?: string } { + if (publish) { + return { status: publish.status, errorDetails: publish.errorDetails }; + } + if (migrate) { + return { status: migrate.status, errorDetails: migrate.errorDetails }; + } + if (analyze) { + return { status: analyze.status, errorDetails: analyze.errorDetails }; + } + + return { status: 'pending', errorDetails: undefined }; +} + +/** + * Project status is calculated from its modules. + * + * Its "state" is accompanied by summary of its modules statuses. + */ +export function calculateProjectStatus( + projectModules: Module[], + initJob?: Job, +): ProjectStatus { + const total = projectModules.length; + if (!initJob && total === 0) { + return { + state: 'created', + modulesSummary: { + total: 0, + finished: 0, + waiting: 0, + pending: 0, + running: 0, + error: 0, + }, + }; + } + + const error = projectModules.filter( + module => module.status === 'error', + ).length; + const finished = projectModules.filter( + module => + module.status === 'success' && module.publish?.status === 'success', + ).length; + const waiting = projectModules.filter( + module => module.status === 'success' && !module.publish, + ).length; + const pending = projectModules.filter( + module => module.status === 'pending', + ).length; + const running = projectModules.filter( + module => module.status === 'running', + ).length; + + let state: ProjectStatusState; + if (error > 0) { + state = 'failed'; // At least one module is in error state + } else if (['pending', 'running'].includes(initJob?.status ?? '')) { + state = 'initializing'; // Project's init job is running or scheduling + } else if (initJob?.status === 'success') { + if (total > 0 && finished === total) { + state = 'completed'; // All modules are in success state + } else if (total === 0 || pending === total) { + state = 'initialized'; // Module list is empty or all modules are in pending state + } else { + state = 'inProgress'; // At least one module is beyond the pending state + } + } else { + state = 'failed'; + } + + return { + state, + modulesSummary: { + total, + finished, + waiting, + pending, + running, + error, + }, + }; +} diff --git a/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/Module.model.ts b/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/Module.model.ts index 3cf9de2dc50..89e416caf30 100644 --- a/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/Module.model.ts +++ b/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/Module.model.ts @@ -18,6 +18,7 @@ // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** import { Job } from '../models/Job.model'; +import { ModuleStatus } from '../models/ModuleStatus.model'; /** * @public @@ -42,4 +43,9 @@ export interface Module { analyze?: Job; migrate?: Job; publish?: Job; + status?: ModuleStatus; + /** + * Detailed error information if the module failed to execute + */ + errorDetails?: string; } diff --git a/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/ModuleStatus.model.ts b/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/ModuleStatus.model.ts new file mode 100644 index 00000000000..e6da640cea4 --- /dev/null +++ b/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/ModuleStatus.model.ts @@ -0,0 +1,24 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// ****************************************************************** +// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * +// ****************************************************************** + +/** + * @public + */ +export type ModuleStatus = 'pending' | 'running' | 'success' | 'error'; diff --git a/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/ModulesStatusSummary.model.ts b/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/ModulesStatusSummary.model.ts new file mode 100644 index 00000000000..6d16f4643e8 --- /dev/null +++ b/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/ModulesStatusSummary.model.ts @@ -0,0 +1,49 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// ****************************************************************** +// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * +// ****************************************************************** + +/** + * @public + */ +export interface ModulesStatusSummary { + /** + * Total number of modules in the project + */ + total: number; + /** + * Number of modules in success state of the publish phase (no more work is needed) + */ + finished: number; + /** + * Number of modules in success state of a non-publish phase (means waiting for human interaction) + */ + waiting: number; + /** + * Number of modules in pending state (scheduled for execution but not actually running) + */ + pending: number; + /** + * Number of modules in running state (actually running) + */ + running: number; + /** + * Number of modules in error state (execution is over but failed) + */ + error: number; +} diff --git a/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/Project.model.ts b/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/Project.model.ts index c156ff76241..6e070ba3645 100644 --- a/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/Project.model.ts +++ b/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/Project.model.ts @@ -18,6 +18,7 @@ // * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * // ****************************************************************** import { Artifact } from '../models/Artifact.model'; +import { ProjectStatus } from '../models/ProjectStatus.model'; /** * @public @@ -64,4 +65,5 @@ export interface Project { */ createdBy: string; migrationPlan?: Artifact; + status?: ProjectStatus; } diff --git a/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/ProjectStatus.model.ts b/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/ProjectStatus.model.ts new file mode 100644 index 00000000000..d7b0d5665dc --- /dev/null +++ b/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/ProjectStatus.model.ts @@ -0,0 +1,29 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// ****************************************************************** +// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * +// ****************************************************************** +import { ModulesStatusSummary } from '../models/ModulesStatusSummary.model'; +import { ProjectStatusState } from '../models/ProjectStatusState.model'; + +/** + * @public + */ +export interface ProjectStatus { + state: ProjectStatusState; + modulesSummary: ModulesStatusSummary; +} diff --git a/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/ProjectStatusState.model.ts b/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/ProjectStatusState.model.ts new file mode 100644 index 00000000000..63638d30e72 --- /dev/null +++ b/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/ProjectStatusState.model.ts @@ -0,0 +1,30 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// ****************************************************************** +// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. * +// ****************************************************************** + +/** + * @public + */ +export type ProjectStatusState = + | 'created' + | 'initializing' + | 'initialized' + | 'inProgress' + | 'completed' + | 'failed'; diff --git a/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/index.ts b/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/index.ts index 182be79bb9b..aaade911c2a 100644 --- a/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/index.ts +++ b/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/models/index.ts @@ -24,7 +24,11 @@ export * from '../models/JobStatusEnum.model'; export * from '../models/MigrationPhase.model'; export * from '../models/Module.model'; export * from '../models/ModulePhase.model'; +export * from '../models/ModuleStatus.model'; +export * from '../models/ModulesStatusSummary.model'; export * from '../models/Project.model'; +export * from '../models/ProjectStatus.model'; +export * from '../models/ProjectStatusState.model'; export * from '../models/ProjectsGet200Response.model'; export * from '../models/ProjectsPostRequest.model'; export * from '../models/ProjectsProjectIdCollectArtifactsPost200Response.model'; diff --git a/workspaces/x2a/plugins/x2a-common/report.api.md b/workspaces/x2a/plugins/x2a-common/report.api.md index 3c24e3e5ca1..2013e9038bd 100644 --- a/workspaces/x2a/plugins/x2a-common/report.api.md +++ b/workspaces/x2a/plugins/x2a-common/report.api.md @@ -107,6 +107,7 @@ export type MigrationPhase = 'init' | 'analyze' | 'migrate' | 'publish'; export interface Module { // (undocumented) analyze?: Job; + errorDetails?: string; id: string; // (undocumented) migrate?: Job; @@ -115,11 +116,26 @@ export interface Module { // (undocumented) publish?: Job; sourcePath: string; + // (undocumented) + status?: ModuleStatus; } // @public (undocumented) export type ModulePhase = 'analyze' | 'migrate' | 'publish'; +// @public (undocumented) +export interface ModulesStatusSummary { + error: number; + finished: number; + pending: number; + running: number; + total: number; + waiting: number; +} + +// @public (undocumented) +export type ModuleStatus = 'pending' | 'running' | 'success' | 'error'; + // @public export function normalizeRepoUrl(url: string): string; @@ -135,6 +151,8 @@ export interface Project { name: string; sourceRepoBranch: string; sourceRepoUrl: string; + // (undocumented) + status?: ProjectStatus; targetRepoBranch: string; targetRepoUrl: string; } @@ -302,6 +320,17 @@ export interface ProjectsProjectIdRunPostRequest { userPrompt?: string; } +// @public (undocumented) +export interface ProjectStatus { + // (undocumented) + modulesSummary: ModulesStatusSummary; + // (undocumented) + state: ProjectStatusState; +} + +// @public (undocumented) +export type ProjectStatusState = 'created' | 'initializing' | 'initialized' | 'inProgress' | 'completed' | 'failed'; + // @public export interface RequestOptions { // (undocumented) diff --git a/workspaces/x2a/plugins/x2a/package.json b/workspaces/x2a/plugins/x2a/package.json index 7a443031adf..8e2590c82ee 100644 --- a/workspaces/x2a/plugins/x2a/package.json +++ b/workspaces/x2a/plugins/x2a/package.json @@ -43,6 +43,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", "@mui/material": "^7.3.7", + "@mui/x-charts": "^8.27.0", "@red-hat-developer-hub/backstage-plugin-x2a-common": "workspace:*", "react-use": "^17.2.4" }, diff --git a/workspaces/x2a/plugins/x2a/report.api.md b/workspaces/x2a/plugins/x2a/report.api.md index 81a2ebf765a..95d0f846c94 100644 --- a/workspaces/x2a/plugins/x2a/report.api.md +++ b/workspaces/x2a/plugins/x2a/report.api.md @@ -26,44 +26,62 @@ root: RouteRef; // @public export const x2aPluginTranslationRef: TranslationRef<"plugin.x2a", { -readonly "module.name": string; -readonly "module.status": string; -readonly "module.actions.runNextPhase": string; -readonly "module.artifacts": string; -readonly "module.phases.none": string; -readonly "module.phases.analyze": string; -readonly "module.phases.init": string; -readonly "module.phases.migrate": string; -readonly "module.phases.publish": string; -readonly "module.lastPhase": string; -readonly "module.sourcePath": string; -readonly "module.startedAt": string; -readonly "module.finishedAt": string; -readonly "page.title": string; -readonly "page.subtitle": string; -readonly "page.devTitle": string; readonly "table.columns.name": string; readonly "table.columns.description": string; -readonly "table.columns.status": string; -readonly "table.columns.createdAt": string; readonly "table.columns.abbreviation": string; +readonly "table.columns.status": string; readonly "table.columns.sourceRepo": string; readonly "table.columns.targetRepo": string; +readonly "table.columns.createdAt": string; readonly "table.actions.deleteProject": string; readonly "table.detailPanel": string; readonly "table.projectsCount": string; -readonly "common.newProject": string; +readonly "page.title": string; +readonly "page.subtitle": string; +readonly "page.devTitle": string; +readonly "sidebar.x2a.title": string; readonly "project.id": string; readonly "project.description": string; readonly "project.abbreviation": string; readonly "project.createdBy": string; +readonly "project.statuses.none": string; +readonly "project.statuses.created": string; +readonly "project.statuses.initializing": string; +readonly "project.statuses.initialized": string; +readonly "project.statuses.inProgress": string; +readonly "project.statuses.completed": string; +readonly "project.statuses.failed": string; +readonly "common.newProject": string; +readonly "wizard.cancel": string; +readonly "wizard.back": string; +readonly "wizard.next": string; +readonly "module.summary.running": string; +readonly "module.summary.total": string; +readonly "module.summary.error": string; +readonly "module.summary.finished": string; +readonly "module.summary.waiting": string; +readonly "module.summary.pending": string; +readonly "module.name": string; +readonly "module.status": string; +readonly "module.actions.runNextPhase": string; +readonly "module.statuses.none": string; +readonly "module.statuses.running": string; +readonly "module.statuses.error": string; +readonly "module.statuses.pending": string; +readonly "module.statuses.success": string; +readonly "module.phases.none": string; +readonly "module.phases.init": string; +readonly "module.phases.analyze": string; +readonly "module.phases.migrate": string; +readonly "module.phases.publish": string; +readonly "module.lastPhase": string; +readonly "module.sourcePath": string; +readonly "module.artifacts": string; +readonly "module.startedAt": string; +readonly "module.finishedAt": string; readonly "artifact.types.migration_plan": string; readonly "artifact.types.module_migration_plan": string; readonly "artifact.types.migrated_sources": string; -readonly "sidebar.x2a.title": string; -readonly "wizard.cancel": string; -readonly "wizard.next": string; -readonly "wizard.back": string; }>; // @public diff --git a/workspaces/x2a/plugins/x2a/src/components/ModuleTable/ModuleStatusCell.tsx b/workspaces/x2a/plugins/x2a/src/components/ModuleTable/ModuleStatusCell.tsx new file mode 100644 index 00000000000..62db8ae12d9 --- /dev/null +++ b/workspaces/x2a/plugins/x2a/src/components/ModuleTable/ModuleStatusCell.tsx @@ -0,0 +1,42 @@ +/** + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ModuleStatus } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; +import { Tooltip } from '@material-ui/core'; + +import { useTranslation } from '../../hooks/useTranslation'; + +export const ModuleStatusCell = ({ + status, + errorDetails, +}: { + status?: ModuleStatus; + errorDetails?: string; +}) => { + const { t } = useTranslation(); + + const statusText = t(`module.statuses.${status || 'none'}`); + if (errorDetails) { + return ( +
+ +
{statusText}
+
+
+ ); + } + return
{statusText}
; +}; diff --git a/workspaces/x2a/plugins/x2a/src/components/ModuleTable/ModuleTable.tsx b/workspaces/x2a/plugins/x2a/src/components/ModuleTable/ModuleTable.tsx index 9e3e9314e35..797e68c3951 100644 --- a/workspaces/x2a/plugins/x2a/src/components/ModuleTable/ModuleTable.tsx +++ b/workspaces/x2a/plugins/x2a/src/components/ModuleTable/ModuleTable.tsx @@ -32,6 +32,7 @@ import { useClientService } from '../../ClientService'; import { Artifacts } from './Artifacts'; import { humanizeDate } from '../tools'; import { getAuthTokenDescriptor, useRepoAuthentication } from '../../repoAuth'; +import { ModuleStatusCell } from './ModuleStatusCell'; const getLastJob = (rowData: Module) => { const phases: ('publish' | 'migrate' | 'analyze')[] = [ @@ -109,11 +110,19 @@ const useColumns = ({ }, [t], ); - - return useMemo(() => { + return useMemo((): TableColumn[] => { return [ { field: 'name', title: t('module.name') }, - { field: 'status', title: t('module.status') }, + { + field: 'status', + render: (rowData: Module) => ( + + ), + title: t('module.status'), + }, { field: 'sourcePath', title: t('module.sourcePath') }, { render: lastPhaseCell, title: t('module.lastPhase') }, { render: artifactsCell, title: t('module.artifacts') }, diff --git a/workspaces/x2a/plugins/x2a/src/components/ProjectList/ProjectStatusCell.tsx b/workspaces/x2a/plugins/x2a/src/components/ProjectList/ProjectStatusCell.tsx new file mode 100644 index 00000000000..a5230bba7bd --- /dev/null +++ b/workspaces/x2a/plugins/x2a/src/components/ProjectList/ProjectStatusCell.tsx @@ -0,0 +1,159 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useState } from 'react'; +import { ProjectStatus } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; + +import { PieChart, PieValueType } from '@mui/x-charts'; +import { Grid, makeStyles, Tooltip } from '@material-ui/core'; + +import { useTranslation } from '../../hooks/useTranslation'; + +const size = 25; + +const styles = makeStyles({ + tooltip: { + width: 100, + }, +}); + +const TooltipItem = ({ + label, + value, +}: { + label: string | React.ReactNode; + value: number; +}) => { + return ( + <> + + {label} + + + {value} + + + ); +}; + +export const ProjectStatusCell = ({ + projectStatus, +}: { + projectStatus?: ProjectStatus; +}) => { + const { t } = useTranslation(); + const classes = styles(); + const [open, setOpen] = useState(false); + + if (!projectStatus) { + return undefined; + } + + const modulesSummary = projectStatus.modulesSummary; + + let data: PieValueType[] = []; + let tooltipContent = <>; + if (modulesSummary) { + data = [ + { + label: t('module.summary.finished'), + value: modulesSummary.finished, + color: '#00C49F', + }, + { + label: t('module.summary.waiting'), + value: modulesSummary.waiting, + color: '#FFBB28', + }, + { + label: t('module.summary.pending'), + value: modulesSummary.pending, + color: '#FF8042', + }, + { + label: t('module.summary.running'), + value: modulesSummary.running, + color: '#FF8042', + }, + { + label: t('module.summary.error'), + value: modulesSummary.error, + color: '#FF0000', + }, + ]; + + tooltipContent = ( + + {t('module.summary.total')}} + value={modulesSummary.total} + /> + + + + + + + ); + } + + return ( + + {modulesSummary && ( + + setOpen(false)} + leaveDelay={1000} + placement="bottom" + arrow + title={tooltipContent} + > + { + event.stopPropagation(); + setOpen(!open); + }} + /> + + + )} + + + {t(`project.statuses.${projectStatus.state || 'none'}`)} + + + ); +}; diff --git a/workspaces/x2a/plugins/x2a/src/components/ProjectList/ProjectTable.tsx b/workspaces/x2a/plugins/x2a/src/components/ProjectList/ProjectTable.tsx index c12bcc2370c..3d3b050a616 100644 --- a/workspaces/x2a/plugins/x2a/src/components/ProjectList/ProjectTable.tsx +++ b/workspaces/x2a/plugins/x2a/src/components/ProjectList/ProjectTable.tsx @@ -35,6 +35,7 @@ import { useTranslation } from '../../hooks/useTranslation'; import { Repository } from './Repository'; import { OrderDirection } from './types'; import { DetailPanel } from './DetailPanel'; +import { ProjectStatusCell } from './ProjectStatusCell'; type ProjectTableProps = { forceRefresh: () => void; @@ -83,6 +84,7 @@ const useColumns = ( orderDirection: OrderDirection, ): TableColumn[] => { const { t } = useTranslation(); + return useMemo(() => { const getDefaultSort = (index: number): OrderDirection => { if (index === orderBy) { @@ -101,6 +103,9 @@ const useColumns = ( { title: t('table.columns.status'), field: 'status', + render: (rowData: Project) => ( + + ), defaultSort: getDefaultSort(1), }, { diff --git a/workspaces/x2a/plugins/x2a/src/translations/de.ts b/workspaces/x2a/plugins/x2a/src/translations/de.ts index 48378d59f2a..f5e233111c9 100644 --- a/workspaces/x2a/plugins/x2a/src/translations/de.ts +++ b/workspaces/x2a/plugins/x2a/src/translations/de.ts @@ -43,6 +43,13 @@ const x2aPluginTranslationDe = createTranslationMessages({ 'project.id': 'ID', 'project.abbreviation': 'Abkürzung', 'project.createdBy': 'Erstellt von', + 'project.statuses.none': '-', + 'project.statuses.created': 'Erstellt', + 'project.statuses.initializing': 'Wird initialisiert', + 'project.statuses.initialized': 'Initialisiert', + 'project.statuses.inProgress': 'In Bearbeitung', + 'project.statuses.completed': 'Abgeschlossen', + 'project.statuses.failed': 'Fehlgeschlagen', 'common.newProject': 'Neues Projekt', 'wizard.cancel': 'Abbrechen', 'wizard.back': 'Zurück', @@ -52,6 +59,12 @@ const x2aPluginTranslationDe = createTranslationMessages({ 'module.phases.analyze': 'Analysieren', 'module.phases.migrate': 'Migrieren', 'module.phases.publish': 'Veröffentlichen', + 'module.summary.total': 'Gesamt', + 'module.summary.finished': 'Abgeschlossen', + 'module.summary.waiting': 'Wartend', + 'module.summary.pending': 'Ausstehend', + 'module.summary.running': 'Läuft', + 'module.summary.error': 'Fehler', 'module.actions.runNextPhase': 'Nächste Phase ausführen', 'module.lastPhase': 'Letzte Phase', 'module.name': 'Name', @@ -62,6 +75,11 @@ const x2aPluginTranslationDe = createTranslationMessages({ 'module.finishedAt': 'Beendet am', 'artifact.types.migration_plan': 'Migrationsplan', 'artifact.types.module_migration_plan': 'Modulplan', + 'module.statuses.none': '-', + 'module.statuses.pending': 'Ausstehend', + 'module.statuses.running': 'Läuft', + 'module.statuses.success': 'Erfolg', + 'module.statuses.error': 'Fehler', 'artifact.types.migrated_sources': 'Migrierte Quellen', }, }); diff --git a/workspaces/x2a/plugins/x2a/src/translations/es.ts b/workspaces/x2a/plugins/x2a/src/translations/es.ts index 3a1f5ec0f2d..df77023ad9f 100644 --- a/workspaces/x2a/plugins/x2a/src/translations/es.ts +++ b/workspaces/x2a/plugins/x2a/src/translations/es.ts @@ -43,6 +43,13 @@ const x2aPluginTranslationEs = createTranslationMessages({ 'project.id': 'ID', 'project.abbreviation': 'Abreviatura', 'project.createdBy': 'Creado por', + 'project.statuses.none': '-', + 'project.statuses.created': 'Creado', + 'project.statuses.initializing': 'Inicializando', + 'project.statuses.initialized': 'Inicializado', + 'project.statuses.inProgress': 'En curso', + 'project.statuses.completed': 'Completado', + 'project.statuses.failed': 'Fallido', 'common.newProject': 'Nuevo proyecto', 'wizard.cancel': 'Cancelar', 'wizard.back': 'Atrás', @@ -52,6 +59,12 @@ const x2aPluginTranslationEs = createTranslationMessages({ 'module.phases.analyze': 'Analizar', 'module.phases.migrate': 'Migrar', 'module.phases.publish': 'Publicar', + 'module.summary.total': 'Total', + 'module.summary.finished': 'Finalizado', + 'module.summary.waiting': 'En espera', + 'module.summary.pending': 'Pendiente', + 'module.summary.running': 'En ejecución', + 'module.summary.error': 'Error', 'module.actions.runNextPhase': 'Ejecutar siguiente fase', 'module.lastPhase': 'Última fase', 'module.name': 'Nombre', @@ -62,6 +75,11 @@ const x2aPluginTranslationEs = createTranslationMessages({ 'module.finishedAt': 'Finalizado el', 'artifact.types.migration_plan': 'Plan de migración', 'artifact.types.module_migration_plan': 'Plan del módulo', + 'module.statuses.none': '-', + 'module.statuses.pending': 'Pendiente', + 'module.statuses.running': 'En ejecución', + 'module.statuses.success': 'Éxito', + 'module.statuses.error': 'Error', 'artifact.types.migrated_sources': 'Fuentes migradas', }, }); diff --git a/workspaces/x2a/plugins/x2a/src/translations/fr.ts b/workspaces/x2a/plugins/x2a/src/translations/fr.ts index abea9d47562..3f3d273785a 100644 --- a/workspaces/x2a/plugins/x2a/src/translations/fr.ts +++ b/workspaces/x2a/plugins/x2a/src/translations/fr.ts @@ -43,6 +43,13 @@ const x2aPluginTranslationFr = createTranslationMessages({ 'project.id': 'ID', 'project.abbreviation': 'Abréviation', 'project.createdBy': 'Créé par', + 'project.statuses.none': '-', + 'project.statuses.created': 'Créé', + 'project.statuses.initializing': 'Initialisation en cours', + 'project.statuses.initialized': 'Initialisé', + 'project.statuses.inProgress': 'En cours', + 'project.statuses.completed': 'Terminé', + 'project.statuses.failed': 'Échoué', 'common.newProject': 'Nouveau projet', 'wizard.cancel': 'Annuler', 'wizard.back': 'Retour', @@ -52,6 +59,12 @@ const x2aPluginTranslationFr = createTranslationMessages({ 'module.phases.analyze': 'Analyser', 'module.phases.migrate': 'Migrer', 'module.phases.publish': 'Publier', + 'module.summary.total': 'Total', + 'module.summary.finished': 'Terminé', + 'module.summary.waiting': 'En attente', + 'module.summary.pending': 'En attente', + 'module.summary.running': 'En cours', + 'module.summary.error': 'Erreur', 'module.actions.runNextPhase': 'Exécuter la phase suivante', 'module.lastPhase': 'Dernière phase', 'module.name': 'Nom', @@ -62,6 +75,11 @@ const x2aPluginTranslationFr = createTranslationMessages({ 'module.finishedAt': 'Terminé le', 'artifact.types.migration_plan': 'Plan de migration du projet', 'artifact.types.module_migration_plan': 'Plan du module', + 'module.statuses.none': '-', + 'module.statuses.pending': 'En attente', + 'module.statuses.running': 'En cours', + 'module.statuses.success': 'Succès', + 'module.statuses.error': 'Erreur', 'artifact.types.migrated_sources': 'Sources migrées', }, }); diff --git a/workspaces/x2a/plugins/x2a/src/translations/it.ts b/workspaces/x2a/plugins/x2a/src/translations/it.ts index 8b5ce2e5f86..3f567a1b468 100644 --- a/workspaces/x2a/plugins/x2a/src/translations/it.ts +++ b/workspaces/x2a/plugins/x2a/src/translations/it.ts @@ -43,6 +43,13 @@ const x2aPluginTranslationIt = createTranslationMessages({ 'project.id': 'ID', 'project.abbreviation': 'Abbreviazione', 'project.createdBy': 'Creato da', + 'project.statuses.none': '-', + 'project.statuses.created': 'Creato', + 'project.statuses.initializing': 'In inizializzazione', + 'project.statuses.initialized': 'Inizializzato', + 'project.statuses.inProgress': 'In corso', + 'project.statuses.completed': 'Completato', + 'project.statuses.failed': 'Fallito', 'common.newProject': 'Nuovo progetto', 'wizard.cancel': 'Annulla', 'wizard.back': 'Indietro', @@ -52,6 +59,12 @@ const x2aPluginTranslationIt = createTranslationMessages({ 'module.phases.analyze': 'Analizzare', 'module.phases.migrate': 'Migrare', 'module.phases.publish': 'Pubblicare', + 'module.summary.total': 'Totale', + 'module.summary.finished': 'Completato', + 'module.summary.waiting': 'In attesa', + 'module.summary.pending': 'In attesa', + 'module.summary.running': 'In esecuzione', + 'module.summary.error': 'Errore', 'module.actions.runNextPhase': 'Esegui fase successiva', 'module.lastPhase': 'Ultima fase', 'module.name': 'Nome', @@ -62,6 +75,11 @@ const x2aPluginTranslationIt = createTranslationMessages({ 'module.finishedAt': 'Terminato il', 'artifact.types.migration_plan': 'Piano di migrazione', 'artifact.types.module_migration_plan': 'Piano del modulo', + 'module.statuses.none': '-', + 'module.statuses.pending': 'In attesa', + 'module.statuses.running': 'In esecuzione', + 'module.statuses.success': 'Successo', + 'module.statuses.error': 'Errore', 'artifact.types.migrated_sources': 'Sorgenti migrate', }, }); diff --git a/workspaces/x2a/plugins/x2a/src/translations/ref.ts b/workspaces/x2a/plugins/x2a/src/translations/ref.ts index 98cbfff1098..423398df24b 100644 --- a/workspaces/x2a/plugins/x2a/src/translations/ref.ts +++ b/workspaces/x2a/plugins/x2a/src/translations/ref.ts @@ -54,6 +54,15 @@ export const x2aPluginMessages = { id: 'ID', abbreviation: 'Abbreviation', createdBy: 'Created By', + statuses: { + none: '-', + created: 'Created', + initializing: 'Initializing', + initialized: 'Initialized', + inProgress: 'In progress', + completed: 'Completed', + failed: 'Failed', + }, }, common: { newProject: 'New Project', @@ -71,6 +80,14 @@ export const x2aPluginMessages = { migrate: 'Migrate', publish: 'Publish', }, + summary: { + total: 'Total', + finished: 'Finished', + waiting: 'Waiting', + pending: 'Pending', + running: 'Running', + error: 'Error', + }, actions: { runNextPhase: 'Run Next Phase', }, @@ -81,6 +98,13 @@ export const x2aPluginMessages = { artifacts: 'Artifacts', startedAt: 'Started At', finishedAt: 'Finished At', + statuses: { + none: '-', + pending: 'Pending', + running: 'Running', + success: 'Success', + error: 'Error', + }, }, artifact: { types: { diff --git a/workspaces/x2a/tsconfig.json b/workspaces/x2a/tsconfig.json index eac19c5b926..e0ba8569c08 100644 --- a/workspaces/x2a/tsconfig.json +++ b/workspaces/x2a/tsconfig.json @@ -8,7 +8,7 @@ "plugins/*/dev", "plugins/*/migrations" ], - "exclude": ["node_modules"], + "exclude": ["node_modules", "**/node_modules"], "compilerOptions": { "outDir": "dist-types", "rootDir": ".", diff --git a/workspaces/x2a/yarn.lock b/workspaces/x2a/yarn.lock index ef3e73c244d..97932e4b2d1 100644 --- a/workspaces/x2a/yarn.lock +++ b/workspaces/x2a/yarn.lock @@ -8173,7 +8173,7 @@ __metadata: languageName: node linkType: hard -"@mui/utils@npm:^7.3.7": +"@mui/utils@npm:^7.3.5, @mui/utils@npm:^7.3.7": version: 7.3.7 resolution: "@mui/utils@npm:7.3.7" dependencies: @@ -8193,6 +8193,90 @@ __metadata: languageName: node linkType: hard +"@mui/x-charts-vendor@npm:8.26.0": + version: 8.26.0 + resolution: "@mui/x-charts-vendor@npm:8.26.0" + dependencies: + "@babel/runtime": ^7.28.4 + "@types/d3-array": ^3.2.2 + "@types/d3-color": ^3.1.3 + "@types/d3-format": ^3.0.4 + "@types/d3-interpolate": ^3.0.4 + "@types/d3-path": ^3.1.1 + "@types/d3-scale": ^4.0.9 + "@types/d3-shape": ^3.1.7 + "@types/d3-time": ^3.0.4 + "@types/d3-time-format": ^4.0.3 + "@types/d3-timer": ^3.0.2 + d3-array: ^3.2.4 + d3-color: ^3.1.0 + d3-format: ^3.1.0 + d3-interpolate: ^3.0.1 + d3-path: ^3.1.0 + d3-scale: ^4.0.2 + d3-shape: ^3.2.0 + d3-time: ^3.1.0 + d3-time-format: ^4.1.0 + d3-timer: ^3.0.1 + flatqueue: ^3.0.0 + internmap: ^2.0.3 + checksum: 0ba4e56b18a84eab339f371c1a63a288c6879b5c4ed7d2c5be1f33b074e393953bab3462a4850e373d136ce08d1ae14e7cd921a9258dd017a0e000868718c8cb + languageName: node + linkType: hard + +"@mui/x-charts@npm:^8.27.0": + version: 8.27.0 + resolution: "@mui/x-charts@npm:8.27.0" + dependencies: + "@babel/runtime": ^7.28.4 + "@mui/utils": ^7.3.5 + "@mui/x-charts-vendor": 8.26.0 + "@mui/x-internal-gestures": 0.4.0 + "@mui/x-internals": 8.26.0 + bezier-easing: ^2.1.0 + clsx: ^2.1.1 + prop-types: ^15.8.1 + reselect: ^5.1.1 + use-sync-external-store: ^1.6.0 + peerDependencies: + "@emotion/react": ^11.9.0 + "@emotion/styled": ^11.8.1 + "@mui/material": ^5.15.14 || ^6.0.0 || ^7.0.0 + "@mui/system": ^5.15.14 || ^6.0.0 || ^7.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + checksum: cd8c656683509053f006eee8a66f0763d59a093324dbe662b2fd86b685514741ac847cff865e38e38cd3627bde1f2ca77ec64da00496df38f64572ec3628e75a + languageName: node + linkType: hard + +"@mui/x-internal-gestures@npm:0.4.0": + version: 0.4.0 + resolution: "@mui/x-internal-gestures@npm:0.4.0" + dependencies: + "@babel/runtime": ^7.28.4 + checksum: d8c46e4d022d47a9c67779e8146b1471e2746a66393aa14411a19f90d4daabe69f3461a2d9f8355fadee149647a4af2f6a53566ebc277591eba4f46e8b72cc43 + languageName: node + linkType: hard + +"@mui/x-internals@npm:8.26.0": + version: 8.26.0 + resolution: "@mui/x-internals@npm:8.26.0" + dependencies: + "@babel/runtime": ^7.28.4 + "@mui/utils": ^7.3.5 + reselect: ^5.1.1 + use-sync-external-store: ^1.6.0 + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 153b84993c6365f51748be041c5fda27942bae9bec2e9324c64055d1df1a46c387dc703544e1b006a4d59c34aa9b9227b65a07ef37e4e0ae9dd73b3e7af5b4d1 + languageName: node + linkType: hard + "@n1ru4l/push-pull-async-iterable-iterator@npm:^3.1.0": version: 3.2.0 resolution: "@n1ru4l/push-pull-async-iterable-iterator@npm:3.2.0" @@ -11631,6 +11715,7 @@ __metadata: "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.61 "@mui/material": ^7.3.7 + "@mui/x-charts": ^8.27.0 "@red-hat-developer-hub/backstage-plugin-x2a-common": "workspace:*" "@testing-library/jest-dom": ^6.0.0 "@testing-library/react": ^14.0.0 @@ -14466,6 +14551,82 @@ __metadata: languageName: node linkType: hard +"@types/d3-array@npm:^3.2.2": + version: 3.2.2 + resolution: "@types/d3-array@npm:3.2.2" + checksum: 72e8e2abe0911cb431d6f3fe0a1f71b915356b679d4d9c826f52941bb30210c0fe8299dde066b08d9986754c620f031b13b13ab6dfc60d404eceab66a075dd5d + languageName: node + linkType: hard + +"@types/d3-color@npm:*, @types/d3-color@npm:^3.1.3": + version: 3.1.3 + resolution: "@types/d3-color@npm:3.1.3" + checksum: 8a0e79a709929502ec4effcee2c786465b9aec51b653ba0b5d05dbfec3e84f418270dd603002d94021885061ff592f614979193bd7a02ad76317f5608560e357 + languageName: node + linkType: hard + +"@types/d3-format@npm:^3.0.4": + version: 3.0.4 + resolution: "@types/d3-format@npm:3.0.4" + checksum: e69421cd93861a0c080084b0b23d4a5d6a427497559e46898189002fb756dae2c7c858b465308f6bcede7272b90e39ce8adab810bded2309035a5d9556c59134 + languageName: node + linkType: hard + +"@types/d3-interpolate@npm:^3.0.4": + version: 3.0.4 + resolution: "@types/d3-interpolate@npm:3.0.4" + dependencies: + "@types/d3-color": "*" + checksum: efd2770e174e84fc7316fdafe03cf3688451f767dde1fa6211610137f495be7f3923db7e1723a6961a0e0e9ae0ed969f4f47c038189fa0beb1d556b447922622 + languageName: node + linkType: hard + +"@types/d3-path@npm:*, @types/d3-path@npm:^3.1.1": + version: 3.1.1 + resolution: "@types/d3-path@npm:3.1.1" + checksum: fee8f6b0d3b28a3611c7d7fda3bf2f79392ded266f54b03a220f205c42117644bdcd33dcbf4853da3cca02229f1c669d2a60d5d297a24ce459ba8271ccb26c03 + languageName: node + linkType: hard + +"@types/d3-scale@npm:^4.0.9": + version: 4.0.9 + resolution: "@types/d3-scale@npm:4.0.9" + dependencies: + "@types/d3-time": "*" + checksum: c44265a38e538983686b1b8d159abfb4e81c09b33316f3a68f0f372d38400fa950ad531644d25230cc7b48ea5adb50270fc54823f088979ade62dcd0225f7aa3 + languageName: node + linkType: hard + +"@types/d3-shape@npm:^3.1.7": + version: 3.1.8 + resolution: "@types/d3-shape@npm:3.1.8" + dependencies: + "@types/d3-path": "*" + checksum: 659d51882dccc85d24817bdbcd50589212d12e24eb2aad19bae073665ed25443026e120966faa8523f0412f8a30f7c16002499cea3eb87d25b3011e0ee42e6a2 + languageName: node + linkType: hard + +"@types/d3-time-format@npm:^4.0.3": + version: 4.0.3 + resolution: "@types/d3-time-format@npm:4.0.3" + checksum: e981fc9780697a9d8c5d1ddf1167d9c6bc28e4e610afddff1384fe55e6eb52cb65309b2a0a1d4cf817413b0a80b9f1a652fe0b2cb8054ace4eafff80a6093aa5 + languageName: node + linkType: hard + +"@types/d3-time@npm:*, @types/d3-time@npm:^3.0.4": + version: 3.0.4 + resolution: "@types/d3-time@npm:3.0.4" + checksum: 0c296884571ce70c4bbd4ea9cd1c93c0c8aee602c6c806b056187dd4ee49daf70c2f41da94b25ba0d796edf8ca83cbb87fe6d1cdda7ca669ab800170ece1c12b + languageName: node + linkType: hard + +"@types/d3-timer@npm:^3.0.2": + version: 3.0.2 + resolution: "@types/d3-timer@npm:3.0.2" + checksum: 1643eebfa5f4ae3eb00b556bbc509444d88078208ec2589ddd8e4a24f230dd4cf2301e9365947e70b1bee33f63aaefab84cd907822aae812b9bc4871b98ab0e1 + languageName: node + linkType: hard + "@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.7": version: 4.1.12 resolution: "@types/debug@npm:4.1.12" @@ -16947,6 +17108,13 @@ __metadata: languageName: node linkType: hard +"bezier-easing@npm:^2.1.0": + version: 2.1.0 + resolution: "bezier-easing@npm:2.1.0" + checksum: ec22666b5385979b3e7a63eaf2631395b1be5beb3aed4f14cc70ae9eb09032082584f7e3eec045e4c122466987ed7e159f5b39f82b8a9870cd55e75b0d55a7d5 + languageName: node + linkType: hard + "bfj@npm:^8.0.0": version: 8.0.0 resolution: "bfj@npm:8.0.0" @@ -18963,7 +19131,16 @@ __metadata: languageName: node linkType: hard -"d3-color@npm:1 - 3": +"d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:^3.2.4": + version: 3.2.4 + resolution: "d3-array@npm:3.2.4" + dependencies: + internmap: 1 - 2 + checksum: a5976a6d6205f69208478bb44920dd7ce3e788c9dceb86b304dbe401a4bfb42ecc8b04c20facde486e9adcb488b5d1800d49393a3f81a23902b68158e12cddd0 + languageName: node + linkType: hard + +"d3-color@npm:1 - 3, d3-color@npm:^3.1.0": version: 3.1.0 resolution: "d3-color@npm:3.1.0" checksum: 4931fbfda5d7c4b5cfa283a13c91a954f86e3b69d75ce588d06cde6c3628cebfc3af2069ccf225e982e8987c612aa7948b3932163ce15eb3c11cd7c003f3ee3b @@ -18994,7 +19171,14 @@ __metadata: languageName: node linkType: hard -"d3-interpolate@npm:1 - 3": +"d3-format@npm:1 - 3, d3-format@npm:^3.1.0": + version: 3.1.2 + resolution: "d3-format@npm:3.1.2" + checksum: 2ce13417b3186311df3fd924028cd516ec3e96d7c3eb6df9c83f6c2ed43de1717e6c5119a385b7744ef84e2b8a4c678ad95a2b2998391803ceb0d809e235cff4 + languageName: node + linkType: hard + +"d3-interpolate@npm:1 - 3, d3-interpolate@npm:1.2.0 - 3, d3-interpolate@npm:^3.0.1": version: 3.0.1 resolution: "d3-interpolate@npm:3.0.1" dependencies: @@ -19010,6 +19194,19 @@ __metadata: languageName: node linkType: hard +"d3-scale@npm:^4.0.2": + version: 4.0.2 + resolution: "d3-scale@npm:4.0.2" + dependencies: + d3-array: 2.10.0 - 3 + d3-format: 1 - 3 + d3-interpolate: 1.2.0 - 3 + d3-time: 2.1.1 - 3 + d3-time-format: 2 - 4 + checksum: a9c770d283162c3bd11477c3d9d485d07f8db2071665f1a4ad23eec3e515e2cefbd369059ec677c9ac849877d1a765494e90e92051d4f21111aa56791c98729e + languageName: node + linkType: hard + "d3-selection@npm:2 - 3, d3-selection@npm:3, d3-selection@npm:^3.0.0": version: 3.0.0 resolution: "d3-selection@npm:3.0.0" @@ -19017,7 +19214,7 @@ __metadata: languageName: node linkType: hard -"d3-shape@npm:^3.0.0": +"d3-shape@npm:^3.0.0, d3-shape@npm:^3.2.0": version: 3.2.0 resolution: "d3-shape@npm:3.2.0" dependencies: @@ -19026,7 +19223,25 @@ __metadata: languageName: node linkType: hard -"d3-timer@npm:1 - 3": +"d3-time-format@npm:2 - 4, d3-time-format@npm:^4.1.0": + version: 4.1.0 + resolution: "d3-time-format@npm:4.1.0" + dependencies: + d3-time: 1 - 3 + checksum: 7342bce28355378152bbd4db4e275405439cabba082d9cd01946d40581140481c8328456d91740b0fe513c51ec4a467f4471ffa390c7e0e30ea30e9ec98fcdf4 + languageName: node + linkType: hard + +"d3-time@npm:1 - 3, d3-time@npm:2.1.1 - 3, d3-time@npm:^3.1.0": + version: 3.1.0 + resolution: "d3-time@npm:3.1.0" + dependencies: + d3-array: 2 - 3 + checksum: 613b435352a78d9f31b7f68540788186d8c331b63feca60ad21c88e9db1989fe888f97f242322ebd6365e45ec3fb206a4324cd4ca0dfffa1d9b5feb856ba00a7 + languageName: node + linkType: hard + +"d3-timer@npm:1 - 3, d3-timer@npm:^3.0.1": version: 3.0.1 resolution: "d3-timer@npm:3.0.1" checksum: 1cfddf86d7bca22f73f2c427f52dfa35c49f50d64e187eb788dcad6e927625c636aa18ae4edd44d084eb9d1f81d8ca4ec305dae7f733c15846a824575b789d73 @@ -21390,6 +21605,13 @@ __metadata: languageName: node linkType: hard +"flatqueue@npm:^3.0.0": + version: 3.0.0 + resolution: "flatqueue@npm:3.0.0" + checksum: fb1129fa8acc2255e42430345e46d4a5430ed45772f4e1c3ebf9fd2c7f16d9dae8a93cf0d651fd6c22a5e437d31c6971ff633bba41b60426cb7fdfdbddaa06bf + languageName: node + linkType: hard + "flatstr@npm:^1.0.12": version: 1.0.12 resolution: "flatstr@npm:1.0.12" @@ -23357,6 +23579,13 @@ __metadata: languageName: node linkType: hard +"internmap@npm:1 - 2, internmap@npm:^2.0.3": + version: 2.0.3 + resolution: "internmap@npm:2.0.3" + checksum: 7ca41ec6aba8f0072fc32fa8a023450a9f44503e2d8e403583c55714b25efd6390c38a87161ec456bf42d7bc83aab62eb28f5aef34876b1ac4e60693d5e1d241 + languageName: node + linkType: hard + "interpret@npm:^2.2.0": version: 2.2.0 resolution: "interpret@npm:2.2.0"