UNPKG

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

Version:
512 lines (509 loc) 18.4 kB
import { jsxs, jsx } from 'react/jsx-runtime'; import { StatusOK } from '@backstage/core-components'; 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, RepositorySelection } from '../types/types.esm.js'; const gitlabFeatureFlag = false; const descendingComparator = (a, b, orderBy) => { let value1 = get(a, orderBy); let value2 = get(b, orderBy); const order = { [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 (orderBy === "selectedRepositories") { value1 = Object.keys(value1)?.length; value2 = Object.values(value2)?.length; } if (orderBy === "catalogInfoYaml.status") { value1 = order[value1 || RepositoryStatus.NotGenerated]; value2 = order[value2 || RepositoryStatus.NotGenerated]; } 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) => ({ apiVersion: "backstage.io/v1alpha1", kind: "Component", metadata: { name: componentName, annotations: { "github.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) => { const importJobUrl = repositoryUrl ? `${baseUrl}/bulk-import/repositories?repository=${repositoryUrl}&defaultBranch=${defaultBranch}` : `${baseUrl}/bulk-import/repositories`; 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) }; }; 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, showIcon, prUrl, isApprovalToolGitlab = false) => { if (!status) { return ""; } const labelText = "Added"; switch (status) { case "WAIT_PR_APPROVAL": return showIcon ? /* @__PURE__ */ jsx( WaitingForPR, { url: prUrl, isApprovalToolGitlab } ) : "Waiting for Approval"; case "ADDED": return showIcon ? /* @__PURE__ */ jsxs( Typography, { component: "span", style: { display: "flex", alignItems: "baseline" }, children: [ /* @__PURE__ */ jsx(StatusOK, {}), "Added" ] } ) : labelText; default: 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) => { acc.push({ approvalTool: approvalTool.toLocaleUpperCase(), 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 ), github: { 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, options) => { if (options?.fetchOrganizations) { return `${backendUrl}/api/bulk-import/organizations?pagePerIntegration=${page}&sizePerIntegration=${size}&search=${searchString}`; } if (options?.orgName) { return `${backendUrl}/api/bulk-import/organizations/${options.orgName}/repositories?pagePerIntegration=${page}&sizePerIntegration=${size}&search=${searchString}`; } return `${backendUrl}/api/bulk-import/repositories?pagePerIntegration=${page}&sizePerIntegration=${size}&search=${searchString}`; }; const getCustomisedErrorMessage = (status) => { let message = ""; let showRepositoryLink = false; status?.forEach((s) => { if (s === RepositoryStatus.PR_ERROR) { message = message.concat( "Couldn't create a new PR due to insufficient permissions. Contact your administrator.", "\n" ); showRepositoryLink = true; } if (s === RepositoryStatus.CATALOG_INFO_FILE_EXISTS_IN_REPO) { message = message.concat( "Since catalog-info.yaml already exists in the repository, no new PR will be created. However, the entity will still be registered in the catalog page.", "\n" ); } if (s === RepositoryStatus.CATALOG_ENTITY_CONFLICT) { message = message.concat( "Couldn't create a new PR because of catalog entity conflict.", "\n" ); } if (s === RepositoryStatus.REPO_EMPTY) { message = message.concat( "Couldn't create a new PR because the repository is empty. Push an initial commit to the repository.", "\n" ); } if (s === RepositoryStatus.CODEOWNERS_FILE_NOT_FOUND_IN_REPO) { message = message.concat( "CODEOWNERS file is missing from the repository. Add a CODEOWNERS file to create a new PR.", "\n" ); } }); if (!message) { message = status?.join("\n") || ""; } return { message, showRepositoryLink }; }; const calculateLastUpdated = (dateString) => { 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 `${diffInDays} ${diffInDays > 1 ? "days" : "day"} ago`; } if (diffInHours > 0) { return `${diffInHours} ${diffInHours > 1 ? "hours" : "hour"} ago`; } if (diffInMinutes > 0) { return `${diffInMinutes} ${diffInMinutes > 1 ? "minutes" : "minute"} ago`; } return `${diffInSeconds} ${diffInSeconds > 1 ? "seconds" : "second"} ago`; }; const evaluatePRTemplate = (repositoryStatus) => { try { const entity = jsyaml.loadAll( repositoryStatus.github.pullRequest.catalogInfoContent )[0]; const isInvalid = !entity?.metadata?.name || !entity?.apiVersion || !entity?.kind; return { pullReqPreview: { pullRequestUrl: repositoryStatus.github.pullRequest.url, prTitle: repositoryStatus.github.pullRequest.title, prDescription: repositoryStatus.github.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.github.pullRequest.url, prTitle: repositoryStatus.github.pullRequest.title, prDescription: repositoryStatus.github.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: `https://github.com/${val?.name}`, totalReposInOrg: val.totalRepoCount } }; }, {} ) || {}; return { organizations: orgData, totalOrganizations: result?.totalCount }; }; const prepareDataForRepositories = (result, user, baseUrl) => { const repoData = result?.repositories?.reduce((acc, val) => { const id = val.id; return { ...acc, [id]: { id, repoName: val.name, 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" ) } } }; }, {}) || {}; 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}`; return { ...acc, [id]: { id, source: val.source, 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" ), pullRequest: val?.github?.pullRequest?.url || "", lastUpdated: val.lastUpdate } } }; }, {}); return { repoData, totalJobs: addedRepositories?.totalCount || 0 }; }; const validateKeyValuePair = yup.string().nullable().test( "is-key-value-pair", "Each entry must have a key and a value separated by a colon.", (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) => yup.object().shape({ prTitle: yup.string().required(`${approvalTool} title is required`), prDescription: yup.string().required(`${approvalTool} description is required`), componentName: yup.string().matches( componentNameRegex, `"${yup.string()}" is not valid; expected a string that is sequences of [a-zA-Z0-9] separated by any of [-_.], at most 63 characters in total. To learn more about catalog file format, visit: https://github.com/backstage/backstage/blob/master/docs/architecture-decisions/adr002-default-catalog-file-format.md` ).required("Component name is required"), useCodeOwnersFile: yup.boolean(), entityOwner: yup.string().when("useCodeOwnersFile", { is: false, then: (schema) => schema.required("Entity Owner is required"), otherwise: (schema) => schema.notRequired() }), prLabels: validateKeyValuePair, prAnnotations: validateKeyValuePair, prSpec: validateKeyValuePair }); export { areAllRowsSelected, calculateLastUpdated, cleanComponentName, componentNameRegex, convertKeyValuePairsToString, defaultCatalogInfoYaml, descendingComparator, evaluatePRTemplate, evaluateRowForOrg, evaluateRowForRepo, filterSelectedForActiveDrawer, filterSelectedRepositoriesOnActivePage, getApi, getComparator, getCustomisedErrorMessage, getImportStatus, getJobErrors, getPRTemplate, getValidationSchema, getYamlKeyValuePairs, gitlabFeatureFlag, prepareDataForAddedRepositories, prepareDataForOrganizations, prepareDataForRepositories, prepareDataForSubmission, updateWithNewSelectedRepositories, urlHelper }; //# sourceMappingURL=repository-utils.esm.js.map