diff --git a/workspaces/x2a/.changeset/clean-books-wish.md b/workspaces/x2a/.changeset/clean-books-wish.md new file mode 100644 index 0000000000..adc60326cb --- /dev/null +++ b/workspaces/x2a/.changeset/clean-books-wish.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 +--- + +Adding ModulePage with details, former phase retrigger and logs. diff --git a/workspaces/x2a/.changeset/huge-windows-slide.md b/workspaces/x2a/.changeset/huge-windows-slide.md new file mode 100644 index 0000000000..5563076911 --- /dev/null +++ b/workspaces/x2a/.changeset/huge-windows-slide.md @@ -0,0 +1,6 @@ +--- +'@red-hat-developer-hub/backstage-plugin-x2a-backend': patch +'@red-hat-developer-hub/backstage-plugin-x2a-common': patch +--- + +Add GET /projects/:projectId/modules/:moduleId handler 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 8a855a29f6..0d2cac2235 100644 --- a/workspaces/x2a/plugins/x2a-backend/src/router/modules.test.ts +++ b/workspaces/x2a/plugins/x2a-backend/src/router/modules.test.ts @@ -25,6 +25,7 @@ import { createTestModule, createTestProject, mockInputProject, + mockProject2, supportedDatabaseIds, tearDownRouters, } from './__testUtils__/routerTestHelpers'; @@ -181,6 +182,222 @@ describe('createRouter – modules', () => { ); }); + describe('GET /projects/:projectId/modules/:moduleId', () => { + it.each(supportedDatabaseIds)( + 'should return 200 and module when project and module exist - %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); + const module = await createTestModule(x2aDatabase, project.id, { + name: 'Single Module', + sourcePath: '/single', + }); + + const response = await request(app) + .get(`/projects/${project.id}/modules/${module.id}`) + .send(); + + expect(response.status).toBe(200); + expect(response.body).toMatchObject({ + id: module.id, + name: 'Single Module', + sourcePath: '/single', + projectId: project.id, + }); + expect(response.body.analyze).toBeUndefined(); + expect(response.body.migrate).toBeUndefined(); + expect(response.body.publish).toBeUndefined(); + }, + LONG_TEST_TIMEOUT, + ); + + it.each(supportedDatabaseIds)( + 'should include last analyze/migrate/publish jobs when jobs exist - %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); + const module = await createTestModule(x2aDatabase, project.id, { + name: 'Module With Jobs', + sourcePath: '/with-jobs', + }); + + await createTestJob(x2aDatabase, { + projectId: project.id, + moduleId: module.id, + phase: 'analyze', + status: 'success', + }); + await createTestJob(x2aDatabase, { + projectId: project.id, + moduleId: module.id, + phase: 'migrate', + status: 'running', + }); + await createTestJob(x2aDatabase, { + projectId: project.id, + moduleId: module.id, + phase: 'publish', + status: 'pending', + }); + + const response = await request(app) + .get(`/projects/${project.id}/modules/${module.id}`) + .send(); + + expect(response.status).toBe(200); + expect(response.body.name).toBe('Module With Jobs'); + expect(response.body.analyze).toBeDefined(); + expect(response.body.analyze.status).toBe('success'); + expect(response.body.migrate).toBeDefined(); + expect(response.body.migrate.status).toBe('running'); + expect(response.body.publish).toBeDefined(); + expect(response.body.publish.status).toBe('pending'); + }, + LONG_TEST_TIMEOUT, + ); + + it.each(supportedDatabaseIds)( + 'should never return callbackToken in analyze, migrate or publish jobs - %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); + const module = await createTestModule(x2aDatabase, project.id); + + await createTestJob(x2aDatabase, { + projectId: project.id, + moduleId: module.id, + phase: 'analyze', + status: 'success', + }); + await createTestJob(x2aDatabase, { + projectId: project.id, + moduleId: module.id, + phase: 'migrate', + status: 'success', + }); + await createTestJob(x2aDatabase, { + projectId: project.id, + moduleId: module.id, + phase: 'publish', + status: 'success', + }); + + const response = await request(app) + .get(`/projects/${project.id}/modules/${module.id}`) + .send(); + + expect(response.status).toBe(200); + expect(response.body.analyze).toBeDefined(); + expect(response.body.analyze).not.toHaveProperty('callbackToken'); + expect(response.body.migrate).toBeDefined(); + expect(response.body.migrate).not.toHaveProperty('callbackToken'); + expect(response.body.publish).toBeDefined(); + expect(response.body.publish).not.toHaveProperty('callbackToken'); + }, + LONG_TEST_TIMEOUT, + ); + + it.each(supportedDatabaseIds)( + 'should return 404 when project does not exist - %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); + const module = await createTestModule(x2aDatabase, project.id); + + const response = await request(app) + .get(`/projects/${nonExistentId}/modules/${module.id}`) + .send(); + + expect(response.status).toBe(404); + expect(response.body).toMatchObject({ + error: { + name: 'NotFoundError', + message: expect.stringContaining('not found'), + }, + }); + }, + ); + + it.each(supportedDatabaseIds)( + 'should return 404 when module does not exist - %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); + + const response = await request(app) + .get(`/projects/${project.id}/modules/${nonExistentId}`) + .send(); + + expect(response.status).toBe(404); + expect(response.body).toMatchObject({ + error: { + name: 'NotFoundError', + message: expect.stringContaining('not found'), + }, + }); + }, + ); + + it.each(supportedDatabaseIds)( + 'should return 404 when module belongs to different project - %p', + async databaseId => { + const { client } = await createDatabase(databaseId); + const x2aDatabase = X2ADatabaseService.create({ + logger: mockServices.logger.mock(), + dbClient: client, + }); + const app = await createApp(client); + const project1 = await createTestProject(x2aDatabase); + const project2 = await createTestProject(x2aDatabase, mockProject2); + const moduleOfProject2 = await createTestModule( + x2aDatabase, + project2.id, + { name: 'Other Project Module', sourcePath: '/other' }, + ); + + const response = await request(app) + .get(`/projects/${project1.id}/modules/${moduleOfProject2.id}`) + .send(); + + expect(response.status).toBe(404); + expect(response.body).toMatchObject({ + error: { + name: 'NotFoundError', + message: expect.stringMatching( + /does not belong to project|not found/, + ), + }, + }); + }, + LONG_TEST_TIMEOUT, + ); + }); + describe('POST /projects/:projectId/modules', () => { it.each(supportedDatabaseIds)( 'should create a module and return 201 - %p', diff --git a/workspaces/x2a/plugins/x2a-backend/src/router/modules.ts b/workspaces/x2a/plugins/x2a-backend/src/router/modules.ts index 7466aefe39..04b84e811c 100644 --- a/workspaces/x2a/plugins/x2a-backend/src/router/modules.ts +++ b/workspaces/x2a/plugins/x2a-backend/src/router/modules.ts @@ -100,6 +100,65 @@ export function registerModuleRoutes( res.json(response); }); + router.get('/projects/:projectId/modules/:moduleId', async (req, res) => { + const endpoint = 'GET /projects/:projectId/modules/:moduleId'; + const { projectId, moduleId } = req.params; + logger.info( + `${endpoint} request received: projectId=${projectId}, moduleId=${moduleId}`, + ); + + // Get user credentials + const credentials = await httpAuth.credentials(req, { allow: ['user'] }); + + // Verify project exists and the user is permitted to access it + const project = await x2aDatabase.getProject( + { projectId }, + { credentials }, + ); + if (!project) { + throw new NotFoundError(`Project "${projectId}" not found.`); + } + + // Get module + const module = await x2aDatabase.getModule({ id: moduleId }); + if (!module) { + throw new NotFoundError(`Module "${moduleId}" not found.`); + } + if (module.projectId !== projectId) { + throw new NotFoundError( + `Module "${moduleId}" does not belong to project "${projectId}".`, + ); + } + + // Fetch last jobs + const lastAnalyzeJobsOfModule = await x2aDatabase.listJobs({ + projectId, + moduleId, + phase: 'analyze', + lastJobOnly: true, + }); + const lastMigrateJobsOfModule = await x2aDatabase.listJobs({ + projectId, + moduleId, + phase: 'migrate', + lastJobOnly: true, + }); + const lastPublishJobsOfModule = await x2aDatabase.listJobs({ + projectId, + moduleId, + phase: 'publish', + lastJobOnly: true, + }); + + // Update module with last jobs + module.analyze = removeSensitiveFromJob(lastAnalyzeJobsOfModule[0]); + module.migrate = removeSensitiveFromJob(lastMigrateJobsOfModule[0]); + module.publish = removeSensitiveFromJob(lastPublishJobsOfModule[0]); + + // TODO: calculate module's status from the last jobs + res.json(module); + }); + // TODO: This is a TEMPORARY endpoint for testing only. // According to the ADR (lines 202-213), this endpoint should sync modules by: // 1. Fetching the migration project plan from the target repo diff --git a/workspaces/x2a/plugins/x2a-backend/src/schema/openapi.yaml b/workspaces/x2a/plugins/x2a-backend/src/schema/openapi.yaml index 3da4b926a2..e789a610d1 100644 --- a/workspaces/x2a/plugins/x2a-backend/src/schema/openapi.yaml +++ b/workspaces/x2a/plugins/x2a-backend/src/schema/openapi.yaml @@ -261,6 +261,30 @@ paths: '404': description: Project not found + /projects/{projectId}/modules/{moduleId}: + get: + summary: Returns a module by ID + parameters: + - in: path + name: projectId + schema: + type: string + required: true + - in: path + name: moduleId + schema: + type: string + required: true + responses: + '200': + description: A single module by ID + content: + application/json: + schema: + $ref: '#/components/schemas/Module' + '404': + description: Project or module not found + /projects/{projectId}/modules/{moduleId}/run: post: summary: Triggers a migration phase for a specific module @@ -428,6 +452,8 @@ paths: description: Project or job not found '400': description: Invalid request data + +### Components components: schemas: Project: diff --git a/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/apis/Api.server.ts b/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/apis/Api.server.ts index 8627b40e28..61c47dcdcd 100644 --- a/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/apis/Api.server.ts +++ b/workspaces/x2a/plugins/x2a-backend/src/schema/openapi/generated/apis/Api.server.ts @@ -99,6 +99,16 @@ export type ProjectsProjectIdModulesGet = { }; response: Array | void; }; +/** + * @public + */ +export type ProjectsProjectIdModulesModuleIdGet = { + path: { + projectId: string; + moduleId: string; + }; + response: Module | void; +}; /** * @public */ @@ -158,6 +168,8 @@ export type EndpointMap = { '#get|/projects/{projectId}/modules': ProjectsProjectIdModulesGet; + '#get|/projects/{projectId}/modules/{moduleId}': ProjectsProjectIdModulesModuleIdGet; + '#get|/projects/{projectId}/modules/{moduleId}/log': ProjectsProjectIdModulesModuleIdLogGet; '#post|/projects/{projectId}/modules/{moduleId}/run': ProjectsProjectIdModulesModuleIdRunPost; 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 40bf256796..0949fee1b5 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 @@ -404,6 +404,44 @@ export const spec = { } } }, + "/projects/{projectId}/modules/{moduleId}": { + "get": { + "summary": "Returns a module by ID", + "parameters": [ + { + "in": "path", + "name": "projectId", + "schema": { + "type": "string" + }, + "required": true + }, + { + "in": "path", + "name": "moduleId", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "A single module by ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Module" + } + } + } + }, + "404": { + "description": "Project or module not found" + } + } + } + }, "/projects/{projectId}/modules/{moduleId}/run": { "post": { "summary": "Triggers a migration phase for a specific module", diff --git a/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/apis/Api.client.ts b/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/apis/Api.client.ts index 86634ef66a..562ee777b1 100644 --- a/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/apis/Api.client.ts +++ b/workspaces/x2a/plugins/x2a-common/client/src/schema/openapi/generated/apis/Api.client.ts @@ -113,6 +113,15 @@ export type ProjectsProjectIdModulesGet = { projectId: string; }; }; +/** + * @public + */ +export type ProjectsProjectIdModulesModuleIdGet = { + path: { + projectId: string; + moduleId: string; + }; +}; /** * @public */ @@ -334,6 +343,34 @@ export class DefaultApiClient { }); } + /** + * Returns a module by ID + * @param projectId - + * @param moduleId - + */ + public async projectsProjectIdModulesModuleIdGet( + // @ts-ignore + request: ProjectsProjectIdModulesModuleIdGet, + options?: RequestOptions, + ): Promise> { + const baseUrl = await this.discoveryApi.getBaseUrl(pluginId); + + const uriTemplate = `/projects/{projectId}/modules/{moduleId}`; + + const uri = parser.parse(uriTemplate).expand({ + projectId: request.path.projectId, + moduleId: request.path.moduleId, + }); + + return await this.fetchApi.fetch(`${baseUrl}${uri}`, { + headers: { + 'Content-Type': 'application/json', + ...(options?.token && { Authorization: `Bearer ${options?.token}` }), + }, + method: 'GET', + }); + } + /** * Returns logs for the latest job of a module * @param projectId - Project UUID diff --git a/workspaces/x2a/plugins/x2a/package.json b/workspaces/x2a/plugins/x2a/package.json index 7a443031ad..0460f88107 100644 --- a/workspaces/x2a/plugins/x2a/package.json +++ b/workspaces/x2a/plugins/x2a/package.json @@ -39,6 +39,7 @@ "@backstage/core-plugin-api": "^1.12.0", "@backstage/plugin-catalog": "^1.32.0", "@backstage/theme": "^0.7.0", + "@backstage/ui": "^0.9.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", diff --git a/workspaces/x2a/plugins/x2a/src/components/ItemField.tsx b/workspaces/x2a/plugins/x2a/src/components/ItemField.tsx new file mode 100644 index 0000000000..21d1665600 --- /dev/null +++ b/workspaces/x2a/plugins/x2a/src/components/ItemField.tsx @@ -0,0 +1,34 @@ +/** + * 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 { AboutField } from '@backstage/plugin-catalog'; +import { Typography } from '@material-ui/core'; + +export const ItemField = ({ + label, + value, +}: { + label: string; + value: string | React.ReactNode; +}) => { + return ( + + + {typeof value === 'string' ? {value} : value} + + + ); +}; diff --git a/workspaces/x2a/plugins/x2a/src/components/ModulePage/ArtifactsCard.tsx b/workspaces/x2a/plugins/x2a/src/components/ModulePage/ArtifactsCard.tsx new file mode 100644 index 0000000000..0a9eaac78b --- /dev/null +++ b/workspaces/x2a/plugins/x2a/src/components/ModulePage/ArtifactsCard.tsx @@ -0,0 +1,87 @@ +/* + * 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 { Card, CardBody, CardHeader } from '@backstage/ui'; +import { + Artifact, + Module, +} from '@red-hat-developer-hub/backstage-plugin-x2a-common'; +import { Grid } from '@material-ui/core'; + +import { useTranslation } from '../../hooks/useTranslation'; +import { ItemField } from '../ItemField'; +import { Link } from '@backstage/core-components'; +import { buildArtifactUrl, humanizeArtifactType } from '../tools'; + +const ArtifactLink = ({ artifact }: { artifact?: Artifact }) => { + const { t } = useTranslation(); + if (!artifact) { + return t('module.phases.none'); + } + return ( + + {humanizeArtifactType(t, artifact.type)} + + ); +}; + +export const ArtifactsCard = ({ module }: { module?: Module }) => { + const { t } = useTranslation(); + + const migrationPlanArtifact = undefined; // TODO: from project + const moduleMigrationPlanArtifact = module?.analyze?.artifacts?.find( + artifact => artifact.type === 'module_migration_plan', + ); + const migratedSourcesArtifact = module?.migrate?.artifacts?.find( + artifact => artifact.type === 'migrated_sources', + ); + + return ( + + {t('modulePage.artifacts.title')} + + + + } + /> + + + } + /> + + + } + /> + + + {t('modulePage.artifacts.description')} + + + + + ); +}; diff --git a/workspaces/x2a/plugins/x2a/src/components/ModulePage/ModuleDetailsCard.tsx b/workspaces/x2a/plugins/x2a/src/components/ModulePage/ModuleDetailsCard.tsx new file mode 100644 index 0000000000..19b3ab75c1 --- /dev/null +++ b/workspaces/x2a/plugins/x2a/src/components/ModulePage/ModuleDetailsCard.tsx @@ -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. + */ + +import { Card, CardBody, CardHeader } from '@backstage/ui'; +import { Module } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; +import { useTranslation } from '../../hooks/useTranslation'; + +export const ModuleDetailsCard = ({ module }: { module?: Module }) => { + const { t } = useTranslation(); + return ( + + Module Details + TODO: Module details, Expected next action hint. + + ); +}; diff --git a/workspaces/x2a/plugins/x2a/src/components/ModulePage/ModulePage.tsx b/workspaces/x2a/plugins/x2a/src/components/ModulePage/ModulePage.tsx new file mode 100644 index 0000000000..9c32c53cfa --- /dev/null +++ b/workspaces/x2a/plugins/x2a/src/components/ModulePage/ModulePage.tsx @@ -0,0 +1,71 @@ +/* + * 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 useAsync from 'react-use/lib/useAsync'; +import { useRouteRefParams } from '@backstage/core-plugin-api'; +import { Content, Header, Page } from '@backstage/core-components'; +import { Grid } from '@material-ui/core'; + +import { moduleRouteRef } from '../../routes'; +import { useClientService } from '../../ClientService'; +import { useTranslation } from '../../hooks/useTranslation'; +import { ArtifactsCard } from './ArtifactsCard'; +import { ModuleDetailsCard } from './ModuleDetailsCard'; +import { PhasesCard } from './PhasesCard'; +import { ModulePageBreadcrumb } from './ModulePageBreadcrumb'; + +export const ModulePage = () => { + const { projectId, moduleId } = useRouteRefParams(moduleRouteRef); + const clientService = useClientService(); + const { t } = useTranslation(); + + const { + loading: moduleLoading, + error: moduleError, + value: module, + } = useAsync(async () => { + const response = await clientService.projectsProjectIdModulesModuleIdGet({ + path: { projectId, moduleId }, + }); + return await response.json(); + }, [moduleId]); + + return ( + +
+ +

{t('modulePage.title')}

+ + } + /> + + + + + + + + + + + + + + + + ); +}; diff --git a/workspaces/x2a/plugins/x2a/src/components/ModulePage/ModulePageBreadcrumb.tsx b/workspaces/x2a/plugins/x2a/src/components/ModulePage/ModulePageBreadcrumb.tsx new file mode 100644 index 0000000000..f30279e59d --- /dev/null +++ b/workspaces/x2a/plugins/x2a/src/components/ModulePage/ModulePageBreadcrumb.tsx @@ -0,0 +1,44 @@ +/** + * 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 { Breadcrumbs, Link } from '@backstage/core-components'; +import { makeStyles, Typography } from '@material-ui/core'; +import { useTranslation } from '../../hooks/useTranslation'; + +const useStyles = makeStyles(() => ({ + breadcrumbs: { + '& p': { + textDecoration: 'none', + }, + }, +})); + +export const ModulePageBreadcrumb = () => { + const { t } = useTranslation(); + const classes = useStyles(); + + return ( + + {t('page.title')} + {t('modulePage.title')} + Breadcrumbs + + ); +}; diff --git a/workspaces/x2a/plugins/x2a/src/components/ModulePage/PhasesCard.tsx b/workspaces/x2a/plugins/x2a/src/components/ModulePage/PhasesCard.tsx new file mode 100644 index 0000000000..2b97da66a7 --- /dev/null +++ b/workspaces/x2a/plugins/x2a/src/components/ModulePage/PhasesCard.tsx @@ -0,0 +1,177 @@ +/* + * 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 { + Button, + Card, + CardBody, + Tab, + TabList, + TabPanel, + Tabs, +} from '@backstage/ui'; +import { Grid, makeStyles, Typography } from '@material-ui/core'; +import { + Job, + Module, +} from '@red-hat-developer-hub/backstage-plugin-x2a-common'; +import { useTranslation } from '../../hooks/useTranslation'; +import { ItemField } from '../ItemField'; +import { humanizeDate } from '../tools'; + +const useStyles = makeStyles(() => ({ + tab: { + width: '33%', + }, +})); + +const RunAction = ({ + instructions, + actionText, +}: { + instructions: string; + actionText: string; +}) => { + return ( + <> + + + + + {instructions} + + + ); +}; + +const PhaseDetails = ({ phase }: { phase?: Job }) => { + const { t } = useTranslation(); + const empty = t('module.phases.none'); + + // TODO: preselect the last tab when entering the page + /* + Details:Duration, logs +*/ + const duration = 'TODO: Duration'; + + return ( + + {phase && ( + + )} + {!phase && ( + + )} + {/* TODO: Button for canceling the current job execution */} + + + + + + + + + + + + + + + + + + + + + + {/* Telemetry */} + + ); +}; + +export const PhasesCard = ({ module }: { module?: Module }) => { + const { t } = useTranslation(); + const classes = useStyles(); + + const analyzePhase = module?.analyze; + const migratePhase = module?.migrate; + const publishPhase = module?.publish; + + const moduleMigrationPlanArtifact = analyzePhase?.artifacts?.find( + artifact => artifact.type === 'module_migration_plan', + ); + const migratedSourcesArtifact = migratePhase?.artifacts?.find( + artifact => artifact.type === 'migrated_sources', + ); + + return ( + + + + + + {t('module.phases.analyze')} + + + {t('module.phases.migrate')} + + + {t('module.phases.publish')} + + + + + + + + + + + + + + + ); +}; diff --git a/workspaces/x2a/plugins/x2a/src/components/ModulePage/index.ts b/workspaces/x2a/plugins/x2a/src/components/ModulePage/index.ts new file mode 100644 index 0000000000..244f34405f --- /dev/null +++ b/workspaces/x2a/plugins/x2a/src/components/ModulePage/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { ModulePage } from './ModulePage'; diff --git a/workspaces/x2a/plugins/x2a/src/components/ModuleTable/Artifacts.tsx b/workspaces/x2a/plugins/x2a/src/components/ModuleTable/Artifacts.tsx index a9fe44108e..2534796437 100644 --- a/workspaces/x2a/plugins/x2a/src/components/ModuleTable/Artifacts.tsx +++ b/workspaces/x2a/plugins/x2a/src/components/ModuleTable/Artifacts.tsx @@ -32,6 +32,7 @@ export const ArtifactLink = ({ targetRepoUrl, }: { artifact: Artifact; + // TODO: the targetRepoUrl is probably not needed, the artifact.value should contain full URL targetRepoUrl: string; }) => { const classes = styles(); diff --git a/workspaces/x2a/plugins/x2a/src/components/ModuleTable/ModuleTable.tsx b/workspaces/x2a/plugins/x2a/src/components/ModuleTable/ModuleTable.tsx index 9e3e9314e3..2c1387c7fb 100644 --- a/workspaces/x2a/plugins/x2a/src/components/ModuleTable/ModuleTable.tsx +++ b/workspaces/x2a/plugins/x2a/src/components/ModuleTable/ModuleTable.tsx @@ -21,7 +21,8 @@ import { ModulePhase, Project, } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; -import { Table, TableColumn } from '@backstage/core-components'; +import { Link, Table, TableColumn } from '@backstage/core-components'; +import { useRouteRef } from '@backstage/core-plugin-api'; import PlayArrowIcon from '@material-ui/icons/PlayArrow'; import { MaterialTableProps } from '@material-table/core/types'; import Alert from '@material-ui/lab/Alert'; @@ -32,6 +33,7 @@ import { useClientService } from '../../ClientService'; import { Artifacts } from './Artifacts'; import { humanizeDate } from '../tools'; import { getAuthTokenDescriptor, useRepoAuthentication } from '../../repoAuth'; +import { moduleRouteRef } from '../../routes'; const getLastJob = (rowData: Module) => { const phases: ('publish' | 'migrate' | 'analyze')[] = [ @@ -66,6 +68,7 @@ const useColumns = ({ targetRepoUrl: string; }): TableColumn[] => { const { t } = useTranslation(); + const modulePath = useRouteRef(moduleRouteRef); const lastPhaseCell = useCallback( (rowData: Module) => { @@ -93,7 +96,7 @@ const useColumns = ({ if (!lastJob) { return
{t('module.phases.none')}
; } - const formatted = humanizeDate(new Date(lastJob.startedAt)); + const formatted = humanizeDate(lastJob.startedAt); return
{formatted}
; }, [t], @@ -104,15 +107,31 @@ const useColumns = ({ if (!lastJob?.finishedAt) { return
{t('module.phases.none')}
; } - const formatted = humanizeDate(new Date(lastJob.finishedAt)); + const formatted = humanizeDate(lastJob.finishedAt); return
{formatted}
; }, [t], ); + const nameCell = useCallback( + (rowData: Module) => { + return ( + + {rowData.name} + + ); + }, + [modulePath], + ); + return useMemo(() => { return [ - { field: 'name', title: t('module.name') }, + { render: nameCell, title: t('module.name') }, { field: 'status', title: t('module.status') }, { field: 'sourcePath', title: t('module.sourcePath') }, { render: lastPhaseCell, title: t('module.lastPhase') }, @@ -120,7 +139,14 @@ const useColumns = ({ { render: startedAtCell, title: t('module.startedAt') }, { render: finishedAtCell, title: t('module.finishedAt') }, ]; - }, [t, lastPhaseCell, artifactsCell, startedAtCell, finishedAtCell]); + }, [ + t, + lastPhaseCell, + artifactsCell, + startedAtCell, + finishedAtCell, + nameCell, + ]); }; const canRunNextPhase = ({ module }: { module: Module }) => { diff --git a/workspaces/x2a/plugins/x2a/src/components/NewProjectPage.tsx.toBeDeleted b/workspaces/x2a/plugins/x2a/src/components/NewProjectPage.tsx.toBeDeleted new file mode 100644 index 0000000000..501247c959 --- /dev/null +++ b/workspaces/x2a/plugins/x2a/src/components/NewProjectPage.tsx.toBeDeleted @@ -0,0 +1,88 @@ +/* + * 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 { Content, Header, InfoCard, Page } from '@backstage/core-components'; +import { Grid, Step, StepLabel, Stepper } from '@material-ui/core'; +import { useMemo, useState } from 'react'; +import { useTranslation } from '../../hooks/useTranslation'; +import { WizardActions } from './WizardActions'; +import { basePath } from '../../routes'; + +export const NewProjectPage = () => { + const [activeStep, setActiveStep] = useState(0); + const { t } = useTranslation(); + + const steps = useMemo( + () => [ + { + title: t('newProjectPage.steps.jobNameAndDescription'), + content:
{t('newProjectPage.steps.jobNameAndDescription')}
, + }, + { + title: t('newProjectPage.steps.sourceAndTargetRepos'), + content:
{t('newProjectPage.steps.sourceAndTargetRepos')}
, + }, + { + title: t('newProjectPage.steps.reviewAndStart'), + content:
{t('newProjectPage.steps.lastStep')}
, + }, + ], + [t], + ); + + return ( + +
+ + + + + + {steps.map(step => ( + + + {step.title} + + + ))} + + } + actions={ + 0} + onCancelLink={basePath} + onBack={() => setActiveStep(activeStep - 1)} + onNext={() => setActiveStep(activeStep + 1)} + /> + } + > + {steps[activeStep].content} + + + + + + ); +}; diff --git a/workspaces/x2a/plugins/x2a/src/components/ProjectList/DetailPanel.tsx b/workspaces/x2a/plugins/x2a/src/components/ProjectList/DetailPanel.tsx index 05c73c320b..35bb0a4df9 100644 --- a/workspaces/x2a/plugins/x2a/src/components/ProjectList/DetailPanel.tsx +++ b/workspaces/x2a/plugins/x2a/src/components/ProjectList/DetailPanel.tsx @@ -24,6 +24,7 @@ import { useClientService } from '../../ClientService'; import { Progress, ResponseErrorPanel } from '@backstage/core-components'; import { ModuleTable } from '../ModuleTable'; import { ArtifactLink } from '../ModuleTable/Artifacts'; +import { ItemField } from '../ItemField'; const useStyles = makeStyles(() => ({ detailPanel: { @@ -36,22 +37,6 @@ const gridItemProps: GridProps = { item: true, }; -const ItemField = ({ - label, - value, -}: { - label: string; - value: string | React.ReactNode; -}) => { - return ( - - - {typeof value === 'string' ? {value} : value} - - - ); -}; - export const DetailPanel = ({ project }: { project: Project }) => { const { t } = useTranslation(); const styles = useStyles(); diff --git a/workspaces/x2a/plugins/x2a/src/components/ProjectList/EmptyProjectList.tsx b/workspaces/x2a/plugins/x2a/src/components/ProjectList/EmptyProjectList.tsx index 98f34525b9..c93d21012e 100644 --- a/workspaces/x2a/plugins/x2a/src/components/ProjectList/EmptyProjectList.tsx +++ b/workspaces/x2a/plugins/x2a/src/components/ProjectList/EmptyProjectList.tsx @@ -17,6 +17,7 @@ import { makeStyles } from '@material-ui/core'; import { EmptyState, LinkButton } from '@backstage/core-components'; import emptyProjectListImage from './EmptyProjectListImage.png'; import { CREATE_CHEF_PROJECT_TEMPLATE_PATH } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; +import { useSeedTestData } from '../../useSeedTestData'; const useStyles = makeStyles({ top: { @@ -60,6 +61,9 @@ const NextSteps = () => { export const EmptyProjectList = () => { const styles = useStyles(); + // Do not merge + useSeedTestData(); + return (
{ return ( // relative to x2a/ + } /> } /> ); diff --git a/workspaces/x2a/plugins/x2a/src/components/WizardActions.tsx.toBeDeleted b/workspaces/x2a/plugins/x2a/src/components/WizardActions.tsx.toBeDeleted new file mode 100644 index 0000000000..697e1f83bb --- /dev/null +++ b/workspaces/x2a/plugins/x2a/src/components/WizardActions.tsx.toBeDeleted @@ -0,0 +1,59 @@ +/* + * 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 { LinkButton } from '@backstage/core-components'; +import { Button, Grid } from '@material-ui/core'; +import { useTranslation } from '../../hooks/useTranslation'; + +export type WizardActionsProps = { + canNext: boolean; + canBack: boolean; + onCancelLink: string; + onBack: () => void; + onNext: () => void; +}; + +export const WizardActions = ({ + canNext, + canBack, + onCancelLink, + onBack, + onNext, +}: WizardActionsProps) => { + const { t } = useTranslation(); + return ( + + + + {t('wizard.cancel')} + + + + + + + + + ); +}; diff --git a/workspaces/x2a/plugins/x2a/src/components/tools/humanizeDate.ts b/workspaces/x2a/plugins/x2a/src/components/tools/humanizeDate.ts index 854c83f1a7..8dac0fb581 100644 --- a/workspaces/x2a/plugins/x2a/src/components/tools/humanizeDate.ts +++ b/workspaces/x2a/plugins/x2a/src/components/tools/humanizeDate.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -export const humanizeDate = (date: Date): string => { - return date.toLocaleString(undefined, { +export const humanizeDate = (date: Date | string): string => { + return new Date(date).toLocaleString(undefined, { month: 'numeric', day: 'numeric', year: 'numeric', diff --git a/workspaces/x2a/plugins/x2a/src/routes.ts b/workspaces/x2a/plugins/x2a/src/routes.ts index e2f7c7f503..b3cbed16b5 100644 --- a/workspaces/x2a/plugins/x2a/src/routes.ts +++ b/workspaces/x2a/plugins/x2a/src/routes.ts @@ -13,10 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createRouteRef } from '@backstage/core-plugin-api'; +import { createRouteRef, createSubRouteRef } from '@backstage/core-plugin-api'; export const basePath = '/x2a'; export const rootRouteRef = createRouteRef({ id: 'x2a', }); + +export const moduleRouteRef = createSubRouteRef({ + id: 'x2a.module', + parent: rootRouteRef, + path: '/projects/:projectId/modules/:moduleId', +}); diff --git a/workspaces/x2a/plugins/x2a/src/translations/ref.ts b/workspaces/x2a/plugins/x2a/src/translations/ref.ts index 98cbfff109..c8eed04e75 100644 --- a/workspaces/x2a/plugins/x2a/src/translations/ref.ts +++ b/workspaces/x2a/plugins/x2a/src/translations/ref.ts @@ -33,6 +33,31 @@ export const x2aPluginMessages = { 'Initiate and track the asynchronous conversions of Chef files into production-ready Ansible Playbooks.', devTitle: 'Conversion Hub', }, + modulePage: { + title: 'Module Details', + artifacts: { + title: 'Artifacts', + migration_plan: 'Overall project migration plan', + module_migration_plan: 'Module plan by analysis', + migrated_sources: 'Migrated Sources', + description: + 'These artifacts are generated by the conversion process and are available for review.', + }, + phases: { + id: 'ID', + duration: 'Duration', + k8sJobName: 'Kubernetes Job Name', + startedAt: 'Started At', + status: 'Status', + errorDetails: 'Error Details', + reanalyzeInstructions: + 'The module migration plan is already present. In case the overall project migration plan has been updated, retrigger the analysis to reflect the changes.', + rerunAnalyze: 'Recreate the module migration plan', + analyzeInstructions: + 'Before running the analysis, review the overall project migration plan first, its content will drive the analysis of the module.', + runAnalyze: 'Create module migration plan', + }, + }, table: { columns: { name: 'Name', diff --git a/workspaces/x2a/yarn.lock b/workspaces/x2a/yarn.lock index ef3e73c244..3089336d04 100644 --- a/workspaces/x2a/yarn.lock +++ b/workspaces/x2a/yarn.lock @@ -11627,6 +11627,7 @@ __metadata: "@backstage/plugin-catalog": ^1.32.0 "@backstage/test-utils": ^1.7.13 "@backstage/theme": ^0.7.0 + "@backstage/ui": ^0.9.1 "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.61