UNPKG

@red-hat-developer-hub/backstage-plugin-bulk-import

Version:
612 lines (609 loc) 21.1 kB
import { jsx, jsxs } from 'react/jsx-runtime'; import { Link, StatusOK } from '@backstage/core-components'; import { useApi, configApiRef } from '@backstage/core-plugin-api'; import Typography from '@mui/material/Typography'; import * as jsyaml from 'js-yaml'; import { get } from 'lodash'; import * as yaml from 'yaml'; import * as yup from 'yup'; import { WaitingForPR } from '../components/WaitingForPR.esm.js'; import { RepositoryStatus, ApprovalTool, RepositorySelection } from '../types/types.esm.js'; import { isGithubJob } from '../types/response-types.esm.js'; import { getWorkflowStatusInfo } from './orchestratorStatus.esm.js'; import { getTaskStatusInfo } from './task-status.esm.js'; const TaskLink = ({ taskId, t }) => { const configApi = useApi(configApiRef); const appBaseUrl = configApi.getString("app.baseUrl"); if (!taskId) return null; return /* @__PURE__ */ jsx( Link, { to: `${appBaseUrl}/create/tasks/${taskId}`, "data-testid": "view-task-link", style: { paddingLeft: "5px", display: "inline-flex", alignItems: "center" }, children: t("tasks.viewTask") } ); }; const WorkflowLink = ({ workflowId, t }) => { const configApi = useApi(configApiRef); const appBaseUrl = configApi.getString("app.baseUrl"); if (!workflowId) return null; return /* @__PURE__ */ jsx( Link, { to: `${appBaseUrl}/orchestrator/instances/${workflowId}`, "data-testid": "view-workflow-link", style: { paddingLeft: "5px", display: "inline-flex", alignItems: "center" }, children: t("workflows.viewWorkflow") } ); }; const descendingComparator = (a, b, orderBy) => { let value1 = get(a, orderBy); let value2 = get(b, orderBy); ({ [RepositoryStatus.ADDED]: 1, [RepositoryStatus.Ready]: 2, [RepositoryStatus.WAIT_PR_APPROVAL]: 3, [RepositoryStatus.PR_ERROR]: 4, [RepositoryStatus.CATALOG_ENTITY_CONFLICT]: 4, [RepositoryStatus.CATALOG_INFO_FILE_EXISTS_IN_REPO]: 4, [RepositoryStatus.CODEOWNERS_FILE_NOT_FOUND_IN_REPO]: 4, [RepositoryStatus.REPO_EMPTY]: 4, [RepositoryStatus.NotGenerated]: 5 }); if (typeof value1 === "string" && typeof value2 === "string") { value1 = value1.toLowerCase(); value2 = value2.toLowerCase(); } if (value2 < value1) { return -1; } if (value2 > value1) { return 1; } return 0; }; const getComparator = (order, orderBy) => { return order === "desc" ? (a, b) => descendingComparator(a, b, orderBy) : (a, b) => -descendingComparator(a, b, orderBy); }; const defaultCatalogInfoYaml = (componentName, repoName, orgName, owner, gitProvider) => { return { apiVersion: "backstage.io/v1alpha1", kind: "Component", metadata: { name: componentName, annotations: { [`${gitProvider}.com/project-slug`]: `${orgName}/${repoName}` } }, spec: { type: "other", lifecycle: "unknown", owner } }; }; const componentNameRegex = /^([a-zA-Z0-9]+[-_.])*[a-zA-Z0-9]+$|^[a-zA-Z0-9]{1,63}$/; const cleanComponentName = (input) => { const cleanedStr = input.replace(/^[$._-]+|[$._-]+$/g, ""); if (componentNameRegex.test(input)) { return input; } if (componentNameRegex.test(cleanedStr)) { return cleanedStr; } return "my-component"; }; const getPRTemplate = (componentName, orgName, entityOwner, baseUrl, repositoryUrl, defaultBranch, gitProvider) => { const importJobUrl = repositoryUrl ? `${baseUrl}/bulk-import?repository=${repositoryUrl}&defaultBranch=${defaultBranch}` : `${baseUrl}/bulk-import`; const name = cleanComponentName(componentName); return { componentName: name, entityOwner, prTitle: "Add catalog-info.yaml config file", prDescription: `This pull request adds a **Backstage entity metadata file** to this repository so that the component can be added to the [software catalog](${baseUrl}/catalog). After this pull request is merged, the component will become available. For more information, read an [overview of the Backstage software catalog](https://backstage.io/docs/features/software-catalog/). View the import job in your app [here](${importJobUrl}).`, useCodeOwnersFile: false, yaml: defaultCatalogInfoYaml( name, componentName, orgName, entityOwner, gitProvider ) }; }; const getYamlKeyValuePairs = (prYamlInput) => { const keyValuePairs = {}; const keyValueEntries = prYamlInput.split(";").map((entry) => entry.trim()); keyValueEntries.forEach((entry) => { const [key, ...valueParts] = entry.split(":"); const value = valueParts.join(":").trim(); if (key && value) { keyValuePairs[key.trim()] = value.replace(/(^['"])|(['"]$)/g, ""); } }); return keyValuePairs; }; const updateWithNewSelectedRepositories = (existingSelectedRepositories, selectedRepos) => { return Object.keys(selectedRepos).length === 0 ? {} : Object.keys(selectedRepos).reduce((acc, sr) => { const existingRepo = existingSelectedRepositories[sr]; if (existingRepo) { return { ...acc, ...{ [existingRepo.id]: existingRepo } }; } return { ...acc, ...{ [sr]: { ...selectedRepos[sr], catalogInfoYaml: { ...selectedRepos[sr].catalogInfoYaml, status: RepositoryStatus.Ready } } } }; }, {}); }; const filterSelectedForActiveDrawer = (repositories, selectedRepos) => { return Object.keys(selectedRepos).reduce( (acc, repoId) => repositories?.map((r) => r.id).includes(repoId) ? { ...acc, repoId: selectedRepos[repoId] } : acc, {} ); }; const filterSelectedRepositoriesOnActivePage = (activePageTableData, selectedRepositories) => { return Object.keys(selectedRepositories).filter((repoId) => { return activePageTableData.some((activeRepo) => activeRepo.id === repoId); }); }; const urlHelper = (url) => { if (!url || url === "") { return "-"; } return url.split("https://")[1] || url; }; const getImportStatus = (status, t, showIcon, prUrl, taskOrWorkflowId) => { if (!status) { return ""; } const labelText = t("status.imported"); if (status === "WAIT_PR_APPROVAL") { return showIcon ? /* @__PURE__ */ jsx(WaitingForPR, { url: prUrl }) : t("status.waitingForApproval"); } if (status === "ADDED") { return showIcon ? /* @__PURE__ */ jsxs( Typography, { component: "span", style: { display: "flex", alignItems: "baseline" }, children: [ /* @__PURE__ */ jsx(StatusOK, {}), t("status.imported") ] } ) : labelText; } if (taskOrWorkflowId && status.startsWith("TASK")) { const { taskLabelText, taskIcon } = getTaskStatusInfo(status, t); return showIcon ? /* @__PURE__ */ jsxs( Typography, { component: "span", style: { display: "flex", alignItems: "baseline" }, children: [ taskIcon, taskLabelText, /* @__PURE__ */ jsx(TaskLink, { taskId: taskOrWorkflowId, t }) ] } ) : /* @__PURE__ */ jsxs( Typography, { component: "span", style: { display: "flex", alignItems: "baseline" }, children: [ taskLabelText, /* @__PURE__ */ jsx(TaskLink, { taskId: taskOrWorkflowId, t }) ] } ); } if (taskOrWorkflowId && status.startsWith("WORKFLOW")) { const { workflowLabelText, workflowIcon } = getWorkflowStatusInfo( status, t ); return showIcon ? /* @__PURE__ */ jsxs( Typography, { component: "span", style: { display: "flex", alignItems: "baseline" }, children: [ workflowIcon, workflowLabelText, /* @__PURE__ */ jsx(WorkflowLink, { workflowId: taskOrWorkflowId, t }) ] } ) : /* @__PURE__ */ jsxs( Typography, { component: "span", style: { display: "flex", alignItems: "baseline" }, children: [ workflowLabelText, /* @__PURE__ */ jsx(WorkflowLink, { workflowId: taskOrWorkflowId, t }) ] } ); } return ""; }; const evaluateRowForRepo = (tableData, selectedRepositories) => { return tableData.map((td) => { const repo = selectedRepositories[td.id]; if (repo) { const newtd = { ...td, catalogInfoYaml: repo.catalogInfoYaml }; return newtd; } return td; }); }; const evaluateRowForOrg = (tableData, selectedRepositories) => { return tableData?.map((td) => { const selectedReposFromOrg = Object.values(selectedRepositories)?.reduce( (acc, repo) => repo.orgName === td.orgName ? { ...acc, [repo.id]: repo } : acc, {} ) || []; const orgRowData = { ...td, selectedRepositories: selectedReposFromOrg, ...Object.keys(selectedReposFromOrg)?.length > 0 ? { catalogInfoYaml: { status: RepositoryStatus.Ready } } : {} }; return orgRowData; }); }; const areAllRowsSelected = (repositoryType, alreadyAdded, isItemSelected, orgRepositoriesCount, selectedRepositories) => { return repositoryType === RepositorySelection.Organization ? (Object.keys(selectedRepositories)?.length || 0) + alreadyAdded === orgRepositoriesCount : !!isItemSelected; }; const getJobErrors = (createJobResponse) => { return createJobResponse?.reduce( (acc, res) => { if (res.errors?.length > 0) { const errs = res.status === RepositoryStatus.PR_ERROR ? [res.status] : res.errors.filter( (e) => e !== RepositoryStatus.CATALOG_INFO_FILE_EXISTS_IN_REPO ); const hasInfo = res.errors.includes( RepositoryStatus.CATALOG_INFO_FILE_EXISTS_IN_REPO ); const repoId = `${res.repository.organization}/${res.repository.name}`; const repoErr = { [`${repoId}`]: { repository: { name: res.repository.name || "", organization: res.repository.organization || "" }, catalogEntityName: res.catalogEntityName || "", error: { message: errs } } }; const repoInfo = { [`${repoId}`]: { ...repoErr[`${repoId}`], error: { message: [RepositoryStatus.CATALOG_INFO_FILE_EXISTS_IN_REPO] } } }; return { ...acc, ...hasInfo ? { infos: { ...acc.infos, ...repoInfo } } : {}, ...errs.length > 0 ? { errors: { ...acc.errors, ...repoErr } } : {} }; } return acc; }, { infos: null, errors: null } ); }; const convertKeyValuePairsToString = (keyValuePairs) => { return keyValuePairs ? Object.entries(keyValuePairs).filter((val) => val).map(([key, value]) => `${key.trim()}: ${value?.trim() || ""}`).join("; ") : ""; }; const prepareDataForSubmission = (repositories, approvalTool) => Object.values(repositories).reduce( (acc, repo) => { const gitProvider = approvalTool === ApprovalTool.Gitlab ? "gitlab" : "github"; acc.push({ approvalTool, codeOwnersFileAsEntityOwner: repo.catalogInfoYaml?.prTemplate?.useCodeOwnersFile || false, catalogEntityName: repo.catalogInfoYaml?.prTemplate?.componentName || repo?.repoName || "my-component", repository: { id: repo.id, url: repo.repoUrl || "", name: repo.repoName || "", organization: repo.orgName || "", defaultBranch: repo.defaultBranch || "" }, catalogInfoContent: yaml.stringify( repo.catalogInfoYaml?.prTemplate?.yaml, null, 2 ), [gitProvider]: { pullRequest: { title: repo.catalogInfoYaml?.prTemplate?.prTitle || "Add catalog-info.yaml config file", body: repo.catalogInfoYaml?.prTemplate?.prDescription || "" } } }); return acc; }, [] ); const getApi = (backendUrl, page, size, searchString, approvalTool, options) => { if (options?.fetchOrganizations) { return `${backendUrl}/api/bulk-import/organizations?pagePerIntegration=${page}&sizePerIntegration=${size}&search=${searchString}&approvalTool=${approvalTool}`; } if (options?.orgName) { return `${backendUrl}/api/bulk-import/organizations/${options.orgName}/repositories?pagePerIntegration=${page}&sizePerIntegration=${size}&search=${searchString}&approvalTool=${approvalTool}`; } return `${backendUrl}/api/bulk-import/repositories?pagePerIntegration=${page}&sizePerIntegration=${size}&search=${searchString}&approvalTool=${approvalTool}`; }; const getCustomisedErrorMessage = (status, t) => { let message = ""; let showRepositoryLink = false; status?.forEach((s) => { if (s === RepositoryStatus.PR_ERROR) { message = message.concat(t("errors.prErrorPermissions"), "\n"); showRepositoryLink = true; } if (s === RepositoryStatus.CATALOG_INFO_FILE_EXISTS_IN_REPO) { message = message.concat(t("errors.catalogInfoExists"), "\n"); } if (s === RepositoryStatus.CATALOG_ENTITY_CONFLICT) { message = message.concat(t("errors.catalogEntityConflict"), "\n"); } if (s === RepositoryStatus.REPO_EMPTY) { message = message.concat(t("errors.repoEmpty"), "\n"); } if (s === RepositoryStatus.CODEOWNERS_FILE_NOT_FOUND_IN_REPO) { message = message.concat(t("errors.codeOwnersNotFound"), "\n"); } }); if (!message) { message = status?.join("\n") || ""; } return { message, showRepositoryLink }; }; const calculateLastUpdated = (dateString, t) => { if (!dateString) { return ""; } const givenDate = new Date(dateString); const currentDate = /* @__PURE__ */ new Date(); const diffInMilliseconds = currentDate.getTime() - givenDate.getTime(); const diffInSeconds = Math.floor(diffInMilliseconds / 1e3); const diffInMinutes = Math.floor(diffInSeconds / 60); const diffInHours = Math.floor(diffInMinutes / 60); const diffInDays = Math.floor(diffInHours / 24); if (diffInDays > 0) { return t("time.daysAgo", { count: diffInDays }); } if (diffInHours > 0) { return t("time.hoursAgo", { count: diffInHours }); } if (diffInMinutes > 0) { return t("time.minutesAgo", { count: diffInMinutes }); } return t("time.secondsAgo", { count: diffInSeconds }); }; const evaluatePRTemplate = (repositoryStatus) => { const gitProvider = isGithubJob(repositoryStatus) ? "github" : "gitlab"; try { const entity = jsyaml.loadAll( repositoryStatus[gitProvider]?.pullRequest.catalogInfoContent ?? "" )[0]; const isInvalid = !entity?.metadata?.name || !entity?.apiVersion || !entity?.kind; return { pullReqPreview: { pullRequestUrl: repositoryStatus[gitProvider]?.pullRequest.url, prTitle: repositoryStatus[gitProvider]?.pullRequest.title, prDescription: repositoryStatus[gitProvider]?.pullRequest.body, prAnnotations: convertKeyValuePairsToString( entity?.metadata?.annotations ), prLabels: convertKeyValuePairsToString(entity?.metadata?.labels), prSpec: convertKeyValuePairsToString( entity?.spec ), componentName: entity?.metadata?.name, entityOwner: entity?.spec?.owner, useCodeOwnersFile: !entity?.spec?.owner, yaml: entity }, isInvalidEntity: isInvalid }; } catch (e) { return { pullReqPreview: { pullRequestUrl: repositoryStatus[gitProvider]?.pullRequest.url, prTitle: repositoryStatus[gitProvider]?.pullRequest.title, prDescription: repositoryStatus[gitProvider]?.pullRequest.body, prAnnotations: undefined, prLabels: undefined, prSpec: undefined, componentName: undefined, entityOwner: undefined, useCodeOwnersFile: false, yaml: {} }, isInvalidEntity: true }; } }; const prepareDataForOrganizations = (result) => { const orgData = result?.organizations?.reduce( (acc, val) => { return { ...acc, [val.id]: { id: val.id, orgName: val.name, organizationUrl: `${val?.url}`, totalReposInOrg: val.totalRepoCount, approvalTool: result.approvalTool } }; }, {} ) || {}; return { organizations: orgData, totalOrganizations: result?.totalCount }; }; const prepareDataForRepositories = (result, user, baseUrl) => { const gitProvider = result?.approvalTool === ApprovalTool.Gitlab ? "gitlab" : "github"; const repoData = result?.repositories?.reduce((acc, val) => { const id = val.id; return { ...acc, [id]: { id, repoName: val.name, approvalTool: result?.approvalTool ?? ApprovalTool.Git, defaultBranch: val.defaultBranch || "main", orgName: val.organization, repoUrl: val.url, organizationUrl: val.url?.substring( 0, val.url.indexOf(val?.name || "") - 1 ), catalogInfoYaml: { prTemplate: getPRTemplate( val.name || "", val.organization || "", user, baseUrl || "", val.url || "", val.defaultBranch || "main", gitProvider ) } } }; }, {}) || {}; return { repositories: repoData, totalRepositories: result?.totalCount }; }; const prepareDataForAddedRepositories = (addedRepositories, user, baseUrl) => { if (!Array.isArray(addedRepositories?.imports)) { return { repoData: {}, totalJobs: 0 }; } const importJobs = addedRepositories; const repoData = importJobs.imports?.reduce((acc, val) => { const id = `${val.repository.organization}/${val.repository.name}`; const gitProvider = isGithubJob(val) ? "github" : "gitlab"; const result = { ...acc, [id]: { id, source: val.source, approvalTool: val.approvalTool, repoName: val.repository.name, defaultBranch: val.repository.defaultBranch, orgName: val.repository.organization, repoUrl: val.repository.url, organizationUrl: val?.repository?.url?.substring( 0, val.repository.url.indexOf(val?.repository?.name || "") - 1 ), catalogInfoYaml: { status: val.status ? RepositoryStatus[val.status] : RepositoryStatus.NotGenerated, prTemplate: getPRTemplate( val.repository.name || "", val.repository.organization || "", user, baseUrl, val.repository.url || "", val.repository.defaultBranch || "main", gitProvider ), pullRequest: val[gitProvider]?.pullRequest?.url || "", lastUpdated: val.lastUpdate } } }; if (val.task) { result[id].task = { id: val.task.taskId, status: val.status }; } if (val.workflow) { result[id].workflow = { id: val.workflow.workflowId, status: val.status }; } return result; }, {}); return { repoData, totalJobs: addedRepositories?.totalCount || 0 }; }; const validateKeyValuePair = (t) => yup.string().nullable().test("is-key-value-pair", t("validation.keyValuePairFormat"), (value) => { if (!value) return true; const keyValuePairs = value.split(";").map((pair) => pair.trim()); for (const pair of keyValuePairs) { if (pair) { const [key, val] = pair.split(":").map((part) => part.trim()); if (!key || !val) { return false; } } } return true; }); const getValidationSchema = (approvalTool, t) => yup.object().shape({ prTitle: yup.string().required(t("validation.titleRequired", { approvalTool })), prDescription: yup.string().required(t("validation.descriptionRequired", { approvalTool })), componentName: yup.string().matches( componentNameRegex, t("validation.componentNameInvalid", { value: yup.string() }) ).required(t("validation.componentNameRequired")), useCodeOwnersFile: yup.boolean(), entityOwner: yup.string().when("useCodeOwnersFile", { is: false, then: (schema) => schema.required(t("validation.entityOwnerRequired")), otherwise: (schema) => schema.notRequired() }), prLabels: validateKeyValuePair(t), prAnnotations: validateKeyValuePair(t), prSpec: validateKeyValuePair(t) }); export { TaskLink, WorkflowLink, areAllRowsSelected, calculateLastUpdated, cleanComponentName, componentNameRegex, convertKeyValuePairsToString, defaultCatalogInfoYaml, descendingComparator, evaluatePRTemplate, evaluateRowForOrg, evaluateRowForRepo, filterSelectedForActiveDrawer, filterSelectedRepositoriesOnActivePage, getApi, getComparator, getCustomisedErrorMessage, getImportStatus, getJobErrors, getPRTemplate, getValidationSchema, getYamlKeyValuePairs, prepareDataForAddedRepositories, prepareDataForOrganizations, prepareDataForRepositories, prepareDataForSubmission, updateWithNewSelectedRepositories, urlHelper }; //# sourceMappingURL=repository-utils.esm.js.map